Ztorrent/internal/tracker/tracker.go
itexpert228 636fd5baa8
huy
2026-03-06 12:02:52 +03:00

212 lines
4.2 KiB
Go

package tracker
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/jackpal/bencode-go"
"github.com/veggiedefender/torrent-client/internal/torrentfile"
)
type TrackerResponse struct {
FailureReason string `bencode:"failure reason"`
Interval int `bencode:"interval"`
Peers string `bencode:"peers"`
}
type Peer struct {
IP net.IP
Port uint16
}
type AnnounceOptions struct {
PeerID [20]byte
Port uint16
Uploaded int64
Downloaded int64
NumWant int
Timeout time.Duration
}
type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
func GetPeers(ctx context.Context, tf *torrentfile.TorrentFile, opts AnnounceOptions) ([]Peer, error) {
opts = normalizeOptions(opts)
client := &http.Client{Timeout: opts.Timeout}
return getPeersWithClient(ctx, client, tf, opts)
}
func getPeersWithClient(ctx context.Context, client httpDoer, tf *torrentfile.TorrentFile, opts AnnounceOptions) ([]Peer, error) {
if tf == nil {
return nil, errors.New("torrent metadata is nil")
}
opts = normalizeOptions(opts)
if client == nil {
client = &http.Client{Timeout: opts.Timeout}
}
announceURL, err := buildAnnounceURL(tf, opts)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, announceURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("tracker returned HTTP %d", resp.StatusCode)
}
var tr TrackerResponse
err = bencode.Unmarshal(resp.Body, &tr)
if err != nil {
return nil, err
}
if tr.FailureReason != "" {
return nil, fmt.Errorf("tracker failure: %s", tr.FailureReason)
}
return parsePeers([]byte(tr.Peers))
}
func normalizeOptions(opts AnnounceOptions) AnnounceOptions {
if opts.Port == 0 {
opts.Port = 6881
}
if opts.NumWant <= 0 {
opts.NumWant = 50
}
if opts.Timeout <= 0 {
opts.Timeout = 10 * time.Second
}
return opts
}
func buildAnnounceURL(tf *torrentfile.TorrentFile, opts AnnounceOptions) (string, error) {
if tf == nil {
return "", errors.New("torrent metadata is nil")
}
baseURL, err := url.Parse(tf.Announce)
if err != nil {
return "", fmt.Errorf("invalid tracker URL %q: %w", tf.Announce, err)
}
if baseURL.Scheme != "http" && baseURL.Scheme != "https" {
return "", fmt.Errorf("unsupported tracker scheme %q (only http/https are supported)", baseURL.Scheme)
}
if baseURL.Host == "" {
return "", errors.New("tracker URL host is empty")
}
uploaded := opts.Uploaded
if uploaded < 0 {
uploaded = 0
}
downloaded := opts.Downloaded
if downloaded < 0 {
downloaded = 0
}
left := int64(tf.Length) - downloaded
if left < 0 {
left = 0
}
parts := make([]string, 0, 10)
if baseURL.RawQuery != "" {
parts = append(parts, baseURL.RawQuery)
}
parts = append(parts,
"info_hash="+escapeBinary(tf.InfoHash[:]),
"peer_id="+escapeBinary(opts.PeerID[:]),
"port="+strconv.Itoa(int(opts.Port)),
"uploaded="+strconv.FormatInt(uploaded, 10),
"downloaded="+strconv.FormatInt(downloaded, 10),
"left="+strconv.FormatInt(left, 10),
"compact=1",
"numwant="+strconv.Itoa(opts.NumWant),
)
baseURL.RawQuery = strings.Join(parts, "&")
return baseURL.String(), nil
}
func escapeBinary(data []byte) string {
const hex = "0123456789ABCDEF"
var b strings.Builder
for _, c := range data {
if isURLUnreserved(c) {
b.WriteByte(c)
continue
}
b.WriteByte('%')
b.WriteByte(hex[c>>4])
b.WriteByte(hex[c&0x0F])
}
return b.String()
}
func isURLUnreserved(c byte) bool {
switch {
case c >= 'a' && c <= 'z':
return true
case c >= 'A' && c <= 'Z':
return true
case c >= '0' && c <= '9':
return true
case c == '-', c == '.', c == '_', c == '~':
return true
default:
return false
}
}
func parsePeers(data []byte) ([]Peer, error) {
if len(data) == 0 {
return []Peer{}, nil
}
if len(data)%6 != 0 {
return nil, fmt.Errorf("invalid compact peers length %d", len(data))
}
var peers []Peer
for i := 0; i < len(data); i += 6 {
ip := net.IP(data[i : i+4])
port := uint16(data[i+4])<<8 |
uint16(data[i+5])
peers = append(peers, Peer{
IP: ip,
Port: port,
})
}
return peers, nil
}