├── .github ├── dependabot.yml └── workflows │ ├── golangci-lint.yml │ └── go-test.yml ├── .gitignore ├── go.mod ├── rlimit.go ├── tracker.go ├── peercache ├── peercache.go └── peercache_test.go ├── README.md ├── main.go ├── dht.go ├── go.sum ├── .golangci.yml └── LICENSE /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | dhtproxy 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | *~ 28 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '12 4 20 * *' 7 | 8 | jobs: 9 | golangci: 10 | name: lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: golangci-lint 15 | uses: golangci/golangci-lint-action@v2 16 | with: 17 | version: latest 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/die-net/dhtproxy 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/hashicorp/golang-lru v1.0.2 7 | github.com/jackpal/bencode-go v1.0.1 8 | github.com/nictuku/dht v0.0.0-20201226073453-fd1c1dd3d66a 9 | github.com/stretchr/testify v1.9.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 15 | github.com/nictuku/nettools v0.0.0-20150117095333-8867a2107ad3 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/youtube/vitess v2.1.1+incompatible // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /rlimit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "syscall" 7 | ) 8 | 9 | var maxConnections = flag.Int("maxConnections", getRlimitMax(syscall.RLIMIT_NOFILE), "The number of incoming connections to allow.") 10 | 11 | func getRlimitMax(resource int) int { 12 | var rlimit syscall.Rlimit 13 | 14 | if err := syscall.Getrlimit(resource, &rlimit); err != nil { 15 | return 0 16 | } 17 | 18 | return int(rlimit.Max) 19 | } 20 | 21 | func setRlimit(resource, value int) { 22 | rlimit := &syscall.Rlimit{Cur: uint64(value), Max: uint64(value)} 23 | 24 | err := syscall.Setrlimit(resource, rlimit) 25 | if err != nil { 26 | log.Fatalln("Error Setting Rlimit ", err) 27 | } 28 | } 29 | 30 | func setRlimitFromFlags() { 31 | setRlimit(syscall.RLIMIT_NOFILE, *maxConnections) 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: go-test 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '12 4 20 * *' 7 | 8 | jobs: 9 | 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | go: ['1.17', '1.18', '1.19', '1.20', '1.21', '1.22'] 16 | 17 | steps: 18 | - uses: actions/setup-go@v2 19 | with: 20 | go-version: ${{ matrix.go }} 21 | - uses: actions/checkout@v2 22 | - run: go test -v -race -coverprofile=profile.cov ./... 23 | - name: Send coverage 24 | uses: shogo82148/actions-goveralls@v1 25 | with: 26 | path-to-profile: profile.cov 27 | flag-name: Go-${{ matrix.go }} 28 | parallel: true 29 | 30 | # notifies that all test jobs are finished. 31 | finish: 32 | needs: test 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: shogo82148/actions-goveralls@v1 36 | with: 37 | parallel-finished: true 38 | 39 | -------------------------------------------------------------------------------- /tracker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "time" 7 | 8 | bencode "github.com/jackpal/bencode-go" 9 | "github.com/nictuku/dht" 10 | ) 11 | 12 | type TrackerResponse struct { 13 | Interval int64 "interval" //nolint:govet // Bencode-go uses non-comformant struct tags 14 | MinInterval int64 "min interval" //nolint:govet // Bencode-go uses non-comformant struct tags 15 | Complete int "complete" //nolint:govet // Bencode-go uses non-comformant struct tags 16 | Incomplete int "incomplete" //nolint:govet // Bencode-go uses non-comformant struct tags 17 | Peers string "peers" //nolint:govet // Bencode-go uses non-comformant struct tags 18 | } 19 | 20 | func trackerHandler(w http.ResponseWriter, r *http.Request) { 21 | if r.FormValue("compact") != "1" { 22 | http.Error(w, "Only compact protocol supported.", 400) 23 | return 24 | } 25 | 26 | infoHash := dht.InfoHash(r.FormValue("info_hash")) 27 | if len(infoHash) != 20 { 28 | http.Error(w, "Bad info_hash.", 400) 29 | return 30 | } 31 | 32 | response := TrackerResponse{ 33 | Interval: 300, 34 | MinInterval: 60, 35 | } 36 | 37 | peers, ok := peerCache.Get(string(infoHash)) 38 | 39 | dhtNode.Find(infoHash) 40 | 41 | if !ok || len(peers) == 0 { 42 | response.Interval = 30 43 | response.MinInterval = 10 44 | 45 | time.Sleep(5 * time.Second) 46 | 47 | peers, ok = peerCache.Get(string(infoHash)) 48 | } 49 | 50 | if ok && len(peers) > 0 { 51 | response.Incomplete = len(peers) 52 | response.Peers = strings.Join(peers, "") 53 | } 54 | 55 | w.Header().Set("Content-Type", "application/octet-stream") 56 | 57 | if err := bencode.Marshal(w, response); err != nil { 58 | http.Error(w, err.Error(), 500) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /peercache/peercache.go: -------------------------------------------------------------------------------- 1 | package peercache 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | 7 | simplelru "github.com/hashicorp/golang-lru/simplelru" 8 | ) 9 | 10 | type peerList struct { 11 | peers []string 12 | } 13 | 14 | func (p *peerList) Size() int { 15 | return len(p.peers) 16 | } 17 | 18 | type Cache struct { 19 | listLimit int 20 | mu sync.Mutex // This can't be a RWMutex because lru.Get() reorders the list. 21 | lru *simplelru.LRU 22 | } 23 | 24 | func New(size, listLimit int) (*Cache, error) { 25 | lru, err := simplelru.NewLRU(size, nil) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | c := &Cache{ 31 | lru: lru, 32 | listLimit: listLimit, 33 | } 34 | 35 | return c, nil 36 | } 37 | 38 | func (c *Cache) Add(ih string, peers []string) { 39 | c.mu.Lock() 40 | defer c.mu.Unlock() 41 | 42 | list, ok := c.get(ih) 43 | if !ok || list == nil { 44 | list = &peerList{} 45 | } 46 | 47 | peers: 48 | for _, peer := range peers { 49 | // If we already have this peer in the list of peers, don't add it. 50 | for _, p := range list.peers { 51 | if p == peer { 52 | continue peers 53 | } 54 | } 55 | 56 | // Append peers up to listLimit, then randomly replace one. 57 | if len(list.peers) < c.listLimit { 58 | list.peers = append(list.peers, peer) 59 | } else { 60 | list.peers[rand.Intn(len(list.peers))] = peer 61 | } 62 | } 63 | 64 | c.lru.Add(ih, list) 65 | } 66 | 67 | func (c *Cache) Get(ih string) ([]string, bool) { 68 | c.mu.Lock() 69 | defer c.mu.Unlock() 70 | 71 | if list, ok := c.get(ih); ok && list != nil { 72 | return list.peers, true 73 | } 74 | 75 | return nil, false 76 | } 77 | 78 | func (c *Cache) get(ih string) (*peerList, bool) { 79 | p, ok := c.lru.Get(ih) 80 | if !ok { 81 | return nil, false 82 | } 83 | 84 | list, ok := p.(*peerList) 85 | return list, ok 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dhtproxy [![Build Status](https://github.com/die-net/dhtproxy/actions/workflows/go-test.yml/badge.svg)](https://github.com/die-net/dhtproxy/actions/workflows/go-test.yml) [![Coverage Status](https://coveralls.io/repos/github/die-net/dhtproxy/badge.svg?branch=main)](https://coveralls.io/github/die-net/dhtproxy?branch=main) [![Go Report Card](https://goreportcard.com/badge/github.com/die-net/dhtproxy)](https://goreportcard.com/report/github.com/die-net/dhtproxy) 2 | 3 | This is a proxy that accepts BitTorrent tracker [announce requests](https://wiki.theory.org/BitTorrent_Tracker_Protocol) over HTTP and converts them to [mainline DHT](https://en.wikipedia.org/wiki/Mainline_DHT) lookups. This allows clients which are unable to use DHT to bootstrap some peers in a trackerless swarm, after which it can hopefully use [PeX](https://en.wikipedia.org/wiki/Peer_exchange) to find more. 4 | 5 | #### Usage 6 | 7 | * [Install Go](https://golang.org/doc/install) and set up your $GOPATH. 8 | * ```go get github.com/die-net/dhtproxy``` 9 | * ```$GOPATH/bin/dhtproxy -listen=:6969``` 10 | * In your BitTorrent client, add a tracker of http://127.0.0.1:6969/announce for any torrents that you'd like to use dhtproxy. 11 | 12 | #### Limitations 13 | 14 | * Is read-only from the DHT. It doesn't record "announce" information from its clients and share it with either the DHT or other clients of the dhtproxy. If too much of a swarm is behind dhtproxy, the nodes won't be able to find each other. 15 | * All DHT nodes are returned as having an incomplete copy of the torrent data, thus clients will show all DHT nodes as "peers" instead of "seeds". This is cosmetic-only; clients will still be able to use seeds normally when they connect to them. 16 | * Only supports the "compact" tracker protocol, and returns an error if a client tries to use the non-compact protocol. The non-compact protocol returns the peer_id for each peer, which is not available from the DHT. 17 | * Uses [nictuku's DHT implementation](https://github.com/nictuku/dht) whose API isn't well suited to this task. Consequently, dhtproxy may have trouble picking up new additions to the DHT for a particular infohash, and ends up using more memory than would be ideal. A temporary workaround is to restart dhtproxy occasionally. 18 | -------------------------------------------------------------------------------- /peercache/peercache_test.go: -------------------------------------------------------------------------------- 1 | package peercache 2 | 3 | import ( 4 | "math/rand" 5 | "strconv" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var entries = []struct { 13 | key string 14 | peers []string 15 | }{ 16 | {"1", []string{"one"}}, 17 | {"2", []string{"two"}}, 18 | {"3", []string{"three"}}, 19 | {"4", []string{"four"}}, 20 | {"5", []string{"five", "six", "seven", "eight"}}, 21 | } 22 | 23 | func TestCache(t *testing.T) { 24 | c, err := New(10, 4) 25 | assert.NoError(t, err) 26 | 27 | for _, e := range entries { 28 | c.Add(e.key, e.peers) 29 | } 30 | 31 | _, ok := c.Get("missing") 32 | assert.False(t, ok) 33 | 34 | for _, e := range entries { 35 | peers, ok := c.Get(e.key) 36 | if assert.True(t, ok) { 37 | assert.Equal(t, e.peers, peers) 38 | } 39 | } 40 | } 41 | 42 | func TestSize(t *testing.T) { 43 | c, err := New(3, 2) 44 | assert.NoError(t, err) 45 | 46 | for _, e := range entries { 47 | c.Add(e.key, e.peers) 48 | } 49 | 50 | count := 0 51 | for _, e := range entries { 52 | if _, ok := c.Get(e.key); ok { 53 | count++ 54 | } 55 | } 56 | assert.Equal(t, 3, count, "Should only find 3 entries") 57 | } 58 | 59 | func TestLimit(t *testing.T) { 60 | c, err := New(10, 4) 61 | assert.NoError(t, err) 62 | 63 | c.Add("1", []string{"one", "two", "three", "four", "five", "six"}) 64 | peers, ok := c.Get("1") 65 | assert.Equal(t, 4, len(peers), "len(peers) should be 4.") 66 | assert.True(t, ok) 67 | 68 | for _, p := range []string{"one", "two", "three", "four", "five", "six"} { 69 | c.Add("2", []string{p}) 70 | } 71 | peers, ok = c.Get("2") 72 | assert.Equal(t, 4, len(peers), "len(peers) should be 4.") 73 | assert.True(t, ok) 74 | } 75 | 76 | func TestRace(t *testing.T) { 77 | c, err := New(100000, 4) 78 | assert.NoError(t, err) 79 | 80 | wg := sync.WaitGroup{} 81 | for worker := 0; worker < 8; worker++ { 82 | wg.Add(1) 83 | go func() { 84 | testRaceWorker(c) 85 | wg.Done() 86 | }() 87 | } 88 | wg.Wait() 89 | } 90 | 91 | func testRaceWorker(c *Cache) { 92 | peers := []string{"asdf"} 93 | 94 | for n := 0; n < 1000; n++ { 95 | _, _ = c.Get(randKey(100)) 96 | c.Add(randKey(100), peers) 97 | } 98 | } 99 | 100 | func randKey(n int32) string { 101 | return strconv.Itoa(int(rand.Int31n(n))) 102 | } 103 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | _ "net/http/pprof" //nolint:gosec // TODO: Expose this on a different port. 8 | "time" 9 | 10 | "github.com/die-net/dhtproxy/peercache" 11 | ) 12 | 13 | var ( 14 | listenAddr = flag.String("listen", ":6969", "The [IP]:port to listen for incoming HTTP requests.") 15 | debugAddr = flag.String("debugListen", "", "The [IP]:port to listen for pprof HTTP requests. (\"\" = disable)") 16 | dhtPortUDP = flag.Int("dhtPortUDP", 0, "The UDP port number to use for DHT requests") 17 | dhtResetInterval = flag.Duration("dhtResetInterval", time.Hour, "How often to reset the DHT client (0 = disable)") 18 | targetNumPeers = flag.Int("targetNumPeers", 8, "The number of DHT peers to try to find for a given node") 19 | peerCacheSize = flag.Int("peerCacheSize", 16384, "The max number of infohashes to keep a list of peers for.") 20 | maxWant = flag.Int("maxWant", 200, "The largest number of peers to return in one request.") 21 | 22 | peerCache *peercache.Cache 23 | dhtNode *DhtNode 24 | ) 25 | 26 | func main() { 27 | flag.Parse() 28 | 29 | setRlimitFromFlags() 30 | 31 | var err error 32 | peerCache, err = peercache.New(*peerCacheSize, *maxWant) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | dhtNode, err = NewDhtNode(*dhtPortUDP, *targetNumPeers, *dhtResetInterval, peerCache) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | if *debugAddr != "" { 43 | // Serve /debug/pprof/* on default mux 44 | go func() { 45 | srv := &http.Server{ 46 | Addr: *debugAddr, 47 | ReadTimeout: 10 * time.Second, 48 | WriteTimeout: 30 * time.Second, 49 | IdleTimeout: 240 * time.Second, 50 | Handler: http.DefaultServeMux, 51 | } 52 | log.Fatal(srv.ListenAndServe()) 53 | }() 54 | } 55 | 56 | mux := http.NewServeMux() 57 | mux.HandleFunc("/robots.txt", robotsDisallowHandler) 58 | mux.HandleFunc("/announce", trackerHandler) 59 | 60 | srv := &http.Server{ 61 | Addr: *listenAddr, 62 | ReadTimeout: 10 * time.Second, 63 | WriteTimeout: 30 * time.Second, 64 | IdleTimeout: 240 * time.Second, 65 | Handler: mux, 66 | } 67 | log.Fatal(srv.ListenAndServe()) 68 | } 69 | 70 | func robotsDisallowHandler(w http.ResponseWriter, r *http.Request) { 71 | w.Header().Set("Content-Type", "text/plain") 72 | _, _ = w.Write([]byte("User-agent: *\nDisallow: /\n")) 73 | } 74 | -------------------------------------------------------------------------------- /dht.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/die-net/dhtproxy/peercache" 8 | 9 | "github.com/nictuku/dht" 10 | ) 11 | 12 | func init() { 13 | // Current list of bootstrap nodes from: 14 | // https://git.deluge-torrent.org/deluge/tree/deluge/core/preferencesmanager.py#n274 15 | dht.DefaultConfig.DHTRouters = "router.bittorrent.com:6881,router.utorrent.com:6881,router.bitcomet.com:6881,dht.transmissionbt.com:6881,dht.aelitis.com:6881" 16 | 17 | // Don't rate-limit (by silently dropping) packets by default. 18 | // Assume we want the info. 19 | dht.DefaultConfig.RateLimit = -1 20 | 21 | dht.RegisterFlags(nil) 22 | } 23 | 24 | type DhtNode struct { 25 | port int 26 | numTargetPeers int 27 | node *dht.DHT 28 | c *peercache.Cache 29 | resetter *time.Ticker 30 | } 31 | 32 | func NewDhtNode(port, numTargetPeers int, resetInterval time.Duration, c *peercache.Cache) (*DhtNode, error) { 33 | d := &DhtNode{ 34 | port: port, 35 | numTargetPeers: numTargetPeers, 36 | c: c, 37 | } 38 | 39 | if err := d.Reset(); err != nil { 40 | return nil, err 41 | } 42 | 43 | if resetInterval > 0 { 44 | d.resetter = time.NewTicker(resetInterval) 45 | go d.doResets() 46 | } 47 | 48 | return d, nil 49 | } 50 | 51 | func (d *DhtNode) Reset() error { 52 | d.stop() 53 | 54 | conf := dht.NewConfig() 55 | conf.Port = d.port 56 | conf.NumTargetPeers = d.numTargetPeers 57 | 58 | node, err := dht.New(conf) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | d.node = node 64 | 65 | go func() { _ = d.node.Run() }() 66 | 67 | go d.drainResults(d.c) 68 | 69 | return nil 70 | } 71 | 72 | func (d *DhtNode) doResets() { 73 | for range d.resetter.C { 74 | if err := d.Reset(); err != nil { 75 | log.Fatal("DHT reset failed: ", err) 76 | } 77 | } 78 | } 79 | 80 | func (d *DhtNode) drainResults(c *peercache.Cache) { 81 | for r := range d.node.PeersRequestResults { 82 | for ih, peers := range r { 83 | c.Add(string(ih), peers) 84 | } 85 | } 86 | } 87 | 88 | func (d *DhtNode) Find(ih dht.InfoHash) { 89 | // TODO: This is still racy vs Reset() 90 | if d.node != nil { 91 | timer := time.AfterFunc(time.Minute, func() { 92 | log.Fatal("d.node.PeersRequest() took longer than a minute.") 93 | }) 94 | defer timer.Stop() 95 | 96 | d.node.PeersRequest(string(ih), false) 97 | } 98 | } 99 | 100 | func (d *DhtNode) Stop() { 101 | if d.resetter != nil { 102 | d.resetter.Stop() 103 | d.resetter = nil 104 | } 105 | d.stop() 106 | } 107 | 108 | func (d *DhtNode) stop() { 109 | if d.node != nil { 110 | timer := time.AfterFunc(time.Minute, func() { 111 | log.Fatal("d.node.Stop() took longer than a minute.") 112 | }) 113 | defer timer.Stop() 114 | 115 | d.node.Stop() 116 | d.node = nil 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 5 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 6 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 7 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 8 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 9 | github.com/jackpal/bencode-go v0.0.0-20180813173944-227668e840fa/go.mod h1:5FSBQ74yhCl5oQ+QxRPYzWMONFnxbL68/23eezsBI5c= 10 | github.com/jackpal/bencode-go v1.0.1 h1:zaP3EoaaFaVg1gUfXdpZfvmta/iQpIQQuP8bJkUbGe0= 11 | github.com/jackpal/bencode-go v1.0.1/go.mod h1:5FSBQ74yhCl5oQ+QxRPYzWMONFnxbL68/23eezsBI5c= 12 | github.com/nictuku/dht v0.0.0-20201226073453-fd1c1dd3d66a h1:jCE9Fk4S0dnR133jsymN8ED3/c6g+8m415Z6WmmqRTc= 13 | github.com/nictuku/dht v0.0.0-20201226073453-fd1c1dd3d66a/go.mod h1:OQ6jH4HHRpskJXlLCOeB1ckBSXXD4PmnPaIaFSi1dtI= 14 | github.com/nictuku/nettools v0.0.0-20150117095333-8867a2107ad3 h1:q6P6rwaWsdWQlaDt0DYPtmpj37fq2S4/IrZQO0zx488= 15 | github.com/nictuku/nettools v0.0.0-20150117095333-8867a2107ad3/go.mod h1:m19Kd92g5zm0IuGkdZo/OHBSPp9mqGlevOJu00nBoYs= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 20 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 21 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 25 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 26 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 27 | github.com/youtube/vitess v2.1.1+incompatible h1:SE+P7DNX/jw5RHFs5CHRhZQjq402EJFCD33JhzQMdDw= 28 | github.com/youtube/vitess v2.1.1+incompatible/go.mod h1:hpMim5/30F1r+0P8GGtB29d0gWHr0IZ5unS+CG0zMx8= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 33 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Config file for golangci-lint 2 | 3 | # options for analysis running 4 | run: 5 | # timeout for analysis, e.g. 30s, 5m, default is 1m. This has to be long 6 | # enough to handle an empty cache on a slow machine. 7 | timeout: 2m 8 | 9 | # include test files or not, default is true 10 | tests: true 11 | 12 | # Run "golangci-lint linters" for a list of available linters. Don't enable 13 | # any linters here, or they can't be disabled on the commandline. 14 | linters: 15 | disable-all: true 16 | enable: 17 | - asciicheck 18 | - bodyclose 19 | - err113 20 | - errcheck 21 | - errorlint 22 | - exhaustive 23 | - exportloopref 24 | - gocritic 25 | - gofumpt 26 | - goimports 27 | - goprintffuncname 28 | - gosec 29 | - gosimple 30 | - govet 31 | - ineffassign 32 | - makezero 33 | - misspell 34 | - nakedret 35 | - noctx 36 | - nolintlint 37 | - prealloc 38 | - predeclared 39 | - revive 40 | - rowserrcheck 41 | - sqlclosecheck 42 | - staticcheck 43 | - tparallel 44 | - typecheck 45 | - unconvert 46 | - unused 47 | - wastedassign 48 | 49 | fast: false 50 | 51 | # all available settings of specific linters 52 | linters-settings: 53 | errcheck: 54 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 55 | # default is false: such cases aren't reported by default. 56 | check-blank: false 57 | 58 | # List of functions to exclude from checking, where each entry is a single function to exclude. 59 | # See https://github.com/kisielk/errcheck#excluding-functions for details. 60 | # exclude-functions: 61 | exhaustive: 62 | # If enum-like constants don't use all cases in a switch statement, 63 | # consider a default good enough. 64 | default-signifies-exhaustive: true 65 | gocritic: 66 | # all checks list: https://github.com/go-critic/checkers 67 | # Enable all checks by enabling all tags, then disable a few. 68 | enabled-tags: 69 | - diagnostic 70 | - experimental 71 | - opinionated 72 | - performance 73 | - style 74 | disabled-checks: 75 | # Don't be aggressive with comments. 76 | - commentedOutCode 77 | # Catches legit uses of case checking. 78 | - equalFold 79 | # Many defers can be skipped when exiting. Could fix this with a log.Fatal replacement. 80 | - exitAfterDefer 81 | # Disagree with the style recommendations for these three. 82 | - ifElseChain 83 | - octalLiteral 84 | - unnamedResult 85 | - filepathJoin 86 | - tooManyResultsChecker 87 | settings: 88 | captLocal: 89 | paramsOnly: false 90 | hugeParam: 91 | # Allowing 512 byte parameters. 92 | sizeThreshold: 512 93 | nestingReduce: 94 | # How many nested blocks before suggesting early exit. 95 | bodyWidth: 4 96 | rangeExprCopy: 97 | # Avoid copying arrays larger than this in range statement. 98 | sizeThreshold: 512 99 | rangeValCopy: 100 | # Avoid copying range values larger than this on every iteration. 101 | sizeThreshold: 128 102 | truncateCmp: 103 | skipArchDependent: false 104 | underef: 105 | skipRecvDeref: false 106 | goimports: 107 | # put imports beginning with prefix after 3rd-party packages; 108 | # it's a comma-separated list of prefixes 109 | local-prefixes: github.com/die-net/ 110 | govet: 111 | # Enable most of the non-default linters too. 112 | enable: 113 | - asmdecl 114 | - assign 115 | - atomic 116 | - atomicalign 117 | - bools 118 | - buildtag 119 | - cgocall 120 | - composite 121 | - copylock 122 | - durationcheck 123 | - errorsas 124 | - findcall 125 | - httpresponse 126 | - ifaceassert 127 | - loopclosure 128 | - lostcancel 129 | - nilfunc 130 | - nilness 131 | - printf 132 | - shadow 133 | - shift 134 | - sortslice 135 | - stdmethods 136 | - stringintconv 137 | - structtag 138 | - testinggoroutine 139 | - tests 140 | - unmarshal 141 | - unreachable 142 | - unsafeptr 143 | - unusedresult 144 | disable: 145 | # We need to fix a few tests that rely on this first. 146 | - deepequalerrors 147 | nakedret: 148 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 149 | max-func-lines: 6 150 | nolintlint: 151 | allow-unused: false 152 | allow-leading-space: false 153 | require-explanation: true 154 | revive: 155 | # Show all issues, not just those with a high confidence 156 | confidence: 0.0 157 | rules: 158 | - name: atomic 159 | # - name: bare-return 160 | - name: blank-imports 161 | # - name: confusing-naming 162 | # - name: confusing-results 163 | - name: constant-logical-expr 164 | - name: context-as-argument 165 | - name: context-keys-type 166 | # - name: deep-exit 167 | # - name: defer 168 | - name: dot-imports 169 | # - name: early-return 170 | # - name: empty-block 171 | - name: error-naming 172 | - name: error-return 173 | - name: error-strings 174 | - name: errorf 175 | - name: exported 176 | # - name: get-return 177 | - name: identical-branches 178 | - name: if-return 179 | - name: increment-decrement 180 | - name: indent-error-flow 181 | # - name: import-shadowing 182 | - name: modifies-parameter 183 | # - name: modifies-value-receiver 184 | - name: package-comments 185 | - name: range 186 | - name: range-val-in-closure 187 | - name: range-val-address 188 | - name: receiver-naming 189 | # - name: redefines-builtin-id 190 | - name: string-of-int 191 | # - name: struct-tag 192 | - name: superfluous-else 193 | - name: time-naming 194 | # - name: var-naming 195 | # - name: var-declaration 196 | - name: unconditional-recursion 197 | # - name: unexported-naming 198 | - name: unexported-return 199 | - name: unnecessary-stmt 200 | - name: unreachable-code 201 | # - name: unused-parameter 202 | # - name: unused-receiver 203 | - name: waitgroup-by-value 204 | 205 | # output configuration options 206 | output: 207 | # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" 208 | formats: 209 | - format: line-number 210 | 211 | # print lines of code with issue, default is true 212 | print-issued-lines: false 213 | 214 | # sorts results by: filepath, line and column 215 | sort-results: true 216 | 217 | issues: 218 | # List of regexps of issue texts to exclude, empty list by default. 219 | # But independently from this option we use default exclude patterns, 220 | # it can be disabled by `exclude-use-default: false`. To list all 221 | # excluded by default patterns execute `golangci-lint run --help` 222 | exclude: 223 | # revive: We don't require comments, only that they be properly formatted 224 | - should have( a package)? comment 225 | 226 | # revive: Don't force variable scope changes 227 | - (indent-error-flow|superfluous-else).*drop this else and outdent its block .move short variable declaration to its own line if necessary. 228 | 229 | # govet: Allow the most common form of shadowing 230 | - declaration of .err. shadows declaration 231 | 232 | # gocritic: We use named Err return as part of our defer handling pattern. 233 | - captLocal. .Err. should not be capitalized 234 | 235 | # govet: Allow an unused noCopy struct field to disallow copying 236 | - .noCopy. is unused 237 | 238 | # gosec: Let errcheck complain about this instead 239 | - G104. Errors unhandled 240 | 241 | # gosec: All URLs are variable in our code; this isn't useful 242 | - G107. Potential HTTP request made with variable url 243 | 244 | # gosec: Complaining about every exec.Command() is annoying; we'll audit them 245 | - G204. Subprocess launching should be audited 246 | - G204. Subprocess launched with variable 247 | - G204. Subprocess launched with function call as argument or cmd arguments 248 | 249 | # gosec: Too many false positives for legit uses of files and directories 250 | - G301. Expect directory permissions to be 0750 or less 251 | - G302. Expect file permissions to be 0600 or less 252 | - G306. Expect WriteFile permissions to be 0600 or less 253 | 254 | # gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' 255 | - G304. Potential file inclusion via variable 256 | 257 | # gosec: Complaining about every use of math/rand is annoying. 258 | - G404. Use of weak random number generator 259 | 260 | # gosec: We're allowing SHA1 for now, but MD5, DES, and RC4 need to be audited 261 | - G401. Use of weak cryptographic primitive 262 | - G505. Blocklisted import crypto/sha1. weak cryptographic primitive 263 | 264 | # Exclude some linters from running on template-generated code, where we 265 | # can't fix the output. 266 | exclude-rules: 267 | 268 | # Independently from option `exclude` we use default exclude patterns, 269 | # it can be disabled by this option. To list all 270 | # excluded by default patterns execute `golangci-lint run --help`. 271 | # Default value for this option is true. 272 | exclude-use-default: false 273 | 274 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 275 | max-issues-per-linter: 0 276 | 277 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 278 | max-same-issues: 0 279 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------