mirror of
https://github.com/sot-tech/mochi.git
synced 2026-06-19 19:06:15 -07:00
b365abd296
* remove peer argument from scrape swarm storage call * replace Peer field with netip.Addr in ScrapeRequest * add man for keydb storage * update readme
189 lines
5.9 KiB
Go
189 lines
5.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/sot-tech/mochi/bittorrent"
|
|
"github.com/sot-tech/mochi/storage"
|
|
)
|
|
|
|
// Hook abstracts the concept of anything that needs to interact with a
|
|
// BitTorrent client's request and response to a BitTorrent tracker.
|
|
// PreHooks and PostHooks both use the same interface.
|
|
//
|
|
// A Hook can implement stop.Stopper if clean shutdown is required.
|
|
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.PeerStorage
|
|
}
|
|
|
|
func (h *swarmInteractionHook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceRequest, _ *bittorrent.AnnounceResponse) (outCtx context.Context, err error) {
|
|
outCtx = ctx
|
|
if ctx.Value(SkipSwarmInteractionKey) != nil {
|
|
return
|
|
}
|
|
|
|
var storeFn func(bittorrent.InfoHash, bittorrent.Peer) error
|
|
|
|
switch {
|
|
case req.Event == bittorrent.Stopped:
|
|
storeFn = func(hash bittorrent.InfoHash, peer bittorrent.Peer) error {
|
|
err = h.store.DeleteSeeder(hash, peer)
|
|
if err != nil && !errors.Is(err, storage.ErrResourceDoesNotExist) {
|
|
return err
|
|
}
|
|
|
|
err = h.store.DeleteLeecher(hash, peer)
|
|
if err != nil && !errors.Is(err, storage.ErrResourceDoesNotExist) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
case req.Event == bittorrent.Completed:
|
|
storeFn = h.store.GraduateLeecher
|
|
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.)
|
|
storeFn = h.store.PutSeeder
|
|
default:
|
|
storeFn = h.store.PutLeecher
|
|
}
|
|
|
|
if err = storeFn(req.InfoHash, req.Peer); err == nil && len(req.InfoHash) == bittorrent.InfoHashV2Len {
|
|
err = storeFn(req.InfoHash.TruncateV1(), req.Peer)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (h *swarmInteractionHook) HandleScrape(ctx context.Context, _ *bittorrent.ScrapeRequest, _ *bittorrent.ScrapeResponse) (context.Context, error) {
|
|
// Scrapes have no effect on the swarm.
|
|
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.PeerStorage
|
|
}
|
|
|
|
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.
|
|
resp.Incomplete, resp.Complete, _ = h.store.ScrapeSwarm(req.InfoHash)
|
|
if len(req.InfoHash) == bittorrent.InfoHashV2Len {
|
|
incomplete, complete, _ := h.store.ScrapeSwarm(req.InfoHash.TruncateV1())
|
|
resp.Incomplete, resp.Complete = resp.Incomplete+incomplete, resp.Complete+complete
|
|
}
|
|
|
|
err = h.appendPeers(req, resp)
|
|
return ctx, err
|
|
}
|
|
|
|
func (h *responseHook) appendPeers(req *bittorrent.AnnounceRequest, resp *bittorrent.AnnounceResponse) error {
|
|
seeding := req.Left == 0
|
|
max := int(req.NumWant)
|
|
storePeers, err := h.store.AnnouncePeers(req.InfoHash, seeding, max, req.Peer)
|
|
if err != nil && !errors.Is(err, storage.ErrResourceDoesNotExist) {
|
|
return err
|
|
}
|
|
err = nil
|
|
peers := make([]bittorrent.Peer, 0, len(resp.IPv4Peers)+len(resp.IPv6Peers)+len(storePeers))
|
|
|
|
// append peers, which added in middleware
|
|
if req.Peer.Addr().Is6() {
|
|
peers = append(peers, resp.IPv6Peers...)
|
|
peers = append(peers, resp.IPv4Peers...)
|
|
} else {
|
|
peers = append(peers, resp.IPv4Peers...)
|
|
peers = append(peers, resp.IPv6Peers...)
|
|
}
|
|
peers = append(peers, storePeers...)
|
|
|
|
// 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)
|
|
}
|
|
|
|
uniquePeers := make(map[bittorrent.Peer]interface{}, len(peers))
|
|
|
|
resp.IPv4Peers = make([]bittorrent.Peer, 0, len(peers)/2)
|
|
resp.IPv6Peers = make([]bittorrent.Peer, 0, len(peers)/2)
|
|
|
|
for _, p := range peers {
|
|
if err != nil || len(uniquePeers) > max {
|
|
break
|
|
}
|
|
if _, found := uniquePeers[p]; !found {
|
|
uniquePeers[p] = nil
|
|
if p.Addr().Is6() {
|
|
resp.IPv6Peers = append(resp.IPv6Peers, p)
|
|
} else if p.Addr().Is4() {
|
|
resp.IPv4Peers = append(resp.IPv4Peers, p)
|
|
} else {
|
|
err = bittorrent.ErrInvalidIP
|
|
}
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (h *responseHook) HandleScrape(ctx context.Context, req *bittorrent.ScrapeRequest, resp *bittorrent.ScrapeResponse) (context.Context, error) {
|
|
if ctx.Value(SkipResponseHookKey) != nil {
|
|
return ctx, nil
|
|
}
|
|
|
|
for _, infoHash := range req.InfoHashes {
|
|
scr := bittorrent.Scrape{InfoHash: infoHash}
|
|
scr.Incomplete, scr.Complete, scr.Snatches = h.store.ScrapeSwarm(infoHash)
|
|
if len(infoHash) == bittorrent.InfoHashV2Len {
|
|
leechers, seeders, snatched := h.store.ScrapeSwarm(infoHash.TruncateV1())
|
|
scr.Incomplete, scr.Complete, scr.Snatches = scr.Incomplete+leechers, scr.Complete+seeders, scr.Snatches+snatched
|
|
}
|
|
|
|
resp.Files = append(resp.Files, scr)
|
|
}
|
|
|
|
return ctx, nil
|
|
}
|