Merge pull request #90 from chihaya/api

Pull API out of HTTP
This commit is contained in:
Jimmy Zelinskie 2015-10-14 13:34:45 -04:00
commit 350cf7cdec
12 changed files with 459 additions and 255 deletions

167
api/api.go Normal file
View file

@ -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)
}
}

167
api/routes.go Normal file
View file

@ -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
}

View file

@ -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()
}()
}

View file

@ -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{

View file

@ -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",

View file

@ -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)
}

View file

@ -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,
},
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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")

View file

@ -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 {