overhaul everything

This commit is contained in:
Jimmy Zelinskie
2014-06-23 22:47:43 -04:00
parent 18f6c32d97
commit 3bfb3074b4
27 changed files with 1193 additions and 1143 deletions
+265
View File
@@ -0,0 +1,265 @@
// 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 models implements the models for an abstraction over the
// multiple data stores used by a BitTorrent tracker.
package models
import (
"errors"
"net/http"
"path"
"strconv"
"time"
"github.com/chihaya/chihaya/config"
"github.com/chihaya/chihaya/models/query"
)
var (
// ErrMalformedRequest is returned when a request does no have the required
// parameters.
ErrMalformedRequest = errors.New("malformed request")
)
// Peer is the internal representation of a participant in a swarm.
type Peer struct {
ID string `json:"id"`
UserID uint64 `json:"user_id"`
TorrentID uint64 `json:"torrent_id"`
IP string `json:"ip"`
Port uint64 `json:"port"`
Uploaded uint64 `json:"uploaded"`
Downloaded uint64 `json:"downloaded`
Left uint64 `json:"left"`
LastAnnounce int64 `json:"last_announce"`
}
// Key is a helper that returns the proper format for keys used for maps
// of peers (i.e. torrent.Seeders & torrent.Leechers).
func (p Peer) Key() string {
return p.ID + ":" + strconv.FormatUint(p.UserID, 36)
}
// Torrent is the internal representation of a swarm for a given torrent file.
type Torrent struct {
ID uint64 `json:"id"`
Infohash string `json:"infohash"`
Active bool `json:"active"`
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) bool {
_, exists := t.Seeders[p.Key()]
return exists
}
// InLeecherPool returns true if a peer is within a torrent's pool of leechers.
func (t *Torrent) InLeecherPool(p *Peer) bool {
_, exists := t.Leechers[p.Key()]
return exists
}
// NewPeer creates a new peer using the information provided by an announce.
func NewPeer(t *Torrent, u *User, a *Announce) *Peer {
return &Peer{
ID: a.PeerID,
UserID: u.ID,
TorrentID: t.ID,
IP: a.IP,
Port: a.Port,
Uploaded: a.Uploaded,
Downloaded: a.Downloaded,
Left: a.Left,
LastAnnounce: time.Now().Unix(),
}
}
// User is the internal representation of 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 represents all of the data from an announce request.
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 string `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(r *http.Request, conf *config.Config) (*Announce, error) {
q, err := query.New(r.URL.RawQuery)
if err != nil {
return nil, err
}
compact := q.Params["compact"] != "0"
downloaded, downloadedErr := q.Uint64("downloaded")
event, _ := q.Params["event"]
infohash, _ := q.Params["info_hash"]
ip, _ := q.RequestedIP(r)
left, leftErr := q.Uint64("left")
numWant := q.RequestedPeerCount(conf.DefaultNumWant)
dir, _ := path.Split(r.URL.Path)
peerID, _ := q.Params["peer_id"]
port, portErr := q.Uint64("port")
uploaded, uploadedErr := q.Uint64("uploaded")
if downloadedErr != nil ||
infohash == "" ||
leftErr != nil ||
peerID == "" ||
portErr != nil ||
uploadedErr != nil ||
ip == "" ||
len(dir) != 34 {
return nil, ErrMalformedRequest
}
return &Announce{
Config: conf,
Request: r,
Compact: compact,
Downloaded: downloaded,
Event: event,
IP: ip,
Infohash: infohash,
Left: left,
NumWant: numWant,
Passkey: dir[1:33],
PeerID: peerID,
Port: port,
Uploaded: uploaded,
}, nil
}
// ClientID returns the part of a PeerID that identifies the 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 a difference in statistics for a peer.
// It is used for communicating changes to be recorded by the 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 does stuff
func NewAnnounceDelta(p *Peer, u *User, a *Announce, t *Torrent, created, snatched bool) *AnnounceDelta {
rawDeltaUp := p.Uploaded - a.Uploaded
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 represents all of the data from an scrape request.
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(r *http.Request, c *config.Config) (*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.
return nil, ErrMalformedRequest
}
q.Infohashes = []string{q.Params["infohash"]}
}
return &Scrape{
Config: c,
Request: r,
Passkey: passkey,
Infohashes: q.Infohashes,
}, nil
}
+62
View File
@@ -0,0 +1,62 @@
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)
}
}
}
+152
View File
@@ -0,0 +1,152 @@
// 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 query implements a fast, simple URL Query parser.
package query
import (
"errors"
"net/http"
"net/url"
"strconv"
)
// Query represents a parsed URL.Query.
type Query struct {
Infohashes []string
Params map[string]string
}
// New parses a raw url query.
func New(query string) (*Query, error) {
var (
keyStart, keyEnd int
valStart, valEnd int
firstInfohash string
onKey = true
hasInfohash = false
q = &Query{
Infohashes: nil,
Params: make(map[string]string),
}
)
for i, length := 0, len(query); i < length; i++ {
separator := query[i] == '&' || query[i] == ';' || query[i] == '?'
if separator || i == length-1 {
if onKey {
keyStart = i + 1
continue
}
if i == length-1 && !separator {
if query[i] == '=' {
continue
}
valEnd = i
}
keyStr, err := url.QueryUnescape(query[keyStart : keyEnd+1])
if err != nil {
return nil, err
}
valStr, err := url.QueryUnescape(query[valStart : valEnd+1])
if err != nil {
return nil, err
}
q.Params[keyStr] = valStr
if keyStr == "info_hash" {
if hasInfohash {
// Multiple infohashes
if q.Infohashes == nil {
q.Infohashes = []string{firstInfohash}
}
q.Infohashes = append(q.Infohashes, valStr)
} else {
firstInfohash = valStr
hasInfohash = true
}
}
onKey = true
keyStart = i + 1
} else if query[i] == '=' {
onKey = false
valStart = i + 1
} else if onKey {
keyEnd = i
} else {
valEnd = i
}
}
return q, nil
}
// Uint64 is a helper to obtain a uints of any base from a Query. After being
// called, you can safely cast the uint64 to your desired base.
func (q *Query) Uint64(key string) (uint64, error) {
str, exists := q.Params[key]
if !exists {
return 0, errors.New("value does not exist for key: " + key)
}
val, err := strconv.ParseUint(str, 10, 64)
if err != nil {
return 0, err
}
return val, nil
}
// RequestedPeerCount returns the request peer count or the provided fallback.
func (q Query) RequestedPeerCount(fallback int) int {
if numWantStr, exists := q.Params["numWant"]; exists {
numWant, err := strconv.Atoi(numWantStr)
if err != nil {
return fallback
}
return numWant
}
return fallback
}
// RequestedIP returns the requested IP address from a Query.
func (q Query) RequestedIP(r *http.Request) (string, error) {
if ip, ok := q.Params["ip"]; ok {
return ip, nil
}
if ip, ok := q.Params["ipv4"]; ok {
return ip, nil
}
if xRealIPs, ok := q.Params["X-Real-Ip"]; ok {
return string(xRealIPs[0]), nil
}
if r.RemoteAddr == "" {
return "127.0.0.1", nil
}
portIndex := len(r.RemoteAddr) - 1
for ; portIndex >= 0; portIndex-- {
if r.RemoteAddr[portIndex] == ':' {
break
}
}
if portIndex != -1 {
return r.RemoteAddr[0:portIndex], nil
}
return "", errors.New("failed to parse IP address")
}
+98
View File
@@ -0,0 +1,98 @@
// 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 query
import (
"net/url"
"testing"
)
var (
baseAddr = "https://www.subdomain.tracker.com:80/"
testInfoHash = "01234567890123456789"
testPeerID = "-TEST01-6wfG2wk6wWLc"
ValidAnnounceArguments = []url.Values{
url.Values{"info_hash": {testInfoHash}, "peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {testPeerID}, "ip": {"192.168.0.1"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {testPeerID}, "ip": {"192.168.0.1"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "numwant": {"28"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {testPeerID}, "ip": {"192.168.0.1"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "event": {"stopped"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {testPeerID}, "ip": {"192.168.0.1"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "event": {"started"}, "numwant": {"13"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "no_peer_id": {"1"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "compact": {"0"}, "no_peer_id": {"1"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "compact": {"0"}, "no_peer_id": {"1"}, "key": {"peerKey"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {testPeerID}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "compact": {"0"}, "no_peer_id": {"1"}, "key": {"peerKey"}, "trackerid": {"trackerId"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {"%3Ckey%3A+0x90%3E"}, "port": {"6881"}, "downloaded": {"1234"}, "left": {"4321"}, "compact": {"0"}, "no_peer_id": {"1"}, "key": {"peerKey"}, "trackerid": {"trackerId"}},
url.Values{"info_hash": {testInfoHash}, "peer_id": {"%3Ckey%3A+0x90%3E"}, "compact": {"1"}},
}
InvalidQueries = []string{
baseAddr + "announce/?" + "info_hash=%0%a",
}
)
func mapArrayEqual(boxed map[string][]string, unboxed map[string]string) bool {
if len(boxed) != len(unboxed) {
return false
}
for mapKey, mapVal := range boxed {
// Always expect box to hold only one element
if len(mapVal) != 1 || mapVal[0] != unboxed[mapKey] {
return false
}
}
return true
}
func TestValidQueries(t *testing.T) {
for parseIndex, parseVal := range ValidAnnounceArguments {
parsedQueryObj, err := New(baseAddr + "announce/?" + parseVal.Encode())
if err != nil {
t.Error(err)
}
if !mapArrayEqual(parseVal, parsedQueryObj.Params) {
t.Errorf("Incorrect parse at item %d.\n Expected=%v\n Recieved=%v\n", parseIndex, parseVal, parsedQueryObj.Params)
}
}
}
func TestInvalidQueries(t *testing.T) {
for parseIndex, parseStr := range InvalidQueries {
parsedQueryObj, err := New(parseStr)
if err == nil {
t.Error("Should have produced error", parseIndex)
}
if parsedQueryObj != nil {
t.Error("Should be nil after error", parsedQueryObj, parseIndex)
}
}
}
func BenchmarkParseQuery(b *testing.B) {
for bCount := 0; bCount < b.N; bCount++ {
for parseIndex, parseStr := range ValidAnnounceArguments {
parsedQueryObj, err := New(baseAddr + "announce/?" + parseStr.Encode())
if err != nil {
b.Error(err, parseIndex)
b.Log(parsedQueryObj)
}
}
}
}
func BenchmarkURLParseQuery(b *testing.B) {
for bCount := 0; bCount < b.N; bCount++ {
for parseIndex, parseStr := range ValidAnnounceArguments {
parsedQueryObj, err := url.ParseQuery(baseAddr + "announce/?" + parseStr.Encode())
if err != nil {
b.Error(err, parseIndex)
b.Log(parsedQueryObj)
}
}
}
}