commit 9005122a18642d2407c19b9dc44ba07e215bb504 Author: WeebDataHoarder <57538841+WeebDataHoarder@users.noreply.github.com> Date: Sun Jan 16 13:49:45 2022 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..757fee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea \ No newline at end of file diff --git a/FinalCommander.go b/FinalCommander.go new file mode 100644 index 0000000..9e70a9a --- /dev/null +++ b/FinalCommander.go @@ -0,0 +1,566 @@ +package main + +import ( + "bytes" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base32" + "encoding/binary" + "encoding/hex" + "encoding/pem" + "flag" + "fmt" + "github.com/ipfs/go-cid" + "github.com/mroth/weightedrand" + "github.com/multiformats/go-multihash" + "log" + "math/big" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" +) + +var base32Encoding = base32.NewEncoding("qpzry9x8gf2tvdw0s3jn54khce6mua7l").WithPadding(base32.NoPadding) + +var sniAddress string + +var privateKey ed25519.PrivateKey +var messageCacheLimit = 4096 +var signedMessageCacheLimit int +var messageCacheMutex sync.RWMutex +var messageCache = make(map[string]*ContentMessage) + +var contentServers []*ContentServer + +type ContentServer struct { + Index int + Address string + Weight uint + LastCheckResult bool + LastCheck time.Time + LastCheckMutex sync.RWMutex +} + +func (s *ContentServer) getURL(args ...string) string { + return fmt.Sprintf("https://%s/%s", s.Address, strings.Join(args, "/")) +} + +func (s *ContentServer) getCheckResult() bool { + s.LastCheckMutex.RLock() + defer s.LastCheckMutex.RUnlock() + return s.LastCheckResult +} + +func (s *ContentServer) setCheckResult(result bool) { + s.LastCheckMutex.Lock() + defer s.LastCheckMutex.Unlock() + s.LastCheckResult = result +} + +func (s *ContentServer) check() { + customTransport := http.DefaultTransport.(*http.Transport).Clone() + customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + client := &http.Client{ + Transport: customTransport, + Timeout: 5 * time.Second, + } + response, err := client.Head(s.getURL()) + + if err != nil { + s.setCheckResult(false) + return + } + + defer response.Body.Close() + + if response.StatusCode != http.StatusBadRequest { + s.setCheckResult(false) + return + } + s.setCheckResult(true) +} + +func selectNextContentServer(skip []int) *ContentServer { + inSkip := func(i int) bool { + for _, c := range skip { + if i == c { + return true + } + } + return false + } + + var choosingList []weightedrand.Choice + for _, c := range contentServers { + if !inSkip(c.Index) && c.getCheckResult() { + choosingList = append(choosingList, weightedrand.NewChoice(c, c.Weight)) + } + } + + chooser, err := weightedrand.NewChooser(choosingList...) + + if err != nil { + return nil + } + + return chooser.Pick().(*ContentServer) +} + +//TODO: move this to a library +type ContentMessage struct { + Version uint64 + PublicKey ed25519.PublicKey + IssueTime int64 + Identifier cid.Cid + VerificationResult *bool + Signature []byte +} + +func (s *ContentMessage) sign(privateKey ed25519.PrivateKey) { + s.PublicKey = make([]byte, ed25519.PublicKeySize) + copy(s.PublicKey, privateKey[32:]) + s.Signature = ed25519.Sign(privateKey, s.encodeMessage()) + s.VerificationResult = nil +} + +func (s *ContentMessage) verify() bool { + currentTime := time.Now() + + notBefore := currentTime.Add(-time.Hour) //Only one hour before time + notAfter := currentTime.Add(time.Hour * 24) //Only 24 hours after time + issueTime := time.Unix(s.IssueTime, 0) + + if issueTime.Before(notBefore) { + return false + } + + if issueTime.After(notAfter) { + return false + } + + messageCacheMutex.RLock() + k := string(s.encode()) + cachedMessage, ok := messageCache[k] + messageCacheMutex.RUnlock() + if ok { + return *cachedMessage.VerificationResult + } + + messageCacheMutex.Lock() + defer messageCacheMutex.Unlock() + + if s.VerificationResult == nil { + makeBool := func(v bool) *bool { return &v } + s.VerificationResult = makeBool(ed25519.Verify(s.PublicKey, s.encodeMessage(), s.Signature)) + } + + if len(messageCache) >= messageCacheLimit { + //Find oldest value, remove it + var item *ContentMessage + for _, e := range messageCache { + if item == nil || e.IssueTime < item.IssueTime { + item = e + } + } + + if item != nil { + delete(messageCache, string(item.encode())) + } + } + + messageCache[k] = s + + return *s.VerificationResult +} + +func (s *ContentMessage) encodeMessage() []byte { + message := &bytes.Buffer{} + + buf := make([]byte, binary.MaxVarintLen64) + + n := binary.PutUvarint(buf, s.Version) //signature version + _, _ = message.Write(buf[:n]) + + if s.Version == 0 { + _, _ = message.Write(s.PublicKey) + n = binary.PutVarint(buf, s.IssueTime) //time + _, _ = message.Write(buf[:n]) + + _, _ = s.Identifier.WriteBytes(message) + + return message.Bytes() + } + + return nil +} + +func (s *ContentMessage) encode() []byte { + message := s.encodeMessage() + + if message == nil || len(s.Signature) != ed25519.SignatureSize { + return nil + } + + return append(message, s.Signature...) +} + +func (s ContentMessage) String() string { + return fmt.Sprintf("%d %x %d %s %x", s.Version, s.PublicKey, s.IssueTime, s.Identifier.String(), s.Signature) +} + +func DecodeContentMessage(signatureBytes []byte) *ContentMessage { + message := ContentMessage{} + + buffer := bytes.NewBuffer(signatureBytes) + + var err error + + message.Version, err = binary.ReadUvarint(buffer) + if err != nil { + log.Print(err) + return nil + } + + if message.Version == 0 { + message.PublicKey = make([]byte, ed25519.PublicKeySize) + _, err := buffer.Read(message.PublicKey) + if err != nil { + log.Print(message.String()) + log.Print(err) + return nil + } + message.IssueTime, err = binary.ReadVarint(buffer) + if err != nil { + log.Print(message.String()) + log.Print(err) + return nil + } + + _, message.Identifier, err = cid.CidFromReader(buffer) + if err != nil { + log.Print(message.String()) + log.Print(err) + return nil + } + + message.Signature = make([]byte, ed25519.SignatureSize) + _, err = buffer.Read(message.Signature) + if err != nil { + log.Print(message.String()) + log.Print(err) + return nil + } + + if buffer.Len() != 0 { //Unknown extra data + log.Print(message.String()) + log.Printf("%x", buffer.Bytes()) + return nil + } + + return &message + + } + + return nil +} + +func createSelfSignedCertificate() ([]byte, []byte) { + x509Template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{}, + NotBefore: time.Unix(0, 0).UTC(), + NotAfter: time.Date(time.Now().UTC().Year()+10, 0, 0, 0, 0, 0, 0, time.UTC), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 1, + } + + privateBogusKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + log.Fatal(err) + } + + certBytes, err := x509.CreateCertificate(rand.Reader, &x509Template, &x509Template, privateBogusKey.Public(), privateBogusKey) + if err != nil { + log.Fatal(err) + } + + keyBytes, err := x509.MarshalPKCS8PrivateKey(privateBogusKey) + if err != nil { + log.Fatal(err) + } + + return pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }), + pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + }) +} + +func isSNIAllowed(sni string) bool { + return len(sniAddress) == 0 || sni == sniAddress +} + +func setOtherHeaders(w http.ResponseWriter) { + w.Header().Set("Server", "FinalCommander") + w.Header().Set("Vary", "Content-Encoding") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Robots-Tags", "noindex, nofollow, notranslate") + w.Header().Set("Referrer-Policy", "origin") +} +func setCORSHeaders(w http.ResponseWriter) { + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Max-Age", "7200") //Firefox caps this to 86400, Chrome to 7200. Default is 5 seconds (!!!) + w.Header().Set("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "DNT,ETag,Origin,Accept,Accept-Language,X-Requested-With,Range") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Expose-Headers", "*") + + //CORP, COEP, COOP + w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") + w.Header().Set("Cross-Origin-Resource-Policy", "cross-origin") + w.Header().Set("Cross-Origin-Opener-Policy", "unsafe-none") +} + +func encodeIntegerList(data []int) []byte { + message := &bytes.Buffer{} + buf := make([]byte, binary.MaxVarintLen64) + + for _, i := range data { + n := binary.PutVarint(buf, int64(i)) + _, _ = message.Write(buf[:n]) + } + + return message.Bytes() +} +func decodeIntegerList(data []byte) []int { + buf := bytes.NewBuffer(data) + var result []int + + for { + if buf.Len() <= 0 { + break + } + + i, err := binary.ReadVarint(buf) + if err != nil { + //TODO: maybe should error + break + } + result = append(result, int(i)) + } + + return result +} + +func handle(w http.ResponseWriter, r *http.Request) { + if len(r.Host) > 0 && r.Host == r.TLS.ServerName { //Prevents rebinding / DNS stuff + w.WriteHeader(http.StatusNotFound) + return + } + + if r.Method == "GET" || r.Method == "HEAD" { + log.Printf("Serve %s", r.URL.Path) + setOtherHeaders(w) + setCORSHeaders(w) + pathElements := strings.Split(r.URL.Path, "/") + if len(pathElements) < 3 { + log.Printf("1") + w.WriteHeader(http.StatusBadRequest) + return + } + + hashType := strings.ToLower(pathElements[1]) + + hash, err := hex.DecodeString(pathElements[2]) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + var skip []int + + if len(pathElements) > 3 { + data, err := base32Encoding.DecodeString(pathElements[3]) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + skip = decodeIntegerList(data) + } + + var identifier cid.Cid + + if hashType == "sha256" && len(hash) == 32 { + mh, _ := multihash.Encode(hash, multihash.SHA2_256) + identifier = cid.NewCidV1(cid.Raw, mh) + } else if hashType == "md5" && len(hash) == 16 { + mh, _ := multihash.Encode(hash, multihash.MD5) + identifier = cid.NewCidV1(cid.Raw, mh) + } else { + w.WriteHeader(http.StatusNotFound) + return + } + + contentServer := selectNextContentServer(skip) + if contentServer == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + //TODO: signing cache + message := &ContentMessage{ + Version: 0, + IssueTime: time.Now().UTC().Unix(), + Identifier: identifier, + } + message.sign(privateKey) + + skip = append(skip, contentServer.Index) + + http.Redirect(w, r, contentServer.getURL(base32Encoding.EncodeToString(message.encode()), base32Encoding.EncodeToString(encodeIntegerList(skip))), http.StatusFound) + } else if r.Method == "OPTIONS" { + setOtherHeaders(w) + setCORSHeaders(w) + w.WriteHeader(http.StatusNoContent) + } else { + w.WriteHeader(http.StatusNotImplemented) + } +} + +func checkContentServers() { + checkTime := time.Now().Add(-15 * time.Minute) + for _, c := range contentServers { + if c.LastCheck.Before(checkTime) { + c.check() + } + } +} + +func main() { + //TODO: OCSP + certificatePath := flag.String("certificate", "ssl.crt", "Path to SSL certificate file.") + keypairPath := flag.String("keypair", "ssl.key", "Path to SSL key file.") + + listenAddress := flag.String("listen", ":7777", "Address/port to lisent on.") + + weightedServerList := flag.String("servers", "", "Weighted list of servers to use. All use HTTPs. Format address:PORT/WEIGHT,[...]") + + sniAddressOption := flag.String("sni", "", "Define SNI address if desired. Empty will serve any requests regardless.") + + signatureCacheLimitOption := flag.Int("siglimit", 128, "Maximum number of lingering valid signature produced cached.") + + flag.Parse() + + signedMessageCacheLimit = *signatureCacheLimitOption + + var err error + + privateKeyEnv := os.Getenv("PRIVATE_KEY") + + if privateKeyEnv == "" { + log.Print("No PRIVATE_KEY environment variable specified, generating new identity") + publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader) + + log.Printf("Public Ed25519 key (share this): %s", base32Encoding.EncodeToString(publicKey)) + log.Printf("Private Ed25519 key (keep safe!): %s", base32Encoding.EncodeToString(privateKey)) + return + } + + privateKey, err = base32Encoding.DecodeString(privateKeyEnv) + if err != nil { + log.Fatal(err) + } + publicKey := make([]byte, ed25519.PublicKeySize) + copy(publicKey, privateKey[32:]) + log.Printf("Loaded Private Ed25519 key, Public %s", base32Encoding.EncodeToString(publicKey)) + + if len(privateKey) != ed25519.PrivateKeySize { + log.Fatal("Wrong Private key length") + } + + for i, s := range strings.Split(*weightedServerList, ",") { + p := strings.Split(s, "/") + if len(p) != 2 { + log.Fatalf("Invalid weighted server %s", s) + } + + weight, err := strconv.ParseUint(p[1], 10, 32) + if err != nil { + log.Fatal(err) + } + + contentServers = append(contentServers, &ContentServer{ + Index: i, + Address: p[0], + Weight: uint(weight), + LastCheckResult: false, + LastCheck: time.Date(1970, 0, 0, 0, 0, 0, 0, time.UTC), + }) + } + + //TODO: cron this + checkContentServers() + + sniAddress = strings.ToLower(*sniAddressOption) + + bogusCertificatePEM, bogusKeyPairPEM := createSelfSignedCertificate() + + bogusCertificate, err := tls.X509KeyPair(bogusCertificatePEM, bogusKeyPairPEM) + if err != nil { + log.Fatal(err) + } + serverCertificate, err := tls.LoadX509KeyPair(*certificatePath, *keypairPath) + if err != nil { + log.Fatal(err) + } + + server := &http.Server{ + Addr: *listenAddress, + Handler: http.HandlerFunc(handle), + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: 0, //max supported, currently TLS 1.3 + CurvePreferences: []tls.CurveID{ + tls.X25519, + tls.CurveP256, + tls.CurveP384, + }, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + }, + PreferServerCipherSuites: false, + SessionTicketsDisabled: false, + Renegotiation: tls.RenegotiateFreelyAsClient, + NextProtos: []string{ + "h2", + "http/1.1", + }, + GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + if isSNIAllowed(info.ServerName) { + return &serverCertificate, nil + } + return &bogusCertificate, nil + }, + }, + } + + log.Printf("Serving on %s", *listenAddress) + + log.Fatal(server.ListenAndServeTLS(*certificatePath, *keypairPath)) +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f0b2305 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2022 FinalCommander Contributors All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7aab48 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# FinalCommander + +Content-addressable storage redirector. + + +## Usage + +`$ go run .` + +Build via `$ go build -o fcmm` \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8c68a3d --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.gammaspectra.live/S.O.N.G/FinalCommander + +go 1.14 + +require ( + github.com/ipfs/go-cid v0.1.0 + github.com/mroth/weightedrand v0.4.1 + github.com/multiformats/go-multihash v0.0.15 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..876814e --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +github.com/ipfs/go-cid v0.1.0 h1:YN33LQulcRHjfom/i25yoOZR4Telp1Hr/2RU3d0PnC0= +github.com/ipfs/go-cid v0.1.0/go.mod h1:rH5/Xv83Rfy8Rw6xG+id3DYAMUVmem1MowoKwdXmN2o= +github.com/klauspost/cpuid/v2 v2.0.4 h1:g0I61F2K2DjRHz1cnxlkNSBIaePVoJIjjnHui8QHbiw= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mroth/weightedrand v0.4.1 h1:rHcbUBopmi/3x4nnrvwGJBhX9d0vk+KgoLUZeDP6YyI= +github.com/mroth/weightedrand v0.4.1/go.mod h1:3p2SIcC8al1YMzGhAIoXD+r9olo/g/cdJgAD905gyNE= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multihash v0.0.15 h1:hWOPdrNqDjwHDx82vsYGSDZNyktOJJ2dzZJzFkOV1jM= +github.com/multiformats/go-multihash v0.0.15/go.mod h1:D6aZrWNLFTV/ynMpKsNtB40mJzmCl4jb1alC0OvHiHg= +github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= +github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=