├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bttracker └── tracker.go ├── peer ├── peer.go └── peer_test.go ├── registry ├── inmem │ └── inmem_registry.go ├── redis │ └── redis_registry.go └── registry.go └── server └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | tracker 2 | bttracker/bttracker 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | COPY bttracker/bttracker /usr/bin/ 4 | 5 | EXPOSE 80 6 | ENTRYPOINT ["bttracker"] 7 | CMD ["-redis-addr", "redis"] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Michael Crosby. michael@crosbymichael.com 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, 7 | modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, 20 | DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH 24 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## go based HTTP simple bittorrent tracker 2 | 3 | You can use this by import the `server` package that implements `http.Handler` to use in your own 4 | applications or use the binary `bttracker` to create a standalone bittorrent tracker. 5 | 6 | 7 | You have the option to use an in-memory registry for keeping peer data or using a redis server for storing the 8 | peer data. You can always implement your own registry as well. 9 | 10 | ### btracker 11 | 12 | ```bash 13 | bttracker -h 14 | Usage of bttracker: 15 | -addr=":9090": address of the tracker 16 | -debug=false: enable debug mode for logging 17 | -interval=120: interval for when Peers should poll for new peers 18 | -min-interval=30: min poll interval for new peers 19 | -redis-addr="": address to a redis server for persistent peer data 20 | -redis-pass="": password to use to connect to the redis server 21 | ``` 22 | 23 | ### License MIT 24 | Copyright (c) 2014 Michael Crosby. michael@crosbymichael.com 25 | 26 | Permission is hereby granted, free of charge, to any person 27 | obtaining a copy of this software and associated documentation 28 | files (the "Software"), to deal in the Software without 29 | restriction, including without limitation the rights to use, copy, 30 | modify, merge, publish, distribute, sublicense, and/or sell copies 31 | of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be 35 | included in all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 38 | EXPRESS OR IMPLIED, 39 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 41 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 42 | HOLDERS BE LIABLE FOR ANY CLAIM, 43 | DAMAGES OR OTHER LIABILITY, 44 | WHETHER IN AN ACTION OF CONTRACT, 45 | TORT OR OTHERWISE, 46 | ARISING FROM, OUT OF OR IN CONNECTION WITH 47 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 48 | -------------------------------------------------------------------------------- /bttracker/tracker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/crosbymichael/tracker/registry" 11 | "github.com/crosbymichael/tracker/registry/inmem" 12 | "github.com/crosbymichael/tracker/registry/redis" 13 | "github.com/crosbymichael/tracker/server" 14 | ) 15 | 16 | var ( 17 | flAddr = flag.String("addr", ":9090", "address of the tracker") 18 | flDebug = flag.Bool("debug", false, "enable debug mode for logging") 19 | flInterval = flag.Int("interval", 120, "interval for when Peers should poll for new peers") 20 | flMinInterval = flag.Int("min-interval", 30, "min poll interval for new peers") 21 | flRedisAddr = flag.String("redis-addr", "", "address to a redis server for persistent peer data") 22 | flRedisPass = flag.String("redis-pass", "", "password to use to connect to the redis server") 23 | 24 | mux sync.Mutex 25 | ) 26 | 27 | func main() { 28 | flag.Parse() 29 | var ( 30 | logger = logrus.New() 31 | registry registry.Registry 32 | ) 33 | 34 | if *flDebug { 35 | logger.Level = logrus.DebugLevel 36 | } 37 | 38 | if *flRedisAddr != "" { 39 | registry = redis.New(*flRedisAddr, *flRedisPass) 40 | } else { 41 | registry = inmem.New() 42 | } 43 | 44 | s := server.New(*flInterval, *flMinInterval, registry, logger) 45 | if err := http.ListenAndServe(*flAddr, s); err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /peer/peer.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/vbatts/go-bt/bencode" 12 | ) 13 | 14 | // Peer represents a bittorrent peer 15 | type Peer struct { 16 | ID string `json:"id,omitempty" bencode:"id,omitempty"` 17 | IP string `json:"ip,omitempty" bencode:"ip,omitempty"` 18 | Port int `json:"port,omitempty" bencode:"port,omitempty"` 19 | InfoHash string `json:"info_hash,omitempty" bencode:"info_hash,omitempty"` 20 | Key string `json:"key,omitempty" bencode:"key,omitempty"` 21 | BytesLeft uint64 `json:"bytes_left,omitempty" bencode:"bytes_left,omitempty"` 22 | 23 | computedHash string `bencode:"-"` 24 | } 25 | 26 | // IsSeed returns true if the peer has no more bytes left to receive 27 | func (p *Peer) IsSeed() bool { 28 | return p.BytesLeft == 0 29 | } 30 | 31 | // BTSerialize returns the peer's information serialized in the the bencoding format 32 | func (p *Peer) BTSerialize() (string, error) { 33 | buf, err := bencode.Marshal(*p) 34 | return string(buf), err 35 | } 36 | 37 | // PeerFromRequest returns a peer from an http GET request 38 | func PeerFromRequest(r *http.Request) (*Peer, error) { 39 | v := r.URL.Query() 40 | 41 | port, err := strconv.Atoi(v.Get("port")) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | left, err := strconv.ParseUint(v.Get("left"), 10, 64) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | p := &Peer{ 52 | IP: strings.Split(r.RemoteAddr, ":")[0], // we only need the ip not the port 53 | Port: port, 54 | ID: v.Get("peer_id"), 55 | InfoHash: v.Get("info_hash"), 56 | Key: v.Get("key"), 57 | BytesLeft: left, 58 | } 59 | 60 | return p, nil 61 | } 62 | 63 | // Hash returns a sha1 of the peer ID and InfoHash 64 | func (p *Peer) Hash() string { 65 | if p.computedHash == "" { 66 | hash := sha1.New() 67 | fmt.Fprint(hash, p.ID, p.InfoHash) 68 | 69 | p.computedHash = hex.EncodeToString(hash.Sum(nil)) 70 | } 71 | 72 | return p.computedHash 73 | } 74 | -------------------------------------------------------------------------------- /peer/peer_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSerialize(t *testing.T) { 8 | tests := []struct { 9 | p Peer 10 | expected string 11 | }{ 12 | { 13 | p: Peer{ 14 | IP: "10.10.10.10", 15 | Port: 55555, 16 | }, 17 | expected: "d2:ip11:10.10.10.104:porti55555ee", 18 | }, 19 | { 20 | p: Peer{ 21 | ID: "1000", 22 | IP: "10.10.10.10", 23 | Port: 55555, 24 | InfoHash: "deadbeef", 25 | Key: "secret_key", 26 | BytesLeft: 10000, 27 | }, 28 | expected: "d10:bytes_lefti10000e2:id4:10009:info_hash8:deadbeef2:ip11:10.10.10.103:key10:secret_key4:porti55555ee", 29 | }, 30 | } 31 | 32 | for _, test := range tests { 33 | got, err := test.p.BTSerialize() 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if got != test.expected { 38 | t.Errorf("expected %q, got %q", test.expected, got) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /registry/inmem/inmem_registry.go: -------------------------------------------------------------------------------- 1 | package inmem 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/crosbymichael/tracker/peer" 8 | "github.com/crosbymichael/tracker/registry" 9 | ) 10 | 11 | // InMemRegistry implements a registry that stores Peer information in memory 12 | // and is lost if the process is restarted. 13 | type InMemRegistry struct { 14 | sync.Mutex 15 | 16 | peers map[string]*peerData 17 | } 18 | 19 | type peerData struct { 20 | p *peer.Peer 21 | expires time.Time 22 | } 23 | 24 | // NewInMemoryRegistry returns a new in memory registry for storing peer information 25 | func New() registry.Registry { 26 | return &InMemRegistry{ 27 | peers: make(map[string]*peerData), 28 | } 29 | } 30 | 31 | func (r *InMemRegistry) FetchPeers() ([]*peer.Peer, error) { 32 | r.Lock() 33 | 34 | var ( 35 | out = []*peer.Peer{} 36 | now = time.Now() 37 | ) 38 | 39 | for _, p := range r.peers { 40 | if p.expires.After(now) { 41 | out = append(out, p.p) 42 | } else { 43 | key := r.getKey(p.p) 44 | delete(r.peers, key) 45 | } 46 | } 47 | 48 | r.Unlock() 49 | 50 | return out, nil 51 | } 52 | 53 | func (r *InMemRegistry) SavePeer(p *peer.Peer, ttl int) error { 54 | r.Lock() 55 | 56 | key := r.getKey(p) 57 | r.peers[key] = &peerData{ 58 | p: p, 59 | expires: time.Now().Add(time.Duration(ttl) * time.Second), 60 | } 61 | 62 | r.Unlock() 63 | 64 | return nil 65 | } 66 | 67 | func (r *InMemRegistry) DeletePeer(p *peer.Peer) error { 68 | r.Lock() 69 | 70 | key := r.getKey(p) 71 | delete(r.peers, key) 72 | 73 | r.Unlock() 74 | 75 | return nil 76 | } 77 | 78 | func (r *InMemRegistry) Close() error { 79 | return nil 80 | } 81 | 82 | func (r *InMemRegistry) getKey(p *peer.Peer) string { 83 | return p.Hash() 84 | } 85 | -------------------------------------------------------------------------------- /registry/redis/redis_registry.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/crosbymichael/tracker/peer" 8 | "github.com/crosbymichael/tracker/registry" 9 | "github.com/garyburd/redigo/redis" 10 | ) 11 | 12 | type RedisRegistry struct { 13 | pool *redis.Pool 14 | } 15 | 16 | func New(addr, pass string) registry.Registry { 17 | return &RedisRegistry{ 18 | pool: newPool(addr, pass), 19 | } 20 | } 21 | 22 | func (r *RedisRegistry) FetchPeers() ([]*peer.Peer, error) { 23 | out := []*peer.Peer{} 24 | 25 | keys, err := redis.Strings(r.do("KEYS", "tracker:peer:*")) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | for _, key := range keys { 31 | data, err := redis.String(r.do("GET", key)) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | var p *peer.Peer 37 | if err := json.Unmarshal([]byte(data), &p); err != nil { 38 | return nil, err 39 | } 40 | 41 | out = append(out, p) 42 | } 43 | 44 | return out, nil 45 | } 46 | 47 | func (r *RedisRegistry) SavePeer(p *peer.Peer, ttl int) error { 48 | data, err := json.Marshal(p) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | key := r.getKey(p) 54 | if _, err := r.do("SETEX", key, ttl, string(data)); err != nil { 55 | return err 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (r *RedisRegistry) DeletePeer(p *peer.Peer) error { 62 | key := r.getKey(p) 63 | 64 | if _, err := r.do("DEL", key); err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (r *RedisRegistry) Close() error { 72 | return r.pool.Close() 73 | } 74 | 75 | func (r *RedisRegistry) getKey(p *peer.Peer) string { 76 | return fmt.Sprintf("tracker:peer:%s", p.Hash()) 77 | } 78 | 79 | func (r *RedisRegistry) do(cmd string, args ...interface{}) (interface{}, error) { 80 | conn := r.pool.Get() 81 | defer conn.Close() 82 | 83 | return conn.Do(cmd, args...) 84 | } 85 | 86 | func newPool(addr, pass string) *redis.Pool { 87 | return redis.NewPool(func() (redis.Conn, error) { 88 | c, err := redis.Dial("tcp", addr) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | if pass != "" { 94 | if _, err := c.Do("AUTH", pass); err != nil { 95 | return nil, err 96 | } 97 | } 98 | 99 | return c, nil 100 | }, 10) 101 | } 102 | -------------------------------------------------------------------------------- /registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "github.com/crosbymichael/tracker/peer" 5 | ) 6 | 7 | // Registry impements a persistent store for peers in the tracker 8 | type Registry interface { 9 | // FetchPeers returns all the current peers in the tracker 10 | FetchPeers() ([]*peer.Peer, error) 11 | 12 | // SavePeer saves the current peer in the registry with a specified ttl 13 | SavePeer(*peer.Peer, int) error 14 | 15 | // DeletePeer removes the peer form the registry 16 | DeletePeer(*peer.Peer) error 17 | 18 | // Close closes any resources used by the registry 19 | Close() error 20 | } 21 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/Sirupsen/logrus" 9 | "github.com/crosbymichael/tracker/peer" 10 | "github.com/crosbymichael/tracker/registry" 11 | ) 12 | 13 | const bencodingFormat = "d8:intervali%de12:min intervali%de8:completei%de10:incompletei%de5:peersl%see" 14 | 15 | // Server implements the http.Handler interface to serve traffic for a bittorrent tracker 16 | type Server struct { 17 | interval int 18 | minInterval int 19 | registry registry.Registry 20 | logger *logrus.Logger 21 | 22 | mux *http.ServeMux 23 | } 24 | 25 | // New returns a new http.Handler for serving bittorrent tracker traffic 26 | func New(interval, minInterval int, registry registry.Registry, logger *logrus.Logger) http.Handler { 27 | s := &Server{ 28 | interval: interval, 29 | minInterval: minInterval, 30 | registry: registry, 31 | logger: logger, 32 | mux: http.NewServeMux(), 33 | } 34 | 35 | s.mux.HandleFunc("/", s.tracker) 36 | 37 | return s 38 | } 39 | 40 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 41 | s.mux.ServeHTTP(w, r) 42 | } 43 | 44 | func (s *Server) tracker(w http.ResponseWriter, r *http.Request) { 45 | s.logger.Debugf("url: %q; headers: \"%#v\"", r.URL.String(), r.Header) 46 | 47 | if r.URL.Query().Get("port") == "" || r.URL.Query().Get("left") == "" { 48 | http.Error(w, "missing information", http.StatusBadRequest) 49 | return 50 | } 51 | 52 | peer, err := peer.PeerFromRequest(r) 53 | if err != nil { 54 | s.logger.WithField("error", err).Error("parsing peer from request") 55 | http.Error(w, err.Error(), http.StatusBadRequest) 56 | 57 | return 58 | } 59 | 60 | switch event := r.URL.Query().Get("event"); event { 61 | case "stopped": 62 | s.logger.WithFields(logrus.Fields{ 63 | "id": peer.Hash(), 64 | "event": event, 65 | }).Debug("received peer stop event") 66 | 67 | if err := s.registry.DeletePeer(peer); err != nil { 68 | s.logger.WithField("error", err).Error("remove peer from registry") 69 | http.Error(w, err.Error(), http.StatusInternalServerError) 70 | } 71 | 72 | return 73 | default: 74 | s.logger.WithFields(logrus.Fields{ 75 | "id": peer.Hash(), 76 | "event": event, 77 | }).Debug("received peer event") 78 | } 79 | 80 | if err := s.registry.SavePeer(peer, s.interval); err != nil { 81 | s.logger.WithField("error", err).Error("save peer from registry") 82 | http.Error(w, err.Error(), http.StatusInternalServerError) 83 | 84 | return 85 | } 86 | 87 | peers, err := s.registry.FetchPeers() 88 | if err != nil { 89 | s.logger.WithField("error", err).Error("fetch peers from registry") 90 | http.Error(w, err.Error(), http.StatusInternalServerError) 91 | 92 | return 93 | } 94 | 95 | var ( 96 | completed int 97 | active = []string{} 98 | ) 99 | 100 | // build the bencoding strings for all the peers in the tracker 101 | for _, p := range peers { 102 | if p.IsSeed() { 103 | completed++ 104 | 105 | // don't allow seeds to see each other 106 | if peer.IsSeed() { 107 | continue 108 | } 109 | } 110 | 111 | s.logger.WithField("id", p.Hash()).Debug("active peer") 112 | 113 | buf, err := p.BTSerialize() 114 | if err != nil { 115 | s.logger.WithField("error", err).Errorf("serializing failed: %s", err) 116 | continue 117 | } 118 | active = append(active, buf) 119 | } 120 | 121 | data := fmt.Sprintf(bencodingFormat, s.interval, s.minInterval, completed, len(active), strings.Join(active, "")) 122 | 123 | if _, err := fmt.Fprint(w, data); err != nil { 124 | s.logger.WithField("error", err).Error("write data to response") 125 | } 126 | } 127 | --------------------------------------------------------------------------------