transition to httprouter

This commit is contained in:
Jimmy Zelinskie 2014-07-01 21:40:29 -04:00
parent 9cb5b82dc7
commit 6d5290d85e
14 changed files with 353 additions and 503 deletions

View file

@ -10,8 +10,6 @@ import (
"io"
"os"
"time"
"github.com/golang/glog"
)
// Duration wraps a time.Duration and adds JSON marshalling.
@ -57,16 +55,15 @@ 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{
var DefaultConfig = Config{
Addr: ":6881",
Tracker: DriverConfig{
Driver: "mock",
@ -76,11 +73,11 @@ func New() *Config {
},
Private: false,
Freeleech: false,
Whitelist: false,
Announce: Duration{30 * time.Minute},
MinAnnounce: Duration{15 * time.Minute},
ReadTimeout: Duration{20 % time.Second},
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
}

View file

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

View file

@ -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 t.cfg.Whitelist {
err = conn.ClientWhitelisted(ann.ClientID())
if err != nil {
fail(err, w, r)
return
fail(w, r, err)
return http.StatusOK
}
}
var user *models.User
if s.conf.Private {
user, err = conn.FindUser(announce.Passkey)
if t.cfg.Private {
user, err = conn.FindUser(ann.Passkey)
if err != nil {
fail(err, w, r)
return
fail(w, r, err)
return http.StatusOK
}
}
torrent, err := conn.FindTorrent(announce.Infohash)
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")

154
http/announce_test.go Normal file
View file

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

86
http/http.go Normal file
View file

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

View file

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

45
main.go
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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