diff --git a/cmd/wl/main.go b/cmd/wl/main.go index 0c8d369..0de39c7 100644 --- a/cmd/wl/main.go +++ b/cmd/wl/main.go @@ -316,7 +316,13 @@ func runGet(opts getOpts) error { clientFiles[i] = client.FileEntry{Path: f.Path, Length: f.Length} } - v1Hash, _ := hex.DecodeString(mag.InfoHashV1) + // A v2-only magnet has no v1 hash, which is fine; but if one is present it + // must be valid hex — reject a malformed value at the boundary rather than + // handing a truncated hash to the downloader. + v1Hash, err := hex.DecodeString(mag.InfoHashV1) + if err != nil { + return fmt.Errorf("invalid v1 info hash %q: %w", mag.InfoHashV1, err) + } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() diff --git a/internal/client/tracker.go b/internal/client/tracker.go index 10d7274..c5ec640 100644 --- a/internal/client/tracker.go +++ b/internal/client/tracker.go @@ -61,6 +61,7 @@ func Announce(ctx context.Context, trackerURL, infoHash, peerID string, port int var trackerResponse struct { FailureReason string `bencode:"failure reason"` Peers string `bencode:"peers"` + Peers6 string `bencode:"peers6"` } if err := bencode.DecodeBytes(data, &trackerResponse); err != nil { @@ -71,12 +72,22 @@ func Announce(ctx context.Context, trackerURL, infoHash, peerID string, port int return nil, fmt.Errorf("tracker failure: %s", trackerResponse.FailureReason) } - return unpackPeers([]byte(trackerResponse.Peers)) + // BEP 3 compact IPv4 (6 bytes/peer) plus BEP 7 compact IPv6 (18 bytes/peer). + addrs, err := unpackPeers([]byte(trackerResponse.Peers), net.IPv4len) + if err != nil { + return nil, err + } + addrs6, err := unpackPeers([]byte(trackerResponse.Peers6), net.IPv6len) + if err != nil { + return nil, err + } + return append(addrs, addrs6...), nil } -// unpackPeers parses the compact BEP 3 peer list (6 bytes per peer: 4 for IPv4, 2 for port). -func unpackPeers(peers []byte) ([]string, error) { - const peerSize = 6 +// unpackPeers parses a compact peer list: ipLen address bytes (4 for IPv4, +// 16 for IPv6) followed by a 2-byte big-endian port, per peer. +func unpackPeers(peers []byte, ipLen int) ([]string, error) { + peerSize := ipLen + 2 if len(peers)%peerSize != 0 { return nil, fmt.Errorf("invalid peers string length: %d", len(peers)) } @@ -86,8 +97,8 @@ func unpackPeers(peers []byte) ([]string, error) { for i := 0; i < numPeers; i++ { offset := i * peerSize - ip := net.IPv4(peers[offset], peers[offset+1], peers[offset+2], peers[offset+3]) - port := binary.BigEndian.Uint16(peers[offset+4 : offset+6]) + ip := net.IP(peers[offset : offset+ipLen]) + port := binary.BigEndian.Uint16(peers[offset+ipLen : offset+peerSize]) addrs[i] = net.JoinHostPort(ip.String(), strconv.Itoa(int(port))) } diff --git a/internal/client/tracker_test.go b/internal/client/tracker_test.go index 92e6d70..f5389c5 100644 --- a/internal/client/tracker_test.go +++ b/internal/client/tracker_test.go @@ -19,7 +19,7 @@ func TestUnpackPeers(t *testing.T) { 5, 6, 7, 8, 0x1f, 0x90, // 8080 = 0x1f90 } - addrs, err := unpackPeers(peers) + addrs, err := unpackPeers(peers, net.IPv4len) if err != nil { t.Fatalf("unpackPeers failed: %v", err) } @@ -35,10 +35,29 @@ func TestUnpackPeers(t *testing.T) { } } +func TestUnpackPeersIPv6(t *testing.T) { + t.Parallel() + // One IPv6 peer: [::1]:6881 — 16 address bytes + 2 port bytes. + peers := make([]byte, 0, 18) + peers = append(peers, net.ParseIP("::1").To16()...) + peers = append(peers, 0x1a, 0xe1) // 6881 + + addrs, err := unpackPeers(peers, net.IPv6len) + if err != nil { + t.Fatalf("unpackPeers (v6) failed: %v", err) + } + if len(addrs) != 1 { + t.Fatalf("expected 1 peer, got %d", len(addrs)) + } + if addrs[0] != "[::1]:6881" { + t.Errorf("expected [::1]:6881, got %s", addrs[0]) + } +} + func TestUnpackPeersInvalidLength(t *testing.T) { t.Parallel() peers := []byte{1, 2, 3, 4, 5} // 5 bytes, not a multiple of 6 - _, err := unpackPeers(peers) + _, err := unpackPeers(peers, net.IPv4len) if err == nil { t.Error("expected error for invalid length") } diff --git a/internal/tracker/registry.go b/internal/tracker/registry.go index 001a3f6..6ae7d8d 100644 --- a/internal/tracker/registry.go +++ b/internal/tracker/registry.go @@ -1,6 +1,7 @@ package tracker import ( + "crypto/subtle" "database/sql" "encoding/json" "errors" @@ -89,7 +90,7 @@ func HandleAPI(w http.ResponseWriter, r *http.Request) { case http.MethodPost: if registryKey != "" { - if r.Header.Get("X-Weightless-Key") != registryKey { + if subtle.ConstantTimeCompare([]byte(r.Header.Get("X-Weightless-Key")), []byte(registryKey)) != 1 { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } @@ -144,7 +145,7 @@ func HandleAPI(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: if registryKey != "" { - if r.Header.Get("X-Weightless-Key") != registryKey { + if subtle.ConstantTimeCompare([]byte(r.Header.Get("X-Weightless-Key")), []byte(registryKey)) != 1 { http.Error(w, "Unauthorized", http.StatusUnauthorized) return }