From 6ab65a49a8a72dea1a28968b2ab42a85fd4566ec Mon Sep 17 00:00:00 2001 From: themihai Date: Tue, 7 Nov 2017 20:14:13 +0200 Subject: [PATCH] feat(config): Allows the torrent client to customise the client identity and connection config --- client.go | 20 ++++++---- client_test.go | 20 +++++----- cmd/tracker-announce/main.go | 2 +- config.go | 72 ++++++++++++++++++++++++++++++++++++ global.go | 23 ------------ torrent.go | 6 +-- tracker/http.go | 18 +-------- tracker/tracker.go | 9 +++-- tracker/tracker_test.go | 17 ++++++++- tracker/udp_test.go | 8 ++-- tracker_scraper.go | 2 +- 11 files changed, 127 insertions(+), 70 deletions(-) diff --git a/client.go b/client.go index 47d3b54b..ed0f4a59 100644 --- a/client.go +++ b/client.go @@ -232,6 +232,10 @@ func NewClient(cfg *Config) (cl *Client, err error) { }, } } + if cfg == nil { + cfg = &Config{} + } + cfg.setDefaults() defer func() { if err != nil { @@ -239,7 +243,7 @@ func NewClient(cfg *Config) (cl *Client, err error) { } }() cl = &Client{ - halfOpenLimit: defaultHalfOpenConnsPerTorrent, + halfOpenLimit: cfg.HalfOpenConnsPerTorrent, config: *cfg, dopplegangerAddrs: make(map[string]struct{}), torrents: make(map[metainfo.Hash]*Torrent), @@ -280,7 +284,7 @@ func NewClient(cfg *Config) (cl *Client, err error) { if cfg.PeerID != "" { missinggo.CopyExact(&cl.peerID, cfg.PeerID) } else { - o := copy(cl.peerID[:], bep20) + o := copy(cl.peerID[:], cfg.Bep20) _, err = rand.Read(cl.peerID[o:]) if err != nil { panic("error generating peer id") @@ -486,7 +490,7 @@ func countDialResult(err error) { } } -func reducedDialTimeout(max time.Duration, halfOpenLimit int, pendingPeers int) (ret time.Duration) { +func reducedDialTimeout(minDialTimeout, max time.Duration, halfOpenLimit int, pendingPeers int) (ret time.Duration) { ret = max / time.Duration((pendingPeers+halfOpenLimit)/halfOpenLimit) if ret < minDialTimeout { ret = minDialTimeout @@ -604,7 +608,7 @@ func (cl *Client) handshakesConnection(ctx context.Context, nc net.Conn, t *Torr c = cl.newConnection(nc) c.headerEncrypted = encryptHeader c.uTP = utp - ctx, cancel := context.WithTimeout(ctx, handshakesTimeout) + ctx, cancel := context.WithTimeout(ctx, cl.config.HandshakesTimeout) defer cancel() dl, ok := ctx.Deadline() if !ok { @@ -989,7 +993,7 @@ func (cl *Client) runInitiatedHandshookConn(c *connection, t *Torrent) { } func (cl *Client) runReceivedConn(c *connection) { - err := c.conn.SetDeadline(time.Now().Add(handshakesTimeout)) + err := c.conn.SetDeadline(time.Now().Add(cl.config.HandshakesTimeout)) if err != nil { panic(err) } @@ -1046,7 +1050,7 @@ func (cl *Client) sendInitialMessages(conn *connection, torrent *Torrent) { } return }(), - "v": extendedHandshakeClientVersion, + "v": cl.config.ExtendedHandshakeClientVersion, // No upload queue is implemented yet. "reqq": 64, } @@ -1204,13 +1208,13 @@ func (cl *Client) newTorrent(ih metainfo.Hash, specStorage storage.ClientImpl) ( cl: cl, infoHash: ih, peers: make(map[peersKey]Peer), - conns: make(map[*connection]struct{}, 2*defaultEstablishedConnsPerTorrent), + conns: make(map[*connection]struct{}, 2*cl.config.EstablishedConnsPerTorrent), halfOpen: make(map[string]Peer), pieceStateChanges: pubsub.NewPubSub(), storageOpener: storageClient, - maxEstablishedConns: defaultEstablishedConnsPerTorrent, + maxEstablishedConns: cl.config.EstablishedConnsPerTorrent, networkingEnabled: true, requestStrategy: 2, diff --git a/client_test.go b/client_test.go index f589186c..e06601f4 100644 --- a/client_test.go +++ b/client_test.go @@ -132,23 +132,25 @@ func TestUnmarshalPEXMsg(t *testing.T) { } func TestReducedDialTimeout(t *testing.T) { + cfg := &Config{} + cfg.setDefaults() for _, _case := range []struct { Max time.Duration HalfOpenLimit int PendingPeers int ExpectedReduced time.Duration }{ - {nominalDialTimeout, 40, 0, nominalDialTimeout}, - {nominalDialTimeout, 40, 1, nominalDialTimeout}, - {nominalDialTimeout, 40, 39, nominalDialTimeout}, - {nominalDialTimeout, 40, 40, nominalDialTimeout / 2}, - {nominalDialTimeout, 40, 80, nominalDialTimeout / 3}, - {nominalDialTimeout, 40, 4000, nominalDialTimeout / 101}, + {cfg.NominalDialTimeout, 40, 0, cfg.NominalDialTimeout}, + {cfg.NominalDialTimeout, 40, 1, cfg.NominalDialTimeout}, + {cfg.NominalDialTimeout, 40, 39, cfg.NominalDialTimeout}, + {cfg.NominalDialTimeout, 40, 40, cfg.NominalDialTimeout / 2}, + {cfg.NominalDialTimeout, 40, 80, cfg.NominalDialTimeout / 3}, + {cfg.NominalDialTimeout, 40, 4000, cfg.NominalDialTimeout / 101}, } { - reduced := reducedDialTimeout(_case.Max, _case.HalfOpenLimit, _case.PendingPeers) + reduced := reducedDialTimeout(cfg.MinDialTimeout, _case.Max, _case.HalfOpenLimit, _case.PendingPeers) expected := _case.ExpectedReduced - if expected < minDialTimeout { - expected = minDialTimeout + if expected < cfg.MinDialTimeout { + expected = cfg.MinDialTimeout } if reduced != expected { t.Fatalf("expected %s, got %s", _case.ExpectedReduced, reduced) diff --git a/cmd/tracker-announce/main.go b/cmd/tracker-announce/main.go index 1e2f86a8..9c99a3b1 100644 --- a/cmd/tracker-announce/main.go +++ b/cmd/tracker-announce/main.go @@ -39,7 +39,7 @@ func main() { ar.InfoHash = ts.InfoHash for _, tier := range ts.Trackers { for _, tURI := range tier { - resp, err := tracker.Announce(tURI, &ar) + resp, err := tracker.Announce(torrent.DefaultHTTPClient, tURI, &ar) if err != nil { log.Print(err) continue diff --git a/config.go b/config.go index c0abb23e..e70c9b80 100644 --- a/config.go +++ b/config.go @@ -6,8 +6,24 @@ import ( "github.com/anacrolix/torrent/iplist" "github.com/anacrolix/torrent/storage" + + "crypto/tls" + "net" + "net/http" + "time" ) +var DefaultHTTPClient = &http.Client{ + Timeout: time.Second * 15, + Transport: &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 15 * time.Second, + }).Dial, + TLSHandshakeTimeout: 15 * time.Second, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, +} + // Override Client defaults. type Config struct { // Store torrent file data in this directory unless TorrentDataOpener is @@ -59,6 +75,62 @@ type Config struct { DisableIPv6 bool `long:"disable-ipv6"` // Perform logging and any other behaviour that will help debug. Debug bool `help:"enable debug logging"` + + // HTTP client used to query the tracker endpoint. Default is DefaultHTTPClient + HTTP *http.Client + // Updated occasionally to when there's been some changes to client + // behaviour in case other clients are assuming anything of us. See also + // `bep20`. + ExtendedHandshakeClientVersion string // default "go.torrent dev 20150624" + // Peer ID client identifier prefix. We'll update this occasionally to + // reflect changes to client behaviour that other clients may depend on. + // Also see `extendedHandshakeClientVersion`. + Bep20 string // default "-GT0001-" + + NominalDialTimeout time.Duration // default time.Second * 30 + MinDialTimeout time.Duration // default 5 * time.Second + EstablishedConnsPerTorrent int // default 80 + HalfOpenConnsPerTorrent int // default 80 + TorrentPeersHighWater int // default 200 + TorrentPeersLowWater int // default 50 + + // Limit how long handshake can take. This is to reduce the lingering + // impact of a few bad apples. 4s loses 1% of successful handshakes that + // are obtained with 60s timeout, and 5% of unsuccessful handshakes. + HandshakesTimeout time.Duration // default 20 * time.Second +} + +func (cfg *Config) setDefaults() { + if cfg.HTTP == nil { + cfg.HTTP = DefaultHTTPClient + } + if cfg.ExtendedHandshakeClientVersion == "" { + cfg.ExtendedHandshakeClientVersion = "go.torrent dev 20150624" + } + if cfg.Bep20 == "" { + cfg.Bep20 = "-GT0001-" + } + if cfg.NominalDialTimeout == 0 { + cfg.NominalDialTimeout = 30 * time.Second + } + if cfg.MinDialTimeout == 0 { + cfg.MinDialTimeout = 5 * time.Second + } + if cfg.EstablishedConnsPerTorrent == 0 { + cfg.EstablishedConnsPerTorrent = 80 + } + if cfg.HalfOpenConnsPerTorrent == 0 { + cfg.HalfOpenConnsPerTorrent = 80 + } + if cfg.TorrentPeersHighWater == 0 { + cfg.TorrentPeersHighWater = 200 + } + if cfg.TorrentPeersLowWater == 0 { + cfg.TorrentPeersLowWater = 50 + } + if cfg.HandshakesTimeout == 0 { + cfg.HandshakesTimeout = 20 * time.Second + } } type EncryptionPolicy struct { diff --git a/global.go b/global.go index ed06fccc..ffc30f49 100644 --- a/global.go +++ b/global.go @@ -3,7 +3,6 @@ package torrent import ( "crypto" "expvar" - "time" ) const ( @@ -11,18 +10,6 @@ const ( maxRequests = 250 // Maximum pending requests we allow peers to send us. defaultChunkSize = 0x4000 // 16KiB - // Updated occasionally to when there's been some changes to client - // behaviour in case other clients are assuming anything of us. See also - // `bep20`. - extendedHandshakeClientVersion = "go.torrent dev 20150624" - // Peer ID client identifier prefix. We'll update this occasionally to - // reflect changes to client behaviour that other clients may depend on. - // Also see `extendedHandshakeClientVersion`. - bep20 = "-GT0001-" - - nominalDialTimeout = time.Second * 30 - minDialTimeout = 5 * time.Second - // Justification for set bits follows. // // Extension protocol ([5]|=0x10): @@ -36,16 +23,6 @@ const ( // http://www.bittorrent.org/beps/bep_0005.html defaultExtensionBytes = "\x00\x00\x00\x00\x00\x10\x00\x01" - defaultEstablishedConnsPerTorrent = 80 - defaultHalfOpenConnsPerTorrent = 80 - torrentPeersHighWater = 200 - torrentPeersLowWater = 50 - - // Limit how long handshake can take. This is to reduce the lingering - // impact of a few bad apples. 4s loses 1% of successful handshakes that - // are obtained with 60s timeout, and 5% of unsuccessful handshakes. - handshakesTimeout = 20 * time.Second - // These are our extended message IDs. Peers will use these values to // select which extension a message is intended for. metadataExtendedId = iota + 1 // 0 is reserved for deleting keys diff --git a/torrent.go b/torrent.go index 0060cff1..926e6316 100644 --- a/torrent.go +++ b/torrent.go @@ -223,7 +223,7 @@ func (t *Torrent) unclosedConnsAsSlice() (ret []*connection) { func (t *Torrent) addPeer(p Peer) { cl := t.cl cl.openNewConns(t) - if len(t.peers) >= torrentPeersHighWater { + if len(t.peers) >= cl.config.TorrentPeersHighWater { return } key := peersKey{string(p.IP), p.Port} @@ -1151,7 +1151,7 @@ func (t *Torrent) wantPeers() bool { if t.closed.IsSet() { return false } - if len(t.peers) > torrentPeersLowWater { + if len(t.peers) > t.cl.config.TorrentPeersLowWater { return false } return t.needData() || t.seeding() @@ -1261,7 +1261,7 @@ func (t *Torrent) consumeDHTAnnounce(pvs <-chan dht.PeersValues) { t.addPeers(addPeers) numPeers := len(t.peers) cl.mu.Unlock() - if numPeers >= torrentPeersHighWater { + if numPeers >= cl.config.TorrentPeersHighWater { return } case <-t.closed.LockedChan(&cl.mu): diff --git a/tracker/http.go b/tracker/http.go index 4ca80f8f..b0ab3f95 100644 --- a/tracker/http.go +++ b/tracker/http.go @@ -2,15 +2,12 @@ package tracker import ( "bytes" - "crypto/tls" "errors" "fmt" "io" - "net" "net/http" "net/url" "strconv" - "time" "github.com/anacrolix/missinggo/httptoo" @@ -27,17 +24,6 @@ type httpResponse struct { Peers interface{} `bencode:"peers"` } -var netClient = &http.Client{ - Timeout: time.Second * 15, - Transport: &http.Transport{ - Dial: (&net.Dialer{ - Timeout: 15 * time.Second, - }).Dial, - TLSHandshakeTimeout: 15 * time.Second, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, -} - func (r *httpResponse) UnmarshalPeers() (ret []Peer, err error) { switch v := r.Peers.(type) { case string: @@ -87,12 +73,12 @@ func setAnnounceParams(_url *url.URL, ar *AnnounceRequest) { _url.RawQuery = q.Encode() } -func announceHTTP(ar *AnnounceRequest, _url *url.URL, host string) (ret AnnounceResponse, err error) { +func announceHTTP(cl *http.Client, ar *AnnounceRequest, _url *url.URL, host string) (ret AnnounceResponse, err error) { _url = httptoo.CopyURL(_url) setAnnounceParams(_url, ar) req, err := http.NewRequest("GET", _url.String(), nil) req.Host = host - resp, err := netClient.Do(req) + resp, err := cl.Do(req) if err != nil { return } diff --git a/tracker/tracker.go b/tracker/tracker.go index 9f14041e..4ed76f11 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -3,6 +3,7 @@ package tracker import ( "errors" "net" + "net/http" "net/url" ) @@ -53,18 +54,18 @@ var ( ErrBadScheme = errors.New("unknown scheme") ) -func Announce(urlStr string, req *AnnounceRequest) (res AnnounceResponse, err error) { - return AnnounceHost(urlStr, req, "") +func Announce(cl *http.Client, urlStr string, req *AnnounceRequest) (res AnnounceResponse, err error) { + return AnnounceHost(cl, urlStr, req, "") } -func AnnounceHost(urlStr string, req *AnnounceRequest, host string) (res AnnounceResponse, err error) { +func AnnounceHost(cl *http.Client, urlStr string, req *AnnounceRequest, host string) (res AnnounceResponse, err error) { _url, err := url.Parse(urlStr) if err != nil { return } switch _url.Scheme { case "http", "https": - return announceHTTP(req, _url, host) + return announceHTTP(cl, req, _url, host) case "udp": return announceUDP(req, _url) default: diff --git a/tracker/tracker_test.go b/tracker/tracker_test.go index d095af5a..bf397e9d 100644 --- a/tracker/tracker_test.go +++ b/tracker/tracker_test.go @@ -1,12 +1,27 @@ package tracker import ( + "crypto/tls" + "net" + "net/http" "testing" + "time" ) +var defaultClient = &http.Client{ + Timeout: time.Second * 15, + Transport: &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 15 * time.Second, + }).Dial, + TLSHandshakeTimeout: 15 * time.Second, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, +} + func TestUnsupportedTrackerScheme(t *testing.T) { t.Parallel() - _, err := Announce("lol://tracker.openbittorrent.com:80/announce", nil) + _, err := Announce(defaultClient, "lol://tracker.openbittorrent.com:80/announce", nil) if err != ErrBadScheme { t.Fatal(err) } diff --git a/tracker/udp_test.go b/tracker/udp_test.go index 55946aae..78434d32 100644 --- a/tracker/udp_test.go +++ b/tracker/udp_test.go @@ -114,7 +114,7 @@ func TestAnnounceLocalhost(t *testing.T) { go func() { require.NoError(t, srv.serveOne()) }() - ar, err := Announce(fmt.Sprintf("udp://%s/announce", srv.pc.LocalAddr().String()), &req) + ar, err := Announce(defaultClient, fmt.Sprintf("udp://%s/announce", srv.pc.LocalAddr().String()), &req) require.NoError(t, err) assert.EqualValues(t, 1, ar.Seeders) assert.EqualValues(t, 2, len(ar.Peers)) @@ -130,7 +130,7 @@ func TestUDPTracker(t *testing.T) { } rand.Read(req.PeerId[:]) copy(req.InfoHash[:], []uint8{0xa3, 0x56, 0x41, 0x43, 0x74, 0x23, 0xe6, 0x26, 0xd9, 0x38, 0x25, 0x4a, 0x6b, 0x80, 0x49, 0x10, 0xa6, 0x67, 0xa, 0xc1}) - ar, err := Announce("udp://tracker.openbittorrent.com:80/announce", &req) + ar, err := Announce(defaultClient, "udp://tracker.openbittorrent.com:80/announce", &req) // Skip any net errors as we don't control the server. if _, ok := err.(net.Error); ok { t.Skip(err) @@ -166,7 +166,7 @@ func TestAnnounceRandomInfoHashThirdParty(t *testing.T) { wg.Add(1) go func(url string) { defer wg.Done() - resp, err := Announce(url, &req) + resp, err := Announce(defaultClient, url, &req) if err != nil { t.Logf("error announcing to %s: %s", url, err) return @@ -202,7 +202,7 @@ func TestURLPathOption(t *testing.T) { } defer conn.Close() go func() { - _, err := Announce((&url.URL{ + _, err := Announce(defaultClient, (&url.URL{ Scheme: "udp", Host: conn.LocalAddr().String(), Path: "/announce", diff --git a/tracker_scraper.go b/tracker_scraper.go index 3463133c..a090bc1d 100644 --- a/tracker_scraper.go +++ b/tracker_scraper.go @@ -85,7 +85,7 @@ func (me *trackerScraper) announce() (ret trackerAnnounceResult) { me.t.cl.mu.Lock() req := me.t.announceRequest() me.t.cl.mu.Unlock() - res, err := tracker.AnnounceHost(urlToUse, &req, host) + res, err := tracker.AnnounceHost(me.t.cl.config.HTTP, urlToUse, &req, host) if err != nil { ret.Err = err return -- 2.48.1