├── model ├── thread.go ├── peer.go └── post.go ├── db ├── config.go ├── db.go └── db_pq.go ├── webroot └── static │ ├── override.css │ └── style.css ├── signals ├── signals.go └── signals_unix.go ├── .gitignore ├── storage ├── store.go └── file.go ├── js └── main.go ├── gossip ├── gossip.go ├── http.go └── http_feed.go ├── network └── lookup.go ├── util └── move.go ├── Makefile ├── readme.md ├── LICENSE ├── api └── api.go ├── go.mod ├── torrent └── grabber.go ├── main.go ├── templates └── base │ └── index.html.tmpl ├── web └── web.go └── go.sum /model/thread.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ThreadInfo struct { 4 | } 5 | -------------------------------------------------------------------------------- /db/config.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | type Config struct { 4 | URL string 5 | } 6 | -------------------------------------------------------------------------------- /webroot/static/override.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | put your custom css here 5 | */ 6 | -------------------------------------------------------------------------------- /signals/signals.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | type ReloadFunc func() 4 | type ExitFunc func() 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.key 2 | *.db 3 | .vscode 4 | node_modules 5 | *.min.js 6 | *.min.css 7 | file_storage 8 | *.dat 9 | *.wasm 10 | bitchand -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/majestrate/bitchan/model" 5 | ) 6 | 7 | type Facade interface { 8 | MigrateAll() error 9 | GetThreads(limit int) ([]model.ThreadInfo, error) 10 | } 11 | -------------------------------------------------------------------------------- /storage/store.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | type Store interface { 4 | SetRoot(d string) 5 | GetRoot() string 6 | GetAllTorrents() []string 7 | } 8 | 9 | func NewStorage() Store { 10 | return newFileStore() 11 | } 12 | -------------------------------------------------------------------------------- /js/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | var log = logrus.New() 8 | 9 | func main() { 10 | log.WithFields(logrus.Fields{ 11 | "state": "starting", 12 | }).Info("bitchan main start") 13 | } 14 | -------------------------------------------------------------------------------- /webroot/static/style.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | html, body { 4 | background-color: #ddd; 5 | color: #333; 6 | } 7 | 8 | .link { 9 | color: #0daabb; 10 | font-style: italic; 11 | } 12 | 13 | .cite { 14 | background-color: #aaa; 15 | } 16 | -------------------------------------------------------------------------------- /model/peer.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Peer contains info about a peer 4 | type Peer struct { 5 | URL string `bencode:"bitchan-peer-url"` 6 | } 7 | 8 | // PeerList maps hostname to peer url 9 | type PeerList struct { 10 | Peers map[string]Peer `bencode:"bitchan-peers"` 11 | Time int64 `bencode:"bitchan-time"` 12 | } 13 | -------------------------------------------------------------------------------- /gossip/gossip.go: -------------------------------------------------------------------------------- 1 | package gossip 2 | 3 | import ( 4 | "github.com/majestrate/bitchan/model" 5 | "net/url" 6 | ) 7 | 8 | type Gossiper interface { 9 | BroadcastLocalPost(*model.Post) 10 | AddNeighboor(u *url.URL) bool 11 | Stop() 12 | Bootstrap() 13 | ForEachPeer(func(model.Peer)) 14 | } 15 | 16 | func NewServer(hostname string) Gossiper { 17 | return newHttpGossiper(hostname) 18 | } 19 | -------------------------------------------------------------------------------- /network/lookup.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | func init() { 9 | net.DefaultResolver.PreferGo = true 10 | } 11 | 12 | func LookupSelf() (string, error) { 13 | ips, err := net.LookupIP("localhost.loki") 14 | if err != nil { 15 | return "", err 16 | } 17 | names, err := net.LookupAddr(ips[0].String()) 18 | if err != nil { 19 | return "", err 20 | } 21 | return strings.TrimSuffix(names[0], "."), nil 22 | } 23 | -------------------------------------------------------------------------------- /util/move.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | // Move a file from src to dst 9 | func Move(src, dst string) error { 10 | 11 | s_file, err := os.Open(src) 12 | if err != nil { 13 | return err 14 | } 15 | d_file, err := os.Create(dst) 16 | if err != nil { 17 | s_file.Close() 18 | return err 19 | } 20 | defer func() { 21 | d_file.Sync() 22 | d_file.Close() 23 | s_file.Close() 24 | os.Remove(src) 25 | }() 26 | _, err = io.Copy(d_file, s_file) 27 | return err 28 | } 29 | -------------------------------------------------------------------------------- /db/db_pq.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "database/sql" 5 | _ "github.com/lib/pq" 6 | "github.com/majestrate/bitchan/model" 7 | ) 8 | 9 | type pqFacade struct { 10 | db *sql.DB 11 | } 12 | 13 | func (self *pqFacade) MigrateAll() error { 14 | // TODO: this 15 | return nil 16 | } 17 | 18 | func (self *pqFacade) GetThreads(limit int) ([]model.ThreadInfo, error) { 19 | // TODO: this 20 | return nil, nil 21 | } 22 | 23 | func NewPQ(conf Config) (*pqFacade, error) { 24 | db, err := sql.Open("postgres", conf.URL) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &pqFacade{db: db}, nil 29 | } 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPO := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 2 | 3 | BITCHAN_WASM := $(REPO)/webroot/static/bitchan.wasm 4 | WASM_EXEC_JS := $(REPO)/webroot/static/wasm_exec.min.js 5 | GOROOT = $(shell go env GOROOT) 6 | EXE = bitchand 7 | 8 | all: mistake 9 | 10 | mistake: $(BITCHAN_WASM) $(EXE) 11 | 12 | $(EXE): 13 | go build -o $(EXE) 14 | 15 | $(BITCHAN_WASM): 16 | GOOS=js GOARCH=wasm go build -o '$(BITCHAN_WASM)' github.com/majestrate/bitchan/js 17 | cp '$(GOROOT)/misc/wasm/wasm_exec.js' '$(WASM_EXEC_JS)' 18 | 19 | clean: repent 20 | 21 | repent: 22 | rm -f '$(BITCHAN_WASM)' '$(WASM_EXEC_JS)' '$(EXE)' 23 | go clean -a 24 | -------------------------------------------------------------------------------- /signals/signals_unix.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | ) 8 | 9 | var chnl = make(chan os.Signal) 10 | 11 | var onExit ExitFunc 12 | var onReload ReloadFunc 13 | 14 | func SetupSignals(reload ReloadFunc, exit ExitFunc) { 15 | onExit = exit 16 | onReload = reload 17 | signal.Notify(chnl, os.Interrupt, syscall.SIGHUP) 18 | } 19 | 20 | func Wait() { 21 | for { 22 | sig := <-chnl 23 | switch sig { 24 | case os.Interrupt: 25 | if onExit != nil { 26 | onExit() 27 | return 28 | } 29 | case syscall.SIGHUP: 30 | if onReload != nil { 31 | onReload() 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /storage/file.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | type fileStorage struct { 9 | RootDir string 10 | } 11 | 12 | func (f *fileStorage) SetRoot(path string) { 13 | f.RootDir = path 14 | os.Mkdir(f.RootDir, os.FileMode(0700)) 15 | } 16 | 17 | func (f *fileStorage) GetRoot() string { 18 | return f.RootDir 19 | } 20 | 21 | func (f *fileStorage) GetAllTorrents() []string { 22 | matches, err := filepath.Glob(filepath.Join(f.RootDir, "*.torrent")) 23 | if err != nil { 24 | return []string{} 25 | } 26 | return matches 27 | } 28 | 29 | func newFileStore() *fileStorage { 30 | return &fileStorage{} 31 | } 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # bitchan 2 | 3 | bittorrent imageboard 4 | 5 | (this software is experimental) 6 | 7 | ## build deps 8 | 9 | * go 1.13.x 10 | * GNU Make 11 | * git 12 | * postgresql 13 | 14 | ## building 15 | 16 | initial build: 17 | 18 | $ git clone https://github.com/majestrate/bitchan 19 | $ cd bitchan 20 | $ make 21 | 22 | running: 23 | 24 | $ ./bitchand your.domain.tld 25 | 26 | then go to `http://your.domain.tld:8800/` 27 | 28 | add the bootstrap node (from your server server): 29 | 30 | $ curl http://i2p.rocks:8800/bitchan/v1/peer-with-me?host=your.domain.tld 31 | 32 | ## development 33 | 34 | building: 35 | 36 | $ make mistake 37 | 38 | clean: 39 | 40 | $ make repent 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Jeff Becker 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/anacrolix/torrent/bencode" 5 | "github.com/anacrolix/torrent/metainfo" 6 | "github.com/majestrate/bitchan/gossip" 7 | "github.com/majestrate/bitchan/storage" 8 | "github.com/majestrate/bitchan/torrent" 9 | "os" 10 | ) 11 | 12 | type ApiServer struct { 13 | Torrent *torrent.Grabber 14 | Storage storage.Store 15 | Gossip gossip.Gossiper 16 | } 17 | 18 | func (a *ApiServer) Stop() { 19 | a.Torrent.Stop() 20 | a.Gossip.Stop() 21 | } 22 | 23 | func (a *ApiServer) MakeTorrent(rootf, outf string) (string, error) { 24 | var mi metainfo.MetaInfo 25 | mi.Announce = "udp://opentracker.i2p.rocks:6969/announce" 26 | var miInfo metainfo.Info 27 | miInfo.PieceLength = 128 * 1024 28 | err := miInfo.BuildFromFilePath(rootf) 29 | if err != nil { 30 | return "", err 31 | } 32 | mi.InfoBytes, err = bencode.Marshal(miInfo) 33 | if err != nil { 34 | return "", err 35 | } 36 | f, err := os.Create(outf) 37 | if err != nil { 38 | return "", err 39 | } 40 | infohash_hex := mi.HashInfoBytes().HexString() 41 | defer f.Close() 42 | return infohash_hex, mi.Write(f) 43 | } 44 | 45 | func NewAPI() *ApiServer { 46 | return &ApiServer{} 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/majestrate/bitchan 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/RoaringBitmap/roaring v0.4.23 // indirect 7 | github.com/anacrolix/dht/v2 v2.6.1 // indirect 8 | github.com/anacrolix/go-libutp v1.0.3 // indirect 9 | github.com/anacrolix/log v0.7.0 10 | github.com/anacrolix/multiless v0.0.0-20200413040533-acfd16f65d5d // indirect 11 | github.com/anacrolix/torrent v1.15.2 12 | github.com/elliotchance/orderedmap v1.2.2 // indirect 13 | github.com/gin-gonic/gin v1.7.0 14 | github.com/golang/protobuf v1.4.2 // indirect 15 | github.com/huandu/xstrings v1.3.1 // indirect 16 | github.com/jcmturner/gokrb5/v8 v8.3.0 // indirect 17 | github.com/lib/pq v1.6.0 18 | github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 20 | github.com/modern-go/reflect2 v1.0.1 // indirect 21 | github.com/mschoch/smat v0.2.0 // indirect 22 | github.com/sirupsen/logrus v1.6.0 23 | github.com/tinylib/msgp v1.1.2 // indirect 24 | github.com/zeebo/bencode v1.0.0 25 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect 26 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect 27 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect 28 | google.golang.org/protobuf v1.24.0 // indirect 29 | gopkg.in/yaml.v2 v2.3.0 // indirect 30 | lukechampine.com/blake3 v1.0.0 31 | ) 32 | -------------------------------------------------------------------------------- /model/post.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "github.com/zeebo/bencode" 6 | "io" 7 | "lukechampine.com/blake3" 8 | ) 9 | 10 | type PostInfo struct { 11 | PostedAt int64 `json:"posted_at"` 12 | InfoHash string `json:"infohash_hex"` 13 | Name string `json:"name",omit-empty` 14 | } 15 | 16 | type Post struct { 17 | MetaInfoURL string `bencode:"bitchan-metainfo-url"` 18 | MetaInfoHash string `bencode:"bitchan-infohash-hex",omit-empty` 19 | Version string `bencode:"bitchan-version",omit-empty` 20 | PostedAt int64 `bencode:"bitchan-posted-at"` 21 | PubKey string `bencode:"bitchan-poster-pubkey"` 22 | Signature string `bencode:"z",omit-empty` 23 | } 24 | 25 | func (p *Post) ToInfo() PostInfo { 26 | return PostInfo{ 27 | PostedAt: p.PostedAt, 28 | InfoHash: p.MetaInfoHash, 29 | } 30 | } 31 | 32 | func (p *Post) WriteToFile(w io.Writer) error { 33 | enc := bencode.NewEncoder(w) 34 | return enc.Encode(p) 35 | } 36 | 37 | func (p *Post) ReadFromFile(r io.Reader) error { 38 | dec := bencode.NewDecoder(r) 39 | return dec.Decode(p) 40 | } 41 | 42 | func (p *Post) hashme() []byte { 43 | h := blake3.New(32, nil) 44 | enc := bencode.NewEncoder(h) 45 | enc.Encode(p) 46 | return h.Sum(nil) 47 | } 48 | 49 | func (p *Post) Verify() bool { 50 | sig := []byte(p.Signature) 51 | p.Signature = "" 52 | digest := p.hashme() 53 | k := ed25519.PublicKey([]byte(p.PubKey)) 54 | if len(k) == 0 { 55 | return false 56 | } 57 | return ed25519.Verify(k, digest, sig) 58 | } 59 | 60 | func (p *Post) Sign(sk ed25519.PrivateKey) { 61 | p.PubKey = string(sk.Public().(ed25519.PublicKey)[:]) 62 | p.Signature = "" 63 | digest := p.hashme() 64 | p.Signature = string(ed25519.Sign(sk, digest)) 65 | } 66 | 67 | type PostResponse struct { 68 | Response string `bencode:"bitchan-post-response"` 69 | Version string `bencode:"bitchan-version",omit-empty` 70 | Time int64 `bencode:"bitchan-time"` 71 | } 72 | 73 | const DefaultPostVersion = "1.0" 74 | -------------------------------------------------------------------------------- /torrent/grabber.go: -------------------------------------------------------------------------------- 1 | package torrent 2 | 3 | import ( 4 | "errors" 5 | alog "github.com/anacrolix/log" 6 | "github.com/anacrolix/torrent" 7 | "github.com/anacrolix/torrent/metainfo" 8 | "github.com/majestrate/bitchan/gossip" 9 | "github.com/majestrate/bitchan/storage" 10 | "github.com/sirupsen/logrus" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "path/filepath" 15 | ) 16 | 17 | var log = logrus.New() 18 | 19 | var ErrNotAdded = errors.New("torrent not addded") 20 | 21 | type Grabber struct { 22 | Client *torrent.Client 23 | gossiper gossip.Gossiper 24 | store storage.Store 25 | } 26 | 27 | func (g *Grabber) ForEachSeed(visit func(*torrent.Torrent)) { 28 | for _, t := range g.Client.Torrents() { 29 | if t.Seeding() { 30 | visit(t) 31 | } 32 | } 33 | } 34 | 35 | func (g *Grabber) Grab(metainfoURL string) error { 36 | log.WithFields(logrus.Fields{ 37 | "url": metainfoURL, 38 | }).Info("grabbing metainfo") 39 | resp, err := http.Get(metainfoURL) 40 | if err != nil { 41 | log.WithFields(logrus.Fields{ 42 | "url": metainfoURL, 43 | "err": err, 44 | }).Error("grabbing metainfo failed") 45 | return err 46 | } 47 | defer resp.Body.Close() 48 | mi, err := metainfo.Load(resp.Body) 49 | if err != nil { 50 | log.WithFields(logrus.Fields{ 51 | "url": metainfoURL, 52 | "err": err, 53 | }).Error("reading metainfo failed") 54 | return err 55 | } 56 | t, err := g.Client.AddTorrent(mi) 57 | if err != nil { 58 | log.WithFields(logrus.Fields{ 59 | "url": metainfoURL, 60 | "err": err, 61 | }).Error("failed to add torrent") 62 | return err 63 | } 64 | 65 | log.WithFields(logrus.Fields{ 66 | "url": metainfoURL, 67 | }).Info("download starting") 68 | u, _ := url.Parse(metainfoURL) 69 | f, err := os.Create(filepath.Join(g.store.GetRoot(), filepath.Base(u.Path))) 70 | if err == nil { 71 | defer f.Close() 72 | mi.Write(f) 73 | } 74 | if t.Seeding() { 75 | log.WithFields(logrus.Fields{ 76 | "url": metainfoURL, 77 | "infohash": t.InfoHash().HexString(), 78 | }).Info("seeding") 79 | }else { 80 | log.WithFields(logrus.Fields{ 81 | "url": metainfoURL, 82 | "infohash": t.InfoHash().HexString(), 83 | }).Info("downloading") 84 | t.DownloadAll() 85 | } 86 | return nil 87 | } 88 | 89 | func (g *Grabber) Stop() { 90 | g.Client.Close() 91 | } 92 | 93 | func NewGrabber(st storage.Store, g gossip.Gossiper) *Grabber { 94 | cfg := torrent.NewDefaultClientConfig() 95 | cfg.DataDir = st.GetRoot() 96 | cfg.Seed = true 97 | cfg.Debug = false 98 | cfg.Logger = alog.Discard 99 | t, _ := torrent.NewClient(cfg) 100 | return &Grabber{ 101 | Client: t, 102 | gossiper: g, 103 | store: st, 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /gossip/http.go: -------------------------------------------------------------------------------- 1 | package gossip 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/base64" 6 | "github.com/majestrate/bitchan/model" 7 | "github.com/sirupsen/logrus" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "sync" 12 | ) 13 | 14 | type httpGossiper struct { 15 | neighboors sync.Map 16 | hostname string 17 | } 18 | 19 | func (g *httpGossiper) forEachFeed(visit func(*HttpFeed)) { 20 | g.neighboors.Range(func(_, v interface{}) bool { 21 | visit(v.(*HttpFeed)) 22 | return true 23 | }) 24 | } 25 | 26 | func (g *httpGossiper) Stop() { 27 | g.forEachFeed(func(f *HttpFeed) { 28 | f.Stop() 29 | }) 30 | } 31 | 32 | func (g *httpGossiper) ForEachPeer(visit func(model.Peer)) { 33 | g.forEachFeed(func(feed *HttpFeed) { 34 | visit(model.Peer{ 35 | URL: feed.u.String(), 36 | }) 37 | }) 38 | } 39 | 40 | func (g *httpGossiper) Bootstrap() { 41 | g.forEachFeed(func(feed *HttpFeed) { 42 | go func() { 43 | l := feed.FetchNeighboors() 44 | if l == nil { 45 | return 46 | } 47 | for name, peer := range l.Peers { 48 | u, _ := url.Parse(peer.URL) 49 | if u == nil { 50 | continue 51 | } 52 | if u.Host == name { 53 | g.AddNeighboor(u) 54 | } 55 | } 56 | }() 57 | }) 58 | } 59 | 60 | func newHttpGossiper(hostname string) *httpGossiper { 61 | return &httpGossiper{ 62 | hostname: hostname, 63 | } 64 | } 65 | 66 | func (g *httpGossiper) BroadcastLocalPost(p *model.Post) { 67 | g.forEachFeed(func(feed *HttpFeed) { 68 | go feed.Publish(p) 69 | }) 70 | } 71 | 72 | func (g *httpGossiper) AddNeighboor(n *url.URL) bool { 73 | _, has := g.neighboors.Load(n.Host) 74 | if has { 75 | log.WithFields(logrus.Fields{ 76 | "host": n.Host, 77 | }).Error("already have neighboor") 78 | return false 79 | } else { 80 | // read pubkey 81 | pkurl, _ := url.Parse(n.String()) 82 | pkurl.Path = "/bitchan/v1/pubkey" 83 | resp, err := http.Get(pkurl.String()) 84 | if err != nil { 85 | 86 | log.WithFields(logrus.Fields{ 87 | "url": pkurl.String(), 88 | "error": err, 89 | }).Error("failed to do request") 90 | return false 91 | } 92 | defer resp.Body.Close() 93 | dec := base64.NewDecoder(base64.StdEncoding, resp.Body) 94 | pk := make(ed25519.PublicKey, 32) 95 | _, err = io.ReadFull(dec, pk[:]) 96 | if err != nil { 97 | log.WithFields(logrus.Fields{ 98 | "url": pkurl.String(), 99 | "error": err, 100 | }).Error("failed to read pubkey") 101 | return false 102 | } 103 | g.neighboors.Store(n.Host, newHttpFeed(n, pk)) 104 | addme, _ := url.Parse(n.String()) 105 | addme.Path = "/bitchan/v1/peer?host=" + g.hostname 106 | _, err = http.Get(addme.String()) 107 | if err == nil { 108 | log.WithFields(logrus.Fields{ 109 | "host": n.Host, 110 | }).Info("added neighboor") 111 | } 112 | return true 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/majestrate/bitchan/api" 5 | "github.com/majestrate/bitchan/db" 6 | "github.com/majestrate/bitchan/gossip" 7 | "github.com/majestrate/bitchan/model" 8 | "github.com/majestrate/bitchan/network" 9 | "github.com/majestrate/bitchan/signals" 10 | "github.com/majestrate/bitchan/storage" 11 | "github.com/majestrate/bitchan/torrent" 12 | "github.com/majestrate/bitchan/web" 13 | "github.com/sirupsen/logrus" 14 | "github.com/zeebo/bencode" 15 | "io" 16 | "net/http" 17 | "net/url" 18 | "os" 19 | ) 20 | 21 | var log = logrus.New() 22 | 23 | func newDecoder(r io.Reader) *bencode.Decoder { 24 | dec := bencode.NewDecoder(r) 25 | dec.SetFailOnUnorderedKeys(true) 26 | return dec 27 | } 28 | 29 | func main() { 30 | var err error 31 | var host string 32 | if len(os.Args) == 1 { 33 | host, err = network.LookupSelf() 34 | } else { 35 | host = os.Args[1] 36 | if len(os.Args) > 2 { 37 | 38 | } 39 | } 40 | if err != nil { 41 | log.WithFields(logrus.Fields{ 42 | "error": err, 43 | }).Error("failed to resolve our hostname") 44 | } 45 | log.WithFields(logrus.Fields{ 46 | "hostname": host, 47 | }).Info("set hostname") 48 | 49 | port := "8800" 50 | 51 | h := web.New(host, port) 52 | h.EnsureKeyFile("identity.key") 53 | h.Api = api.NewAPI() 54 | h.DB, err = db.NewPQ(db.Config{URL: "host=/var/run/postgresql"}) 55 | if err != nil { 56 | log.WithFields(logrus.Fields{ 57 | "error": err, 58 | }).Error("Failed to open database") 59 | return 60 | } 61 | h.Api.Storage = storage.NewStorage() 62 | h.Api.Storage.SetRoot("file_storage") 63 | h.Api.Gossip = gossip.NewServer(host) 64 | h.Api.Torrent = torrent.NewGrabber(h.Api.Storage, h.Api.Gossip) 65 | 66 | s := &http.Server{ 67 | Addr: ":" + port, 68 | Handler: h, 69 | } 70 | signals.SetupSignals(func() { 71 | 72 | }, func() { 73 | s.Close() 74 | h.Stop() 75 | }) 76 | go func() { 77 | h.SetupRoutes() 78 | log.Infof("staring up...") 79 | f, err := os.Open("peers.dat") 80 | if err == nil { 81 | defer f.Close() 82 | var list model.PeerList 83 | list.Peers = make(map[string]model.Peer) 84 | dec := newDecoder(f) 85 | dec.Decode(&list) 86 | go h.AddPeerList(list) 87 | } 88 | s.ListenAndServe() 89 | }() 90 | go func() { 91 | for _, torrent := range h.Api.Storage.GetAllTorrents() { 92 | h.Api.Torrent.Client.AddTorrentFromFile(torrent) 93 | } 94 | }() 95 | signals.Wait() 96 | log.Infof("Saving peers...") 97 | var list model.PeerList 98 | list.Peers = make(map[string]model.Peer) 99 | h.Api.Gossip.ForEachPeer(func(p model.Peer) { 100 | u, _ := url.Parse(p.URL) 101 | if u != nil { 102 | list.Peers[u.Host] = p 103 | } 104 | }) 105 | f, err := os.Create("peers.dat") 106 | if err != nil { 107 | log.WithFields(logrus.Fields{ 108 | "error": err, 109 | }).Error("failed to open peers file") 110 | return 111 | } 112 | defer f.Close() 113 | enc := bencode.NewEncoder(f) 114 | err = enc.Encode(&list) 115 | if err != nil { 116 | log.WithFields(logrus.Fields{ 117 | "error": err, 118 | }).Error("failed to save peers file") 119 | return 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /gossip/http_feed.go: -------------------------------------------------------------------------------- 1 | package gossip 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ed25519" 6 | "encoding/base64" 7 | "github.com/majestrate/bitchan/model" 8 | "github.com/sirupsen/logrus" 9 | "github.com/zeebo/bencode" 10 | "io" 11 | "lukechampine.com/blake3" 12 | "net/http" 13 | "net/url" 14 | ) 15 | 16 | var log = logrus.New() 17 | 18 | type HttpFeed struct { 19 | u *url.URL 20 | pk ed25519.PublicKey 21 | shouldQuit bool 22 | } 23 | 24 | func (f *HttpFeed) Stop() { 25 | f.shouldQuit = true 26 | } 27 | 28 | func newHttpFeed(u *url.URL, pk ed25519.PublicKey) *HttpFeed { 29 | return &HttpFeed{ 30 | u: u, 31 | pk: pk[:], 32 | } 33 | } 34 | 35 | const HttpFeedMimeType = "application/x-bitchan-metadata" 36 | 37 | func newDecoder(r io.Reader) *bencode.Decoder { 38 | dec := bencode.NewDecoder(r) 39 | dec.SetFailOnUnorderedKeys(true) 40 | return dec 41 | } 42 | 43 | func (f *HttpFeed) verifySig(digest, sig []byte) bool { 44 | return ed25519.Verify(f.pk, digest, sig) 45 | } 46 | 47 | func encodeKey(pk ed25519.PublicKey) string { 48 | return base64.StdEncoding.EncodeToString(pk[:]) 49 | } 50 | 51 | func doPost(remoteURL string, obj interface{}) error { 52 | buf := new(bytes.Buffer) 53 | bencode.NewEncoder(buf).Encode(obj) 54 | resp, err := http.Post(remoteURL, HttpFeedMimeType, buf) 55 | if err != nil { 56 | log.WithFields(logrus.Fields{ 57 | "url": remoteURL, 58 | "error": err, 59 | }).Error("failed to http post") 60 | return err 61 | } 62 | var r model.PostResponse 63 | defer resp.Body.Close() 64 | dec := newDecoder(resp.Body) 65 | err = dec.Decode(&r) 66 | if err != nil { 67 | log.WithFields(logrus.Fields{ 68 | "url": remoteURL, 69 | "error": err, 70 | }).Error("failed to read http post response") 71 | return err 72 | } 73 | return nil 74 | } 75 | 76 | func (f *HttpFeed) fetchVerified(remoteURL string, decode func(io.Reader) (interface{}, error)) (interface{}, error) { 77 | resp, err := http.Get(remoteURL) 78 | if err != nil { 79 | return nil, err 80 | } 81 | val := resp.Header.Get("X-Bitchan-Ed25519-B3-Signature") 82 | sig, err := base64.StdEncoding.DecodeString(val) 83 | if err != nil { 84 | log.WithFields(logrus.Fields{ 85 | "url": remoteURL, 86 | "error": err, 87 | "header": val, 88 | }).Error("failed to decode signature header") 89 | return nil, err 90 | } 91 | defer resp.Body.Close() 92 | h := blake3.New(32, nil) 93 | r := io.TeeReader(resp.Body, h) 94 | val_i, err := decode(r) 95 | if err != nil { 96 | log.WithFields(logrus.Fields{ 97 | "url": remoteURL, 98 | "error": err, 99 | }).Error("decode failed") 100 | return nil, err 101 | } 102 | digest := h.Sum(nil) 103 | if f.verifySig(digest, sig) { 104 | return val_i, err 105 | } 106 | log.WithFields(logrus.Fields{ 107 | "url": remoteURL, 108 | "sig": val, 109 | "pk": encodeKey(f.pk), 110 | }).Error("signature verify failed") 111 | return nil, err 112 | } 113 | 114 | func (f *HttpFeed) FetchNeighboors() *model.PeerList { 115 | val, err := f.fetchVerified(f.u.String(), func(r io.Reader) (interface{}, error) { 116 | list := new(model.PeerList) 117 | dec := newDecoder(r) 118 | err := dec.Decode(list) 119 | return list, err 120 | }) 121 | if err != nil { 122 | return nil 123 | } 124 | if val == nil { 125 | return nil 126 | } 127 | return val.(*model.PeerList) 128 | } 129 | 130 | func (f *HttpFeed) Publish(p *model.Post) { 131 | if f.shouldQuit { 132 | return 133 | } 134 | doPost(f.u.String(), p) 135 | } 136 | -------------------------------------------------------------------------------- /templates/base/index.html.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "base/index.html.tmpl" }} 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 26 |
27 | 28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 | 185 | 186 | 187 | {{ end }} 188 | -------------------------------------------------------------------------------- /web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ed25519" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | "encoding/base64" 9 | "fmt" 10 | "github.com/anacrolix/torrent" 11 | "github.com/anacrolix/torrent/metainfo" 12 | "github.com/gin-gonic/gin" 13 | "github.com/majestrate/bitchan/api" 14 | "github.com/majestrate/bitchan/db" 15 | "github.com/majestrate/bitchan/gossip" 16 | "github.com/majestrate/bitchan/model" 17 | "github.com/majestrate/bitchan/util" 18 | "github.com/zeebo/bencode" 19 | "io" 20 | "io/ioutil" 21 | "lukechampine.com/blake3" 22 | "mime/multipart" 23 | "net" 24 | "net/http" 25 | "net/url" 26 | "os" 27 | "path/filepath" 28 | "sort" 29 | "strconv" 30 | "strings" 31 | "time" 32 | ) 33 | 34 | type MiddleWare struct { 35 | Api *api.ApiServer 36 | router *gin.Engine 37 | privkey ed25519.PrivateKey 38 | self model.Peer 39 | hostname string 40 | port string 41 | DB db.Facade 42 | } 43 | 44 | func (m *MiddleWare) AddPeerList(l model.PeerList) { 45 | for _, peer := range l.Peers { 46 | u, _ := url.Parse(peer.URL) 47 | if u != nil { 48 | go m.Api.Gossip.AddNeighboor(u) 49 | } 50 | } 51 | } 52 | 53 | func (m *MiddleWare) EnsureKeyFile(fname string) error { 54 | _, err := os.Stat(fname) 55 | if os.IsNotExist(err) { 56 | err = nil 57 | seed := make([]byte, 32) 58 | n, err := rand.Read(seed) 59 | if n != 32 || err != nil { 60 | return err 61 | } 62 | err = ioutil.WriteFile(fname, seed, os.FileMode(0600)) 63 | if err != nil { 64 | return err 65 | } 66 | } else if err != nil { 67 | return err 68 | } 69 | 70 | data, err := ioutil.ReadFile(fname) 71 | if err != nil { 72 | return err 73 | } 74 | m.privkey = ed25519.NewKeyFromSeed(data) 75 | return nil 76 | } 77 | 78 | func (m *MiddleWare) ServeHTTP(w http.ResponseWriter, r *http.Request) { 79 | m.router.ServeHTTP(w, r) 80 | } 81 | 82 | const BitchanPubKeyContentType = "application/x-bitchan-identity" 83 | 84 | var b64enc = base64.StdEncoding 85 | 86 | func encodeSig(sig []byte) string { 87 | return b64enc.EncodeToString(sig[:]) 88 | } 89 | 90 | func encodePubKey(k ed25519.PublicKey) string { 91 | return b64enc.EncodeToString(k[:]) 92 | } 93 | 94 | func newDecoder(r io.Reader) *bencode.Decoder { 95 | dec := bencode.NewDecoder(r) 96 | dec.SetFailOnUnorderedKeys(true) 97 | return dec 98 | } 99 | 100 | type sortedPostInfo struct { 101 | data []model.PostInfo 102 | } 103 | 104 | func (s *sortedPostInfo) Load(root string) error { 105 | for idx, data := range s.data { 106 | var meta model.Post 107 | fpath := filepath.Join(root, data.InfoHash+".bitchan") 108 | f, err := os.Open(fpath) 109 | if err != nil { 110 | return err 111 | } 112 | err = meta.ReadFromFile(f) 113 | if err != nil { 114 | return err 115 | } 116 | s.data[idx].PostedAt = meta.PostedAt 117 | } 118 | return nil 119 | } 120 | 121 | func (s *sortedPostInfo) Len() int { 122 | return len(s.data) 123 | } 124 | 125 | func (s *sortedPostInfo) Less(i, j int) bool { 126 | return s.data[j].PostedAt < s.data[i].PostedAt 127 | } 128 | 129 | func (s *sortedPostInfo) Swap(i, j int) { 130 | s.data[i], s.data[j] = s.data[j], s.data[i] 131 | } 132 | 133 | type sortedFileInfo struct { 134 | data []os.FileInfo 135 | } 136 | 137 | func (s *sortedFileInfo) Len() int { 138 | return len(s.data) 139 | } 140 | 141 | func (s *sortedFileInfo) Less(i, j int) bool { 142 | return s.data[i].ModTime().After(s.data[j].ModTime()) 143 | } 144 | 145 | func (s *sortedFileInfo) Swap(i, j int) { 146 | s.data[i], s.data[j] = s.data[j], s.data[i] 147 | } 148 | 149 | func (m *MiddleWare) renderDirJSON(dirname string, ctx *gin.Context) { 150 | f, err := os.Open(dirname) 151 | if err != nil { 152 | ctx.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()}) 153 | return 154 | } 155 | fileInfos, err := f.Readdir(0) 156 | f.Close() 157 | if err != nil { 158 | ctx.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()}) 159 | return 160 | } 161 | infos := &sortedFileInfo{data: fileInfos[:]} 162 | sort.Sort(infos) 163 | var files []string 164 | for _, info := range infos.data { 165 | files = append(files, "/files/"+filepath.Base(dirname)+"/"+info.Name()) 166 | } 167 | ctx.JSON(http.StatusOK, map[string]interface{}{"files": files}) 168 | } 169 | 170 | func (m *MiddleWare) makeFilesURL(fname string) string { 171 | return "http://" + net.JoinHostPort(m.hostname, m.port) + "/files/" + filepath.Base(fname) 172 | } 173 | 174 | func mktmp(root, ext string) string { 175 | now := time.Now().UnixNano() 176 | var b [4]byte 177 | rand.Read(b[:]) 178 | r := strings.Trim(base64.URLEncoding.EncodeToString(b[:]), "=") 179 | return filepath.Join(root, fmt.Sprintf("%d-%s%s", now, r, ext)) 180 | } 181 | 182 | func (m *MiddleWare) makePost(hdr *multipart.FileHeader, text string) (p *model.Post, err error) { 183 | 184 | h := sha256.New() 185 | ext := filepath.Ext(hdr.Filename) 186 | tmpfile := mktmp(os.TempDir(), ext) 187 | inf, err := hdr.Open() 188 | if err != nil { 189 | return nil, err 190 | } 191 | defer inf.Close() 192 | of, err := os.Create(tmpfile) 193 | if err != nil { 194 | return nil, err 195 | } 196 | r := io.TeeReader(inf, h) 197 | _, err = io.Copy(of, r) 198 | of.Close() 199 | if err != nil { 200 | os.Remove(tmpfile) 201 | return nil, err 202 | } 203 | d := h.Sum(nil) 204 | filehash := "__" + base64.URLEncoding.EncodeToString(d[:]) 205 | fname := filehash + ext 206 | real_rootf := filepath.Join(m.Api.Storage.GetRoot(), filehash) 207 | real_fname := filepath.Join(real_rootf, fname) 208 | os.Mkdir(real_rootf, os.FileMode(0700)) 209 | 210 | tmpdir := mktmp(os.TempDir(), "") 211 | os.Mkdir(tmpdir, os.FileMode(0700)) 212 | torrent_rootf := filepath.Join(tmpdir, filehash) 213 | os.Mkdir(torrent_rootf, os.FileMode(0700)) 214 | torrent_fname := filepath.Join(torrent_rootf, fname) 215 | 216 | err = util.Move(tmpfile, torrent_fname) 217 | if err != nil { 218 | os.Remove(tmpfile) 219 | return nil, err 220 | } 221 | 222 | torrentFile := filepath.Join(m.Api.Storage.GetRoot(), fname+".torrent") 223 | torrent_txt := "" 224 | real_txt := "" 225 | if len(text) > 0 { 226 | text_fname := fmt.Sprintf("%s-%d.txt", m.hostname, time.Now().UnixNano()) 227 | torrent_txt = filepath.Join(torrent_rootf, text_fname) 228 | real_txt = filepath.Join(real_rootf, text_fname) 229 | err = ioutil.WriteFile(torrent_txt, []byte(text), os.FileMode(0700)) 230 | } 231 | var infohash_hex string 232 | if err == nil { 233 | infohash_hex, err = m.Api.MakeTorrent(torrent_rootf, torrentFile) 234 | if err == nil { 235 | _, err = os.Stat(real_fname) 236 | if os.IsNotExist(err) { 237 | err = util.Move(torrent_fname, real_fname) 238 | } 239 | if real_txt != "" && torrent_txt != "" { 240 | util.Move(torrent_txt, real_txt) 241 | } 242 | } 243 | } 244 | if err != nil { 245 | os.RemoveAll(tmpdir) 246 | os.Remove(tmpfile) 247 | os.Remove(fname) 248 | os.Remove(torrentFile) 249 | return nil, err 250 | } 251 | now := time.Now().UnixNano() 252 | p = &model.Post{ 253 | MetaInfoURL: m.makeFilesURL(torrentFile), 254 | PostedAt: now, 255 | MetaInfoHash: infohash_hex, 256 | } 257 | p.Sign(m.privkey) 258 | go m.Api.Torrent.Grab(p.MetaInfoURL) 259 | metaFile := filepath.Join(m.Api.Storage.GetRoot(), infohash_hex+".bitchan") 260 | mf, err := os.Create(metaFile) 261 | if err == nil { 262 | err = p.WriteToFile(mf) 263 | mf.Close() 264 | } 265 | return p, err 266 | } 267 | 268 | func (m *MiddleWare) torrentURL(t *torrent.Torrent) string { 269 | mi := t.Metainfo() 270 | info, _ := mi.UnmarshalInfo() 271 | return m.makeFilesURL(info.Name) 272 | } 273 | 274 | func (m *MiddleWare) SetupRoutes() { 275 | m.router.LoadHTMLGlob("templates/**/*") 276 | 277 | // sendresult sends signed result 278 | sendResult := func(c *gin.Context, buf *bytes.Buffer, ct string) { 279 | h := blake3.New(32, nil) 280 | str := buf.String() 281 | io.Copy(h, buf) 282 | sig := ed25519.Sign(m.privkey, h.Sum(nil)) 283 | c.Header("X-Bitchan-Ed25519-B3-Signature", encodeSig(sig)) 284 | c.Header("Content-Type", ct) 285 | c.String(http.StatusOK, str) 286 | } 287 | 288 | m.router.GET("/", func(c *gin.Context) { 289 | c.HTML(http.StatusOK, "base/index.html.tmpl", gin.H{ 290 | "title": "bitchan on " + m.hostname, 291 | }) 292 | }) 293 | 294 | m.router.StaticFS("/static", http.Dir(filepath.Join("webroot", "static"))) 295 | 296 | m.router.StaticFS("/files", http.Dir(m.Api.Storage.GetRoot())) 297 | 298 | m.router.GET("/bitchan/v1/files.json", func(c *gin.Context) { 299 | path := c.DefaultQuery("name", "") 300 | if len(path) == 0 { 301 | infohash_hex := c.DefaultQuery("infohash_hex", "") 302 | h := metainfo.NewHashFromHex(infohash_hex[:]) 303 | t, ok := m.Api.Torrent.Client.Torrent(h) 304 | if !ok { 305 | c.JSON(http.StatusNotFound, map[string]interface{}{"error": "not found"}) 306 | return 307 | } 308 | path = t.Name() 309 | 310 | } 311 | name := filepath.Clean(path) 312 | if len(name) == 0 || name == "." { 313 | c.JSON(http.StatusNotFound, map[string]interface{}{"error": "no name or infohash provided"}) 314 | return 315 | } 316 | root := m.Api.Storage.GetRoot() 317 | m.renderDirJSON(filepath.Join(root, name), c) 318 | }) 319 | 320 | m.router.GET("/bitchan/v1/posts.json", func(c *gin.Context) { 321 | limit_str := c.DefaultQuery("limit", "10") 322 | limit, _ := strconv.Atoi(limit_str) 323 | if limit <= 0 { 324 | limit = 1 325 | } 326 | if limit > 10 { 327 | limit = 10 328 | } 329 | var posts sortedPostInfo 330 | m.Api.Torrent.ForEachSeed(func(t *torrent.Torrent) { 331 | posts.data = append(posts.data, model.PostInfo{ 332 | InfoHash: t.InfoHash().HexString(), 333 | Name: t.Name(), 334 | }) 335 | }) 336 | err := posts.Load(m.Api.Storage.GetRoot()) 337 | if err != nil { 338 | c.JSON(http.StatusInternalServerError, gin.H{ 339 | "error": err.Error(), 340 | }) 341 | return 342 | } 343 | sort.Sort(&posts) 344 | c.JSON(http.StatusOK, gin.H{ 345 | "posts": posts.data, 346 | }) 347 | }) 348 | 349 | m.router.GET("/bitchan/v1/post", func(c *gin.Context) { 350 | infohash := c.DefaultQuery("infohash_hex", "") 351 | if infohash == "" { 352 | c.String(http.StatusNotFound, "not found") 353 | return 354 | } 355 | h := metainfo.NewHashFromHex(infohash[:]) 356 | t, ok := m.Api.Torrent.Client.Torrent(h) 357 | if !ok { 358 | c.String(http.StatusNotFound, "not found") 359 | return 360 | } 361 | c.Redirect(http.StatusFound, m.torrentURL(t)) 362 | }) 363 | 364 | m.router.GET("/bitchan/v1/admin/add-peer", func(c *gin.Context) { 365 | rhost, _, err := net.SplitHostPort(c.Request.RemoteAddr) 366 | rip := net.ParseIP(rhost) 367 | if !rip.IsLoopback() { 368 | // deny 369 | c.String(http.StatusForbidden, "nah") 370 | return 371 | } 372 | u := c.DefaultQuery("url", "") 373 | if u == "" { 374 | c.String(http.StatusBadRequest, "no url provided") 375 | return 376 | } 377 | remote, err := url.Parse(u) 378 | if err != nil { 379 | c.String(http.StatusBadRequest, err.Error()) 380 | return 381 | } 382 | if m.Api.Gossip.AddNeighboor(remote) { 383 | c.String(http.StatusCreated, "added") 384 | } else { 385 | c.String(http.StatusBadRequest, "not added") 386 | } 387 | }) 388 | 389 | m.router.POST("/bitchan/v1/post", func(c *gin.Context) { 390 | 391 | f, err := c.FormFile("file") 392 | if err != nil { 393 | c.String(http.StatusInternalServerError, "no file provided ("+err.Error()+")") 394 | return 395 | } 396 | 397 | text := c.DefaultPostForm("comment", "") 398 | 399 | p, err := m.makePost(f, text) 400 | if err != nil { 401 | c.String(http.StatusInternalServerError, err.Error()) 402 | return 403 | } 404 | m.Api.Gossip.BroadcastLocalPost(p) 405 | responseType := c.DefaultQuery("t", "") 406 | if responseType == "plain" { 407 | c.Redirect(http.StatusFound, "/bitchan/v1/post?infohash_hex="+p.MetaInfoHash) 408 | return 409 | } 410 | if responseType == "json" { 411 | c.JSON(http.StatusCreated, p.ToInfo()) 412 | return 413 | } 414 | c.Redirect(http.StatusFound, "/") 415 | }) 416 | 417 | m.router.GET("/bitchan/v1/admin/bootstrap", func(c *gin.Context) { 418 | rhost, _, _ := net.SplitHostPort(c.Request.RemoteAddr) 419 | rip := net.ParseIP(rhost) 420 | if !rip.IsLoopback() { 421 | // deny 422 | c.String(http.StatusForbidden, "nah") 423 | return 424 | } 425 | go m.Api.Gossip.Bootstrap() 426 | c.String(http.StatusCreated, "bootstrap started") 427 | }) 428 | 429 | m.router.GET("/bitchan/v1/self", func(c *gin.Context) { 430 | buf := new(bytes.Buffer) 431 | enc := bencode.NewEncoder(buf) 432 | enc.Encode(m.self) 433 | sendResult(c, buf, gossip.HttpFeedMimeType) 434 | }) 435 | m.router.GET("/bitchan/v1/pubkey", func(c *gin.Context) { 436 | pk := m.privkey.Public().(ed25519.PublicKey) 437 | c.Header("Content-Type", BitchanPubKeyContentType) 438 | c.String(http.StatusOK, encodePubKey(pk)) 439 | }) 440 | m.router.GET("/bitchan/v1/peer-with-me", func(c *gin.Context) { 441 | port := c.DefaultQuery("port", "8800") 442 | rhost, _, err := net.SplitHostPort(c.Request.RemoteAddr) 443 | if err != nil { 444 | c.String(http.StatusInternalServerError, err.Error()) 445 | return 446 | } 447 | host := c.DefaultQuery("host", "") 448 | if host == "" { 449 | names, err := net.LookupAddr(rhost) 450 | if err != nil { 451 | c.String(http.StatusInternalServerError, err.Error()) 452 | return 453 | } 454 | host = strings.TrimSuffix(names[0], ".") 455 | } else { 456 | addrs, err := net.LookupIP(host) 457 | if err != nil { 458 | c.String(http.StatusInternalServerError, err.Error()) 459 | return 460 | } 461 | found := false 462 | for _, addr := range addrs { 463 | if addr.String() == rhost { 464 | found = true 465 | } 466 | } 467 | if !found { 468 | c.String(http.StatusForbidden, "spoofed name") 469 | return 470 | } 471 | } 472 | fedurl := "http://" + net.JoinHostPort(host, port) + "/bitchan/v1/federate" 473 | u, err := url.Parse(fedurl) 474 | if err != nil { 475 | c.String(http.StatusBadRequest, err.Error()) 476 | return 477 | } 478 | if m.Api.Gossip.AddNeighboor(u) { 479 | c.String(http.StatusCreated, "") 480 | } else { 481 | c.String(http.StatusForbidden, "not added") 482 | } 483 | }) 484 | m.router.POST("/bitchan/v1/federate", func(c *gin.Context) { 485 | ct := c.Request.Header.Get("Content-Type") 486 | if ct != gossip.HttpFeedMimeType { 487 | c.String(http.StatusForbidden, "") 488 | return 489 | } 490 | 491 | var p model.Post 492 | defer c.Request.Body.Close() 493 | dec := newDecoder(c.Request.Body) 494 | err := dec.Decode(&p) 495 | if err != nil { 496 | c.String(http.StatusInternalServerError, err.Error()) 497 | return 498 | } 499 | if !p.Verify() { 500 | c.String(http.StatusForbidden, "bad post signature") 501 | return 502 | } 503 | err = m.Api.Torrent.Grab(p.MetaInfoURL) 504 | if err != nil { 505 | c.String(http.StatusInternalServerError, err.Error()) 506 | return 507 | } 508 | var result model.PostResponse 509 | result.Response = "accepted" 510 | result.Time = time.Now().UnixNano() 511 | buf := new(bytes.Buffer) 512 | enc := bencode.NewEncoder(buf) 513 | enc.Encode(result) 514 | sendResult(c, buf, gossip.HttpFeedMimeType) 515 | }) 516 | m.router.GET("/bitchan/v1/federate", func(c *gin.Context) { 517 | var list model.PeerList 518 | list.Peers = make(map[string]model.Peer) 519 | list.Time = time.Now().UnixNano() 520 | m.Api.Gossip.ForEachPeer(func(p model.Peer) { 521 | u, _ := url.Parse(p.URL) 522 | if u == nil { 523 | return 524 | } 525 | list.Peers[u.Host] = p 526 | }) 527 | 528 | list.Peers[m.hostname] = m.self 529 | buf := new(bytes.Buffer) 530 | enc := bencode.NewEncoder(buf) 531 | err := enc.Encode(list) 532 | if err != nil { 533 | c.String(http.StatusInternalServerError, err.Error()) 534 | return 535 | } 536 | sendResult(c, buf, gossip.HttpFeedMimeType) 537 | }) 538 | } 539 | 540 | func (m *MiddleWare) Stop() { 541 | m.Api.Stop() 542 | } 543 | 544 | func New(host string, port string) *MiddleWare { 545 | m := &MiddleWare{ 546 | Api: nil, 547 | router: gin.Default(), 548 | hostname: host, 549 | port: port, 550 | self: model.Peer{ 551 | URL: "http://" + net.JoinHostPort(host, port) + "/bitchan/v1/federate", 552 | }, 553 | } 554 | return m 555 | } 556 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= 2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= 6 | github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= 7 | github.com/RoaringBitmap/roaring v0.4.18/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= 8 | github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= 9 | github.com/RoaringBitmap/roaring v0.4.23 h1:gpyfd12QohbqhFO4NVDUdoPOCXsyahYRQhINmlHxKeo= 10 | github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= 11 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 12 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 13 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 14 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 15 | github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4= 16 | github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro= 17 | github.com/alexflint/go-arg v1.1.0/go.mod h1:3Rj4baqzWaGGmZA2+bVTV8zQOZEjBQAPBnL5xLT+ftY= 18 | github.com/alexflint/go-arg v1.2.0/go.mod h1:3Rj4baqzWaGGmZA2+bVTV8zQOZEjBQAPBnL5xLT+ftY= 19 | github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= 20 | github.com/anacrolix/dht v0.0.0-20180412060941-24cbf25b72a4 h1:0yHJvFiGQhJ1gSHJOR8xzmnx45orEt7uiIB6guf0+zc= 21 | github.com/anacrolix/dht v0.0.0-20180412060941-24cbf25b72a4/go.mod h1:hQfX2BrtuQsLQMYQwsypFAab/GvHg8qxwVi4OJdR1WI= 22 | github.com/anacrolix/dht/v2 v2.0.1/go.mod h1:GbTT8BaEtfqab/LPd5tY41f3GvYeii3mmDUK300Ycyo= 23 | github.com/anacrolix/dht/v2 v2.2.1-0.20191103020011-1dba080fb358/go.mod h1:d7ARx3WpELh9uOEEr0+8wvQeVTOkPse4UU6dKpv4q0E= 24 | github.com/anacrolix/dht/v2 v2.3.2-0.20200103043204-8dce00767ebd/go.mod h1:cgjKyErDnKS6Mej5D1fEqBKg3KwFF2kpFZJp3L6/fGI= 25 | github.com/anacrolix/dht/v2 v2.5.1-0.20200317023935-129f05e9b752/go.mod h1:7RLvyOjm+ZPA7vgFRP+1eRjFzrh27p/nF0VCk5LcjoU= 26 | github.com/anacrolix/dht/v2 v2.6.1 h1:ItcXTeOMNDh3FXGg+O8CPGgteOkWZ7kE/alG1OJ3TXI= 27 | github.com/anacrolix/dht/v2 v2.6.1/go.mod h1:7jf6kAZRU7FFwyeLP3f4LVH8WglAHdMte1mGcq8HQAg= 28 | github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= 29 | github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= 30 | github.com/anacrolix/envpprof v1.0.1/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= 31 | github.com/anacrolix/envpprof v1.1.0 h1:hz8QWMN1fA01YNQsUtVvl9hBXQWWMxSnHHoOK9IdrNY= 32 | github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= 33 | github.com/anacrolix/go-libutp v0.0.0-20180522111405-6baeb806518d/go.mod h1:beQSaSxwH2d9Eeu5ijrEnHei5Qhk+J6cDm1QkWFru4E= 34 | github.com/anacrolix/go-libutp v1.0.2/go.mod h1:uIH0A72V++j0D1nnmTjjZUiH/ujPkFxYWkxQ02+7S0U= 35 | github.com/anacrolix/go-libutp v1.0.3 h1:ei7ie78Usg4cCdUa/GL0J3+RJD86Tp3UwSwY3j7DQvI= 36 | github.com/anacrolix/go-libutp v1.0.3/go.mod h1:8vSGX5g0b4eebsDBNVQHUXSCwYaN18Lnkse0hUW8/5w= 37 | github.com/anacrolix/log v0.0.0-20180412014343-2323884b361d/go.mod h1:sf/7c2aTldL6sRQj/4UKyjgVZBu2+M2z9wf7MmwPiew= 38 | github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= 39 | github.com/anacrolix/log v0.3.1-0.20190913000754-831e4ffe0174/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= 40 | github.com/anacrolix/log v0.3.1-0.20191001111012-13cede988bcd/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= 41 | github.com/anacrolix/log v0.4.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= 42 | github.com/anacrolix/log v0.5.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= 43 | github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= 44 | github.com/anacrolix/log v0.6.1-0.20200416071330-f58a030e6149/go.mod h1:s5yBP/j046fm9odtUTbHOfDUq/zh1W8OkPpJtnX0oQI= 45 | github.com/anacrolix/log v0.7.0 h1:koGkC/K0LjIbrhLhwfpsfMuvu8nhvY7J4TmLVc1mAwE= 46 | github.com/anacrolix/log v0.7.0/go.mod h1:s5yBP/j046fm9odtUTbHOfDUq/zh1W8OkPpJtnX0oQI= 47 | github.com/anacrolix/missinggo v0.0.0-20180522035225-b4a5853e62ff/go.mod h1:b0p+7cn+rWMIphK1gDH2hrDuwGOcbB6V4VXeSsEfHVk= 48 | github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s= 49 | github.com/anacrolix/missinggo v0.2.1-0.20190310234110-9fbdc9f242a8/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= 50 | github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= 51 | github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= 52 | github.com/anacrolix/missinggo v1.2.1 h1:0IE3TqX5y5D0IxeMwTyIgqdDew4QrzcXaaEnJQyjHvw= 53 | github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y= 54 | github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw= 55 | github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ= 56 | github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY= 57 | github.com/anacrolix/missinggo/v2 v2.2.1-0.20191103010835-12360f38ced0/go.mod h1:ZzG3/cc3t+5zcYWAgYrJW0MBsSwNwOkTlNquBbP51Bc= 58 | github.com/anacrolix/missinggo/v2 v2.3.0/go.mod h1:ZzG3/cc3t+5zcYWAgYrJW0MBsSwNwOkTlNquBbP51Bc= 59 | github.com/anacrolix/missinggo/v2 v2.3.1/go.mod h1:3XNH0OEmyMUZuvXmYdl+FDfXd0vvSZhvOLy8CFx8tLg= 60 | github.com/anacrolix/missinggo/v2 v2.4.1-0.20200227072623-f02f6484f997 h1:ufGZtSzDyevqcHyFtAWm2HMjmezh/mCemzC1VFEjsu0= 61 | github.com/anacrolix/missinggo/v2 v2.4.1-0.20200227072623-f02f6484f997/go.mod h1:KY+ij+mWvwGuqSuecLjjPv5LFw5ICUc1UvRems3VAZE= 62 | github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw= 63 | github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg= 64 | github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc= 65 | github.com/anacrolix/multiless v0.0.0-20191223025854-070b7994e841/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= 66 | github.com/anacrolix/multiless v0.0.0-20200413040533-acfd16f65d5d h1:Vg2zfJt8ZRyaOSXfDmgwqAuFTj0zQ8UF1hTw7X12WmY= 67 | github.com/anacrolix/multiless v0.0.0-20200413040533-acfd16f65d5d/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= 68 | github.com/anacrolix/stm v0.1.0/go.mod h1:ZKz7e7ERWvP0KgL7WXfRjBXHNRhlVRlbBQecqFtPq+A= 69 | github.com/anacrolix/stm v0.1.1-0.20191106051447-e749ba3531cf/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= 70 | github.com/anacrolix/stm v0.2.0 h1:q4IrMj/PPQM2OgHkQcWEVmiOXcuf0Kkil2rWasQeGkU= 71 | github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= 72 | github.com/anacrolix/sync v0.0.0-20171108081538-eee974e4f8c1/go.mod h1:+u91KiUuf0lyILI6x3n/XrW7iFROCZCG+TjgK8nW52w= 73 | github.com/anacrolix/sync v0.0.0-20180611022320-3c4cb11f5a01/go.mod h1:+u91KiUuf0lyILI6x3n/XrW7iFROCZCG+TjgK8nW52w= 74 | github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk= 75 | github.com/anacrolix/sync v0.2.0 h1:oRe22/ZB+v7v/5Mbc4d2zE0AXEZy0trKyKLjqYOt6tY= 76 | github.com/anacrolix/sync v0.2.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= 77 | github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= 78 | github.com/anacrolix/tagflag v0.0.0-20180605133421-f477c8c2f14c/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= 79 | github.com/anacrolix/tagflag v0.0.0-20180803105420-3a8ff5428f76/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= 80 | github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= 81 | github.com/anacrolix/tagflag v1.0.1/go.mod h1:gb0fiMQ02qU1djCSqaxGmruMvZGrMwSReidMB0zjdxo= 82 | github.com/anacrolix/torrent v0.0.0-20180622074351-fefeef4ee9eb/go.mod h1:3vcFVxgOASslNXHdivT8spyMRBanMCenHRpe0u5vpBs= 83 | github.com/anacrolix/torrent v1.7.1/go.mod h1:uvOcdpOjjrAq3uMP/u1Ide35f6MJ/o8kMnFG8LV3y6g= 84 | github.com/anacrolix/torrent v1.9.0/go.mod h1:jJJ6lsd2LD1eLHkUwFOhy7I0FcLYH0tHKw2K7ZYMHCs= 85 | github.com/anacrolix/torrent v1.11.0/go.mod h1:FwBai7SyOFlflvfEOaM88ag/jjcBWxTOqD6dVU/lKKA= 86 | github.com/anacrolix/torrent v1.15.0/go.mod h1:MFc6KcbpAyfwGqOyRkdarUK9QnKA/FkVg0usFk1OQxU= 87 | github.com/anacrolix/torrent v1.15.2 h1:qA+t1TyhFbRl42Lw1DiwtCiEIVZrAKJMCVSVllcbNBo= 88 | github.com/anacrolix/torrent v1.15.2/go.mod h1:sJtcAZtlGaZLo7wCXT/EZV+hATsq0Bg6pVhhzACY0E0= 89 | github.com/anacrolix/upnp v0.1.1 h1:v5C+wBiku2zmwFR5B+pUfdNBL5TfPtyO+sWuw+/VEDg= 90 | github.com/anacrolix/upnp v0.1.1/go.mod h1:LXsbsp5h+WGN7YR+0A7iVXm5BL1LYryDev1zuJMWYQo= 91 | github.com/anacrolix/utp v0.0.0-20180219060659-9e0e1d1d0572 h1:kpt6TQTVi6gognY+svubHfxxpq0DLU9AfTQyZVc3UOc= 92 | github.com/anacrolix/utp v0.0.0-20180219060659-9e0e1d1d0572/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk= 93 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 94 | github.com/benbjohnson/immutable v0.2.0 h1:t0rW3lNFwfQ85IDO1mhMbumxdVSti4nnVaal4r45Oio= 95 | github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= 96 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 97 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 98 | github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= 99 | github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= 100 | github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= 101 | github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= 102 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 103 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 104 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 105 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 106 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 107 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 108 | github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 109 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 110 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 111 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 112 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 113 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 114 | github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 115 | github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= 116 | github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 117 | github.com/elliotchance/orderedmap v1.2.0/go.mod h1:8hdSl6jmveQw8ScByd3AaNHNk51RhbTazdqtTty+NFw= 118 | github.com/elliotchance/orderedmap v1.2.2 h1:U5tjNwkj4PjuySqnbkIiiGrj8Ovw83domXHeLeb7OgY= 119 | github.com/elliotchance/orderedmap v1.2.2/go.mod h1:8hdSl6jmveQw8ScByd3AaNHNk51RhbTazdqtTty+NFw= 120 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 121 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 122 | github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= 123 | github.com/frankban/quicktest v1.9.0 h1:jfEA+Psfr/pHsRJYPpHiNu7PGJnGctNxvTaM3K1EyXk= 124 | github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= 125 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 126 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 127 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 128 | github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU= 129 | github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 130 | github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= 131 | github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= 132 | github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a h1:FQqoVvjbiUioBBFUL5up+h+GdCa/AnJsL/1bIs/veSI= 133 | github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= 134 | github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= 135 | github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= 136 | github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8= 137 | github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= 138 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 139 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 140 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 141 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 142 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 143 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 144 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 145 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 146 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 147 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 148 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 149 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 150 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 151 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 152 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 153 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 154 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 155 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 156 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 157 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 158 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 159 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 160 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 161 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 162 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 163 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 164 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 165 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 166 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 167 | github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 168 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 169 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 170 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 171 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 172 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 173 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 174 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 175 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 176 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 177 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 178 | github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 179 | github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw= 180 | github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 181 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 182 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 183 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 184 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 185 | github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= 186 | github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 187 | github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= 188 | github.com/gosuri/uilive v0.0.3/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= 189 | github.com/gosuri/uiprogress v0.0.0-20170224063937-d0567a9d84a1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0= 190 | github.com/gosuri/uiprogress v0.0.1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0= 191 | github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= 192 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 193 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 194 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 195 | github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= 196 | github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 197 | github.com/huandu/xstrings v1.2.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 198 | github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 199 | github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= 200 | github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 201 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 202 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 203 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 204 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 205 | github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= 206 | github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= 207 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 208 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 209 | github.com/jcmturner/gokrb5/v8 v8.2.0/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZgZdoFrZaZNM= 210 | github.com/jcmturner/gokrb5/v8 v8.3.0 h1:+a/zAxqOO5Ljb5UGIUMOnxf5u6kMh9gWqOG67KBICK8= 211 | github.com/jcmturner/gokrb5/v8 v8.3.0/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZgZdoFrZaZNM= 212 | github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0= 213 | github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 214 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 215 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 216 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 217 | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 218 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 219 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 220 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 221 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 222 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 223 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 224 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 225 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 226 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 227 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 228 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 229 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 230 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 231 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 232 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 233 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 234 | github.com/lib/pq v1.6.0 h1:I5DPxhYJChW9KYc66se+oKFFQX6VuQrKiprsX6ivRZc= 235 | github.com/lib/pq v1.6.0/go.mod h1:4vXEAYvW1fRQ2/FhZ78H73A60MHw1geSm145z2mdY1g= 236 | github.com/lukechampine/stm v0.0.0-20191022212748-05486c32d236/go.mod h1:wTLsd5FC9rts7GkMpsPGk64CIuea+03yaLAp19Jmlg8= 237 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 238 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 239 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 240 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 241 | github.com/mattn/go-sqlite3 v1.7.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 242 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 243 | github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 244 | github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 245 | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= 246 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 247 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 248 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 249 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 250 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 251 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 252 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 253 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 254 | github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= 255 | github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= 256 | github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= 257 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 258 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 259 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 260 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 261 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 262 | github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= 263 | github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 264 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 265 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 266 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 267 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 268 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 269 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 270 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 271 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 272 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 273 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 274 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 275 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 276 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 277 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 278 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 279 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 280 | github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= 281 | github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= 282 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 283 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 284 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 285 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 286 | github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 287 | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= 288 | github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= 289 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 290 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 291 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 292 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 293 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 294 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 295 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 296 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 297 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 298 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 299 | github.com/syncthing/syncthing v0.14.48-rc.4/go.mod h1:nw3siZwHPA6M8iSfjDCWQ402eqvEIasMQOE8nFOxy7M= 300 | github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 301 | github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 302 | github.com/tinylib/msgp v1.1.1/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 303 | github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= 304 | github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 305 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 306 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 307 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 308 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 309 | github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 310 | github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 311 | github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc= 312 | github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 313 | github.com/willf/bloom v0.0.0-20170505221640-54e3b963ee16/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= 314 | github.com/willf/bloom v2.0.3+incompatible h1:QDacWdqcAUI1MPOwIQZRy9kOR7yxfyEmxX8Wdm2/JPA= 315 | github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= 316 | github.com/zeebo/bencode v1.0.0 h1:zgop0Wu1nu4IexAZeCZ5qbsjU4O1vMrfCrVgUjbHVuA= 317 | github.com/zeebo/bencode v1.0.0/go.mod h1:Ct7CkrWIQuLWAy9M3atFHYq4kG9Ao/SsY5cdtCXmp9Y= 318 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 319 | go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= 320 | go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= 321 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 322 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 323 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 324 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 325 | golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 326 | golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 327 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 328 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 329 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 330 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 331 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 332 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 333 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 334 | golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 335 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 336 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 337 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 338 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 339 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 340 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 341 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 342 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 343 | golang.org/x/net v0.0.0-20190318221613-d196dffd7c2b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 344 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 345 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 346 | golang.org/x/net v0.0.0-20191125084936-ffdde1057850/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 347 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 348 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= 349 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 350 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 351 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 352 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 353 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 354 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 355 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 356 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 357 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 358 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 359 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 360 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 361 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 362 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 363 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 364 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 365 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 366 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 367 | golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 368 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 369 | golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 370 | golang.org/x/sys v0.0.0-20191126131656-8a8471f7e56d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 371 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 372 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 373 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 374 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 375 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y= 376 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 377 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 378 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 379 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 380 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 381 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 382 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= 383 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 384 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 385 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 386 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 387 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 388 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 389 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 390 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 391 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 392 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 393 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 394 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 395 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 396 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 397 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 398 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 399 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 400 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 401 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 402 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 403 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 404 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 405 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 406 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 407 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 408 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 409 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 410 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 411 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 412 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 413 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 414 | google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= 415 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 416 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 417 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 418 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 419 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 420 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 421 | gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= 422 | gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= 423 | gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= 424 | gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= 425 | gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= 426 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 427 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 428 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 429 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 430 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 431 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 432 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 433 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 434 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 435 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 436 | lukechampine.com/blake3 v1.0.0 h1:dNj1NVD7SLgkU7dykKjmmOSOTTx7ZmxnDyUyvxnQP2Q= 437 | lukechampine.com/blake3 v1.0.0/go.mod h1:e0XQzEQp6LtbXBhzYxRoh6s3kcmX+fMMg8sC9VgWloQ= 438 | --------------------------------------------------------------------------------