Wires anacrolix/torrent's built-in webtorrent package so a browser running webtorrent.js can fetch pieces from this CLI via WebRTC data channels. The daemon stays the seeder; we never relay bytes through TorrentClaw infrastructure — same legal posture as today. Changes: - internal/config: new [downloads.webrtc] section (enabled/trackers/stun_servers/turn_servers/turn_user/turn_pass). Disabled by default, opt-in via config.toml. When enabled but trackers / STUN slices are empty, defaults are reapplied on Load() so users get a working setup with a single `enabled = true`. - internal/engine: TorrentConfig gains WebRTCEnabled / WebRTCTrackers / ICEServers; NewTorrentDownloader populates ClientConfig.ICEServerList and forces NoUpload=false when WebRTC is on (browsers can't pull otherwise). buildMagnet now accepts variadic extra trackers and the downloader method prepends WSS trackers so anacrolix's webtorrent.TrackerClient picks them up first. - internal/engine/webrtc.go: BuildICEServers helper converts the TOML WebRTCConfig into []webrtc.ICEServer with shared TURN credentials. - internal/cmd/daemon.go + download.go: pass WebRTC config through to the engine. Tests (8 new, all green; full suite 0 lint issues, 0 vet): - buildMagnet free function: defaults-only, with extras, trim+empty-skip - downloader method: WebRTC disabled keeps WSS out, enabled prepends them - BuildICEServers: nil when disabled, STUN-only path, TURN+credentials - NewTorrentDownloader: full WebRTC-enabled construction (logs WebRTC peer enabled, magnet contains wss://tracker.torrentclaw.com) End-to-end smoke (browser ↔ unarr peer transfer) is deferred to a manual test once tracker.torrentclaw.com WSS is live.
177 lines
6.1 KiB
Go
177 lines
6.1 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/torrentclaw/unarr/internal/config"
|
|
)
|
|
|
|
const validHash = "aaf2c71b0e0a03d3f9b2a3e1d5c6b7a8f0e1d2c3"
|
|
|
|
// TestBuildMagnet_NoExtras verifies the legacy free-function path keeps
|
|
// emitting only the static defaultTrackers list.
|
|
func TestBuildMagnet_NoExtras(t *testing.T) {
|
|
got := buildMagnet(validHash)
|
|
if !strings.HasPrefix(got, "magnet:?xt=urn:btih:"+validHash) {
|
|
t.Fatalf("magnet missing xt: %s", got)
|
|
}
|
|
if !strings.Contains(got, url.QueryEscape("udp://tracker.opentrackr.org:1337/announce")) {
|
|
t.Fatal("expected default UDP tracker absent")
|
|
}
|
|
if strings.Contains(got, "wss%3A") {
|
|
t.Fatalf("unexpected WSS tracker leaked when none requested: %s", got)
|
|
}
|
|
}
|
|
|
|
// TestBuildMagnet_WithExtraTrackers verifies extraTrackers (e.g. WebRTC
|
|
// WSS endpoints) are prepended before the defaults and properly URL-encoded.
|
|
func TestBuildMagnet_WithExtraTrackers(t *testing.T) {
|
|
got := buildMagnet(validHash, "wss://tracker.torrentclaw.com")
|
|
encWss := url.QueryEscape("wss://tracker.torrentclaw.com")
|
|
encUDP := url.QueryEscape("udp://tracker.opentrackr.org:1337/announce")
|
|
if !strings.Contains(got, "tr="+encWss) {
|
|
t.Fatalf("WSS tracker missing: %s", got)
|
|
}
|
|
wssIdx := strings.Index(got, encWss)
|
|
udpIdx := strings.Index(got, encUDP)
|
|
if wssIdx < 0 || udpIdx < 0 || wssIdx > udpIdx {
|
|
t.Fatalf("WSS tracker should appear BEFORE UDP defaults: wss=%d udp=%d", wssIdx, udpIdx)
|
|
}
|
|
}
|
|
|
|
// TestBuildMagnet_TrimsAndSkipsEmpty makes sure callers passing config-derived
|
|
// slices with stray whitespace or empty strings don't get malformed magnets.
|
|
func TestBuildMagnet_TrimsAndSkipsEmpty(t *testing.T) {
|
|
got := buildMagnet(validHash, " wss://tracker.torrentclaw.com ", "", " ")
|
|
encWss := url.QueryEscape("wss://tracker.torrentclaw.com")
|
|
if !strings.Contains(got, "tr="+encWss) {
|
|
t.Fatalf("trimmed WSS tracker missing: %s", got)
|
|
}
|
|
if strings.Contains(got, "tr=&") || strings.HasSuffix(got, "tr=") {
|
|
t.Fatalf("empty tracker emitted: %s", got)
|
|
}
|
|
}
|
|
|
|
// TestTorrentDownloader_buildMagnet_WebRTCDisabled confirms the downloader
|
|
// method does NOT inject WebRTCTrackers when WebRTCEnabled is false.
|
|
func TestTorrentDownloader_buildMagnet_WebRTCDisabled(t *testing.T) {
|
|
d := &TorrentDownloader{cfg: TorrentConfig{
|
|
WebRTCEnabled: false,
|
|
WebRTCTrackers: []string{"wss://tracker.torrentclaw.com"},
|
|
}}
|
|
got := d.buildMagnet(validHash)
|
|
if strings.Contains(got, "wss%3A") {
|
|
t.Fatalf("WSS tracker leaked while WebRTCEnabled=false: %s", got)
|
|
}
|
|
}
|
|
|
|
// TestTorrentDownloader_buildMagnet_WebRTCEnabled confirms the WSS trackers
|
|
// are present when WebRTCEnabled is true.
|
|
func TestTorrentDownloader_buildMagnet_WebRTCEnabled(t *testing.T) {
|
|
d := &TorrentDownloader{cfg: TorrentConfig{
|
|
WebRTCEnabled: true,
|
|
WebRTCTrackers: []string{"wss://tracker.torrentclaw.com", "wss://tracker2.example.com"},
|
|
}}
|
|
got := d.buildMagnet(validHash)
|
|
for _, want := range []string{
|
|
"wss://tracker.torrentclaw.com",
|
|
"wss://tracker2.example.com",
|
|
} {
|
|
if !strings.Contains(got, url.QueryEscape(want)) {
|
|
t.Fatalf("expected tracker %q missing in magnet: %s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildICEServers_DisabledReturnsNil ensures we don't leak STUN/TURN
|
|
// configuration into the torrent client when the user has WebRTC off.
|
|
func TestBuildICEServers_DisabledReturnsNil(t *testing.T) {
|
|
got := BuildICEServers(config.WebRTCConfig{
|
|
Enabled: false,
|
|
STUNServers: []string{"stun:stun.l.google.com:19302"},
|
|
})
|
|
if got != nil {
|
|
t.Fatalf("expected nil ICE servers when disabled, got %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestBuildICEServers_STUNOnly converts STUN entries to bare ICEServer
|
|
// records with no credentials.
|
|
func TestBuildICEServers_STUNOnly(t *testing.T) {
|
|
got := BuildICEServers(config.WebRTCConfig{
|
|
Enabled: true,
|
|
STUNServers: []string{"stun:stun.l.google.com:19302", "", "stun:stun1.l.google.com:19302"},
|
|
})
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 STUN servers (empty skipped), got %d (%+v)", len(got), got)
|
|
}
|
|
if got[0].URLs[0] != "stun:stun.l.google.com:19302" {
|
|
t.Fatalf("first server unexpected: %+v", got[0])
|
|
}
|
|
if got[0].Username != "" || got[0].Credential != nil {
|
|
t.Fatalf("STUN entry should have no credentials, got %+v", got[0])
|
|
}
|
|
}
|
|
|
|
// TestNewTorrentDownloader_WebRTCEnabled creates a downloader with the
|
|
// WebRTC peer fully wired up and confirms the constructor doesn't error
|
|
// (anacrolix accepts the ICE server list, port binds, etc.).
|
|
func TestNewTorrentDownloader_WebRTCEnabled(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dl, err := NewTorrentDownloader(TorrentConfig{
|
|
DataDir: dir,
|
|
ListenPort: 0, // let the OS pick — avoid clashes in CI
|
|
WebRTCEnabled: true,
|
|
WebRTCTrackers: []string{"wss://tracker.torrentclaw.com"},
|
|
ICEServers: BuildICEServers(config.WebRTCConfig{
|
|
Enabled: true,
|
|
STUNServers: []string{"stun:stun.l.google.com:19302"},
|
|
}),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("WebRTC-enabled downloader failed to start: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := dl.Shutdown(context.Background()); err != nil {
|
|
t.Logf("shutdown: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Magnet for any task should now contain the WSS tracker.
|
|
got := dl.buildMagnet(validHash)
|
|
if !strings.Contains(got, "wss%3A%2F%2Ftracker.torrentclaw.com") {
|
|
t.Fatalf("WebRTC magnet missing WSS tracker: %s", got)
|
|
}
|
|
}
|
|
|
|
// TestBuildICEServers_TURNWithCreds applies TURNUser/TURNPass to every TURN
|
|
// entry so the operator only specifies them once.
|
|
func TestBuildICEServers_TURNWithCreds(t *testing.T) {
|
|
got := BuildICEServers(config.WebRTCConfig{
|
|
Enabled: true,
|
|
STUNServers: []string{"stun:stun.l.google.com:19302"},
|
|
TURNServers: []string{"turn:turn.example.com:3478"},
|
|
TURNUser: "alice",
|
|
TURNPass: "s3cr3t",
|
|
})
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 1 STUN + 1 TURN, got %d", len(got))
|
|
}
|
|
turn := got[1]
|
|
if turn.URLs[0] != "turn:turn.example.com:3478" {
|
|
t.Fatalf("TURN URL wrong: %+v", turn)
|
|
}
|
|
if turn.Username != "alice" {
|
|
t.Fatalf("TURN username wrong: %s", turn.Username)
|
|
}
|
|
if turn.Credential != "s3cr3t" {
|
|
t.Fatalf("TURN credential wrong: %v", turn.Credential)
|
|
}
|
|
if turn.CredentialType != webrtc.ICECredentialTypePassword {
|
|
t.Fatalf("TURN credential type wrong: %v", turn.CredentialType)
|
|
}
|
|
}
|