diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..adb8f34 --- /dev/null +++ b/api/api.go @@ -0,0 +1,167 @@ +// Copyright 2015 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 api implements a RESTful HTTP JSON API server for a BitTorrent +// tracker. +package api + +import ( + "net" + "net/http" + "time" + + "github.com/golang/glog" + "github.com/julienschmidt/httprouter" + "github.com/tylerb/graceful" + + "github.com/chihaya/chihaya/config" + "github.com/chihaya/chihaya/stats" + "github.com/chihaya/chihaya/tracker" +) + +// Server represents an API server for a torrent tracker. +type Server struct { + config *config.Config + tracker *tracker.Tracker + grace *graceful.Server + stopping bool +} + +// NewServer returns a new API server for a given configuration and tracker +// instance. +func NewServer(cfg *config.Config, tkr *tracker.Tracker) *Server { + return &Server{ + config: cfg, + tracker: tkr, + } +} + +// Stop cleanly shuts down the server. +func (s *Server) Stop() { + if !s.stopping { + s.grace.Stop(s.grace.Timeout) + } +} + +// Serve runs an API server, blocking until the server has shut down. +func (s *Server) Serve() { + glog.V(0).Info("Starting API on ", s.config.APIConfig.ListenAddr) + + if s.config.APIConfig.ListenLimit != 0 { + glog.V(0).Info("Limiting connections to ", s.config.APIConfig.ListenLimit) + } + + grace := &graceful.Server{ + Timeout: s.config.APIConfig.RequestTimeout.Duration, + ConnState: s.connState, + ListenLimit: s.config.APIConfig.ListenLimit, + + NoSignalHandling: true, + Server: &http.Server{ + Addr: s.config.APIConfig.ListenAddr, + Handler: newRouter(s), + ReadTimeout: s.config.APIConfig.ReadTimeout.Duration, + WriteTimeout: s.config.APIConfig.WriteTimeout.Duration, + }, + } + + s.grace = grace + grace.SetKeepAlivesEnabled(false) + grace.ShutdownInitiated = func() { s.stopping = true } + + if err := grace.ListenAndServe(); err != nil { + if opErr, ok := err.(*net.OpError); !ok || (ok && opErr.Op != "accept") { + glog.Errorf("Failed to gracefully run API server: %s", err.Error()) + return + } + } + + glog.Info("API server shut down cleanly") +} + +// newRouter returns a router with all the routes. +func newRouter(s *Server) *httprouter.Router { + r := httprouter.New() + + if s.config.PrivateEnabled { + r.PUT("/users/:passkey", makeHandler(s.putUser)) + r.DELETE("/users/:passkey", makeHandler(s.delUser)) + } + + if s.config.ClientWhitelistEnabled { + r.GET("/clients/:clientID", makeHandler(s.getClient)) + r.PUT("/clients/:clientID", makeHandler(s.putClient)) + r.DELETE("/clients/:clientID", makeHandler(s.delClient)) + } + + r.GET("/torrents/:infohash", makeHandler(s.getTorrent)) + r.PUT("/torrents/:infohash", makeHandler(s.putTorrent)) + r.DELETE("/torrents/:infohash", makeHandler(s.delTorrent)) + r.GET("/check", makeHandler(s.check)) + r.GET("/stats", makeHandler(s.stats)) + + return r +} + +// connState is used by graceful in order to gracefully shutdown. It also +// keeps track of connection stats. +func (s *Server) connState(conn net.Conn, state http.ConnState) { + switch state { + case http.StateNew: + stats.RecordEvent(stats.AcceptedConnection) + + case http.StateClosed: + stats.RecordEvent(stats.ClosedConnection) + + case http.StateHijacked: + panic("connection impossibly hijacked") + + // Ignore the following cases. + case http.StateActive, http.StateIdle: + + default: + glog.Errorf("Connection transitioned to unknown state %s (%d)", state, state) + } +} + +// ResponseHandler is an HTTP handler that returns a status code. +type ResponseHandler func(http.ResponseWriter, *http.Request, httprouter.Params) (int, error) + +// makeHandler wraps our ResponseHandlers while timing requests, collecting, +// stats, logging, and handling errors. +func makeHandler(handler ResponseHandler) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + start := time.Now() + httpCode, err := handler(w, r, p) + duration := time.Since(start) + + var msg string + if err != nil { + msg = err.Error() + } else if httpCode != http.StatusOK { + msg = http.StatusText(httpCode) + } + + if len(msg) > 0 { + http.Error(w, msg, httpCode) + stats.RecordEvent(stats.ErroredRequest) + } + + if len(msg) > 0 || glog.V(2) { + reqString := r.URL.Path + " " + r.RemoteAddr + if glog.V(3) { + reqString = r.URL.RequestURI() + " " + r.RemoteAddr + } + + if len(msg) > 0 { + glog.Errorf("[API - %9s] %s (%d - %s)", duration, reqString, httpCode, msg) + } else { + glog.Infof("[API - %9s] %s (%d)", duration, reqString, httpCode) + } + } + + stats.RecordEvent(stats.HandledRequest) + stats.RecordTiming(stats.ResponseTime, duration) + } +} diff --git a/api/routes.go b/api/routes.go new file mode 100644 index 0000000..4786ecd --- /dev/null +++ b/api/routes.go @@ -0,0 +1,167 @@ +// Copyright 2015 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 api + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "runtime" + + "github.com/julienschmidt/httprouter" + + "github.com/chihaya/chihaya/stats" + "github.com/chihaya/chihaya/tracker/models" +) + +const jsonContentType = "application/json; charset=UTF-8" + +func handleError(err error) (int, error) { + if err == nil { + return http.StatusOK, nil + } else if _, ok := err.(models.NotFoundError); ok { + stats.RecordEvent(stats.ClientError) + return http.StatusNotFound, nil + } else if _, ok := err.(models.ClientError); ok { + stats.RecordEvent(stats.ClientError) + return http.StatusBadRequest, nil + } + return http.StatusInternalServerError, err +} + +func (s *Server) check(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + // Attempt to ping the backend if private tracker is enabled. + if s.config.PrivateEnabled { + if err := s.tracker.Backend.Ping(); err != nil { + return handleError(err) + } + } + + _, err := w.Write([]byte("STILL-ALIVE")) + return handleError(err) +} + +func (s *Server) stats(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + w.Header().Set("Content-Type", jsonContentType) + + var err error + var val interface{} + query := r.URL.Query() + + stats.DefaultStats.GoRoutines = runtime.NumGoroutine() + + if _, flatten := query["flatten"]; flatten { + val = stats.DefaultStats.Flattened() + } else { + val = stats.DefaultStats + } + + if _, pretty := query["pretty"]; pretty { + var buf []byte + buf, err = json.MarshalIndent(val, "", " ") + + if err == nil { + _, err = w.Write(buf) + } + } else { + err = json.NewEncoder(w).Encode(val) + } + + return handleError(err) +} + +func (s *Server) getTorrent(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + infohash, err := url.QueryUnescape(p.ByName("infohash")) + if err != nil { + return http.StatusNotFound, err + } + + torrent, err := s.tracker.FindTorrent(infohash) + if err != nil { + return handleError(err) + } + + w.Header().Set("Content-Type", jsonContentType) + e := json.NewEncoder(w) + return handleError(e.Encode(torrent)) +} + +func (s *Server) putTorrent(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return http.StatusInternalServerError, err + } + + var torrent models.Torrent + err = json.Unmarshal(body, &torrent) + if err != nil { + return http.StatusBadRequest, err + } + + s.tracker.PutTorrent(&torrent) + return http.StatusOK, nil +} + +func (s *Server) delTorrent(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + infohash, err := url.QueryUnescape(p.ByName("infohash")) + if err != nil { + return http.StatusNotFound, err + } + + s.tracker.DeleteTorrent(infohash) + return http.StatusOK, nil +} + +func (s *Server) getUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + user, err := s.tracker.FindUser(p.ByName("passkey")) + if err == models.ErrUserDNE { + return http.StatusNotFound, err + } else if err != nil { + return http.StatusInternalServerError, err + } + + w.Header().Set("Content-Type", jsonContentType) + e := json.NewEncoder(w) + return handleError(e.Encode(user)) +} + +func (s *Server) putUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return http.StatusInternalServerError, err + } + + var user models.User + err = json.Unmarshal(body, &user) + if err != nil { + return http.StatusBadRequest, err + } + + s.tracker.PutUser(&user) + return http.StatusOK, nil +} + +func (s *Server) delUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + s.tracker.DeleteUser(p.ByName("passkey")) + return http.StatusOK, nil +} + +func (s *Server) getClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + if err := s.tracker.ClientApproved(p.ByName("clientID")); err != nil { + return http.StatusNotFound, err + } + return http.StatusOK, nil +} + +func (s *Server) putClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + s.tracker.PutClient(p.ByName("clientID")) + return http.StatusOK, nil +} + +func (s *Server) delClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { + s.tracker.DeleteClient(p.ByName("clientID")) + return http.StatusOK, nil +} diff --git a/chihaya.go b/chihaya.go index c594ba1..72f7be4 100644 --- a/chihaya.go +++ b/chihaya.go @@ -2,6 +2,9 @@ // Use of this source code is governed by the BSD 2-Clause license, // which can be found in the LICENSE file. +// Package chihaya implements the ability to boot the Chihaya BitTorrent +// tracker with your own imports that can dynamically register additional +// functionality. package chihaya import ( @@ -14,6 +17,7 @@ import ( "github.com/golang/glog" + "github.com/chihaya/chihaya/api" "github.com/chihaya/chihaya/config" "github.com/chihaya/chihaya/http" "github.com/chihaya/chihaya/stats" @@ -34,6 +38,11 @@ func init() { flag.StringVar(&configPath, "config", "", "path to the configuration file") } +type server interface { + Serve() + Stop() +} + // Boot starts Chihaya. By exporting this function, anyone can import their own // custom drivers into their own package main and then call chihaya.Boot. func Boot() { @@ -65,28 +74,29 @@ func Boot() { glog.Fatal("New: ", err) } - var wg sync.WaitGroup - var servers []tracker.Server + var servers []server - if cfg.HTTPListenAddr != "" { - wg.Add(1) - srv := http.NewServer(cfg, tkr) + if cfg.APIConfig.ListenAddr != "" { + srv := api.NewServer(cfg, tkr) servers = append(servers, srv) - - go func() { - defer wg.Done() - srv.Serve(cfg.HTTPListenAddr) - }() } - if cfg.UDPListenAddr != "" { - wg.Add(1) + if cfg.HTTPConfig.ListenAddr != "" { + srv := http.NewServer(cfg, tkr) + servers = append(servers, srv) + } + + if cfg.UDPConfig.ListenAddr != "" { srv := udp.NewServer(cfg, tkr) servers = append(servers, srv) + } + var wg sync.WaitGroup + for _, srv := range servers { + wg.Add(1) go func() { defer wg.Done() - srv.Serve(cfg.UDPListenAddr) + srv.Serve() }() } diff --git a/config/config.go b/config/config.go index e9a613a..b91903b 100644 --- a/config/config.go +++ b/config/config.go @@ -90,24 +90,34 @@ type TrackerConfig struct { WhitelistConfig } -// HTTPConfig is the configuration for HTTP functionality. -type HTTPConfig struct { - HTTPListenAddr string `json:"http_listen_addr"` - HTTPRequestTimeout Duration `json:"http_request_timeout"` - HTTPReadTimeout Duration `json:"http_read_timeout"` - HTTPWriteTimeout Duration `json:"http_write_timeout"` - HTTPListenLimit int `json:"http_listen_limit"` +// APIConfig is the configuration for an HTTP JSON API server. +type APIConfig struct { + ListenAddr string `json:"api_listen_addr"` + RequestTimeout Duration `json:"api_request_timeout"` + ReadTimeout Duration `json:"api_read_timeout"` + WriteTimeout Duration `json:"api_write_timeout"` + ListenLimit int `json:"api_listen_limit"` } -// UDPConfig is the configuration for HTTP functionality. +// HTTPConfig is the configuration for the HTTP protocol. +type HTTPConfig struct { + ListenAddr string `json:"http_listen_addr"` + RequestTimeout Duration `json:"http_request_timeout"` + ReadTimeout Duration `json:"http_read_timeout"` + WriteTimeout Duration `json:"http_write_timeout"` + ListenLimit int `json:"http_listen_limit"` +} + +// UDPConfig is the configuration for the UDP protocol. type UDPConfig struct { - UDPListenAddr string `json:"udp_listen_addr"` - UDPReadBufferSize int `json:"udp_read_buffer_size"` + ListenAddr string `json:"udp_listen_addr"` + ReadBufferSize int `json:"udp_read_buffer_size"` } // Config is the global configuration for an instance of Chihaya. type Config struct { TrackerConfig + APIConfig HTTPConfig UDPConfig DriverConfig @@ -139,15 +149,22 @@ var DefaultConfig = Config{ }, }, + APIConfig: APIConfig{ + ListenAddr: ":6880", + RequestTimeout: Duration{10 * time.Second}, + ReadTimeout: Duration{10 * time.Second}, + WriteTimeout: Duration{10 * time.Second}, + }, + HTTPConfig: HTTPConfig{ - HTTPListenAddr: ":6881", - HTTPRequestTimeout: Duration{10 * time.Second}, - HTTPReadTimeout: Duration{10 * time.Second}, - HTTPWriteTimeout: Duration{10 * time.Second}, + ListenAddr: ":6881", + RequestTimeout: Duration{10 * time.Second}, + ReadTimeout: Duration{10 * time.Second}, + WriteTimeout: Duration{10 * time.Second}, }, UDPConfig: UDPConfig{ - UDPListenAddr: ":6882", + ListenAddr: ":6882", }, DriverConfig: DriverConfig{ diff --git a/example_config.json b/example_config.json index bf24235..1df454a 100644 --- a/example_config.json +++ b/example_config.json @@ -15,6 +15,11 @@ "respect_af": false, "client_whitelist_enabled": false, "client_whitelist": ["OP1011"], + "api_listen_addr": ":6880", + "api_request_timeout": "4s", + "api_read_timeout": "4s", + "api_write_timeout": "4s", + "api_listen_limit": 0, "udp_listen_addr": ":6881", "http_listen_addr": ":6881", "http_request_timeout": "4s", diff --git a/http/announce_test.go b/http/announce_test.go index 1406cea..64d79ef 100644 --- a/http/announce_test.go +++ b/http/announce_test.go @@ -5,9 +5,7 @@ package http import ( - "net/http" "net/http/httptest" - "net/url" "reflect" "strconv" "testing" @@ -20,7 +18,7 @@ import ( ) func TestPublicAnnounce(t *testing.T) { - srv, err := setupTracker(&config.DefaultConfig) + srv, err := setupTracker(nil, nil) if err != nil { t.Fatal(err) } @@ -49,24 +47,25 @@ func TestPublicAnnounce(t *testing.T) { } func TestTorrentPurging(t *testing.T) { - cfg := config.DefaultConfig - srv, err := setupTracker(&cfg) + tkr, err := tracker.New(&config.DefaultConfig) + if err != nil { + t.Fatalf("failed to create new tracker instance: %s", err) + } + + srv, err := setupTracker(nil, tkr) if err != nil { t.Fatal(err) } defer srv.Close() - torrentAPIPath := srv.URL + "/torrents/" + url.QueryEscape(infoHash) - // Add one seeder. peer := makePeerParams("peer1", true) announce(peer, srv) - _, status, err := fetchPath(torrentAPIPath) + // Make sure the torrent was created. + _, err = tkr.FindTorrent(infoHash) if err != nil { - t.Fatal(err) - } else if status != http.StatusOK { - t.Fatalf("expected torrent to exist (got %s)", http.StatusText(status)) + t.Fatalf("expected torrent to exist after announce: %s", err) } // Remove seeder. @@ -74,11 +73,9 @@ func TestTorrentPurging(t *testing.T) { peer["event"] = "stopped" announce(peer, srv) - _, status, err = fetchPath(torrentAPIPath) - if err != nil { - t.Fatal(err) - } else if status != http.StatusNotFound { - t.Fatalf("expected torrent to have been purged (got %s)", http.StatusText(status)) + _, err = tkr.FindTorrent(infoHash) + if err != models.ErrTorrentDNE { + t.Fatalf("expected torrent to have been purged: %s", err) } } @@ -87,23 +84,25 @@ func TestStalePeerPurging(t *testing.T) { cfg.MinAnnounce = config.Duration{10 * time.Millisecond} cfg.ReapInterval = config.Duration{10 * time.Millisecond} - srv, err := setupTracker(&cfg) + tkr, err := tracker.New(&cfg) + if err != nil { + t.Fatalf("failed to create new tracker instance: %s", err) + } + + srv, err := setupTracker(&cfg, tkr) if err != nil { t.Fatal(err) } defer srv.Close() - torrentAPIPath := srv.URL + "/torrents/" + url.QueryEscape(infoHash) - // Add one seeder. peer1 := makePeerParams("peer1", true) announce(peer1, srv) - _, status, err := fetchPath(torrentAPIPath) + // Make sure the torrent was created. + _, err = tkr.FindTorrent(infoHash) if err != nil { - t.Fatal(err) - } else if status != http.StatusOK { - t.Fatalf("expected torrent to exist (got %s)", http.StatusText(status)) + t.Fatalf("expected torrent to exist after announce: %s", err) } // Add a leecher. @@ -115,11 +114,9 @@ func TestStalePeerPurging(t *testing.T) { // Let them both expire. time.Sleep(30 * time.Millisecond) - _, status, err = fetchPath(torrentAPIPath) - if err != nil { - t.Fatal(err) - } else if status != http.StatusNotFound { - t.Fatalf("expected torrent to have been purged (got %s)", http.StatusText(status)) + _, err = tkr.FindTorrent(infoHash) + if err != models.ErrTorrentDNE { + t.Fatalf("expected torrent to have been purged: %s", err) } } @@ -170,7 +167,7 @@ func TestPreferredSubnet(t *testing.T) { cfg.PreferredIPv6Subnet = 16 cfg.DualStackedPeers = false - srv, err := setupTracker(&cfg) + srv, err := setupTracker(&cfg, nil) if err != nil { t.Fatal(err) } @@ -235,7 +232,7 @@ func TestPreferredSubnet(t *testing.T) { } func TestCompactAnnounce(t *testing.T) { - srv, err := setupTracker(&config.DefaultConfig) + srv, err := setupTracker(nil, nil) if err != nil { t.Fatal(err) } diff --git a/http/http.go b/http/http.go index 8a10a5f..df0ac2c 100644 --- a/http/http.go +++ b/http/http.go @@ -2,7 +2,8 @@ // Use of this source code is governed by the BSD 2-Clause license, // which can be found in the LICENSE file. -// Package http implements an http-serving BitTorrent tracker. +// Package http implements a BitTorrent tracker over the HTTP protocol as per +// BEP 3. package http import ( @@ -75,26 +76,11 @@ func newRouter(s *Server) *httprouter.Router { if s.config.PrivateEnabled { r.GET("/users/:passkey/announce", makeHandler(s.serveAnnounce)) r.GET("/users/:passkey/scrape", makeHandler(s.serveScrape)) - - r.PUT("/users/:passkey", makeHandler(s.putUser)) - r.DELETE("/users/:passkey", makeHandler(s.delUser)) } else { r.GET("/announce", makeHandler(s.serveAnnounce)) r.GET("/scrape", makeHandler(s.serveScrape)) } - if s.config.ClientWhitelistEnabled { - r.GET("/clients/:clientID", makeHandler(s.getClient)) - r.PUT("/clients/:clientID", makeHandler(s.putClient)) - r.DELETE("/clients/:clientID", makeHandler(s.delClient)) - } - - r.GET("/torrents/:infohash", makeHandler(s.getTorrent)) - r.PUT("/torrents/:infohash", makeHandler(s.putTorrent)) - r.DELETE("/torrents/:infohash", makeHandler(s.delTorrent)) - r.GET("/check", makeHandler(s.check)) - r.GET("/stats", makeHandler(s.stats)) - return r } @@ -120,24 +106,24 @@ func (s *Server) connState(conn net.Conn, state http.ConnState) { } // Serve runs an HTTP server, blocking until the server has shut down. -func (s *Server) Serve(addr string) { - glog.V(0).Info("Starting HTTP on ", addr) +func (s *Server) Serve() { + glog.V(0).Info("Starting HTTP on ", s.config.HTTPConfig.ListenAddr) - if s.config.HTTPListenLimit != 0 { - glog.V(0).Info("Limiting connections to ", s.config.HTTPListenLimit) + if s.config.HTTPConfig.ListenLimit != 0 { + glog.V(0).Info("Limiting connections to ", s.config.HTTPConfig.ListenLimit) } grace := &graceful.Server{ - Timeout: s.config.HTTPRequestTimeout.Duration, + Timeout: s.config.HTTPConfig.RequestTimeout.Duration, ConnState: s.connState, - ListenLimit: s.config.HTTPListenLimit, + ListenLimit: s.config.HTTPConfig.ListenLimit, NoSignalHandling: true, Server: &http.Server{ - Addr: addr, + Addr: s.config.HTTPConfig.ListenAddr, Handler: newRouter(s), - ReadTimeout: s.config.HTTPReadTimeout.Duration, - WriteTimeout: s.config.HTTPWriteTimeout.Duration, + ReadTimeout: s.config.HTTPConfig.ReadTimeout.Duration, + WriteTimeout: s.config.HTTPConfig.WriteTimeout.Duration, }, } diff --git a/http/http_test.go b/http/http_test.go index 19a7aca..8e102dc 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -27,10 +27,17 @@ func init() { stats.DefaultStats = stats.New(config.StatsConfig{}) } -func setupTracker(cfg *config.Config) (*httptest.Server, error) { - tkr, err := tracker.New(cfg) - if err != nil { - return nil, err +func setupTracker(cfg *config.Config, tkr *tracker.Tracker) (*httptest.Server, error) { + if cfg == nil { + cfg = &config.DefaultConfig + } + + if tkr == nil { + var err error + tkr, err = tracker.New(cfg) + if err != nil { + return nil, err + } } return createServer(tkr, cfg) } diff --git a/http/routes.go b/http/routes.go index b405b65..4dea41b 100644 --- a/http/routes.go +++ b/http/routes.go @@ -5,11 +5,7 @@ package http import ( - "encoding/json" - "io/ioutil" "net/http" - "net/url" - "runtime" "github.com/julienschmidt/httprouter" @@ -17,62 +13,6 @@ import ( "github.com/chihaya/chihaya/tracker/models" ) -const jsonContentType = "application/json; charset=UTF-8" - -func handleError(err error) (int, error) { - if err == nil { - return http.StatusOK, nil - } else if _, ok := err.(models.NotFoundError); ok { - stats.RecordEvent(stats.ClientError) - return http.StatusNotFound, nil - } else if _, ok := err.(models.ClientError); ok { - stats.RecordEvent(stats.ClientError) - return http.StatusBadRequest, nil - } - return http.StatusInternalServerError, err -} - -func (s *Server) check(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - // Attempt to ping the backend if private tracker is enabled. - if s.config.PrivateEnabled { - if err := s.tracker.Backend.Ping(); err != nil { - return handleError(err) - } - } - - _, err := w.Write([]byte("STILL-ALIVE")) - return handleError(err) -} - -func (s *Server) stats(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - w.Header().Set("Content-Type", jsonContentType) - - var err error - var val interface{} - query := r.URL.Query() - - stats.DefaultStats.GoRoutines = runtime.NumGoroutine() - - if _, flatten := query["flatten"]; flatten { - val = stats.DefaultStats.Flattened() - } else { - val = stats.DefaultStats - } - - if _, pretty := query["pretty"]; pretty { - var buf []byte - buf, err = json.MarshalIndent(val, "", " ") - - if err == nil { - _, err = w.Write(buf) - } - } else { - err = json.NewEncoder(w).Encode(val) - } - - return handleError(err) -} - func handleTorrentError(err error, w *Writer) (int, error) { if err == nil { return http.StatusOK, nil @@ -104,96 +44,3 @@ func (s *Server) serveScrape(w http.ResponseWriter, r *http.Request, p httproute return handleTorrentError(s.tracker.HandleScrape(scrape, writer), writer) } - -func (s *Server) getTorrent(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - infohash, err := url.QueryUnescape(p.ByName("infohash")) - if err != nil { - return http.StatusNotFound, err - } - - torrent, err := s.tracker.FindTorrent(infohash) - if err != nil { - return handleError(err) - } - - w.Header().Set("Content-Type", jsonContentType) - e := json.NewEncoder(w) - return handleError(e.Encode(torrent)) -} - -func (s *Server) putTorrent(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - return http.StatusInternalServerError, err - } - - var torrent models.Torrent - err = json.Unmarshal(body, &torrent) - if err != nil { - return http.StatusBadRequest, err - } - - s.tracker.PutTorrent(&torrent) - return http.StatusOK, nil -} - -func (s *Server) delTorrent(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - infohash, err := url.QueryUnescape(p.ByName("infohash")) - if err != nil { - return http.StatusNotFound, err - } - - s.tracker.DeleteTorrent(infohash) - return http.StatusOK, nil -} - -func (s *Server) getUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - user, err := s.tracker.FindUser(p.ByName("passkey")) - if err == models.ErrUserDNE { - return http.StatusNotFound, err - } else if err != nil { - return http.StatusInternalServerError, err - } - - w.Header().Set("Content-Type", jsonContentType) - e := json.NewEncoder(w) - return handleError(e.Encode(user)) -} - -func (s *Server) putUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - return http.StatusInternalServerError, err - } - - var user models.User - err = json.Unmarshal(body, &user) - if err != nil { - return http.StatusBadRequest, err - } - - s.tracker.PutUser(&user) - return http.StatusOK, nil -} - -func (s *Server) delUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - s.tracker.DeleteUser(p.ByName("passkey")) - return http.StatusOK, nil -} - -func (s *Server) getClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - if err := s.tracker.ClientApproved(p.ByName("clientID")); err != nil { - return http.StatusNotFound, err - } - return http.StatusOK, nil -} - -func (s *Server) putClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - s.tracker.PutClient(p.ByName("clientID")) - return http.StatusOK, nil -} - -func (s *Server) delClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { - s.tracker.DeleteClient(p.ByName("clientID")) - return http.StatusOK, nil -} diff --git a/http/scrape_test.go b/http/scrape_test.go index 8dc89f1..e2c174a 100644 --- a/http/scrape_test.go +++ b/http/scrape_test.go @@ -13,11 +13,10 @@ import ( "testing" "github.com/chihaya/bencode" - "github.com/chihaya/chihaya/config" ) func TestPublicScrape(t *testing.T) { - srv, err := setupTracker(&config.DefaultConfig) + srv, err := setupTracker(nil, nil) if err != nil { t.Fatal(err) } diff --git a/udp/udp.go b/udp/udp.go index 7b8066a..fdc0b78 100644 --- a/udp/udp.go +++ b/udp/udp.go @@ -2,8 +2,8 @@ // Use of this source code is governed by the BSD 2-Clause license, // which can be found in the LICENSE file. -// Package udp implements a UDP BitTorrent tracker per BEP 15. -// IPv6 is currently unsupported as there is no widely-implemented standard. +// Package udp implements a BitTorrent tracker over the UDP protocol as per +// BEP 15. package udp import ( @@ -29,12 +29,12 @@ type Server struct { connIDGen *ConnectionIDGenerator } -func (s *Server) serve(listenAddr string) error { +func (s *Server) serve() error { if s.sock != nil { return errors.New("server already booted") } - udpAddr, err := net.ResolveUDPAddr("udp", listenAddr) + udpAddr, err := net.ResolveUDPAddr("udp", s.config.UDPConfig.ListenAddr) if err != nil { close(s.booting) return err @@ -47,8 +47,8 @@ func (s *Server) serve(listenAddr string) error { } defer sock.Close() - if s.config.UDPReadBufferSize > 0 { - sock.SetReadBuffer(s.config.UDPReadBufferSize) + if s.config.UDPConfig.ReadBufferSize > 0 { + sock.SetReadBuffer(s.config.UDPConfig.ReadBufferSize) } pool := bufferpool.New(1000, 2048) @@ -92,17 +92,20 @@ func (s *Server) serve(listenAddr string) error { } // Serve runs a UDP server, blocking until the server has shut down. -func (s *Server) Serve(addr string) { - glog.V(0).Info("Starting UDP on ", addr) +func (s *Server) Serve() { + glog.V(0).Info("Starting UDP on ", s.config.UDPConfig.ListenAddr) go func() { // Generate a new IV every hour. for range time.Tick(time.Hour) { + if s.done { + return + } s.connIDGen.NewIV() } }() - if err := s.serve(addr); err != nil { + if err := s.serve(); err != nil { glog.Errorf("Failed to run UDP server: %s", err.Error()) } else { glog.Info("UDP server shut down cleanly") diff --git a/udp/udp_test.go b/udp/udp_test.go index 463f010..99e306d 100644 --- a/udp/udp_test.go +++ b/udp/udp_test.go @@ -19,7 +19,6 @@ import ( ) var ( - testPort = "34137" connectAction = []byte{0, 0, 0, byte(connectActionID)} announceAction = []byte{0, 0, 0, byte(announceActionID)} scrapeAction = []byte{0, 0, 0, byte(scrapeActionID)} @@ -36,7 +35,7 @@ func setupTracker(cfg *config.Config) (*Server, chan struct{}, error) { done := make(chan struct{}) go func() { - if err := srv.serve(":" + testPort); err != nil { + if err := srv.serve(); err != nil { panic(err) } close(done) @@ -47,7 +46,7 @@ func setupTracker(cfg *config.Config) (*Server, chan struct{}, error) { } func setupSocket() (*net.UDPAddr, *net.UDPConn, error) { - srvAddr, err := net.ResolveUDPAddr("udp", "localhost:"+testPort) + srvAddr, err := net.ResolveUDPAddr("udp", config.DefaultConfig.UDPConfig.ListenAddr) if err != nil { return nil, nil, err } @@ -57,7 +56,7 @@ func setupSocket() (*net.UDPAddr, *net.UDPConn, error) { return nil, nil, err } - return srvAddr, sock, err + return srvAddr, sock, nil } func makeTransactionID() []byte {