├── .dockerignore ├── .gitignore ├── Dockerfile.build ├── Dockerfile.cli ├── Dockerfile.srv ├── common ├── Types.go ├── md5 │ └── MD5.go ├── tls │ └── TLS.go └── serialize │ └── Serialize.go ├── srv ├── Types.go ├── Server.go └── Handlers.go ├── go.mod ├── cli ├── Types.go └── Client.go ├── LICENSE ├── Readme.md ├── cmd ├── cli │ └── main.go ├── srv │ └── main.go └── Cmd.md └── go.sum /.dockerignore: -------------------------------------------------------------------------------- 1 | cmd/cli/dummyfile* 2 | cmd/srv/dummyfile* 3 | cmd/srv/*.qlog -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/cli/dummyfile* 2 | cmd/srv/dummyfile* 3 | cmd/srv/*.qlog -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 as quicdependencies 2 | 3 | ENV QUIC_GO_DISABLE_ECN=true 4 | ENV HOME /home 5 | 6 | RUN apt-get update && \ 7 | apt-get install -y openssl libssl-dev -------------------------------------------------------------------------------- /Dockerfile.cli: -------------------------------------------------------------------------------- 1 | FROM quicdependencies as cli 2 | 3 | WORKDIR $HOME/quiccli 4 | 5 | COPY cli ./cli 6 | COPY cmd ./cmd 7 | COPY common ./common 8 | COPY go.mod \ 9 | go.sum ./ 10 | 11 | RUN go build -o quiccli ./cmd/cli/main.go 12 | 13 | EXPOSE 1235 14 | 15 | ENTRYPOINT ["./quiccli"] -------------------------------------------------------------------------------- /Dockerfile.srv: -------------------------------------------------------------------------------- 1 | FROM quicdependencies as srv 2 | 3 | WORKDIR $HOME/quicsrv 4 | 5 | COPY srv ./srv 6 | COPY cmd ./cmd 7 | COPY common ./common 8 | COPY go.mod \ 9 | go.sum ./ 10 | 11 | RUN go build -o quicsrv ./cmd/srv/main.go 12 | 13 | EXPOSE 1234 14 | 15 | ENTRYPOINT ["./quicsrv"] -------------------------------------------------------------------------------- /common/Types.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | 4 | const FTRANSFER_PROTO = "quic-file-transfer" 5 | const DEFAULT_HANDSHAKE_TIME = 3 6 | const MAX_FILENAME_LENGTH = 1024 7 | const CLIENT_PAYLOAD_MAX_LENGTH = MAX_FILENAME_LENGTH + 1 8 | const FILE_META_PAYLOAD_MAX_LENGTH = 24 9 | const CHUNK_META_PAYLOAD_MAX_LENGTH = 16 10 | const NET_PROTOCOL = "udp4" 11 | 12 | const ( 13 | NO_ERROR = 0x0 14 | INTERNAL_ERROR = 0x1 15 | CONNECTION_ERROR = 0x2 16 | TRANSPORT_ERROR = 0x3 17 | ) -------------------------------------------------------------------------------- /srv/Types.go: -------------------------------------------------------------------------------- 1 | package srv 2 | 3 | import ( 4 | "crypto/tls" 5 | 6 | "github.com/quic-go/quic-go" 7 | ) 8 | 9 | 10 | // QuicServerOpts: the options for the quic server on init 11 | type QuicServerOpts struct { 12 | // Host: the host for server 13 | Host string 14 | // Port: the port the host is listening on 15 | Port int 16 | // TlsCert: the server certificate 17 | TlsCert *tls.Certificate 18 | // EnableTracer: adds a file logger to capture events on the http3 server 19 | EnableTracer bool 20 | } 21 | 22 | // QuicServer: the quic server implementation 23 | type QuicServer struct { 24 | listener *quic.EarlyListener 25 | host string 26 | port int 27 | } 28 | 29 | const STREAM_CHUNK_BUFFER_SIZE = 1024 * 1024 * 2 // 2KiB -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sirgallo/quicfiletransfer 2 | 3 | go 1.20 4 | 5 | require github.com/quic-go/quic-go v0.40.0 6 | 7 | require ( 8 | github.com/francoispqt/gojay v1.2.13 // indirect 9 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 10 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect 11 | github.com/onsi/ginkgo/v2 v2.9.5 // indirect 12 | github.com/quic-go/qtls-go1-20 v0.4.1 // indirect 13 | go.uber.org/mock v0.3.0 // indirect 14 | golang.org/x/crypto v0.4.0 // indirect 15 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect 16 | golang.org/x/mod v0.11.0 // indirect 17 | golang.org/x/net v0.10.0 // indirect 18 | golang.org/x/sys v0.8.0 // indirect 19 | golang.org/x/tools v0.9.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /cli/Types.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | 4 | // QuicClientOpts: options on client init 5 | type QuicClientOpts struct { 6 | // Host: the host for the remote server 7 | RemoteHost string 8 | // RemotePort: the port for the remote server 9 | RemotePort int 10 | // ClientPort: the port the client starts the udp connection with 11 | ClientPort int 12 | // Streams: the number of streams the client should open (100 is default max) 13 | Streams uint8 14 | // CheckMD5: optionally check the md5 file to ensure validity of data 15 | CheckMd5 bool 16 | } 17 | 18 | // QuicClient: the quic client implementation 19 | type QuicClient struct { 20 | remoteAddress string 21 | cliPort int 22 | streams uint8 23 | dstFile string 24 | checkMd5 bool 25 | } 26 | 27 | // OpenConnectionOpts: options to pass when opening a new connection 28 | type OpenConnectionOpts struct { 29 | // Insecure: tells the client to not verify server certs. Should only be used for testing 30 | Insecure bool 31 | } 32 | 33 | 34 | const HANDSHAKE_TIMEOUT = 3 35 | const WRITE_BUFFER_SIZE = 1024 * 1024 * 8 // 8MB -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 sirgallo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # QUIC File Transfer Service 2 | 3 | ## a cli + srv for transferring large files 4 | 5 | 6 | ## Design 7 | 8 | The `File Transfer Service` utilizes the [quic-go](https://github.com/quic-go/quic-go) implementation of the [quic](https://en.wikipedia.org/wiki/QUIC) Protocol, built on top of `UDP`. Since `quic` allows for multiplexing of streams on a single connection, the service takes advantage of this to attempt to speed up file transfers by processing and writing the file from the remote host (the server) to the destination (the client) concurrently. 9 | 10 | A client attempts to make a connection to a host running the server implementation. If the connection is successful, the client then opens a stream, or multiple streams, to the host, requesting a file, along with providing the current stream and the total number of streams opened. The server determines the number of chunks and size of each chunk to then stream back to the client. Each stream is made aware of its start offset and the size of the chunk it is processing. 11 | 12 | [0RTT](https://http3-explained.haxx.se/en/quic/quic-0rtt) has also been enabled, which reduces the number of handshakes needed to make a secure connection. 13 | 14 | An optional `MD5` checksum can be calculated as well for the transferred file to verify that the content is the same as the source file. The server provides its own `MD5` for comparison once the file is written. However, this would only be an additional level of redundancy as `quic` has a reliability guarantee already built into the protocol. 15 | 16 | 17 | ## cmd 18 | 19 | A server and client implementation are both provided. For usage and configuration, check [CMD](./cmd/Cmd.md). -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | 8 | "github.com/sirgallo/quicfiletransfer/cli" 9 | ) 10 | 11 | 12 | const STREAMS = 1 13 | 14 | 15 | func main() { 16 | homeDir, getHomeDirErr := os.UserHomeDir() 17 | if getHomeDirErr != nil { log.Fatal(getHomeDirErr) } 18 | 19 | cwd, getCwdErr := os.Getwd() 20 | if getCwdErr != nil { log.Fatal(getCwdErr) } 21 | 22 | var host, filename, srcFolder, dstFolder string 23 | var port, cliport, streams int 24 | var insecure, checkMd5 bool 25 | 26 | flag.StringVar(&host, "host", "127.0.0.1", "the host where the remote file exists") 27 | flag.IntVar(&port, "port", 1234, "the port serving the file") 28 | flag.IntVar(&cliport, "cliPort", 1235, "the port the client establishes udp connection on") 29 | flag.StringVar(&filename, "filename", "dummyfile", "the name of the file to transfer") 30 | flag.StringVar(&srcFolder, "srcFolder", homeDir, "the source folder for the file on the remote system") 31 | flag.StringVar(&dstFolder, "dstFolder", cwd, "the destination folder on the local system") 32 | flag.IntVar(&streams, "streams", STREAMS, "determine the total number of streams to launch for a connection") 33 | flag.BoolVar(&insecure, "insecure", false, "whether or not to use an insecure connection") 34 | flag.BoolVar(&checkMd5, "checkMd5", false, "whether or not to additionally compute + check the md5checksum for the file") 35 | 36 | flag.Parse() 37 | 38 | cliOpts := &cli.QuicClientOpts{ 39 | RemoteHost: host, 40 | RemotePort: port, 41 | ClientPort: cliport, 42 | Streams: uint8(streams), 43 | CheckMd5: checkMd5, 44 | } 45 | 46 | client, newCliErr := cli.NewClient(cliOpts) 47 | if newCliErr != nil { log.Fatal(newCliErr) } 48 | 49 | openOpts := &cli.OpenConnectionOpts{ Insecure: insecure } 50 | path, transferErr := client.StartFileTransferStream(openOpts, filename, srcFolder, dstFolder) 51 | if transferErr != nil { log.Fatal(transferErr) } 52 | 53 | log.Printf("new path: %s\n", *path) 54 | } -------------------------------------------------------------------------------- /common/md5/MD5.go: -------------------------------------------------------------------------------- 1 | package md5 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "errors" 7 | "io" 8 | "os" 9 | "regexp" 10 | ) 11 | 12 | 13 | //============================================= MD5 14 | 15 | 16 | // CalculateMD5 17 | // Calculate MD5Checksum for the transferred file. 18 | // Return back the byte array representation. 19 | func CalculateMD5(filePath string) ([]byte, error) { 20 | f, openErr := os.OpenFile(filePath, os.O_RDONLY, 0666) 21 | if openErr != nil { return nil, openErr } 22 | 23 | _, seekErr := f.Seek(0, 0) 24 | if seekErr != nil { return nil, seekErr } 25 | 26 | hash := md5.New() 27 | _, generateMd5Err := io.Copy(hash, f) 28 | if generateMd5Err != nil { return nil, generateMd5Err } 29 | 30 | return hash.Sum(nil), nil 31 | } 32 | 33 | // DeserializeMD5ToHex 34 | // Tranform byte representation of MD5Checksum to hex. 35 | func DeserializeMD5ToHex(input []byte) (string, error) { 36 | if len(input) != 16 { return "", errors.New("input length for md5sum should be 16") } 37 | return hex.EncodeToString(input), nil 38 | } 39 | 40 | // SerializeMD5ToBytes 41 | // Transform a string representation of MD5Checksum to byte array. 42 | // For transferring on the wire. 43 | func SerializeMD5ToBytes(input string) ([]byte, error) { 44 | controlCharRegex := regexp.MustCompile(`[\x00-\x1F\x7F]`) 45 | md5 := controlCharRegex.ReplaceAllLiteralString(input, "") 46 | 47 | md5Bytes, decodeErr := hex.DecodeString(md5) 48 | if decodeErr != nil { return nil, decodeErr } 49 | 50 | return md5Bytes, nil 51 | } 52 | 53 | // ReadMD5FromFile 54 | // Read a MD5Checksum from a file. 55 | // The hex representation is then serialized to byte array. 56 | func ReadMD5FromFile(md5FilePath string) ([]byte, error) { 57 | data, err := os.ReadFile(md5FilePath) 58 | if err != nil { return nil, err } 59 | 60 | md5Bytes, sErr := SerializeMD5ToBytes(string(data)) 61 | if sErr != nil { return nil, sErr } 62 | if len(md5Bytes) != 16 { return nil, errors.New("md5 sum incorrect length") } 63 | 64 | return md5Bytes, nil 65 | } -------------------------------------------------------------------------------- /common/tls/TLS.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "math/big" 11 | "time" 12 | ) 13 | 14 | 15 | //============================================= TLS Self Signed Certs 16 | 17 | 18 | // DO NOT USE FOR PRODUCTION PURPOSES 19 | 20 | 21 | func GenerateTLSCert(org string) (*tls.Certificate, error) { 22 | privKey, genPrivKeyErr := generatePrivateKey() 23 | if genPrivKeyErr != nil { return nil, genPrivKeyErr } 24 | 25 | certBytes, genSelfSignedErr := createSelfSignedCert(org, privKey) 26 | if genSelfSignedErr != nil { return nil, genSelfSignedErr } 27 | 28 | return &tls.Certificate{ Certificate: [][]byte{ certBytes }, PrivateKey: privKey }, nil 29 | } 30 | 31 | func createSelfSignedCert(org string, privKey *ecdsa.PrivateKey) ([]byte, error) { 32 | template, genCertErr := generateCertTemplate(org) 33 | if genCertErr != nil { return nil, genCertErr } 34 | 35 | certBytes, certErr := x509.CreateCertificate(rand.Reader, template, template, &privKey.PublicKey, privKey) 36 | if certErr != nil { return nil, certErr } 37 | 38 | return certBytes, nil 39 | } 40 | 41 | func generateCertTemplate(org string) (*x509.Certificate, error) { 42 | notBefore := time.Now() 43 | notAfter := notBefore.Add(365 * 24 * time.Hour) 44 | 45 | serialNumber, randErr := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) 46 | if randErr != nil { return nil, randErr } 47 | 48 | return &x509.Certificate{ 49 | SerialNumber: serialNumber, 50 | Subject: pkix.Name{ Organization: []string{ org }}, 51 | NotBefore: notBefore, 52 | NotAfter: notAfter, 53 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 54 | ExtKeyUsage: []x509.ExtKeyUsage{ x509.ExtKeyUsageServerAuth }, 55 | BasicConstraintsValid: true, 56 | }, nil 57 | } 58 | 59 | func generatePrivateKey() (*ecdsa.PrivateKey, error) { 60 | privKey, genErr := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 61 | if genErr != nil { return nil, genErr } 62 | 63 | return privKey, nil 64 | } -------------------------------------------------------------------------------- /common/serialize/Serialize.go: -------------------------------------------------------------------------------- 1 | package serialize 2 | 3 | import "errors" 4 | import "encoding/binary" 5 | import "math/big" 6 | 7 | 8 | //============================================= Serialize 9 | 10 | 11 | // Below are utility functions for serializing and deserializing primitives into and from byte arrays 12 | 13 | 14 | func SerializeBigInt(in *big.Int, totalBytes int) []byte { 15 | buf := make([]byte, totalBytes) 16 | return in.FillBytes(buf) 17 | } 18 | 19 | func DeserializeBigInt(data []byte, totalBytes int) (*big.Int, error) { 20 | if len(data) != totalBytes { return nil, errors.New("invalid data length for total bytes provided") } 21 | 22 | num := new(big.Int) 23 | num.SetBytes(data) 24 | return num, nil 25 | } 26 | 27 | func SerializeUint64(in uint64) []byte { 28 | buf := make([]byte, 8) 29 | binary.LittleEndian.PutUint64(buf, in) 30 | return buf 31 | } 32 | 33 | func DeserializeUint64(data []byte) (uint64, error) { 34 | if len(data) != 8 { return uint64(0), errors.New("invalid data length for byte slice to uint64") } 35 | return binary.LittleEndian.Uint64(data), nil 36 | } 37 | 38 | func SerializeUint32(in uint32) []byte { 39 | buf := make([]byte, 4) 40 | binary.LittleEndian.PutUint32(buf, in) 41 | return buf 42 | } 43 | 44 | func DeserializeUint32(data []byte) (uint32, error) { 45 | if len(data) != 4 { return uint32(0), errors.New("invalid data length for byte slice to uint32") } 46 | return binary.LittleEndian.Uint32(data), nil 47 | } 48 | 49 | func SerializeUint16(in uint16) []byte { 50 | buf := make([]byte, 2) 51 | binary.LittleEndian.PutUint16(buf, in) 52 | return buf 53 | } 54 | 55 | func DeserializeUint16(data []byte) (uint16, error) { 56 | if len(data) != 2 { return uint16(0), errors.New("invalid data length for byte slice to uint16") } 57 | return binary.LittleEndian.Uint16(data), nil 58 | } 59 | 60 | func SerializeBool(isTrue bool) byte { 61 | if isTrue { 62 | return 0x01 63 | } else { return 0x00 } 64 | } 65 | 66 | func DeserializeBool(data byte) bool { 67 | if data == 0x01 { 68 | return true 69 | } else { return false } 70 | } -------------------------------------------------------------------------------- /cmd/srv/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "log" 7 | "os" 8 | 9 | "github.com/sirgallo/quicfiletransfer/srv" 10 | 11 | customtls "github.com/sirgallo/quicfiletransfer/common/tls" 12 | ) 13 | 14 | 15 | const HOST = "0.0.0.0" 16 | const PORT = 1234 17 | const ORG = "test" 18 | 19 | 20 | func main() { 21 | var host, org, certPath, keyPath string 22 | var port int 23 | var enableTracer bool 24 | 25 | flag.StringVar(&host, "host", HOST, "the host IP/domain for the quic server") 26 | flag.IntVar(&port, "port", PORT, "the port tot listen on") 27 | flag.StringVar(&org, "org", ORG, "the organization for self signed certs") 28 | flag.StringVar(&certPath, "certPath", "", "the path to the cert. If not provided will generate self signed") 29 | flag.StringVar(&keyPath, "keyPath", "", "the path the private key. If not provided will generate self signed") 30 | flag.BoolVar(&enableTracer, "enableTracer", false, "enable the tracer. This creates a log file in the working directory") 31 | 32 | flag.Parse() 33 | 34 | var cert *tls.Certificate 35 | switch { 36 | case certPath == "" || keyPath == "": 37 | srvSelfSigned, genSrvCertErr := customtls.GenerateTLSCert(ORG) 38 | if genSrvCertErr != nil { log.Fatal(genSrvCertErr) } 39 | 40 | cert = srvSelfSigned 41 | default: 42 | fCert, readCertErr := os.ReadFile(certPath) 43 | if readCertErr != nil { log.Fatalf("Failed to read certificate file: %v", readCertErr) } 44 | 45 | fKey, readKeyErr := os.ReadFile(keyPath) 46 | if readKeyErr != nil { log.Fatalf("Failed to read private key file: %v", readKeyErr) } 47 | 48 | tlsCert, getCertErr := tls.X509KeyPair(fCert, fKey) 49 | if getCertErr != nil { log.Fatalf("Failed to load certificate: %v", getCertErr) } 50 | 51 | cert = &tlsCert 52 | } 53 | 54 | srvOpts := &srv.QuicServerOpts{ Host: host, Port: port, TlsCert: cert, EnableTracer: enableTracer } 55 | server, newSrvErr := srv.NewQuicServer(srvOpts) 56 | if newSrvErr != nil { log.Fatal(newSrvErr) } 57 | 58 | err := server.Listen() 59 | if err != nil { log.Fatal(err) } 60 | 61 | select{} 62 | } -------------------------------------------------------------------------------- /srv/Server.go: -------------------------------------------------------------------------------- 1 | package srv 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "log" 8 | "net" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/quic-go/quic-go" 14 | "github.com/quic-go/quic-go/logging" 15 | "github.com/quic-go/quic-go/qlog" 16 | 17 | "github.com/sirgallo/quicfiletransfer/common" 18 | ) 19 | 20 | 21 | //============================================= Server 22 | 23 | 24 | // NewQuicServer 25 | // Create the quic file transfer server. 26 | // If tracer is enabled, a log of all events will be dumped to the directy the server is run in. 27 | func NewQuicServer(opts *QuicServerOpts) (*QuicServer, error) { 28 | tlsConfig := &tls.Config{ 29 | Certificates: []tls.Certificate{ *opts.TlsCert }, 30 | NextProtos: []string{ common.FTRANSFER_PROTO }, 31 | } 32 | 33 | quicConfig := &quic.Config{ Allow0RTT: true, EnableDatagrams: true, KeepAlivePeriod: 3 * time.Second } 34 | 35 | if opts.EnableTracer { 36 | log.Println("enable tracer:", opts.EnableTracer) 37 | tracer := func(ctx context.Context, p logging.Perspective, connID quic.ConnectionID) *logging.ConnectionTracer { 38 | role := "server" 39 | if p == logging.PerspectiveClient { role = "client" } 40 | 41 | filename := fmt.Sprintf("./log_%s_%s.qlog", connID, role) 42 | f, createErr := os.Create(filename) 43 | if createErr != nil { log.Fatal(createErr) } 44 | 45 | return qlog.NewConnectionTracer(f, p, connID) 46 | } 47 | 48 | quicConfig.Tracer = tracer 49 | } 50 | 51 | udpConn, udpErr := net.ListenUDP(common.NET_PROTOCOL, &net.UDPAddr{ IP: net.ParseIP(opts.Host), Port: opts.Port }) 52 | if udpErr != nil { return nil, udpErr } 53 | 54 | tr := quic.Transport{ Conn: udpConn } 55 | listener, listenQuicErr := tr.ListenEarly(tlsConfig, quicConfig) 56 | if listenQuicErr != nil { return nil, listenQuicErr } 57 | 58 | log.Printf("quic transport layer started for: %s\n", listener.Addr().String()) 59 | return &QuicServer{ host: opts.Host, port: opts.Port, listener: listener }, nil 60 | } 61 | 62 | // Listen 63 | // Begin accepting and processing connections from clients. 64 | // This is asynchronous. 65 | func (srv *QuicServer) Listen() error { 66 | defer srv.listener.Close() 67 | 68 | var listenWG sync.WaitGroup 69 | 70 | listenWG.Add(1) 71 | go func() { 72 | defer listenWG.Done() 73 | for { 74 | conn, connErr := srv.listener.Accept(context.Background()) 75 | if connErr != nil { 76 | log.Println("connection error:", connErr.Error()) 77 | continue 78 | } 79 | 80 | go func () { 81 | handleErr := handleConnection(conn) 82 | if handleErr != nil { log.Println("error on handler:", handleErr.Error()) } 83 | }() 84 | } 85 | }() 86 | 87 | listenWG.Wait() 88 | return nil 89 | } -------------------------------------------------------------------------------- /cmd/Cmd.md: -------------------------------------------------------------------------------- 1 | # cmd 2 | 3 | This is an example of quic client/server interaction for large files. 4 | 5 | To generate a random large file in the `cmd/srv` directory (this is our dummy service), run: 6 | ```bash 7 | dd if=/dev/urandom of=dummyfile bs=1G count=10 8 | ``` 9 | 10 | The above will generate a `10GB` file, with random values. 11 | 12 | Next generate a md5hash from the file. This will be used to ensure the transferred file's integrity. 13 | 14 | `macOS`: 15 | ```bash 16 | md5 -r dummyfile | sed 's/ dummyfile//' > dummyfile.md5 17 | ``` 18 | 19 | `linux`: 20 | ```bash 21 | md5sum dummyfile | awk '{print $1}' > dummyfile.md5 22 | ``` 23 | 24 | The server has these optional command line arguments: 25 | ``` 26 | -host=string -> the server host (default is 0.0.0.0) 27 | -port=int -> the port the server host is serving from (default is 1234) 28 | -org=string -> the organization for self signed certs (default is test) 29 | -certPath=string -> the path to the valid tls cert file (default is "") 30 | -keyPath=string -> the path to the valid tls private key file (default is "") 31 | -enableTracer=bool -> enable the tracer, which will create a log file for all events (default is false) 32 | ``` 33 | 34 | By default, if neither `certPath` or `keyPath` are provided, a self signed cert is generated. 35 | 36 | To run the server (in `./srv`): 37 | ```bash 38 | go run main.go 39 | ``` 40 | 41 | The server also implements a tracer, so a log file is dopped in `./srv`, where all events will be written to. 42 | 43 | **NOTE** Enabling the tracer will have a performance impact on the server. 44 | 45 | The cli has these optional command line arguments: 46 | ``` 47 | -host=string -> the remote host (default is 127.0.0.1) 48 | -port=int -> the port the remote host is serving from (default is 1234) 49 | -cliPort=int -> the port the client establishes udp connection on (default is 1235) 50 | -filename=string -> the name of the file to be transfered (default is dummyfile) 51 | -srcFolder=string -> the path to the file on the remote server(default is //quicfiletransfer/cmd/srv) 52 | -dstFolder=string -> the path to the destination folder on the local machine (default is //quicfiletransfer/cmd/cli) 53 | -insecure=bool -> determines if the client should verify the server's cert (default is false) 54 | -streams=int -> the number of streams to open on the file transfer (default is 1) 55 | -checkMd5=bool -> perform additional md5 check against remote md5 file (default is false) 56 | ``` 57 | 58 | **NOTE** The insecure flag should only be used in development 59 | 60 | In a separate terminal window (in `./cli`), run the following to test the `50GB` file transfer (local needs to be `insecure` connection): 61 | ```bash 62 | go run main.go -filename=dummyfile -srcFolder=//quicfiletransfer/cmd/srv -dstFolder=//quicfiletransfer/cmd/cli -insecure=true -checkMd5=true 63 | ``` 64 | 65 | 66 | # docker 67 | 68 | `build server` 69 | ```bash 70 | docker build -f Dockerfile.build -t quicdependencies . 71 | docker build -f Dockerfile.srv -t srv . 72 | ``` 73 | 74 | `build client` 75 | ```bash 76 | docker build -f Dockerfile.build -t quicdependencies . 77 | docker build -f Dockerfile.cli -t cli . 78 | ``` 79 | 80 | `run client` 81 | ```bash 82 | docker run --net=host \ 83 | --cpus= \ 84 | --memory=g \ 85 | -p 1235:1235 \ 86 | -v /:/home/quiccli/files cli \ 87 | -host= \ 88 | -port= \ 89 | -filename=dummyfile \ 90 | -srcFolder=/home/quicsrv/files \ 91 | -dstFolder=/home/quiccli/files \ 92 | -insecure=true \ 93 | -checkMd5=true \ 94 | -streams= 95 | ``` 96 | 97 | `run server` 98 | ```bash 99 | docker run --net=host \ 100 | --cpus= \ 101 | --memory=g \ 102 | -p 1234:1234 \ 103 | -v /:/home/quicsrv/files srv 104 | ``` 105 | 106 | 107 | # tests 108 | 109 | ## remote 110 | 111 | `system - both server and client` 112 | ``` 113 | 48 cores, 32GB RAM, 11T SSD 114 | ``` 115 | 116 | ``` 117 | Test (3 runs, 2 streams): 118 | 119 | rsync: 120 | run 1: 3m37s - 46.08 MB/s 121 | run 2: 3m40s - 45.45 MB/s 122 | run 3: 3m34s - 46.54 MB/s 123 | 124 | quic: 125 | run 1: 1m17s - 129.87 MB/s 126 | run 2: 1m13s - 136.99 MB/s 127 | run 3: 1m14s - 135.14 MB/s 128 | 129 | rsync avg => 46.02 MB/s 130 | quic avg => 134 MB/s 131 | 132 | quic file transfer over 2.91x rsync for 10GB file 133 | ``` 134 | 135 | 136 | # Note 137 | 138 | **on linux, the udp receive buffer size may need to be increased for better performance** 139 | 140 | ```bash 141 | sysctl -w net.core.rmem_max=2500000 142 | sysctl -w net.core.wmem_max=2500000 143 | ``` -------------------------------------------------------------------------------- /srv/Handlers.go: -------------------------------------------------------------------------------- 1 | package srv 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "os" 8 | "sync" 9 | 10 | "github.com/quic-go/quic-go" 11 | 12 | "github.com/sirgallo/quicfiletransfer/common" 13 | "github.com/sirgallo/quicfiletransfer/common/md5" 14 | "github.com/sirgallo/quicfiletransfer/common/serialize" 15 | ) 16 | 17 | 18 | //============================================= Server Handlers 19 | 20 | 21 | // handleConnection 22 | // Accept multiple streams from a single connection since QUIC can multiplex streams. 23 | func handleConnection(conn quic.Connection) error { 24 | for { 25 | stream, streamErr := conn.AcceptStream(context.Background()) 26 | if streamErr != nil { 27 | conn.CloseWithError(common.CONNECTION_ERROR, streamErr.Error()) 28 | return streamErr 29 | } 30 | 31 | go handleCommStream(conn, stream) 32 | } 33 | } 34 | 35 | // handleCommStream 36 | // The bidirectional communication channel between the client and server. 37 | // For individual streams get the file to transfer. 38 | // The server opens the file and determines the size of the chunk to send to the client. 39 | // The server then sends a metadata payload to the client containing filesize, chunksize, and the start offset to process. 40 | // The data from the chunk in the file is written to the stream to be received by the client. 41 | func handleCommStream(conn quic.Connection, commStream quic.Stream) error { 42 | defer commStream.Close() 43 | 44 | buf := make([]byte, common.CLIENT_PAYLOAD_MAX_LENGTH) 45 | payloadLength, readPayloadErr := commStream.Read(buf) 46 | if readPayloadErr != nil { 47 | conn.CloseWithError(common.TRANSPORT_ERROR, readPayloadErr.Error()) 48 | return readPayloadErr 49 | } 50 | 51 | totalStreamsForFile := uint8(buf[0]) 52 | fileName := string(buf[1:payloadLength]) 53 | 54 | log.Printf("filename: %s, total streams for file: %d\n", fileName, totalStreamsForFile) 55 | 56 | file, openErr := os.Open(fileName) 57 | if openErr != nil { 58 | conn.CloseWithError(common.INTERNAL_ERROR, openErr.Error()) 59 | return openErr 60 | } 61 | 62 | fileStat, statErr := file.Stat() 63 | if statErr != nil { 64 | file.Close() 65 | conn.CloseWithError(common.INTERNAL_ERROR, openErr.Error()) 66 | return statErr 67 | } 68 | 69 | file.Close() 70 | 71 | fileSize := uint64(fileStat.Size()) 72 | md5, getMd5Err := md5.ReadMD5FromFile(fileName + ".md5") 73 | if getMd5Err != nil { 74 | conn.CloseWithError(common.INTERNAL_ERROR, getMd5Err.Error()) 75 | return getMd5Err 76 | } 77 | 78 | log.Printf("fileSize: %d\n", fileSize) 79 | 80 | metaPayload := func() []byte { 81 | p := make([]byte, common.FILE_META_PAYLOAD_MAX_LENGTH) 82 | copy(p[:8], serialize.SerializeUint64(fileSize)) 83 | copy(p[8:], md5) 84 | 85 | return p 86 | }() 87 | 88 | _, writeMetaErr := commStream.Write(metaPayload) 89 | if writeMetaErr != nil { 90 | conn.CloseWithError(common.TRANSPORT_ERROR, writeMetaErr.Error()) 91 | return writeMetaErr 92 | } 93 | 94 | var multiplexWG sync.WaitGroup 95 | for s := range make([]uint8, totalStreamsForFile) { 96 | multiplexWG.Add(1) 97 | 98 | dataStream, openStreamErr := conn.OpenUniStream() 99 | if openStreamErr != nil { 100 | conn.CloseWithError(common.TRANSPORT_ERROR, openStreamErr.Error()) 101 | return openStreamErr 102 | } 103 | 104 | go func(s uint8) { 105 | defer multiplexWG.Done() 106 | defer dataStream.Close() 107 | 108 | chunkSize := fileSize / uint64(totalStreamsForFile) 109 | startOffset := uint64(s) * chunkSize 110 | 111 | if fileSize % uint64(totalStreamsForFile) != 0 && uint8(s) == totalStreamsForFile - 1 { 112 | chunkSize += fileSize % uint64(totalStreamsForFile) 113 | } 114 | 115 | log.Printf("startOffset: %d, chunkSize: %d\n", startOffset, chunkSize) 116 | 117 | sendPayload := func() []byte { 118 | p := make([]byte, common.CHUNK_META_PAYLOAD_MAX_LENGTH) 119 | copy(p[:8], serialize.SerializeUint64(startOffset)) 120 | copy(p[8:], serialize.SerializeUint64(chunkSize)) 121 | 122 | return p 123 | }() 124 | 125 | _, writeErr := dataStream.Write(sendPayload) 126 | if writeErr != nil { 127 | conn.CloseWithError(common.TRANSPORT_ERROR, openErr.Error()) 128 | return 129 | } 130 | 131 | f, openChunkErr := os.OpenFile(fileName, os.O_RDONLY, 0666) 132 | if openChunkErr != nil { 133 | conn.CloseWithError(common.INTERNAL_ERROR, openChunkErr.Error()) 134 | return 135 | } 136 | 137 | defer f.Close() 138 | 139 | totalBytesStreamed := int64(0) 140 | for int64(chunkSize) > totalBytesStreamed { 141 | _, seekErr := f.Seek(int64(startOffset) + totalBytesStreamed, 0) 142 | if seekErr != nil { 143 | conn.CloseWithError(common.INTERNAL_ERROR, seekErr.Error()) 144 | return 145 | } 146 | 147 | var n int64 148 | var streamFileErr error 149 | 150 | copyChunk := func () int64 { 151 | if totalBytesStreamed + int64(STREAM_CHUNK_BUFFER_SIZE) > int64(chunkSize) { 152 | return int64(chunkSize) - totalBytesStreamed 153 | } 154 | 155 | return int64(STREAM_CHUNK_BUFFER_SIZE) 156 | }() 157 | 158 | n, streamFileErr = io.CopyN(dataStream, f, copyChunk) 159 | if streamFileErr == io.EOF { break } 160 | if streamFileErr != nil && streamFileErr != io.EOF { 161 | conn.CloseWithError(common.TRANSPORT_ERROR, streamFileErr.Error()) 162 | return 163 | } 164 | 165 | totalBytesStreamed += n 166 | 167 | _, writeBytesErr := commStream.Write(serialize.SerializeUint64(uint64(n))) 168 | if writeBytesErr != nil { 169 | conn.CloseWithError(common.TRANSPORT_ERROR, writeBytesErr.Error()) 170 | return 171 | } 172 | } 173 | 174 | log.Println("successfully transferred chunk", s) 175 | }(uint8(s)) 176 | } 177 | 178 | multiplexWG.Wait() 179 | 180 | log.Println("done") 181 | return nil 182 | } -------------------------------------------------------------------------------- /cli/Client.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "errors" 8 | "io" 9 | "log" 10 | "net" 11 | "os" 12 | "path/filepath" 13 | "runtime" 14 | "strconv" 15 | "sync" 16 | "sync/atomic" 17 | "time" 18 | 19 | "github.com/quic-go/quic-go" 20 | 21 | "github.com/sirgallo/quicfiletransfer/common" 22 | "github.com/sirgallo/quicfiletransfer/common/md5" 23 | "github.com/sirgallo/quicfiletransfer/common/serialize" 24 | ) 25 | 26 | 27 | //============================================= Client 28 | 29 | 30 | // NewClient 31 | // Create a new quic file transfer client. 32 | func NewClient(opts *QuicClientOpts) (*QuicClient, error) { 33 | remoteHostPort := net.JoinHostPort(opts.RemoteHost, strconv.Itoa(opts.RemotePort)) 34 | log.Printf("remote server address: %s\n", remoteHostPort) 35 | 36 | return &QuicClient{ 37 | remoteAddress: remoteHostPort, 38 | cliPort: opts.ClientPort, 39 | streams: opts.Streams, 40 | checkMd5: opts.CheckMd5, 41 | }, nil 42 | } 43 | 44 | // StartFileTransferStream 45 | // Invoke a file transfer operation. 46 | // The client provides the total number of streams to open. 47 | // Once each stream receives a metadata response from the server, the file is resized. 48 | // The streams for the client connection then receive and write the file chunks from the server to disk. 49 | func (cli *QuicClient) StartFileTransferStream(connectOpts *OpenConnectionOpts, filename, src, dst string) (*string, error){ 50 | var clientWG sync.WaitGroup 51 | 52 | isResizing := uint64(0) 53 | srcPath := filepath.Join(src, filename) 54 | cli.dstFile = filepath.Join(dst, filename) 55 | 56 | f, createErr := os.Create(cli.dstFile) 57 | if createErr != nil { return nil, createErr } 58 | f.Close() 59 | 60 | conn, connErr := cli.openConnection(connectOpts) 61 | if connErr != nil { return nil, connErr } 62 | defer conn.CloseWithError(common.NO_ERROR, "closing") 63 | 64 | commStream, openCommStreamErr := conn.OpenStream() 65 | if openCommStreamErr != nil { 66 | conn.CloseWithError(common.CONNECTION_ERROR, openCommStreamErr.Error()) 67 | return nil, openCommStreamErr 68 | } 69 | 70 | fileReq := func() []byte { 71 | tags := []byte{ cli.streams } 72 | return append(tags, []byte(srcPath)...) 73 | }() 74 | 75 | _, fileReqErr := commStream.Write(fileReq) 76 | if fileReqErr != nil { 77 | conn.CloseWithError(common.TRANSPORT_ERROR, fileReqErr.Error()) 78 | return nil, fileReqErr 79 | } 80 | 81 | buf := make([]byte, common.FILE_META_PAYLOAD_MAX_LENGTH) 82 | payloadLength, readPayloadErr := commStream.Read(buf) 83 | if readPayloadErr != nil { 84 | conn.CloseWithError(common.TRANSPORT_ERROR, readPayloadErr.Error()) 85 | return nil, readPayloadErr 86 | } 87 | 88 | remoteFileSize, sourceMd5, desMetaErr := cli.deserializeMetaPayload(buf[:payloadLength]) 89 | if desMetaErr != nil { 90 | conn.CloseWithError(common.INTERNAL_ERROR, desMetaErr.Error()) 91 | return nil, desMetaErr 92 | } 93 | 94 | resizeErr := cli.resizeDstFile(&isResizing, int64(remoteFileSize)) 95 | if resizeErr != nil { 96 | conn.CloseWithError(common.INTERNAL_ERROR, resizeErr.Error()) 97 | return nil, resizeErr 98 | } 99 | 100 | streamStartTime := time.Now() 101 | 102 | clientWG.Add(1) 103 | go func() { 104 | defer clientWG.Done() 105 | 106 | totBytes := uint64(0) 107 | for { 108 | buf := make([]byte, 8) 109 | _, readErr := commStream.Read(buf) 110 | if readErr == io.EOF { 111 | log.Println("done") 112 | return 113 | } 114 | 115 | if readErr != nil { 116 | conn.CloseWithError(common.TRANSPORT_ERROR, readErr.Error()) 117 | return 118 | } 119 | 120 | chunkBytes, desErr := serialize.DeserializeUint64(buf) 121 | if desErr != nil { 122 | conn.CloseWithError(common.INTERNAL_ERROR, desErr.Error()) 123 | return 124 | } 125 | 126 | totBytes += chunkBytes 127 | 128 | p := (float64(totBytes) / float64(remoteFileSize)) * 100 129 | currTime := time.Now() 130 | log.Printf("total bytes received: %d, percentage of total: %f, time elapsed: %v\n", totBytes, p, currTime.Sub(streamStartTime)) 131 | } 132 | }() 133 | 134 | for range make([]uint8, cli.streams) { 135 | dataStream, openSendStreamErr := conn.AcceptUniStream(context.Background()) 136 | if openSendStreamErr != nil { 137 | conn.CloseWithError(common.CONNECTION_ERROR, openSendStreamErr.Error()) 138 | return nil, openSendStreamErr 139 | } 140 | 141 | go func() { 142 | buf := make([]byte, common.CHUNK_META_PAYLOAD_MAX_LENGTH) 143 | payloadLength, readPayloadErr := dataStream.Read(buf) 144 | if readPayloadErr != nil { 145 | conn.CloseWithError(common.TRANSPORT_ERROR, readPayloadErr.Error()) 146 | return 147 | } 148 | 149 | startOffset, chunkSize, desErr := cli.deserializeChunkPayload(buf[:payloadLength]) 150 | if desErr != nil { 151 | conn.CloseWithError(common.INTERNAL_ERROR, desErr.Error()) 152 | return 153 | } 154 | 155 | log.Printf("startOffset: %d, chunkSize: %d\n", startOffset, chunkSize) 156 | 157 | f, openErr := os.OpenFile(cli.dstFile, os.O_RDWR, 0666) 158 | if openErr != nil { return } 159 | defer f.Close() 160 | 161 | _, seekErr := f.Seek(int64(startOffset), 0) 162 | if seekErr != nil { 163 | conn.CloseWithError(common.INTERNAL_ERROR, seekErr.Error()) 164 | return 165 | } 166 | 167 | writeBuffer := make([]byte, WRITE_BUFFER_SIZE) 168 | totalBytesRead := 0 169 | 170 | for int(chunkSize) > totalBytesRead { 171 | nRead, readErr := io.ReadFull(dataStream, writeBuffer) 172 | if readErr != nil && readErr != io.EOF && readErr != io.ErrUnexpectedEOF { 173 | conn.CloseWithError(common.TRANSPORT_ERROR, readErr.Error()) 174 | return 175 | } 176 | 177 | nWritten, writeErr := f.Write(writeBuffer[:nRead]) 178 | if writeErr != nil { 179 | conn.CloseWithError(common.INTERNAL_ERROR, writeErr.Error()) 180 | return 181 | } 182 | 183 | totalBytesRead += nWritten 184 | } 185 | }() 186 | } 187 | 188 | clientWG.Wait() 189 | 190 | streamEndTime := time.Now() 191 | streamElapsedTime := streamEndTime.Sub(streamStartTime) 192 | 193 | log.Println("file transfer complete, connection can now close") 194 | log.Println("total elapsed time for file transfer", streamElapsedTime) 195 | 196 | if cli.checkMd5 { return cli.performMd5Check(sourceMd5) } 197 | return &cli.dstFile, nil 198 | } 199 | 200 | // openConnection 201 | // Open a connection to a http3 server running over quic. 202 | // The DialEarly function attempts to make a connection using 0-RTT. 203 | func (cli *QuicClient) openConnection(opts *OpenConnectionOpts) (quic.Connection, error) { 204 | tlsConfig := &tls.Config{ InsecureSkipVerify: opts.Insecure, NextProtos: []string{ common.FTRANSFER_PROTO }} 205 | quicConfig := &quic.Config{ EnableDatagrams: true } 206 | 207 | udpAddr, getAddrErr := net.ResolveUDPAddr(common.NET_PROTOCOL, cli.remoteAddress) 208 | if getAddrErr != nil { return nil, getAddrErr } 209 | 210 | udpConn, udpErr := net.ListenUDP(common.NET_PROTOCOL, &net.UDPAddr{ Port: cli.cliPort }) 211 | if udpErr != nil { return nil, udpErr } 212 | 213 | ctx, cancel := context.WithTimeout(context.Background(), HANDSHAKE_TIMEOUT * time.Second) 214 | defer cancel() 215 | 216 | tr := &quic.Transport{ Conn: udpConn } 217 | conn, connErr := tr.DialEarly(ctx, udpAddr, tlsConfig, quicConfig) 218 | if connErr != nil { return nil, connErr } 219 | 220 | log.Println("connection made with:", conn.RemoteAddr()) 221 | return conn, nil 222 | } 223 | 224 | // deserializePayload 225 | // Initial metadata payload with remote filesize and md5. 226 | // Format: 227 | // bytes 0-7: uint64 representing the size of the file 228 | // bytes 8-23: md5 in byte format 229 | func (cli *QuicClient) deserializeMetaPayload(payload []byte) (uint64, []byte, error) { 230 | if len(payload) != common.FILE_META_PAYLOAD_MAX_LENGTH { return 0, nil, errors.New("payload incorrect length") } 231 | 232 | remoteFileSize, desFSizeErr := serialize.DeserializeUint64(payload[:8]) 233 | if desFSizeErr != nil { return 0, nil, desFSizeErr } 234 | 235 | return remoteFileSize, payload[8:], nil 236 | } 237 | 238 | // deserializeChunkPayload 239 | // Metadata payload regarding chunk size and start offset in file. 240 | // Format: 241 | // bytes 0-7: uint64 representing the start offset in the file where the stream should begin processing 242 | // bytes 8-16: uint64 representing the size of the chunk being received by the stream 243 | func (cli *QuicClient) deserializeChunkPayload(payload []byte) (uint64, uint64, error) { 244 | if len(payload) != common.CHUNK_META_PAYLOAD_MAX_LENGTH { return 0, 0, errors.New("payload incorrect length") } 245 | 246 | startOffset, desOffsetErr := serialize.DeserializeUint64(payload[:8]) 247 | if desOffsetErr != nil { return 0, 0, desOffsetErr } 248 | 249 | chunkSize, desChunkSizeErr := serialize.DeserializeUint64(payload[8:]) 250 | if desChunkSizeErr != nil { return 0, 0, desChunkSizeErr } 251 | 252 | return startOffset, chunkSize, nil 253 | } 254 | 255 | // resizeDstFile 256 | // When the streams receive the metadata, the file created needs to be resized to match the size of the remote file. 257 | func (cli *QuicClient) resizeDstFile(isResizing *uint64, remoteFileSize int64) error { 258 | f, openErr := os.OpenFile(cli.dstFile, os.O_RDWR, 0666) 259 | if openErr != nil { return openErr } 260 | defer f.Close() 261 | 262 | fSize := int64(0) 263 | for fSize != remoteFileSize { 264 | stat, statErr := f.Stat() 265 | if statErr != nil { return statErr } 266 | 267 | fSize = stat.Size() 268 | if atomic.CompareAndSwapUint64(isResizing, 0, 1) { 269 | truncateErr := f.Truncate(remoteFileSize) 270 | if truncateErr != nil { return truncateErr } 271 | break 272 | } 273 | 274 | runtime.Gosched() 275 | } 276 | 277 | return nil 278 | } 279 | 280 | // performMd5Check 281 | // Optionally perform and md5 check on the transferred file. 282 | func (cli *QuicClient) performMd5Check(sourceMd5 []byte) (*string, error){ 283 | md5StartTime := time.Now() 284 | log.Println("calculating md5 checksum") 285 | 286 | md5Bytes, md5Err := md5.CalculateMD5(cli.dstFile) 287 | if md5Err != nil { return nil, md5Err } 288 | 289 | md5EndTime := time.Now() 290 | md5ElapsedTime := md5EndTime.Sub(md5StartTime) 291 | 292 | log.Printf("calculated md5: %v, source md5: %v\n", md5Bytes, sourceMd5) 293 | log.Println("total elapsed time for md5 calculation:", md5ElapsedTime) 294 | 295 | if ! bytes.Equal(md5Bytes, sourceMd5) { 296 | remErr := os.Remove(cli.dstFile) 297 | if remErr != nil { return nil, remErr } 298 | return nil, errors.New("md5 checksums did not match") 299 | } 300 | 301 | md5File, createFileErr := os.Create(cli.dstFile + ".md5") 302 | if createFileErr != nil { return nil, createFileErr } 303 | defer md5File.Close() 304 | 305 | md5Hex, decodeErr := md5.DeserializeMD5ToHex(md5Bytes) 306 | if decodeErr != nil { return nil, decodeErr } 307 | 308 | _, md5WriteErr := md5File.Write([]byte(md5Hex)) 309 | if md5WriteErr != nil { return nil, md5WriteErr } 310 | 311 | log.Println("md5 check passed, done") 312 | return &cli.dstFile, nil 313 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 5 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 6 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 7 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 8 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 9 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 10 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 11 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 12 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 13 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 14 | github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 15 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 16 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 17 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 18 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 19 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 24 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 25 | github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= 26 | github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= 27 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 28 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 29 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 30 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 31 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 32 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 33 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 34 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 35 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 36 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 37 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 38 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 39 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 40 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 42 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 43 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 44 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 45 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 46 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 47 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 48 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 49 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 50 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 51 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 52 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 53 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 54 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 55 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 56 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 57 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 58 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 59 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 60 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 61 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 62 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 63 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 64 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 65 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 66 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 67 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 68 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 69 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 70 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 71 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 72 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 73 | github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= 74 | github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= 75 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 76 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 77 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 81 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 82 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 83 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 84 | github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= 85 | github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= 86 | github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw= 87 | github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= 88 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 89 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 90 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 91 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 92 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 93 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 94 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 95 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 96 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 97 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 98 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 99 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 100 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 101 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 102 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 103 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 104 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 105 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 106 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 107 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 108 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 109 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 110 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 111 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 112 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 113 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 114 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 115 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 116 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 117 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 118 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 119 | github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= 120 | github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= 121 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 122 | go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= 123 | go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 124 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 125 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 126 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 127 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 128 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 129 | golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= 130 | golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= 131 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 132 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= 133 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 134 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 135 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 136 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 137 | golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= 138 | golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 139 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 140 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 141 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 142 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 143 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 144 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 145 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 146 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 147 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 148 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 149 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 150 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 151 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 152 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 153 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 154 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 155 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 156 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 157 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 159 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 160 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 161 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 162 | golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 164 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 165 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 166 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 167 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 168 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 169 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 170 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 171 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 172 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 173 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 174 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 175 | golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= 176 | golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= 177 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 178 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 179 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 180 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 181 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 182 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 183 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 184 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 185 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 186 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 187 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 188 | google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 189 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 190 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 191 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 192 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 193 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 194 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 195 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 196 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 197 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 198 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 199 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 200 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 201 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 202 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 203 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 204 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 205 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 206 | --------------------------------------------------------------------------------