transition to httprouter
This commit is contained in:
parent
9cb5b82dc7
commit
6d5290d85e
14 changed files with 353 additions and 503 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
18
example.json
18
example.json
|
@ -1,25 +1,21 @@
|
|||
{
|
||||
"network": "tcp",
|
||||
"addr": ":34000",
|
||||
"addr": ":6881",
|
||||
|
||||
"tracker": {
|
||||
"driver": "redis",
|
||||
"network": "tcp",
|
||||
"host": "127.0.0.1",
|
||||
"port": "6379",
|
||||
"user": "root",
|
||||
"pass": "",
|
||||
"prefix": "test:",
|
||||
"driver": "mock"
|
||||
},
|
||||
|
||||
"max_idle_conns": 3,
|
||||
"idle_timeout": "240s"
|
||||
"backend": {
|
||||
"driver": "mock"
|
||||
},
|
||||
|
||||
"private": true,
|
||||
"freeleech": false,
|
||||
"whitelist": false,
|
||||
|
||||
"announce": "30m",
|
||||
"min_announce": "15m",
|
||||
"read_timeout": "20s",
|
||||
"request_timeout": "10s",
|
||||
"default_num_want": 50
|
||||
}
|
|
@ -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
154
http/announce_test.go
Normal 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
86
http/http.go
Normal 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)
|
||||
}
|
|
@ -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
45
main.go
|
@ -7,7 +7,6 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
|
@ -16,7 +15,7 @@ import (
|
|||
"github.com/chihaya/chihaya/config"
|
||||
_ "github.com/chihaya/chihaya/drivers/backend/mock"
|
||||
_ "github.com/chihaya/chihaya/drivers/tracker/mock"
|
||||
"github.com/chihaya/chihaya/server"
|
||||
"github.com/chihaya/chihaya/http"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -42,50 +41,26 @@ func main() {
|
|||
defer f.Close()
|
||||
|
||||
pprof.StartCPUProfile(f)
|
||||
glog.V(1).Info("started profiling")
|
||||
glog.Info("started profiling")
|
||||
|
||||
defer func() {
|
||||
pprof.StopCPUProfile()
|
||||
glog.V(1).Info("stopped profiling")
|
||||
glog.Info("stopped profiling")
|
||||
}()
|
||||
}
|
||||
|
||||
// Load the config file.
|
||||
conf, err := config.Open(configPath)
|
||||
cfg, err := config.Open(configPath)
|
||||
if err != nil {
|
||||
glog.Fatalf("failed to parse configuration file: %s\n", err)
|
||||
}
|
||||
|
||||
// Create a new server.
|
||||
s, err := server.New(conf)
|
||||
if err != nil {
|
||||
glog.Fatalf("failed to create server: %s\n", err)
|
||||
if cfg == &config.DefaultConfig {
|
||||
glog.Info("using default config")
|
||||
} else {
|
||||
glog.Infof("loaded config file: %s", configPath)
|
||||
}
|
||||
|
||||
// Spawn a goroutine to handle interrupts and safely shut down.
|
||||
go func() {
|
||||
interrupts := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupts, os.Interrupt)
|
||||
|
||||
<-interrupts
|
||||
glog.V(1).Info("caught interrupt, shutting down...")
|
||||
|
||||
err := s.Stop()
|
||||
if err != nil {
|
||||
glog.Fatalf("failed to shutdown cleanly: %s", err)
|
||||
}
|
||||
|
||||
glog.V(1).Info("shutdown cleanly")
|
||||
|
||||
<-interrupts
|
||||
|
||||
glog.Flush()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// Start the server listening and handling requests.
|
||||
err = s.ListenAndServe()
|
||||
if err != nil {
|
||||
glog.Fatalf("failed to start server: %s\n", err)
|
||||
}
|
||||
http.Serve(cfg)
|
||||
glog.Info("gracefully shutdown")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
138
server/server.go
138
server/server.go
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in a new issue