mirror of
https://github.com/sot-tech/mochi.git
synced 2026-05-29 21:59:27 -07:00
Separate tracker logic from the http package, step 1
This commit is contained in:
300
tracker/models/models.go
Normal file
300
tracker/models/models.go
Normal file
@@ -0,0 +1,300 @@
|
||||
// Copyright 2014 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 models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/chihaya/chihaya/config"
|
||||
"github.com/chihaya/chihaya/models/query"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMalformedRequest is returned when an http.Request does not have the
|
||||
// required parameters to create a model.
|
||||
ErrMalformedRequest = errors.New("malformed request")
|
||||
)
|
||||
|
||||
// Peer is a participant in a swarm.
|
||||
type Peer struct {
|
||||
ID string `json:"id"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
TorrentID uint64 `json:"torrent_id"`
|
||||
|
||||
IP net.IP `json:"ip"`
|
||||
Port uint64 `json:"port"`
|
||||
|
||||
Uploaded uint64 `json:"uploaded"`
|
||||
Downloaded uint64 `json:"downloaded"`
|
||||
Left uint64 `json:"left"`
|
||||
LastAnnounce int64 `json:"last_announce"`
|
||||
}
|
||||
|
||||
// NewPeer returns the Peer representation of an Announce. When provided nil
|
||||
// for the announce parameter, it panics. When provided nil for the user or
|
||||
// torrent parameter, it returns a Peer{UserID: 0} or Peer{TorrentID: 0}
|
||||
// respectively.
|
||||
func NewPeer(a *Announce, u *User, t *Torrent) *Peer {
|
||||
if a == nil {
|
||||
panic("tracker: announce cannot equal nil")
|
||||
}
|
||||
|
||||
var userID uint64
|
||||
if u != nil {
|
||||
userID = u.ID
|
||||
}
|
||||
|
||||
var torrentID uint64
|
||||
if t != nil {
|
||||
torrentID = t.ID
|
||||
}
|
||||
|
||||
return &Peer{
|
||||
ID: a.PeerID,
|
||||
UserID: userID,
|
||||
TorrentID: torrentID,
|
||||
IP: a.IP,
|
||||
Port: a.Port,
|
||||
Uploaded: a.Uploaded,
|
||||
Downloaded: a.Downloaded,
|
||||
Left: a.Left,
|
||||
LastAnnounce: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// Key returns the unique key used to look-up a peer in a swarm (i.e
|
||||
// Torrent.Seeders & Torrent.Leechers).
|
||||
func (p Peer) Key() string {
|
||||
return p.ID + ":" + strconv.FormatUint(p.UserID, 36)
|
||||
}
|
||||
|
||||
// Torrent is a swarm for a given torrent file.
|
||||
type Torrent struct {
|
||||
ID uint64 `json:"id"`
|
||||
Infohash string `json:"infohash"`
|
||||
|
||||
Seeders map[string]Peer `json:"seeders"`
|
||||
Leechers map[string]Peer `json:"leechers"`
|
||||
|
||||
Snatches uint64 `json:"snatches"`
|
||||
UpMultiplier float64 `json:"up_multiplier"`
|
||||
DownMultiplier float64 `json:"down_multiplier"`
|
||||
LastAction int64 `json:"last_action"`
|
||||
}
|
||||
|
||||
// InSeederPool returns true if a peer is within a Torrent's pool of seeders.
|
||||
func (t *Torrent) InSeederPool(p *Peer) (exists bool) {
|
||||
_, exists = t.Seeders[p.Key()]
|
||||
return
|
||||
}
|
||||
|
||||
// InLeecherPool returns true if a peer is within a Torrent's pool of leechers.
|
||||
func (t *Torrent) InLeecherPool(p *Peer) (exists bool) {
|
||||
_, exists = t.Leechers[p.Key()]
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Torrent) PeerCount() int {
|
||||
return len(t.Seeders) + len(t.Leechers)
|
||||
}
|
||||
|
||||
// User is a registered user for private trackers.
|
||||
type User struct {
|
||||
ID uint64 `json:"id"`
|
||||
Passkey string `json:"passkey"`
|
||||
|
||||
UpMultiplier float64 `json:"up_multiplier"`
|
||||
DownMultiplier float64 `json:"down_multiplier"`
|
||||
Snatches uint64 `json:"snatches"`
|
||||
}
|
||||
|
||||
// Announce is an Announce by a Peer.
|
||||
type Announce struct {
|
||||
Config *config.Config `json:"config"`
|
||||
Request *http.Request `json:"request"`
|
||||
|
||||
Compact bool `json:"compact"`
|
||||
Downloaded uint64 `json:"downloaded"`
|
||||
Event string `json:"event"`
|
||||
IP net.IP `json:"ip"`
|
||||
Infohash string `json:"infohash"`
|
||||
Left uint64 `json:"left"`
|
||||
NumWant int `json:"numwant"`
|
||||
Passkey string `json:"passkey"`
|
||||
PeerID string `json:"peer_id"`
|
||||
Port uint64 `json:"port"`
|
||||
Uploaded uint64 `json:"uploaded"`
|
||||
}
|
||||
|
||||
// NewAnnounce parses an HTTP request and generates an Announce.
|
||||
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
|
||||
}
|
||||
|
||||
compact := q.Params["compact"] != "0"
|
||||
event, _ := q.Params["event"]
|
||||
numWant := q.RequestedPeerCount(cfg.NumWantFallback)
|
||||
|
||||
infohash, exists := q.Params["info_hash"]
|
||||
if !exists {
|
||||
return nil, ErrMalformedRequest
|
||||
}
|
||||
|
||||
peerID, exists := q.Params["peer_id"]
|
||||
if !exists {
|
||||
return nil, ErrMalformedRequest
|
||||
}
|
||||
|
||||
ip, err := q.RequestedIP(r)
|
||||
if err != nil {
|
||||
return nil, ErrMalformedRequest
|
||||
}
|
||||
|
||||
port, err := q.Uint64("port")
|
||||
if err != nil {
|
||||
return nil, ErrMalformedRequest
|
||||
}
|
||||
|
||||
left, err := q.Uint64("left")
|
||||
if err != nil {
|
||||
return nil, ErrMalformedRequest
|
||||
}
|
||||
|
||||
downloaded, err := q.Uint64("downloaded")
|
||||
if err != nil {
|
||||
return nil, ErrMalformedRequest
|
||||
}
|
||||
|
||||
uploaded, err := q.Uint64("uploaded")
|
||||
if err != nil {
|
||||
return nil, ErrMalformedRequest
|
||||
}
|
||||
|
||||
return &Announce{
|
||||
Config: cfg,
|
||||
Request: r,
|
||||
Compact: compact,
|
||||
Downloaded: downloaded,
|
||||
Event: event,
|
||||
IP: ip,
|
||||
Infohash: infohash,
|
||||
Left: left,
|
||||
NumWant: numWant,
|
||||
Passkey: p.ByName("passkey"),
|
||||
PeerID: peerID,
|
||||
Port: port,
|
||||
Uploaded: uploaded,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClientID returns the part of a PeerID that identifies a Peer's client
|
||||
// software.
|
||||
func (a Announce) ClientID() (clientID string) {
|
||||
length := len(a.PeerID)
|
||||
if length >= 6 {
|
||||
if a.PeerID[0] == '-' {
|
||||
if length >= 7 {
|
||||
clientID = a.PeerID[1:7]
|
||||
}
|
||||
} else {
|
||||
clientID = a.PeerID[0:6]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AnnounceDelta contains the changes to a Peer's state. These changes are
|
||||
// recorded by the backend driver.
|
||||
type AnnounceDelta struct {
|
||||
Peer *Peer
|
||||
Torrent *Torrent
|
||||
User *User
|
||||
|
||||
// Created is true if this announce created a new peer or changed an existing
|
||||
// peer's address
|
||||
Created bool
|
||||
// Snatched is true if this announce completed the download
|
||||
Snatched bool
|
||||
|
||||
// Uploaded contains the raw upload delta for this announce, in bytes
|
||||
Uploaded uint64
|
||||
// Downloaded contains the raw download delta for this announce, in bytes
|
||||
Downloaded uint64
|
||||
}
|
||||
|
||||
// NewAnnounceDelta calculates a Peer's download and upload deltas between
|
||||
// Announces and generates an AnnounceDelta.
|
||||
func NewAnnounceDelta(a *Announce, p *Peer, u *User, t *Torrent, created, snatched bool) *AnnounceDelta {
|
||||
var (
|
||||
rawDeltaUp = p.Uploaded - a.Uploaded
|
||||
rawDeltaDown uint64
|
||||
)
|
||||
|
||||
if !a.Config.Freeleech {
|
||||
rawDeltaDown = p.Downloaded - a.Downloaded
|
||||
}
|
||||
|
||||
// Restarting a torrent may cause a delta to be negative.
|
||||
if rawDeltaUp < 0 {
|
||||
rawDeltaUp = 0
|
||||
}
|
||||
|
||||
if rawDeltaDown < 0 {
|
||||
rawDeltaDown = 0
|
||||
}
|
||||
|
||||
return &AnnounceDelta{
|
||||
Peer: p,
|
||||
Torrent: t,
|
||||
User: u,
|
||||
|
||||
Created: created,
|
||||
Snatched: snatched,
|
||||
|
||||
Uploaded: uint64(float64(rawDeltaUp) * u.UpMultiplier * t.UpMultiplier),
|
||||
Downloaded: uint64(float64(rawDeltaDown) * u.DownMultiplier * t.DownMultiplier),
|
||||
}
|
||||
}
|
||||
|
||||
// Scrape is a Scrape by a Peer.
|
||||
type Scrape struct {
|
||||
Config *config.Config `json:"config"`
|
||||
Request *http.Request `json:"request"`
|
||||
|
||||
Passkey string
|
||||
Infohashes []string
|
||||
}
|
||||
|
||||
// NewScrape parses an HTTP request and generates a Scrape.
|
||||
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
|
||||
}
|
||||
|
||||
if q.Infohashes == nil {
|
||||
if _, exists := q.Params["info_hash"]; !exists {
|
||||
// There aren't any infohashes.
|
||||
return nil, ErrMalformedRequest
|
||||
}
|
||||
q.Infohashes = []string{q.Params["info_hash"]}
|
||||
}
|
||||
|
||||
return &Scrape{
|
||||
Config: cfg,
|
||||
Request: r,
|
||||
|
||||
Passkey: p.ByName("passkey"),
|
||||
Infohashes: q.Infohashes,
|
||||
}, nil
|
||||
}
|
||||
66
tracker/models/models_test.go
Normal file
66
tracker/models/models_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2014 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type PeerClientPair struct {
|
||||
announce Announce
|
||||
clientID string
|
||||
}
|
||||
|
||||
var TestClients = []PeerClientPair{
|
||||
{Announce{PeerID: "-AZ3034-6wfG2wk6wWLc"}, "AZ3034"},
|
||||
{Announce{PeerID: "-AZ3042-6ozMq5q6Q3NX"}, "AZ3042"},
|
||||
{Announce{PeerID: "-BS5820-oy4La2MWGEFj"}, "BS5820"},
|
||||
{Announce{PeerID: "-AR6360-6oZyyMWoOOBe"}, "AR6360"},
|
||||
{Announce{PeerID: "-AG2083-s1hiF8vGAAg0"}, "AG2083"},
|
||||
{Announce{PeerID: "-AG3003-lEl2Mm4NEO4n"}, "AG3003"},
|
||||
{Announce{PeerID: "-MR1100-00HS~T7*65rm"}, "MR1100"},
|
||||
{Announce{PeerID: "-LK0140-ATIV~nbEQAMr"}, "LK0140"},
|
||||
{Announce{PeerID: "-KT2210-347143496631"}, "KT2210"},
|
||||
{Announce{PeerID: "-TR0960-6ep6svaa61r4"}, "TR0960"},
|
||||
{Announce{PeerID: "-XX1150-dv220cotgj4d"}, "XX1150"},
|
||||
{Announce{PeerID: "-AZ2504-192gwethivju"}, "AZ2504"},
|
||||
{Announce{PeerID: "-KT4310-3L4UvarKuqIu"}, "KT4310"},
|
||||
{Announce{PeerID: "-AZ2060-0xJQ02d4309O"}, "AZ2060"},
|
||||
{Announce{PeerID: "-BD0300-2nkdf08Jd890"}, "BD0300"},
|
||||
{Announce{PeerID: "-A~0010-a9mn9DFkj39J"}, "A~0010"},
|
||||
{Announce{PeerID: "-UT2300-MNu93JKnm930"}, "UT2300"},
|
||||
{Announce{PeerID: "-UT2300-KT4310KT4301"}, "UT2300"},
|
||||
|
||||
{Announce{PeerID: "T03A0----f089kjsdf6e"}, "T03A0-"},
|
||||
{Announce{PeerID: "S58B-----nKl34GoNb75"}, "S58B--"},
|
||||
{Announce{PeerID: "M4-4-0--9aa757Efd5Bl"}, "M4-4-0"},
|
||||
|
||||
{Announce{PeerID: "AZ2500BTeYUzyabAfo6U"}, "AZ2500"}, // BitTyrant
|
||||
{Announce{PeerID: "exbc0JdSklm834kj9Udf"}, "exbc0J"}, // Old BitComet
|
||||
{Announce{PeerID: "FUTB0L84j542mVc84jkd"}, "FUTB0L"}, // Alt BitComet
|
||||
{Announce{PeerID: "XBT054d-8602Jn83NnF9"}, "XBT054"}, // XBT
|
||||
{Announce{PeerID: "OP1011affbecbfabeefb"}, "OP1011"}, // Opera
|
||||
{Announce{PeerID: "-ML2.7.2-kgjjfkd9762"}, "ML2.7."}, // MLDonkey
|
||||
{Announce{PeerID: "-BOWA0C-SDLFJWEIORNM"}, "BOWA0C"}, // Bits on Wheels
|
||||
{Announce{PeerID: "Q1-0-0--dsn34DFn9083"}, "Q1-0-0"}, // Queen Bee
|
||||
{Announce{PeerID: "Q1-10-0-Yoiumn39BDfO"}, "Q1-10-"}, // Queen Bee Alt
|
||||
{Announce{PeerID: "346------SDFknl33408"}, "346---"}, // TorreTopia
|
||||
{Announce{PeerID: "QVOD0054ABFFEDCCDEDB"}, "QVOD00"}, // Qvod
|
||||
|
||||
{Announce{PeerID: ""}, ""},
|
||||
{Announce{PeerID: "-"}, ""},
|
||||
{Announce{PeerID: "12345"}, ""},
|
||||
{Announce{PeerID: "-12345"}, ""},
|
||||
{Announce{PeerID: "123456"}, "123456"},
|
||||
{Announce{PeerID: "-123456"}, "123456"},
|
||||
}
|
||||
|
||||
func TestClientID(t *testing.T) {
|
||||
for _, pair := range TestClients {
|
||||
if parsedID := pair.announce.ClientID(); parsedID != pair.clientID {
|
||||
t.Error("Incorrectly parsed peer ID", pair.announce.PeerID, "as", parsedID)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user