├── 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 |
12 |
13 |
14 |
15 |
26 |
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 |
--------------------------------------------------------------------------------