From 11833b45cfbec636653d64cec3f8db42d70f051e Mon Sep 17 00:00:00 2001 From: Matt Joiner Date: Tue, 26 Sep 2023 22:19:51 +1000 Subject: [PATCH] Support scraping from HTTP trackers --- cmd/torrent/scrape.go | 23 +++++---------------- tracker/client.go | 2 ++ tracker/http/scrape.go | 47 ++++++++++++++++++++++++++++++++++++++++++ tracker/udp.go | 12 +++++++++++ tracker/udp/scrape.go | 8 ++++--- 5 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 tracker/http/scrape.go diff --git a/cmd/torrent/scrape.go b/cmd/torrent/scrape.go index ce5fc06c..01a59b7f 100644 --- a/cmd/torrent/scrape.go +++ b/cmd/torrent/scrape.go @@ -3,12 +3,11 @@ package main import ( "context" "fmt" - "net/url" "github.com/davecgh/go-spew/spew" "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/tracker/udp" + "github.com/anacrolix/torrent/tracker" ) type scrapeCfg struct { @@ -17,25 +16,13 @@ type scrapeCfg struct { } func scrape(flags scrapeCfg) error { - trackerUrl, err := url.Parse(flags.Tracker) + cc, err := tracker.NewClient(flags.Tracker, tracker.NewClientOpts{}) if err != nil { - return fmt.Errorf("parsing tracker url: %w", err) - } - cc, err := udp.NewConnClient(udp.NewConnClientOpts{ - Network: trackerUrl.Scheme, - Host: trackerUrl.Host, - //Ipv6: nil, - //Logger: log.Logger{}, - }) - if err != nil { - return fmt.Errorf("creating new udp tracker conn client: %w", err) + err = fmt.Errorf("creating new tracker client: %w", err) + return err } defer cc.Close() - var ihs []udp.InfoHash - for _, ih := range flags.InfoHashes { - ihs = append(ihs, ih) - } - scrapeOut, err := cc.Client.Scrape(context.TODO(), ihs) + scrapeOut, err := cc.Scrape(context.TODO(), flags.InfoHashes) if err != nil { return fmt.Errorf("scraping: %w", err) } diff --git a/tracker/client.go b/tracker/client.go index 1aaf2561..3b7e2aba 100644 --- a/tracker/client.go +++ b/tracker/client.go @@ -9,10 +9,12 @@ import ( trHttp "github.com/anacrolix/torrent/tracker/http" "github.com/anacrolix/torrent/tracker/udp" + "github.com/anacrolix/torrent/types/infohash" ) type Client interface { Announce(context.Context, AnnounceRequest, AnnounceOpt) (AnnounceResponse, error) + Scrape(ctx context.Context, ihs []infohash.T) (out udp.ScrapeResponse, err error) Close() error } diff --git a/tracker/http/scrape.go b/tracker/http/scrape.go new file mode 100644 index 00000000..6940370f --- /dev/null +++ b/tracker/http/scrape.go @@ -0,0 +1,47 @@ +package httpTracker + +import ( + "context" + "log" + "net/http" + "net/url" + + "github.com/anacrolix/torrent/bencode" + "github.com/anacrolix/torrent/tracker/udp" + "github.com/anacrolix/torrent/types/infohash" +) + +type scrapeResponse struct { + Files files `bencode:"files"` +} + +// Bencode should support bencode.Unmarshalers from a string in the dict key position. +type files = map[string]udp.ScrapeInfohashResult + +func (cl Client) Scrape(ctx context.Context, ihs []infohash.T) (out udp.ScrapeResponse, err error) { + _url := cl.url_.JoinPath("..", "scrape") + query, err := url.ParseQuery(_url.RawQuery) + if err != nil { + return + } + for _, ih := range ihs { + query.Add("info_hash", ih.AsString()) + } + _url.RawQuery = query.Encode() + log.Printf("%q", _url.String()) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, _url.String(), nil) + if err != nil { + return + } + resp, err := cl.hc.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + var decodedResp scrapeResponse + err = bencode.NewDecoder(resp.Body).Decode(&decodedResp) + for _, ih := range ihs { + out = append(out, decodedResp.Files[ih.AsString()]) + } + return +} diff --git a/tracker/udp.go b/tracker/udp.go index db486948..cf681887 100644 --- a/tracker/udp.go +++ b/tracker/udp.go @@ -4,8 +4,11 @@ import ( "context" "encoding/binary" + "github.com/anacrolix/generics" + trHttp "github.com/anacrolix/torrent/tracker/http" "github.com/anacrolix/torrent/tracker/udp" + "github.com/anacrolix/torrent/types/infohash" ) type udpClient struct { @@ -13,6 +16,15 @@ type udpClient struct { requestUri string } +func (c *udpClient) Scrape(ctx context.Context, ihs []infohash.T) (out udp.ScrapeResponse, err error) { + return c.cl.Client.Scrape( + ctx, + generics.SliceMap(ihs, func(from infohash.T) udp.InfoHash { + return from + }), + ) +} + func (c *udpClient) Close() error { return c.cl.Close() } diff --git a/tracker/udp/scrape.go b/tracker/udp/scrape.go index 331f109e..13a69b99 100644 --- a/tracker/udp/scrape.go +++ b/tracker/udp/scrape.go @@ -5,7 +5,9 @@ type ScrapeRequest []InfoHash type ScrapeResponse []ScrapeInfohashResult type ScrapeInfohashResult struct { - Seeders int32 - Completed int32 - Leechers int32 + // I'm not sure why the fields are named differently for HTTP scrapes. + // https://www.bittorrent.org/beps/bep_0048.html + Seeders int32 `bencode:"complete"` + Completed int32 `bencode:"downloaded"` + Leechers int32 `bencode:"incomplete"` } -- 2.44.0