Initial commit

This commit is contained in:
DataHoarder 2022-01-16 13:49:45 +01:00
commit 9005122a18
6 changed files with 634 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.idea

566
FinalCommander.go Normal file
View File

@ -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))
}

9
LICENSE Normal file
View File

@ -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.

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# FinalCommander
Content-addressable storage redirector.
## Usage
`$ go run .`
Build via `$ go build -o fcmm`

9
go.mod Normal file
View File

@ -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
)

39
go.sum Normal file
View File

@ -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=