373 lines
8.7 KiB
Go
373 lines
8.7 KiB
Go
package torrent
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/veggiedefender/torrent-client/internal/torrentfile"
|
|
)
|
|
|
|
const (
|
|
wireProtocolString = "BitTorrent protocol"
|
|
|
|
msgChoke = 0
|
|
msgUnchoke = 1
|
|
msgInterested = 2
|
|
msgNotInterested = 3
|
|
msgHave = 4
|
|
msgBitfield = 5
|
|
msgRequest = 6
|
|
msgPiece = 7
|
|
|
|
maxWireMessageSize = 2 * 1024 * 1024
|
|
requestBlockSize = 16 * 1024
|
|
|
|
peerConnectTimeout = 8 * time.Second
|
|
peerReadTimeout = 15 * time.Second
|
|
peerWriteTimeout = 10 * time.Second
|
|
)
|
|
|
|
type wireMessage struct {
|
|
ID int
|
|
Payload []byte
|
|
}
|
|
|
|
type peerClient struct {
|
|
conn net.Conn
|
|
|
|
have []bool
|
|
hasPieceInfo bool
|
|
peerIsChoked bool
|
|
}
|
|
|
|
func newPeerClient(ctx context.Context, addr string, infoHash [20]byte, peerID [20]byte, pieceCount int) (*peerClient, error) {
|
|
dialer := net.Dialer{Timeout: peerConnectTimeout}
|
|
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pc := &peerClient{
|
|
conn: conn,
|
|
have: make([]bool, pieceCount),
|
|
peerIsChoked: true,
|
|
}
|
|
|
|
if err := pc.sendHandshake(ctx, infoHash, peerID); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
if err := pc.readHandshake(ctx, infoHash); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
if err := pc.sendMessage(ctx, msgInterested, nil); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
if err := pc.readInitialMessages(ctx); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return pc, nil
|
|
}
|
|
|
|
func (pc *peerClient) Close() error {
|
|
return pc.conn.Close()
|
|
}
|
|
|
|
func (pc *peerClient) PieceAvailability() ([]bool, bool) {
|
|
have := make([]bool, len(pc.have))
|
|
copy(have, pc.have)
|
|
return have, pc.hasPieceInfo
|
|
}
|
|
|
|
func (pc *peerClient) DownloadPiece(ctx context.Context, pieceIndex int, pieceLength int) ([]byte, error) {
|
|
if pieceLength <= 0 {
|
|
return nil, fmt.Errorf("invalid piece length %d", pieceLength)
|
|
}
|
|
|
|
if err := pc.waitForUnchoke(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
piece := make([]byte, pieceLength)
|
|
for offset := 0; offset < pieceLength; {
|
|
blockLength := requestBlockSize
|
|
if remaining := pieceLength - offset; remaining < blockLength {
|
|
blockLength = remaining
|
|
}
|
|
|
|
if err := pc.sendRequest(ctx, pieceIndex, offset, blockLength); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
block, err := pc.readPieceBlock(ctx, pieceIndex, offset, blockLength)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copy(piece[offset:], block)
|
|
offset += len(block)
|
|
}
|
|
|
|
return piece, nil
|
|
}
|
|
|
|
func (pc *peerClient) readInitialMessages(ctx context.Context) error {
|
|
if err := pc.setReadDeadlineFromContext(ctx, 2*time.Second); err != nil {
|
|
return err
|
|
}
|
|
defer pc.conn.SetReadDeadline(time.Time{})
|
|
|
|
for {
|
|
msg, err := readWireMessage(pc.conn)
|
|
if err != nil {
|
|
if isTimeout(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
pc.consumeMessage(msg)
|
|
}
|
|
}
|
|
|
|
func (pc *peerClient) waitForUnchoke(ctx context.Context) error {
|
|
if !pc.peerIsChoked {
|
|
return nil
|
|
}
|
|
|
|
deadline := time.Now().Add(12 * time.Second)
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return errors.New("peer did not unchoke")
|
|
}
|
|
|
|
if err := pc.setReadDeadlineFromContext(ctx, peerReadTimeout); err != nil {
|
|
return err
|
|
}
|
|
msg, err := readWireMessage(pc.conn)
|
|
if err != nil {
|
|
if isTimeout(err) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
pc.consumeMessage(msg)
|
|
if msg.ID == msgUnchoke {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (pc *peerClient) sendRequest(ctx context.Context, pieceIndex, begin, length int) error {
|
|
payload := make([]byte, 12)
|
|
binary.BigEndian.PutUint32(payload[0:4], uint32(pieceIndex))
|
|
binary.BigEndian.PutUint32(payload[4:8], uint32(begin))
|
|
binary.BigEndian.PutUint32(payload[8:12], uint32(length))
|
|
return pc.sendMessage(ctx, msgRequest, payload)
|
|
}
|
|
|
|
func (pc *peerClient) readPieceBlock(ctx context.Context, pieceIndex, begin, expectedLen int) ([]byte, error) {
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := pc.setReadDeadlineFromContext(ctx, peerReadTimeout); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msg, err := readWireMessage(pc.conn)
|
|
if err != nil {
|
|
if isTimeout(err) {
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
switch msg.ID {
|
|
case msgPiece:
|
|
if len(msg.Payload) < 8 {
|
|
continue
|
|
}
|
|
gotIndex := int(binary.BigEndian.Uint32(msg.Payload[0:4]))
|
|
gotBegin := int(binary.BigEndian.Uint32(msg.Payload[4:8]))
|
|
block := msg.Payload[8:]
|
|
if gotIndex != pieceIndex || gotBegin != begin {
|
|
pc.consumeMessage(msg)
|
|
continue
|
|
}
|
|
if len(block) == 0 {
|
|
continue
|
|
}
|
|
if len(block) > expectedLen {
|
|
block = block[:expectedLen]
|
|
}
|
|
return block, nil
|
|
case msgChoke:
|
|
pc.consumeMessage(msg)
|
|
return nil, errors.New("peer choked")
|
|
default:
|
|
pc.consumeMessage(msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (pc *peerClient) consumeMessage(msg wireMessage) {
|
|
switch msg.ID {
|
|
case msgChoke:
|
|
pc.peerIsChoked = true
|
|
case msgUnchoke:
|
|
pc.peerIsChoked = false
|
|
case msgHave:
|
|
if len(msg.Payload) < 4 {
|
|
return
|
|
}
|
|
idx := int(binary.BigEndian.Uint32(msg.Payload[:4]))
|
|
if idx >= 0 && idx < len(pc.have) {
|
|
pc.have[idx] = true
|
|
pc.hasPieceInfo = true
|
|
}
|
|
case msgBitfield:
|
|
pc.hasPieceInfo = true
|
|
for i := range pc.have {
|
|
pc.have[i] = bitfieldHasPiece(msg.Payload, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (pc *peerClient) sendHandshake(ctx context.Context, infoHash [20]byte, peerID [20]byte) error {
|
|
payload := make([]byte, 49+len(wireProtocolString))
|
|
payload[0] = byte(len(wireProtocolString))
|
|
copy(payload[1:1+len(wireProtocolString)], wireProtocolString)
|
|
copy(payload[1+len(wireProtocolString)+8:1+len(wireProtocolString)+8+20], infoHash[:])
|
|
copy(payload[1+len(wireProtocolString)+8+20:], peerID[:])
|
|
|
|
if err := pc.setWriteDeadlineFromContext(ctx, peerWriteTimeout); err != nil {
|
|
return err
|
|
}
|
|
_, err := pc.conn.Write(payload)
|
|
return err
|
|
}
|
|
|
|
func (pc *peerClient) readHandshake(ctx context.Context, expectedInfoHash [20]byte) error {
|
|
head := make([]byte, 1)
|
|
if err := pc.setReadDeadlineFromContext(ctx, peerReadTimeout); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.ReadFull(pc.conn, head); err != nil {
|
|
return err
|
|
}
|
|
|
|
pstrlen := int(head[0])
|
|
if pstrlen <= 0 || pstrlen > 64 {
|
|
return fmt.Errorf("invalid handshake pstrlen %d", pstrlen)
|
|
}
|
|
|
|
rest := make([]byte, pstrlen+48)
|
|
if _, err := io.ReadFull(pc.conn, rest); err != nil {
|
|
return err
|
|
}
|
|
|
|
if string(rest[:pstrlen]) != wireProtocolString {
|
|
return errors.New("invalid peer protocol string")
|
|
}
|
|
|
|
infoHashOffset := pstrlen + 8
|
|
if !bytes.Equal(rest[infoHashOffset:infoHashOffset+20], expectedInfoHash[:]) {
|
|
return errors.New("peer info_hash mismatch")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (pc *peerClient) sendMessage(ctx context.Context, msgID int, payload []byte) error {
|
|
if err := pc.setWriteDeadlineFromContext(ctx, peerWriteTimeout); err != nil {
|
|
return err
|
|
}
|
|
|
|
length := uint32(1 + len(payload))
|
|
buf := make([]byte, 4+length)
|
|
binary.BigEndian.PutUint32(buf[0:4], length)
|
|
buf[4] = byte(msgID)
|
|
copy(buf[5:], payload)
|
|
|
|
_, err := pc.conn.Write(buf)
|
|
return err
|
|
}
|
|
|
|
func readWireMessage(r io.Reader) (wireMessage, error) {
|
|
var lengthBuf [4]byte
|
|
if _, err := io.ReadFull(r, lengthBuf[:]); err != nil {
|
|
return wireMessage{}, err
|
|
}
|
|
|
|
length := binary.BigEndian.Uint32(lengthBuf[:])
|
|
if length == 0 {
|
|
return wireMessage{ID: -1}, nil
|
|
}
|
|
if length > maxWireMessageSize {
|
|
return wireMessage{}, fmt.Errorf("wire message too large: %d", length)
|
|
}
|
|
|
|
msg := make([]byte, length)
|
|
if _, err := io.ReadFull(r, msg); err != nil {
|
|
return wireMessage{}, err
|
|
}
|
|
return wireMessage{ID: int(msg[0]), Payload: msg[1:]}, nil
|
|
}
|
|
|
|
func bitfieldHasPiece(bitfield []byte, index int) bool {
|
|
byteIndex := index / 8
|
|
if byteIndex < 0 || byteIndex >= len(bitfield) {
|
|
return false
|
|
}
|
|
bitOffset := 7 - (index % 8)
|
|
return bitfield[byteIndex]&(1<<bitOffset) != 0
|
|
}
|
|
|
|
func pieceSizeForIndex(tf *torrentfile.TorrentFile, pieceIndex int) int {
|
|
if pieceIndex < 0 || pieceIndex >= len(tf.PieceHashes) {
|
|
return 0
|
|
}
|
|
if pieceIndex == len(tf.PieceHashes)-1 {
|
|
used := tf.PieceLength * (len(tf.PieceHashes) - 1)
|
|
return tf.Length - used
|
|
}
|
|
return tf.PieceLength
|
|
}
|
|
|
|
func (pc *peerClient) setReadDeadlineFromContext(ctx context.Context, fallback time.Duration) error {
|
|
deadline := time.Now().Add(fallback)
|
|
if ctxDeadline, ok := ctx.Deadline(); ok && ctxDeadline.Before(deadline) {
|
|
deadline = ctxDeadline
|
|
}
|
|
return pc.conn.SetReadDeadline(deadline)
|
|
}
|
|
|
|
func (pc *peerClient) setWriteDeadlineFromContext(ctx context.Context, fallback time.Duration) error {
|
|
deadline := time.Now().Add(fallback)
|
|
if ctxDeadline, ok := ctx.Deadline(); ok && ctxDeadline.Before(deadline) {
|
|
deadline = ctxDeadline
|
|
}
|
|
return pc.conn.SetWriteDeadline(deadline)
|
|
}
|
|
|
|
func isTimeout(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
var netErr net.Error
|
|
return errors.As(err, &netErr) && netErr.Timeout()
|
|
}
|