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" "io"
"os" "os"
"time" "time"
"github.com/golang/glog"
) )
// Duration wraps a time.Duration and adds JSON marshalling. // Duration wraps a time.Duration and adds JSON marshalling.
@ -57,16 +55,15 @@ type Config struct {
Private bool `json:"private"` Private bool `json:"private"`
Freeleech bool `json:"freeleech"` Freeleech bool `json:"freeleech"`
Whitelist bool `json:"whitelist"`
Announce Duration `json:"announce"` Announce Duration `json:"announce"`
MinAnnounce Duration `json:"min_announce"` MinAnnounce Duration `json:"min_announce"`
ReadTimeout Duration `json:"read_timeout"` RequestTimeout Duration `json:"request_timeout"`
NumWantFallback int `json:"default_num_want"` NumWantFallback int `json:"default_num_want"`
} }
// New returns a default configuration. var DefaultConfig = Config{
func New() *Config {
return &Config{
Addr: ":6881", Addr: ":6881",
Tracker: DriverConfig{ Tracker: DriverConfig{
Driver: "mock", Driver: "mock",
@ -76,20 +73,19 @@ func New() *Config {
}, },
Private: false, Private: false,
Freeleech: false, Freeleech: false,
Whitelist: false,
Announce: Duration{30 * time.Minute}, Announce: Duration{30 * time.Minute},
MinAnnounce: Duration{15 * time.Minute}, MinAnnounce: Duration{15 * time.Minute},
ReadTimeout: Duration{20 % time.Second}, RequestTimeout: Duration{10 * time.Second},
NumWantFallback: 50, NumWantFallback: 50,
} }
}
// Open is a shortcut to open a file, read it, and generate a Config. // Open is a shortcut to open a file, read it, and generate a Config.
// It supports relative and absolute paths. Given "", it returns the result of // It supports relative and absolute paths. Given "", it returns the result of
// New. // New.
func Open(path string) (*Config, error) { func Open(path string) (*Config, error) {
if path == "" { if path == "" {
glog.V(1).Info("using default config") return &DefaultConfig, nil
return New(), nil
} }
f, err := os.Open(os.ExpandEnv(path)) f, err := os.Open(os.ExpandEnv(path))
@ -102,7 +98,6 @@ func Open(path string) (*Config, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
glog.V(1).Infof("loaded config file: %s", path)
return conf, nil return conf, nil
} }

View file

@ -1,25 +1,21 @@
{ {
"network": "tcp", "network": "tcp",
"addr": ":34000", "addr": ":6881",
"tracker": { "tracker": {
"driver": "redis", "driver": "mock"
"network": "tcp", },
"host": "127.0.0.1",
"port": "6379",
"user": "root",
"pass": "",
"prefix": "test:",
"max_idle_conns": 3, "backend": {
"idle_timeout": "240s" "driver": "mock"
}, },
"private": true, "private": true,
"freeleech": false, "freeleech": false,
"whitelist": false,
"announce": "30m", "announce": "30m",
"min_announce": "15m", "min_announce": "15m",
"read_timeout": "20s", "request_timeout": "10s",
"default_num_want": 50 "default_num_want": 50
} }

View file

@ -2,87 +2,78 @@
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.
package server package http
import ( import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"github.com/golang/glog" "github.com/julienschmidt/httprouter"
"github.com/chihaya/chihaya/bencode" "github.com/chihaya/chihaya/bencode"
"github.com/chihaya/chihaya/drivers/tracker" "github.com/chihaya/chihaya/drivers/tracker"
"github.com/chihaya/chihaya/models" "github.com/chihaya/chihaya/models"
) )
func (s Server) serveAnnounce(w http.ResponseWriter, r *http.Request) { func (t *Tracker) ServeAnnounce(w http.ResponseWriter, r *http.Request, p httprouter.Params) int {
announce, err := models.NewAnnounce(r, s.conf) ann, err := models.NewAnnounce(t.cfg, r, p)
if err != nil { if err != nil {
fail(err, w, r) fail(w, r, err)
return return http.StatusOK
} }
conn, err := s.trackerPool.Get() conn, err := t.tp.Get()
if err != nil { if err != nil {
fail(err, w, r) return http.StatusInternalServerError
return
} }
err = conn.ClientWhitelisted(announce.ClientID()) if t.cfg.Whitelist {
err = conn.ClientWhitelisted(ann.ClientID())
if err != nil { if err != nil {
fail(err, w, r) fail(w, r, err)
return return http.StatusOK
}
} }
var user *models.User var user *models.User
if s.conf.Private { if t.cfg.Private {
user, err = conn.FindUser(announce.Passkey) user, err = conn.FindUser(ann.Passkey)
if err != nil { if err != nil {
fail(err, w, r) fail(w, r, err)
return return http.StatusOK
} }
} }
torrent, err := conn.FindTorrent(announce.Infohash) torrent, err := conn.FindTorrent(ann.Infohash)
if err != nil { if err != nil {
fail(err, w, r) fail(w, r, err)
return 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 { if err != nil {
fail(err, w, r) return http.StatusInternalServerError
return
} }
snatched, err := handleEvent(conn, announce, peer, user, torrent) snatched, err := handleEvent(conn, ann, peer, user, torrent)
if err != nil { if err != nil {
fail(err, w, r) return http.StatusInternalServerError
return
} }
if s.conf.Private { if t.cfg.Private {
delta := models.NewAnnounceDelta(announce, peer, user, torrent, created, snatched) delta := models.NewAnnounceDelta(ann, peer, user, torrent, created, snatched)
s.backendConn.RecordAnnounce(delta) 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() return http.StatusOK
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)
}
} }
func updateTorrent(c tracker.Conn, a *models.Announce, p *models.Peer, t *models.Torrent) (created bool, err error) { 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) { 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) ipv4s, ipv6s := getPeers(a, u, t, peerCount)
if len(ipv4s) > 0 { if len(ipv4s) > 0 {
// 6 is the number of bytes that represents 1 compact IPv4 address. // 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 { for _, peer := range ipv4s {
if ip := peer.IP.To4(); ip != nil { 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 { if len(ipv6s) > 0 {
// 18 is the number of bytes that represents 1 compact IPv6 address. // 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 { for _, peer := range ipv6s {
if ip := peer.IP.To16(); ip != nil { 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 a.Left == 0 {
// If they're seeding, give them only leechers. // If they're seeding, give them only leechers.
splitPeers(&ipv4s, &ipv6s, a, u, t.Leechers, peerCount) 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 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 { for _, peer := range peers {
if count >= peerCount { if count >= peerCount {
break 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 { 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 { } else if ip := peer.IP.To16(); len(ip) == 16 {
*ipv6s = append(*ipv6s, &peer) *ipv6s = append(*ipv6s, peer)
} }
count++ count++
@ -269,10 +263,10 @@ func writePeersList(w io.Writer, a *models.Announce, u *models.User, t *models.T
fmt.Fprintf(w, "l") fmt.Fprintf(w, "l")
for _, peer := range ipv4s { for _, peer := range ipv4s {
writePeerDict(w, peer) writePeerDict(w, &peer)
} }
for _, peer := range ipv6s { for _, peer := range ipv6s {
writePeerDict(w, peer) writePeerDict(w, &peer)
} }
fmt.Fprintf(w, "e") 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, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.
package server package http
import ( import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings"
"github.com/golang/glog" "github.com/julienschmidt/httprouter"
"github.com/chihaya/chihaya/bencode" "github.com/chihaya/chihaya/bencode"
"github.com/chihaya/chihaya/models" "github.com/chihaya/chihaya/models"
) )
func (s *Server) serveScrape(w http.ResponseWriter, r *http.Request) { func (t *Tracker) ServeScrape(w http.ResponseWriter, r *http.Request, p httprouter.Params) int {
scrape, err := models.NewScrape(r, s.conf) scrape, err := models.NewScrape(t.cfg, r, p)
if err != nil { if err != nil {
fail(err, w, r) fail(w, r, err)
return return http.StatusOK
} }
conn, err := s.trackerPool.Get() conn, err := t.tp.Get()
if err != nil { if err != nil {
fail(err, w, r) return http.StatusInternalServerError
} }
var user *models.User if t.cfg.Private {
if s.conf.Private { _, err = conn.FindUser(scrape.Passkey)
user, err = conn.FindUser(scrape.Passkey)
if err != nil { if err != nil {
fail(err, w, r) fail(w, r, err)
return return http.StatusOK
} }
} }
var ( var torrents []*models.Torrent
torrents []*models.Torrent
torrentIDs []string
)
for _, infohash := range scrape.Infohashes { for _, infohash := range scrape.Infohashes {
torrent, err := conn.FindTorrent(infohash) torrent, err := conn.FindTorrent(infohash)
if err != nil { if err != nil {
fail(err, w, r) fail(w, r, err)
return return http.StatusOK
} }
torrents = append(torrents, torrent) torrents = append(torrents, torrent)
torrentIDs = append(torrentIDs, string(torrent.ID))
} }
bencoder := bencode.NewEncoder(w) bencoder := bencode.NewEncoder(w)
@ -59,22 +53,7 @@ func (s *Server) serveScrape(w http.ResponseWriter, r *http.Request) {
} }
fmt.Fprintf(w, "e") fmt.Fprintf(w, "e")
w.(http.Flusher).Flush() return http.StatusOK
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, ", "),
)
}
} }
func writeTorrentStatus(w io.Writer, t *models.Torrent) { func writeTorrentStatus(w io.Writer, t *models.Torrent) {

45
main.go
View file

@ -7,7 +7,6 @@ package main
import ( import (
"flag" "flag"
"os" "os"
"os/signal"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
@ -16,7 +15,7 @@ import (
"github.com/chihaya/chihaya/config" "github.com/chihaya/chihaya/config"
_ "github.com/chihaya/chihaya/drivers/backend/mock" _ "github.com/chihaya/chihaya/drivers/backend/mock"
_ "github.com/chihaya/chihaya/drivers/tracker/mock" _ "github.com/chihaya/chihaya/drivers/tracker/mock"
"github.com/chihaya/chihaya/server" "github.com/chihaya/chihaya/http"
) )
var ( var (
@ -42,50 +41,26 @@ func main() {
defer f.Close() defer f.Close()
pprof.StartCPUProfile(f) pprof.StartCPUProfile(f)
glog.V(1).Info("started profiling") glog.Info("started profiling")
defer func() { defer func() {
pprof.StopCPUProfile() pprof.StopCPUProfile()
glog.V(1).Info("stopped profiling") glog.Info("stopped profiling")
}() }()
} }
// Load the config file. // Load the config file.
conf, err := config.Open(configPath) cfg, err := config.Open(configPath)
if err != nil { if err != nil {
glog.Fatalf("failed to parse configuration file: %s\n", err) glog.Fatalf("failed to parse configuration file: %s\n", err)
} }
if cfg == &config.DefaultConfig {
// Create a new server. glog.Info("using default config")
s, err := server.New(conf) } else {
if err != nil { glog.Infof("loaded config file: %s", configPath)
glog.Fatalf("failed to create server: %s\n", err)
} }
// 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. // Start the server listening and handling requests.
err = s.ListenAndServe() http.Serve(cfg)
if err != nil { glog.Info("gracefully shutdown")
glog.Fatalf("failed to start server: %s\n", err)
}
} }

View file

@ -10,12 +10,12 @@ import (
"errors" "errors"
"net" "net"
"net/http" "net/http"
"path"
"strconv" "strconv"
"time" "time"
"github.com/chihaya/chihaya/config" "github.com/chihaya/chihaya/config"
"github.com/chihaya/chihaya/models/query" "github.com/chihaya/chihaya/models/query"
"github.com/julienschmidt/httprouter"
) )
var ( var (
@ -133,7 +133,7 @@ type Announce struct {
} }
// NewAnnounce parses an HTTP request and generates an Announce. // 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) q, err := query.New(r.URL.RawQuery)
if err != nil { if err != nil {
return nil, err return nil, err
@ -144,8 +144,7 @@ func NewAnnounce(r *http.Request, conf *config.Config) (*Announce, error) {
infohash, _ := q.Params["info_hash"] infohash, _ := q.Params["info_hash"]
peerID, _ := q.Params["peer_id"] peerID, _ := q.Params["peer_id"]
dir, _ := path.Split(r.URL.Path) numWant := q.RequestedPeerCount(cfg.NumWantFallback)
numWant := q.RequestedPeerCount(conf.NumWantFallback)
ip, ipErr := q.RequestedIP(r) ip, ipErr := q.RequestedIP(r)
port, portErr := q.Uint64("port") port, portErr := q.Uint64("port")
@ -160,13 +159,12 @@ func NewAnnounce(r *http.Request, conf *config.Config) (*Announce, error) {
peerID == "" || peerID == "" ||
portErr != nil || portErr != nil ||
uploadedErr != nil || uploadedErr != nil ||
ipErr != nil || ipErr != nil {
len(dir) != 34 {
return nil, ErrMalformedRequest return nil, ErrMalformedRequest
} }
return &Announce{ return &Announce{
Config: conf, Config: cfg,
Request: r, Request: r,
Compact: compact, Compact: compact,
Downloaded: downloaded, Downloaded: downloaded,
@ -175,7 +173,7 @@ func NewAnnounce(r *http.Request, conf *config.Config) (*Announce, error) {
Infohash: infohash, Infohash: infohash,
Left: left, Left: left,
NumWant: numWant, NumWant: numWant,
Passkey: dir[1:33], Passkey: p.ByName("passkey"),
PeerID: peerID, PeerID: peerID,
Port: port, Port: port,
Uploaded: uploaded, Uploaded: uploaded,
@ -261,21 +259,12 @@ type Scrape struct {
} }
// NewScrape parses an HTTP request and generates a Scrape. // 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) q, err := query.New(r.URL.RawQuery)
if err != nil { if err != nil {
return nil, err 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 q.Infohashes == nil {
if _, exists := q.Params["infohash"]; !exists { if _, exists := q.Params["infohash"]; !exists {
// There aren't any infohashes. // There aren't any infohashes.
@ -285,10 +274,10 @@ func NewScrape(r *http.Request, c *config.Config) (*Scrape, error) {
} }
return &Scrape{ return &Scrape{
Config: c, Config: cfg,
Request: r, Request: r,
Passkey: passkey, Passkey: p.ByName("passkey"),
Infohashes: q.Infohashes, Infohashes: q.Infohashes,
}, nil }, nil
} }

View file

@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
) )
// Query represents a parsed URL.Query. // Query represents a parsed URL.Query.
@ -60,7 +61,7 @@ func New(query string) (*Query, error) {
return nil, err return nil, err
} }
q.Params[keyStr] = valStr q.Params[strings.ToLower(keyStr)] = valStr
if keyStr == "info_hash" { if keyStr == "info_hash" {
if hasInfohash { if hasInfohash {
@ -109,7 +110,7 @@ func (q *Query) Uint64(key string) (uint64, error) {
// RequestedPeerCount returns the request peer count or the provided fallback. // RequestedPeerCount returns the request peer count or the provided fallback.
func (q Query) RequestedPeerCount(fallback int) int { 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) numWant, err := strconv.Atoi(numWantStr)
if err != nil { if err != nil {
return fallback 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 { if ip := net.ParseIP(string(xRealIPs[0])); ip != nil {
return 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)
}