Skip to content

Instantly share code, notes, and snippets.

@liamzebedee
Created June 27, 2024 12:49
Show Gist options
  • Save liamzebedee/4145b51f7cba2f9c24e516d8a778634c to your computer and use it in GitHub Desktop.
Save liamzebedee/4145b51f7cba2f9c24e516d8a778634c to your computer and use it in GitHub Desktop.
// https://wiki.theory.org/BitTorrent_Tracker_Protocol
// https://www.bittorrent.org/beps/bep_0015.html
package core
import (
"bytes"
"encoding/binary"
"fmt"
"math/rand"
"net"
"time"
)
const (
protocolID = 0x41727101980
actionConnect = 0
actionAnnounce = 1
actionScrape = 2
)
type ConnectRequest struct {
ProtocolID int64
Action int32
TransactionID int32
}
type ConnectResponse struct {
Action int32
TransactionID int32
ConnectionID int64
}
type AnnounceRequest struct {
ConnectionID int64
Action int32
TransactionID int32
InfoHash [20]byte
PeerID [20]byte
Downloaded int64
Left int64
Uploaded int64
Event int32
IPAddress int32
Key int32
NumWant int32
Port int16
}
type AnnounceResponse struct {
Action int32
TransactionID int32
Interval int32
Leechers int32
Seeders int32
IPs []Peer
}
type ScrapeRequest struct {
ConnectionID int64
Action int32
TransactionID int32
InfoHashes [][20]byte
}
type ScrapeResponse struct {
Action int32
TransactionID int32
Torrents []TorrentStats
}
type TorrentStats struct {
Seeders int32
Completed int32
Leechers int32
}
type Peer struct {
IP net.IP
Port uint16
}
func RunTrackerDemo(infohash [20]byte, peerId [20]byte) {
trackerAddr := "tracker.opentrackr.org:1337"
conn, err := net.Dial("udp", trackerAddr)
if err != nil {
fmt.Println("Error connecting to tracker:", err)
return
}
defer conn.Close()
// Step 1: Connect
transactionID := rand.Int31()
connectReq := ConnectRequest{
ProtocolID: protocolID,
Action: actionConnect,
TransactionID: transactionID,
}
if err := sendConnectRequest(conn, connectReq); err != nil {
fmt.Println("Error sending connect request:", err)
return
}
connectResp, err := receiveConnectResponse(conn, transactionID)
if err != nil {
fmt.Println("Error receiving connect response:", err)
return
}
// Step 2: Announce
announceReq := AnnounceRequest{
ConnectionID: connectResp.ConnectionID,
Action: actionAnnounce,
TransactionID: rand.Int31(),
InfoHash: infohash,
PeerID: peerId,
Downloaded: 0,
Left: 100,
Uploaded: 0,
Event: 0,
IPAddress: 0,
Key: rand.Int31(),
NumWant: -1,
Port: 6881,
}
if err := sendAnnounceRequest(conn, announceReq); err != nil {
fmt.Println("Error sending announce request:", err)
return
}
announceResp, err := receiveAnnounceResponse(conn, announceReq.TransactionID)
if err != nil {
fmt.Println("Error receiving announce response:", err)
return
}
fmt.Printf("Received Announce Response: %+v\n", announceResp)
}
func sendConnectRequest(conn net.Conn, req ConnectRequest) error {
buf := new(bytes.Buffer)
if err := binary.Write(buf, binary.BigEndian, req); err != nil {
return err
}
_, err := conn.Write(buf.Bytes())
return err
}
func receiveConnectResponse(conn net.Conn, transactionID int32) (*ConnectResponse, error) {
buf := make([]byte, 16)
conn.SetReadDeadline(time.Now().Add(15 * time.Second))
if _, err := conn.Read(buf); err != nil {
return nil, err
}
var resp ConnectResponse
if err := binary.Read(bytes.NewReader(buf), binary.BigEndian, &resp); err != nil {
return nil, err
}
if resp.TransactionID != transactionID {
return nil, fmt.Errorf("transaction ID mismatch")
}
return &resp, nil
}
func sendAnnounceRequest(conn net.Conn, req AnnounceRequest) error {
buf := new(bytes.Buffer)
if err := binary.Write(buf, binary.BigEndian, req); err != nil {
return err
}
_, err := conn.Write(buf.Bytes())
return err
}
func receiveAnnounceResponse(conn net.Conn, transactionID int32) (*AnnounceResponse, error) {
buf := make([]byte, 1024) // Adjust buffer size as needed
conn.SetReadDeadline(time.Now().Add(15 * time.Second))
n, err := conn.Read(buf)
if err != nil {
return nil, err
}
if n < 20 {
return nil, fmt.Errorf("announce response too short")
}
var action, txnID, interval, leechers, seeders int32
r := bytes.NewReader(buf[:20])
if err := binary.Read(r, binary.BigEndian, &action); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &txnID); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &interval); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &leechers); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &seeders); err != nil {
return nil, err
}
if txnID != transactionID {
return nil, fmt.Errorf("transaction ID mismatch")
}
numPeers := (n - 20) / 6
peers := make([]Peer, numPeers)
for i := 0; i < numPeers; i++ {
ip := net.IPv4(buf[20+6*i], buf[21+6*i], buf[22+6*i], buf[23+6*i])
port := binary.BigEndian.Uint16(buf[24+6*i : 26+6*i])
peers[i] = Peer{IP: ip, Port: port}
}
return &AnnounceResponse{
Action: action,
TransactionID: txnID,
Interval: interval,
Leechers: leechers,
Seeders: seeders,
IPs: peers,
}, nil
}
func sendScrapeRequest(conn net.Conn, req ScrapeRequest) error {
buf := new(bytes.Buffer)
if err := binary.Write(buf, binary.BigEndian, req.ConnectionID); err != nil {
return err
}
if err := binary.Write(buf, binary.BigEndian, req.Action); err != nil {
return err
}
if err := binary.Write(buf, binary.BigEndian, req.TransactionID); err != nil {
return err
}
for _, hash := range req.InfoHashes {
if err := binary.Write(buf, binary.BigEndian, hash); err != nil {
return err
}
}
_, err := conn.Write(buf.Bytes())
return err
}
func receiveScrapeResponse(conn net.Conn, transactionID int32) (*ScrapeResponse, error) {
buf := make([]byte, 1024) // Adjust buffer size as needed
conn.SetReadDeadline(time.Now().Add(15 * time.Second))
n, err := conn.Read(buf)
if err != nil {
return nil, err
}
if n < 8 {
return nil, fmt.Errorf("scrape response too short")
}
var action, txnID int32
r := bytes.NewReader(buf[:8])
if err := binary.Read(r, binary.BigEndian, &action); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &txnID); err != nil {
return nil, err
}
if txnID != transactionID {
return nil, fmt.Errorf("transaction ID mismatch")
}
numTorrents := (n - 8) / 12
torrents := make([]TorrentStats, numTorrents)
for i := 0; i < numTorrents; i++ {
offset := 8 + i*12
torrents[i] = TorrentStats{
Seeders: int32(binary.BigEndian.Uint32(buf[offset:])),
Completed: int32(binary.BigEndian.Uint32(buf[offset+4:])),
Leechers: int32(binary.BigEndian.Uint32(buf[offset+8:])),
}
}
return &ScrapeResponse{
Action: action,
TransactionID: txnID,
Torrents: torrents,
}, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment