196 lines
6.2 KiB
Go
196 lines
6.2 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
|
|
"github.com/chihaya/chihaya/bittorrent"
|
|
"github.com/chihaya/chihaya/storage"
|
|
)
|
|
|
|
// Hook abstracts the concept of anything that needs to interact with a
|
|
// BitTorrent client's request and response to a BitTorrent tracker.
|
|
type Hook interface {
|
|
HandleAnnounce(context.Context, *bittorrent.AnnounceRequest, *bittorrent.AnnounceResponse) (context.Context, error)
|
|
HandleScrape(context.Context, *bittorrent.ScrapeRequest, *bittorrent.ScrapeResponse) (context.Context, error)
|
|
}
|
|
|
|
type skipSwarmInteraction struct{}
|
|
|
|
// SkipSwarmInteractionKey is a key for the context of an Announce to control
|
|
// whether the swarm interaction middleware should run.
|
|
// Any non-nil value set for this key will cause the swarm interaction
|
|
// middleware to skip.
|
|
var SkipSwarmInteractionKey = skipSwarmInteraction{}
|
|
|
|
type swarmInteractionHook struct {
|
|
store storage.PeerStore
|
|
}
|
|
|
|
func (h *swarmInteractionHook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceRequest, resp *bittorrent.AnnounceResponse) (_ context.Context, err error) {
|
|
if ctx.Value(SkipSwarmInteractionKey) != nil {
|
|
return ctx, nil
|
|
}
|
|
|
|
switch {
|
|
case req.Event == bittorrent.Stopped:
|
|
err = h.store.DeleteSeeder(req.InfoHash, req.Peer)
|
|
if err != nil && err != storage.ErrResourceDoesNotExist {
|
|
return ctx, err
|
|
}
|
|
|
|
err = h.store.DeleteLeecher(req.InfoHash, req.Peer)
|
|
if err != nil && err != storage.ErrResourceDoesNotExist {
|
|
return ctx, err
|
|
}
|
|
case req.Event == bittorrent.Completed:
|
|
err = h.store.GraduateLeecher(req.InfoHash, req.Peer)
|
|
return ctx, err
|
|
case req.Left == 0:
|
|
// Completed events will also have Left == 0, but by making this
|
|
// an extra case we can treat "old" seeders differently from
|
|
// graduating leechers. (Calling PutSeeder is probably faster
|
|
// than calling GraduateLeecher.)
|
|
err = h.store.PutSeeder(req.InfoHash, req.Peer)
|
|
return ctx, err
|
|
default:
|
|
err = h.store.PutLeecher(req.InfoHash, req.Peer)
|
|
return ctx, err
|
|
}
|
|
|
|
return ctx, nil
|
|
}
|
|
|
|
func (h *swarmInteractionHook) HandleScrape(ctx context.Context, _ *bittorrent.ScrapeRequest, _ *bittorrent.ScrapeResponse) (context.Context, error) {
|
|
// Scrapes have no effect on the swarm.
|
|
return ctx, nil
|
|
}
|
|
|
|
// ErrInvalidIP indicates an invalid IP for an Announce.
|
|
var ErrInvalidIP = errors.New("invalid IP")
|
|
|
|
// sanitizationHook enforces semantic assumptions about requests that may have
|
|
// not been accounted for in a tracker frontend.
|
|
//
|
|
// The SanitizationHook performs the following checks:
|
|
// - maxNumWant: Checks whether the numWant parameter of an announce is below
|
|
// a limit. Sets it to the limit if the value is higher.
|
|
// - defaultNumWant: Checks whether the numWant parameter of an announce is
|
|
// zero. Sets it to the default if it is.
|
|
// - IP sanitization: Checks whether the announcing Peer's IP address is either
|
|
// IPv4 or IPv6. Returns ErrInvalidIP if the address is neither IPv4 nor
|
|
// IPv6. Sets the Peer.AddressFamily field accordingly. Truncates IPv4
|
|
// addresses to have a length of 4 bytes.
|
|
type sanitizationHook struct {
|
|
maxNumWant uint32
|
|
defaultNumWant uint32
|
|
}
|
|
|
|
func (h *sanitizationHook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceRequest, resp *bittorrent.AnnounceResponse) (context.Context, error) {
|
|
if req.NumWant > h.maxNumWant {
|
|
req.NumWant = h.maxNumWant
|
|
}
|
|
|
|
if req.NumWant == 0 {
|
|
req.NumWant = h.defaultNumWant
|
|
}
|
|
|
|
if ip := req.Peer.IP.To4(); ip != nil {
|
|
req.Peer.IP.IP = ip
|
|
req.Peer.IP.AddressFamily = bittorrent.IPv4
|
|
} else if len(req.Peer.IP.IP) == net.IPv6len { // implies req.Peer.IP.To4() == nil
|
|
req.Peer.IP.AddressFamily = bittorrent.IPv6
|
|
} else {
|
|
return ctx, ErrInvalidIP
|
|
}
|
|
|
|
return ctx, nil
|
|
}
|
|
|
|
func (h *sanitizationHook) HandleScrape(ctx context.Context, req *bittorrent.ScrapeRequest, resp *bittorrent.ScrapeResponse) (context.Context, error) {
|
|
return ctx, nil
|
|
}
|
|
|
|
type skipResponseHook struct{}
|
|
|
|
// SkipResponseHookKey is a key for the context of an Announce or Scrape to
|
|
// control whether the response middleware should run.
|
|
// Any non-nil value set for this key will cause the response middleware to
|
|
// skip.
|
|
var SkipResponseHookKey = skipResponseHook{}
|
|
|
|
type scrapeAddressType struct{}
|
|
|
|
// ScrapeIsIPv6Key is the key under which to store whether or not the
|
|
// address used to request a scrape was an IPv6 address.
|
|
// The value is expected to be of type bool.
|
|
// A missing value or a value that is not a bool for this key is equivalent to
|
|
// it being set to false.
|
|
var ScrapeIsIPv6Key = scrapeAddressType{}
|
|
|
|
type responseHook struct {
|
|
store storage.PeerStore
|
|
}
|
|
|
|
func (h *responseHook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceRequest, resp *bittorrent.AnnounceResponse) (_ context.Context, err error) {
|
|
if ctx.Value(SkipResponseHookKey) != nil {
|
|
return ctx, nil
|
|
}
|
|
|
|
// Add the Scrape data to the response.
|
|
s := h.store.ScrapeSwarm(req.InfoHash, req.IP.AddressFamily)
|
|
resp.Incomplete = s.Incomplete
|
|
resp.Complete = s.Complete
|
|
|
|
err = h.appendPeers(req, resp)
|
|
return ctx, err
|
|
}
|
|
|
|
func (h *responseHook) appendPeers(req *bittorrent.AnnounceRequest, resp *bittorrent.AnnounceResponse) error {
|
|
seeding := req.Left == 0
|
|
peers, err := h.store.AnnouncePeers(req.InfoHash, seeding, int(req.NumWant), req.Peer)
|
|
if err != nil && err != storage.ErrResourceDoesNotExist {
|
|
return err
|
|
}
|
|
|
|
// Some clients expect a minimum of their own peer representation returned to
|
|
// them if they are the only peer in a swarm.
|
|
if len(peers) == 0 {
|
|
if seeding {
|
|
resp.Complete++
|
|
} else {
|
|
resp.Incomplete++
|
|
}
|
|
peers = append(peers, req.Peer)
|
|
}
|
|
|
|
switch req.IP.AddressFamily {
|
|
case bittorrent.IPv4:
|
|
resp.IPv4Peers = peers
|
|
case bittorrent.IPv6:
|
|
resp.IPv6Peers = peers
|
|
default:
|
|
panic("attempted to append peer that is neither IPv4 nor IPv6")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *responseHook) HandleScrape(ctx context.Context, req *bittorrent.ScrapeRequest, resp *bittorrent.ScrapeResponse) (context.Context, error) {
|
|
if ctx.Value(SkipResponseHookKey) != nil {
|
|
return ctx, nil
|
|
}
|
|
|
|
v6, _ := ctx.Value(ScrapeIsIPv6Key).(bool)
|
|
|
|
for _, infoHash := range req.InfoHashes {
|
|
if v6 {
|
|
resp.Files[infoHash] = h.store.ScrapeSwarm(infoHash, bittorrent.IPv6)
|
|
} else {
|
|
resp.Files[infoHash] = h.store.ScrapeSwarm(infoHash, bittorrent.IPv4)
|
|
}
|
|
}
|
|
|
|
return ctx, nil
|
|
}
|