From af8c41ebe9f8c845251667dcbbd0874a9d973557 Mon Sep 17 00:00:00 2001
From: Matt Joiner <anacrolix@gmail.com>
Date: Tue, 22 Jun 2021 23:28:26 +1000
Subject: [PATCH] Extract HTTP tracker client into separate package

---
 tracker/expvar.go               |  6 ---
 tracker/{ => http}/http.go      | 84 ++++++++++++++++++++++-----------
 tracker/{ => http}/http_test.go |  5 +-
 tracker/http/peer.go            | 38 +++++++++++++++
 tracker/peer.go                 | 37 ---------------
 tracker/shared/shared.go        | 10 ++++
 tracker/tracker.go              | 36 ++++++++------
 tracker/udp.go                  |  3 +-
 8 files changed, 130 insertions(+), 89 deletions(-)
 rename tracker/{ => http}/http.go (75%)
 rename tracker/{ => http}/http_test.go (98%)
 create mode 100644 tracker/http/peer.go
 create mode 100644 tracker/shared/shared.go

diff --git a/tracker/expvar.go b/tracker/expvar.go
index de5a4985..bf031739 100644
--- a/tracker/expvar.go
+++ b/tracker/expvar.go
@@ -1,7 +1 @@
 package tracker
-
-import (
-	"expvar"
-)
-
-var vars = expvar.NewMap("tracker")
diff --git a/tracker/http.go b/tracker/http/http.go
similarity index 75%
rename from tracker/http.go
rename to tracker/http/http.go
index eb265045..c74243cd 100644
--- a/tracker/http.go
+++ b/tracker/http/http.go
@@ -1,8 +1,10 @@
-package tracker
+package http
 
 import (
 	"bytes"
+	"context"
 	"crypto/tls"
+	"expvar"
 	"fmt"
 	"io"
 	"math"
@@ -13,10 +15,38 @@ import (
 
 	"github.com/anacrolix/dht/v2/krpc"
 	"github.com/anacrolix/missinggo/httptoo"
-
 	"github.com/anacrolix/torrent/bencode"
+	"github.com/anacrolix/torrent/tracker/shared"
+	"github.com/anacrolix/torrent/tracker/udp"
 )
 
+var vars = expvar.NewMap("tracker/http")
+
+type Client struct {
+	hc *http.Client
+}
+
+type NewClientOpts struct {
+	Proxy      func(*http.Request) (*url.URL, error)
+	ServerName string
+}
+
+func NewClient(opts NewClientOpts) Client {
+	return Client{
+		hc: &http.Client{
+			Transport: &http.Transport{
+				Proxy: opts.Proxy,
+				TLSClientConfig: &tls.Config{
+					InsecureSkipVerify: true,
+					ServerName:         opts.ServerName,
+				},
+				// This is for S3 trackers that hold connections open.
+				DisableKeepAlives: true,
+			},
+		},
+	}
+}
+
 type HttpResponse struct {
 	FailureReason string `bencode:"failure reason"`
 	Interval      int32  `bencode:"interval"`
@@ -66,7 +96,7 @@ func (me *Peers) UnmarshalBencode(b []byte) (err error) {
 	}
 }
 
-func setAnnounceParams(_url *url.URL, ar *AnnounceRequest, opts Announce) {
+func setAnnounceParams(_url *url.URL, ar *AnnounceRequest, opts AnnounceOpt) {
 	q := _url.Query()
 
 	q.Set("key", strconv.FormatInt(int64(ar.Key), 10))
@@ -86,7 +116,7 @@ func setAnnounceParams(_url *url.URL, ar *AnnounceRequest, opts Announce) {
 	}
 	q.Set("left", strconv.FormatInt(left, 10))
 
-	if ar.Event != None {
+	if ar.Event != shared.None {
 		q.Set("event", ar.Event.String())
 	}
 	// http://stackoverflow.com/questions/17418004/why-does-tracker-server-not-understand-my-request-bittorrent-protocol
@@ -104,36 +134,27 @@ func setAnnounceParams(_url *url.URL, ar *AnnounceRequest, opts Announce) {
 		// addresses for other address-families, although it's not encouraged.
 		q.Add("ip", ipString)
 	}
-	doIp("ipv4", opts.ClientIp4.IP)
-	doIp("ipv6", opts.ClientIp6.IP)
+	doIp("ipv4", opts.ClientIp4)
+	doIp("ipv6", opts.ClientIp6)
 	_url.RawQuery = q.Encode()
 }
 
-func announceHTTP(opt Announce, _url *url.URL) (ret AnnounceResponse, err error) {
+type AnnounceOpt struct {
+	UserAgent  string
+	HostHeader string
+	ClientIp4  net.IP
+	ClientIp6  net.IP
+}
+
+type AnnounceRequest = udp.AnnounceRequest
+
+func (cl Client) Announce(ctx context.Context, ar AnnounceRequest, opt AnnounceOpt, _url *url.URL) (ret AnnounceResponse, err error) {
 	_url = httptoo.CopyURL(_url)
-	setAnnounceParams(_url, &opt.Request, opt)
-	req, err := http.NewRequest("GET", _url.String(), nil)
+	setAnnounceParams(_url, &ar, opt)
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, _url.String(), nil)
 	req.Header.Set("User-Agent", opt.UserAgent)
 	req.Host = opt.HostHeader
-	if opt.Context != nil {
-		req = req.WithContext(opt.Context)
-	}
-	resp, err := (&http.Client{
-		//Timeout: time.Second * 15,
-		Transport: &http.Transport{
-			//Dial: (&net.Dialer{
-			//	Timeout: 15 * time.Second,
-			//}).Dial,
-			Proxy: opt.HTTPProxy,
-			//TLSHandshakeTimeout: 15 * time.Second,
-			TLSClientConfig: &tls.Config{
-				InsecureSkipVerify: true,
-				ServerName:         opt.ServerName,
-			},
-			// This is for S3 trackers that hold connections open.
-			DisableKeepAlives: true,
-		},
-	}).Do(req)
+	resp, err := cl.hc.Do(req)
 	if err != nil {
 		return
 	}
@@ -175,3 +196,10 @@ func announceHTTP(opt Announce, _url *url.URL) (ret AnnounceResponse, err error)
 	}
 	return
 }
+
+type AnnounceResponse struct {
+	Interval int32 // Minimum seconds the local peer should wait before next announce.
+	Leechers int32
+	Seeders  int32
+	Peers    []Peer
+}
diff --git a/tracker/http_test.go b/tracker/http/http_test.go
similarity index 98%
rename from tracker/http_test.go
rename to tracker/http/http_test.go
index 474c4aa4..f0066de7 100644
--- a/tracker/http_test.go
+++ b/tracker/http/http_test.go
@@ -1,12 +1,11 @@
-package tracker
+package http
 
 import (
 	"testing"
 
+	"github.com/anacrolix/torrent/bencode"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-
-	"github.com/anacrolix/torrent/bencode"
 )
 
 func TestUnmarshalHTTPResponsePeerDicts(t *testing.T) {
diff --git a/tracker/http/peer.go b/tracker/http/peer.go
new file mode 100644
index 00000000..f715e20b
--- /dev/null
+++ b/tracker/http/peer.go
@@ -0,0 +1,38 @@
+package http
+
+import (
+	"fmt"
+	"net"
+
+	"github.com/anacrolix/dht/v2/krpc"
+)
+
+type Peer struct {
+	IP   net.IP
+	Port int
+	ID   []byte
+}
+
+func (p Peer) String() string {
+	loc := net.JoinHostPort(p.IP.String(), fmt.Sprintf("%d", p.Port))
+	if len(p.ID) != 0 {
+		return fmt.Sprintf("%x at %s", p.ID, loc)
+	} else {
+		return loc
+	}
+}
+
+// Set from the non-compact form in BEP 3.
+func (p *Peer) FromDictInterface(d map[string]interface{}) {
+	p.IP = net.ParseIP(d["ip"].(string))
+	if _, ok := d["peer id"]; ok {
+		p.ID = []byte(d["peer id"].(string))
+	}
+	p.Port = int(d["port"].(int64))
+}
+
+func (p Peer) FromNodeAddr(na krpc.NodeAddr) Peer {
+	p.IP = na.IP
+	p.Port = na.Port
+	return p
+}
diff --git a/tracker/peer.go b/tracker/peer.go
index 3cda3dc3..bf031739 100644
--- a/tracker/peer.go
+++ b/tracker/peer.go
@@ -1,38 +1 @@
 package tracker
-
-import (
-	"fmt"
-	"net"
-
-	"github.com/anacrolix/dht/v2/krpc"
-)
-
-type Peer struct {
-	IP   net.IP
-	Port int
-	ID   []byte
-}
-
-func (p Peer) String() string {
-	loc := net.JoinHostPort(p.IP.String(), fmt.Sprintf("%d", p.Port))
-	if len(p.ID) != 0 {
-		return fmt.Sprintf("%x at %s", p.ID, loc)
-	} else {
-		return loc
-	}
-}
-
-// Set from the non-compact form in BEP 3.
-func (p *Peer) FromDictInterface(d map[string]interface{}) {
-	p.IP = net.ParseIP(d["ip"].(string))
-	if _, ok := d["peer id"]; ok {
-		p.ID = []byte(d["peer id"].(string))
-	}
-	p.Port = int(d["port"].(int64))
-}
-
-func (p Peer) FromNodeAddr(na krpc.NodeAddr) Peer {
-	p.IP = na.IP
-	p.Port = na.Port
-	return p
-}
diff --git a/tracker/shared/shared.go b/tracker/shared/shared.go
new file mode 100644
index 00000000..7859ea99
--- /dev/null
+++ b/tracker/shared/shared.go
@@ -0,0 +1,10 @@
+package shared
+
+import "github.com/anacrolix/torrent/tracker/udp"
+
+const (
+	None      udp.AnnounceEvent = iota
+	Completed                   // The local peer just completed the torrent.
+	Started                     // The local peer has just resumed this torrent.
+	Stopped                     // The local peer is leaving the swarm.
+)
diff --git a/tracker/tracker.go b/tracker/tracker.go
index 0a187574..66344fe2 100644
--- a/tracker/tracker.go
+++ b/tracker/tracker.go
@@ -8,26 +8,25 @@ import (
 	"time"
 
 	"github.com/anacrolix/dht/v2/krpc"
+	trHttp "github.com/anacrolix/torrent/tracker/http"
+	"github.com/anacrolix/torrent/tracker/shared"
 	"github.com/anacrolix/torrent/tracker/udp"
 )
 
+const (
+	None      = shared.None
+	Started   = shared.Started
+	Stopped   = shared.Stopped
+	Completed = shared.Completed
+)
+
 type AnnounceRequest = udp.AnnounceRequest
 
-type AnnounceResponse struct {
-	Interval int32 // Minimum seconds the local peer should wait before next announce.
-	Leechers int32
-	Seeders  int32
-	Peers    []Peer
-}
+type AnnounceResponse = trHttp.AnnounceResponse
 
-type AnnounceEvent = udp.AnnounceEvent
+type Peer = trHttp.Peer
 
-const (
-	None      AnnounceEvent = iota
-	Completed               // The local peer just completed the torrent.
-	Started                 // The local peer has just resumed this torrent.
-	Stopped                 // The local peer is leaving the swarm.
-)
+type AnnounceEvent = udp.AnnounceEvent
 
 var (
 	ErrBadScheme = errors.New("unknown scheme")
@@ -66,7 +65,16 @@ func (me Announce) Do() (res AnnounceResponse, err error) {
 	}
 	switch _url.Scheme {
 	case "http", "https":
-		return announceHTTP(me, _url)
+		cl := trHttp.NewClient(trHttp.NewClientOpts{
+			Proxy:      me.HTTPProxy,
+			ServerName: me.ServerName,
+		})
+		return cl.Announce(me.Context, me.Request, trHttp.AnnounceOpt{
+			UserAgent:  me.UserAgent,
+			HostHeader: me.HostHeader,
+			ClientIp4:  me.ClientIp4.IP,
+			ClientIp6:  me.ClientIp6.IP,
+		}, _url)
 	case "udp", "udp4", "udp6":
 		return announceUDP(me, _url)
 	default:
diff --git a/tracker/udp.go b/tracker/udp.go
index 4fb00b11..ac9f3e1c 100644
--- a/tracker/udp.go
+++ b/tracker/udp.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/anacrolix/dht/v2/krpc"
 	"github.com/anacrolix/missinggo"
+	trHttp "github.com/anacrolix/torrent/tracker/http"
 	"github.com/anacrolix/torrent/tracker/udp"
 )
 
@@ -73,7 +74,7 @@ func (c *udpAnnounce) Do(req AnnounceRequest) (res AnnounceResponse, err error)
 	res.Leechers = h.Leechers
 	res.Seeders = h.Seeders
 	for _, cp := range nas.NodeAddrs() {
-		res.Peers = append(res.Peers, Peer{}.FromNodeAddr(cp))
+		res.Peers = append(res.Peers, trHttp.Peer{}.FromNodeAddr(cp))
 	}
 	return
 }
-- 
2.51.0