mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 00:50:27 +00:00
380 lines
9.4 KiB
Go
380 lines
9.4 KiB
Go
package bt
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
mrand "math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
udpConnectRequestMagic = 0x41727101980
|
|
udpConnectTimeout = 15
|
|
udpConnectRetries = 8
|
|
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
|
|
Detail see 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
|
|
Detail see 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 int32
|
|
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], uint32(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
|
|
*/
|
|
type UdpAnnounceResponse struct {
|
|
Action uint32
|
|
TransactionId uint32
|
|
Interval uint32
|
|
Leechers uint32
|
|
Seeders uint32
|
|
IP []uint32
|
|
Port []uint16
|
|
}
|
|
|
|
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.IP = make([]uint32, count, count)
|
|
response.Port = make([]uint16, count, count)
|
|
for i := 0; i < count; i++ {
|
|
begin := 20 + 6*i
|
|
response.IP[i] = binary.BigEndian.Uint32(buf[begin : begin+4])
|
|
response.Port[i] = binary.BigEndian.Uint16(buf[begin+4 : begin+6])
|
|
}
|
|
return response
|
|
}
|
|
|
|
type UdpTrackerContent struct {
|
|
URL *url.URL
|
|
MetaInfo *MetaInfo
|
|
Conn *net.UDPConn
|
|
ConnectRequest *UdpConnectRequest
|
|
ConnectResponse *UdpConnectResponse
|
|
AnnounceRequest *UdpAnnounceRequest
|
|
AnnounceResponse *UdpAnnounceResponse
|
|
}
|
|
|
|
func NewUdpTrackerContent(url *url.URL, metaInfo *MetaInfo) *UdpTrackerContent {
|
|
return &UdpTrackerContent{
|
|
URL: url,
|
|
MetaInfo: metaInfo,
|
|
}
|
|
}
|
|
|
|
func (content *UdpTrackerContent) connect() error {
|
|
host, portStr, err := net.SplitHostPort(content.URL.Host)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return err
|
|
}
|
|
port, err := strconv.ParseInt(portStr, 10, 32)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return err
|
|
}
|
|
ip, err := lookupIP(host, content.URL.Scheme)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return err
|
|
}
|
|
|
|
srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0}
|
|
dstAddr := &net.UDPAddr{
|
|
IP: ip,
|
|
Port: int(port),
|
|
}
|
|
conn, err := net.DialUDP(content.URL.Scheme, srcAddr, dstAddr)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return err
|
|
}
|
|
// Connect
|
|
request := NewUdpConnectRequest()
|
|
var response *UdpConnectResponse
|
|
for n := 0; n < udpConnectRetries; n++ {
|
|
buf := request.Encode()
|
|
conn.Write(buf)
|
|
// 15 * 2 ^ n seconds
|
|
timeout := udpConnectTimeout * math.Pow(2, float64(n))
|
|
conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(timeout)))
|
|
len, err := conn.Read(buf)
|
|
if len == 16 && err == nil {
|
|
response = NewUdpConnectResponse(buf)
|
|
break
|
|
}
|
|
}
|
|
if response == nil {
|
|
conn.Close()
|
|
return errors.New("UDP tracker connect error")
|
|
}
|
|
if response.Action != udpActionConnect || request.TransactionId != response.TransactionId {
|
|
conn.Close()
|
|
return errors.New("UDP tracker connect response error")
|
|
}
|
|
content.Conn = conn
|
|
content.ConnectRequest = request
|
|
content.ConnectResponse = response
|
|
return nil
|
|
}
|
|
|
|
func (content *UdpTrackerContent) announce() error {
|
|
request := NewUdpAnnounceRequest(content.ConnectResponse.ConnectionId)
|
|
request.InfoHash = content.MetaInfo.InfoHash
|
|
peerID, err := genPeerID()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return err
|
|
}
|
|
request.PeerID = peerID
|
|
request.Downloaded = 0
|
|
request.Uploaded = 0
|
|
request.Left = content.MetaInfo.GetTotalSize() - request.Downloaded
|
|
request.Event = 0
|
|
request.IP = 0
|
|
request.Key = 0
|
|
request.NumWant = 50
|
|
request.Port = 6882
|
|
encode := request.encode()
|
|
content.Conn.Write(encode)
|
|
buf := make([]byte, 512)
|
|
len, err := content.Conn.Read(buf)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return err
|
|
}
|
|
response := NewUdpAnnounceResponse(buf[:len])
|
|
fmt.Println(response)
|
|
return nil
|
|
}
|
|
|
|
func DoTracker(announce string, metaInfo *MetaInfo) {
|
|
if announce != "" {
|
|
url, _ := url.Parse(announce)
|
|
switch url.Scheme {
|
|
case "http", "https":
|
|
// TODO UDP test
|
|
break
|
|
httpTracker(url, metaInfo)
|
|
case "udp", "udp4", "udp6":
|
|
udpTracker(url, metaInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
func genPeerID() ([20]byte, error) {
|
|
peerId := [20]byte{'G', 'P', '0', '0', '1'}
|
|
_, err := rand.Read(peerId[5:])
|
|
if err != nil {
|
|
return peerId, err
|
|
}
|
|
return peerId, nil
|
|
}
|
|
|
|
// http://bittorrent.org/beps/bep_0003.html#trackers
|
|
func httpTracker(url *url.URL, metaInfo *MetaInfo) {
|
|
query := url.Query()
|
|
query.Add("info_hash", string(metaInfo.InfoHash[:]))
|
|
// Generate peer ID
|
|
peerID, err := genPeerID()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
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 {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
var httpClient = &http.Client{
|
|
Timeout: time.Second * time.Duration(15),
|
|
}
|
|
request.Header.Set("User-Agent", "gopeed/0.0.1")
|
|
fmt.Println("request:" + url.String())
|
|
response, err := httpClient.Do(request)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
defer response.Body.Close()
|
|
resp, err := ioutil.ReadAll(response.Body)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
}
|
|
fmt.Println(string(resp))
|
|
}
|
|
|
|
// http://bittorrent.org/beps/bep_0015.html
|
|
func udpTracker(url *url.URL, metaInfo *MetaInfo) {
|
|
content := NewUdpTrackerContent(url, metaInfo)
|
|
err := content.connect()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
content.announce()
|
|
}
|
|
|
|
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
|
|
}
|