mirror of
https://github.com/sot-tech/mochi.git
synced 2026-07-10 10:38:11 -07:00
Separate tracker logic from the http package, step 1
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
// 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 tracker
|
||||
|
||||
import (
|
||||
"github.com/chihaya/chihaya/tracker/models"
|
||||
)
|
||||
|
||||
func (t *Tracker) HandleAnnounce(ann *models.Announce, w Writer) error {
|
||||
conn, err := t.Pool.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.cfg.Whitelist {
|
||||
err = conn.FindClient(ann.ClientID())
|
||||
if err == ErrClientUnapproved {
|
||||
w.WriteError(err)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var user *models.User
|
||||
if t.cfg.Private {
|
||||
user, err = conn.FindUser(ann.Passkey)
|
||||
if err == ErrUserDNE {
|
||||
w.WriteError(err)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var torrent *models.Torrent
|
||||
torrent, err = conn.FindTorrent(ann.Infohash)
|
||||
switch {
|
||||
case !t.cfg.Private && err == ErrTorrentDNE:
|
||||
torrent = &models.Torrent{
|
||||
Infohash: ann.Infohash,
|
||||
Seeders: make(map[string]models.Peer),
|
||||
Leechers: make(map[string]models.Peer),
|
||||
}
|
||||
|
||||
err = conn.PutTorrent(torrent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case t.cfg.Private && err == ErrTorrentDNE:
|
||||
w.WriteError(err)
|
||||
return nil
|
||||
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
|
||||
peer := models.NewPeer(ann, user, torrent)
|
||||
|
||||
created, err := updateTorrent(conn, ann, peer, torrent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
snatched, err := handleEvent(conn, ann, peer, user, torrent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.cfg.Private {
|
||||
delta := models.NewAnnounceDelta(ann, peer, user, torrent, created, snatched)
|
||||
err = t.backend.RecordAnnounce(delta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if t.cfg.PurgeInactiveTorrents && torrent.PeerCount() == 0 {
|
||||
// Rather than deleting the torrent explicitly, let the tracker driver
|
||||
// ensure there are no race conditions.
|
||||
conn.PurgeInactiveTorrent(torrent.Infohash)
|
||||
}
|
||||
|
||||
return w.WriteAnnounce(newAnnounceResponse(ann, user, torrent))
|
||||
}
|
||||
|
||||
func updateTorrent(c Conn, a *models.Announce, p *models.Peer, t *models.Torrent) (created bool, err error) {
|
||||
c.TouchTorrent(t.Infohash)
|
||||
|
||||
switch {
|
||||
case t.InSeederPool(p):
|
||||
err = c.PutSeeder(t.Infohash, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Seeders[p.Key()] = *p
|
||||
|
||||
case t.InLeecherPool(p):
|
||||
err = c.PutLeecher(t.Infohash, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Leechers[p.Key()] = *p
|
||||
|
||||
default:
|
||||
if a.Left == 0 {
|
||||
err = c.PutSeeder(t.Infohash, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Seeders[p.Key()] = *p
|
||||
} else {
|
||||
err = c.PutLeecher(t.Infohash, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Leechers[p.Key()] = *p
|
||||
}
|
||||
created = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handleEvent(c 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.DeleteSeeder(t.Infohash, p.Key())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delete(t.Seeders, p.Key())
|
||||
} else if t.InLeecherPool(p) {
|
||||
err = c.DeleteLeecher(t.Infohash, p.Key())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delete(t.Leechers, p.Key())
|
||||
}
|
||||
|
||||
case a.Event == "completed":
|
||||
err = c.IncrementSnatches(t.Infohash)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
snatched = true
|
||||
t.Snatches++
|
||||
|
||||
if t.InLeecherPool(p) {
|
||||
err = LeecherFinished(c, t.Infohash, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case t.InLeecherPool(p) && a.Left == 0:
|
||||
// A leecher completed but the event was never received.
|
||||
err = LeecherFinished(c, t.Infohash, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func newAnnounceResponse(a *models.Announce, u *models.User, t *models.Torrent) *AnnounceResponse {
|
||||
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)
|
||||
}
|
||||
|
||||
res := &AnnounceResponse{
|
||||
Complete: seedCount,
|
||||
Incomplete: leechCount,
|
||||
Interval: a.Config.Announce.Duration,
|
||||
MinInterval: a.Config.MinAnnounce.Duration,
|
||||
Compact: a.Compact,
|
||||
}
|
||||
|
||||
if a.NumWant > 0 && a.Event != "stopped" && a.Event != "paused" {
|
||||
res.IPv4Peers, res.IPv6Peers = getPeers(a, u, t, peerCount)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func getPeers(a *models.Announce, u *models.User, t *models.Torrent, wanted int) (ipv4s, ipv6s PeerList) {
|
||||
if a.Left == 0 {
|
||||
// If they're seeding, give them only leechers.
|
||||
return appendPeers(nil, nil, u, t.Leechers, wanted)
|
||||
} else {
|
||||
// If they're leeching, prioritize giving them seeders.
|
||||
ipv4s, ipv6s = appendPeers(nil, nil, u, t.Seeders, wanted)
|
||||
return appendPeers(ipv4s, ipv6s, u, t.Leechers, wanted-len(ipv4s)-len(ipv6s))
|
||||
}
|
||||
}
|
||||
|
||||
func appendPeers(ipv4s, ipv6s PeerList, u *models.User, peers map[string]models.Peer, wanted int) (PeerList, PeerList) {
|
||||
count := 0
|
||||
|
||||
for _, peer := range peers {
|
||||
if count >= wanted {
|
||||
break
|
||||
}
|
||||
|
||||
if u != nil && 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 ipv4s, ipv6s
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// 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 tracker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/chihaya/chihaya/config"
|
||||
"github.com/chihaya/chihaya/tracker/models"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUserDNE is returned when a user does not exist.
|
||||
ErrUserDNE = errors.New("user does not exist")
|
||||
// ErrTorrentDNE is returned when a torrent does not exist.
|
||||
ErrTorrentDNE = errors.New("torrent does not exist")
|
||||
// ErrClientUnapproved is returned when a clientID is not in the whitelist.
|
||||
ErrClientUnapproved = errors.New("client is not approved")
|
||||
// ErrInvalidPasskey is returned when a passkey is not properly formatted.
|
||||
ErrInvalidPasskey = errors.New("passkey is invalid")
|
||||
|
||||
drivers = make(map[string]Driver)
|
||||
)
|
||||
|
||||
// Driver represents an interface to pool of connections to models used for
|
||||
// the tracker.
|
||||
type Driver interface {
|
||||
New(*config.DriverConfig) Pool
|
||||
}
|
||||
|
||||
// Register makes a database driver available by the provided name.
|
||||
// If Register is called twice with the same name or if driver is nil,
|
||||
// it panics.
|
||||
func Register(name string, driver Driver) {
|
||||
if driver == nil {
|
||||
panic("tracker: Register driver is nil")
|
||||
}
|
||||
if _, dup := drivers[name]; dup {
|
||||
panic("tracker: Register called twice for driver " + name)
|
||||
}
|
||||
drivers[name] = driver
|
||||
}
|
||||
|
||||
// Open creates a pool of data store connections specified by a configuration.
|
||||
func Open(cfg *config.DriverConfig) (Pool, error) {
|
||||
driver, ok := drivers[cfg.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"unknown driver %q (forgotten import?)",
|
||||
cfg.Name,
|
||||
)
|
||||
}
|
||||
pool := driver.New(cfg)
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// Pool represents a thread-safe pool of connections to the data store
|
||||
// that can be used to safely within concurrent goroutines.
|
||||
type Pool interface {
|
||||
Close() error
|
||||
Get() (Conn, error)
|
||||
}
|
||||
|
||||
// Conn represents a connection to the data store that can be used
|
||||
// to make reads/writes.
|
||||
type Conn interface {
|
||||
// Torrent interactions
|
||||
FindTorrent(infohash string) (*models.Torrent, error)
|
||||
PutTorrent(t *models.Torrent) error
|
||||
DeleteTorrent(infohash string) error
|
||||
IncrementSnatches(infohash string) error
|
||||
TouchTorrent(infohash string) error
|
||||
PutLeecher(infohash string, p *models.Peer) error
|
||||
DeleteLeecher(infohash, peerkey string) error
|
||||
PutSeeder(infohash string, p *models.Peer) error
|
||||
DeleteSeeder(infohash, peerkey string) error
|
||||
|
||||
PurgeInactiveTorrent(infohash string) error
|
||||
PurgeInactivePeers(purgeEmptyTorrents bool, before time.Time) error
|
||||
|
||||
// User interactions
|
||||
FindUser(passkey string) (*models.User, error)
|
||||
PutUser(u *models.User) error
|
||||
DeleteUser(passkey string) error
|
||||
|
||||
// Whitelist interactions
|
||||
FindClient(clientID string) error
|
||||
PutClient(clientID string) error
|
||||
DeleteClient(clientID string) error
|
||||
}
|
||||
|
||||
// LeecherFinished moves a peer from the leeching pool to the seeder pool.
|
||||
func LeecherFinished(c Conn, infohash string, p *models.Peer) error {
|
||||
err := c.DeleteLeecher(infohash, p.Key())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.PutSeeder(infohash, p)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// 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 memory
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/chihaya/chihaya/tracker"
|
||||
"github.com/chihaya/chihaya/tracker/models"
|
||||
)
|
||||
|
||||
// Conn implements a connection to a memory-based tracker data store.
|
||||
type Conn struct {
|
||||
*Pool
|
||||
}
|
||||
|
||||
func (c *Conn) FindUser(passkey string) (*models.User, error) {
|
||||
c.usersM.RLock()
|
||||
defer c.usersM.RUnlock()
|
||||
|
||||
user, exists := c.users[passkey]
|
||||
if !exists {
|
||||
return nil, tracker.ErrUserDNE
|
||||
}
|
||||
return &*user, nil
|
||||
}
|
||||
|
||||
func (c *Conn) FindTorrent(infohash string) (*models.Torrent, error) {
|
||||
c.torrentsM.RLock()
|
||||
defer c.torrentsM.RUnlock()
|
||||
|
||||
torrent, exists := c.torrents[infohash]
|
||||
if !exists {
|
||||
return nil, tracker.ErrTorrentDNE
|
||||
}
|
||||
return &*torrent, nil
|
||||
}
|
||||
|
||||
func (c *Conn) FindClient(peerID string) error {
|
||||
c.whitelistM.RLock()
|
||||
defer c.whitelistM.RUnlock()
|
||||
|
||||
_, ok := c.whitelist[peerID]
|
||||
if !ok {
|
||||
return tracker.ErrClientUnapproved
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) IncrementSnatches(infohash string) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
t, ok := c.torrents[infohash]
|
||||
if !ok {
|
||||
return tracker.ErrTorrentDNE
|
||||
}
|
||||
t.Snatches++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) TouchTorrent(infohash string) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
t, ok := c.torrents[infohash]
|
||||
if !ok {
|
||||
return tracker.ErrTorrentDNE
|
||||
}
|
||||
t.LastAction = time.Now().Unix()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) AddLeecher(infohash string, p *models.Peer) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
t, ok := c.torrents[infohash]
|
||||
if !ok {
|
||||
return tracker.ErrTorrentDNE
|
||||
}
|
||||
t.Leechers[p.Key()] = *p
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) AddSeeder(infohash string, p *models.Peer) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
t, ok := c.torrents[infohash]
|
||||
if !ok {
|
||||
return tracker.ErrTorrentDNE
|
||||
}
|
||||
t.Seeders[p.Key()] = *p
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) DeleteLeecher(infohash, peerkey string) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
t, ok := c.torrents[infohash]
|
||||
if !ok {
|
||||
return tracker.ErrTorrentDNE
|
||||
}
|
||||
delete(t.Leechers, peerkey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) DeleteSeeder(infohash, peerkey string) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
t, ok := c.torrents[infohash]
|
||||
if !ok {
|
||||
return tracker.ErrTorrentDNE
|
||||
}
|
||||
delete(t.Seeders, peerkey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) PutLeecher(infohash string, p *models.Peer) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
t, ok := c.torrents[infohash]
|
||||
if !ok {
|
||||
return tracker.ErrTorrentDNE
|
||||
}
|
||||
t.Leechers[p.Key()] = *p
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) PutSeeder(infohash string, p *models.Peer) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
t, ok := c.torrents[infohash]
|
||||
if !ok {
|
||||
return tracker.ErrTorrentDNE
|
||||
}
|
||||
t.Seeders[p.Key()] = *p
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) PutTorrent(t *models.Torrent) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
c.torrents[t.Infohash] = &*t
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) DeleteTorrent(infohash string) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
delete(c.torrents, infohash)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) PurgeInactiveTorrent(infohash string) error {
|
||||
c.torrentsM.Lock()
|
||||
defer c.torrentsM.Unlock()
|
||||
|
||||
torrent, exists := c.torrents[infohash]
|
||||
if !exists {
|
||||
return tracker.ErrTorrentDNE
|
||||
}
|
||||
|
||||
if torrent.PeerCount() == 0 {
|
||||
delete(c.torrents, infohash)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) PutUser(u *models.User) error {
|
||||
c.usersM.Lock()
|
||||
defer c.usersM.Unlock()
|
||||
|
||||
c.users[u.Passkey] = &*u
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) DeleteUser(passkey string) error {
|
||||
c.usersM.Lock()
|
||||
defer c.usersM.Unlock()
|
||||
|
||||
delete(c.users, passkey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) PutClient(peerID string) error {
|
||||
c.whitelistM.Lock()
|
||||
defer c.whitelistM.Unlock()
|
||||
|
||||
c.whitelist[peerID] = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) DeleteClient(peerID string) error {
|
||||
c.whitelistM.Lock()
|
||||
defer c.whitelistM.Unlock()
|
||||
|
||||
delete(c.whitelist, peerID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) PurgeInactivePeers(purgeEmptyTorrents bool, before time.Time) error {
|
||||
unixtime := before.Unix()
|
||||
|
||||
// Build array of map keys to operate on.
|
||||
c.torrentsM.RLock()
|
||||
index := 0
|
||||
keys := make([]string, len(c.torrents))
|
||||
|
||||
for infohash, _ := range c.torrents {
|
||||
keys[index] = infohash
|
||||
index++
|
||||
}
|
||||
|
||||
c.torrentsM.RUnlock()
|
||||
|
||||
// Process keys.
|
||||
for _, infohash := range keys {
|
||||
runtime.Gosched() // Let other goroutines run, since this is low priority.
|
||||
|
||||
c.torrentsM.Lock()
|
||||
torrent := c.torrents[infohash]
|
||||
|
||||
if torrent == nil {
|
||||
continue // Torrent deleted since keys were computed.
|
||||
}
|
||||
|
||||
for key, peer := range torrent.Seeders {
|
||||
if peer.LastAnnounce <= unixtime {
|
||||
delete(torrent.Seeders, key)
|
||||
}
|
||||
}
|
||||
|
||||
for key, peer := range torrent.Leechers {
|
||||
if peer.LastAnnounce <= unixtime {
|
||||
delete(torrent.Leechers, key)
|
||||
}
|
||||
}
|
||||
|
||||
peers := torrent.PeerCount()
|
||||
c.torrentsM.Unlock()
|
||||
|
||||
if purgeEmptyTorrents && peers == 0 {
|
||||
c.PurgeInactiveTorrent(infohash)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 memory implements a Chihaya tracker storage driver within memory.
|
||||
// Stored values will not persist if the tracker is restarted.
|
||||
package memory
|
||||
|
||||
import (
|
||||
"github.com/chihaya/chihaya/config"
|
||||
"github.com/chihaya/chihaya/tracker"
|
||||
"github.com/chihaya/chihaya/tracker/models"
|
||||
)
|
||||
|
||||
type driver struct{}
|
||||
|
||||
func (d *driver) New(conf *config.DriverConfig) tracker.Pool {
|
||||
return &Pool{
|
||||
users: make(map[string]*models.User),
|
||||
torrents: make(map[string]*models.Torrent),
|
||||
whitelist: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
tracker.Register("memory", &driver{})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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 memory
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/chihaya/chihaya/tracker"
|
||||
"github.com/chihaya/chihaya/tracker/models"
|
||||
)
|
||||
|
||||
type Pool struct {
|
||||
users map[string]*models.User
|
||||
usersM sync.RWMutex
|
||||
|
||||
torrents map[string]*models.Torrent
|
||||
torrentsM sync.RWMutex
|
||||
|
||||
whitelist map[string]bool
|
||||
whitelistM sync.RWMutex
|
||||
}
|
||||
|
||||
func (p *Pool) Get() (tracker.Conn, error) {
|
||||
return &Conn{
|
||||
Pool: p,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Pool) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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 tracker
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/chihaya/chihaya/tracker/models"
|
||||
)
|
||||
|
||||
type PeerList []*models.Peer
|
||||
|
||||
type AnnounceResponse struct {
|
||||
Complete, Incomplete int
|
||||
Interval, MinInterval time.Duration
|
||||
IPv4Peers, IPv6Peers PeerList
|
||||
|
||||
Compact bool
|
||||
}
|
||||
|
||||
type ScrapeResponse struct {
|
||||
Files []*models.Torrent
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
WriteError(error) error
|
||||
WriteAnnounce(*AnnounceResponse) error
|
||||
WriteScrape(*ScrapeResponse) error
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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 tracker
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
func PurgeInactivePeers(p Pool, purgeEmptyTorrents bool, threshold, interval time.Duration) {
|
||||
for _ = range time.NewTicker(interval).C {
|
||||
before := time.Now().Add(-threshold)
|
||||
glog.V(0).Infof("Purging peers with no announces since %s", before)
|
||||
|
||||
conn, err := p.Get()
|
||||
|
||||
if err != nil {
|
||||
glog.Error("Unable to get connection for a routine")
|
||||
continue
|
||||
}
|
||||
|
||||
err = conn.PurgeInactivePeers(purgeEmptyTorrents, before)
|
||||
if err != nil {
|
||||
glog.Errorf("Error purging torrents: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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 tracker
|
||||
|
||||
import (
|
||||
"github.com/chihaya/chihaya/tracker/models"
|
||||
)
|
||||
|
||||
func (t *Tracker) HandleScrape(scrape *models.Scrape, w Writer) error {
|
||||
conn, err := t.Pool.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.cfg.Private {
|
||||
_, err = conn.FindUser(scrape.Passkey)
|
||||
if err == ErrUserDNE {
|
||||
w.WriteError(err)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var torrents []*models.Torrent
|
||||
for _, infohash := range scrape.Infohashes {
|
||||
torrent, err := conn.FindTorrent(infohash)
|
||||
if err == ErrTorrentDNE {
|
||||
w.WriteError(err)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
torrents = append(torrents, torrent)
|
||||
}
|
||||
|
||||
return w.WriteScrape(&ScrapeResponse{torrents})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 tracker provides a generic interface for manipulating a
|
||||
// BitTorrent tracker's fast-moving data.
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"github.com/chihaya/chihaya/config"
|
||||
"github.com/chihaya/chihaya/drivers/backend"
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
cfg *config.Config
|
||||
Pool Pool
|
||||
backend backend.Conn
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) (*Tracker, error) {
|
||||
pool, err := Open(&cfg.Tracker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bc, err := backend.Open(&cfg.Backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go PurgeInactivePeers(
|
||||
pool,
|
||||
cfg.PurgeInactiveTorrents,
|
||||
cfg.Announce.Duration*2,
|
||||
cfg.Announce.Duration,
|
||||
)
|
||||
|
||||
return &Tracker{
|
||||
cfg: cfg,
|
||||
Pool: pool,
|
||||
backend: bc,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user