mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
431 lines
11 KiB
Go
431 lines
11 KiB
Go
package tracker
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
mrand "math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/monkeyWie/gopeed/protocol/bt/metainfo"
|
|
"github.com/monkeyWie/gopeed/protocol/bt/peer"
|
|
)
|
|
|
|
const (
|
|
udpConnectRequestMagic = 0x41727101980
|
|
udpConnectTimeout = 15
|
|
udpConnectRetries = 3
|
|
udpActionConnect = 0
|
|
udpActionAnnounce = 1
|
|
udpActionScrape = 2
|
|
udpActionError = 3
|
|
)
|
|
|
|
/*
|
|
Offset size Name Value
|
|
0 64-bit integer protocol_id 0x41727101980 // magic constant
|
|
8 32-bit integer action 0 // connect
|
|
12 32-bit integer transaction_id
|
|
16
|
|
Per https://www.libtorrent.org/udp_tracker_protocol.html#connecting
|
|
*/
|
|
type udpConnectRequest struct {
|
|
protocolId uint64
|
|
action uint32
|
|
transactionId uint32
|
|
}
|
|
|
|
func newUdpConnectRequest() *udpConnectRequest {
|
|
return &udpConnectRequest{
|
|
protocolId: udpConnectRequestMagic,
|
|
action: udpActionConnect,
|
|
transactionId: mrand.Uint32(),
|
|
}
|
|
}
|
|
|
|
func (req *udpConnectRequest) encode() []byte {
|
|
buf := make([]byte, 16)
|
|
binary.BigEndian.PutUint64(buf[:8], req.protocolId)
|
|
binary.BigEndian.PutUint32(buf[8:12], req.action)
|
|
binary.BigEndian.PutUint32(buf[12:], req.transactionId)
|
|
return buf
|
|
}
|
|
|
|
/*
|
|
Offset size Name Value
|
|
0 32-bit integer action 0 // connect
|
|
4 32-bit integer transaction_id
|
|
8 64-bit integer connection_id
|
|
16
|
|
*/
|
|
type udpConnectResponse struct {
|
|
action uint32
|
|
transactionId uint32
|
|
connectionId uint64
|
|
}
|
|
|
|
func newUdpConnectResponse(buf []byte) *udpConnectResponse {
|
|
return &udpConnectResponse{
|
|
action: binary.BigEndian.Uint32(buf[:4]),
|
|
transactionId: binary.BigEndian.Uint32(buf[4:8]),
|
|
connectionId: binary.BigEndian.Uint64(buf[8:]),
|
|
}
|
|
}
|
|
|
|
/*
|
|
Offset size Name Value
|
|
0 64-bit integer connection_id
|
|
8 32-bit integer action 1 // announce
|
|
12 32-bit integer transaction_id
|
|
16 20-byte string info_hash
|
|
36 20-byte string peer_id
|
|
56 64-bit integer downloaded
|
|
64 64-bit integer left
|
|
72 64-bit integer uploaded
|
|
80 32-bit integer event 0 // 0: none; 1: completed; 2: started; 3: stopped
|
|
84 32-bit integer ip address 0 // default
|
|
88 32-bit integer key
|
|
92 32-bit integer num_want -1 // default
|
|
96 16-bit integer port
|
|
98
|
|
Per https://www.libtorrent.org/udp_tracker_protocol.html#announcing
|
|
*/
|
|
type udpAnnounceRequest struct {
|
|
connectionId uint64
|
|
action uint32
|
|
transactionId uint32
|
|
infoHash [20]byte
|
|
peerID [20]byte
|
|
downloaded uint64
|
|
left uint64
|
|
uploaded uint64
|
|
event uint32
|
|
ip uint32
|
|
key uint32
|
|
numWant uint32
|
|
port uint16
|
|
}
|
|
|
|
func newUdpAnnounceRequest(connectionId uint64) *udpAnnounceRequest {
|
|
return &udpAnnounceRequest{
|
|
connectionId: connectionId,
|
|
action: udpActionAnnounce,
|
|
transactionId: mrand.Uint32(),
|
|
}
|
|
}
|
|
|
|
func (req *udpAnnounceRequest) encode() []byte {
|
|
buf := make([]byte, 98, 98)
|
|
binary.BigEndian.PutUint64(buf[:8], req.connectionId)
|
|
binary.BigEndian.PutUint32(buf[8:12], req.action)
|
|
binary.BigEndian.PutUint32(buf[12:16], req.transactionId)
|
|
copy(buf[16:36], req.infoHash[:])
|
|
copy(buf[36:56], req.peerID[:])
|
|
binary.BigEndian.PutUint64(buf[56:64], req.downloaded)
|
|
binary.BigEndian.PutUint64(buf[64:72], req.left)
|
|
binary.BigEndian.PutUint64(buf[72:80], req.uploaded)
|
|
binary.BigEndian.PutUint32(buf[80:84], req.event)
|
|
binary.BigEndian.PutUint32(buf[84:88], req.ip)
|
|
binary.BigEndian.PutUint32(buf[88:92], req.key)
|
|
binary.BigEndian.PutUint32(buf[92:96], req.numWant)
|
|
binary.BigEndian.PutUint16(buf[96:98], req.port)
|
|
return buf
|
|
}
|
|
|
|
/*
|
|
Offset size Name Value
|
|
0 32-bit integer action 1 // announce
|
|
4 32-bit integer transaction_id
|
|
8 32-bit integer interval
|
|
12 32-bit integer leechers
|
|
16 32-bit integer seeders
|
|
20 + 6 * n 32-bit integer ip address
|
|
24 + 6 * n 16-bit integer TCP port
|
|
20 + 6 * N
|
|
Per https://www.libtorrent.org/udp_tracker_protocol.html#announcing
|
|
*/
|
|
type udpAnnounceResponse struct {
|
|
action uint32
|
|
transactionId uint32
|
|
interval uint32
|
|
leechers uint32
|
|
seeders uint32
|
|
peers []peer.Peer
|
|
}
|
|
|
|
func newUdpAnnounceResponse(buf []byte) *udpAnnounceResponse {
|
|
response := &udpAnnounceResponse{
|
|
action: binary.BigEndian.Uint32(buf[:4]),
|
|
transactionId: binary.BigEndian.Uint32(buf[4:8]),
|
|
interval: binary.BigEndian.Uint32(buf[8:12]),
|
|
leechers: binary.BigEndian.Uint32(buf[12:16]),
|
|
seeders: binary.BigEndian.Uint32(buf[16:20]),
|
|
}
|
|
count := (len(buf) - 20) / 6
|
|
response.peers = make([]peer.Peer, count, count)
|
|
for i := 0; i < count; i++ {
|
|
ipBegin := 20 + 6*i
|
|
portBegin := ipBegin + 4
|
|
response.peers[i] = peer.Peer{
|
|
IP: binary.BigEndian.Uint32(buf[ipBegin:portBegin]),
|
|
Port: binary.BigEndian.Uint16(buf[portBegin : portBegin+2]),
|
|
}
|
|
}
|
|
return response
|
|
}
|
|
|
|
type Tracker struct {
|
|
PeerID [20]byte
|
|
MetaInfo *metainfo.MetaInfo
|
|
}
|
|
|
|
func (tracker *Tracker) connect(conn *net.UDPConn, timeout int64) (response *udpConnectResponse, err error) {
|
|
request := newUdpConnectRequest()
|
|
buf := request.encode()
|
|
conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(timeout)))
|
|
conn.Write(buf)
|
|
len, err := conn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len != 16 {
|
|
err = NewTrackerError(ErrResponse, errors.New("invalid response"))
|
|
return
|
|
}
|
|
response = newUdpConnectResponse(buf)
|
|
return
|
|
}
|
|
|
|
func (tracker *Tracker) announce(conn *net.UDPConn, timeout int64, connectionId uint64) (response *udpAnnounceResponse, err error) {
|
|
request := newUdpAnnounceRequest(connectionId)
|
|
request.infoHash = tracker.MetaInfo.GetInfoHash()
|
|
request.peerID = tracker.PeerID
|
|
request.downloaded = 0
|
|
request.uploaded = 0
|
|
request.left = tracker.MetaInfo.GetTotalSize() - request.downloaded
|
|
request.event = 0
|
|
request.ip = 0
|
|
request.key = 0
|
|
request.numWant = 200
|
|
request.port = 6882
|
|
encode := request.encode()
|
|
conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(timeout)))
|
|
conn.Write(encode)
|
|
buf := make([]byte, 20+request.numWant*6)
|
|
len, err := conn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len < 20 {
|
|
err = errors.New("announce fail")
|
|
return
|
|
}
|
|
response = newUdpAnnounceResponse(buf[:len])
|
|
return
|
|
}
|
|
|
|
// 通过tracker服务器获取peer信息
|
|
func (tracker *Tracker) Tracker() <-chan []peer.Peer {
|
|
peersCh := make(chan []peer.Peer)
|
|
metaInfo := tracker.MetaInfo
|
|
|
|
checkAnnounceList := func(announceList [][]string) bool {
|
|
if len(announceList) > 0 {
|
|
for _, l := range announceList {
|
|
if len(l) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 只要获取到了peers就直接返回
|
|
if checkAnnounceList(metaInfo.AnnounceList) {
|
|
wg := sync.WaitGroup{}
|
|
for _, announceArr := range metaInfo.AnnounceList {
|
|
if len(announceArr) > 0 {
|
|
for _, announce := range announceArr {
|
|
wg.Add(1)
|
|
go func(announce string) {
|
|
peers, err := tracker.DoTracker(announce)
|
|
if err == nil {
|
|
peersCh <- peers
|
|
}
|
|
wg.Done()
|
|
}(announce)
|
|
}
|
|
}
|
|
}
|
|
go func() {
|
|
wg.Wait()
|
|
close(peersCh)
|
|
}()
|
|
} else {
|
|
go func() {
|
|
peers, err := tracker.DoTracker(metaInfo.Announce)
|
|
if err == nil {
|
|
peersCh <- peers
|
|
}
|
|
close(peersCh)
|
|
}()
|
|
}
|
|
return peersCh
|
|
}
|
|
|
|
func (tracker *Tracker) DoTracker(announce string) (peers []peer.Peer, err error) {
|
|
if announce != "" {
|
|
url, _ := url.Parse(announce)
|
|
switch url.Scheme {
|
|
case "http", "https":
|
|
return tracker.httpTracker(url)
|
|
case "udp", "udp4", "udp6":
|
|
return tracker.udpTracker(url)
|
|
default:
|
|
return nil, errors.New("unsupported protocol")
|
|
}
|
|
}
|
|
return nil, errors.New("empty announce")
|
|
}
|
|
|
|
// http://bittorrent.org/beps/bep_0003.html#trackers
|
|
func (tracker *Tracker) httpTracker(url *url.URL) (peers []peer.Peer, err error) {
|
|
metaInfo := tracker.MetaInfo
|
|
peerID := tracker.PeerID
|
|
|
|
query := url.Query()
|
|
infoHash := metaInfo.GetInfoHash()
|
|
query.Add("info_hash", string(infoHash[:]))
|
|
query.Add("peer_id", string(peerID[:]))
|
|
query.Add("port", "6882")
|
|
query.Add("uploaded", "0")
|
|
query.Add("downloaded", "0")
|
|
query.Add("left", strconv.FormatInt(int64(metaInfo.GetTotalSize()), 10))
|
|
url.RawQuery = query.Encode()
|
|
request, err := http.NewRequest("GET", url.String(), nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var httpClient = &http.Client{
|
|
Timeout: time.Second * time.Duration(15),
|
|
}
|
|
request.Header.Set("User-Agent", "gopeed/0.0.1")
|
|
response, err := httpClient.Do(request)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer response.Body.Close()
|
|
resp, err := ioutil.ReadAll(response.Body)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
return
|
|
}
|
|
}
|
|
// TODO Parse HTTP tracker response
|
|
fmt.Println(resp)
|
|
return nil, nil
|
|
}
|
|
|
|
// http://bittorrent.org/beps/bep_0015.html
|
|
func (tracker *Tracker) udpTracker(url *url.URL) (peers []peer.Peer, err error) {
|
|
conn, err := dial(url)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
|
|
announceResponse, err := func() (announceResponse *udpAnnounceResponse, err error) {
|
|
var connectResponse *udpConnectResponse
|
|
// 0:connect 1:announce
|
|
state := udpActionConnect
|
|
// 访问超时最多重试8次,当有请求成功则将重试次数重置为0
|
|
for n := 0; n < udpConnectRetries; n++ {
|
|
// 15 * 2 ^ n seconds
|
|
timeout := int64(udpConnectTimeout * math.Pow(2, float64(n)))
|
|
switch state {
|
|
case udpActionConnect:
|
|
connectResponse, err = tracker.connect(conn, timeout)
|
|
if err != nil {
|
|
if oe, ok := err.(*net.OpError); ok && oe.Timeout() {
|
|
break
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
n = -1
|
|
state = udpActionAnnounce
|
|
break
|
|
case udpActionAnnounce:
|
|
announceResponse, err = tracker.announce(conn, timeout, connectResponse.connectionId)
|
|
if err != nil {
|
|
if oe, ok := err.(*net.OpError); ok && oe.Timeout() {
|
|
break
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}()
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return announceResponse.peers, nil
|
|
}
|
|
|
|
func dial(trackerUrl *url.URL) (*net.UDPConn, error) {
|
|
host, portStr, err := net.SplitHostPort(trackerUrl.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port, err := strconv.ParseInt(portStr, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ip, err := lookupIP(host, trackerUrl.Scheme)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0}
|
|
dstAddr := &net.UDPAddr{
|
|
IP: ip,
|
|
Port: int(port),
|
|
}
|
|
return net.DialUDP(trackerUrl.Scheme, srcAddr, dstAddr)
|
|
}
|
|
|
|
func lookupIP(host string, scheme string) (ip net.IP, err error) {
|
|
ips, _ := net.LookupIP(host)
|
|
if len(ips) == 0 {
|
|
err = errors.New("no ips")
|
|
return
|
|
}
|
|
for _, ip = range ips {
|
|
switch scheme {
|
|
case "udp", "udp4":
|
|
if ip.To4() != nil {
|
|
return
|
|
}
|
|
case "udp6":
|
|
if ip.To4() == nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
err = errors.New("no available ips ")
|
|
return
|
|
}
|