mirror of
https://github.com/sot-tech/mochi.git
synced 2026-07-15 04:48:10 -07:00
transition to httprouter
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
// 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"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/chihaya/chihaya/bencode"
|
||||
"github.com/chihaya/chihaya/drivers/tracker"
|
||||
"github.com/chihaya/chihaya/models"
|
||||
)
|
||||
|
||||
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(w, r, err)
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
conn, err := t.tp.Get()
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
if t.cfg.Whitelist {
|
||||
err = conn.ClientWhitelisted(ann.ClientID())
|
||||
if err != nil {
|
||||
fail(w, r, err)
|
||||
return http.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
var user *models.User
|
||||
if t.cfg.Private {
|
||||
user, err = conn.FindUser(ann.Passkey)
|
||||
if err != nil {
|
||||
fail(w, r, err)
|
||||
return http.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
torrent, err := conn.FindTorrent(ann.Infohash)
|
||||
if err != nil {
|
||||
fail(w, r, err)
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
peer := models.NewPeer(ann, user, torrent)
|
||||
|
||||
created, err := updateTorrent(conn, ann, peer, torrent)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
snatched, err := handleEvent(conn, ann, peer, user, torrent)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
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, ann, user, torrent)
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func updateTorrent(c tracker.Conn, a *models.Announce, p *models.Peer, t *models.Torrent) (created bool, err error) {
|
||||
if !t.Active && a.Left == 0 {
|
||||
err = c.MarkActive(t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case t.InSeederPool(p):
|
||||
err = c.SetSeeder(t, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case t.InLeecherPool(p):
|
||||
err = c.SetLeecher(t, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
if a.Left == 0 {
|
||||
err = c.AddSeeder(t, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = c.AddLeecher(t, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
created = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handleEvent(c tracker.Conn, a *models.Announce, p *models.Peer, u *models.User, t *models.Torrent) (snatched bool, err error) {
|
||||
switch {
|
||||
case a.Event == "stopped" || a.Event == "paused":
|
||||
if t.InSeederPool(p) {
|
||||
err = c.RemoveSeeder(t, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if t.InLeecherPool(p) {
|
||||
err = c.RemoveLeecher(t, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case a.Event == "completed":
|
||||
err = c.IncrementSnatches(t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
snatched = true
|
||||
|
||||
if t.InLeecherPool(p) {
|
||||
err = tracker.LeecherFinished(c, t, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case t.InLeecherPool(p) && a.Left == 0:
|
||||
// A leecher completed but the event was never received
|
||||
err = tracker.LeecherFinished(c, t, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func writeAnnounceResponse(w io.Writer, a *models.Announce, u *models.User, t *models.Torrent) {
|
||||
seedCount := len(t.Seeders)
|
||||
leechCount := len(t.Leechers)
|
||||
|
||||
var peerCount int
|
||||
if a.Left == 0 {
|
||||
peerCount = minInt(a.NumWant, leechCount)
|
||||
} else {
|
||||
peerCount = minInt(a.NumWant, leechCount+seedCount-1)
|
||||
}
|
||||
|
||||
bencoder := bencode.NewEncoder(w)
|
||||
fmt.Fprintf(w, "d")
|
||||
bencoder.Encode("complete")
|
||||
bencoder.Encode(seedCount)
|
||||
bencoder.Encode("incomplete")
|
||||
bencoder.Encode(leechCount)
|
||||
bencoder.Encode("interval")
|
||||
bencoder.Encode(a.Config.Announce.Duration)
|
||||
bencoder.Encode("min interval")
|
||||
bencoder.Encode(a.Config.MinAnnounce.Duration)
|
||||
|
||||
if a.NumWant > 0 && a.Event != "stopped" && a.Event != "paused" {
|
||||
if a.Compact {
|
||||
writePeersCompact(w, a, u, t, peerCount)
|
||||
} else {
|
||||
writePeersList(w, a, u, t, peerCount)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "e")
|
||||
}
|
||||
|
||||
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.
|
||||
bencoder.Encode("peers")
|
||||
fmt.Fprintf(w, "%d:", len(ipv4s)*6)
|
||||
|
||||
for _, peer := range ipv4s {
|
||||
if ip := peer.IP.To4(); ip != nil {
|
||||
w.Write(ip)
|
||||
w.Write([]byte{byte(peer.Port >> 8), byte(peer.Port & 0xff)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ipv6s) > 0 {
|
||||
// 18 is the number of bytes that represents 1 compact IPv6 address.
|
||||
bencoder.Encode("peers6")
|
||||
fmt.Fprintf(w, "%d:", len(ipv6s)*18)
|
||||
|
||||
for _, peer := range ipv6s {
|
||||
if ip := peer.IP.To16(); ip != nil {
|
||||
w.Write(ip)
|
||||
w.Write([]byte{byte(peer.Port >> 8), byte(peer.Port & 0xff)})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
// If they're leeching, prioritize giving them seeders.
|
||||
count := splitPeers(&ipv4s, &ipv6s, a, u, t.Seeders, peerCount)
|
||||
splitPeers(&ipv4s, &ipv6s, a, u, t.Leechers, peerCount-count)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if a.Config.Private && peer.UserID == u.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
if ip := peer.IP.To4(); len(ip) == 4 {
|
||||
*ipv4s = append(*ipv4s, peer)
|
||||
} else if ip := peer.IP.To16(); len(ip) == 16 {
|
||||
*ipv6s = append(*ipv6s, peer)
|
||||
}
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func writePeersList(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)
|
||||
|
||||
bencoder.Encode("peers")
|
||||
fmt.Fprintf(w, "l")
|
||||
|
||||
for _, peer := range ipv4s {
|
||||
writePeerDict(w, &peer)
|
||||
}
|
||||
for _, peer := range ipv6s {
|
||||
writePeerDict(w, &peer)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "e")
|
||||
}
|
||||
|
||||
func writePeerDict(w io.Writer, peer *models.Peer) {
|
||||
bencoder := bencode.NewEncoder(w)
|
||||
fmt.Fprintf(w, "d")
|
||||
bencoder.Encode("ip")
|
||||
bencoder.Encode(peer.IP.String())
|
||||
bencoder.Encode("peer id")
|
||||
bencoder.Encode(peer.ID)
|
||||
bencoder.Encode("port")
|
||||
bencoder.Encode(peer.Port)
|
||||
fmt.Fprintf(w, "e")
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/chihaya/chihaya/bencode"
|
||||
"github.com/chihaya/chihaya/models"
|
||||
)
|
||||
|
||||
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(w, r, err)
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
conn, err := t.tp.Get()
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
if t.cfg.Private {
|
||||
_, err = conn.FindUser(scrape.Passkey)
|
||||
if err != nil {
|
||||
fail(w, r, err)
|
||||
return http.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
var torrents []*models.Torrent
|
||||
for _, infohash := range scrape.Infohashes {
|
||||
torrent, err := conn.FindTorrent(infohash)
|
||||
if err != nil {
|
||||
fail(w, r, err)
|
||||
return http.StatusOK
|
||||
}
|
||||
torrents = append(torrents, torrent)
|
||||
}
|
||||
|
||||
bencoder := bencode.NewEncoder(w)
|
||||
fmt.Fprintf(w, "d")
|
||||
bencoder.Encode("files")
|
||||
for _, torrent := range torrents {
|
||||
writeTorrentStatus(w, torrent)
|
||||
}
|
||||
fmt.Fprintf(w, "e")
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func writeTorrentStatus(w io.Writer, t *models.Torrent) {
|
||||
bencoder := bencode.NewEncoder(w)
|
||||
bencoder.Encode(t.Infohash)
|
||||
fmt.Fprintf(w, "d")
|
||||
bencoder.Encode("complete")
|
||||
bencoder.Encode(len(t.Seeders))
|
||||
bencoder.Encode("downloaded")
|
||||
bencoder.Encode(t.Snatches)
|
||||
bencoder.Encode("incomplete")
|
||||
bencoder.Encode(len(t.Leechers))
|
||||
fmt.Fprintf(w, "e")
|
||||
}
|
||||
Reference in New Issue
Block a user