diff --git a/tracker/client.go b/tracker/client.go new file mode 100644 index 00000000..bbda6b3a --- /dev/null +++ b/tracker/client.go @@ -0,0 +1,49 @@ +package tracker + +import ( + "context" + "net/url" + + trHttp "github.com/anacrolix/torrent/tracker/http" + "github.com/anacrolix/torrent/tracker/udp" +) + +type Client interface { + Announce(context.Context, AnnounceRequest, trHttp.AnnounceOpt) (AnnounceResponse, error) + Close() error +} + +type NewClientOpts struct { + Http trHttp.NewClientOpts + // Overrides the network in the scheme. Probably a legacy thing. + UdpNetwork string +} + +func NewClient(urlStr string, opts NewClientOpts) (Client, error) { + _url, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + switch _url.Scheme { + case "http", "https": + return trHttp.NewClient(_url, opts.Http), nil + case "udp", "udp4", "udp6": + network := _url.Scheme + if opts.UdpNetwork != "" { + network = opts.UdpNetwork + } + cc, err := udp.NewConnClient(udp.NewConnClientOpts{ + Network: network, + Host: _url.Host, + }) + if err != nil { + return nil, err + } + return &udpClient{ + cl: cc, + requestUri: _url.RequestURI(), + }, nil + default: + return nil, ErrBadScheme + } +} diff --git a/tracker/http/client.go b/tracker/http/client.go new file mode 100644 index 00000000..30db3691 --- /dev/null +++ b/tracker/http/client.go @@ -0,0 +1,41 @@ +package http + +import ( + "crypto/tls" + "net/http" + "net/url" +) + +type Client struct { + hc *http.Client + url_ *url.URL +} + +type ProxyFunc func(*http.Request) (*url.URL, error) + +type NewClientOpts struct { + Proxy ProxyFunc + ServerName string +} + +func NewClient(url_ *url.URL, opts NewClientOpts) Client { + return Client{ + url_: url_, + 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, + }, + }, + } +} + +func (cl Client) Close() error { + cl.hc.CloseIdleConnections() + return nil +} diff --git a/tracker/http/http.go b/tracker/http/http.go index c74243cd..c4f2fac4 100644 --- a/tracker/http/http.go +++ b/tracker/http/http.go @@ -3,7 +3,6 @@ package http import ( "bytes" "context" - "crypto/tls" "expvar" "fmt" "io" @@ -13,7 +12,6 @@ import ( "net/url" "strconv" - "github.com/anacrolix/dht/v2/krpc" "github.com/anacrolix/missinggo/httptoo" "github.com/anacrolix/torrent/bencode" "github.com/anacrolix/torrent/tracker/shared" @@ -22,80 +20,6 @@ import ( 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"` - TrackerId string `bencode:"tracker id"` - Complete int32 `bencode:"complete"` - Incomplete int32 `bencode:"incomplete"` - Peers Peers `bencode:"peers"` - // BEP 7 - Peers6 krpc.CompactIPv6NodeAddrs `bencode:"peers6"` -} - -type Peers []Peer - -func (me *Peers) UnmarshalBencode(b []byte) (err error) { - var _v interface{} - err = bencode.Unmarshal(b, &_v) - if err != nil { - return - } - switch v := _v.(type) { - case string: - vars.Add("http responses with string peers", 1) - var cnas krpc.CompactIPv4NodeAddrs - err = cnas.UnmarshalBinary([]byte(v)) - if err != nil { - return - } - for _, cp := range cnas { - *me = append(*me, Peer{ - IP: cp.IP[:], - Port: int(cp.Port), - }) - } - return - case []interface{}: - vars.Add("http responses with list peers", 1) - for _, i := range v { - var p Peer - p.FromDictInterface(i.(map[string]interface{})) - *me = append(*me, p) - } - return - default: - vars.Add("http responses with unhandled peers type", 1) - err = fmt.Errorf("unsupported type: %T", _v) - return - } -} - func setAnnounceParams(_url *url.URL, ar *AnnounceRequest, opts AnnounceOpt) { q := _url.Query() @@ -148,8 +72,8 @@ type AnnounceOpt struct { 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) +func (cl Client) Announce(ctx context.Context, ar AnnounceRequest, opt AnnounceOpt) (ret AnnounceResponse, err error) { + _url := httptoo.CopyURL(cl.url_) setAnnounceParams(_url, &ar, opt) req, err := http.NewRequestWithContext(ctx, http.MethodGet, _url.String(), nil) req.Header.Set("User-Agent", opt.UserAgent) diff --git a/tracker/http/protocol.go b/tracker/http/protocol.go new file mode 100644 index 00000000..0a54a1b3 --- /dev/null +++ b/tracker/http/protocol.go @@ -0,0 +1,57 @@ +package http + +import ( + "fmt" + + "github.com/anacrolix/dht/v2/krpc" + "github.com/anacrolix/torrent/bencode" +) + +type HttpResponse struct { + FailureReason string `bencode:"failure reason"` + Interval int32 `bencode:"interval"` + TrackerId string `bencode:"tracker id"` + Complete int32 `bencode:"complete"` + Incomplete int32 `bencode:"incomplete"` + Peers Peers `bencode:"peers"` + // BEP 7 + Peers6 krpc.CompactIPv6NodeAddrs `bencode:"peers6"` +} + +type Peers []Peer + +func (me *Peers) UnmarshalBencode(b []byte) (err error) { + var _v interface{} + err = bencode.Unmarshal(b, &_v) + if err != nil { + return + } + switch v := _v.(type) { + case string: + vars.Add("http responses with string peers", 1) + var cnas krpc.CompactIPv4NodeAddrs + err = cnas.UnmarshalBinary([]byte(v)) + if err != nil { + return + } + for _, cp := range cnas { + *me = append(*me, Peer{ + IP: cp.IP[:], + Port: int(cp.Port), + }) + } + return + case []interface{}: + vars.Add("http responses with list peers", 1) + for _, i := range v { + var p Peer + p.FromDictInterface(i.(map[string]interface{})) + *me = append(*me, p) + } + return + default: + vars.Add("http responses with unhandled peers type", 1) + err = fmt.Errorf("unsupported type: %T", _v) + return + } +} diff --git a/tracker/tracker.go b/tracker/tracker.go index 66344fe2..edc84dcc 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -51,10 +51,17 @@ type Announce struct { const DefaultTrackerAnnounceTimeout = 15 * time.Second func (me Announce) Do() (res AnnounceResponse, err error) { - _url, err := url.Parse(me.TrackerUrl) + cl, err := NewClient(me.TrackerUrl, NewClientOpts{ + Http: trHttp.NewClientOpts{ + Proxy: me.HTTPProxy, + ServerName: me.ServerName, + }, + UdpNetwork: me.UdpNetwork, + }) if err != nil { return } + defer cl.Close() if me.Context == nil { // This is just to maintain the old behaviour that should be a timeout of 15s. Users can // override it by providing their own Context. See comments elsewhere about longer timeouts @@ -63,22 +70,10 @@ func (me Announce) Do() (res AnnounceResponse, err error) { defer cancel() me.Context = ctx } - switch _url.Scheme { - case "http", "https": - 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: - err = ErrBadScheme - return - } + return cl.Announce(me.Context, me.Request, trHttp.AnnounceOpt{ + UserAgent: me.UserAgent, + HostHeader: me.HostHeader, + ClientIp4: me.ClientIp4.IP, + ClientIp6: me.ClientIp6.IP, + }) } diff --git a/tracker/udp.go b/tracker/udp.go index f7afc9ea..db486948 100644 --- a/tracker/udp.go +++ b/tracker/udp.go @@ -1,36 +1,31 @@ package tracker import ( + "context" "encoding/binary" - "net/url" trHttp "github.com/anacrolix/torrent/tracker/http" "github.com/anacrolix/torrent/tracker/udp" ) -type udpAnnounce struct { - url url.URL - a *Announce +type udpClient struct { + cl *udp.ConnClient + requestUri string } -func (c *udpAnnounce) Do(req AnnounceRequest) (res AnnounceResponse, err error) { - cl, err := udp.NewConnClient(udp.NewConnClientOpts{ - Network: c.dialNetwork(), - Host: c.url.Host, - Ipv6: nil, - }) - if err != nil { - return - } - defer cl.Close() - if req.IPAddress == 0 && c.a.ClientIp4.IP != nil { +func (c *udpClient) Close() error { + return c.cl.Close() +} + +func (c *udpClient) Announce(ctx context.Context, req AnnounceRequest, opts trHttp.AnnounceOpt) (res AnnounceResponse, err error) { + if req.IPAddress == 0 && opts.ClientIp4 != nil { // I think we're taking bytes in big-endian order (all IPs), and writing it to a natively // ordered uint32. This will be correctly ordered when written back out by the UDP client // later. I'm ignoring the fact that IPv6 announces shouldn't have an IP address, we have a // perfectly good IPv4 address. - req.IPAddress = binary.BigEndian.Uint32(c.a.ClientIp4.IP.To4()) + req.IPAddress = binary.BigEndian.Uint32(opts.ClientIp4.To4()) } - h, nas, err := cl.Announce(c.a.Context, req, udp.Options{RequestUri: c.url.RequestURI()}) + h, nas, err := c.cl.Announce(ctx, req, udp.Options{RequestUri: c.requestUri}) if err != nil { return } @@ -42,19 +37,3 @@ func (c *udpAnnounce) Do(req AnnounceRequest) (res AnnounceResponse, err error) } return } - -func (c *udpAnnounce) dialNetwork() string { - if c.a.UdpNetwork != "" { - return c.a.UdpNetwork - } - return "udp" -} - -// TODO: Split on IPv6, as BEP 15 says response peer decoding depends on network in use. -func announceUDP(opt Announce, _url *url.URL) (AnnounceResponse, error) { - ua := udpAnnounce{ - url: *_url, - a: &opt, - } - return ua.Do(opt.Request) -} diff --git a/tracker/udp/conn-client.go b/tracker/udp/conn-client.go index 64020569..802ceae7 100644 --- a/tracker/udp/conn-client.go +++ b/tracker/udp/conn-client.go @@ -54,17 +54,20 @@ func ipv6(opt *bool, network string, conn net.Conn) bool { return rip.To16() != nil && rip.To4() == nil } -func NewConnClient(opts NewConnClientOpts) (cc ConnClient, err error) { - cc.conn, err = net.Dial(opts.Network, opts.Host) +func NewConnClient(opts NewConnClientOpts) (cc *ConnClient, err error) { + conn, err := net.Dial(opts.Network, opts.Host) if err != nil { return } - cc.ipv6 = ipv6(opts.Ipv6, opts.Network, cc.conn) - go cc.reader() - cc.cl = Client{ - Dispatcher: &cc.d, - Writer: cc.conn, + cc = &ConnClient{ + cl: Client{ + Writer: conn, + }, + conn: conn, + ipv6: ipv6(opts.Ipv6, opts.Network, conn), } + cc.cl.Dispatcher = &cc.d + go cc.reader() return }