diff --git a/config/config.go b/config/config.go index c239bac..7c20e3a 100644 --- a/config/config.go +++ b/config/config.go @@ -10,8 +10,6 @@ import ( "io" "os" "time" - - "github.com/golang/glog" ) // Duration wraps a time.Duration and adds JSON marshalling. @@ -57,30 +55,29 @@ type Config struct { Private bool `json:"private"` Freeleech bool `json:"freeleech"` + Whitelist bool `json:"whitelist"` Announce Duration `json:"announce"` MinAnnounce Duration `json:"min_announce"` - ReadTimeout Duration `json:"read_timeout"` + RequestTimeout Duration `json:"request_timeout"` NumWantFallback int `json:"default_num_want"` } -// New returns a default configuration. -func New() *Config { - return &Config{ - Addr: ":6881", - Tracker: DriverConfig{ - Driver: "mock", - }, - Backend: DriverConfig{ - Driver: "mock", - }, - Private: false, - Freeleech: false, - Announce: Duration{30 * time.Minute}, - MinAnnounce: Duration{15 * time.Minute}, - ReadTimeout: Duration{20 % time.Second}, - NumWantFallback: 50, - } +var DefaultConfig = Config{ + Addr: ":6881", + Tracker: DriverConfig{ + Driver: "mock", + }, + Backend: DriverConfig{ + Driver: "mock", + }, + Private: false, + Freeleech: false, + Whitelist: false, + Announce: Duration{30 * time.Minute}, + MinAnnounce: Duration{15 * time.Minute}, + RequestTimeout: Duration{10 * time.Second}, + NumWantFallback: 50, } // Open is a shortcut to open a file, read it, and generate a Config. @@ -88,8 +85,7 @@ func New() *Config { // New. func Open(path string) (*Config, error) { if path == "" { - glog.V(1).Info("using default config") - return New(), nil + return &DefaultConfig, nil } f, err := os.Open(os.ExpandEnv(path)) @@ -102,7 +98,6 @@ func Open(path string) (*Config, error) { if err != nil { return nil, err } - glog.V(1).Infof("loaded config file: %s", path) return conf, nil } diff --git a/example.json b/example.json index 3675729..27bc39d 100644 --- a/example.json +++ b/example.json @@ -1,25 +1,21 @@ { "network": "tcp", - "addr": ":34000", + "addr": ":6881", "tracker": { - "driver": "redis", - "network": "tcp", - "host": "127.0.0.1", - "port": "6379", - "user": "root", - "pass": "", - "prefix": "test:", + "driver": "mock" + }, - "max_idle_conns": 3, - "idle_timeout": "240s" + "backend": { + "driver": "mock" }, "private": true, "freeleech": false, + "whitelist": false, "announce": "30m", "min_announce": "15m", - "read_timeout": "20s", + "request_timeout": "10s", "default_num_want": 50 -} \ No newline at end of file +} diff --git a/server/serve_announce.go b/http/announce.go similarity index 74% rename from server/serve_announce.go rename to http/announce.go index d9f6acd..106a198 100644 --- a/server/serve_announce.go +++ b/http/announce.go @@ -2,87 +2,78 @@ // Use of this source code is governed by the BSD 2-Clause license, // which can be found in the LICENSE file. -package server +package http import ( "fmt" "io" "net/http" - "github.com/golang/glog" + "github.com/julienschmidt/httprouter" "github.com/chihaya/chihaya/bencode" "github.com/chihaya/chihaya/drivers/tracker" "github.com/chihaya/chihaya/models" ) -func (s Server) serveAnnounce(w http.ResponseWriter, r *http.Request) { - announce, err := models.NewAnnounce(r, s.conf) +func (t *Tracker) ServeAnnounce(w http.ResponseWriter, r *http.Request, p httprouter.Params) int { + ann, err := models.NewAnnounce(t.cfg, r, p) if err != nil { - fail(err, w, r) - return + fail(w, r, err) + return http.StatusOK } - conn, err := s.trackerPool.Get() + conn, err := t.tp.Get() if err != nil { - fail(err, w, r) - return + return http.StatusInternalServerError } - err = conn.ClientWhitelisted(announce.ClientID()) - if err != nil { - fail(err, w, r) - return - } - - var user *models.User - if s.conf.Private { - user, err = conn.FindUser(announce.Passkey) + if t.cfg.Whitelist { + err = conn.ClientWhitelisted(ann.ClientID()) if err != nil { - fail(err, w, r) - return + fail(w, r, err) + return http.StatusOK } } - torrent, err := conn.FindTorrent(announce.Infohash) + var user *models.User + if t.cfg.Private { + user, err = conn.FindUser(ann.Passkey) + if err != nil { + fail(w, r, err) + return http.StatusOK + } + } + + torrent, err := conn.FindTorrent(ann.Infohash) if err != nil { - fail(err, w, r) - return + fail(w, r, err) + return http.StatusOK } - peer := models.NewPeer(announce, user, torrent) + peer := models.NewPeer(ann, user, torrent) - created, err := updateTorrent(conn, announce, peer, torrent) + created, err := updateTorrent(conn, ann, peer, torrent) if err != nil { - fail(err, w, r) - return + return http.StatusInternalServerError } - snatched, err := handleEvent(conn, announce, peer, user, torrent) + snatched, err := handleEvent(conn, ann, peer, user, torrent) if err != nil { - fail(err, w, r) - return + return http.StatusInternalServerError } - if s.conf.Private { - delta := models.NewAnnounceDelta(announce, peer, user, torrent, created, snatched) - s.backendConn.RecordAnnounce(delta) + if t.cfg.Private { + delta := models.NewAnnounceDelta(ann, peer, user, torrent, created, snatched) + err = t.bc.RecordAnnounce(delta) + if err != nil { + return http.StatusInternalServerError + } } - writeAnnounceResponse(w, announce, user, torrent) + writeAnnounceResponse(w, ann, user, torrent) - w.(http.Flusher).Flush() - - if s.conf.Private { - glog.V(5).Infof( - "announce: ip: %s user: %s torrent: %s", - announce.IP, - user.ID, - torrent.ID, - ) - } else { - glog.V(5).Infof("announce: ip: %s torrent: %s", announce.IP, torrent.ID) - } + return http.StatusOK } func updateTorrent(c tracker.Conn, a *models.Announce, p *models.Peer, t *models.Torrent) (created bool, err error) { @@ -199,11 +190,13 @@ func writeAnnounceResponse(w io.Writer, a *models.Announce, u *models.User, t *m } func writePeersCompact(w io.Writer, a *models.Announce, u *models.User, t *models.Torrent, peerCount int) { + bencoder := bencode.NewEncoder(w) ipv4s, ipv6s := getPeers(a, u, t, peerCount) if len(ipv4s) > 0 { // 6 is the number of bytes that represents 1 compact IPv4 address. - fmt.Fprintf(w, "peers%d:", len(ipv4s)*6) + bencoder.Encode("peers") + fmt.Fprintf(w, "%d:", len(ipv4s)*6) for _, peer := range ipv4s { if ip := peer.IP.To4(); ip != nil { @@ -215,7 +208,8 @@ func writePeersCompact(w io.Writer, a *models.Announce, u *models.User, t *model if len(ipv6s) > 0 { // 18 is the number of bytes that represents 1 compact IPv6 address. - fmt.Fprintf(w, "peers6%d:", len(ipv6s)*18) + bencoder.Encode("peers6") + fmt.Fprintf(w, "%d:", len(ipv6s)*18) for _, peer := range ipv6s { if ip := peer.IP.To16(); ip != nil { @@ -226,7 +220,7 @@ func writePeersCompact(w io.Writer, a *models.Announce, u *models.User, t *model } } -func getPeers(a *models.Announce, u *models.User, t *models.Torrent, peerCount int) (ipv4s, ipv6s []*models.Peer) { +func getPeers(a *models.Announce, u *models.User, t *models.Torrent, peerCount int) (ipv4s, ipv6s []models.Peer) { if a.Left == 0 { // If they're seeding, give them only leechers. splitPeers(&ipv4s, &ipv6s, a, u, t.Leechers, peerCount) @@ -239,7 +233,7 @@ func getPeers(a *models.Announce, u *models.User, t *models.Torrent, peerCount i return } -func splitPeers(ipv4s, ipv6s *[]*models.Peer, a *models.Announce, u *models.User, peers map[string]models.Peer, peerCount int) (count int) { +func splitPeers(ipv4s, ipv6s *[]models.Peer, a *models.Announce, u *models.User, peers map[string]models.Peer, peerCount int) (count int) { for _, peer := range peers { if count >= peerCount { break @@ -250,9 +244,9 @@ func splitPeers(ipv4s, ipv6s *[]*models.Peer, a *models.Announce, u *models.User } if ip := peer.IP.To4(); len(ip) == 4 { - *ipv4s = append(*ipv4s, &peer) + *ipv4s = append(*ipv4s, peer) } else if ip := peer.IP.To16(); len(ip) == 16 { - *ipv6s = append(*ipv6s, &peer) + *ipv6s = append(*ipv6s, peer) } count++ @@ -269,10 +263,10 @@ func writePeersList(w io.Writer, a *models.Announce, u *models.User, t *models.T fmt.Fprintf(w, "l") for _, peer := range ipv4s { - writePeerDict(w, peer) + writePeerDict(w, &peer) } for _, peer := range ipv6s { - writePeerDict(w, peer) + writePeerDict(w, &peer) } fmt.Fprintf(w, "e") diff --git a/http/announce_test.go b/http/announce_test.go new file mode 100644 index 0000000..93fdb55 --- /dev/null +++ b/http/announce_test.go @@ -0,0 +1,154 @@ +// Copyright 2013 The Chihaya Authors. All rights reserved. +// Use of this source code is governed by the BSD 2-Clause license, +// which can be found in the LICENSE file. + +package http + +import ( + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/chihaya/chihaya/config" + "github.com/chihaya/chihaya/drivers/backend" + _ "github.com/chihaya/chihaya/drivers/backend/mock" + "github.com/chihaya/chihaya/drivers/tracker" + _ "github.com/chihaya/chihaya/drivers/tracker/mock" + "github.com/chihaya/chihaya/models" +) + +type primer func(tracker.Pool, backend.Conn) error + +func (t *Tracker) prime(p primer) error { + return p(t.tp, t.bc) +} + +func loadTestData(tkr *Tracker) (err error) { + return tkr.prime(func(tp tracker.Pool, bc backend.Conn) (err error) { + conn, err := tp.Get() + if err != nil { + return + } + + err = conn.AddUser(&models.User{ + ID: 1, + Passkey: "yby47f04riwpndba456rqxtmifenqxx1", + }) + if err != nil { + return + } + err = conn.AddUser(&models.User{ + ID: 2, + Passkey: "yby47f04riwpndba456rqxtmifenqxx2", + }) + if err != nil { + return + } + err = conn.AddUser(&models.User{ + ID: 3, + Passkey: "yby47f04riwpndba456rqxtmifenqxx3", + }) + if err != nil { + return + } + + err = conn.WhitelistClient("TR2820") + if err != nil { + return + } + + torrent := &models.Torrent{ + ID: 1, + Infohash: string([]byte{0x89, 0xd4, 0xbc, 0x52, 0x11, 0x16, 0xca, 0x1d, 0x42, 0xa2, 0xf3, 0x0d, 0x1f, 0x27, 0x4d, 0x94, 0xe4, 0x68, 0x1d, 0xaf}), + Seeders: make(map[string]models.Peer), + Leechers: make(map[string]models.Peer), + } + + err = conn.AddTorrent(torrent) + if err != nil { + return + } + + err = conn.AddLeecher(torrent, &models.Peer{ + ID: "-TR2820-l71jtqkl8xx1", + UserID: 1, + TorrentID: torrent.ID, + IP: net.ParseIP("127.0.0.1"), + Port: 34000, + Left: 0, + }) + if err != nil { + return + } + + err = conn.AddLeecher(torrent, &models.Peer{ + ID: "-TR2820-l71jtqkl8xx3", + UserID: 3, + TorrentID: torrent.ID, + IP: net.ParseIP("2001::53aa:64c:0:7f83:bc43:dec9"), + Port: 34000, + Left: 0, + }) + + return + }) +} + +func testRoute(cfg *config.Config, url string) (bodystr string, err error) { + tkr, err := NewTracker(cfg) + if err != nil { + return + } + + err = loadTestData(tkr) + if err != nil { + return + } + + srv := httptest.NewServer(setupRoutes(tkr, cfg)) + defer srv.Close() + + url = srv.URL + url + + resp, err := http.Get(url) + if err != nil { + return + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return + } + + return string(body), nil +} + +// TODO Make more wrappers for testing routes with less boilerplate +func TestPrivateAnnounce(t *testing.T) { + cfg := config.DefaultConfig + cfg.Private = true + + url := "/yby47f04riwpndba456rqxtmifenqxx2/announce?info_hash=%89%d4%bcR%11%16%ca%1dB%a2%f3%0d%1f%27M%94%e4h%1d%af&peer_id=-TR2820-l71jtqkl898b&port=51413&uploaded=0&downloaded=0&left=0&numwant=1&key=3c8e3319&compact=0" + golden1 := "d8:completei1e10:incompletei2e8:intervali1800e12:min intervali900e5:peersld2:ip9:127.0.0.17:peer id20:-TR2820-l71jtqkl8xx14:porti34000eeee" + golden2 := "d8:completei1e10:incompletei2e8:intervali1800e12:min intervali900e5:peersld2:ip32:2001:0:53aa:64c:0:7f83:bc43:dec97:peer id20:-TR2820-l71jtqkl8xx34:porti34000eeee" + got, err := testRoute(&cfg, url) + if err != nil { + t.Error(err) + } + if got != golden1 && got != golden2 { + t.Errorf("\ngot: %s\nwanted: %s\nwanted: %s", got, golden1, golden2) + } + + url = "/yby47f04riwpndba456rqxtmifenqxx2/announce?info_hash=%89%d4%bcR%11%16%ca%1dB%a2%f3%0d%1f%27M%94%e4h%1d%af&peer_id=-TR2820-l71jtqkl898b&port=51413&uploaded=0&downloaded=0&left=0&numwant=2&key=3c8e3319&compact=0" + golden1 = "d8:completei1e10:incompletei2e8:intervali1800e12:min intervali900e5:peersld2:ip9:127.0.0.17:peer id20:-TR2820-l71jtqkl8xx14:porti34000eed2:ip32:2001:0:53aa:64c:0:7f83:bc43:dec97:peer id20:-TR2820-l71jtqkl8xx34:porti34000eeee" + got, err = testRoute(&cfg, url) + if err != nil { + t.Error(err) + } + if got != golden1 { + t.Errorf("\ngot: %s\nwanted: %s", got, golden1) + } +} diff --git a/http/http.go b/http/http.go new file mode 100644 index 0000000..b0d5a3a --- /dev/null +++ b/http/http.go @@ -0,0 +1,86 @@ +// Copyright 2013 The Chihaya Authors. All rights reserved. +// Use of this source code is governed by the BSD 2-Clause license, +// which can be found in the LICENSE file. + +package http + +import ( + "fmt" + "net/http" + "time" + + "github.com/golang/glog" + "github.com/julienschmidt/httprouter" + "github.com/stretchr/graceful" + + "github.com/chihaya/chihaya/config" + "github.com/chihaya/chihaya/drivers/backend" + "github.com/chihaya/chihaya/drivers/tracker" +) + +type Tracker struct { + cfg *config.Config + tp tracker.Pool + bc backend.Conn +} + +func NewTracker(cfg *config.Config) (*Tracker, error) { + tp, err := tracker.Open(&cfg.Tracker) + if err != nil { + return nil, err + } + + bc, err := backend.Open(&cfg.Backend) + if err != nil { + return nil, err + } + + return &Tracker{ + cfg: cfg, + tp: tp, + bc: bc, + }, nil +} + +type ResponseHandler func(http.ResponseWriter, *http.Request, httprouter.Params) int + +func makeHandler(handler ResponseHandler) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + start := time.Now() + code := handler(w, r, p) + glog.Infof( + "Completed %v %s %s in %v", + code, + http.StatusText(code), + r.URL.Path, + time.Since(start), + ) + } +} + +func setupRoutes(t *Tracker, cfg *config.Config) *httprouter.Router { + r := httprouter.New() + if cfg.Private { + r.GET("/:passkey/announce", makeHandler(t.ServeAnnounce)) + r.GET("/:passkey/scrape", makeHandler(t.ServeScrape)) + } else { + r.GET("/announce", makeHandler(t.ServeAnnounce)) + r.GET("/scrape", makeHandler(t.ServeScrape)) + } + + return r +} + +func Serve(cfg *config.Config) { + t, err := NewTracker(cfg) + if err != nil { + glog.Fatal("New: ", err) + } + + graceful.Run(cfg.Addr, cfg.RequestTimeout.Duration, setupRoutes(t, cfg)) +} + +func fail(w http.ResponseWriter, r *http.Request, err error) { + errmsg := err.Error() + fmt.Fprintf(w, "d14:failure reason%d:%se", len(errmsg), errmsg) +} diff --git a/server/serve_scrape.go b/http/scrape.go similarity index 56% rename from server/serve_scrape.go rename to http/scrape.go index 078d1b0..948de5a 100644 --- a/server/serve_scrape.go +++ b/http/scrape.go @@ -2,53 +2,47 @@ // Use of this source code is governed by the BSD 2-Clause license, // which can be found in the LICENSE file. -package server +package http import ( "fmt" "io" "net/http" - "strings" - "github.com/golang/glog" + "github.com/julienschmidt/httprouter" "github.com/chihaya/chihaya/bencode" "github.com/chihaya/chihaya/models" ) -func (s *Server) serveScrape(w http.ResponseWriter, r *http.Request) { - scrape, err := models.NewScrape(r, s.conf) +func (t *Tracker) ServeScrape(w http.ResponseWriter, r *http.Request, p httprouter.Params) int { + scrape, err := models.NewScrape(t.cfg, r, p) if err != nil { - fail(err, w, r) - return + fail(w, r, err) + return http.StatusOK } - conn, err := s.trackerPool.Get() + conn, err := t.tp.Get() if err != nil { - fail(err, w, r) + return http.StatusInternalServerError } - var user *models.User - if s.conf.Private { - user, err = conn.FindUser(scrape.Passkey) + if t.cfg.Private { + _, err = conn.FindUser(scrape.Passkey) if err != nil { - fail(err, w, r) - return + fail(w, r, err) + return http.StatusOK } } - var ( - torrents []*models.Torrent - torrentIDs []string - ) + var torrents []*models.Torrent for _, infohash := range scrape.Infohashes { torrent, err := conn.FindTorrent(infohash) if err != nil { - fail(err, w, r) - return + fail(w, r, err) + return http.StatusOK } torrents = append(torrents, torrent) - torrentIDs = append(torrentIDs, string(torrent.ID)) } bencoder := bencode.NewEncoder(w) @@ -59,22 +53,7 @@ func (s *Server) serveScrape(w http.ResponseWriter, r *http.Request) { } fmt.Fprintf(w, "e") - w.(http.Flusher).Flush() - - if s.conf.Private { - glog.V(5).Infof( - "scrape: ip: %s user: %s torrents: %s", - r.RemoteAddr, - user.ID, - strings.Join(torrentIDs, ", "), - ) - } else { - glog.V(5).Infof( - "scrape: ip: %s torrents: %s", - r.RemoteAddr, - strings.Join(torrentIDs, ", "), - ) - } + return http.StatusOK } func writeTorrentStatus(w io.Writer, t *models.Torrent) { diff --git a/main.go b/main.go index f7fa8a0..f28d989 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,6 @@ package main import ( "flag" "os" - "os/signal" "runtime" "runtime/pprof" @@ -16,7 +15,7 @@ import ( "github.com/chihaya/chihaya/config" _ "github.com/chihaya/chihaya/drivers/backend/mock" _ "github.com/chihaya/chihaya/drivers/tracker/mock" - "github.com/chihaya/chihaya/server" + "github.com/chihaya/chihaya/http" ) var ( @@ -42,50 +41,26 @@ func main() { defer f.Close() pprof.StartCPUProfile(f) - glog.V(1).Info("started profiling") + glog.Info("started profiling") defer func() { pprof.StopCPUProfile() - glog.V(1).Info("stopped profiling") + glog.Info("stopped profiling") }() } // Load the config file. - conf, err := config.Open(configPath) + cfg, err := config.Open(configPath) if err != nil { glog.Fatalf("failed to parse configuration file: %s\n", err) } - - // Create a new server. - s, err := server.New(conf) - if err != nil { - glog.Fatalf("failed to create server: %s\n", err) + if cfg == &config.DefaultConfig { + glog.Info("using default config") + } else { + glog.Infof("loaded config file: %s", configPath) } - // Spawn a goroutine to handle interrupts and safely shut down. - go func() { - interrupts := make(chan os.Signal, 1) - signal.Notify(interrupts, os.Interrupt) - - <-interrupts - glog.V(1).Info("caught interrupt, shutting down...") - - err := s.Stop() - if err != nil { - glog.Fatalf("failed to shutdown cleanly: %s", err) - } - - glog.V(1).Info("shutdown cleanly") - - <-interrupts - - glog.Flush() - os.Exit(0) - }() - // Start the server listening and handling requests. - err = s.ListenAndServe() - if err != nil { - glog.Fatalf("failed to start server: %s\n", err) - } + http.Serve(cfg) + glog.Info("gracefully shutdown") } diff --git a/models/models.go b/models/models.go index 957f36c..3f82804 100644 --- a/models/models.go +++ b/models/models.go @@ -10,12 +10,12 @@ import ( "errors" "net" "net/http" - "path" "strconv" "time" "github.com/chihaya/chihaya/config" "github.com/chihaya/chihaya/models/query" + "github.com/julienschmidt/httprouter" ) var ( @@ -133,7 +133,7 @@ type Announce struct { } // NewAnnounce parses an HTTP request and generates an Announce. -func NewAnnounce(r *http.Request, conf *config.Config) (*Announce, error) { +func NewAnnounce(cfg *config.Config, r *http.Request, p httprouter.Params) (*Announce, error) { q, err := query.New(r.URL.RawQuery) if err != nil { return nil, err @@ -144,8 +144,7 @@ func NewAnnounce(r *http.Request, conf *config.Config) (*Announce, error) { infohash, _ := q.Params["info_hash"] peerID, _ := q.Params["peer_id"] - dir, _ := path.Split(r.URL.Path) - numWant := q.RequestedPeerCount(conf.NumWantFallback) + numWant := q.RequestedPeerCount(cfg.NumWantFallback) ip, ipErr := q.RequestedIP(r) port, portErr := q.Uint64("port") @@ -160,13 +159,12 @@ func NewAnnounce(r *http.Request, conf *config.Config) (*Announce, error) { peerID == "" || portErr != nil || uploadedErr != nil || - ipErr != nil || - len(dir) != 34 { + ipErr != nil { return nil, ErrMalformedRequest } return &Announce{ - Config: conf, + Config: cfg, Request: r, Compact: compact, Downloaded: downloaded, @@ -175,7 +173,7 @@ func NewAnnounce(r *http.Request, conf *config.Config) (*Announce, error) { Infohash: infohash, Left: left, NumWant: numWant, - Passkey: dir[1:33], + Passkey: p.ByName("passkey"), PeerID: peerID, Port: port, Uploaded: uploaded, @@ -261,21 +259,12 @@ type Scrape struct { } // NewScrape parses an HTTP request and generates a Scrape. -func NewScrape(r *http.Request, c *config.Config) (*Scrape, error) { +func NewScrape(cfg *config.Config, r *http.Request, p httprouter.Params) (*Scrape, error) { q, err := query.New(r.URL.RawQuery) if err != nil { return nil, err } - var passkey string - if c.Private { - dir, _ := path.Split(r.URL.Path) - if len(dir) != 34 { - return nil, ErrMalformedRequest - } - passkey = dir[1:34] - } - if q.Infohashes == nil { if _, exists := q.Params["infohash"]; !exists { // There aren't any infohashes. @@ -285,10 +274,10 @@ func NewScrape(r *http.Request, c *config.Config) (*Scrape, error) { } return &Scrape{ - Config: c, + Config: cfg, Request: r, - Passkey: passkey, + Passkey: p.ByName("passkey"), Infohashes: q.Infohashes, }, nil } diff --git a/models/query/query.go b/models/query/query.go index 6c74ae5..aa3558d 100644 --- a/models/query/query.go +++ b/models/query/query.go @@ -11,6 +11,7 @@ import ( "net/http" "net/url" "strconv" + "strings" ) // Query represents a parsed URL.Query. @@ -60,7 +61,7 @@ func New(query string) (*Query, error) { return nil, err } - q.Params[keyStr] = valStr + q.Params[strings.ToLower(keyStr)] = valStr if keyStr == "info_hash" { if hasInfohash { @@ -109,7 +110,7 @@ func (q *Query) Uint64(key string) (uint64, error) { // RequestedPeerCount returns the request peer count or the provided fallback. func (q Query) RequestedPeerCount(fallback int) int { - if numWantStr, exists := q.Params["numWant"]; exists { + if numWantStr, exists := q.Params["numwant"]; exists { numWant, err := strconv.Atoi(numWantStr) if err != nil { return fallback @@ -140,7 +141,7 @@ func (q Query) RequestedIP(r *http.Request) (net.IP, error) { } } - if xRealIPs, ok := q.Params["X-Real-Ip"]; ok { + if xRealIPs, ok := q.Params["x-real-ip"]; ok { if ip := net.ParseIP(string(xRealIPs[0])); ip != nil { return ip, nil } diff --git a/server/serve_announce_test.go b/server/serve_announce_test.go deleted file mode 100644 index 4974f7b..0000000 --- a/server/serve_announce_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2013 The Chihaya Authors. All rights reserved. -// Use of this source code is governed by the BSD 2-Clause license, -// which can be found in the LICENSE file. - -package server - -import ( - "net" - "net/http" - "net/http/httptest" - "testing" - - "github.com/chihaya/chihaya/config" - "github.com/chihaya/chihaya/drivers/backend" - _ "github.com/chihaya/chihaya/drivers/backend/mock" - "github.com/chihaya/chihaya/drivers/tracker" - _ "github.com/chihaya/chihaya/drivers/tracker/mock" - "github.com/chihaya/chihaya/models" -) - -func TestAnnounce(t *testing.T) { - s, err := New(config.New()) - if err != nil { - t.Error(err) - } - - err = s.Prime(func(t tracker.Pool, b backend.Conn) (err error) { - conn, err := t.Get() - if err != nil { - return - } - - err = conn.AddUser(&models.User{ - ID: 1, - Passkey: "yby47f04riwpndba456rqxtmifenq5h6", - }) - if err != nil { - return - } - - err = conn.WhitelistClient("TR2820") - if err != nil { - return - } - - torrent := &models.Torrent{ - ID: 1, - Infohash: string([]byte{0x89, 0xd4, 0xbc, 0x52, 0x11, 0x16, 0xca, 0x1d, 0x42, 0xa2, 0xf3, 0x0d, 0x1f, 0x27, 0x4d, 0x94, 0xe4, 0x68, 0x1d, 0xaf}), - Seeders: make(map[string]models.Peer), - Leechers: make(map[string]models.Peer), - } - - err = conn.AddTorrent(torrent) - if err != nil { - return - } - - err = conn.AddLeecher(torrent, &models.Peer{ - ID: "-TR2820-l71jtqkl898b", - UserID: 1, - TorrentID: torrent.ID, - IP: net.ParseIP("127.0.0.1"), - Port: 34000, - Left: 0, - }) - - return - }) - if err != nil { - t.Error(err) - } - - url := "http://localhost:6881/yby47f04riwpndba456rqxtmifenq5h6/announce?info_hash=%89%d4%bcR%11%16%ca%1dB%a2%f3%0d%1f%27M%94%e4h%1d%af&peer_id=-TR2820-l71jtqkl898b&port=51413&uploaded=0&downloaded=0&left=0&numwant=1&key=3c8e3319&compact=0&supportcrypto=1" - r, err := http.NewRequest("GET", url, nil) - if err != nil { - t.Error(err) - } - - w := httptest.NewRecorder() - s.serveAnnounce(w, r) - - if w.Body.String() != "d8:completei1e10:incompletei1e8:intervali1800e12:min intervali900e5:peersld2:ip9:127.0.0.17:peer id20:-TR2820-l71jtqkl898b4:porti34000eeee" { - t.Errorf("improper response from server:\n%s", w.Body.String()) - } - -} diff --git a/server/serve_stats.go b/server/serve_stats.go deleted file mode 100644 index b2d5f6a..0000000 --- a/server/serve_stats.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2013 The Chihaya Authors. All rights reserved. -// Use of this source code is governed by the BSD 2-Clause license, -// which can be found in the LICENSE file. - -package server - -import ( - "encoding/json" - "net/http" - "sync/atomic" - "time" - - "github.com/chihaya/chihaya/config" -) - -type stats struct { - Uptime config.Duration `json:"uptime"` - RPM int64 `json:"req_per_min"` -} - -func (s *Server) serveStats(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - stats, _ := json.Marshal(&stats{ - config.Duration{time.Now().Sub(s.startTime)}, - s.rpm, - }) - - length, _ := w.Write(stats) - w.Header().Set("Content-Length", string(length)) - w.(http.Flusher).Flush() -} - -func (s *Server) updateStats() { - for _ = range time.NewTicker(time.Minute).C { - s.rpm = s.deltaRequests - atomic.StoreInt64(&s.deltaRequests, 0) - } -} diff --git a/server/serve_stats_test.go b/server/serve_stats_test.go deleted file mode 100644 index b6db80c..0000000 --- a/server/serve_stats_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2013 The Chihaya Authors. All rights reserved. -// Use of this source code is governed by the BSD 2-Clause license, -// which can be found in the LICENSE file. - -package server - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/chihaya/chihaya/config" - _ "github.com/chihaya/chihaya/drivers/backend/mock" - _ "github.com/chihaya/chihaya/drivers/tracker/mock" -) - -func TestStats(t *testing.T) { - s, err := New(config.New()) - if err != nil { - t.Error(err) - } - - r, err := http.NewRequest("GET", "127.0.0.1:80/stats", nil) - if err != nil { - t.Error(err) - } - - w := httptest.NewRecorder() - s.serveStats(w, r) - - if w.Code != 200 { - t.Error("/stats did not return 200 OK") - } - - if w.Header()["Content-Type"][0] != "application/json" { - t.Error("/stats did not return JSON") - } -} diff --git a/server/server.go b/server/server.go deleted file mode 100644 index f4d623f..0000000 --- a/server/server.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2013 The Chihaya Authors. All rights reserved. -// Use of this source code is governed by the BSD 2-Clause license, -// which can be found in the LICENSE file. - -// Package server implements a BitTorrent tracker -package server - -import ( - "errors" - "io" - "net" - "net/http" - "path" - "strconv" - "sync/atomic" - "time" - - "github.com/etix/stoppableListener" - "github.com/golang/glog" - - "github.com/chihaya/chihaya/config" - "github.com/chihaya/chihaya/drivers/backend" - "github.com/chihaya/chihaya/drivers/tracker" -) - -// Server represents BitTorrent tracker server. -type Server struct { - conf *config.Config - - // These are open connections/pools. - listener *stoppableListener.StoppableListener - trackerPool tracker.Pool - backendConn backend.Conn - - // These are for collecting stats. - startTime time.Time - deltaRequests int64 - rpm int64 - - http.Server -} - -// New creates a new Server. -func New(conf *config.Config) (*Server, error) { - trackerPool, err := tracker.Open(&conf.Tracker) - if err != nil { - return nil, err - } - - backendConn, err := backend.Open(&conf.Backend) - if err != nil { - return nil, err - } - - s := &Server{ - conf: conf, - trackerPool: trackerPool, - backendConn: backendConn, - Server: http.Server{ - Addr: conf.Addr, - ReadTimeout: conf.ReadTimeout.Duration, - }, - } - s.Server.Handler = s - - return s, nil -} - -// ListenAndServe starts listening and handling incoming HTTP requests. -func (s *Server) ListenAndServe() error { - l, err := net.Listen("tcp", s.Addr) - if err != nil { - return err - } - - sl := stoppableListener.Handle(l) - s.listener = sl - s.startTime = time.Now() - - go s.updateStats() - s.Serve(s.listener) - - return nil -} - -// Stop cleanly ends the handling of incoming HTTP requests. -func (s *Server) Stop() error { - // Wait for current requests to finish being handled. - s.listener.Stop <- true - - err := s.trackerPool.Close() - if err != nil { - return err - } - - err = s.backendConn.Close() - if err != nil { - return err - } - - return s.listener.Close() -} - -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer atomic.AddInt64(&s.deltaRequests, 1) - r.Close = true - - _, action := path.Split(r.URL.Path) - switch action { - case "announce": - s.serveAnnounce(w, r) - return - case "scrape": - s.serveScrape(w, r) - return - case "stats": - s.serveStats(w, r) - return - default: - fail(errors.New("unknown action"), w, r) - return - } -} - -func fail(err error, w http.ResponseWriter, r *http.Request) { - errmsg := err.Error() - msg := "d14:failure reason" + strconv.Itoa(len(errmsg)) + ":" + errmsg + "e" - length, _ := io.WriteString(w, msg) - w.Header().Add("Content-Length", string(length)) - - w.(http.Flusher).Flush() - - glog.V(5).Infof( - "failed request: ip: %s failure: %s", - r.RemoteAddr, - errmsg, - ) -} diff --git a/server/server_test.go b/server/server_test.go deleted file mode 100644 index 1ddffad..0000000 --- a/server/server_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Chihaya Authors. All rights reserved. -// Use of this source code is governed by the BSD 2-Clause license, -// which can be found in the LICENSE file. - -package server - -import ( - "github.com/chihaya/chihaya/drivers/backend" - "github.com/chihaya/chihaya/drivers/tracker" -) - -// Primer represents a function that can prime drivers with data. -type Primer func(tracker.Pool, backend.Conn) error - -// Prime executes a priming function on the server. -func (s *Server) Prime(p Primer) error { - return p(s.trackerPool, s.backendConn) -}