├── .gitignore ├── physmem ├── physmem.go ├── physmem_bsd.go ├── physmem_linux.go ├── physmem_stubs.go └── physmem_windows.go ├── dht ├── dht_windows.go ├── dht_stubs.go ├── dht.h └── dht_export.go ├── fuse ├── fuse_test.go ├── fuse_stubs.go └── fuse.go ├── alloc ├── alloc.go ├── alloc_unix.go └── alloc_test.go ├── rundht ├── rundht_stubs.go └── rundht.go ├── go.mod ├── hash ├── hash_test.go └── hash.go ├── mono ├── mono_test.go └── mono.go ├── pex ├── pex_test.go └── pex.go ├── LICENCE ├── webseed ├── webseed_test.go ├── hoffman.go ├── webseed.go └── getright.go ├── path ├── path.go └── path_test.go ├── tor ├── torrents.go ├── requests.go ├── writer.go ├── metadata.go ├── initial.go ├── reader.go ├── piece │ └── piece_test.go └── torfile.go ├── crypto ├── conn.go └── crypto_test.go ├── README ├── httpclient └── httpclient.go ├── go.sum ├── rate ├── rate_test.go └── rate.go ├── known └── known.go ├── protocol ├── requests.go ├── handshake.go ├── writer.go ├── protocol_test.go └── reader.go ├── tracker ├── tracker.go ├── http.go └── udp.go ├── bitmap ├── bitmap.go └── bitmap_test.go ├── config └── config.go ├── peer ├── requests │ ├── requests_test.go │ └── requests.go └── event.go └── storrent.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | dht.dat 3 | storrent 4 | -------------------------------------------------------------------------------- /physmem/physmem.go: -------------------------------------------------------------------------------- 1 | // Package physmem computes the amount of physical memory on the machine. 2 | 3 | package physmem 4 | -------------------------------------------------------------------------------- /dht/dht_windows.go: -------------------------------------------------------------------------------- 1 | // +build cgo,windows 2 | 3 | package dht 4 | 5 | /* 6 | #cgo LDFLAGS: -lws2_32 7 | */ 8 | import "C" 9 | 10 | -------------------------------------------------------------------------------- /fuse/fuse_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package fuse 5 | 6 | import ( 7 | "testing" 8 | 9 | "bazil.org/fuse/fs" 10 | ) 11 | 12 | func TestHashable(t *testing.T) { 13 | table := make(map[fs.Node]int) 14 | 15 | table[root(0)] = 42 16 | table[directory{}] = 57 17 | table[file{}] = 12 18 | } 19 | -------------------------------------------------------------------------------- /physmem/physmem_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd 2 | // +build darwin dragonfly freebsd netbsd openbsd 3 | 4 | package physmem 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | func Total() (int64, error) { 9 | v, err := unix.SysctlUint64("hw.memsize") 10 | return int64(v), err 11 | } 12 | -------------------------------------------------------------------------------- /fuse/fuse_stubs.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package fuse 5 | 6 | import ( 7 | "errors" 8 | ) 9 | 10 | var ErrNotImplemented = errors.New("not implemented") 11 | 12 | func Serve(mountpoint string) error { 13 | return ErrNotImplemented 14 | } 15 | 16 | func Close(mountpoint string) error { 17 | return ErrNotImplemented 18 | } 19 | -------------------------------------------------------------------------------- /physmem/physmem_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package physmem 5 | 6 | import "syscall" 7 | 8 | // Total returns the amount of memory on the local machine in bytes. 9 | func Total() (int64, error) { 10 | var info syscall.Sysinfo_t 11 | err := syscall.Sysinfo(&info) 12 | if err != nil { 13 | return -1, err 14 | } 15 | return int64(info.Totalram) * int64(info.Unit), nil 16 | } 17 | -------------------------------------------------------------------------------- /dht/dht_stubs.go: -------------------------------------------------------------------------------- 1 | // +build !cgo 2 | 3 | package dht 4 | 5 | import ( 6 | "net/netip" 7 | ) 8 | 9 | func Available() bool { 10 | return false 11 | } 12 | 13 | func Ping(netip.AddrPort) error { 14 | return nil 15 | } 16 | 17 | func Announce(id []byte, ipv6 bool, port uint16) error { 18 | return nil 19 | } 20 | 21 | func Count() (good4 int, good6 int, 22 | dubious4 int, dubious6 int, 23 | incoming4 int, incoming6 int) { 24 | return 25 | } 26 | 27 | -------------------------------------------------------------------------------- /physmem/physmem_stubs.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd 2 | // +build !linux,!windows,!darwin,!dragonfly,!freebsd,!netbsd,!openbsd 3 | 4 | package physmem 5 | 6 | import "errors" 7 | 8 | // Total is not implemented on this platform. On other platforms, it 9 | // returns the amount of memory on the local machine in bytes. 10 | func Total() (int64, error) { 11 | return -1, errors.New("cannot compute physical memory on this platform") 12 | } 13 | -------------------------------------------------------------------------------- /alloc/alloc.go: -------------------------------------------------------------------------------- 1 | // +build !unix,!linux 2 | 3 | package alloc 4 | 5 | import ( 6 | "log" 7 | "sync/atomic" 8 | ) 9 | 10 | func init() { 11 | log.Printf("Using generic memory allocator") 12 | } 13 | 14 | var allocated int64 15 | 16 | func Alloc(size int) ([]byte, error) { 17 | atomic.AddInt64(&allocated, int64(size)) 18 | return make([]byte, size), nil 19 | } 20 | 21 | func Free(p []byte) error { 22 | atomic.AddInt64(&allocated, -int64(len(p))) 23 | return nil 24 | } 25 | 26 | func Bytes() int64 { 27 | return atomic.LoadInt64(&allocated) 28 | } 29 | -------------------------------------------------------------------------------- /rundht/rundht_stubs.go: -------------------------------------------------------------------------------- 1 | // +build !cgo 2 | 3 | package rundht 4 | 5 | import ( 6 | "context" 7 | "net/netip" 8 | ) 9 | 10 | func Read(filename string) ([]byte, []netip.AddrPort, error) { 11 | return nil, nil, nil 12 | } 13 | 14 | func Run(ctx context.Context, id []byte, port int) (<-chan struct{}, error) { 15 | return nil, nil 16 | } 17 | 18 | func Handle(dhtevent <-chan struct{}) { 19 | return 20 | } 21 | 22 | func Bootstrap(ctx context.Context, nodes []netip.AddrPort) { 23 | return 24 | } 25 | 26 | func Write(filename string, id []byte) error { 27 | return nil 28 | } 29 | 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jech/storrent 2 | 3 | go 1.22 4 | 5 | require ( 6 | bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5 7 | github.com/jech/portmap v0.0.0-20240609101148-1151a9a8a46b 8 | github.com/zeebo/bencode v1.0.0 9 | golang.org/x/net v0.28.0 10 | golang.org/x/sys v0.24.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/huin/goupnp v1.3.0 // indirect 16 | github.com/jackpal/gateway v1.0.15 // indirect 17 | github.com/jackpal/go-nat-pmp v1.0.2 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/stretchr/objx v0.5.2 // indirect 20 | github.com/stretchr/testify v1.9.0 // indirect 21 | golang.org/x/sync v0.8.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /hash/hash_test.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto/rand" 5 | "testing" 6 | ) 7 | 8 | func TestRoundtrip(t *testing.T) { 9 | for i := 0; i < 100; i++ { 10 | var h Hash = make([]byte, 20) 11 | rand.Read(h) 12 | h2 := Parse(h.String()) 13 | if !h.Equal(h2) { 14 | t.Errorf("Mismatch (%v != %v)", h, h2) 15 | } 16 | } 17 | } 18 | 19 | func TestParse(t *testing.T) { 20 | h1 := Parse("WRN7ZT6NKMA6SSXYKAFRUGDDIFJUNKI2") 21 | h2 := Parse("b45bfccfcd5301e94af8500b1a1863415346a91a") 22 | if !h1.Equal(h2) { 23 | t.Errorf("Mismatch (%v != %v)", h1.String(), h2.String()) 24 | } 25 | } 26 | 27 | func TestParseFail(t *testing.T) { 28 | if Parse("toto") != nil { 29 | t.Errorf("Parse successful 1") 30 | } 31 | if Parse("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz") != nil { 32 | t.Errorf("Parse successful 2") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mono/mono_test.go: -------------------------------------------------------------------------------- 1 | package mono 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestSub(t *testing.T) { 9 | t100 := Time(100) 10 | t200 := Time(200) 11 | 12 | if v := t100.Sub(t200); v != 0 { 13 | t.Errorf("Expected 0, got %v", v) 14 | } 15 | 16 | if v := t100.Sub(t100); v != 0 { 17 | t.Errorf("Expected 0, got %v", v) 18 | } 19 | 20 | if v := t200.Sub(t100); v != 100 { 21 | t.Errorf("Expected 100, got %v", v) 22 | } 23 | 24 | if !t100.Before(t200) { 25 | t.Errorf("100 is not before 200") 26 | } 27 | 28 | if t100.Before(t100) { 29 | t.Errorf("100 is before 100") 30 | } 31 | 32 | if t200.Before(t100) { 33 | t.Errorf("200 is before 100") 34 | } 35 | } 36 | 37 | func TestNow(t *testing.T) { 38 | t1 := Now() 39 | time.Sleep(3 * time.Second / 2) 40 | if v := Since(t1); v != 1 { 41 | t.Errorf("Expected 1, got %v", v) 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /alloc/alloc_unix.go: -------------------------------------------------------------------------------- 1 | // +build unix linux 2 | 3 | package alloc 4 | 5 | import ( 6 | "sync/atomic" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | const cutoff = 128 * 1024 12 | 13 | var allocated int64 14 | 15 | func Alloc(size int) ([]byte, error) { 16 | if size < cutoff { 17 | atomic.AddInt64(&allocated, int64(size)) 18 | return make([]byte, size), nil 19 | } 20 | p, err := unix.Mmap(-1, 0, size, 21 | unix.PROT_READ|unix.PROT_WRITE, 22 | unix.MAP_PRIVATE|unix.MAP_ANONYMOUS) 23 | if err != nil { 24 | return nil, err 25 | } 26 | atomic.AddInt64(&allocated, int64(cap(p))) 27 | return p[:size], err 28 | } 29 | 30 | func Free(p []byte) error { 31 | if len(p) < cutoff { 32 | atomic.AddInt64(&allocated, -int64(cap(p))) 33 | return nil 34 | } 35 | err := unix.Munmap(p) 36 | atomic.AddInt64(&allocated, -int64(cap(p))) 37 | return err 38 | } 39 | 40 | func Bytes() int64 { 41 | return atomic.LoadInt64(&allocated) 42 | } 43 | -------------------------------------------------------------------------------- /physmem/physmem_windows.go: -------------------------------------------------------------------------------- 1 | package physmem 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | ) 7 | 8 | var kernel32 = syscall.NewLazyDLL("kernel32.dll") 9 | var globalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx") 10 | 11 | type memoryStatusEx struct { 12 | dwLength uint32 13 | dwMemoryLoad uint32 14 | ullTotalPhys uint64 15 | ullAvailPhys uint64 16 | ullTotalPageFile uint64 17 | ullAvailPageFile uint64 18 | ullTotalVirtual uint64 19 | ullAvailVirtual uint64 20 | ullAvailExtendedVirtual uint64 21 | } 22 | 23 | // Total returns the amount of memory on the local machine in bytes. 24 | func Total() (int64, error) { 25 | var info memoryStatusEx 26 | info.dwLength = uint32(unsafe.Sizeof(info)) 27 | rc, _, err := globalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&info))) 28 | if rc == 0 { 29 | return 0, err 30 | } 31 | return int64(info.ullTotalPhys), nil 32 | } 33 | -------------------------------------------------------------------------------- /hash/hash.go: -------------------------------------------------------------------------------- 1 | // Package hash implements 20-byte hashes as used by the BitTorrent protocol. 2 | package hash 3 | 4 | import ( 5 | "encoding/base32" 6 | "encoding/hex" 7 | ) 8 | 9 | // Hash is the type of 20-byte hashes 10 | type Hash []byte 11 | 12 | // A HashPair is a pair of hashes. 13 | type HashPair struct { 14 | First Hash 15 | Second Hash 16 | } 17 | 18 | func (hash Hash) String() string { 19 | if hash == nil { 20 | return "" 21 | } 22 | return hex.EncodeToString(hash) 23 | } 24 | 25 | func (hash Hash) Equal(h Hash) bool { 26 | if len(hash) != 20 || len(h) != 20 { 27 | panic("Hash has bad length") 28 | } 29 | for i := 0; i < 20; i++ { 30 | if hash[i] != h[i] { 31 | return false 32 | } 33 | } 34 | return true 35 | } 36 | 37 | // Parse handles both hex and base-32 strings. 38 | func Parse(s string) Hash { 39 | h, err := hex.DecodeString(s) 40 | if err == nil && len(h) == 20 { 41 | return h 42 | } 43 | h, err = base32.StdEncoding.DecodeString(s) 44 | if err == nil && len(h) == 20 { 45 | return h 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pex/pex_test.go: -------------------------------------------------------------------------------- 1 | package pex 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestParseV4(t *testing.T) { 9 | a := []byte("123456abcdef") 10 | f := []byte{1, 0x12} 11 | peers := ParseCompact(a, f, false) 12 | if len(peers) != 2 { 13 | t.Errorf("bad length %v", len(peers)) 14 | } 15 | a4, f4, a6, f6 := FormatCompact(peers) 16 | if !bytes.Equal(a4, a) { 17 | t.Errorf("bad value") 18 | } 19 | if !bytes.Equal(f4, f) { 20 | t.Errorf("bad flags") 21 | } 22 | if len(a6) != 0 || len(f6) != 0 { 23 | t.Errorf("creation ex nihilo") 24 | } 25 | } 26 | 27 | func TestParseV6(t *testing.T) { 28 | a := []byte("123456789012345678abcdefghijklmnopqr") 29 | f := []byte{1, 2} 30 | peers := ParseCompact(a, f, true) 31 | if len(peers) != 2 { 32 | t.Errorf("bad length %v", len(peers)) 33 | } 34 | a4, f4, a6, f6 := FormatCompact(peers) 35 | if !bytes.Equal(a6, a) { 36 | t.Errorf("bad value") 37 | } 38 | if !bytes.Equal(f6, f) { 39 | t.Errorf("bad flags") 40 | } 41 | if len(a4) != 0 || len(f4) != 0 { 42 | t.Errorf("creation ex nihilo") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2020 by Juliusz Chroboczek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /webseed/webseed_test.go: -------------------------------------------------------------------------------- 1 | package webseed 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type crTest struct { 8 | s string 9 | o, l, fl int64 10 | } 11 | 12 | var good = []crTest{ 13 | crTest{"bytes 10-20/30", 10, 11, 30}, 14 | crTest{"bytes 10-29/30", 10, 20, 30}, 15 | crTest{"bytes 10-20/*", 10, 11, -1}, 16 | crTest{"bytes */30", -1, -1, 30}, 17 | } 18 | 19 | var bad = []string{ 20 | "octet 10-20/30", 21 | "bytes 10-20/30 foo", 22 | "bytes 10-20/", 23 | "bytes *-20/30", 24 | "bytes 10-*/30", 25 | "bytes 20-10/30", 26 | "bytes 10-20/15", 27 | "bytes 10-20/20", 28 | } 29 | 30 | func TestContentRange(t *testing.T) { 31 | for _, test := range good { 32 | o, l, fl, err := parseContentRange(test.s) 33 | if err != nil { 34 | t.Errorf("Parse %v: %v", test.s, err) 35 | } 36 | if o != test.o || l != test.l || fl != test.fl { 37 | t.Errorf("Parse %v: got %v, %v, %v expected %v, %v, %v", 38 | test.s, o, l, fl, test.o, test.l, test.fl) 39 | } 40 | } 41 | for _, test := range bad { 42 | o, l, fl, err := parseContentRange(test) 43 | if err == nil { 44 | t.Errorf("Parse %v: got %v, %v, %v expected error", 45 | test, o, l, fl) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mono/mono.go: -------------------------------------------------------------------------------- 1 | // Package mono implements monotonic time in seconds. 2 | package mono 3 | 4 | import ( 5 | "time" 6 | "sync/atomic" 7 | ) 8 | 9 | var origin time.Time 10 | 11 | func init() { 12 | origin = time.Now().Add(-time.Second) 13 | } 14 | 15 | // Time represtents a monotonic time with second granularity. 16 | type Time uint32 17 | 18 | func new(tm time.Time) Time { 19 | d := time.Since(origin) 20 | if d < time.Second || d > time.Duration(^uint32(0))*time.Second { 21 | panic("time overflow") 22 | } 23 | return Time(d / time.Second) 24 | } 25 | 26 | // Sub returns t1 - t2. 27 | func (t1 Time) Sub(t2 Time) uint32 { 28 | if t1 < t2 { 29 | return 0 30 | } 31 | return uint32(t1) - uint32(t2) 32 | } 33 | 34 | // Before returns true if t1 < t2. 35 | func (t1 Time) Before(t2 Time) bool { 36 | return t1 < t2 37 | } 38 | 39 | // Now returns the current monotonic time, as a number of seconds since an 40 | // arbitrary origin. 41 | func Now() Time { 42 | return new(time.Now()) 43 | } 44 | 45 | // Since returns the number of seconds elapsed since t. 46 | func Since(t Time) uint32 { 47 | return Now().Sub(t) 48 | } 49 | 50 | // LoadAtomic performs an atomic load of a mono.Time. 51 | func LoadAtomic(addr *Time) Time { 52 | return Time(atomic.LoadUint32((*uint32)(addr))) 53 | } 54 | 55 | // StoreAtomic performs an atomic store of a mono.Time. 56 | func StoreAtomic(addr *Time, val Time) { 57 | atomic.StoreUint32((*uint32)(addr), uint32(val)) 58 | } 59 | -------------------------------------------------------------------------------- /path/path.go: -------------------------------------------------------------------------------- 1 | // Package path implements file paths represented as lists of strings. 2 | 3 | package path 4 | 5 | import ( 6 | "strings" 7 | ) 8 | 9 | type Path []string 10 | 11 | func (p Path) String() string { 12 | return strings.Join(p, "/") 13 | } 14 | 15 | // Parse converts a Unix-style path to a path. It treats absolute and 16 | // relative paths identically. 17 | func Parse(f string) Path { 18 | path := strings.Split(f, "/") 19 | for len(path) > 0 && path[0] == "" { 20 | path = path[1:] 21 | } 22 | for len(path) > 0 && path[len(path)-1] == "" { 23 | path = path[0 : len(path)-1] 24 | } 25 | return path 26 | } 27 | 28 | func (p Path) Equal(q Path) bool { 29 | if len(p) != len(q) { 30 | return false 31 | } 32 | for i := range p { 33 | if p[i] != q[i] { 34 | return false 35 | } 36 | } 37 | return true 38 | } 39 | 40 | // Within returns true if path p is within directory d. 41 | func (p Path) Within(d Path) bool { 42 | if len(p) <= len(d) { 43 | return false 44 | } 45 | for i := range d { 46 | if p[i] != d[i] { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | 53 | // Compare implements lexicographic ordering on paths. 54 | func (a Path) Compare(b Path) int { 55 | for i := range a { 56 | if i >= len(b) { 57 | return 1 58 | } 59 | if a[i] < b[i] { 60 | return -1 61 | } 62 | if a[i] > b[i] { 63 | return +1 64 | } 65 | } 66 | 67 | if len(a) < len(b) { 68 | return -1 69 | } 70 | return 0 71 | } 72 | -------------------------------------------------------------------------------- /tor/torrents.go: -------------------------------------------------------------------------------- 1 | package tor 2 | 3 | import ( 4 | "bytes" 5 | "github.com/jech/storrent/hash" 6 | "sync" 7 | ) 8 | 9 | // torrents is the set of torrents that we are currently handling 10 | var torrents sync.Map 11 | 12 | // Get finds a torrent by hash. 13 | func Get(hash hash.Hash) *Torrent { 14 | var h [20]byte 15 | copy(h[:], hash) 16 | v, ok := torrents.Load(h) 17 | if !ok { 18 | return nil 19 | } 20 | return v.(*Torrent) 21 | } 22 | 23 | // GetByName gets a torrent by name. If behaves deterministically if 24 | // multiple torrents have the same name. 25 | func GetByName(name string) *Torrent { 26 | var torrent *Torrent 27 | Range(func(h hash.Hash, t *Torrent) bool { 28 | if t.Name == name { 29 | if torrent == nil || bytes.Compare(t.Hash, torrent.Hash) < 0 { 30 | torrent = t 31 | } 32 | } 33 | return true 34 | }) 35 | return torrent 36 | } 37 | 38 | func add(torrent *Torrent) bool { 39 | var h [20]byte 40 | copy(h[:], torrent.Hash) 41 | _, exists := torrents.LoadOrStore(h, torrent) 42 | return !exists 43 | } 44 | 45 | func del(hash hash.Hash) { 46 | var h [20]byte 47 | copy(h[:], hash) 48 | torrents.Delete(h) 49 | } 50 | 51 | func Range(f func(hash.Hash, *Torrent) bool) { 52 | torrents.Range(func(k, v interface{}) bool { 53 | a := k.([20]byte) 54 | return f(a[:], v.(*Torrent)) 55 | }) 56 | } 57 | 58 | func count() int { 59 | count := 0 60 | Range(func(h hash.Hash, t *Torrent) bool { 61 | count++ 62 | return true 63 | }) 64 | return count 65 | } 66 | 67 | func infoHashes(all bool) []hash.HashPair { 68 | var pairs []hash.HashPair 69 | Range(func(h hash.Hash, t *Torrent) bool { 70 | if all || !t.hasProxy() { 71 | pairs = append(pairs, hash.HashPair{h, t.MyId}) 72 | } 73 | return true 74 | }) 75 | return pairs 76 | } 77 | -------------------------------------------------------------------------------- /webseed/hoffman.go: -------------------------------------------------------------------------------- 1 | package webseed 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | nurl "net/url" 10 | "strconv" 11 | 12 | "github.com/jech/storrent/httpclient" 13 | ) 14 | 15 | type Hoffman struct { 16 | base 17 | } 18 | 19 | func (ws *Hoffman) Get(ctx context.Context, proxy string, hash []byte, 20 | index, offset, length uint32, w io.Writer) (int64, error) { 21 | ws.start() 22 | defer ws.stop() 23 | 24 | url, err := nurl.Parse(ws.url) 25 | if err != nil { 26 | return 0, err 27 | } 28 | 29 | v := nurl.Values{} 30 | v.Add("info_hash", string(hash)) 31 | v.Add("piece", fmt.Sprintf("%v", index)) 32 | v.Add("ranges", fmt.Sprintf("%v-%v", offset, offset+length)) 33 | url.RawQuery = v.Encode() 34 | 35 | req, err := http.NewRequest("GET", url.String(), nil) 36 | if err != nil { 37 | ws.error(true) 38 | return 0, err 39 | } 40 | req.Header["User-Agent"] = nil 41 | 42 | client := httpclient.Get("", proxy) 43 | if client == nil { 44 | return 0, errors.New("couldn't get HTTP client") 45 | } 46 | 47 | r, err := client.Do(req.WithContext(ctx)) 48 | if err != nil { 49 | ws.error(true) 50 | return 0, err 51 | } 52 | defer r.Body.Close() 53 | 54 | if r.StatusCode != http.StatusOK { 55 | ws.error(true) 56 | return 0, errors.New(r.Status) 57 | } 58 | 59 | l := int64(-1) 60 | cl := r.Header.Get("Content-Length") 61 | if cl != "" { 62 | var err error 63 | l, err = strconv.ParseInt(cl, 10, 64) 64 | if err != nil { 65 | ws.error(true) 66 | return 0, err 67 | } 68 | if l != int64(length) { 69 | ws.error(true) 70 | return 0, errors.New("length mismatch") 71 | } 72 | } 73 | 74 | reader := io.Reader(r.Body) 75 | if l < 0 { 76 | reader = io.LimitReader(r.Body, int64(length)) 77 | } 78 | 79 | n, err := io.Copy(w, reader) 80 | ws.Accumulate(int(n)) 81 | if n > 0 { 82 | ws.error(false) 83 | } 84 | return n, nil 85 | } 86 | -------------------------------------------------------------------------------- /crypto/conn.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/rc4" 5 | "io" 6 | "net" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // Conn is an encrypted connexion. It implements the net.Conn interface. 12 | type Conn struct { 13 | conn net.Conn 14 | 15 | readmu sync.Mutex 16 | dec *rc4.Cipher 17 | 18 | writemu sync.Mutex 19 | enc *rc4.Cipher 20 | err error 21 | } 22 | 23 | func (c *Conn) Read(b []byte) (n int, err error) { 24 | c.readmu.Lock() 25 | defer c.readmu.Unlock() 26 | n, err = c.conn.Read(b) 27 | c.dec.XORKeyStream(b[:n], b[:n]) 28 | return 29 | } 30 | 31 | var pool sync.Pool = sync.Pool{ 32 | New: func() interface{} { 33 | buf := make([]byte, 32*1024) 34 | return buf 35 | }, 36 | } 37 | 38 | func (c *Conn) Write(b []byte) (n int, err error) { 39 | ibuf := pool.Get() 40 | buf := ibuf.([]byte) 41 | c.writemu.Lock() 42 | defer func() { 43 | c.writemu.Unlock() 44 | pool.Put(ibuf) 45 | }() 46 | 47 | if c.err != nil { 48 | return 0, c.err 49 | } 50 | 51 | for n < len(b) { 52 | m := len(b) - n 53 | if m > len(buf) { 54 | m = len(buf) 55 | } 56 | c.enc.XORKeyStream(buf[:m], b[n:n+m]) 57 | var l int 58 | l, err = c.conn.Write(buf[:m]) 59 | n += l 60 | if err == nil && l < m { 61 | err = io.ErrShortWrite 62 | } 63 | if err != nil { 64 | c.err = err 65 | return 66 | } 67 | } 68 | return 69 | } 70 | 71 | func (c *Conn) Close() error { 72 | return c.conn.Close() 73 | } 74 | 75 | func (c *Conn) LocalAddr() net.Addr { 76 | return c.conn.LocalAddr() 77 | } 78 | 79 | func (c *Conn) RemoteAddr() net.Addr { 80 | return c.conn.RemoteAddr() 81 | } 82 | 83 | func (c *Conn) SetDeadline(t time.Time) error { 84 | return c.conn.SetDeadline(t) 85 | } 86 | 87 | func (c *Conn) SetReadDeadline(t time.Time) error { 88 | return c.conn.SetReadDeadline(t) 89 | } 90 | 91 | func (c *Conn) SetWriteDeadline(t time.Time) error { 92 | return c.conn.SetWriteDeadline(t) 93 | } 94 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Storrent 2 | ======== 3 | 4 | Storrent is a BitTorrent implementation that is optimised for streaming 5 | media: it allows you to watch a movie or to listen to music from a Torrent 6 | without downloading it entirely. 7 | 8 | Storrent works entirely in RAM: except for the DHT bootstrap database, it 9 | will never write anything to disk. It requires a fair amount of memory 10 | (1GB will do, 4GB is better). 11 | 12 | 13 | Installation 14 | ------------ 15 | 16 | git clone https://github.com/jech/storrent 17 | cd storrent 18 | CGO_ENABLED=1 go build 19 | 20 | This will yield a binary called `storrent` or `storrent.exe`. 21 | 22 | Running 23 | ------- 24 | 25 | ./storrent & 26 | 27 | Now go to 28 | 29 | http://localhost:8088 30 | 31 | and add a magnet link or a link to a torrent file. Wait a few seconds, 32 | then hit reload; you should see a link to a playlist which you may 33 | download and feed to your favourite media player. 34 | 35 | 36 | Command-line options 37 | -------------------- 38 | 39 | storrent -help 40 | 41 | By default, storrent takes 1/2 of the total RAM on your machine. You may 42 | tune this with the `-mem` command-line flag: 43 | 44 | storrent -mem $((4 * 1024 * 1024)) & 45 | 46 | By default, storrent attempts to open ports in your NAT box or firewall 47 | using NAT-PMP, or, failing that, uPNP. You may disable port mapping with 48 | the `-portmap` flag: 49 | 50 | storrent -portmap=off 51 | 52 | I usually run storrent in a slightly more paranoid mode than the default: 53 | 54 | storrent -force-encryption -dht passive 55 | 56 | For privacy reasons, storrent doesn't use trackers or webseeds by default; 57 | you may enable them on a torrent-by-torrent basis in the user interface. 58 | Should you wish to override the default, do: 59 | 60 | storrent -use-trackers -use-webseeds 61 | 62 | 63 | Author 64 | ------ 65 | 66 | Juliusz Chroboczek 67 | -------------------------------------------------------------------------------- /pex/pex.go: -------------------------------------------------------------------------------- 1 | // Package PEX implements the data structures used by BitTorrent peer exchange. 2 | package pex 3 | 4 | import ( 5 | "encoding/binary" 6 | "net/netip" 7 | "slices" 8 | ) 9 | 10 | // Peer represents a peer known or announced over PEX. 11 | type Peer struct { 12 | Addr netip.AddrPort 13 | Flags byte 14 | } 15 | 16 | // PEX flags 17 | const ( 18 | Encrypt = 0x01 19 | UploadOnly = 0x02 20 | Outgoing = 0x10 21 | ) 22 | 23 | // Find find a peer in a list of peers. 24 | func Find(p Peer, l []Peer) int { 25 | return slices.IndexFunc(l, func(q Peer) bool { 26 | return p.Addr == q.Addr 27 | }) 28 | } 29 | 30 | // ParseCompact parses a list of PEX peers in compact format. 31 | func ParseCompact(data []byte, flags []byte, ipv6 bool) []Peer { 32 | l := 4 33 | if ipv6 { 34 | l = 16 35 | } 36 | 37 | if len(data)%(l+2) != 0 { 38 | return nil 39 | } 40 | n := len(data) / (l + 2) 41 | 42 | var peers = make([]Peer, 0, n) 43 | for i := 0; i < n; i++ { 44 | j := i * (l + 2) 45 | ip, ok := netip.AddrFromSlice(data[j : j+l]) 46 | if !ok { 47 | continue 48 | } 49 | var flag byte 50 | if i < len(flags) { 51 | flag = flags[i] 52 | } 53 | port := binary.BigEndian.Uint16(data[j+l:]) 54 | addr := netip.AddrPortFrom(ip, port) 55 | peers = append(peers, Peer{Addr: addr, Flags: flag}) 56 | } 57 | return peers 58 | } 59 | 60 | // FormatCompact formats a list of PEX peers in compact format. 61 | func FormatCompact(peers []Peer) (ipv4 []byte, flags4 []byte, ipv6 []byte, flags6 []byte) { 62 | for _, peer := range peers { 63 | if peer.Addr.Addr().Is4() { 64 | v4 := peer.Addr.Addr().As4() 65 | ipv4 = append(ipv4, v4[:]...) 66 | ipv4 = binary.BigEndian.AppendUint16( 67 | ipv4, peer.Addr.Port(), 68 | ) 69 | flags4 = append(flags4, peer.Flags) 70 | } else { 71 | v6 := peer.Addr.Addr().As16() 72 | ipv6 = append(ipv6, v6[:]...) 73 | ipv6 = binary.BigEndian.AppendUint16( 74 | ipv6, peer.Addr.Port(), 75 | ) 76 | flags6 = append(flags6, peer.Flags) 77 | } 78 | } 79 | return 80 | } 81 | -------------------------------------------------------------------------------- /path/path_test.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRoundtrip(t *testing.T) { 8 | paths := []Path{ 9 | []string{}, []string{"a"}, []string{"a","b","c"}, 10 | } 11 | for _, p := range paths { 12 | f := p.String() 13 | q := Parse(f) 14 | if !p.Equal(q) { 15 | t.Errorf("Not equal: %#v -> %#v -> %#v", p, f, q) 16 | } 17 | } 18 | 19 | files := []string{ 20 | "", "a", "a/b/c", 21 | } 22 | for _, f := range files { 23 | p := Parse(f) 24 | g := p.String() 25 | if f != g { 26 | t.Errorf("Not equal: %#v -> %#v -> %#v", g, p, g) 27 | } 28 | } 29 | } 30 | 31 | func testEqual(t *testing.T, p, q Path, expected bool) { 32 | if result := p.Equal(q); result != expected { 33 | t.Errorf("Unexpected: %#v %#v -> %v (expected %v)", 34 | p, q, result, expected, 35 | ) 36 | } 37 | } 38 | 39 | func TestEqualNil(t *testing.T) { 40 | testEqual(t, Path{}, nil, true) 41 | testEqual(t, nil, Path{}, true) 42 | testEqual(t, Path{"a"}, nil, false) 43 | testEqual(t, nil, Path{"a"}, false) 44 | } 45 | 46 | func testWithin(t *testing.T, f, g string, expected bool) { 47 | if result := Parse(f).Within(Parse(g)); result != expected { 48 | t.Errorf("Unexpected: %#v %#v -> %v (expected %v)", 49 | Parse(f), Parse(g), result, expected, 50 | ) 51 | } 52 | } 53 | 54 | func TestWithin(t *testing.T) { 55 | testWithin(t, "", "", false) 56 | testWithin(t, "", "a", false) 57 | testWithin(t, "a", "a", false) 58 | testWithin(t, "a", "b", false) 59 | testWithin(t, "a", "", true) 60 | testWithin(t, "a", "a/b", false) 61 | testWithin(t, "a/b", "a", true) 62 | testWithin(t, "a", "b/a", false) 63 | testWithin(t, "b/a", "a", false) 64 | } 65 | 66 | func testCompare(t *testing.T, f, g string, expected int) { 67 | if result := Parse(f).Compare(Parse(g)); result != expected { 68 | t.Errorf("Unexpected: %#v %#v -> %v (expected %v)", 69 | Parse(f), Parse(g), result, expected, 70 | ) 71 | } 72 | } 73 | 74 | func TestCompare(t *testing.T) { 75 | testCompare(t, "", "", 0) 76 | testCompare(t, "", "a", -1) 77 | testCompare(t, "a", "", 1) 78 | testCompare(t, "a", "b", -1) 79 | testCompare(t, "b", "a", 1) 80 | testCompare(t, "a/a", "a/b", -1) 81 | testCompare(t, "a/b", "a/a", 1) 82 | testCompare(t, "a/a", "b/a", -1) 83 | testCompare(t, "b/a", "a/a", 1) 84 | } 85 | 86 | -------------------------------------------------------------------------------- /httpclient/httpclient.go: -------------------------------------------------------------------------------- 1 | // Package httpclient implements caching for net/http.Client. 2 | 3 | package httpclient 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "math/rand/v2" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | var ErrWrongNetwork = errors.New("wrong network") 17 | 18 | type key struct { 19 | proxy, network string 20 | } 21 | 22 | type client struct { 23 | client *http.Client 24 | time time.Time 25 | } 26 | 27 | var mu sync.Mutex 28 | var clients = make(map[key]client) 29 | 30 | var runExpiry sync.Once 31 | 32 | // Get returns an http.Client that goes through the given proxy. If 33 | // network is not empty, it restricts connections to the given protocol 34 | // ("tcp" or "tcp6"). 35 | func Get(network, proxy string) *http.Client { 36 | runExpiry.Do(func() { 37 | go expire() 38 | }) 39 | 40 | mu.Lock() 41 | defer mu.Unlock() 42 | cl, ok := clients[key{network: network, proxy: proxy}] 43 | if ok { 44 | cl.time = time.Now() 45 | return cl.client 46 | } 47 | dialer := net.Dialer{ 48 | Timeout: 30 * time.Second, 49 | KeepAlive: 30 * time.Second, 50 | DualStack: true, 51 | } 52 | transport := &http.Transport{ 53 | Proxy: func(req *http.Request) (*url.URL, error) { 54 | if proxy == "" { 55 | return nil, nil 56 | } 57 | return url.Parse(proxy) 58 | }, 59 | DialContext: func(ctx context.Context, n, a string) (net.Conn, error) { 60 | if n == "" || n == "tcp" { 61 | if network != "" && network != "tcp" { 62 | n = network 63 | } 64 | } else if n != network { 65 | return nil, ErrWrongNetwork 66 | } 67 | return dialer.DialContext(ctx, n, a) 68 | }, 69 | MaxIdleConns: 30, 70 | IdleConnTimeout: 90 * time.Second, 71 | TLSHandshakeTimeout: 10 * time.Second, 72 | ExpectContinueTimeout: 1 * time.Second, 73 | } 74 | cl = client{ 75 | client: &http.Client{ 76 | Transport: transport, 77 | Timeout: 50 * time.Second, 78 | }, 79 | time: time.Now(), 80 | } 81 | clients[key{network: network, proxy: proxy}] = cl 82 | return cl.client 83 | } 84 | 85 | func expire() { 86 | for { 87 | time.Sleep(time.Minute + rand.N(time.Minute)) 88 | now := time.Now() 89 | func() { 90 | mu.Lock() 91 | defer mu.Unlock() 92 | for k, cl := range clients { 93 | if now.Sub(cl.time) > 10*time.Minute { 94 | delete(clients, k) 95 | } 96 | } 97 | }() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /webseed/webseed.go: -------------------------------------------------------------------------------- 1 | package webseed 2 | 3 | import ( 4 | nurl "net/url" 5 | "sync" 6 | "time" 7 | 8 | "github.com/jech/storrent/rate" 9 | ) 10 | 11 | type Webseed interface { 12 | URL() string 13 | Ready(idle bool) bool 14 | Rate() float64 15 | Count() int 16 | } 17 | 18 | type base struct { 19 | url string 20 | 21 | mu sync.Mutex 22 | count int 23 | errors int 24 | time time.Time 25 | e rate.Estimator 26 | } 27 | 28 | func (ws *base) URL() string { 29 | return ws.url 30 | } 31 | 32 | func (ws *base) start() { 33 | ws.mu.Lock() 34 | defer ws.mu.Unlock() 35 | ws.count++ 36 | if ws.count < 1 { 37 | panic("Eek") 38 | } 39 | if ws.count == 1 { 40 | ws.e.Start() 41 | } 42 | ws.time = time.Now() 43 | } 44 | 45 | func (ws *base) stop() { 46 | ws.mu.Lock() 47 | defer ws.mu.Unlock() 48 | ws.count-- 49 | if ws.count < 0 { 50 | panic("Eek") 51 | } 52 | if ws.count == 0 { 53 | ws.e.Stop() 54 | } 55 | } 56 | 57 | func New(url string, getright bool) Webseed { 58 | u, err := nurl.Parse(url) 59 | if err != nil || (u.Scheme != "http" && u.Scheme != "https") { 60 | return nil 61 | } 62 | if getright { 63 | ws := &GetRight{base{url: url}} 64 | ws.e.Init(10 * time.Second) 65 | return ws 66 | } else { 67 | ws := &Hoffman{base{url: url}} 68 | ws.e.Init(10 * time.Second) 69 | return ws 70 | } 71 | } 72 | 73 | func (ws *base) error(e bool) { 74 | ws.mu.Lock() 75 | if e { 76 | ws.errors++ 77 | } else { 78 | ws.errors = 0 79 | } 80 | ws.mu.Unlock() 81 | } 82 | 83 | func (ws *base) Accumulate(value int) { 84 | ws.mu.Lock() 85 | ws.e.Accumulate(value) 86 | ws.mu.Unlock() 87 | } 88 | 89 | func (ws *base) Rate() float64 { 90 | ws.mu.Lock() 91 | v := ws.e.Estimate() 92 | ws.mu.Unlock() 93 | return v 94 | } 95 | 96 | func (ws *base) Ready(idle bool) bool { 97 | ws.mu.Lock() 98 | defer ws.mu.Unlock() 99 | if ws.errors >= 2 { 100 | t := 10 * time.Second * time.Duration(1<= max { 115 | return false 116 | } 117 | return true 118 | } 119 | 120 | func (ws *base) Count() int { 121 | ws.mu.Lock() 122 | v := ws.count 123 | ws.mu.Unlock() 124 | return v 125 | } 126 | -------------------------------------------------------------------------------- /alloc/alloc_test.go: -------------------------------------------------------------------------------- 1 | package alloc 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "errors" 7 | ) 8 | 9 | const totalSize = 256 * 1024 * 1024 10 | 11 | func doalloc(size int, a [][]byte) error { 12 | for j := 0; j < len(a); j++ { 13 | p, err := Alloc(size) 14 | if err != nil { 15 | return err 16 | } 17 | for k := range p { 18 | p[k] = byte(j+k) 19 | } 20 | a[j] = p 21 | } 22 | return nil 23 | } 24 | 25 | func dofree(a [][]byte) error { 26 | for j := 0; j < len(a); j++ { 27 | for k := range a[j] { 28 | if a[j][k] != byte(j+k) { 29 | return errors.New("memory corruption") 30 | } 31 | } 32 | err := Free(a[j]) 33 | if err != nil { 34 | return err 35 | } 36 | a[j] = nil 37 | } 38 | return nil 39 | } 40 | 41 | var sizes = []int{ 42 | 32 * 1024, 64 * 1024, 128 * 1024, 256 * 1024, 512 * 1024, 43 | 1024 * 1024, 2048 * 1024, 4096 * 1024, 8192 * 1024, 16384 * 1024} 44 | 45 | func TestAlloc(t *testing.T) { 46 | for _, size := range sizes { 47 | t.Run(fmt.Sprintf("%v", size), func(t *testing.T) { 48 | a := make([][]byte, totalSize/size) 49 | err := doalloc(size, a) 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | err = dofree(a) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | if Bytes() != 0 { 58 | t.Error("Memory leak") 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func BenchmarkAllocFree(b *testing.B) { 65 | for _, size := range sizes { 66 | b.Run(fmt.Sprintf("%v", size), func(b *testing.B) { 67 | b.SetBytes(totalSize) 68 | a := make([][]byte, totalSize/size) 69 | for i := 0; i < b.N; i++ { 70 | err := doalloc(size, a) 71 | if err != nil { 72 | b.Error(err) 73 | } 74 | err = dofree(a) 75 | if err != nil { 76 | b.Error(err) 77 | } 78 | } 79 | if Bytes() != 0 { 80 | b.Error("Memory leak") 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func BenchmarkAllocParallel(b *testing.B) { 87 | for _, size := range sizes { 88 | b.Run(fmt.Sprintf("%v", size), func(b *testing.B) { 89 | b.SetBytes(totalSize) 90 | b.RunParallel(func (pb *testing.PB) { 91 | a := make([][]byte, totalSize/size) 92 | for pb.Next() { 93 | err := doalloc(size, a) 94 | if err != nil { 95 | b.Error(err) 96 | } 97 | err = dofree(a) 98 | if err != nil { 99 | b.Error(err) 100 | } 101 | } 102 | }) 103 | if Bytes() != 0 { 104 | b.Error("Memory leak") 105 | } 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /dht/dht.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2009-2011 by Juliusz Chroboczek 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | #ifdef __cplusplus 24 | extern "C" { 25 | #endif 26 | 27 | typedef void 28 | dht_callback_t(void *closure, int event, 29 | const unsigned char *info_hash, 30 | const void *data, size_t data_len); 31 | 32 | #define DHT_EVENT_NONE 0 33 | #define DHT_EVENT_VALUES 1 34 | #define DHT_EVENT_VALUES6 2 35 | #define DHT_EVENT_SEARCH_DONE 3 36 | #define DHT_EVENT_SEARCH_DONE6 4 37 | 38 | extern FILE *dht_debug; 39 | 40 | int dht_init(int s, int s6, const unsigned char *id, const unsigned char *v); 41 | int dht_insert_node(const unsigned char *id, struct sockaddr *sa, int salen); 42 | int dht_ping_node(const struct sockaddr *sa, int salen); 43 | int dht_periodic(const void *buf, size_t buflen, 44 | const struct sockaddr *from, int fromlen, time_t *tosleep, 45 | dht_callback_t *callback, void *closure); 46 | int dht_search(const unsigned char *id, int port, int af, 47 | dht_callback_t *callback, void *closure); 48 | int dht_nodes(int af, 49 | int *good_return, int *dubious_return, int *cached_return, 50 | int *incoming_return); 51 | void dht_dump_tables(FILE *f); 52 | int dht_get_nodes(struct sockaddr_in *sin, int *num, 53 | struct sockaddr_in6 *sin6, int *num6); 54 | int dht_uninit(void); 55 | 56 | /* This must be provided by the user. */ 57 | int dht_sendto(int sockfd, const void *buf, int len, int flags, 58 | const struct sockaddr *to, int tolen); 59 | int dht_blacklisted(const struct sockaddr *sa, int salen); 60 | void dht_hash(void *hash_return, int hash_size, 61 | const void *v1, int len1, 62 | const void *v2, int len2, 63 | const void *v3, int len3); 64 | int dht_random_bytes(void *buf, size_t size); 65 | 66 | #ifdef __cplusplus 67 | } 68 | #endif 69 | -------------------------------------------------------------------------------- /dht/dht_export.go: -------------------------------------------------------------------------------- 1 | //go:build cgo 2 | // +build cgo 3 | 4 | package dht 5 | 6 | import ( 7 | crand "crypto/rand" 8 | "crypto/sha1" 9 | "encoding/binary" 10 | "errors" 11 | "net" 12 | "net/netip" 13 | "os" 14 | "syscall" 15 | "time" 16 | "unsafe" 17 | ) 18 | 19 | /* 20 | extern void dht_set_errno(int); 21 | */ 22 | import "C" 23 | 24 | //export dht_callback 25 | func dht_callback( 26 | closure unsafe.Pointer, 27 | event C.int, 28 | infoHash *C.uchar, 29 | data unsafe.Pointer, 30 | dataLen C.size_t) { 31 | hash := C.GoBytes(unsafe.Pointer(infoHash), 20) 32 | switch event { 33 | case 1: // DHT_EVENT_VALUES 34 | if globalEvents == nil || dataLen%6 != 0 { 35 | return 36 | } 37 | data := unsafe.Slice((*byte)(data), dataLen) 38 | for i := C.size_t(0); i < dataLen/6; i++ { 39 | ip, ok := netip.AddrFromSlice(data[6*i : 6*i+4]) 40 | if ok { 41 | port := binary.BigEndian.Uint16(data[6*i+4:]) 42 | globalEvents <- ValueEvent{hash, 43 | netip.AddrPortFrom(ip, port), 44 | } 45 | } 46 | } 47 | case 2: // DHT_EVENT_VALUES6 48 | if globalEvents == nil || dataLen%18 != 0 { 49 | return 50 | } 51 | data := unsafe.Slice((*byte)(data), dataLen) 52 | for i := C.size_t(0); i < dataLen/18; i++ { 53 | ip, ok := netip.AddrFromSlice(data[18*i : 18*i+16]) 54 | if ok { 55 | port := binary.BigEndian.Uint16(data[18*i+16:]) 56 | globalEvents <- ValueEvent{hash, 57 | netip.AddrPortFrom(ip, port), 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | //export dht_hash 65 | func dht_hash(hash_return unsafe.Pointer, hash_size C.int, 66 | v1 unsafe.Pointer, len1 C.int, 67 | v2 unsafe.Pointer, len2 C.int, 68 | v3 unsafe.Pointer, len3 C.int) { 69 | h := sha1.New() 70 | if len1 > 0 { 71 | h.Write(unsafe.Slice((*byte)(v1), len1)) 72 | } 73 | if len2 > 0 { 74 | h.Write(unsafe.Slice((*byte)(v2), len2)) 75 | } 76 | if len3 > 0 { 77 | h.Write(unsafe.Slice((*byte)(v3), len3)) 78 | } 79 | sum := h.Sum(nil) 80 | r := unsafe.Slice((*byte)(hash_return), hash_size) 81 | copy(r, sum[0:hash_size]) 82 | } 83 | 84 | //export dht_random_bytes 85 | func dht_random_bytes(buf unsafe.Pointer, size C.size_t) C.int { 86 | n, _ := crand.Read(unsafe.Slice((*byte)(buf), size)) 87 | return C.int(n) 88 | } 89 | 90 | //export dht_send_callback 91 | func dht_send_callback(buf unsafe.Pointer, size C.size_t, 92 | ip unsafe.Pointer, iplen C.size_t, port C.uint) C.int { 93 | data := C.GoBytes(buf, C.int(size)) 94 | var addr net.UDPAddr 95 | var conn *net.UDPConn 96 | addr.IP = C.GoBytes(ip, C.int(iplen)) 97 | addr.Port = int(port) 98 | conn = getConn(addr.IP.To4() == nil) 99 | if conn == nil { 100 | C.dht_set_errno(C.int(syscall.EAFNOSUPPORT)) 101 | return -1 102 | } 103 | err := conn.SetWriteDeadline(time.Now().Add(time.Second)) 104 | var n int 105 | if err == nil { 106 | n, err = conn.WriteToUDP(data, &addr) 107 | } 108 | if err != nil { 109 | var e syscall.Errno 110 | if !errors.As(err, &e) { 111 | if os.IsTimeout(err) { 112 | e = syscall.EAGAIN 113 | } else { 114 | e = syscall.EIO 115 | } 116 | } 117 | C.dht_set_errno(C.int(e)) 118 | return -1 119 | } 120 | return C.int(n) 121 | } 122 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5 h1:A0NsYy4lDBZAC6QiYeJ4N+XuHIKBpyhAVRMHRQZKTeQ= 2 | bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5/go.mod h1:gG3RZAMXCa/OTes6rr9EwusmR1OH1tDDy+cg9c5YliY= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= 6 | github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= 7 | github.com/jackpal/gateway v1.0.10 h1:7g3fDo4Cd3RnTu6PzAfw6poO4Y81uNxrxFQFsBFSzJM= 8 | github.com/jackpal/gateway v1.0.10/go.mod h1:+uPBgIllrbkwYCAoDkGSZbjvpre/bGYAFCYIcrH+LHs= 9 | github.com/jackpal/gateway v1.0.15 h1:yb4Gltgr8ApHWWnSyybnDL1vURbqw7ooo7IIL5VZSeg= 10 | github.com/jackpal/gateway v1.0.15/go.mod h1:dbyEDcDhHUh9EmjB9ung81elMUZfG0SoNc2TfTbcj4c= 11 | github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 12 | github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 13 | github.com/jech/portmap v0.0.0-20240609101148-1151a9a8a46b h1:pta4k005q+3DtYDNlV7ZBHCUINJ2D2PAc6QkC1m8ROw= 14 | github.com/jech/portmap v0.0.0-20240609101148-1151a9a8a46b/go.mod h1:kzzVzieUB4zTD0yhjChmgBNX4EMEbINScYTu0hZvXsA= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 18 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 19 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 20 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= 22 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= 23 | github.com/zeebo/bencode v1.0.0 h1:zgop0Wu1nu4IexAZeCZ5qbsjU4O1vMrfCrVgUjbHVuA= 24 | github.com/zeebo/bencode v1.0.0/go.mod h1:Ct7CkrWIQuLWAy9M3atFHYq4kG9Ao/SsY5cdtCXmp9Y= 25 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 26 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 27 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 28 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 29 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 30 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 31 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 32 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 33 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /rate/rate_test.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestEstimator(t *testing.T) { 9 | e := &Estimator{} 10 | e.Init(10 * time.Second) 11 | e.Start() 12 | 13 | t1 := e.time.Add(2 * time.Second) 14 | t2 := e.time.Add(5 * time.Second) 15 | 16 | e.accumulate(10, t1) 17 | 18 | ok := func(v float64) bool { 19 | return v > 0.5 && v <= 1.0 20 | } 21 | 22 | r1 := e.rate(e.value) 23 | if !ok(r1) { 24 | t.Errorf("Got %v", r1) 25 | } 26 | 27 | e.advance(t2) 28 | r2 := e.rate(e.value) 29 | if !ok(r2) { 30 | t.Errorf("Got %v", r2) 31 | } 32 | if r1 <= r2 { 33 | t.Errorf("Got %v then %v", r1, r2) 34 | } 35 | } 36 | 37 | func BenchmarkNow(b *testing.B) { 38 | b.RunParallel(func(pb *testing.PB) { 39 | for pb.Next() { 40 | time.Now() 41 | } 42 | }) 43 | } 44 | 45 | func BenchmarkAdvance(b *testing.B) { 46 | e := &Estimator{} 47 | e.Init(10 * time.Second) 48 | e.Start() 49 | now := time.Now() 50 | b.ResetTimer() 51 | for i := 0; i < b.N; i++ { 52 | now = now.Add(time.Microsecond) 53 | e.advance(now) 54 | } 55 | } 56 | 57 | func BenchmarkAdvanceNow(b *testing.B) { 58 | e := &Estimator{} 59 | e.Init(10 * time.Second) 60 | e.Start() 61 | b.ResetTimer() 62 | for i := 0; i < b.N; i++ { 63 | e.advance(time.Now()) 64 | } 65 | } 66 | 67 | func BenchmarkAccumulate(b *testing.B) { 68 | e := &Estimator{} 69 | e.Init(10 * time.Second) 70 | e.Start() 71 | b.ResetTimer() 72 | for i := 0; i < b.N; i++ { 73 | e.Accumulate(42) 74 | } 75 | } 76 | 77 | func BenchmarkAccumulateAtomic(b *testing.B) { 78 | e := &AtomicEstimator{} 79 | e.Init(10 * time.Second) 80 | e.Start() 81 | b.ResetTimer() 82 | b.RunParallel(func(pb *testing.PB) { 83 | for pb.Next() { 84 | e.Accumulate(42) 85 | } 86 | }) 87 | } 88 | 89 | func BenchmarkAllow(b *testing.B) { 90 | e := &Estimator{} 91 | e.Init(10 * time.Second) 92 | e.Start() 93 | e.Accumulate(10000) 94 | b.ResetTimer() 95 | for i := 0; i < b.N; i++ { 96 | if e.Allow(10000, 100.0) { 97 | b.Errorf("Eek, value=%v", e.value) 98 | } 99 | } 100 | } 101 | 102 | func BenchmarkAllowAtomic(b *testing.B) { 103 | e := &AtomicEstimator{} 104 | e.Init(10 * time.Second) 105 | e.Start() 106 | e.Accumulate(10000) 107 | b.ResetTimer() 108 | b.RunParallel(func(pb *testing.PB) { 109 | for pb.Next() { 110 | if e.Allow(10000, 100.0) { 111 | b.Errorf("Eek, value=%v", e.e.value) 112 | } 113 | } 114 | }) 115 | } 116 | 117 | func BenchmarkAllowAccumulate(b *testing.B) { 118 | e := &Estimator{} 119 | e.Init(10 * time.Second) 120 | e.Start() 121 | e.Accumulate(10000) 122 | b.ResetTimer() 123 | for i := 0; i < b.N; i++ { 124 | if e.Allow(100, 10000.0) { 125 | e.Accumulate(100) 126 | } else { 127 | e.Accumulate(50) 128 | } 129 | } 130 | } 131 | 132 | func BenchmarkAllowAccumulateAtomic(b *testing.B) { 133 | e := &AtomicEstimator{} 134 | e.Init(10 * time.Second) 135 | e.Start() 136 | e.Accumulate(10000) 137 | b.ResetTimer() 138 | b.RunParallel(func(pb *testing.PB) { 139 | for pb.Next() { 140 | if e.Allow(100, 10000.0) { 141 | e.Accumulate(100) 142 | } else { 143 | e.Accumulate(50) 144 | } 145 | } 146 | }) 147 | } 148 | 149 | -------------------------------------------------------------------------------- /tor/requests.go: -------------------------------------------------------------------------------- 1 | package tor 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // IdlePriority is returned for pieces that have been requested for 9 | // reasons other than a client requiring them. 10 | const IdlePriority = int8(math.MinInt8) 11 | 12 | type RequestedPiece struct { 13 | prio []int8 14 | done chan struct{} 15 | } 16 | 17 | // Requested is a set of pieces that are requested for a torrent. 18 | type Requested struct { 19 | pieces map[uint32]*RequestedPiece 20 | time time.Time 21 | } 22 | 23 | // Add adds a piece to a set of requested pieces. It returns a channel 24 | // that will be closed when the piece is complete, as well as a boolean 25 | // that indicates if the priority of the piece has been increased. 26 | func (rs *Requested) Add(index uint32, prio int8, want bool) (<-chan struct{}, bool) { 27 | added := false 28 | r := rs.pieces[index] 29 | if r == nil { 30 | r = &RequestedPiece{} 31 | rs.pieces[index] = r 32 | added = true 33 | } 34 | if prio > IdlePriority { 35 | r.prio = append(r.prio, prio) 36 | added = true 37 | } 38 | 39 | if want && r.done == nil { 40 | r.done = make(chan struct{}) 41 | } 42 | return r.done, added 43 | } 44 | 45 | func (rs *Requested) del(index uint32) { 46 | if rs.pieces[index].done != nil { 47 | close(rs.pieces[index].done) 48 | rs.pieces[index].done = nil 49 | } 50 | delete(rs.pieces, index) 51 | } 52 | 53 | // Del deletes a request for a piece. It returns true if the piece has 54 | // been cancelled (no other clients are requesting this piece). 55 | func (rs *Requested) Del(index uint32, prio int8) bool { 56 | r := rs.pieces[index] 57 | if r == nil { 58 | return false 59 | } 60 | for i, p := range r.prio { 61 | if p == prio { 62 | r.prio = append(r.prio[:i], r.prio[i+1:]...) 63 | if len(r.prio) == 0 { 64 | rs.del(index) 65 | return true 66 | } 67 | return false 68 | } 69 | } 70 | return false 71 | } 72 | 73 | // Count counts the number of request pieces that satisfy a given predicate. 74 | func (rs *Requested) Count(f func(uint32) bool) int { 75 | count := 0 76 | for i := range rs.pieces { 77 | if f(i) { 78 | count++ 79 | } 80 | } 81 | return count 82 | } 83 | 84 | func hasPriority(r *RequestedPiece, prio int8) bool { 85 | if prio == IdlePriority { 86 | return true 87 | } 88 | for _, p := range r.prio { 89 | if p == prio { 90 | return true 91 | } 92 | } 93 | return false 94 | } 95 | 96 | // Done is called to indicate that a piece has finished downloading. 97 | func (rs *Requested) Done(index uint32) { 98 | r := rs.pieces[index] 99 | if r == nil { 100 | return 101 | } 102 | 103 | if r.done != nil { 104 | close(r.done) 105 | r.done = nil 106 | } 107 | 108 | rs.DelIdlePiece(index) 109 | } 110 | 111 | // DelIdle cancels all pieces that are not currently requested by any client. 112 | func (rs *Requested) DelIdle() { 113 | for index := range rs.pieces { 114 | rs.DelIdlePiece(index) 115 | } 116 | } 117 | 118 | // DelIdlePiece deletes a given piece only if it is not currently 119 | // requested by a client. 120 | func (rs *Requested) DelIdlePiece(index uint32) { 121 | r := rs.pieces[index] 122 | if r == nil { 123 | return 124 | } 125 | if len(r.prio) == 0 { 126 | rs.del(index) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tor/writer.go: -------------------------------------------------------------------------------- 1 | package tor 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net" 7 | 8 | "github.com/jech/storrent/peer" 9 | ) 10 | 11 | // A writer writes data to a torrent. It is limited to writing within 12 | // a single piece. It buffers internally any data that is not 13 | // chunk-aligned. 14 | type writer struct { 15 | t *Torrent 16 | index uint32 17 | offset uint32 18 | count uint32 19 | buf []byte 20 | } 21 | 22 | // NewWriter creates a writer. The chunks covered by length have already 23 | // been marked as in-flight by the caller. 24 | func NewWriter(t *Torrent, index, offset, length uint32) *writer { 25 | return &writer{t, index, offset, length, nil} 26 | } 27 | 28 | func (w *writer) writeEvent(e peer.TorEvent) { 29 | select { 30 | case w.t.Event <- e: 31 | case <-w.t.Done: 32 | } 33 | } 34 | 35 | func (w *writer) write(data []byte) (int, error) { 36 | count, complete, err := 37 | w.t.Pieces.AddData(w.index, w.offset, data, ^uint32(0)) 38 | if count > 0 { 39 | w.writeEvent(peer.TorData{nil, 40 | w.index, w.offset, count, complete, 41 | }) 42 | w.count -= count 43 | w.offset += count 44 | } 45 | return int(count), err 46 | } 47 | 48 | var ErrShortWrite = errors.New("short write") 49 | 50 | // Write writes data to a torrent. If the data is not aligned to chunk 51 | // boundaries, it is buffered within the writer. Any completed chunks are 52 | // marked as no longer being in-flight. 53 | func (w *writer) Write(p []byte) (int, error) { 54 | if w.t == nil { 55 | return 0, net.ErrClosed 56 | } 57 | max := int(w.count) - len(w.buf) 58 | if max < 0 { 59 | return 0, ErrShortWrite 60 | } 61 | 62 | q := p 63 | if len(q) > max { 64 | q = q[:max] 65 | } 66 | 67 | var data []byte 68 | if len(w.buf) == 0 { 69 | data = q 70 | } else { 71 | w.buf = append(w.buf, q...) 72 | data = w.buf 73 | } 74 | 75 | n, err := w.write(data) 76 | data = data[n:] 77 | if cap(w.buf) < len(data) { 78 | w.buf = make([]byte, len(data)) 79 | } else { 80 | w.buf = w.buf[:len(data)] 81 | } 82 | copy(w.buf, data) 83 | 84 | peer.DownloadEstimator.Accumulate(len(q)) 85 | 86 | if err == nil && len(q) < len(p) { 87 | err = ErrShortWrite 88 | } 89 | return len(q), err 90 | } 91 | 92 | // ReadFrom copies data from a reader into a torrent. 93 | func (w *writer) ReadFrom(r io.Reader) (int64, error) { 94 | if w.t == nil { 95 | return 0, net.ErrClosed 96 | } 97 | if w.count < uint32(len(w.buf)) { 98 | return 0, io.EOF 99 | } 100 | 101 | if cap(w.buf) < 32768 { 102 | l := len(w.buf) 103 | w.buf = append(w.buf, make([]byte, 32768-l)...) 104 | w.buf = w.buf[:l] 105 | } 106 | 107 | var count int64 108 | var err error 109 | for { 110 | max := 32768 111 | if int(w.count) < max { 112 | max = int(w.count) 113 | } 114 | n, er := r.Read(w.buf[len(w.buf):max]) 115 | if n == 0 { 116 | err = er 117 | break 118 | } 119 | w.buf = w.buf[:len(w.buf)+n] 120 | peer.DownloadEstimator.Accumulate(n) 121 | m, ew := w.write(w.buf) 122 | copy(w.buf, w.buf[m:]) 123 | w.buf = w.buf[:len(w.buf)-m] 124 | count += int64(n) 125 | if er != nil { 126 | if er != io.EOF { 127 | err = er 128 | } 129 | break 130 | } 131 | if ew != nil { 132 | err = ew 133 | break 134 | } 135 | } 136 | return count, err 137 | } 138 | 139 | // Close closes a writer. Any incomplete chunks buffered within the 140 | // writer are lost. Any data remaining is marked as no longer being in 141 | // flight. 142 | func (w *writer) Close() error { 143 | if w.t == nil { 144 | return net.ErrClosed 145 | } 146 | if w.count > 0 { 147 | w.writeEvent(peer.TorDrop{w.index, w.offset, w.count}) 148 | w.count = 0 149 | } 150 | w.t = nil 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /rate/rate.go: -------------------------------------------------------------------------------- 1 | // Package rate implements a rate estimator. 2 | package rate 3 | 4 | import ( 5 | "math" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // Estimator is a rate estimator using exponential decay. It is not 11 | // thread-safe. 12 | type Estimator struct { 13 | interval time.Duration 14 | seconds float64 15 | value float64 16 | base float64 17 | time time.Time 18 | running bool 19 | } 20 | 21 | // Init initialises a rate estimator with the given time constant. 22 | func (e *Estimator) Init(interval time.Duration) { 23 | e.interval = interval 24 | e.time = time.Now() 25 | e.seconds = float64(interval) / float64(time.Second) 26 | e.base = -1.0 / e.seconds 27 | } 28 | 29 | // Start starts a rate estimator. 30 | func (e *Estimator) Start() { 31 | if !e.running { 32 | e.time = time.Now() 33 | e.running = true 34 | } 35 | } 36 | 37 | // Stop stops a rate estimator. 38 | func (e *Estimator) Stop() { 39 | e.running = false 40 | } 41 | 42 | // Time returns the time at which the estimator was advanced. 43 | func (e *Estimator) Time() time.Time { 44 | return e.time 45 | } 46 | 47 | func (e *Estimator) advance(now time.Time) { 48 | if !e.running { 49 | panic("Cannot advance stopped rate estimator") 50 | } 51 | delay := now.Sub(e.time) 52 | e.time = now 53 | if delay <= time.Duration(0) { 54 | return 55 | } 56 | seconds := float64(delay) * (1 / float64(time.Second)) 57 | e.value = e.value * math.Exp(e.base*seconds) 58 | } 59 | 60 | func (e *Estimator) accumulate(value int, now time.Time) { 61 | if !e.running { 62 | return 63 | } 64 | e.value += float64(value) 65 | if e.value < 0 { 66 | e.value = 0 67 | } 68 | } 69 | 70 | func (e *Estimator) rate(value float64) float64 { 71 | return float64(value) / e.seconds 72 | } 73 | 74 | // Estimate returns an estimate of the current rate. 75 | func (e *Estimator) Estimate() float64 { 76 | if e.running { 77 | e.advance(time.Now()) 78 | } 79 | return e.rate(e.value) 80 | } 81 | 82 | // Accumulate notifies the estimator that the given number of bytes has 83 | // been sent or received. 84 | func (e *Estimator) Accumulate(value int) { 85 | if e.running { 86 | now := time.Now() 87 | e.advance(now) 88 | e.accumulate(value, now) 89 | } 90 | } 91 | 92 | // Allow returns true if sending or receiving the given number of bytes 93 | // would not exceed the given target. 94 | func (e *Estimator) Allow(value int, target float64) bool { 95 | if (e.value+float64(value)) <= target*e.seconds { 96 | return true 97 | } 98 | if e.running { 99 | e.advance(time.Now()) 100 | if (e.value + float64(value)) <= target*e.seconds { 101 | return true 102 | } 103 | } 104 | return false 105 | } 106 | 107 | // AtomicEstimator is a thread-save rate estimator. 108 | type AtomicEstimator struct { 109 | sync.Mutex 110 | e Estimator 111 | } 112 | 113 | func (e *AtomicEstimator) Init(interval time.Duration) { 114 | e.Lock() 115 | e.e.Init(interval) 116 | e.Unlock() 117 | } 118 | 119 | func (e *AtomicEstimator) Start() { 120 | e.Lock() 121 | e.e.Start() 122 | e.Unlock() 123 | } 124 | 125 | func (e *AtomicEstimator) Stop() { 126 | e.Lock() 127 | e.e.Stop() 128 | e.Unlock() 129 | } 130 | 131 | func (e *AtomicEstimator) Time() time.Time { 132 | e.Lock() 133 | v := e.e.Time() 134 | e.Unlock() 135 | return v 136 | } 137 | 138 | func (e *AtomicEstimator) Estimate() float64 { 139 | e.Lock() 140 | v := e.e.Estimate() 141 | e.Unlock() 142 | return v 143 | } 144 | 145 | func (e *AtomicEstimator) Accumulate(value int) { 146 | e.Lock() 147 | e.e.Accumulate(value) 148 | e.Unlock() 149 | } 150 | 151 | func (e *AtomicEstimator) Allow(value int, target float64) bool { 152 | e.Lock() 153 | v := e.e.Allow(value, target) 154 | e.Unlock() 155 | return v 156 | } 157 | -------------------------------------------------------------------------------- /known/known.go: -------------------------------------------------------------------------------- 1 | package known 2 | 3 | import ( 4 | "net/netip" 5 | "slices" 6 | "time" 7 | 8 | "github.com/jech/storrent/hash" 9 | ) 10 | 11 | type Peer struct { 12 | Addr netip.AddrPort 13 | Id hash.Hash 14 | TrackerTime time.Time 15 | DHTTime time.Time 16 | PEXTime time.Time 17 | HeardTime time.Time 18 | SeenTime time.Time 19 | ActiveTime time.Time 20 | ConnectAttemptTime time.Time 21 | BadTime time.Time 22 | Attempts uint 23 | Badness int 24 | Version string 25 | } 26 | 27 | type Peers map[netip.AddrPort]*Peer 28 | 29 | type Kind int 30 | 31 | const ( 32 | None Kind = iota 33 | Tracker 34 | DHT 35 | PEX 36 | Heard 37 | Seen 38 | Active 39 | ActiveNoReset 40 | ConnectAttempt 41 | Good 42 | Bad 43 | ) 44 | 45 | func (kp *Peer) Update(version string, kind Kind) { 46 | switch kind { 47 | case None: 48 | 49 | case Tracker: 50 | kp.TrackerTime = time.Now() 51 | case DHT: 52 | kp.DHTTime = time.Now() 53 | case PEX: 54 | kp.PEXTime = time.Now() 55 | case Heard: 56 | kp.HeardTime = time.Now() 57 | case Seen: 58 | kp.SeenTime = time.Now() 59 | case Active: 60 | kp.ActiveTime = time.Now() 61 | kp.Attempts = 0 62 | case ActiveNoReset: 63 | kp.ActiveTime = time.Now() 64 | case ConnectAttempt: 65 | kp.ConnectAttemptTime = time.Now() 66 | kp.Attempts++ 67 | case Good: 68 | if kp.Badness > 0 { 69 | kp.Badness-- 70 | } 71 | case Bad: 72 | kp.BadTime = time.Now() 73 | kp.Badness += 5 74 | default: 75 | panic("Unknown known type") 76 | } 77 | if version != "" { 78 | kp.Version = version 79 | } 80 | } 81 | 82 | func (kp *Peer) Recent() bool { 83 | if time.Since(kp.ActiveTime) < time.Hour { 84 | return true 85 | } 86 | if time.Since(kp.SeenTime) < time.Hour { 87 | return true 88 | } 89 | if time.Since(kp.DHTTime) < 35*time.Minute { 90 | return true 91 | } 92 | if time.Since(kp.TrackerTime) < 35*time.Minute { 93 | return true 94 | } 95 | if time.Since(kp.HeardTime) < time.Hour { 96 | return true 97 | } 98 | if time.Since(kp.PEXTime) < time.Hour { 99 | return true 100 | } 101 | return false 102 | } 103 | 104 | func (kp *Peer) Bad() bool { 105 | return kp.Badness > 20 106 | } 107 | 108 | func (kp *Peer) ReallyBad() bool { 109 | return kp.Badness > 50 110 | } 111 | 112 | func (kp *Peer) age() time.Duration { 113 | when := slices.MaxFunc([]time.Time{ 114 | kp.ActiveTime, kp.SeenTime, kp.HeardTime, kp.ActiveTime, 115 | kp.TrackerTime, kp.DHTTime, kp.PEXTime, 116 | }, func(a, b time.Time) int { return a.Compare(b) }) 117 | return time.Since(when) 118 | } 119 | 120 | func (ps Peers) Count() int { 121 | return len(ps) 122 | } 123 | 124 | func (ps Peers) Expire() { 125 | for k, p := range ps { 126 | if p.age() > time.Hour { 127 | delete(ps, k) 128 | continue 129 | } 130 | if p.Badness > 0 && time.Since(p.BadTime) > 15*time.Minute { 131 | p.Badness = 0 132 | } 133 | } 134 | } 135 | 136 | func Find(peers Peers, addr netip.AddrPort, id hash.Hash, version string, kind Kind) *Peer { 137 | 138 | kp := peers[addr] 139 | 140 | if kp != nil { 141 | if id != nil && kp.Id != nil && !id.Equal(kp.Id) { 142 | kp.Id = nil 143 | } 144 | if kp.Id == nil { 145 | kp.Id = id 146 | } 147 | kp.Update(version, kind) 148 | return kp 149 | } 150 | 151 | if kind == None { 152 | return nil 153 | } 154 | 155 | if !addr.Addr().IsGlobalUnicast() { 156 | return nil 157 | } 158 | kp = &Peer{Addr: addr, Id: id} 159 | kp.Update(version, kind) 160 | peers[addr] = kp 161 | return kp 162 | } 163 | 164 | func FindId(peers Peers, id hash.Hash, addr netip.AddrPort) *Peer { 165 | for _, kp := range peers { 166 | if (kp.Addr == addr || 167 | (addr.Port() == 0 && kp.Addr.Addr() == addr.Addr())) && 168 | (kp.Id != nil && id.Equal(kp.Id)) { 169 | return kp 170 | } 171 | } 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /protocol/requests.go: -------------------------------------------------------------------------------- 1 | // Package protocol implements low-level details of the BitTorrent protocol. 2 | package protocol 3 | 4 | import ( 5 | "net/netip" 6 | 7 | "github.com/zeebo/bencode" 8 | 9 | "github.com/jech/storrent/pex" 10 | ) 11 | 12 | // The subtypes we negotiate for reception of extended messages. We send 13 | // the subtypes requested by the peer, as required by the extension mechanism. 14 | const ( 15 | ExtPex uint8 = 1 16 | ExtMetadata uint8 = 2 17 | ExtDontHave uint8 = 3 18 | ExtUploadOnly uint8 = 4 19 | ) 20 | 21 | // bootOrString is a boolean, but it can unmarshal a string. This works 22 | // around some buggy peers. 23 | type boolOrString bool 24 | 25 | func (bs boolOrString) MarshalBencode() ([]byte, error) { 26 | return bencode.EncodeBytes(bool(bs)) 27 | } 28 | 29 | func (bs *boolOrString) UnmarshalBencode(buf []byte) error { 30 | var b bool 31 | err1 := bencode.DecodeBytes(buf, &b) 32 | if err1 == nil { 33 | *bs = boolOrString(b) 34 | return nil 35 | } 36 | var s string 37 | err2 := bencode.DecodeBytes(buf, &s) 38 | if err2 == nil { 39 | switch s { 40 | case "0": 41 | *bs = false 42 | return nil 43 | case "1": 44 | *bs = true 45 | return nil 46 | } 47 | } 48 | return err1 49 | } 50 | 51 | type extensionInfo struct { 52 | Version string `bencode:"v,omitempty"` 53 | IPv4 []byte `bencode:"ipv4,omitempty"` 54 | IPv6 []byte `bencode:"ipv6,omitempty"` 55 | Port uint16 `bencode:"p,omitempty"` 56 | ReqQ uint32 `bencode:"reqq,omitempty"` 57 | MetadataSize uint32 `bencode:"metadata_size,omitempty"` 58 | Messages map[string]uint8 `bencode:"m,omitempty"` 59 | UploadOnly boolOrString `bencode:"upload_only"` 60 | Encrypt boolOrString `bencode:"e,omitempty"` 61 | } 62 | 63 | type pexInfo struct { 64 | Added []byte `bencode:"added,omitempty"` 65 | AddedF []byte `bencode:"added.f,omitempty"` 66 | Added6 []byte `bencode:"added6,omitempty"` 67 | Added6F []byte `bencode:"added6.f,omitempty"` 68 | Dropped []byte `bencode:"dropped,omitempty"` 69 | Dropped6 []byte `bencode:"dropped6,omitempty"` 70 | } 71 | 72 | type metadataInfo struct { 73 | Type *uint8 `bencode:"msg_type"` 74 | Piece *uint32 `bencode:"piece"` 75 | TotalSize *uint32 `bencode:"total_size"` 76 | } 77 | 78 | type Message interface{} 79 | type Error struct { 80 | Error error 81 | } 82 | type Flush struct{} 83 | type KeepAlive struct{} 84 | type Choke struct{} 85 | type Unchoke struct{} 86 | type Interested struct{} 87 | type NotInterested struct{} 88 | type Have struct { 89 | Index uint32 90 | } 91 | type Bitfield struct { 92 | Bitfield []byte 93 | } 94 | type Request struct { 95 | Index, Begin, Length uint32 96 | } 97 | type Piece struct { 98 | Index, Begin uint32 99 | Data []byte 100 | } 101 | type Cancel struct { 102 | Index, Begin, Length uint32 103 | } 104 | type Port struct { 105 | Port uint16 106 | } 107 | type SuggestPiece struct { 108 | Index uint32 109 | } 110 | type RejectRequest struct { 111 | Index, Begin, Length uint32 112 | } 113 | type AllowedFast struct { 114 | Index uint32 115 | } 116 | type HaveAll struct{} 117 | type HaveNone struct{} 118 | 119 | type Extended0 struct { 120 | Version string 121 | Port uint16 122 | ReqQ uint32 123 | IPv4 netip.Addr 124 | IPv6 netip.Addr 125 | MetadataSize uint32 126 | Messages map[string]uint8 127 | UploadOnly bool 128 | Encrypt bool 129 | } 130 | 131 | type ExtendedPex struct { 132 | Subtype uint8 133 | Added []pex.Peer 134 | Dropped []pex.Peer 135 | } 136 | 137 | type ExtendedMetadata struct { 138 | Subtype uint8 139 | Type uint8 140 | Piece uint32 141 | TotalSize uint32 142 | Data []byte 143 | } 144 | 145 | type ExtendedDontHave struct { 146 | Subtype uint8 147 | Index uint32 148 | } 149 | 150 | type ExtendedUploadOnly struct { 151 | Subtype uint8 152 | Value bool 153 | } 154 | 155 | type ExtendedUnknown struct { 156 | Subtype uint8 157 | } 158 | type Unknown struct{ tpe uint8 } 159 | -------------------------------------------------------------------------------- /tor/metadata.go: -------------------------------------------------------------------------------- 1 | package tor 2 | 3 | import ( 4 | "cmp" 5 | "crypto/sha1" 6 | "errors" 7 | "slices" 8 | 9 | "github.com/jech/storrent/hash" 10 | "github.com/jech/storrent/peer" 11 | ) 12 | 13 | func metadataPeers(t *Torrent, count int) []*peer.Peer { 14 | if len(t.peers) == 0 { 15 | return nil 16 | } 17 | pn := t.rand.Perm(len(t.peers)) 18 | var peers []*peer.Peer 19 | for n := range pn { 20 | if t.peers[n].CanMetadata() { 21 | peers = append(peers, t.peers[n]) 22 | if len(peers) >= count { 23 | break 24 | } 25 | } 26 | } 27 | return peers 28 | } 29 | 30 | func metadataVote(t *Torrent, size uint32) error { 31 | if t.infoComplete != 0 { 32 | return errors.New("metadata complete") 33 | } 34 | 35 | if size <= 0 || size > 128*1024*1024 { 36 | return errors.New("bad size") 37 | } 38 | if t.infoSizeVotes == nil { 39 | t.infoSizeVotes = make(map[uint32]int) 40 | } 41 | t.infoSizeVotes[size]++ 42 | return nil 43 | } 44 | 45 | func metadataGuess(t *Torrent) uint32 { 46 | if t.infoComplete != 0 { 47 | panic("Eek!") 48 | } 49 | if t.infoSizeVotes == nil { 50 | return 0 51 | } 52 | count := -1 53 | var size uint32 54 | for s, c := range t.infoSizeVotes { 55 | if c > count { 56 | count = c 57 | size = s 58 | } 59 | } 60 | return size 61 | } 62 | 63 | func resizeMetadata(t *Torrent, size uint32) error { 64 | if t.infoComplete != 0 { 65 | return errors.New("metadata complete") 66 | } 67 | 68 | if size <= 0 || size > 128*1024*1024 { 69 | return errors.New("bad size") 70 | } 71 | if len(t.Info) != int(size) { 72 | t.Info = make([]byte, size) 73 | t.infoBitmap = nil 74 | t.infoRequested = make([]uint8, (size+16*1024-1)/(16*1024)) 75 | 76 | } 77 | return nil 78 | } 79 | 80 | func requestMetadata(t *Torrent, p *peer.Peer) error { 81 | if t.infoComplete != 0 { 82 | return errors.New("metadata complete") 83 | } 84 | guess := metadataGuess(t) 85 | if guess == 0 { 86 | return errors.New("unknown size") 87 | } 88 | if uint32(len(t.Info)) != guess { 89 | err := resizeMetadata(t, guess) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | var peers []*peer.Peer 96 | if p == nil { 97 | peers = metadataPeers(t, 8) 98 | } else { 99 | peers = []*peer.Peer{p} 100 | } 101 | 102 | cn := t.rand.Perm(len(t.infoRequested)) 103 | slices.SortFunc(cn, func(i, j int) int { 104 | ai := t.infoBitmap.Get(i) 105 | aj := t.infoBitmap.Get(j) 106 | if !ai && aj { 107 | return -1 108 | } 109 | if ai && !aj { 110 | return 1 111 | } 112 | return cmp.Compare(t.infoRequested[i], t.infoRequested[j]) 113 | }) 114 | 115 | j := 0 116 | for _, p := range peers { 117 | if j >= len(cn) || t.infoBitmap.Get(cn[j]) { 118 | return nil 119 | } 120 | 121 | err := maybeWritePeer(p, peer.PeerGetMetadata{uint32(cn[j])}) 122 | if err == nil { 123 | t.infoRequested[cn[j]]++ 124 | j++ 125 | } 126 | } 127 | return nil 128 | } 129 | 130 | func gotMetadata(t *Torrent, index, size uint32, data []byte) (bool, error) { 131 | if t.infoComplete != 0 { 132 | return false, errors.New("metadata complete") 133 | } 134 | if size != uint32(len(t.Info)) { 135 | return false, errors.New("inconsistent metadata size") 136 | } 137 | chunks := len(t.infoRequested) 138 | if int(index) > chunks { 139 | return false, errors.New("chunk beyond end of metadata") 140 | } 141 | if len(data) != 16*1024 && 142 | int(index)*16*1024+len(data) != len(t.Info) { 143 | return false, errors.New("inconsistent metadata length") 144 | } 145 | if t.infoBitmap.Get(int(index)) { 146 | return false, nil 147 | } 148 | copy(t.Info[index*16*1024:], data) 149 | t.infoBitmap.Set(int(index)) 150 | 151 | for i := 0; i < chunks; i++ { 152 | if !t.infoBitmap.Get(i) { 153 | return false, nil 154 | } 155 | } 156 | 157 | hsh := sha1.Sum(t.Info) 158 | h := hash.Hash(hsh[:]) 159 | if !h.Equal(t.Hash) { 160 | t.Info = nil 161 | t.infoBitmap = nil 162 | t.infoRequested = nil 163 | return false, errors.New("hash mismatch") 164 | } 165 | err := t.MetadataComplete() 166 | if err != nil { 167 | t.Info = nil 168 | t.infoBitmap = nil 169 | t.infoRequested = nil 170 | return false, err 171 | } 172 | t.infoSizeVotes = nil 173 | t.infoBitmap = nil 174 | t.infoRequested = nil 175 | return true, nil 176 | } 177 | -------------------------------------------------------------------------------- /tracker/tracker.go: -------------------------------------------------------------------------------- 1 | // Package tracker implements the HTTP and UDP BitTorrent tracker protocols, 2 | // as defined in BEP-3, BEP-7, BEP-23 and BEP-15. 3 | package tracker 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net/netip" 10 | nurl "net/url" 11 | "sync/atomic" 12 | "time" 13 | ) 14 | 15 | var ( 16 | ErrNotReady = errors.New("tracker not ready") 17 | ErrParse = errors.New("couldn't parse tracker reply") 18 | ) 19 | 20 | // Type base implements the tracker lifetime. 21 | type base struct { 22 | url string 23 | time time.Time 24 | interval time.Duration 25 | err error 26 | locked int32 27 | } 28 | 29 | // Type Tracker represents a BitTorrent tracker. 30 | type Tracker interface { 31 | // URL returns the tracker URL 32 | URL() string 33 | // GetState returns the tracker's state. If the state is Error, 34 | // then GetState also returns an error value. 35 | GetState() (State, error) 36 | // Announce performs parallel announces over both IPv4 and IPv6. 37 | Announce(ctx context.Context, hash []byte, myid []byte, 38 | want int, size int64, port4, port6 int, proxy string, 39 | f func(netip.AddrPort) bool) error 40 | } 41 | 42 | // New creates a new tracker from a URL. 43 | func New(url string) Tracker { 44 | u, err := nurl.Parse(url) 45 | if err != nil { 46 | return nil 47 | } 48 | switch u.Scheme { 49 | case "http", "https": 50 | return &HTTP{base: base{url: url}} 51 | case "udp": 52 | return &UDP{base: base{url: url}} 53 | default: 54 | return &Unknown{base: base{url: url}} 55 | } 56 | } 57 | 58 | func (tracker *base) URL() string { 59 | return tracker.url 60 | } 61 | 62 | func (tracker *base) tryLock() bool { 63 | return atomic.CompareAndSwapInt32(&tracker.locked, 0, 1) 64 | } 65 | 66 | func (tracker *base) unlock() { 67 | ok := atomic.CompareAndSwapInt32(&tracker.locked, 1, 0) 68 | if !ok { 69 | panic("unlocking unlocked torrent") 70 | } 71 | } 72 | 73 | func (tracker *base) ready() bool { 74 | interval := tracker.interval 75 | if interval <= 0 { 76 | interval = 30 * time.Minute 77 | } 78 | if interval < 5*time.Minute { 79 | interval = 5 * time.Minute 80 | } 81 | return tracker.time.Add(interval).Before(time.Now()) 82 | } 83 | 84 | // Type State represtents the state of a tracker. 85 | type State int 86 | 87 | const ( 88 | Disabled State = -3 // the tracker is disabled 89 | Error State = -2 // last request failed 90 | Busy State = -1 // a request is in progress 91 | Idle State = 0 // it's not time yet for a new request 92 | Ready State = 1 // a request may be scheduled now 93 | ) 94 | 95 | func (state State) String() string { 96 | switch state { 97 | case Disabled: 98 | return "disabled" 99 | case Error: 100 | return "error" 101 | case Busy: 102 | return "busy" 103 | case Idle: 104 | return "idle" 105 | case Ready: 106 | return "ready" 107 | default: 108 | return fmt.Sprintf("unknown state %d", int(state)) 109 | } 110 | } 111 | 112 | // GetState returns a tracker's state. 113 | func (tracker *base) GetState() (State, error) { 114 | ok := tracker.tryLock() 115 | if !ok { 116 | return Busy, nil 117 | } 118 | ready := tracker.ready() 119 | tracker.unlock() 120 | if ready { 121 | return Ready, nil 122 | } 123 | if tracker.err != nil { 124 | return Error, tracker.err 125 | } 126 | return Idle, nil 127 | } 128 | 129 | // updateInterval updates a tracker's announce interval. 130 | func (tracker *base) updateInterval(interval time.Duration, err error) { 131 | tracker.err = err 132 | if interval > time.Minute { 133 | tracker.interval = interval 134 | } else if tracker.interval < 15*time.Minute { 135 | tracker.interval = 15 * time.Minute 136 | } 137 | } 138 | 139 | // Announce performs parallel announces over both IPv4 and IPv6. 140 | func (tracker *base) Announce(ctx context.Context, hash []byte, myid []byte, 141 | want int, size int64, port4, port6 int, proxy string, 142 | f func(netip.AddrPort) bool) error { 143 | return errors.New("not implemented") 144 | } 145 | 146 | // Unknown represents a tracker with an unknown scheme. 147 | type Unknown struct { 148 | base 149 | } 150 | 151 | // GetState for an tracker with an unknown scheme always returns Disabled. 152 | func (tracker *Unknown) GetState() (State, error) { 153 | return Disabled, nil 154 | } 155 | -------------------------------------------------------------------------------- /bitmap/bitmap.go: -------------------------------------------------------------------------------- 1 | // Package bitmap implements bitmaps that maintain their length in bytes 2 | // and behave compatibly with the bitfields used in the BitTorrent protocol. 3 | package bitmap 4 | 5 | import ( 6 | "math/bits" 7 | "strings" 8 | ) 9 | 10 | type Bitmap []uint8 11 | 12 | func New(length int) Bitmap { 13 | return Bitmap(make([]uint8, (length+7)/8)) 14 | } 15 | 16 | // Get returns true if the ith bit is set. 17 | func (b Bitmap) Get(i int) bool { 18 | if b == nil || i>>3 >= len(b) { 19 | return false 20 | } 21 | return (b[i>>3] & (1 << (7 - uint8(i&7)))) != 0 22 | } 23 | 24 | func (b Bitmap) String() string { 25 | var buf strings.Builder 26 | buf.Grow(len(b)*8 + 2) 27 | buf.WriteByte('[') 28 | for i := 0; i < len(b)*8; i++ { 29 | if b.Get(i) { 30 | buf.WriteByte('1') 31 | } else { 32 | buf.WriteByte('0') 33 | } 34 | } 35 | buf.WriteByte(']') 36 | return buf.String() 37 | } 38 | 39 | // Extend sets the length of the bitmap to at least the smallest multiple 40 | // of 8 that is strictly larger than i. 41 | func (b *Bitmap) Extend(i int) { 42 | if i>>3 >= len(*b) { 43 | *b = append(*b, make([]uint8, (i>>3)+1-len(*b))...) 44 | } 45 | } 46 | 47 | // Set sets the ith bit of the bitmap, extending it if necessery. 48 | func (b *Bitmap) Set(i int) { 49 | b.Extend(i) 50 | (*b)[i>>3] |= (1 << (7 - uint8(i&7))) 51 | } 52 | 53 | // Reset resets the ith bit of the bitmap. 54 | func (b *Bitmap) Reset(i int) { 55 | if i>>3 >= len(*b) { 56 | return 57 | } 58 | (*b)[i>>3] &= ^(1 << (7 - uint8(i&7))) 59 | } 60 | 61 | // Copy copies a bitmap. 62 | func (b Bitmap) Copy() Bitmap { 63 | if b == nil { 64 | return nil 65 | } 66 | c := make([]uint8, len(b)) 67 | copy(c, b) 68 | return c 69 | } 70 | 71 | // Sets all bits from 0 up to n - 1. 72 | func (b *Bitmap) SetMultiple(n int) { 73 | b.Extend(n) 74 | for i := 0; i < (n >> 3); i++ { 75 | (*b)[i] = 0xFF 76 | } 77 | for i := (n & ^7); i < n; i++ { 78 | b.Set(i) 79 | } 80 | } 81 | 82 | // Empty returns true if no bits are set. 83 | func (b Bitmap) Empty() bool { 84 | if b == nil { 85 | return true 86 | } 87 | for _, v := range b { 88 | if v != 0 { 89 | return false 90 | } 91 | } 92 | return true 93 | } 94 | 95 | // All returns true if all bits from 0 up to n - 1 are set. 96 | func (b Bitmap) All(n int) bool { 97 | if n == 0 { 98 | return true 99 | } 100 | if len(b) < n>>3 { 101 | return false 102 | } 103 | for i := 0; i < n>>3; i++ { 104 | if b[i] != 0xFF { 105 | return false 106 | } 107 | } 108 | if n&7 == 0 { 109 | return true 110 | } 111 | if len(b) < n>>3+1 || b[n>>3] != (0xFF<<(8-uint8(n&7))) { 112 | return false 113 | } 114 | return true 115 | } 116 | 117 | // Count returns the number of bits set. 118 | func (b Bitmap) Count() int { 119 | if b == nil { 120 | return 0 121 | } 122 | count := 0 123 | for _, v := range b { 124 | count += bits.OnesCount8(v) 125 | } 126 | return count 127 | } 128 | 129 | // Len returns the index of the highest bit set, plus one. 130 | func (b Bitmap) Len() int { 131 | if len(b) == 0 { 132 | return 0 133 | } 134 | 135 | for i := len(b) - 1; i >= 0; i-- { 136 | if b[i] != 0 { 137 | return i*8 + 8 - bits.TrailingZeros8(b[i]) 138 | } 139 | } 140 | return 0 141 | } 142 | 143 | // EqualValue returns true if two bitmaps have the same bits set 144 | // (ignoring any trailing zeroes). 145 | func (b1 Bitmap) EqualValue(b2 Bitmap) bool { 146 | n := len(b1) 147 | if n > len(b2) { 148 | n = len(b2) 149 | } 150 | 151 | for i := 0; i < n; i++ { 152 | if b1[i] != b2[i] { 153 | return false 154 | } 155 | } 156 | 157 | for i := n; i < len(b1); i++ { 158 | if b1[i] != 0 { 159 | return false 160 | } 161 | } 162 | 163 | for i := n; i < len(b2); i++ { 164 | if b2[i] != 0 { 165 | return false 166 | } 167 | } 168 | 169 | return true 170 | } 171 | 172 | // Range applies a given function on all set values in a bitmap in 173 | // increasing order. The iteration is interrupted when the function 174 | // returns false. 175 | func (b Bitmap) Range(f func(index int) bool) { 176 | for i, v := range b { 177 | if v == 0 { 178 | continue 179 | } 180 | for j := uint8(0); j < 8; j++ { 181 | if (v & (1 << (7 - j))) != 0 { 182 | c := f((i << 3) + int(j)) 183 | if !c { 184 | return 185 | } 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // Package config implements global configuration data for storrent. 2 | package config 3 | 4 | import ( 5 | "fmt" 6 | "sync/atomic" 7 | 8 | "github.com/jech/storrent/hash" 9 | ) 10 | 11 | // ProtocolPort is the port over which we accept TCP connections and UDP 12 | // datagrams. 13 | var ProtocolPort int 14 | 15 | var externalTCPPort, externalUDPPort int32 16 | 17 | // ExternalPort returns the external port used for TCP or UDP traffic for 18 | // a given protocol family. For IPv4, this can be different from ProtocolPort 19 | // if we are behind a NAT. For IPv6, this is always equal to ProtocolPort. 20 | func ExternalPort(tcp bool, ipv6 bool) int { 21 | if ipv6 { 22 | return ProtocolPort 23 | } 24 | if tcp { 25 | return int(atomic.LoadInt32(&externalTCPPort)) 26 | } 27 | return int(atomic.LoadInt32(&externalUDPPort)) 28 | } 29 | 30 | // SetExternalIPv4Port records our idea of the external port. 31 | func SetExternalIPv4Port(port int, tcp bool) { 32 | if tcp { 33 | atomic.StoreInt32(&externalTCPPort, int32(port)) 34 | return 35 | } 36 | atomic.StoreInt32(&externalUDPPort, int32(port)) 37 | } 38 | 39 | // HTTPAddr is the address on which 40 | var HTTPAddr string 41 | 42 | // DHTBootstrap is the filename of the dht.dat file. 43 | var DHTBootstrap string 44 | 45 | // DhtID is our DHT id. 46 | var DhtID hash.Hash 47 | 48 | const ( 49 | MinPeersPerTorrent = 40 50 | MaxPeersPerTorrent = 50 51 | ) 52 | 53 | var MemoryMark int64 54 | 55 | func MemoryHighMark() int64 { 56 | return MemoryMark 57 | } 58 | func MemoryLowMark() int64 { 59 | return MemoryMark * 7 / 8 60 | } 61 | 62 | // ChunkSize defines the size of the chunks that we request. While in 63 | // principle BEP-3 allows arbitrary powers of two, in practice other 64 | // BitTorrent implementations only honour requests for 16kB. 65 | const ChunkSize uint32 = 16 * 1024 66 | 67 | var uploadRate uint32 = 512 * 1024 68 | 69 | // UploadRate specifies our maximum upload rate. This is a hard limit (up 70 | // to fluctuations due to the rate estimator). 71 | func UploadRate() float64 { 72 | return float64(atomic.LoadUint32(&uploadRate)) 73 | } 74 | 75 | func SetUploadRate(rate float64) { 76 | var r uint32 77 | if rate < 0 { 78 | r = 0 79 | } else if rate > float64(^uint32(0)) { 80 | r = ^uint32(0) 81 | } else { 82 | r = uint32(rate + 0.5) 83 | } 84 | atomic.StoreUint32(&uploadRate, r) 85 | } 86 | 87 | var PrefetchRate float64 88 | var idleRate uint32 = 64 * 1024 89 | 90 | // IdleRate is the download rate for which we aim when no client requests 91 | // any data. 92 | func IdleRate() uint32 { 93 | return atomic.LoadUint32(&idleRate) 94 | } 95 | 96 | func SetIdleRate(rate uint32) { 97 | atomic.StoreUint32(&idleRate, rate) 98 | } 99 | 100 | var defaultProxyDialer atomic.Value 101 | 102 | func SetDefaultProxy(s string) error { 103 | defaultProxyDialer.Store(s) 104 | return nil 105 | } 106 | 107 | // DefaultProxy specifies the proxy through which we make both BitTorrent 108 | // and tracker connections. The DHT does not honour the proxy. 109 | func DefaultProxy() string { 110 | return defaultProxyDialer.Load().(string) 111 | } 112 | 113 | type DhtMode int 114 | 115 | const ( 116 | DhtNone DhtMode = iota 117 | DhtPassive 118 | DhtNormal 119 | ) 120 | 121 | func (m DhtMode) String() string { 122 | switch m { 123 | case DhtNone: 124 | return "none" 125 | case DhtPassive: 126 | return "passive" 127 | case DhtNormal: 128 | return "normal" 129 | default: 130 | return fmt.Sprintf("unknown (%v)", int(m)) 131 | } 132 | } 133 | 134 | func ParseDhtMode(s string) (DhtMode, error) { 135 | switch s { 136 | case "none": 137 | return DhtNone, nil 138 | case "passive": 139 | return DhtPassive, nil 140 | case "normal": 141 | return DhtNormal, nil 142 | default: 143 | return DhtNone, fmt.Errorf("unknown DHT mode %v", s) 144 | } 145 | } 146 | 147 | // DefaultDhtMode specifies the DHT mode of newly created torrents. 148 | var DefaultDhtMode DhtMode 149 | 150 | // DefaultUseTrackers, DefaultUseWebseeds specify the options of newly 151 | // created torrents. 152 | var DefaultUseTrackers, DefaultUseWebseeds bool 153 | 154 | // PreferEncryption and ForceEncryption specify the encryption policy for 155 | // newly created torrents. 156 | var PreferEncryption, ForceEncryption bool 157 | 158 | var MultipathTCP bool 159 | 160 | var Debug bool 161 | -------------------------------------------------------------------------------- /peer/requests/requests_test.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/jech/storrent/bitmap" 8 | ) 9 | 10 | func rsequal(rs Requests, a []uint32) bool { 11 | var b bitmap.Bitmap 12 | 13 | for _, v := range a { 14 | b.Set(int(v)) 15 | } 16 | 17 | for _, r := range rs.queue { 18 | if !b.Get(int(r.index)) { 19 | return false 20 | } 21 | b.Reset(int(r.index)) 22 | } 23 | 24 | for _, r := range rs.requested { 25 | v := r.index 26 | if !b.Get(int(v)) { 27 | return false 28 | } 29 | b.Reset(int(v)) 30 | } 31 | return b.Empty() 32 | } 33 | 34 | func check(rs *Requests, t *testing.T) { 35 | var b bitmap.Bitmap 36 | for _, q := range rs.queue { 37 | if b.Get(int(q.index)) { 38 | t.Errorf("Duplicate enqueued") 39 | } 40 | b.Set(int(q.index)) 41 | } 42 | for _, r := range rs.requested { 43 | if b.Get(int(r.index)) { 44 | t.Errorf("Duplicate request") 45 | } 46 | b.Set(int(r.index)) 47 | } 48 | if !b.EqualValue(rs.bitmap) { 49 | t.Errorf("Incorrect bitmap") 50 | } 51 | } 52 | 53 | func TestRequest(t *testing.T) { 54 | a := []uint32{3, 8, 17, 99, 13, 2, 9, 18, 44, 17} 55 | b := []uint32{3, 8, 99, 13, 2, 18, 44} 56 | var rs Requests 57 | check(&rs, t) 58 | 59 | for _, v := range a { 60 | rs.Enqueue(v) 61 | check(&rs, t) 62 | } 63 | 64 | if !rsequal(rs, a) { 65 | t.Errorf("Enqueue failed, expected %v, got %v", a, rs) 66 | } 67 | 68 | rs.DelRequested(17) 69 | check(&rs, t) 70 | rs.DelRequested(9) 71 | check(&rs, t) 72 | if !rsequal(rs, a) { 73 | t.Errorf("DelRequest failed, expected %v, got %v", a, rs) 74 | } 75 | 76 | for len(rs.queue) > 0 { 77 | r, _ := rs.Dequeue() 78 | check(&rs, t) 79 | rs.EnqueueRequest(r) 80 | check(&rs, t) 81 | if !rsequal(rs, a) { 82 | t.Errorf("Request failed, expected %v, got %v", a, rs) 83 | } 84 | } 85 | 86 | rs.Del(17) 87 | check(&rs, t) 88 | rs.DelRequested(9) 89 | check(&rs, t) 90 | if !rsequal(rs, b) { 91 | t.Errorf("Del failed, expected %v, got %v", b, rs) 92 | } 93 | 94 | if rs.requested[0].Cancelled() { 95 | t.Errorf("Cancelled: expected false") 96 | } 97 | index := rs.requested[0].index 98 | rs.Cancel(index) 99 | check(&rs, t) 100 | if !rs.requested[0].Cancelled() { 101 | t.Errorf("Cancelled: expected true") 102 | } 103 | 104 | var c []uint32 105 | for len(rs.requested) > 0 { 106 | index := rs.requested[0].index 107 | rs.Del(index) 108 | c = append(c, index) 109 | check(&rs, t) 110 | } 111 | for _, v := range c { 112 | rs.Enqueue(v) 113 | check(&rs, t) 114 | } 115 | if !rsequal(rs, b) { 116 | t.Errorf("DequeueRequest failed, expected %v, got %v", b, rs) 117 | } 118 | 119 | rs.Del(17) 120 | check(&rs, t) 121 | rs.Del(9) 122 | check(&rs, t) 123 | if !rsequal(rs, b) { 124 | t.Errorf("Del failed, expected %v, got %v", b, rs) 125 | } 126 | 127 | rs = Requests{} 128 | check(&rs, t) 129 | for _, v := range a { 130 | rs.Enqueue(v) 131 | check(&rs, t) 132 | } 133 | for len(rs.queue) != 0 { 134 | q, _ := rs.Dequeue() 135 | rs.EnqueueRequest(q) 136 | check(&rs, t) 137 | if !rsequal(rs, a) { 138 | t.Errorf("Request failed, expected %v, got %v", a, rs) 139 | } 140 | } 141 | } 142 | 143 | func TestExpire(t *testing.T) { 144 | now := time.Now() 145 | ta := now.Add(-180 * time.Second) 146 | tb := now.Add(-30 * time.Second) 147 | tc := now.Add(-15 * time.Second) 148 | td := now.Add(-5 * time.Second) 149 | rs := Requests{ 150 | queue: nil, 151 | requested: []Request{ 152 | Request{index: 0, qtime: ta, rtime: ta}, 153 | Request{index: 1, qtime: ta, rtime: tc}, 154 | Request{index: 2, qtime: ta, rtime: ta, ctime: now}, 155 | Request{index: 3, qtime: ta, rtime: tc, ctime: now}, 156 | Request{index: 4, qtime: ta, rtime: tc, ctime: tc}, 157 | }, 158 | } 159 | for _, r := range rs.requested { 160 | rs.bitmap.Set(int(r.index)) 161 | } 162 | 163 | dropped := 0 164 | canceled := 0 165 | rs.Expire(tb, td, 166 | func(index uint32) { dropped++ }, 167 | func(index uint32) { canceled++ }, 168 | ) 169 | if dropped != 1 { 170 | t.Errorf("Dropped is %v", dropped) 171 | } 172 | if canceled != 1 { 173 | t.Errorf("Canceled is %v", canceled) 174 | } 175 | 176 | if len(rs.requested) != 4 { 177 | t.Errorf("len(requested) is %v", len(rs.requested)) 178 | } 179 | 180 | for i, r := range rs.requested { 181 | if r.index != uint32(i) { 182 | t.Errorf("requested[%v].index is %v", 183 | i, r.index) 184 | } 185 | if r.Cancelled() != (i != 1) { 186 | t.Errorf("Cancelled %v for %v", 187 | r.Cancelled(), i) 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /peer/event.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "net/netip" 5 | "time" 6 | 7 | "github.com/jech/storrent/bitmap" 8 | "github.com/jech/storrent/config" 9 | "github.com/jech/storrent/hash" 10 | "github.com/jech/storrent/known" 11 | "github.com/jech/storrent/pex" 12 | ) 13 | 14 | type TorEvent interface{} 15 | 16 | type TorAddPeer struct { 17 | Peer *Peer 18 | Init []byte 19 | } 20 | 21 | type TorPeerExtended struct { 22 | Peer *Peer 23 | MetadataSize uint32 24 | } 25 | 26 | type TorAddKnown struct { 27 | Peer *Peer 28 | Addr netip.AddrPort 29 | Id hash.Hash 30 | Version string 31 | Kind known.Kind 32 | } 33 | 34 | type TorBadPeer struct { 35 | Peer uint32 36 | Bad bool 37 | } 38 | 39 | type TorStats struct { 40 | Length int64 41 | PieceLength uint32 42 | NumPeers int 43 | NumKnown int 44 | NumTrackers int 45 | NumWebseeds int 46 | } 47 | 48 | type TorGetStats struct { 49 | Ch chan<- *TorStats 50 | } 51 | 52 | type TorGetAvailable struct { 53 | Ch chan<- []uint16 54 | } 55 | 56 | type TorDropPeer struct { 57 | Ch chan<- bool 58 | } 59 | 60 | type TorGetPeer struct { 61 | Id hash.Hash 62 | Ch chan<- *Peer 63 | } 64 | 65 | type TorGetPeers struct { 66 | Ch chan<- []*Peer 67 | } 68 | 69 | type TorGetKnown struct { 70 | Id hash.Hash 71 | Addr netip.AddrPort 72 | Ch chan<- known.Peer 73 | } 74 | 75 | type TorGetKnowns struct { 76 | Ch chan<- []known.Peer 77 | } 78 | 79 | type TorPeerGoaway struct { 80 | Peer *Peer 81 | } 82 | 83 | type TorData struct { 84 | Peer *Peer 85 | Index, Begin, Length uint32 86 | Complete bool 87 | } 88 | 89 | type TorDrop struct { 90 | Index, Begin, Length uint32 91 | } 92 | 93 | type TorRequest struct { 94 | Index uint32 95 | Priority int8 96 | Request bool 97 | Ch chan<- <-chan struct{} 98 | } 99 | 100 | type TorMetaData struct { 101 | Peer *Peer 102 | Size, Index uint32 103 | Data []byte 104 | } 105 | 106 | type TorPeerBitmap struct { 107 | Peer *Peer 108 | Bitmap bitmap.Bitmap 109 | Have bool 110 | } 111 | 112 | type TorPeerHave struct { 113 | Peer *Peer 114 | Index uint32 115 | Have bool 116 | } 117 | 118 | type TorHave struct { 119 | Index uint32 120 | Have bool 121 | } 122 | 123 | type TorPeerInterested struct { 124 | Peer *Peer 125 | Interested bool 126 | } 127 | 128 | type TorPeerUnchoke struct { 129 | Peer *Peer 130 | Unchoke bool 131 | } 132 | 133 | type TorGoAway struct { 134 | } 135 | 136 | type TorAnnounce struct { 137 | IPv6 bool 138 | } 139 | 140 | type TorConf struct { 141 | DhtMode config.DhtMode 142 | UseTrackers bool 143 | UseWebseeds bool 144 | } 145 | 146 | type TorSetConf struct { 147 | Conf TorConf 148 | Ch chan<- struct{} 149 | } 150 | 151 | type TorGetConf struct { 152 | Ch chan<- TorConf 153 | } 154 | 155 | type PeerEvent interface{} 156 | 157 | type PeerMetadataComplete struct { 158 | Info []byte 159 | } 160 | 161 | type PeerRequest struct { 162 | Chunks []uint32 163 | } 164 | 165 | type PeerHave struct { 166 | Index uint32 167 | Have bool 168 | } 169 | 170 | type PeerCancel struct { 171 | Chunk uint32 172 | } 173 | 174 | type PeerCancelPiece struct { 175 | Index uint32 176 | } 177 | 178 | type PeerInterested struct { 179 | Interested bool 180 | } 181 | 182 | type PeerGetMetadata struct { 183 | Index uint32 184 | } 185 | 186 | type PeerPex struct { 187 | Peers []pex.Peer 188 | Add bool 189 | } 190 | 191 | type PeerStatus struct { 192 | Unchoked bool 193 | Interested bool 194 | AmUnchoking bool 195 | AmInterested bool 196 | Seed bool 197 | UploadOnly bool 198 | Qlen int 199 | Download float64 200 | AvgDownload float64 201 | UnchokeTime time.Time 202 | } 203 | 204 | type PeerGetStatus struct { 205 | Ch chan<- PeerStatus 206 | } 207 | 208 | type PeerStats struct { 209 | Unchoked bool 210 | Interested bool 211 | AmUnchoking bool 212 | AmInterested bool 213 | Seed bool 214 | UploadOnly bool 215 | HasProxy bool 216 | Download float64 217 | AvgDownload float64 218 | Upload float64 219 | Rtt time.Duration 220 | Rttvar time.Duration 221 | UnchokeTime time.Time 222 | Rlen int 223 | Qlen int 224 | Ulen int 225 | PieceCount int 226 | NumPex int 227 | } 228 | 229 | type PeerGetPex struct { 230 | Ch chan<- pex.Peer 231 | } 232 | type PeerGetBitmap struct { 233 | Ch chan<- bitmap.Bitmap 234 | } 235 | 236 | type PeerGetHave struct { 237 | Index uint32 238 | Ch chan<- bool 239 | } 240 | 241 | type PeerGetStats struct { 242 | Ch chan<- PeerStats 243 | } 244 | 245 | type PeerGetFast struct { 246 | Ch chan<- []uint32 247 | } 248 | 249 | type PeerUnchoke struct { 250 | Unchoke bool 251 | } 252 | 253 | type PeerDone struct { 254 | } 255 | -------------------------------------------------------------------------------- /webseed/getright.go: -------------------------------------------------------------------------------- 1 | package webseed 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/jech/storrent/httpclient" 13 | ) 14 | 15 | type GetRight struct { 16 | base 17 | } 18 | 19 | type FileError struct { 20 | file []string 21 | err error 22 | } 23 | 24 | func (err FileError) Error() string { 25 | return fmt.Sprintf("%v (%v)", err.err, strings.Join(err.file, "/")) 26 | } 27 | 28 | func (err FileError) Unwrap() error { 29 | return err.err 30 | } 31 | 32 | var ErrParse = errors.New("parse error") 33 | 34 | func parseContentRange(cr string) (offset int64, length int64, fl int64, 35 | err error) { 36 | offset = -1 37 | length = -1 38 | fl = -1 39 | 40 | var end int64 41 | n, err := fmt.Sscanf(cr, "bytes %d-%d/%d\n", &offset, &end, &fl) 42 | if err == nil { 43 | if n != 3 || end < offset || end >= fl { 44 | err = ErrParse 45 | return 46 | } 47 | length = end - offset + 1 48 | return 49 | } 50 | 51 | n, err = fmt.Sscanf(cr, "bytes %d-%d/*\n", &offset, &end) 52 | if err == nil { 53 | if n != 2 || end < offset { 54 | err = ErrParse 55 | return 56 | } 57 | length = end - offset + 1 58 | return 59 | } 60 | 61 | n, err = fmt.Sscanf(cr, "bytes */%d\n", &fl) 62 | if err == nil { 63 | if n != 1 { 64 | err = ErrParse 65 | return 66 | } 67 | return 68 | } 69 | 70 | err = ErrParse 71 | return 72 | } 73 | 74 | func buildUrl(url string, name string, file []string) string { 75 | if !strings.HasSuffix(url, "/") { 76 | if file == nil { 77 | return url 78 | } 79 | url = url + "/" 80 | } 81 | 82 | url = url + name 83 | if file == nil { 84 | return url 85 | } 86 | 87 | if !strings.HasSuffix(url, "/") { 88 | url = url + "/" 89 | } 90 | 91 | url = url + strings.Join(file, "/") 92 | return url 93 | } 94 | 95 | func (ws *GetRight) Get(ctx context.Context, proxy string, 96 | name string, file []string, flength, offset, length int64, 97 | w io.Writer) (int64, error) { 98 | 99 | errorf := func(format string, args... interface{}) error { 100 | return FileError{file, fmt.Errorf(format, args...)} 101 | } 102 | 103 | ws.start() 104 | defer ws.stop() 105 | 106 | url := buildUrl(ws.url, name, file) 107 | req, err := http.NewRequest("GET", url, nil) 108 | if err != nil { 109 | ws.error(true) 110 | return 0, err 111 | } 112 | 113 | req.Header.Set("Range", 114 | fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1)) 115 | req.Header["User-Agent"] = nil 116 | 117 | client := httpclient.Get("", proxy) 118 | if client == nil { 119 | return 0, errors.New("couldn't get HTTP client") 120 | } 121 | 122 | r, err := client.Do(req.WithContext(ctx)) 123 | if err != nil { 124 | ws.error(true) 125 | return 0, err 126 | } 127 | defer r.Body.Close() 128 | 129 | fl := int64(-1) 130 | l := int64(-1) 131 | if r.StatusCode == http.StatusOK { 132 | if offset != 0 { 133 | ws.error(true) 134 | return 0, errorf("server ignored range request") 135 | } 136 | cl := r.Header.Get("Content-Length") 137 | if cl != "" { 138 | var err error 139 | fl, err = strconv.ParseInt(cl, 10, 64) 140 | if err != nil { 141 | ws.error(true) 142 | return 0, FileError{file, err} 143 | } 144 | l = fl 145 | } 146 | } else if r.StatusCode == http.StatusPartialContent { 147 | rng := r.Header.Get("Content-Range") 148 | if rng == "" { 149 | ws.error(true) 150 | return 0, errorf("missing Content-Range") 151 | } 152 | var o int64 153 | o, l, fl, err = parseContentRange(rng) 154 | if err != nil { 155 | ws.error(true) 156 | return 0, FileError{file, err} 157 | } 158 | if o != offset { 159 | ws.error(true) 160 | return 0, errorf("server didn't honour range request") 161 | } 162 | } else if r.StatusCode == http.StatusRequestedRangeNotSatisfiable { 163 | rng := r.Header.Get("Content-Range") 164 | if rng != "" { 165 | _, _, fl, err := parseContentRange(rng) 166 | if err == nil { 167 | ws.error(true) 168 | return 0, errorf("range mismatch " + 169 | "(expected %v, got %v)", 170 | flength, fl) 171 | } 172 | } 173 | ws.error(true) 174 | return 0, FileError{file, errors.New(r.Status)} 175 | } else { 176 | ws.error(true) 177 | return 0, FileError{file, errors.New(r.Status)} 178 | } 179 | 180 | if l < 0 { 181 | l = flength - offset 182 | } 183 | 184 | if fl >= 0 { 185 | if fl != flength { 186 | ws.error(true) 187 | return 0, errorf("range mismatch " + 188 | "(expected %v, got %v)", 189 | flength, fl) 190 | } 191 | } 192 | 193 | reader := io.Reader(r.Body) 194 | if l > length { 195 | reader = io.LimitReader(reader, length) 196 | } 197 | 198 | n, err := io.Copy(w, reader) 199 | ws.Accumulate(int(n)) 200 | if n > 0 { 201 | ws.error(false) 202 | } 203 | return n, nil 204 | } 205 | -------------------------------------------------------------------------------- /tor/initial.go: -------------------------------------------------------------------------------- 1 | package tor 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "net/netip" 8 | "net/url" 9 | "time" 10 | 11 | "golang.org/x/net/proxy" 12 | 13 | "github.com/jech/storrent/config" 14 | "github.com/jech/storrent/crypto" 15 | "github.com/jech/storrent/known" 16 | "github.com/jech/storrent/protocol" 17 | ) 18 | 19 | var ErrMartianAddress = errors.New("martian address") 20 | var ErrConnectionSelf = errors.New("connection to self") 21 | var ErrDuplicateConnection = errors.New("duplicate connection") 22 | 23 | // Server accepts a peer connection. 24 | func Server(conn net.Conn, cryptoOptions *crypto.Options) error { 25 | addr, ok := conn.RemoteAddr().(*net.TCPAddr) 26 | if !ok || !addr.IP.IsGlobalUnicast() { 27 | conn.Close() 28 | return ErrMartianAddress 29 | } 30 | 31 | ip := addr.IP.To4() 32 | if ip == nil { 33 | ip = addr.IP.To16() 34 | } 35 | if ip == nil { 36 | conn.Close() 37 | return errors.New("couldn't parse IP address") 38 | } 39 | 40 | conn, result, init, err := 41 | protocol.ServerHandshake(conn, infoHashes(false), cryptoOptions) 42 | if err != nil { 43 | conn.Close() 44 | return err 45 | } 46 | 47 | t := Get(result.Hash) 48 | if t == nil { 49 | conn.Close() 50 | return protocol.ErrUnknownTorrent 51 | } 52 | 53 | if result.Id.Equal(t.MyId) { 54 | conn.Close() 55 | return ErrConnectionSelf 56 | } 57 | 58 | if t.hasProxy() { 59 | conn.Close() 60 | return errors.New("torrent is proxied") 61 | } 62 | 63 | stats, err := t.GetStats() 64 | if err != nil { 65 | conn.Close() 66 | return err 67 | } 68 | 69 | if stats.NumPeers >= config.MaxPeersPerTorrent { 70 | dropped, _ := t.DropPeer() 71 | if !dropped { 72 | conn.Close() 73 | return nil 74 | } 75 | } 76 | 77 | q, _ := t.GetPeer(result.Id) 78 | if q != nil { 79 | conn.Close() 80 | return ErrDuplicateConnection 81 | } 82 | 83 | ipp, ok := netip.AddrFromSlice(ip) 84 | if !ok { 85 | conn.Close() 86 | return errors.New("couldn't parse address") 87 | } 88 | err = t.NewPeer( 89 | t.proxy, conn, netip.AddrPortFrom(ipp, 0), true, result, init, 90 | ) 91 | if err != nil { 92 | conn.Close() 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | // DialClient connects to a peer and runs the client loop. 99 | func DialClient(ctx context.Context, t *Torrent, addr netip.AddrPort, cryptoOptions *crypto.Options) error { 100 | var conn net.Conn 101 | var err error 102 | cryptoHandshake := 103 | cryptoOptions.PreferCryptoHandshake && 104 | cryptoOptions.AllowCryptoHandshake 105 | 106 | again: 107 | if err := ctx.Err(); err != nil { 108 | return err 109 | } 110 | 111 | if !addr.Addr().IsGlobalUnicast() { 112 | return ErrMartianAddress 113 | } 114 | if port := addr.Port(); port == 0 || port == 1 || port == 22 || port == 25 { 115 | return errors.New("bad port") 116 | } 117 | 118 | if t.proxy == "" { 119 | var dialer net.Dialer 120 | dialer.Timeout = 20 * time.Second 121 | if config.MultipathTCP { 122 | dialer.SetMultipathTCP(true) 123 | } 124 | conn, err = dialer.DialContext(ctx, "tcp", addr.String()) 125 | } else { 126 | var u *url.URL 127 | u, err = url.Parse(t.proxy) 128 | if err != nil { 129 | return err 130 | } 131 | var dialer proxy.Dialer 132 | dialer, err = proxy.FromURL(u, proxy.Direct) 133 | if err != nil { 134 | return err 135 | } 136 | d, ok := dialer.(proxy.ContextDialer) 137 | if !ok { 138 | return errors.New("dialer is not ContextDialer") 139 | } 140 | ctx2, cancel := context.WithTimeout(ctx, 20*time.Second) 141 | conn, err = d.DialContext(ctx2, "tcp", addr.String()) 142 | cancel() 143 | } 144 | if err != nil { 145 | return err 146 | } 147 | 148 | if err := ctx.Err(); err != nil { 149 | conn.Close() 150 | return err 151 | } 152 | 153 | err = Client(conn, t, addr, t.proxy, cryptoHandshake, cryptoOptions) 154 | if errors.Is(err, protocol.ErrBadHandshake) { 155 | if cryptoOptions.PreferCryptoHandshake && 156 | !cryptoOptions.ForceCryptoHandshake { 157 | if cryptoHandshake { 158 | cryptoHandshake = false 159 | goto again 160 | } 161 | } 162 | if !cryptoOptions.PreferCryptoHandshake && 163 | cryptoOptions.AllowCryptoHandshake { 164 | if !cryptoHandshake { 165 | cryptoHandshake = true 166 | goto again 167 | } 168 | } 169 | } 170 | 171 | return err 172 | } 173 | 174 | // Client establishes a connection with a client. 175 | func Client(conn net.Conn, t *Torrent, addr netip.AddrPort, proxy string, cryptoHandshake bool, cryptoOptions *crypto.Options) error { 176 | 177 | stats, err := t.GetStats() 178 | if err != nil { 179 | conn.Close() 180 | return err 181 | } 182 | 183 | if stats.NumPeers >= config.MaxPeersPerTorrent { 184 | conn.Close() 185 | return nil 186 | } 187 | 188 | conn, result, init, err := 189 | protocol.ClientHandshake(conn, cryptoHandshake, 190 | t.Hash, t.MyId, cryptoOptions) 191 | if err != nil { 192 | conn.Close() 193 | return err 194 | } 195 | 196 | err = t.AddKnown(addr, result.Id, "", known.Seen) 197 | if err != nil { 198 | conn.Close() 199 | return err 200 | } 201 | 202 | if result.Id.Equal(t.MyId) { 203 | conn.Close() 204 | return ErrConnectionSelf 205 | } 206 | 207 | q, _ := t.GetPeer(result.Id) 208 | if q != nil { 209 | conn.Close() 210 | return ErrDuplicateConnection 211 | } 212 | 213 | err = t.NewPeer(proxy, conn, addr, false, result, init) 214 | if err != nil { 215 | conn.Close() 216 | return err 217 | } 218 | return nil 219 | } 220 | -------------------------------------------------------------------------------- /tor/reader.go: -------------------------------------------------------------------------------- 1 | package tor 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "net" 8 | "runtime" 9 | 10 | "github.com/jech/storrent/config" 11 | ) 12 | 13 | // requested indicates a piece requested by a reader. 14 | type requested struct { 15 | index uint32 16 | prio int8 17 | } 18 | 19 | // A Reader reads data from a torrent. It can span piece boundaries. 20 | type Reader struct { 21 | torrent *Torrent 22 | offset int64 23 | length int64 24 | 25 | position int64 26 | requested []requested 27 | requestedIndex int 28 | ch <-chan struct{} 29 | context context.Context 30 | } 31 | 32 | // NewReader creates a new Reader. 33 | func (t *Torrent) NewReader(ctx context.Context, offset int64, length int64) *Reader { 34 | r := &Reader{ 35 | torrent: t, 36 | offset: offset, 37 | length: length, 38 | context: ctx, 39 | requestedIndex: -1, 40 | } 41 | runtime.SetFinalizer(r, (*Reader).Close) 42 | return r 43 | } 44 | 45 | // SetContext changes the context that will abort any requests sent by the 46 | // reader. 47 | func (r *Reader) SetContext(ctx context.Context) { 48 | r.context = ctx 49 | } 50 | 51 | // Seek sets the position of a Reader. It doesn't trigger prefetch. 52 | func (r *Reader) Seek(o int64, whence int) (n int64, err error) { 53 | if r.torrent == nil { 54 | return r.position, net.ErrClosed 55 | } 56 | var pos int64 57 | switch whence { 58 | case io.SeekStart: 59 | pos = o 60 | case io.SeekCurrent: 61 | pos = r.position + o 62 | case io.SeekEnd: 63 | pos = r.length + o 64 | default: 65 | return r.position, errors.New("seek: invalid whence") 66 | } 67 | if pos < 0 { 68 | return r.position, errors.New("seek: negative position") 69 | } 70 | r.position = pos 71 | return pos, nil 72 | } 73 | 74 | // chunks returns a list of chunks to request. 75 | func (r *Reader) chunks(pos int64, limit int64) []requested { 76 | if pos < 0 || pos > limit { 77 | return nil 78 | } 79 | 80 | t := r.torrent 81 | ps := int64(t.Pieces.PieceSize()) 82 | index := uint32(pos / ps) 83 | begin := uint32(pos % ps) 84 | max := uint32(limit / ps) 85 | remain := t.Pieces.PieceSize() - begin 86 | 87 | // five second prefetch, but at least one piece 88 | prefetch := 89 | uint32((config.PrefetchRate*5-float64(remain))/float64(ps) + 0.5) 90 | if prefetch < 1 { 91 | prefetch = 1 92 | } 93 | 94 | if index+1+prefetch > max { 95 | prefetch = max - index - 1 96 | } 97 | 98 | i := uint32(0) 99 | c := make([]requested, 0, 2+prefetch) 100 | c = append(c, requested{index + i, 1}) 101 | i++ 102 | 103 | // aggressive prefetch if less than two seconds left 104 | if float64(remain) < config.PrefetchRate*2 { 105 | c = append(c, requested{index + i, 0}) 106 | i++ 107 | } 108 | 109 | for i < prefetch+1 { 110 | c = append(c, requested{index + i, -1}) 111 | i++ 112 | } 113 | return c 114 | } 115 | 116 | // request causes a reader to request chunks from a torrent. 117 | func (r *Reader) request(pos int64, limit int64) (<-chan struct{}, error) { 118 | if r.requestedIndex >= 0 { 119 | index := uint32(pos / int64(r.torrent.Pieces.PieceSize())) 120 | if r.requestedIndex == int(index) { 121 | return r.ch, nil 122 | } 123 | } 124 | 125 | chunks := r.chunks(pos, limit) 126 | old := r.requested 127 | r.requested = make([]requested, 0, len(chunks)) 128 | var done <-chan struct{} 129 | var err error 130 | 131 | for i, c := range chunks { 132 | d, dn, e := 133 | r.torrent.Request(c.index, c.prio, true, i == 0) 134 | if d { 135 | r.requested = append(r.requested, c) 136 | } 137 | if i == 0 { 138 | done = dn 139 | err = e 140 | if err != nil { 141 | break 142 | } 143 | } 144 | } 145 | 146 | for _, c := range old { 147 | r.torrent.Request(c.index, c.prio, false, false) 148 | } 149 | 150 | if len(chunks) > 0 { 151 | r.requestedIndex = int(chunks[0].index) 152 | } else { 153 | r.requestedIndex = -1 154 | } 155 | r.ch = done 156 | return done, err 157 | } 158 | 159 | // Read reads data from a torrent, performing prefetch as necessary. 160 | func (r *Reader) Read(a []byte) (n int, err error) { 161 | t := r.torrent 162 | if t == nil { 163 | err = net.ErrClosed 164 | return 165 | } 166 | 167 | if r.position >= r.length { 168 | r.request(-1, -1) 169 | err = io.EOF 170 | return 171 | } 172 | 173 | err = r.context.Err() 174 | if err != nil { 175 | r.request(-1, -1) 176 | return 177 | } 178 | 179 | done, err := r.request(r.offset+r.position, r.offset+r.length) 180 | if err != nil { 181 | return 182 | } 183 | 184 | if done != nil { 185 | select { 186 | case <-t.Done: 187 | r.request(-1, -1) 188 | err = ErrTorrentDead 189 | return 190 | case <-r.context.Done(): 191 | r.request(-1, -1) 192 | err = r.context.Err() 193 | return 194 | case <-done: 195 | } 196 | } 197 | 198 | if r.position+int64(len(a)) < r.length { 199 | n, err = t.Pieces.ReadAt(a, r.offset+r.position) 200 | } else { 201 | n, err = t.Pieces.ReadAt(a[:r.length-r.position], 202 | r.offset+r.position) 203 | } 204 | if err == nil && int64(n) == r.length-r.position { 205 | err = io.EOF 206 | } 207 | 208 | if err != nil { 209 | r.request(-1, -1) 210 | } 211 | 212 | r.position += int64(n) 213 | return 214 | } 215 | 216 | // Close closes a reader. Any in-flight requests are cancelled. 217 | func (r *Reader) Close() error { 218 | r.request(-1, -1) 219 | r.torrent = nil 220 | runtime.SetFinalizer(r, nil) 221 | return nil 222 | } 223 | -------------------------------------------------------------------------------- /tracker/http.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "net/netip" 8 | nurl "net/url" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "github.com/zeebo/bencode" 14 | 15 | "github.com/jech/storrent/httpclient" 16 | ) 17 | 18 | // HTTP represents a tracker accessed over HTTP or HTTPS. 19 | type HTTP struct { 20 | base 21 | } 22 | 23 | // httpReply is an HTTP tracker's reply. 24 | type httpReply struct { 25 | FailureReason string `bencode:"failure reason"` 26 | RetryIn string `bencode:"retry in"` 27 | Interval int `bencode:"interval"` 28 | Peers bencode.RawMessage `bencode:"peers,omitempty"` 29 | Peers6 []byte `bencode:"peers6,omitempty"` 30 | } 31 | 32 | // peer is a peer returned by a tracker. 33 | type peer struct { 34 | IP string `bencode:"ip"` 35 | Port uint16 `bencode:"port"` 36 | } 37 | 38 | // Announce performs an HTTP announce over both IPv4 and IPv6 in parallel. 39 | func (tracker *HTTP) Announce(ctx context.Context, hash []byte, myid []byte, 40 | want int, size int64, port4, port6 int, proxy string, 41 | f func(netip.AddrPort) bool) error { 42 | 43 | ok := tracker.tryLock() 44 | if !ok { 45 | return ErrNotReady 46 | } 47 | defer tracker.unlock() 48 | 49 | if !tracker.ready() { 50 | return ErrNotReady 51 | } 52 | 53 | tracker.time = time.Now() 54 | 55 | var interval int 56 | var err error 57 | 58 | if proxy != "" { 59 | port := port6 60 | if port == 0 { 61 | port = port4 62 | } 63 | interval, err = announceHTTP( 64 | ctx, "", tracker, hash, myid, want, size, port, proxy, f, 65 | ) 66 | } else { 67 | var i4, i6 int 68 | var e4, e6 error 69 | var wg sync.WaitGroup 70 | wg.Add(2) 71 | go func() { 72 | i4, e4 = announceHTTP( 73 | ctx, "tcp4", tracker, hash, myid, 74 | want, size, port4, proxy, f, 75 | ) 76 | wg.Done() 77 | }() 78 | go func() { 79 | i6, e6 = announceHTTP( 80 | ctx, "tcp6", tracker, hash, myid, 81 | want, size, port6, proxy, f, 82 | ) 83 | wg.Done() 84 | }() 85 | wg.Wait() 86 | if e4 != nil && e6 != nil { 87 | err = e4 88 | } 89 | interval = i4 90 | if interval < i6 { 91 | interval = i6 92 | } 93 | } 94 | 95 | tracker.updateInterval(time.Duration(interval)*time.Second, err) 96 | return err 97 | } 98 | 99 | // announceHTTP performs a single HTTP announce 100 | func announceHTTP(ctx context.Context, protocol string, tracker *HTTP, 101 | hash []byte, myid []byte, want int, size int64, port int, proxy string, 102 | f func(netip.AddrPort) bool) (int, error) { 103 | url, err := nurl.Parse(tracker.url) 104 | if err != nil { 105 | return 0, err 106 | } 107 | 108 | v := nurl.Values{} 109 | v.Set("info_hash", string(hash)) 110 | v.Set("peer_id", string(myid)) 111 | v.Set("numwant", strconv.Itoa(want)) 112 | if port > 0 { 113 | v.Set("port", strconv.Itoa(port)) 114 | } 115 | v.Set("downloaded", strconv.FormatInt(size/2, 10)) 116 | v.Set("uploaded", strconv.FormatInt(2*size, 10)) 117 | v.Set("left", strconv.FormatInt(size/2, 10)) 118 | v.Set("compact", "1") 119 | url.RawQuery = v.Encode() 120 | 121 | req, err := http.NewRequest("GET", url.String(), nil) 122 | if err != nil { 123 | return 0, err 124 | } 125 | req.Close = true 126 | req.Header.Set("Cache-Control", "max-age=0") 127 | req.Header["User-Agent"] = nil 128 | 129 | client := httpclient.Get(protocol, proxy) 130 | if client == nil { 131 | return 0, errors.New("couldn't get HTTP client") 132 | } 133 | 134 | r, err := client.Do(req.WithContext(ctx)) 135 | if err != nil { 136 | return 0, err 137 | } 138 | 139 | defer r.Body.Close() 140 | 141 | if r.StatusCode != 200 { 142 | return 0, errors.New(r.Status) 143 | } 144 | 145 | decoder := bencode.NewDecoder(r.Body) 146 | var reply httpReply 147 | err = decoder.Decode(&reply) 148 | if err != nil { 149 | return 0, err 150 | } 151 | 152 | if reply.FailureReason != "" { 153 | retry := time.Duration(0) 154 | if reply.RetryIn == "never" { 155 | retry = time.Duration(time.Hour * 2400) 156 | } else if reply.RetryIn != "" { 157 | min, err := strconv.Atoi(reply.RetryIn) 158 | if err == nil && min > 0 { 159 | retry = time.Duration(min) * time.Minute 160 | } 161 | } 162 | tracker.interval = retry 163 | err = errors.New(reply.FailureReason) 164 | return 0, err 165 | } 166 | 167 | var peers []byte 168 | err = bencode.DecodeBytes(reply.Peers, &peers) 169 | if err == nil && len(peers)%6 == 0 { 170 | // compact format 171 | for i := 0; i < len(peers); i += 6 { 172 | ip, ok := netip.AddrFromSlice(peers[i : i+4]) 173 | if ok { 174 | port := 256*uint16(peers[i+4]) + 175 | uint16(peers[i+5]) 176 | f(netip.AddrPortFrom(ip, port)) 177 | } 178 | } 179 | } else { 180 | // original format 181 | var peers []peer 182 | err = bencode.DecodeBytes(reply.Peers, &peers) 183 | if err == nil { 184 | for _, p := range peers { 185 | ip, err := netip.ParseAddr(p.IP) 186 | if err == nil { 187 | f(netip.AddrPortFrom(ip, p.Port)) 188 | } 189 | } 190 | } 191 | } 192 | 193 | if len(reply.Peers6)%18 == 0 { 194 | // peers6 is always in compact format 195 | for i := 0; i < len(reply.Peers6); i += 18 { 196 | ip, ok := netip.AddrFromSlice(reply.Peers6[i : i+16]) 197 | if ok { 198 | port := 256*uint16(reply.Peers6[i+16]) + 199 | uint16(reply.Peers6[i+17]) 200 | f(netip.AddrPortFrom(ip, port)) 201 | } 202 | } 203 | } 204 | return reply.Interval, nil 205 | } 206 | -------------------------------------------------------------------------------- /protocol/handshake.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net" 8 | "time" 9 | 10 | "github.com/jech/storrent/crypto" 11 | "github.com/jech/storrent/hash" 12 | ) 13 | 14 | // HandshakeResult is the result of a BitTorrent handshake 15 | type HandshakeResult struct { 16 | // the torrent hash 17 | Hash hash.Hash 18 | // the peer's id 19 | Id hash.Hash 20 | // supported extensions 21 | Dht, Fast, Extended bool 22 | } 23 | 24 | var header = []byte{19, 25 | 0x42, 0x69, 0x74, 0x54, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 26 | 0x20, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c} 27 | 28 | func handshake(infoHash hash.Hash, myid hash.Hash) []byte { 29 | header := []byte{19, 30 | 0x42, 0x69, 0x74, 0x54, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 31 | 0x20, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c} 32 | reserved := []byte{0, 0, 0, 0, 0, 0x10, 0, 0x05} 33 | hs := make([]byte, 0, 20+8+20+20) 34 | hs = append(hs, header...) 35 | hs = append(hs, reserved...) 36 | hs = append(hs, infoHash...) 37 | hs = append(hs, myid...) 38 | return hs 39 | } 40 | 41 | var ErrBadHandshake = errors.New("bad handshake") 42 | var ErrUnknownTorrent = errors.New("unknown torrent") 43 | 44 | func readMore(conn net.Conn, buf []byte, n int, m int) ([]byte, error) { 45 | if m < n { 46 | m = n 47 | } 48 | l := len(buf) 49 | if l >= n { 50 | return buf, nil 51 | } 52 | 53 | buf = append(buf, make([]byte, m-l)...) 54 | k, err := io.ReadAtLeast(conn, buf[l:], n-l) 55 | buf = buf[:l+k] 56 | return buf, err 57 | } 58 | 59 | // ClientHandshake performs the client side of a BitTorrent handshake. 60 | func ClientHandshake(c net.Conn, cryptoHandshake bool, infoHash hash.Hash, myid hash.Hash, cryptoOptions *crypto.Options) (conn net.Conn, result HandshakeResult, init []byte, err error) { 61 | conn = c 62 | 63 | err = conn.SetDeadline(time.Now().Add(30 * time.Second)) 64 | if err != nil { 65 | return 66 | } 67 | 68 | hshk := handshake(infoHash, myid) 69 | 70 | var buf []byte 71 | if cryptoHandshake { 72 | conn, buf, err = crypto.ClientHandshake( 73 | conn, infoHash, hshk, cryptoOptions) 74 | if err != nil { 75 | return 76 | } 77 | } else { 78 | _, err = conn.Write(hshk) 79 | if err != nil { 80 | return 81 | } 82 | } 83 | 84 | buf, err = readMore(conn, buf, 20+8+20+20, 0) 85 | if err != nil { 86 | return 87 | } 88 | 89 | if !bytes.Equal(buf[:20], header) { 90 | err = ErrBadHandshake 91 | return 92 | } 93 | 94 | buf = buf[20:] 95 | 96 | if buf[7]&0x01 != 0 { 97 | result.Dht = true 98 | } 99 | if buf[7]&0x04 != 0 { 100 | result.Fast = true 101 | } 102 | if buf[5]&0x10 != 0 { 103 | result.Extended = true 104 | } 105 | 106 | buf = buf[8:] 107 | 108 | if !bytes.Equal(buf[:20], infoHash) { 109 | err = errors.New("unexpected infoHash") 110 | return 111 | } 112 | result.Hash = make([]byte, 20) 113 | copy(result.Hash, buf) 114 | 115 | buf = buf[20:] 116 | 117 | result.Id = make([]byte, 20) 118 | copy(result.Id, buf) 119 | 120 | buf = buf[20:] 121 | 122 | if len(buf) > 0 { 123 | init = make([]byte, len(buf)) 124 | copy(init, buf) 125 | } 126 | 127 | return 128 | } 129 | 130 | func checkHeader(buf []byte) bool { 131 | if len(buf) < len(header) { 132 | return false 133 | } 134 | 135 | for i, v := range header { 136 | if buf[i] != v { 137 | return false 138 | } 139 | } 140 | return true 141 | } 142 | 143 | // ServerHandshake performs the server side of a BitTorrent handshake. 144 | func ServerHandshake(c net.Conn, hashes []hash.HashPair, cryptoOptions *crypto.Options) (conn net.Conn, result HandshakeResult, init []byte, err error) { 145 | conn = c 146 | err = conn.SetDeadline(time.Now().Add(90 * time.Second)) 147 | if err != nil { 148 | return 149 | } 150 | buf := make([]byte, 20+8+20+20) 151 | var n int 152 | n, err = io.ReadAtLeast(conn, buf, len(header)) 153 | if err != nil { 154 | return 155 | } 156 | 157 | buf = buf[:n] 158 | 159 | ok := checkHeader(buf) 160 | if ok && cryptoOptions.ForceCryptoHandshake { 161 | err = errors.New("plaintext handshake forbidden") 162 | return 163 | } 164 | 165 | var skey hash.Hash 166 | if !ok && cryptoOptions.AllowCryptoHandshake { 167 | skeys := make([][]byte, len(hashes)) 168 | for i := range hashes { 169 | skeys[i] = hashes[i].First 170 | } 171 | var ia []byte 172 | conn, skey, ia, err = 173 | crypto.ServerHandshake(conn, buf, skeys, cryptoOptions) 174 | if err != nil { 175 | return 176 | } 177 | buf = ia 178 | buf, err = readMore(conn, buf, 20, 20+8+20+20) 179 | if err != nil { 180 | return 181 | } 182 | ok = checkHeader(buf) 183 | } 184 | 185 | if !ok { 186 | err = ErrBadHandshake 187 | return 188 | } 189 | 190 | buf = buf[20:] 191 | buf, err = readMore(conn, buf, 8+20, 8+20+20) 192 | if err != nil { 193 | return 194 | } 195 | 196 | if buf[7]&0x01 != 0 { 197 | result.Dht = true 198 | } 199 | if buf[7]&0x04 != 0 { 200 | result.Fast = true 201 | } 202 | if buf[5]&0x10 != 0 { 203 | result.Extended = true 204 | } 205 | buf = buf[8:] 206 | 207 | hsh := hash.Hash(make([]byte, 20)) 208 | copy(hsh, buf) 209 | buf = buf[20:] 210 | 211 | if skey != nil && !skey.Equal(hsh) { 212 | err = errors.New("crypto hash mismatch") 213 | return 214 | } 215 | 216 | found := false 217 | var h hash.HashPair 218 | for _, h = range hashes { 219 | if hsh.Equal(h.First) { 220 | found = true 221 | break 222 | } 223 | } 224 | if !found { 225 | err = ErrUnknownTorrent 226 | return 227 | } 228 | 229 | result.Hash = hsh 230 | 231 | hndshk := handshake(result.Hash, h.Second) 232 | _, err = conn.Write(hndshk) 233 | if err != nil { 234 | return 235 | } 236 | 237 | buf, err = readMore(conn, buf, 20, 0) 238 | if err != nil { 239 | return 240 | } 241 | result.Id = make([]byte, 20) 242 | copy(result.Id, buf) 243 | buf = buf[20:] 244 | 245 | if len(buf) > 0 { 246 | init = make([]byte, len(buf)) 247 | copy(init, buf) 248 | } 249 | 250 | return 251 | } 252 | -------------------------------------------------------------------------------- /peer/requests/requests.go: -------------------------------------------------------------------------------- 1 | // Package requests implements a queue of pending requests. It is 2 | // optimised for frequent membership checks. 3 | package requests 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/jech/storrent/bitmap" 11 | ) 12 | 13 | // Request represents an outgoing request. 14 | type Request struct { 15 | index uint32 16 | qtime time.Time // queue time 17 | rtime time.Time // request time, zero if not requested yet 18 | ctime time.Time // cancel time, zero if not cancelled 19 | } 20 | 21 | // Cancel marks a request as cancelled. 22 | func (r *Request) Cancel() { 23 | r.ctime = time.Now() 24 | } 25 | 26 | // Cancelled returns true if the request was cancelled. 27 | func (r Request) Cancelled() bool { 28 | return !r.ctime.Equal(time.Time{}) 29 | } 30 | 31 | func (r Request) String() string { 32 | c := "" 33 | if r.Cancelled() { 34 | c = ", cancelled" 35 | } 36 | return fmt.Sprintf("[%v at %v,%v%v]", r.index, r.qtime, r.rtime, c) 37 | } 38 | 39 | // Requests represents a request queue. 40 | type Requests struct { 41 | queue []Request // unsent requests 42 | requested []Request // sent requests 43 | bitmap bitmap.Bitmap // membership 44 | } 45 | 46 | func (rs *Requests) String() string { 47 | var b = new(strings.Builder) 48 | 49 | fmt.Fprintf(b, "[[") 50 | for _, v := range rs.queue { 51 | fmt.Fprintf(b, "%v,", v.index) 52 | } 53 | fmt.Fprintf(b, "],[") 54 | for _, v := range rs.requested { 55 | fmt.Fprintf(b, "%v,", v.index) 56 | } 57 | fmt.Fprintf(b, "]]") 58 | return b.String() 59 | } 60 | 61 | // Queue returns the number of requests queued but not sent out. 62 | func (rs *Requests) Queue() int { 63 | return len(rs.queue) 64 | } 65 | 66 | // Requested returns the number of requests sent out. 67 | func (rs *Requests) Requested() int { 68 | return len(rs.requested) 69 | } 70 | 71 | // Cancel cancels a request that has been sent out. It returns two 72 | // booleans: the first one indicates if the request was found, the second 73 | // one indicates whether a Cancel should be sent out. 74 | func (rs *Requests) Cancel(index uint32) (bool, bool) { 75 | if !rs.bitmap.Get(int(index)) { 76 | return false, false 77 | } 78 | for i, r := range rs.requested { 79 | if index == r.index { 80 | cancelled := r.Cancelled() 81 | if !cancelled { 82 | rs.requested[i].Cancel() 83 | } 84 | return true, !cancelled 85 | } 86 | } 87 | return false, false 88 | } 89 | 90 | func (rs *Requests) del(index uint32, reqonly bool) (bool, bool, time.Time) { 91 | if !rs.bitmap.Get(int(index)) { 92 | return false, false, time.Time{} 93 | } 94 | for i, r := range rs.requested { 95 | if index == r.index { 96 | l := len(rs.requested) 97 | rr := rs.requested[i] 98 | if l == 1 { 99 | rs.requested = nil 100 | } else { 101 | rs.requested[i] = rs.requested[l-1] 102 | rs.requested = rs.requested[:l-1] 103 | } 104 | if len(rs.requested) == 0 { 105 | rs.requested = nil 106 | } 107 | rs.bitmap.Reset(int(index)) 108 | return false, true, rr.rtime 109 | } 110 | } 111 | if !reqonly { 112 | for i, q := range rs.queue { 113 | if index == q.index { 114 | l := len(rs.queue) 115 | if l == 1 { 116 | rs.queue = nil 117 | } else { 118 | rs.queue[i] = rs.queue[l-1] 119 | rs.queue = rs.queue[:l-1] 120 | } 121 | rs.bitmap.Reset(int(index)) 122 | return true, false, time.Time{} 123 | } 124 | } 125 | } 126 | if !reqonly { 127 | panic("Requests is broken!") 128 | } 129 | return false, false, time.Time{} 130 | } 131 | 132 | // Del deletes a request, whether it was sent out or not. 133 | func (rs *Requests) Del(index uint32) (bool, bool, time.Time) { 134 | return rs.del(index, false) 135 | } 136 | 137 | // DelRequested deletes a request only if it has been sent out. 138 | func (rs *Requests) DelRequested(index uint32) bool { 139 | _, r, _ := rs.del(index, true) 140 | return r 141 | } 142 | 143 | // Enqueue enqueues a new request. It returns false if the request is 144 | // a duplicate. 145 | func (rs *Requests) Enqueue(index uint32) bool { 146 | if rs.bitmap.Get(int(index)) { 147 | return false 148 | } 149 | r := Request{index: index, qtime: time.Now()} 150 | rs.queue = append(rs.queue, r) 151 | rs.bitmap.Set(int(index)) 152 | return true 153 | } 154 | 155 | // Dequeue returns the next request that should be sent out. 156 | func (rs *Requests) Dequeue() (request Request, index uint32) { 157 | request = rs.queue[0] 158 | index = request.index 159 | rs.bitmap.Reset(int(request.index)) 160 | rs.queue = rs.queue[1:] 161 | if len(rs.queue) == 0 { 162 | rs.queue = nil 163 | } 164 | return 165 | } 166 | 167 | // Enqueue enqueues a request that has been sent out. 168 | func (rs *Requests) EnqueueRequest(r Request) { 169 | if rs.bitmap.Get(int(r.index)) { 170 | panic("Incorrect use of Requests.EnqueueRequest") 171 | } 172 | r.ctime = time.Time{} 173 | rs.bitmap.Set(int(r.index)) 174 | r.rtime = time.Now() 175 | rs.requested = append(rs.requested, r) 176 | } 177 | 178 | // Clear cancels all queued request. It calls the given function for all 179 | // requests that have already been sent out. 180 | func (rs *Requests) Clear(both bool, f func(uint32)) { 181 | oldr := rs.requested 182 | oldq := rs.queue 183 | rs.queue = nil 184 | rs.requested = nil 185 | rs.bitmap = nil 186 | if !both { 187 | rs.requested = oldr 188 | for _, r := range oldr { 189 | rs.bitmap.Set(int(r.index)) 190 | } 191 | } else { 192 | for _, r := range oldr { 193 | f(r.index) 194 | } 195 | } 196 | for _, q := range oldq { 197 | f(q.index) 198 | } 199 | } 200 | 201 | // Expire clears all requests that have been cancelled before time t1 or 202 | // sent out before time t0. 203 | func (rs *Requests) Expire(t0, t1 time.Time, 204 | drop func(index uint32), 205 | cancel func(index uint32)) bool { 206 | 207 | dropped := false 208 | 209 | i := 0 210 | for i < len(rs.requested) { 211 | r := rs.requested[i] 212 | if r.Cancelled() && r.ctime.Before(t1) { 213 | found := rs.DelRequested(r.index) 214 | if !found { 215 | panic("Couldn't delete request") 216 | } 217 | drop(r.index) 218 | dropped = true 219 | // don't increment i 220 | continue 221 | } else if !r.Cancelled() && r.rtime.Before(t0) { 222 | rs.requested[i].Cancel() 223 | cancel(r.index) 224 | } 225 | i++ 226 | } 227 | return dropped 228 | } 229 | -------------------------------------------------------------------------------- /rundht/rundht.go: -------------------------------------------------------------------------------- 1 | //go:build cgo 2 | // +build cgo 3 | 4 | // Package rundht implements the interface between storrent and the DHT. 5 | // The DHT implementation itself is in package dht. 6 | package rundht 7 | 8 | import ( 9 | "context" 10 | "flag" 11 | "log" 12 | "math/rand/v2" 13 | "net" 14 | "net/netip" 15 | "os" 16 | "path/filepath" 17 | "time" 18 | 19 | "github.com/zeebo/bencode" 20 | 21 | "github.com/jech/storrent/config" 22 | "github.com/jech/storrent/dht" 23 | "github.com/jech/storrent/hash" 24 | "github.com/jech/storrent/known" 25 | "github.com/jech/storrent/pex" 26 | "github.com/jech/storrent/tor" 27 | ) 28 | 29 | func init() { 30 | dhtFile := "" 31 | configDir, err := os.UserConfigDir() 32 | if err != nil { 33 | log.Printf("Couldn't determine config dir: %v", err) 34 | } else { 35 | dhtFile = filepath.Join( 36 | filepath.Join(configDir, "storrent"), 37 | "dht.dat", 38 | ) 39 | } 40 | 41 | flag.StringVar(&config.DHTBootstrap, "dht-bootstrap", dhtFile, 42 | "DHT bootstrap `filename`") 43 | } 44 | 45 | // BDHT represents the contents of the dht.dat file. 46 | type BDHT struct { 47 | Id []byte `bencode:"id,omitempty"` 48 | Nodes []byte `bencode:"nodes,omitempty"` 49 | Nodes6 []byte `bencode:"nodes6,omitempty"` 50 | } 51 | 52 | func parseCompact(data []byte, ipv6 bool) []netip.AddrPort { 53 | peers := pex.ParseCompact(data, nil, ipv6) 54 | addrs := make([]netip.AddrPort, len(peers)) 55 | for i, p := range peers { 56 | addrs[i] = p.Addr 57 | } 58 | return addrs 59 | } 60 | 61 | func formatCompact(addrs []netip.AddrPort) ([]byte, []byte) { 62 | peers := make([]pex.Peer, len(addrs)) 63 | for i, a := range addrs { 64 | peers[i].Addr = a 65 | } 66 | n4, _, n6, _ := pex.FormatCompact(peers) 67 | return n4, n6 68 | } 69 | 70 | func read(filename string) (bdht *BDHT, info os.FileInfo, err error) { 71 | var r *os.File 72 | r, err = os.Open(filename) 73 | if err != nil { 74 | return 75 | } 76 | defer r.Close() 77 | 78 | info, err = r.Stat() 79 | if err != nil { 80 | return 81 | } 82 | 83 | decoder := bencode.NewDecoder(r) 84 | err = decoder.Decode(&bdht) 85 | return 86 | } 87 | 88 | // Read reads the dht.dat file and returns a set of potential nodes. 89 | func Read(filename string) (id []byte, nodes []netip.AddrPort, err error) { 90 | bdht, info, err := read(filename) 91 | 92 | if err != nil { 93 | return 94 | } 95 | 96 | // for privacy reasons, don't honour an old id. 97 | if time.Since(info.ModTime()) < time.Hour && len(bdht.Id) == 20 { 98 | id = bdht.Id 99 | } 100 | if bdht.Nodes != nil { 101 | nodes = append(nodes, parseCompact(bdht.Nodes, false)...) 102 | } 103 | if bdht.Nodes6 != nil { 104 | nodes = append(nodes, parseCompact(bdht.Nodes6, true)...) 105 | } 106 | return 107 | } 108 | 109 | // Write writes the dht.dat file. 110 | func Write(filename string, id []byte) error { 111 | addrs, err := dht.GetNodes() 112 | if err != nil { 113 | return err 114 | } 115 | if len(addrs) < 8 { 116 | return nil 117 | } 118 | var bdht BDHT 119 | if len(id) == 20 { 120 | bdht.Id = id 121 | } 122 | bdht.Nodes, bdht.Nodes6 = formatCompact(addrs) 123 | 124 | w, err := os.Create(filename) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | encoder := bencode.NewEncoder(w) 130 | err = encoder.Encode(&bdht) 131 | err2 := w.Close() 132 | if err == nil { 133 | err = err2 134 | } 135 | if err != nil { 136 | os.Remove(filename) 137 | return err 138 | } 139 | return nil 140 | } 141 | 142 | // Bootstrap bootstraps the DHT. 143 | func Bootstrap(ctx context.Context, nodes []netip.AddrPort) { 144 | bootstrap := []string{"dht.transmissionbt.com", "router.bittorrent.com"} 145 | 146 | reannounced4 := false 147 | reannounced6 := false 148 | reannounce := func(ipv6 bool) { 149 | reannounced := reannounced4 150 | if ipv6 { 151 | reannounced = reannounced6 152 | } 153 | if !reannounced { 154 | tor.Range( 155 | func(h hash.Hash, t *tor.Torrent) bool { 156 | tor.Announce(h, ipv6) 157 | return true 158 | }) 159 | } 160 | if !ipv6 { 161 | reannounced4 = true 162 | } else { 163 | reannounced6 = true 164 | } 165 | } 166 | 167 | log.Printf("Bootstrapping DHT from %v nodes", len(nodes)) 168 | defer func() { 169 | g4, g6, d4, d6, _, _ := dht.Count() 170 | log.Printf("DHT bootstrapped (%v/%v %v/%v confirmed nodes)", 171 | g4, g4+d4, g6, g6+d6) 172 | }() 173 | 174 | ni := rand.Perm(len(nodes)) 175 | 176 | for i := 0; i < len(nodes)*3; i++ { 177 | c4, c6, _, _, _, _ := dht.Count() 178 | if c4 >= 9 { 179 | reannounce(false) 180 | } 181 | if c6 >= 9 { 182 | reannounce(true) 183 | } 184 | if reannounced4 && reannounced6 && i >= len(nodes) { 185 | return 186 | } 187 | dht.Ping(nodes[ni[i%len(nodes)]]) 188 | if i < 16 && i < len(nodes) { 189 | doze() 190 | } else { 191 | nap(2, 1) 192 | } 193 | if ctx.Err() != nil { 194 | return 195 | } 196 | } 197 | 198 | nodes = nil 199 | ni = nil 200 | 201 | for _, name := range bootstrap { 202 | ips, err := net.LookupIP(name) 203 | if err != nil { 204 | log.Printf("Couldn't resolve %v: %v", name, err) 205 | continue 206 | } 207 | for _, ip := range ips { 208 | ipp, ok := netip.AddrFromSlice(ip) 209 | if ok { 210 | nodes = append(nodes, 211 | netip.AddrPortFrom(ipp, 6881), 212 | ) 213 | } 214 | } 215 | } 216 | 217 | log.Printf("Bootstrapping DHT from %v fallback nodes", len(nodes)) 218 | 219 | for i := 0; i < len(nodes)*3; i++ { 220 | c4, c6, _, _, _, _ := dht.Count() 221 | if c4 >= 9 { 222 | reannounce(false) 223 | } 224 | if c6 >= 9 { 225 | reannounce(true) 226 | } 227 | if reannounced4 && reannounced6 { 228 | return 229 | } 230 | 231 | dht.Ping(nodes[i%len(nodes)]) 232 | nap(2, 1) 233 | if ctx.Err() != nil { 234 | return 235 | } 236 | } 237 | reannounce(false) 238 | reannounce(true) 239 | } 240 | 241 | // Handle reacts to events sent by the DHT. 242 | func Handle(dhtevent <-chan dht.Event) { 243 | for { 244 | event, ok := <-dhtevent 245 | if !ok { 246 | return 247 | } 248 | switch event := event.(type) { 249 | case dht.ValueEvent: 250 | t := tor.Get(event.Hash) 251 | if t != nil { 252 | t.AddKnown(event.Addr, nil, "", known.DHT) 253 | } 254 | } 255 | } 256 | } 257 | 258 | // Run starts the DHT. 259 | func Run(ctx context.Context, id []byte, port int) (<-chan dht.Event, error) { 260 | return dht.DHT(ctx, id, uint16(port)) 261 | } 262 | 263 | // nap is a long doze. 264 | func nap(n int, m int) { 265 | time.Sleep(time.Duration(n-m/2)*time.Second + 266 | rand.N(time.Duration(m)*time.Second)) 267 | } 268 | 269 | // doze is a short nap. 270 | func doze() { 271 | time.Sleep(time.Millisecond + rand.N(time.Millisecond)) 272 | } 273 | -------------------------------------------------------------------------------- /bitmap/bitmap_test.go: -------------------------------------------------------------------------------- 1 | package bitmap 2 | 3 | import ( 4 | "math/rand/v2" 5 | "testing" 6 | ) 7 | 8 | func TestString(t *testing.T) { 9 | var b Bitmap 10 | for i := 0; i < 14; i++ { 11 | b.Set(i*2 + 1) 12 | } 13 | a := b.String() 14 | e := "[01010101010101010101010101010000]" 15 | if a != e { 16 | t.Errorf("Got %v, expected %v for %v", a, e, []byte(b)) 17 | } 18 | } 19 | 20 | func count(a []bool) int { 21 | count := 0 22 | for _, v := range a { 23 | if v { 24 | count++ 25 | } 26 | } 27 | return count 28 | } 29 | 30 | func TestBitmap(t *testing.T) { 31 | for i := 0; i < 100; i++ { 32 | a := make([]bool, 1000) 33 | b := New(1000) 34 | 35 | if !b.Empty() { 36 | t.Errorf("Empty failed") 37 | } 38 | if b.Count() != 0 { 39 | t.Errorf("Count failed") 40 | } 41 | for i := 0; i < 500; i++ { 42 | index := rand.IntN(1000) 43 | if rand.IntN(3) == 0 { 44 | a[index] = true 45 | b.Set(index) 46 | if b.Empty() { 47 | t.Errorf("Empty failed") 48 | } 49 | } else { 50 | a[index] = false 51 | b.Reset(index) 52 | } 53 | if b.Count() != count(a) { 54 | t.Errorf("Count failed: got %v, expected %v", 55 | b.Count(), count(a)) 56 | } 57 | } 58 | for i := 0; i < 1000; i++ { 59 | if a[i] != b.Get(i) { 60 | t.Errorf("Mismatch at %v: %v != %v", 61 | i, a[i], b.Get(i)) 62 | } 63 | } 64 | c := b.Copy() 65 | for i := 0; i < 1000; i++ { 66 | if b.Get(i) != c.Get(i) { 67 | t.Errorf("Copy mismatch at %v: %v != %v", 68 | i, a[i], b.Get(i)) 69 | } 70 | } 71 | } 72 | } 73 | 74 | func TestLength(t *testing.T) { 75 | for i := 0; i < 1024; i++ { 76 | var b Bitmap 77 | b.Set(i) 78 | if len(b) != i / 8 + 1 { 79 | t.Errorf("%v: got %v", i, len(b)) 80 | } 81 | } 82 | } 83 | 84 | func TestExtend(t *testing.T) { 85 | for i := 0; i < 1024; i++ { 86 | var b Bitmap 87 | b.Extend(i) 88 | if len(b) != i / 8 + 1 { 89 | t.Errorf("%v: got %v", i, len(b)) 90 | } 91 | } 92 | } 93 | 94 | func TestLen(t *testing.T) { 95 | var b Bitmap 96 | if b.Len() != 0 { 97 | t.Errorf("Len failed, expected 0, got %v", b.Len()) 98 | } 99 | for i := 0; i < 20; i++ { 100 | b.Set(i) 101 | if b.Len() != i+1 { 102 | t.Errorf("Len failed, expected %v + 1, got %v", 103 | i, b.Len()) 104 | } 105 | } 106 | b.Reset(18) 107 | if b.Len() != 20 { 108 | t.Errorf("Len failed after Reset, expected %v, got %v", 109 | 20, b.Len()) 110 | } 111 | } 112 | 113 | func TestMultiple(t *testing.T) { 114 | var b Bitmap 115 | b.SetMultiple(47) 116 | for i := 0; i < 47; i++ { 117 | if !b.Get(i) { 118 | t.Errorf("Set Multiple failed, %v is not set", i) 119 | } 120 | } 121 | for i := 47; i < 58; i++ { 122 | if b.Get(i) { 123 | t.Errorf("Set Multiple failed, %v is set", i) 124 | } 125 | } 126 | } 127 | 128 | func TestEqualValue(t *testing.T) { 129 | var b1, b2 Bitmap 130 | if !b1.EqualValue(b2) { 131 | t.Errorf("EqualValue failed") 132 | } 133 | 134 | if !b1.EqualValue([]byte{0, 0, 0}) { 135 | t.Errorf("EqualValue failed") 136 | } 137 | if !Bitmap([]byte{0, 0, 0}).EqualValue(b1) { 138 | t.Errorf("EqualValue failed") 139 | } 140 | 141 | b1.Set(42) 142 | if b1.EqualValue(b2) { 143 | t.Errorf("EqualValue failed") 144 | } 145 | if b2.EqualValue(b1) { 146 | t.Errorf("EqualValue failed") 147 | } 148 | } 149 | 150 | func TestAll(t *testing.T) { 151 | var b Bitmap 152 | if !b.All(0) { 153 | t.Errorf("All failed: empty, 0") 154 | } 155 | if b.All(1) { 156 | t.Errorf("All failed: empty, 1") 157 | } 158 | for i := 0; i < 123; i++ { 159 | b.Set(i) 160 | if !b.All(i + 1) { 161 | t.Errorf("All failed: %v %v", b, i+1) 162 | } 163 | if b.All(i + 2) { 164 | t.Errorf("All failed: %v %v + 1", b, i+1) 165 | } 166 | } 167 | b.Reset(83) 168 | if b.All(123) { 169 | t.Errorf("All failed") 170 | } 171 | } 172 | 173 | func TestRange(t *testing.T) { 174 | var b Bitmap 175 | for i := 0; i < 123; i++ { 176 | b.Set(i * 3) 177 | } 178 | i := 0 179 | b.Range(func(index int) bool { 180 | if index != i * 3 { 181 | t.Errorf("Expected %v, got %v", i * 3, index) 182 | } 183 | i++ 184 | return true 185 | }) 186 | } 187 | 188 | const size = 0x1000 189 | const mask = 0xFFF 190 | 191 | func BenchmarkSet(b *testing.B) { 192 | var bm Bitmap 193 | 194 | var v int 195 | for n := 0; n < b.N; n++ { 196 | bm.Set(v) 197 | v = (v + 1*3) % mask 198 | } 199 | if bm.Count() > b.N { 200 | b.Errorf("Bad count") 201 | } 202 | } 203 | 204 | func BenchmarkSetReset(b *testing.B) { 205 | var bm Bitmap 206 | 207 | var v int 208 | for n := 0; n < b.N; n++ { 209 | bm.Set(v) 210 | v = (v + 1*3) % mask 211 | } 212 | v = 0 213 | for n := 0; n < b.N; n++ { 214 | bm.Reset(v) 215 | v = (v + 1*3) % mask 216 | } 217 | if bm.Count() != 0 { 218 | b.Errorf("Bad count") 219 | } 220 | } 221 | 222 | func BenchmarkGet(b *testing.B) { 223 | var bm Bitmap 224 | 225 | for i := 0; i < size/10; i++ { 226 | bm.Set(rand.IntN(size)) 227 | } 228 | 229 | b.ResetTimer() 230 | 231 | var v, count int 232 | for n := 0; n < b.N; n++ { 233 | if bm.Get(v) { 234 | count++ 235 | } 236 | v = (v + 1*3) % mask 237 | } 238 | 239 | if count > b.N { 240 | b.Errorf("Bad count") 241 | } 242 | } 243 | 244 | func BenchmarkArraySparseCount(b *testing.B) { 245 | a := make([]bool, size) 246 | 247 | for i := 0; i < size/10; i++ { 248 | a[rand.IntN(size)] = true 249 | } 250 | 251 | b.ResetTimer() 252 | 253 | for n := 0; n < b.N; n++ { 254 | if count(a) > size/10 { 255 | b.Errorf("Bad count") 256 | } 257 | } 258 | } 259 | 260 | func BenchmarkSparseCount(b *testing.B) { 261 | var bm Bitmap 262 | 263 | for i := 0; i < size/10; i++ { 264 | bm.Set(rand.IntN(size)) 265 | } 266 | 267 | b.ResetTimer() 268 | 269 | for n := 0; n < b.N; n++ { 270 | if bm.Count() > size/10 { 271 | b.Errorf("Bad count") 272 | } 273 | } 274 | } 275 | 276 | func BenchmarkArrayDenseCount(b *testing.B) { 277 | a := make([]bool, size) 278 | 279 | for i := 0; i < size; i++ { 280 | if i%8 != 7 { 281 | a[i] = true 282 | } 283 | } 284 | 285 | b.ResetTimer() 286 | 287 | for n := 0; n < b.N; n++ { 288 | if count(a) != size-size/8 { 289 | b.Errorf("Bad count") 290 | } 291 | } 292 | } 293 | 294 | func BenchmarkDenseCount(b *testing.B) { 295 | var bm Bitmap 296 | 297 | for i := 0; i < size; i++ { 298 | if i%8 != 7 { 299 | bm.Set(i) 300 | } 301 | } 302 | 303 | b.ResetTimer() 304 | 305 | for n := 0; n < b.N; n++ { 306 | if bm.Count() != size-size/8 { 307 | b.Errorf("Bad count") 308 | } 309 | } 310 | } 311 | 312 | func BenchmarkArrayFullCount(b *testing.B) { 313 | a := make([]bool, size) 314 | 315 | for i := 0; i < size; i++ { 316 | a[i] = true 317 | } 318 | 319 | b.ResetTimer() 320 | 321 | for n := 0; n < b.N; n++ { 322 | if count(a) != size { 323 | b.Errorf("Bad count") 324 | } 325 | } 326 | } 327 | 328 | func BenchmarkFullCount(b *testing.B) { 329 | var bm Bitmap 330 | 331 | for i := 0; i < size; i++ { 332 | bm.Set(i) 333 | } 334 | 335 | b.ResetTimer() 336 | 337 | for n := 0; n < b.N; n++ { 338 | if bm.Count() != size { 339 | b.Errorf("Bad count") 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /tracker/udp.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "errors" 8 | "io" 9 | "math/rand/v2" 10 | "net" 11 | "net/netip" 12 | nurl "net/url" 13 | "sync" 14 | "time" 15 | 16 | "golang.org/x/net/proxy" 17 | ) 18 | 19 | // UDP represents a tracker using the protocol defined in BEP-15. 20 | type UDP struct { 21 | base 22 | } 23 | 24 | // Announce performs a UDP announce over IPv4 and Ipv6 in parallel. 25 | func (tracker *UDP) Announce(ctx context.Context, hash []byte, myid []byte, 26 | want int, size int64, port4, port6 int, proxy string, 27 | f func(netip.AddrPort) bool) error { 28 | ok := tracker.tryLock() 29 | if !ok { 30 | return ErrNotReady 31 | } 32 | defer tracker.unlock() 33 | 34 | if !tracker.ready() { 35 | return ErrNotReady 36 | } 37 | 38 | url, err := nurl.Parse(tracker.url) 39 | if err != nil { 40 | tracker.updateInterval(0, err) 41 | return err 42 | } 43 | 44 | tracker.time = time.Now() 45 | 46 | var i4, i6 time.Duration 47 | var e4, e6 error 48 | var wg sync.WaitGroup 49 | wg.Add(2) 50 | go func() { 51 | i4, e4 = announceUDP(ctx, "udp4", f, 52 | url, hash, myid, want, size, port4, proxy) 53 | wg.Done() 54 | }() 55 | go func() { 56 | i6, e6 = announceUDP(ctx, "udp6", f, 57 | url, hash, myid, want, size, port6, proxy) 58 | wg.Done() 59 | }() 60 | wg.Wait() 61 | if e4 != nil && e6 != nil { 62 | err = e4 63 | } 64 | interval := i4 65 | if interval < i6 { 66 | interval = i6 67 | } 68 | 69 | tracker.updateInterval(interval, err) 70 | return err 71 | } 72 | 73 | func announceUDP(ctx context.Context, prot string, 74 | f func(addr netip.AddrPort) bool, url *nurl.URL, hash, myid []byte, 75 | want int, size int64, port int, prox string) (time.Duration, error) { 76 | 77 | var conn net.Conn 78 | var err error 79 | 80 | if prox == "" { 81 | var dialer net.Dialer 82 | conn, err = dialer.DialContext(ctx, prot, 83 | net.JoinHostPort(url.Hostname(), url.Port())) 84 | } else { 85 | var u *nurl.URL 86 | u, err = url.Parse(prox) 87 | if err != nil { 88 | return 0, err 89 | } 90 | var dialer proxy.Dialer 91 | dialer, err = proxy.FromURL(u, proxy.Direct) 92 | if err != nil { 93 | return 0, err 94 | } 95 | conn, err = dialer.Dial(prot, 96 | net.JoinHostPort(url.Hostname(), url.Port())) 97 | } 98 | 99 | if err != nil { 100 | return 0, err 101 | } 102 | 103 | defer conn.Close() 104 | 105 | w := new(bytes.Buffer) 106 | err = binary.Write(w, binary.BigEndian, uint64(0x41727101980)) 107 | if err != nil { 108 | return 0, err 109 | } 110 | err = binary.Write(w, binary.BigEndian, uint32(0)) 111 | if err != nil { 112 | return 0, err 113 | } 114 | 115 | tid := rand.Uint32() 116 | err = binary.Write(w, binary.BigEndian, tid) 117 | if err != nil { 118 | return 0, err 119 | } 120 | 121 | r, err := udpRequestReply(ctx, conn, w.Bytes(), 16, 0, tid) 122 | if err != nil { 123 | return 0, err 124 | } 125 | 126 | var cid uint64 127 | err = binary.Read(r, binary.BigEndian, &cid) 128 | if err != nil { 129 | return 0, err 130 | } 131 | 132 | w = new(bytes.Buffer) 133 | err = binary.Write(w, binary.BigEndian, cid) 134 | if err != nil { 135 | return 0, err 136 | } 137 | err = binary.Write(w, binary.BigEndian, uint32(1)) // action 138 | if err != nil { 139 | return 0, err 140 | } 141 | tid = rand.Uint32() 142 | err = binary.Write(w, binary.BigEndian, tid) 143 | if err != nil { 144 | return 0, err 145 | } 146 | _, err = w.Write(hash) 147 | if err != nil { 148 | return 0, err 149 | } 150 | _, err = w.Write(myid) 151 | if err != nil { 152 | return 0, err 153 | } 154 | err = binary.Write(w, binary.BigEndian, uint64(size/2)) // downloaded 155 | if err != nil { 156 | return 0, err 157 | } 158 | err = binary.Write(w, binary.BigEndian, int64(size/2)) // left 159 | if err != nil { 160 | return 0, err 161 | } 162 | err = binary.Write(w, binary.BigEndian, uint64(2*size)) // uploaded 163 | if err != nil { 164 | return 0, err 165 | } 166 | err = binary.Write(w, binary.BigEndian, uint32(0)) // event 167 | if err != nil { 168 | return 0, err 169 | } 170 | err = binary.Write(w, binary.BigEndian, uint32(0)) // IP 171 | if err != nil { 172 | return 0, err 173 | } 174 | err = binary.Write(w, binary.BigEndian, uint32(0)) // key 175 | if err != nil { 176 | return 0, err 177 | } 178 | err = binary.Write(w, binary.BigEndian, int32(want)) // num_want 179 | if err != nil { 180 | return 0, err 181 | } 182 | 183 | err = binary.Write(w, binary.BigEndian, uint16(port)) // port 184 | if err != nil { 185 | return 0, err 186 | } 187 | 188 | r, err = udpRequestReply(ctx, conn, w.Bytes(), 20, 1, tid) 189 | if err != nil { 190 | return 0, err 191 | } 192 | 193 | var intvl, leechers, seeders uint32 194 | err = binary.Read(r, binary.BigEndian, &intvl) 195 | if err != nil { 196 | return 0, err 197 | } 198 | interval := time.Duration(intvl) * time.Second 199 | 200 | err = binary.Read(r, binary.BigEndian, &leechers) 201 | if err != nil { 202 | return 0, err 203 | } 204 | err = binary.Read(r, binary.BigEndian, &seeders) 205 | if err != nil { 206 | return 0, err 207 | } 208 | 209 | var len int 210 | switch prot { 211 | case "udp4": 212 | len = 4 213 | case "udp6": 214 | len = 16 215 | default: 216 | panic("Eek") 217 | } 218 | buf := make([]byte, len+2) 219 | for { 220 | _, err = io.ReadFull(r, buf) 221 | if err != nil { 222 | break 223 | } 224 | ip, ok := netip.AddrFromSlice(buf[:len]) 225 | if ok { 226 | port := 256*uint16(buf[len]) + uint16(buf[len+1]) 227 | f(netip.AddrPortFrom(ip, port)) 228 | } 229 | } 230 | 231 | if err == io.EOF { 232 | err = nil 233 | } 234 | 235 | return interval, err 236 | } 237 | 238 | // udpRequestReply sends a UDP request and waits for a reply. If no reply 239 | // is received, it resends the request with exponential backoff up to four 240 | // times. 241 | func udpRequestReply(ctx context.Context, conn net.Conn, request []byte, 242 | min int, action uint32, tid uint32) (*bytes.Reader, error) { 243 | var err error 244 | timeout := 5 * time.Second 245 | for i := 0; i < 4; i++ { 246 | if ctx.Err() != nil { 247 | return nil, ctx.Err() 248 | } 249 | 250 | err = conn.SetDeadline(time.Now().Add(timeout)) 251 | if err != nil { 252 | return nil, err 253 | } 254 | timeout *= 2 255 | _, err = conn.Write(request) 256 | if err != nil { 257 | continue 258 | } 259 | 260 | if ctx.Err() != nil { 261 | return nil, ctx.Err() 262 | } 263 | 264 | buf := make([]byte, 4096) 265 | var n int 266 | n, err = conn.Read(buf) 267 | if err == nil && n < min { 268 | err = ErrParse 269 | } 270 | if err != nil { 271 | continue 272 | } 273 | 274 | r := bytes.NewReader(buf[:n]) 275 | var a, t uint32 276 | 277 | err = binary.Read(r, binary.BigEndian, &a) 278 | if err != nil { 279 | continue 280 | } 281 | err = binary.Read(r, binary.BigEndian, &t) 282 | if err != nil { 283 | continue 284 | } 285 | 286 | if t != tid { 287 | continue 288 | } 289 | 290 | if a == 3 { 291 | message, err := io.ReadAll(r) 292 | if err != nil { 293 | return nil, err 294 | } 295 | return nil, errors.New(string(message)) 296 | } 297 | 298 | if a != action { 299 | return nil, errors.New("action mismatch") 300 | } 301 | 302 | return r, nil 303 | } 304 | if err == nil { 305 | panic("eek") 306 | } 307 | return nil, err 308 | } 309 | -------------------------------------------------------------------------------- /crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | "sync" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var options1 = &Options{ 13 | AllowCryptoHandshake: true, 14 | AllowEncryption: true, 15 | ForceEncryption: true, 16 | } 17 | 18 | var options0 = &Options{ 19 | AllowCryptoHandshake: true, 20 | AllowEncryption: false, 21 | ForceEncryption: false, 22 | } 23 | 24 | func testHandshake(t *testing.T, options *Options) { 25 | client, server := net.Pipe() 26 | 27 | clientIa := make([]byte, 68) 28 | for i := range clientIa { 29 | clientIa[i] = byte(i) 30 | } 31 | 32 | clientSkey := make([]byte, 20) 33 | for i := range clientSkey { 34 | clientSkey[i] = byte(i) 35 | } 36 | 37 | clientData := make([]byte, 5123) 38 | for i := range clientData { 39 | clientData[i] = byte(i) 40 | } 41 | 42 | serverData := make([]byte, 6385) 43 | for i := range serverData { 44 | serverData[i] = byte(42 - i) 45 | } 46 | 47 | var wg sync.WaitGroup 48 | wg.Add(1) 49 | 50 | go func() { 51 | defer wg.Done() 52 | client.SetDeadline(time.Now().Add(5 * time.Second)) 53 | eclient, buf, err := 54 | ClientHandshake(client, clientSkey, clientIa, options) 55 | if err != nil { 56 | t.Errorf("ClientHandshake: %v", err) 57 | client.Close() 58 | return 59 | } 60 | defer eclient.Close() 61 | n, err := eclient.Write(clientData) 62 | if err != nil || n != len(clientData) { 63 | t.Errorf("Client Write: %v %v", n, err) 64 | return 65 | } 66 | data := make([]byte, len(serverData)-len(buf)) 67 | n, err = io.ReadFull(eclient, data[len(buf):]) 68 | if err != nil || n != len(data) { 69 | t.Errorf("Client Read: %v %v", n, err) 70 | return 71 | } 72 | data = append(buf, data...) 73 | if !bytes.Equal(data, serverData) { 74 | t.Errorf("Client data mismatch") 75 | return 76 | } 77 | }() 78 | 79 | server.SetDeadline(time.Now().Add(5 * time.Second)) 80 | 81 | head := make([]byte, 7) 82 | _, err := io.ReadFull(server, head) 83 | if err != nil { 84 | server.Close() 85 | t.Fatalf("Read: %v", err) 86 | } 87 | eserver, skey, _, err := ServerHandshake( 88 | server, head, [][]byte{clientSkey, make([]byte, 20)}, options) 89 | if err != nil { 90 | server.Close() 91 | t.Fatalf("ServerHandshake: %v", err) 92 | } 93 | 94 | defer eserver.Close() 95 | 96 | if len(skey) != len(clientSkey) || !bytes.Equal(skey, clientSkey) { 97 | t.Fatalf("Skey mismatch: %v /= %v", skey, clientSkey) 98 | } 99 | 100 | data := make([]byte, len(clientData)) 101 | _, err = io.ReadFull(eserver, data) 102 | if err != nil { 103 | t.Fatalf("Server Read: %v", err) 104 | } 105 | if !bytes.Equal(data, clientData) { 106 | t.Fatalf("Server data mismatch") 107 | } 108 | 109 | _, err = eserver.Write(serverData) 110 | if err != nil { 111 | t.Fatalf("Server Write: %v", err) 112 | } 113 | err = eserver.Close() 114 | if err != nil { 115 | t.Fatalf("Server close: %v", err) 116 | } 117 | wg.Wait() 118 | } 119 | 120 | func TestHandshake(t *testing.T) { 121 | t.Run("encrypted", func(t *testing.T) { testHandshake(t, options1) }) 122 | t.Run("unencrypted", func(t *testing.T) { testHandshake(t, options0) }) 123 | } 124 | 125 | func BenchmarkPipe(b *testing.B) { 126 | client, server := net.Pipe() 127 | data := make([]byte, 4096) 128 | b.SetBytes(int64(len(data))) 129 | 130 | var wg sync.WaitGroup 131 | wg.Add(1) 132 | 133 | go func(count int) { 134 | defer wg.Done() 135 | defer client.Close() 136 | for i := 0; i < count; i++ { 137 | _, err := client.Write(data) 138 | if err != nil { 139 | b.Errorf("Client Write: %v", err) 140 | } 141 | } 142 | err := client.Close() 143 | if err != nil { 144 | b.Errorf("Client close: %v", err) 145 | return 146 | } 147 | }(b.N) 148 | 149 | defer server.Close() 150 | server.SetDeadline(time.Now().Add(5 * time.Second)) 151 | 152 | buf := make([]byte, 4096) 153 | for i := 0; i < b.N; i++ { 154 | _, err := io.ReadFull(server, buf) 155 | if err != nil { 156 | b.Fatalf("Server Read: %v", err) 157 | } 158 | } 159 | wg.Wait() 160 | } 161 | 162 | func BenchmarkHandshake(b *testing.B) { 163 | skey := make([]byte, 20) 164 | for i := 0; i < b.N; i++ { 165 | client, server := net.Pipe() 166 | go func() { 167 | client.SetDeadline(time.Now().Add(5 * time.Second)) 168 | eclient, buf, err := 169 | ClientHandshake(client, skey, []byte{}, options1) 170 | if err != nil { 171 | b.Errorf("ClientHandshake: %v", err) 172 | client.Close() 173 | return 174 | } 175 | defer eclient.Close() 176 | if len(buf) > 0 { 177 | b.Errorf("ClientHandshake: extra data") 178 | return 179 | } 180 | }() 181 | 182 | server.SetDeadline(time.Now().Add(5 * time.Second)) 183 | 184 | eserver, _, _, err := 185 | ServerHandshake(server, []byte{}, [][]byte{skey}, 186 | options1) 187 | if err != nil { 188 | b.Fatalf("ServerHandshake: %v", err) 189 | } 190 | buf := make([]byte, 1) 191 | n, err := eserver.Read(buf) 192 | if n != 0 || err != io.EOF { 193 | eserver.Close() 194 | b.Fatalf("server read: %v", err) 195 | } 196 | eserver.Close() 197 | } 198 | } 199 | 200 | func BenchmarkClient(b *testing.B) { 201 | client, server := net.Pipe() 202 | skey := make([]byte, 20) 203 | data := make([]byte, 4096) 204 | b.SetBytes(int64(len(data))) 205 | 206 | var wg sync.WaitGroup 207 | wg.Add(1) 208 | 209 | go func(count int) { 210 | defer wg.Done() 211 | client.SetDeadline(time.Now().Add(5 * time.Second)) 212 | eclient, buf, err := 213 | ClientHandshake(client, skey, []byte{}, options1) 214 | if err != nil { 215 | b.Errorf("ClientHandshake: %v", err) 216 | client.Close() 217 | return 218 | } 219 | defer eclient.Close() 220 | if len(buf) > 0 { 221 | b.Errorf("ClientHandshake: extra data") 222 | return 223 | } 224 | for i := 0; i < count; i++ { 225 | _, err = eclient.Write(data) 226 | if err != nil { 227 | b.Errorf("Client Write: %v", err) 228 | return 229 | } 230 | } 231 | }(b.N) 232 | 233 | server.SetDeadline(time.Now().Add(5 * time.Second)) 234 | 235 | eserver, _, _, err := 236 | ServerHandshake(server, []byte{}, [][]byte{skey}, options1) 237 | if err != nil { 238 | server.Close() 239 | b.Fatalf("ServerHandshake: %v", err) 240 | } 241 | defer eserver.Close() 242 | buf := make([]byte, 4096) 243 | for i := 0; i < b.N; i++ { 244 | _, err := io.ReadFull(eserver, buf) 245 | if err != nil { 246 | b.Fatalf("Server Read: %v", err) 247 | } 248 | } 249 | wg.Wait() 250 | } 251 | 252 | func BenchmarkServer(b *testing.B) { 253 | client, server := net.Pipe() 254 | skey := make([]byte, 20) 255 | data := make([]byte, 4096) 256 | b.SetBytes(int64(len(data))) 257 | 258 | var wg sync.WaitGroup 259 | wg.Add(1) 260 | 261 | go func(count int) { 262 | defer wg.Done() 263 | client.SetDeadline(time.Now().Add(5 * time.Second)) 264 | eclient, buf1, err := 265 | ClientHandshake(client, skey, []byte{}, options1) 266 | if err != nil { 267 | b.Errorf("ClientHandshake: %v", err) 268 | client.Close() 269 | return 270 | } 271 | defer eclient.Close() 272 | if len(buf1) > 0 { 273 | b.Errorf("ClientHandshake: extra data") 274 | return 275 | } 276 | buf := make([]byte, 4096) 277 | for i := 0; i < count; i++ { 278 | _, err := io.ReadFull(eclient, buf) 279 | if err != nil { 280 | b.Errorf("Client Read: %v", err) 281 | return 282 | } 283 | } 284 | }(b.N) 285 | 286 | server.SetDeadline(time.Now().Add(5 * time.Second)) 287 | 288 | eserver, _, _, err := 289 | ServerHandshake(server, []byte{}, [][]byte{skey}, options1) 290 | if err != nil { 291 | server.Close() 292 | b.Fatalf("ServerHandshake: %v", err) 293 | } 294 | defer eserver.Close() 295 | for i := 0; i < b.N; i++ { 296 | _, err = eserver.Write(data) 297 | if err != nil { 298 | b.Fatalf("Server Write: %v", err) 299 | } 300 | } 301 | wg.Wait() 302 | } 303 | -------------------------------------------------------------------------------- /protocol/writer.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "log" 7 | "net" 8 | "time" 9 | 10 | "github.com/zeebo/bencode" 11 | 12 | "github.com/jech/storrent/pex" 13 | ) 14 | 15 | func sendMessage(w *bufio.Writer, tpe byte, data1, data2, data3 []byte) error { 16 | buf := w.AvailableBuffer() 17 | buf = binary.BigEndian.AppendUint32( 18 | buf, uint32(len(data1)+len(data2)+len(data3)+1), 19 | ) 20 | buf = append(buf, tpe) 21 | _, err := w.Write(buf) 22 | if err != nil { 23 | return err 24 | } 25 | if data1 != nil { 26 | _, err = w.Write(data1) 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | if data2 != nil { 32 | _, err = w.Write(data2) 33 | if err != nil { 34 | return err 35 | } 36 | } 37 | if data3 != nil { 38 | _, err = w.Write(data3) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func sendMessage0(w *bufio.Writer, tpe byte) error { 47 | buf := w.AvailableBuffer() 48 | buf = binary.BigEndian.AppendUint32(buf, 1) 49 | buf = append(buf, tpe) 50 | _, err := w.Write(buf) 51 | return err 52 | } 53 | 54 | func sendMessageShort(w *bufio.Writer, tpe byte, v uint16) error { 55 | buf := w.AvailableBuffer() 56 | buf = binary.BigEndian.AppendUint32(buf, 3) 57 | buf = append(buf, tpe) 58 | buf = binary.BigEndian.AppendUint16(buf, v) 59 | _, err := w.Write(buf) 60 | return err 61 | } 62 | 63 | func sendMessage1(w *bufio.Writer, tpe byte, v uint32) error { 64 | buf := w.AvailableBuffer() 65 | buf = binary.BigEndian.AppendUint32(buf, 5) 66 | buf = append(buf, tpe) 67 | buf = binary.BigEndian.AppendUint32(buf, v) 68 | _, err := w.Write(buf) 69 | return err 70 | } 71 | 72 | func sendMessage3(w *bufio.Writer, tpe byte, v1, v2, v3 uint32) error { 73 | buf := w.AvailableBuffer() 74 | buf = binary.BigEndian.AppendUint32(buf, 13) 75 | buf = append(buf, tpe) 76 | buf = binary.BigEndian.AppendUint32(buf, v1) 77 | buf = binary.BigEndian.AppendUint32(buf, v2) 78 | buf = binary.BigEndian.AppendUint32(buf, v3) 79 | _, err := w.Write(buf) 80 | return err 81 | } 82 | 83 | func sendExtended(w *bufio.Writer, subtype byte, data1, data2 []byte) error { 84 | return sendMessage(w, 20, []byte{subtype}, data1, data2) 85 | } 86 | 87 | // Write writes a single BitTorrent message to w. If l is not nil, then 88 | // the message is logged. 89 | func Write(w *bufio.Writer, m Message, l *log.Logger) error { 90 | debugf := func(format string, v ...interface{}) { 91 | if l != nil { 92 | l.Printf(format, v...) 93 | } 94 | } 95 | switch m := m.(type) { 96 | case KeepAlive: 97 | debugf("-> KeepAlive") 98 | _, err := w.Write([]byte{0, 0, 0, 0}) 99 | return err 100 | case Choke: 101 | debugf("-> Choke") 102 | return sendMessage0(w, 0) 103 | case Unchoke: 104 | debugf("-> Unchoke") 105 | return sendMessage0(w, 1) 106 | case Interested: 107 | debugf("-> Interested") 108 | return sendMessage0(w, 2) 109 | case NotInterested: 110 | debugf("-> NotInterested") 111 | return sendMessage0(w, 3) 112 | case Have: 113 | debugf("-> Have %v", m.Index) 114 | return sendMessage1(w, 4, m.Index) 115 | case Bitfield: 116 | debugf("-> Bitfield %v", len(m.Bitfield)) 117 | return sendMessage(w, 5, m.Bitfield, nil, nil) 118 | case Request: 119 | debugf("-> Request %v %v %v", m.Index, m.Begin, m.Length) 120 | return sendMessage3(w, 6, m.Index, m.Begin, m.Length) 121 | case Piece: 122 | debugf("-> Piece %v %v %v", m.Index, m.Begin, len(m.Data)) 123 | buf := w.AvailableBuffer() 124 | buf = binary.BigEndian.AppendUint32(buf, 125 | uint32(1+8+len(m.Data))) 126 | buf = append(buf, 7) 127 | buf = binary.BigEndian.AppendUint32(buf, m.Index) 128 | buf = binary.BigEndian.AppendUint32(buf, m.Begin) 129 | _, err := w.Write(buf) 130 | if err == nil { 131 | _, err = w.Write(m.Data) 132 | } 133 | PutBuffer(m.Data) 134 | m.Data = nil 135 | return err 136 | case Cancel: 137 | debugf("-> Cancel %v %v %v", m.Index, m.Begin, m.Length) 138 | return sendMessage3(w, 8, m.Index, m.Begin, m.Length) 139 | case Port: 140 | debugf("-> Port %v", m.Port) 141 | return sendMessageShort(w, 9, m.Port) 142 | case SuggestPiece: 143 | debugf("-> SuggestPiece %v", m.Index) 144 | return sendMessage1(w, 13, m.Index) 145 | case HaveAll: 146 | debugf("-> HaveAll") 147 | return sendMessage0(w, 14) 148 | case HaveNone: 149 | debugf("-> HaveNone") 150 | return sendMessage0(w, 15) 151 | case RejectRequest: 152 | debugf("-> RejectRequest %v %v %v", 153 | m.Index, m.Begin, m.Length) 154 | return sendMessage3(w, 16, m.Index, m.Begin, m.Length) 155 | case AllowedFast: 156 | debugf("-> AllowedFast %v", m.Index) 157 | return sendMessage1(w, 17, m.Index) 158 | case Extended0: 159 | debugf("-> Extended0") 160 | var f extensionInfo 161 | f.Version = m.Version 162 | if m.IPv6.IsValid() { 163 | f.IPv6 = m.IPv6.AsSlice() 164 | } 165 | if m.IPv4.IsValid() { 166 | f.IPv4 = m.IPv4.AsSlice() 167 | } 168 | f.Port = m.Port 169 | f.ReqQ = m.ReqQ 170 | f.MetadataSize = m.MetadataSize 171 | f.Messages = m.Messages 172 | f.UploadOnly = boolOrString(m.UploadOnly) 173 | f.Encrypt = boolOrString(m.Encrypt) 174 | b, err := bencode.EncodeBytes(f) 175 | if err != nil { 176 | return err 177 | } 178 | return sendExtended(w, 0, b, nil) 179 | case ExtendedMetadata: 180 | debugf("-> ExtendedMetadata %v %v", m.Type, m.Piece) 181 | tpe := m.Type 182 | piece := m.Piece 183 | info := &metadataInfo{Type: &tpe, Piece: &piece} 184 | if m.TotalSize > 0 { 185 | totalsize := m.TotalSize 186 | info.TotalSize = &totalsize 187 | } 188 | b, err := bencode.EncodeBytes(info) 189 | if err != nil { 190 | return err 191 | } 192 | if m.Subtype == 0 { 193 | panic("ExtendedMetadata subtype is 0") 194 | } 195 | return sendExtended(w, m.Subtype, b, m.Data) 196 | case ExtendedPex: 197 | debugf("-> ExtendedPex %v %v", len(m.Added), len(m.Dropped)) 198 | a4, f4, a6, f6 := pex.FormatCompact(m.Added) 199 | d4, _, d6, _ := pex.FormatCompact(m.Dropped) 200 | info := pexInfo{ 201 | Added: a4, 202 | AddedF: f4, 203 | Added6: a6, 204 | Added6F: f6, 205 | Dropped: d4, 206 | Dropped6: d6, 207 | } 208 | b, err := bencode.EncodeBytes(info) 209 | if err != nil { 210 | return err 211 | } 212 | return sendExtended(w, m.Subtype, b, nil) 213 | case ExtendedDontHave: 214 | debugf("-> ExtendedDontHave %v", m.Index) 215 | b := make([]byte, 4) 216 | binary.BigEndian.PutUint32(b, m.Index) 217 | if m.Subtype == 0 { 218 | panic("ExtendedDontHave subtype is 0") 219 | } 220 | return sendExtended(w, m.Subtype, b, nil) 221 | default: 222 | panic("Unknown message") 223 | } 224 | } 225 | 226 | // Writer writes BitTorrent messages to conn until either conn or ch is 227 | // closed. To closes done when it's done. If l is not nil, then all 228 | // messages written are logged. 229 | func Writer(conn net.Conn, l *log.Logger, ch <-chan Message, done chan<- struct{}) error { 230 | defer close(done) 231 | 232 | w := bufio.NewWriter(conn) 233 | 234 | write := func(m Message) error { 235 | err := conn.SetWriteDeadline(time.Now().Add(time.Minute)) 236 | if err != nil { 237 | return err 238 | } 239 | return Write(w, m, l) 240 | } 241 | 242 | flush := func() error { 243 | err := conn.SetWriteDeadline(time.Now().Add(time.Minute)) 244 | if err != nil { 245 | return err 246 | } 247 | return w.Flush() 248 | } 249 | 250 | for { 251 | m, ok := <-ch 252 | if !ok { 253 | return nil 254 | } 255 | err := write(m) 256 | if err != nil { 257 | return err 258 | } 259 | inner: 260 | for { 261 | select { 262 | case m, ok := <-ch: 263 | if !ok { 264 | return nil 265 | } 266 | err := write(m) 267 | if err != nil { 268 | return err 269 | } 270 | default: 271 | break inner 272 | } 273 | } 274 | 275 | err = flush() 276 | if err != nil { 277 | return err 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /tor/piece/piece_test.go: -------------------------------------------------------------------------------- 1 | package piece 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | "sync/atomic" 7 | 8 | "github.com/jech/storrent/alloc" 9 | "github.com/jech/storrent/hash" 10 | ) 11 | 12 | var zeroChunkHash = hash.Hash([]byte{ 13 | 0x2e, 0x00, 0x0f, 0xa7, 0xe8, 0x57, 0x59, 0xc7, 0xf4, 0xc2, 14 | 0x54, 0xd4, 0xd9, 0xc3, 0x3e, 0xf4, 0x81, 0xe4, 0x59, 0xa7, 15 | }) 16 | var zeroHash = hash.Hash([]byte{ 17 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19 | }) 20 | 21 | func TestPieces(t *testing.T) { 22 | ps := &Pieces{} 23 | ps.MetadataComplete(256*1024, 10*256*1024+133*1024) 24 | defer func() { 25 | ps.Del() 26 | if a := alloc.Bytes(); a != 0 { 27 | t.Errorf("Memory leak (%v bytes)", a) 28 | } 29 | }() 30 | 31 | if !ps.Bitmap().Empty() { 32 | t.Errorf("Bitmap not empty") 33 | } 34 | if ps.Length() != 10*256*1024+133*1024 { 35 | t.Errorf("Bad length") 36 | } 37 | if ps.PieceSize() != 256*1024 { 38 | t.Errorf("Bad pice size") 39 | } 40 | if ps.PieceLength(4) != 256*1024 { 41 | t.Errorf("Bad length (inner piece)") 42 | } 43 | if ps.PieceLength(10) != 133*1024 { 44 | t.Errorf("Bad length (last piece)") 45 | } 46 | if ps.PieceLength(11) != 0 { 47 | t.Errorf("Bad length (after end)") 48 | } 49 | 50 | if !ps.PieceEmpty(4) { 51 | t.Errorf("Piece is not empty") 52 | } 53 | 54 | o, l := ps.Hole(0, 0) 55 | if o != 0 || l != 256*1024 { 56 | t.Errorf("Hole (empty): %v %v", o, l) 57 | } 58 | 59 | buf := make([]byte, 88) 60 | 61 | n, err := ps.ReadAt(buf, 42*1024) 62 | if n != 0 || err != nil { 63 | t.Errorf("ReadAt: %v %v", n, err) 64 | } 65 | 66 | n, err = ps.ReadAt(buf, ps.Length()) 67 | if n != 0 || err != io.EOF { 68 | t.Errorf("ReadAt: %v %v", n, err) 69 | } 70 | 71 | n, err = ps.ReadAt(buf, ps.Length()+42) 72 | if n != 0 || err != io.EOF { 73 | t.Errorf("ReadAt: %v %v", n, err) 74 | } 75 | 76 | zeroes := make([]byte, 256*1024) 77 | count, complete, err := ps.AddData(1, 0, zeroes[:16384], 1) 78 | if count != 16384 || complete || err != nil { 79 | t.Errorf("Adddata: %v %v %v", count, complete, err) 80 | } 81 | if ps.Complete(1) { 82 | t.Errorf("Piece is complete") 83 | } 84 | if _, bm := ps.PieceBitmap(1); bm.Count() != 1 { 85 | t.Errorf("Piece bitmap not 1") 86 | } 87 | 88 | o, l = ps.Hole(1, 0) 89 | if o != 16*1024 || l != 256*1024-16*1024 { 90 | t.Errorf("Hole (partial): %v %v", o, l) 91 | } 92 | 93 | count, complete, err = ps.AddData(1, 16384, zeroes[16384:], 2) 94 | if count != 256*1024-16384 || !complete || err != nil { 95 | t.Errorf("Adddata: %v %v %v", count, complete, err) 96 | } 97 | if ps.Complete(1) { 98 | t.Errorf("Piece is complete") 99 | } 100 | if _, bm := ps.PieceBitmap(1); bm.Count() != 256/16 { 101 | t.Errorf("Piece bitmap not %v", 256/16) 102 | } 103 | 104 | o, l = ps.Hole(1, 0) 105 | if o != ^uint32(0) || l != ^uint32(0) { 106 | t.Errorf("Hole (partial): %v %v", o, l) 107 | } 108 | 109 | n, err = ps.ReadAt(buf, 256*1024+20000) 110 | if n != 0 || err != nil { 111 | t.Errorf("ReadAt: %v %v", n, err) 112 | } 113 | 114 | done, peers, err := ps.Finalise(1, zeroChunkHash) 115 | if !done || len(peers) != 2 || err != nil { 116 | t.Errorf("Finalise: %v %v", done, err) 117 | } 118 | if !ps.Complete(1) { 119 | t.Errorf("Piece is not complete") 120 | } 121 | 122 | n, err = ps.ReadAt(buf, 256*1024+20000) 123 | if n != len(buf) || err != nil { 124 | t.Errorf("ReadAt: %v %v", n, err) 125 | } 126 | 127 | count, complete, err = ps.AddData(2, 0, zeroes, 3) 128 | if count != 256*1024 || !complete || err != nil { 129 | t.Errorf("AddData: %v %v %v", count, complete, err) 130 | } 131 | 132 | done, _, err = ps.Finalise(2, zeroHash) 133 | if done || err != ErrHashMismatch { 134 | t.Errorf("Finalise (bad): %v %v", done, err) 135 | } 136 | 137 | count, complete, err = 138 | ps.AddData(3, 16*1024, zeroes[:16*1024], 4) 139 | if count != 16*1024 || complete || err != nil { 140 | t.Errorf("AddData: %v %v %v", count, complete, err) 141 | } 142 | 143 | if b := ps.Bytes(); b != 2*256*1024 { 144 | t.Errorf("Bytes: %v", b) 145 | } 146 | 147 | ps.Expire(256*1024, nil, func(index uint32) {}) 148 | 149 | if b := ps.Bytes(); b != 256*1024 { 150 | t.Errorf("Bytes: %v", b) 151 | } 152 | 153 | ps.Del() 154 | if bytes := alloc.Bytes(); bytes != 0 { 155 | t.Errorf("%v bytes allocated", bytes) 156 | } 157 | } 158 | 159 | func BenchmarkAddDel(b *testing.B) { 160 | ps := &Pieces{} 161 | chunk := make([]byte, 16 * 1024) 162 | chunks := uint32(4096) 163 | ps.MetadataComplete(256*1024, int64(chunks) * 16 * 1024) 164 | counter := uint32(0) 165 | b.SetBytes(16 * 1024) 166 | b.RunParallel(func(pb *testing.PB) { 167 | for pb.Next() { 168 | n := (atomic.AddUint32(&counter, 1) - 1) % chunks 169 | index := n / (256 / 16) 170 | begin := (n % (256 / 16)) * 16 * 1024 171 | _, complete, err := 172 | ps.AddData(index, begin, chunk, ^uint32(0)) 173 | if err != nil { 174 | b.Errorf("AddData: %v", err) 175 | } 176 | if complete { 177 | ps.mu.Lock() 178 | ps.del(index, true) 179 | ps.mu.Unlock() 180 | } 181 | } 182 | }) 183 | 184 | ps.Del() 185 | if bytes := alloc.Bytes(); bytes != 0 { 186 | b.Errorf("%v bytes allocated", bytes) 187 | } 188 | } 189 | 190 | func BenchmarkAddFinalise(b *testing.B) { 191 | ps := &Pieces{} 192 | chunk := make([]byte, 16 * 1024) 193 | chunks := uint32(4096) 194 | ps.MetadataComplete(256*1024, int64(chunks) * 16 * 1024) 195 | counter := uint32(0) 196 | b.SetBytes(16 * 1024) 197 | b.RunParallel(func(pb *testing.PB) { 198 | for pb.Next() { 199 | n := (atomic.AddUint32(&counter, 1) - 1) % chunks 200 | index := n / (256 / 16) 201 | begin := (n % (256 / 16)) * 16 * 1024 202 | _, complete, err := 203 | ps.AddData(index, begin, chunk, ^uint32(0)) 204 | if err != nil { 205 | b.Errorf("AddData: %v", err) 206 | } 207 | if complete { 208 | _, _, err := ps.Finalise(index, zeroHash) 209 | if err != ErrHashMismatch { 210 | b.Errorf("Finalise: %v", err) 211 | } 212 | } 213 | } 214 | }) 215 | 216 | ps.Del() 217 | if bytes := alloc.Bytes(); bytes != 0 { 218 | b.Errorf("%v bytes allocated", bytes) 219 | } 220 | } 221 | 222 | func prepare(chunks uint32) (*Pieces, error) { 223 | ps := &Pieces{} 224 | chunk := make([]byte, 16 * 1024) 225 | ps.MetadataComplete(256*1024, int64(chunks) * 16 * 1024) 226 | 227 | for n := uint32(0); n < chunks; n++ { 228 | index := n / (256 / 16) 229 | begin := (n % (256 / 16)) * 16 * 1024 230 | _, complete, err := 231 | ps.AddData(index, begin, chunk, ^uint32(0)) 232 | if err != nil { 233 | return nil, err 234 | } 235 | if complete { 236 | _, _, err := ps.Finalise(index, zeroChunkHash) 237 | if err != nil { 238 | return nil, err 239 | } 240 | } 241 | } 242 | 243 | return ps, nil 244 | } 245 | 246 | func BenchmarkRead(b *testing.B) { 247 | chunks := uint32(4096) 248 | ps, err := prepare(chunks) 249 | if err != nil { 250 | b.Fatalf("Prepare: %v", err) 251 | } 252 | 253 | b.SetBytes(16 * 1024) 254 | b.ResetTimer() 255 | b.RunParallel(func(pb *testing.PB) { 256 | buf := make([]byte, 16384) 257 | n := uint32(0) 258 | for pb.Next() { 259 | c, err := ps.ReadAt(buf, int64(n) * 16 * 1024) 260 | if c != 16384 || err != nil { 261 | b.Errorf("ReadAt: %v %v", c, err) 262 | } 263 | n = (n + 1) % chunks 264 | } 265 | }) 266 | 267 | ps.Del() 268 | if bytes := alloc.Bytes(); bytes != 0 { 269 | b.Errorf("%v bytes allocated", bytes) 270 | } 271 | } 272 | 273 | func BenchmarkReadUpdate(b *testing.B) { 274 | chunks := uint32(4096) 275 | ps, err := prepare(chunks) 276 | if err != nil { 277 | b.Fatalf("Prepare: %v", err) 278 | } 279 | 280 | b.SetBytes(16 * 1024) 281 | b.ResetTimer() 282 | b.RunParallel(func(pb *testing.PB) { 283 | buf := make([]byte, 16384) 284 | n := uint32(0) 285 | for pb.Next() { 286 | complete := ps.UpdateTime(n / (256 / 16)) 287 | if !complete { 288 | b.Errorf("Update: %v", complete) 289 | } 290 | c, err := ps.ReadAt(buf, int64(n) * 16 * 1024) 291 | if c != 16384 || err != nil { 292 | b.Errorf("ReadAt: %v %v", c, err) 293 | } 294 | n = (n + 1) % chunks 295 | } 296 | }) 297 | 298 | ps.Del() 299 | if bytes := alloc.Bytes(); bytes != 0 { 300 | b.Errorf("%v bytes allocated", bytes) 301 | } 302 | } 303 | 304 | -------------------------------------------------------------------------------- /protocol/protocol_test.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | crand "crypto/rand" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net" 12 | "net/netip" 13 | "os" 14 | "reflect" 15 | "sync" 16 | "testing" 17 | 18 | "github.com/jech/storrent/crypto" 19 | "github.com/jech/storrent/hash" 20 | "github.com/jech/storrent/pex" 21 | ) 22 | 23 | func randomHash() hash.Hash { 24 | h := make([]byte, 20) 25 | crand.Read(h) 26 | return h 27 | } 28 | 29 | func testHandshake(t *testing.T, chand bool, opts *crypto.Options) { 30 | h := randomHash() 31 | sid := randomHash() 32 | cid := randomHash() 33 | hashes := []hash.HashPair{hash.HashPair{h, sid}} 34 | c, s := net.Pipe() 35 | var ss net.Conn 36 | var wg sync.WaitGroup 37 | wg.Add(1) 38 | go func() { 39 | defer wg.Done() 40 | var err error 41 | ss, _, _, err = ServerHandshake(s, hashes, opts) 42 | if err != nil { 43 | t.Errorf("ServerHandshake: %v", err) 44 | return 45 | } 46 | ss.Close() 47 | }() 48 | cc, _, _, err := ClientHandshake(c, chand, h, cid, opts) 49 | if err != nil { 50 | c.Close() 51 | t.Fatalf("ClientHandshake: %v", err) 52 | } 53 | defer cc.Close() 54 | wg.Wait() 55 | } 56 | 57 | func TestHandshake(t *testing.T) { 58 | testHandshake(t, false, &crypto.Options{}) 59 | } 60 | 61 | func TestCryptoHandshake(t *testing.T) { 62 | testHandshake(t, true, &crypto.Options{AllowCryptoHandshake: true}) 63 | } 64 | 65 | type mtest struct { 66 | m Message 67 | v string 68 | } 69 | 70 | var messages = []mtest{ 71 | mtest{KeepAlive{}, "\x00\x00\x00\x00"}, 72 | mtest{Choke{}, "\x00\x00\x00\x01\x00"}, 73 | mtest{Unchoke{}, ""}, 74 | mtest{Interested{}, ""}, 75 | mtest{NotInterested{}, ""}, 76 | mtest{Have{42}, ""}, 77 | mtest{Bitfield{[]byte{0xFF, 0xFE, 0x10}}, 78 | "\x00\x00\x00\x04\x05\xff\xfe\x10"}, 79 | mtest{Request{42, 32768, 16384}, ""}, 80 | mtest{Piece{42, 32768, make([]byte, 16384)}, ""}, 81 | mtest{Cancel{42, 32768, 16384}, ""}, 82 | mtest{Port{1234}, "\x00\x00\x00\x03\t\x04\xd2"}, 83 | mtest{SuggestPiece{42}, ""}, 84 | mtest{RejectRequest{42, 32768, 16384}, ""}, 85 | mtest{AllowedFast{42}, ""}, 86 | mtest{HaveAll{}, ""}, 87 | mtest{HaveNone{}, ""}, 88 | mtest{Extended0{"toto", 1234, 256, 89 | netip.MustParseAddr("1.2.3.4"), netip.MustParseAddr("2001::1"), 90 | 1024, nil, false, true}, ""}, 91 | mtest{Extended0{"toto", 1234, 256, 92 | netip.MustParseAddr("1.2.3.4"), 93 | netip.MustParseAddr("2001::1"), 94 | 1024, map[string]uint8{"ut_pex": ExtPex}, false, true}, ""}, 95 | mtest{ExtendedMetadata{ExtMetadata, 0, 2, 64 * 1024, 96 | nil}, "\x00\x00\x00/\x14\x02" + 97 | "d8:msg_typei0e5:piecei2e10:total_sizei65536ee"}, 98 | mtest{ExtendedMetadata{ExtMetadata, 1, 2, 64 * 1024, 99 | make([]byte, 10)}, "\x00\x00\x009\x14\x02" + 100 | "d8:msg_typei1e5:piecei2e10:total_sizei65536ee" + 101 | string(make([]byte, 10))}, 102 | mtest{ExtendedPex{ExtPex, []pex.Peer{ 103 | pex.Peer{Addr: netip.MustParseAddrPort("1.2.3.4:1234"), 104 | Flags: pex.Encrypt | pex.Outgoing}, 105 | pex.Peer{Addr: netip.MustParseAddrPort("[2001::1]:5678"), 106 | Flags: pex.UploadOnly}}, 107 | nil}, "\x00\x00\x00I\x14\x01" + 108 | "d5:added6:\x01\x02\x03\x04\x04\xd2" + 109 | "7:added.f1:\x11" + 110 | "6:added618:\x20\x01\x00\x00\x00\x00\x00\x00" + 111 | "\x00\x00\x00\x00\x00\x00\x00\x01\x16." + 112 | "8:added6.f1:\x02e", 113 | }, 114 | mtest{ExtendedPex{ExtPex, nil, []pex.Peer{ 115 | pex.Peer{Addr: netip.MustParseAddrPort("1.2.3.4:1234")}, 116 | pex.Peer{Addr: netip.MustParseAddrPort("5.6.7.8:4321")}, 117 | pex.Peer{Addr: netip.MustParseAddrPort("[2001::1]:5678")}, 118 | pex.Peer{Addr: netip.MustParseAddrPort("[2001::2]:2345")}}}, ""}, 119 | mtest{ExtendedDontHave{ExtDontHave, 1}, ""}, 120 | } 121 | 122 | type ftest struct { 123 | v string 124 | e error 125 | } 126 | 127 | var failedMessages = []ftest{ 128 | ftest{"", io.EOF}, 129 | ftest{"\x00\x00", io.ErrUnexpectedEOF}, 130 | ftest{"\x00\x00\x00\x01", io.EOF}, 131 | ftest{"\x00\x00\x00\x02\x00", ErrParse}, 132 | ftest{"\x00\x00\x00\x22\x14\x02" + "d5:piecei2e10:total_sizei65536ee", 133 | ErrParse}, 134 | ftest{"\x00\x00\x00\x25\x14\x02" + "d8:msg_typei1e10:total_sizei65536ee", 135 | ErrParse}, 136 | } 137 | 138 | func TestWriter(t *testing.T) { 139 | for _, m := range messages { 140 | t.Run(fmt.Sprintf("%T", m.m), func(t *testing.T) { 141 | p1, p2 := net.Pipe() 142 | w := bufio.NewWriter(p1) 143 | b := make([]byte, 32*1024) 144 | var wg sync.WaitGroup 145 | wg.Add(1) 146 | go func() { 147 | defer wg.Done() 148 | n := 0 149 | for n < len(b) { 150 | m, _ := p2.Read(b[n:]) 151 | if m == 0 { 152 | break 153 | } 154 | n += m 155 | } 156 | b = b[:n] 157 | p2.Close() 158 | }() 159 | err := Write(w, m.m, log.New(io.Discard, "", 0)) 160 | if err != nil { 161 | t.Error(err) 162 | } 163 | err = w.Flush() 164 | if err != nil { 165 | t.Error(err) 166 | } 167 | p1.Close() 168 | wg.Wait() 169 | if m.v != "" { 170 | if string(b) != m.v { 171 | t.Errorf("Got %#v, expected %#v", 172 | string(b), m.v) 173 | } 174 | } 175 | }) 176 | } 177 | } 178 | 179 | func TestReader(t *testing.T) { 180 | for _, m := range messages { 181 | if m.v == "" { 182 | continue 183 | } 184 | t.Run(fmt.Sprintf("%T", m.m), func(t *testing.T) { 185 | r := bufio.NewReader(bytes.NewReader([]byte(m.v))) 186 | mm, err := Read(r, nil) 187 | if err != nil { 188 | t.Fatal(err) 189 | return 190 | } 191 | n, err := r.Read(make([]byte, 32)) 192 | if n != 0 || err != io.EOF { 193 | t.Errorf("%v bytes remaining (%v)", n, m.v) 194 | } 195 | if !reflect.DeepEqual(mm, m.m) { 196 | t.Errorf("Got %#v, expected %#v", mm, m.m) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func TestReaderFail(t *testing.T) { 203 | for _, m := range failedMessages { 204 | t.Run(fmt.Sprintf("%v", m.v), func(t *testing.T) { 205 | r := bufio.NewReader(bytes.NewReader([]byte(m.v))) 206 | _, err := Read(r, nil) 207 | if testing.Verbose() { 208 | fmt.Printf("%v -> %v\n", m.v, err) 209 | } 210 | if !errors.Is(err, m.e) { 211 | t.Errorf("Got %v, expected %v", err, m.e) 212 | } 213 | }) 214 | } 215 | } 216 | 217 | func getLogger() *log.Logger { 218 | if testing.Verbose() { 219 | return log.New(os.Stdout, "", log.LstdFlags) 220 | } 221 | return nil 222 | } 223 | 224 | func TestRoundtrip(t *testing.T) { 225 | r, w := net.Pipe() 226 | reader := make(chan Message, 1024) 227 | readerDone := make(chan struct{}) 228 | logger := getLogger() 229 | go Reader(r, nil, logger, reader, readerDone) 230 | writer := make(chan Message, 1024) 231 | writerDone := make(chan struct{}) 232 | go Writer(w, logger, writer, writerDone) 233 | for _, m := range messages { 234 | select { 235 | case writer <- m.m: 236 | case <-writerDone: 237 | t.Fatal("Writer quit prematurely") 238 | } 239 | mm := <-reader 240 | if !reflect.DeepEqual(m.m, mm) { 241 | var e string 242 | me, ok := mm.(Error) 243 | if ok { 244 | e = fmt.Sprintf(" (%v)", me.Error) 245 | } 246 | t.Errorf("Got %#v%v, expected %#v", mm, e, m.m) 247 | } 248 | } 249 | r.Close() 250 | close(readerDone) 251 | mm, ok := <-reader 252 | if ok { 253 | _, ok := mm.(Error) 254 | if !ok { 255 | t.Errorf("Got %v, expected EOF", mm) 256 | } 257 | } 258 | close(writer) 259 | } 260 | 261 | func benchmarkMessage(m Message, bytes int64, b *testing.B) { 262 | if bytes > 0 { 263 | b.SetBytes(bytes) 264 | } 265 | r, w := net.Pipe() 266 | reader := make(chan Message, 1024) 267 | readerDone := make(chan struct{}) 268 | logger := getLogger() 269 | go Reader(r, nil, logger, reader, readerDone) 270 | writer := make(chan Message, 1024) 271 | writerDone := make(chan struct{}) 272 | go Writer(w, logger, writer, writerDone) 273 | b.ResetTimer() 274 | for i := 0; i < b.N; i++ { 275 | select { 276 | case writer <- m: 277 | case <-writerDone: 278 | b.Errorf("Writer quit prematurely") 279 | } 280 | _, ok := <-reader 281 | if !ok { 282 | b.Errorf("Reader quit prematurely") 283 | } 284 | } 285 | r.Close() 286 | close(readerDone) 287 | close(writer) 288 | } 289 | 290 | func BenchmarkRequest(b *testing.B) { 291 | benchmarkMessage(Request{42, 32768, 16384}, 0, b) 292 | } 293 | 294 | func BenchmarkData(b *testing.B) { 295 | benchmarkMessage(Piece{42, 32768, make([]byte, 16384)}, 16384, b) 296 | } 297 | -------------------------------------------------------------------------------- /fuse/fuse.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package fuse 5 | 6 | import ( 7 | "context" 8 | "hash/fnv" 9 | "io" 10 | "os" 11 | "syscall" 12 | "time" 13 | 14 | "bazil.org/fuse" 15 | "bazil.org/fuse/fs" 16 | 17 | "github.com/jech/storrent/hash" 18 | "github.com/jech/storrent/path" 19 | "github.com/jech/storrent/tor" 20 | ) 21 | 22 | func Serve(mountpoint string) error { 23 | conn, err := fuse.Mount( 24 | mountpoint, 25 | fuse.Subtype("storrent"), 26 | fuse.ReadOnly(), 27 | fuse.MaxReadahead(128*1024), 28 | fuse.AsyncRead(), 29 | ) 30 | if err != nil { 31 | return err 32 | } 33 | go func(conn *fuse.Conn) { 34 | defer conn.Close() 35 | fs.Serve(conn, filesystem(0)) 36 | }(conn) 37 | 38 | return nil 39 | } 40 | 41 | func Close(mountpoint string) error { 42 | return fuse.Unmount(mountpoint) 43 | } 44 | 45 | type filesystem int 46 | 47 | func (fs filesystem) Root() (fs.Node, error) { 48 | return root(0), nil 49 | } 50 | 51 | func fileInode(hash hash.Hash, path path.Path) uint64 { 52 | h := fnv.New64a() 53 | h.Write(hash) 54 | for _, n := range path { 55 | h.Write([]byte(n)) 56 | h.Write([]byte{0}) 57 | } 58 | return h.Sum64() 59 | } 60 | 61 | type root int 62 | 63 | func setuid(a *fuse.Attr) { 64 | uid := os.Getuid() 65 | if uid >= 0 { 66 | a.Uid = uint32(uid) 67 | } 68 | gid := os.Getgid() 69 | if gid >= 0 { 70 | a.Gid = uint32(gid) 71 | } 72 | } 73 | 74 | func (dir root) Attr(ctx context.Context, a *fuse.Attr) error { 75 | a.Inode = 1 76 | a.Mode = os.ModeDir | 0555 77 | setuid(a) 78 | return nil 79 | } 80 | 81 | func (dir root) Lookup(ctx context.Context, name string) (fs.Node, error) { 82 | t := tor.GetByName(name) 83 | if t == nil { 84 | return nil, fuse.ENOENT 85 | } 86 | 87 | var h [20]byte 88 | copy(h[:], t.Hash) 89 | 90 | if t.Files == nil { 91 | return file{h, t.Name}, nil 92 | } 93 | 94 | return directory{hash: h}, nil 95 | } 96 | 97 | func (dir root) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 98 | ents := make([]fuse.Dirent, 0) 99 | tor.Range(func(h hash.Hash, t *tor.Torrent) bool { 100 | if t.InfoComplete() && t.Name != "" { 101 | tpe := fuse.DT_Dir 102 | if t.Files == nil { 103 | tpe = fuse.DT_File 104 | } 105 | ents = append(ents, fuse.Dirent{ 106 | Name: t.Name, 107 | Type: tpe, 108 | Inode: fileInode(t.Hash, nil), 109 | }) 110 | } 111 | return true 112 | }) 113 | return ents, nil 114 | } 115 | 116 | type directory struct { 117 | hash [20]byte 118 | name string 119 | } 120 | 121 | func (dir directory) Hash() hash.Hash { 122 | h := hash.Hash(make([]byte, 20)) 123 | copy(h, dir.hash[:]) 124 | return h 125 | } 126 | 127 | func (dir directory) Attr(ctx context.Context, a *fuse.Attr) error { 128 | t := tor.Get(dir.Hash()) 129 | if t == nil || !t.InfoComplete() { 130 | return fuse.ENOENT 131 | } 132 | 133 | a.Inode = fileInode(dir.Hash(), path.Parse(dir.name)) 134 | a.Mode = os.ModeDir | 0555 135 | setuid(a) 136 | if t.CreationDate > 0 { 137 | a.Mtime = time.Unix(t.CreationDate, 0) 138 | a.Ctime = time.Unix(t.CreationDate, 0) 139 | } 140 | return nil 141 | } 142 | 143 | func (dir directory) Lookup(ctx context.Context, name string) (fs.Node, error) { 144 | t := tor.Get(dir.Hash()) 145 | if t == nil || !t.InfoComplete() { 146 | return nil, fuse.ENOENT 147 | } 148 | 149 | var h [20]byte 150 | copy(h[:], t.Hash) 151 | 152 | pth := path.Parse(dir.name) 153 | for _, f := range t.Files { 154 | if f.Path.Within(pth) && f.Path[len(pth)] == name { 155 | p := append(path.Path(nil), pth...) 156 | p = append(p, name) 157 | filename := p.String() 158 | if len(f.Path) > len(pth)+1 { 159 | return directory{h, filename}, nil 160 | } else { 161 | return file{h, filename}, nil 162 | } 163 | } 164 | } 165 | return nil, fuse.ENOENT 166 | } 167 | 168 | func (dir directory) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 169 | t := tor.Get(dir.Hash()) 170 | if t == nil || !t.InfoComplete() { 171 | return nil, fuse.ENOENT 172 | } 173 | 174 | pth := path.Parse(dir.name) 175 | 176 | ents := make([]fuse.Dirent, 0) 177 | 178 | ents = append(ents, fuse.Dirent{ 179 | Name: ".", 180 | Type: fuse.DT_Dir, 181 | Inode: fileInode(t.Hash, pth), 182 | }) 183 | parentInode := uint64(1) 184 | if len(pth) > 0 { 185 | parentInode = fileInode(t.Hash, pth[:len(pth)-1]) 186 | } 187 | ents = append(ents, fuse.Dirent{ 188 | Name: "..", 189 | Type: fuse.DT_Dir, 190 | Inode: parentInode, 191 | }) 192 | 193 | dirs := make(map[string]bool) 194 | for _, f := range t.Files { 195 | if f.Padding { 196 | continue 197 | } 198 | if !f.Path.Within(pth) { 199 | continue 200 | } 201 | name := f.Path[len(pth)] 202 | tpe := fuse.DT_File 203 | if len(f.Path) > len(pth)+1 { 204 | if dirs[name] { 205 | continue 206 | } 207 | dirs[name] = true 208 | tpe = fuse.DT_Dir 209 | } 210 | ents = append(ents, fuse.Dirent{ 211 | Name: name, 212 | Type: tpe, 213 | Inode: fileInode(t.Hash, f.Path[:len(pth)+1]), 214 | }) 215 | } 216 | return ents, nil 217 | } 218 | 219 | type file struct { 220 | hash [20]byte 221 | name string 222 | } 223 | 224 | func (file file) Hash() hash.Hash { 225 | h := hash.Hash(make([]byte, 20)) 226 | copy(h, file.hash[:]) 227 | return h 228 | } 229 | 230 | func findFile(t *tor.Torrent, path path.Path) *tor.Torfile { 231 | for _, f := range t.Files { 232 | if path.Equal(f.Path) { 233 | return &f 234 | } 235 | } 236 | return nil 237 | } 238 | 239 | func (file file) Attr(ctx context.Context, a *fuse.Attr) error { 240 | t := tor.Get(file.Hash()) 241 | if t == nil || !t.InfoComplete() { 242 | return fuse.ENOENT 243 | } 244 | 245 | pth := path.Parse(file.name) 246 | 247 | var size uint64 248 | if t.Files == nil { 249 | size = uint64(t.Pieces.Length()) 250 | } else { 251 | f := findFile(t, pth) 252 | if f == nil { 253 | return fuse.ENOENT 254 | } 255 | size = uint64(f.Length) 256 | } 257 | 258 | a.Inode = fileInode(t.Hash, pth) 259 | a.Mode = 0444 260 | setuid(a) 261 | a.Size = size 262 | a.Blocks = (size + 511) / 512 263 | if t.CreationDate > 0 { 264 | a.Mtime = time.Unix(t.CreationDate, 0) 265 | a.Ctime = time.Unix(t.CreationDate, 0) 266 | } 267 | return nil 268 | } 269 | 270 | type handle struct { 271 | file file 272 | 273 | // using a semaphore here gives better ordering than a mutex. 274 | // Another benefit is to avoid polluting the mutex profile. 275 | sema chan struct{} 276 | reader *tor.Reader 277 | } 278 | 279 | func (file file) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) { 280 | t := tor.Get(file.Hash()) 281 | if t == nil || !t.InfoComplete() { 282 | return nil, fuse.ENOENT 283 | } 284 | 285 | if !req.Flags.IsReadOnly() { 286 | return nil, fuse.Errno(syscall.EACCES) 287 | } 288 | 289 | var offset, length int64 290 | 291 | if t.Files == nil { 292 | offset = 0 293 | length = t.Pieces.Length() 294 | } else { 295 | f := findFile(t, path.Parse(file.name)) 296 | if f == nil { 297 | return nil, fuse.ENOENT 298 | } 299 | offset = f.Offset 300 | length = f.Length 301 | } 302 | reader := t.NewReader(context.Background(), offset, length) 303 | if reader == nil { 304 | return nil, fuse.EIO 305 | } 306 | 307 | resp.Flags |= fuse.OpenKeepCache 308 | 309 | return &handle{ 310 | file: file, 311 | reader: reader, 312 | sema: make(chan struct{}, 1), 313 | }, nil 314 | } 315 | 316 | func (handle *handle) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { 317 | select { 318 | case <-ctx.Done(): 319 | return fuse.EINTR 320 | case handle.sema <- struct{}{}: 321 | } 322 | defer func() { 323 | <-handle.sema 324 | }() 325 | 326 | _, err := handle.reader.Seek(req.Offset, io.SeekStart) 327 | if err != nil { 328 | return err 329 | } 330 | 331 | handle.reader.SetContext(ctx) 332 | defer handle.reader.SetContext(context.Background()) 333 | 334 | resp.Data = resp.Data[:req.Size] 335 | n, err := io.ReadFull(handle.reader, resp.Data) 336 | resp.Data = resp.Data[:n] 337 | if err == io.EOF || err == io.ErrUnexpectedEOF { 338 | err = nil 339 | } 340 | return err 341 | } 342 | 343 | func (handle *handle) Release(ctx context.Context, req *fuse.ReleaseRequest) error { 344 | handle.sema <- struct{}{} 345 | defer func() { 346 | <-handle.sema 347 | }() 348 | 349 | err := handle.reader.Close() 350 | handle.reader = nil 351 | return err 352 | } 353 | -------------------------------------------------------------------------------- /tor/torfile.go: -------------------------------------------------------------------------------- 1 | package tor 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "errors" 7 | "fmt" 8 | "github.com/zeebo/bencode" 9 | "io" 10 | "net/http" 11 | nurl "net/url" 12 | "strings" 13 | "sync/atomic" 14 | 15 | "github.com/jech/storrent/config" 16 | "github.com/jech/storrent/hash" 17 | "github.com/jech/storrent/httpclient" 18 | "github.com/jech/storrent/path" 19 | "github.com/jech/storrent/tracker" 20 | "github.com/jech/storrent/webseed" 21 | ) 22 | 23 | // BTorrent stores the contents of a torrent file. 24 | type BTorrent struct { 25 | Info bencode.RawMessage `bencode:"info"` 26 | CreationDate int64 `bencode:"creation date,omitempty"` 27 | Announce string `bencode:"announce,omitempty"` 28 | AnnounceList [][]string `bencode:"announce-list,omitempty"` 29 | URLList listOrString `bencode:"url-list,omitempty"` 30 | HTTPSeeds listOrString `bencode:"httpseeds,omitempty"` 31 | } 32 | 33 | // BInfo is the info dictionary of a torrent file. 34 | type BInfo struct { 35 | Name string `bencode:"name"` 36 | Name8 string `bencode:"name.utf-8,omitempty"` 37 | PieceLength uint32 `bencode:"piece length"` 38 | Pieces []byte `bencode:"pieces"` 39 | Length int64 `bencode:"length"` 40 | Files []BFile `bencode:"files,omitempty"` 41 | } 42 | 43 | // BFile is a file entry in a torrent file. 44 | type BFile struct { 45 | Path path.Path `bencode:"path"` 46 | Path8 path.Path `bencode:"path.utf-8,omitempty"` 47 | Length int64 `bencode:"length"` 48 | Attr string `bencode:"attr,omitempty"` 49 | } 50 | 51 | // listOrString is an array of strings. It may unmarshal from either 52 | // a list or a singleton string, and always encodes to a list. 53 | type listOrString []string 54 | 55 | func (ls *listOrString) UnmarshalBencode(v []byte) error { 56 | var s string 57 | err := bencode.DecodeBytes(v, &s) 58 | if err == nil { 59 | *ls = []string{s} 60 | return nil 61 | } 62 | var l []string 63 | err = bencode.DecodeBytes(v, &l) 64 | if err != nil { 65 | return err 66 | } 67 | *ls = l 68 | return nil 69 | } 70 | 71 | func webseedList(urls []string, getright bool) []webseed.Webseed { 72 | var l []webseed.Webseed 73 | for _, u := range urls { 74 | ws := webseed.New(u, getright) 75 | if ws == nil { 76 | continue 77 | } 78 | l = append(l, ws) 79 | } 80 | return l 81 | } 82 | 83 | type UnknownSchemeError struct { 84 | Scheme string 85 | } 86 | 87 | func (e UnknownSchemeError) Error() string { 88 | if e.Scheme == "" { 89 | return "missing scheme" 90 | } 91 | return fmt.Sprintf("unknown scheme %v", e.Scheme) 92 | } 93 | 94 | type ParseURLError struct { 95 | Err error 96 | } 97 | 98 | func (e ParseURLError) Error() string { 99 | return e.Err.Error() 100 | } 101 | 102 | func (e ParseURLError) Unwrap() error { 103 | return e.Err 104 | } 105 | 106 | // GetTorrent fetches a torrent file from a web server. 107 | func GetTorrent(ctx context.Context, proxy string, url string) (*Torrent, error) { 108 | u, err := nurl.Parse(url) 109 | if err == nil && u.Scheme != "http" && u.Scheme != "https" { 110 | err = UnknownSchemeError{u.Scheme} 111 | } 112 | if err != nil { 113 | return nil, ParseURLError{err} 114 | } 115 | 116 | req, err := http.NewRequest("GET", url, nil) 117 | if err != nil { 118 | return nil, err 119 | } 120 | req.Header["User-Agent"] = nil 121 | req.Close = true 122 | 123 | client := httpclient.Get("", proxy) 124 | if client == nil { 125 | return nil, errors.New("couldn't create HTTP client") 126 | } 127 | 128 | r, err := client.Do(req.WithContext(ctx)) 129 | if err != nil { 130 | return nil, err 131 | } 132 | defer r.Body.Close() 133 | 134 | if r.StatusCode != 200 { 135 | return nil, errors.New("upstream server returned: " + r.Status) 136 | } 137 | 138 | t, err := ReadTorrent(proxy, r.Body) 139 | if err != nil { 140 | return nil, err 141 | } 142 | return t, nil 143 | } 144 | 145 | // ReadTorrent reads a torrent from an io.Reader. The given proxy will be 146 | // used when accessing trackers for this torrent. 147 | func ReadTorrent(proxy string, r io.Reader) (*Torrent, error) { 148 | decoder := bencode.NewDecoder(r) 149 | var torrent BTorrent 150 | err := decoder.Decode(&torrent) 151 | if err != nil { 152 | return nil, err 153 | } 154 | if torrent.Info == nil { 155 | return nil, errors.New("couldn't find info") 156 | } 157 | 158 | var announce [][]tracker.Tracker 159 | if torrent.AnnounceList != nil { 160 | announce = 161 | make([][]tracker.Tracker, len(torrent.AnnounceList)) 162 | for i, v := range torrent.AnnounceList { 163 | for _, w := range v { 164 | tr := tracker.New(w) 165 | if tr != nil { 166 | announce[i] = append(announce[i], tr) 167 | } 168 | } 169 | } 170 | } else if torrent.Announce != "" { 171 | tr := tracker.New(torrent.Announce) 172 | if tr != nil { 173 | announce = [][]tracker.Tracker{{tr}} 174 | } 175 | } 176 | 177 | webseeds := webseedList(torrent.URLList, true) 178 | webseeds = append(webseeds, webseedList(torrent.HTTPSeeds, false)...) 179 | 180 | hsh := sha1.Sum(torrent.Info) 181 | h := hash.Hash(hsh[:]) 182 | 183 | t, err := New(proxy, h, "", torrent.Info, torrent.CreationDate, 184 | announce, webseeds) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | err = t.MetadataComplete() 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | return t, nil 195 | } 196 | 197 | // MetadataComplete must be called when a torrent's metadata is complete. 198 | func (torrent *Torrent) MetadataComplete() error { 199 | var info BInfo 200 | err := bencode.DecodeBytes(torrent.Info, &info) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | if len(info.Pieces)%20 != 0 { 206 | return errors.New("pieces has an odd size") 207 | } 208 | if info.PieceLength%config.ChunkSize != 0 { 209 | return errors.New("odd sized piece") 210 | } 211 | hashes := make([]hash.Hash, 0, len(info.Pieces)/20) 212 | for i := 0; i < len(info.Pieces)/20; i++ { 213 | hashes = append(hashes, info.Pieces[i*20:(i+1)*20]) 214 | } 215 | 216 | var files []Torfile 217 | 218 | var length int64 219 | if info.Length > 0 { 220 | if info.Files != nil { 221 | return errors.New("both length and files") 222 | } 223 | length = info.Length 224 | } else { 225 | if info.Files == nil { 226 | return errors.New("neither length nor files") 227 | } 228 | length = 0 229 | for _, f := range info.Files { 230 | path := f.Path8 231 | if path == nil { 232 | path = f.Path 233 | } 234 | if path == nil { 235 | return errors.New("file has no path") 236 | } 237 | files = append(files, 238 | Torfile{Path: path, 239 | Offset: length, 240 | Length: f.Length, 241 | Padding: strings.Contains(f.Attr, "p")}) 242 | length += f.Length 243 | } 244 | } 245 | 246 | chunks := (length + int64(config.ChunkSize) - 1) / 247 | int64(config.ChunkSize) 248 | if chunks != int64(uint32(chunks)) || chunks != int64(int(chunks)) { 249 | return errors.New("torrent too large") 250 | } 251 | torrent.inFlight = make([]uint8, chunks) 252 | 253 | torrent.PieceHashes = hashes 254 | // torrent.Name may have been populated earlier 255 | if info.Name8 != "" { 256 | torrent.Name = info.Name8 257 | } else { 258 | torrent.Name = info.Name 259 | } 260 | if torrent.Name == "" { 261 | return errors.New("torrent has no name") 262 | } 263 | torrent.Pieces.MetadataComplete(info.PieceLength, length) 264 | torrent.Files = files 265 | 266 | atomic.StoreUint32(&torrent.infoComplete, 1) 267 | 268 | return nil 269 | } 270 | 271 | // ReadMagnet parses a magnet link. 272 | func ReadMagnet(proxy string, m string) (*Torrent, error) { 273 | h := hash.Parse(m) 274 | var dn string 275 | var announce [][]tracker.Tracker 276 | var webseeds []webseed.Webseed 277 | if h == nil { 278 | url, err := nurl.Parse(m) 279 | if err != nil { 280 | return nil, nil 281 | } 282 | if url.Scheme != "magnet" { 283 | return nil, nil 284 | } 285 | q := url.Query() 286 | if q == nil { 287 | return nil, errors.New("couldn't parse magnet link") 288 | } 289 | xt := q["xt"] 290 | for _, v := range xt { 291 | if strings.HasPrefix(v, "urn:btih:") { 292 | hh := v[9:] 293 | h = hash.Parse(hh) 294 | if h != nil { 295 | break 296 | } 297 | } 298 | } 299 | if h == nil { 300 | return nil, errors.New("couldn't find btih field") 301 | } 302 | tr := q["tr"] 303 | for _, v := range tr { 304 | t := tracker.New(v) 305 | if t != nil { 306 | announce = append(announce, 307 | []tracker.Tracker{t}) 308 | } 309 | } 310 | as := q["as"] 311 | webseeds = append(webseeds, webseedList(as, true)...) 312 | ws := q["ws"] 313 | webseeds = append(webseeds, webseedList(ws, true)...) 314 | dn = q.Get("dn") 315 | } 316 | return New(proxy, h, dn, nil, 0, announce, webseeds) 317 | } 318 | 319 | // WriteTorrent writes a torrent file to an io.Writer. 320 | func WriteTorrent(w io.Writer, t *Torrent) error { 321 | encoder := bencode.NewEncoder(w) 322 | var a string 323 | var al [][]string 324 | 325 | as := make([][]string, len(t.trackers)) 326 | for i, v := range t.trackers { 327 | as[i] = make([]string, len(v)) 328 | for j, w := range v { 329 | as[i][j] = w.URL() 330 | } 331 | } 332 | 333 | if len(t.trackers) == 1 && len(t.trackers[0]) == 1 { 334 | a = as[0][0] 335 | } else { 336 | if len(t.trackers) > 0 && len(t.trackers[0]) > 0 { 337 | a = as[0][0] 338 | } 339 | al = as 340 | } 341 | 342 | var ul []string 343 | var hs []string 344 | for _, ws := range t.webseeds { 345 | switch ws.(type) { 346 | case *webseed.GetRight: 347 | ul = append(ul, ws.URL()) 348 | case *webseed.Hoffman: 349 | hs = append(hs, ws.URL()) 350 | default: 351 | panic("Eek") 352 | } 353 | } 354 | 355 | return encoder.Encode(&BTorrent{ 356 | Info: t.Info, 357 | CreationDate: t.CreationDate, 358 | Announce: a, 359 | AnnounceList: al, 360 | URLList: ul, 361 | HTTPSeeds: hs, 362 | }) 363 | } 364 | -------------------------------------------------------------------------------- /protocol/reader.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "errors" 8 | "io" 9 | "log" 10 | "net" 11 | "net/netip" 12 | "sync" 13 | "time" 14 | 15 | "github.com/zeebo/bencode" 16 | 17 | "github.com/jech/storrent/config" 18 | "github.com/jech/storrent/pex" 19 | ) 20 | 21 | var ErrParse = errors.New("parse error") 22 | 23 | var pool sync.Pool = sync.Pool{ 24 | New: func() interface{} { 25 | return make([]byte, config.ChunkSize) 26 | }, 27 | } 28 | 29 | // GetBuffer gets a buffer suitable for storing a chunk of BitTorrent data. 30 | func GetBuffer(length int) []byte { 31 | if length == int(config.ChunkSize) { 32 | buf := pool.Get().([]byte) 33 | return buf 34 | } 35 | return make([]byte, length) 36 | } 37 | 38 | // PutBuffer releases a buffer obtained by GetBuffer. The buffer must not 39 | // be used again. 40 | func PutBuffer(buf []byte) { 41 | if len(buf) == int(config.ChunkSize) { 42 | pool.Put(buf) 43 | } 44 | } 45 | 46 | func readUint16(r *bufio.Reader) (uint16, error) { 47 | var v uint16 48 | err := binary.Read(r, binary.BigEndian, &v) 49 | return v, err 50 | } 51 | 52 | func readUint32(r *bufio.Reader) (uint32, error) { 53 | var v uint32 54 | err := binary.Read(r, binary.BigEndian, &v) 55 | return v, err 56 | } 57 | 58 | // Read reads a single BitTorrent message from r. If l is not nil, then 59 | // the message is logged. 60 | func Read(r *bufio.Reader, l *log.Logger) (Message, error) { 61 | debugf := func(format string, v ...interface{}) { 62 | if l != nil { 63 | l.Printf(format, v...) 64 | } 65 | } 66 | length, err := readUint32(r) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | if length == 0 { 72 | debugf("<- KeepAlive") 73 | return KeepAlive{}, nil 74 | } 75 | 76 | if length > 1024*1024 { 77 | return nil, errors.New("TLV too long") 78 | } 79 | 80 | tpe, err := r.ReadByte() 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | switch tpe { 86 | case 0: 87 | if length != 1 { 88 | return nil, ErrParse 89 | } 90 | debugf("<- Choke") 91 | return Choke{}, nil 92 | case 1: 93 | if length != 1 { 94 | return nil, ErrParse 95 | } 96 | debugf("<- Unchoke") 97 | return Unchoke{}, nil 98 | case 2: 99 | if length != 1 { 100 | return nil, ErrParse 101 | } 102 | debugf("<- Interested") 103 | return Interested{}, nil 104 | case 3: 105 | if length != 1 { 106 | return nil, ErrParse 107 | } 108 | debugf("<- NotInterested") 109 | return NotInterested{}, nil 110 | case 4: 111 | if length != 5 { 112 | return nil, ErrParse 113 | } 114 | index, err := readUint32(r) 115 | if err != nil { 116 | return nil, err 117 | } 118 | debugf("<- Have %v", index) 119 | return Have{index}, nil 120 | case 5: 121 | if length < 1 { 122 | return nil, ErrParse 123 | } 124 | bf := make([]byte, length-1) 125 | _, err := io.ReadFull(r, bf) 126 | if err != nil { 127 | return nil, err 128 | } 129 | debugf("<- Bitfield %v", len(bf)) 130 | return Bitfield{bf}, nil 131 | case 6, 8, 16: 132 | if length != 13 { 133 | return nil, ErrParse 134 | } 135 | index, err := readUint32(r) 136 | if err != nil { 137 | return nil, err 138 | } 139 | begin, err := readUint32(r) 140 | if err != nil { 141 | return nil, err 142 | } 143 | length, err := readUint32(r) 144 | if err != nil { 145 | return nil, err 146 | } 147 | if tpe == 6 { 148 | debugf("<- Request %v %v %v", index, begin, length) 149 | return Request{index, begin, length}, nil 150 | } else if tpe == 8 { 151 | debugf("<- Cancel %v %v %v", index, begin, length) 152 | return Cancel{index, begin, length}, nil 153 | } else if tpe == 16 { 154 | debugf("<- RejectRequest %v %v %v", 155 | index, begin, length) 156 | return RejectRequest{index, begin, length}, nil 157 | } 158 | case 7: 159 | if length < 9 { 160 | return nil, ErrParse 161 | } 162 | index, err := readUint32(r) 163 | if err != nil { 164 | return nil, err 165 | } 166 | begin, err := readUint32(r) 167 | if err != nil { 168 | return nil, err 169 | } 170 | if l != nil { 171 | debugf("<- Piece %v %v %v", index, begin, length-9) 172 | } 173 | data := GetBuffer(int(length - 9)) 174 | _, err = io.ReadFull(r, data) 175 | if err != nil { 176 | PutBuffer(data) 177 | return nil, err 178 | } 179 | return Piece{index, begin, data}, nil 180 | case 9: 181 | if length != 3 { 182 | return nil, ErrParse 183 | } 184 | port, err := readUint16(r) 185 | if err != nil { 186 | return nil, err 187 | } 188 | debugf("<- Port %v", port) 189 | return Port{port}, nil 190 | case 13, 17: 191 | if length != 5 { 192 | return nil, ErrParse 193 | } 194 | index, err := readUint32(r) 195 | if err != nil { 196 | return nil, err 197 | } 198 | if tpe == 13 { 199 | debugf("<- SuggestPiece %v", index) 200 | return SuggestPiece{index}, nil 201 | } else { 202 | debugf("<- AllowedFast %v", index) 203 | return AllowedFast{index}, nil 204 | } 205 | case 14, 15: 206 | if length != 1 { 207 | return nil, err 208 | } 209 | if tpe == 14 { 210 | debugf("<- HaveAll") 211 | return HaveAll{}, nil 212 | } else { 213 | debugf("<- HaveNone") 214 | return HaveNone{}, nil 215 | } 216 | case 20: 217 | subtype, err := r.ReadByte() 218 | if err != nil { 219 | return nil, err 220 | } 221 | 222 | switch subtype { 223 | case 0: 224 | var ext extensionInfo 225 | lr := io.LimitReader(r, int64(length-2)) 226 | decoder := bencode.NewDecoder(lr) 227 | err = decoder.Decode(&ext) 228 | if err != nil { 229 | return nil, err 230 | } 231 | _, err = io.Copy(io.Discard, lr) 232 | if err != nil { 233 | return nil, err 234 | } 235 | m := Extended0{} 236 | 237 | m.Version = ext.Version 238 | m.Port = ext.Port 239 | m.ReqQ = ext.ReqQ 240 | ip, ok := netip.AddrFromSlice(ext.IPv4) 241 | if ok && ip.Is4() { 242 | m.IPv4 = ip 243 | } 244 | ip, ok = netip.AddrFromSlice(ext.IPv6) 245 | if ok && ip.Is6() { 246 | m.IPv6 = ip 247 | } 248 | m.MetadataSize = ext.MetadataSize 249 | if len(ext.Messages) > 0 { 250 | m.Messages = ext.Messages 251 | } 252 | m.UploadOnly = bool(ext.UploadOnly) 253 | m.Encrypt = bool(ext.Encrypt) 254 | debugf("<- Extended0 %v", m.Version) 255 | return m, nil 256 | case ExtPex: 257 | var info pexInfo 258 | lr := io.LimitReader(r, int64(length-2)) 259 | decoder := bencode.NewDecoder(lr) 260 | err := decoder.Decode(&info) 261 | if err != nil { 262 | return nil, err 263 | } 264 | _, err = io.Copy(io.Discard, lr) 265 | if err != nil { 266 | return nil, err 267 | } 268 | var added, dropped []pex.Peer 269 | if info.Added != nil && len(info.Added)%6 == 0 { 270 | added = append(added, 271 | pex.ParseCompact(info.Added, 272 | info.AddedF, false)...) 273 | } 274 | if info.Added6 != nil && len(info.Added6)%18 == 0 { 275 | added = append(added, 276 | pex.ParseCompact([]byte(info.Added6), 277 | info.Added6F, true)...) 278 | } 279 | if info.Dropped != nil && len(info.Dropped)%6 == 0 { 280 | dropped = append(dropped, 281 | pex.ParseCompact(info.Dropped, 282 | nil, false)...) 283 | } 284 | if info.Dropped6 != nil && len(info.Dropped6)%18 == 0 { 285 | dropped = append(dropped, 286 | pex.ParseCompact(info.Dropped6, 287 | nil, true)...) 288 | } 289 | debugf("<- ExtendedPex %v %v", 290 | len(added), len(dropped)) 291 | return ExtendedPex{ExtPex, added, dropped}, nil 292 | case ExtMetadata: 293 | var m metadataInfo 294 | data := make([]byte, length-2) 295 | _, err := io.ReadFull(r, data) 296 | if err != nil { 297 | return nil, err 298 | } 299 | decoder := bencode.NewDecoder(bytes.NewReader(data)) 300 | err = decoder.Decode(&m) 301 | if err != nil { 302 | return nil, err 303 | } 304 | var tpe uint8 305 | var index, totalSize uint32 306 | if m.Type == nil || m.Piece == nil { 307 | return nil, ErrParse 308 | } 309 | tpe = *m.Type 310 | index = *m.Piece 311 | if m.TotalSize != nil { 312 | totalSize = *m.TotalSize 313 | } 314 | var metadata []byte 315 | count := decoder.BytesParsed() 316 | if count < len(data) { 317 | metadata = make([]byte, len(data)-count) 318 | copy(metadata, data[count:]) 319 | } 320 | debugf("<- ExtendedMetadata %v %v", tpe, index) 321 | return ExtendedMetadata{ExtMetadata, 322 | tpe, index, totalSize, metadata}, nil 323 | case ExtDontHave: 324 | if length-2 != 4 { 325 | return nil, ErrParse 326 | } 327 | index, err := readUint32(r) 328 | if err != nil { 329 | return nil, err 330 | } 331 | debugf("<- ExtendedDontHave %v", index) 332 | return ExtendedDontHave{ExtDontHave, index}, nil 333 | case ExtUploadOnly: 334 | if length-2 != 1 { 335 | return nil, ErrParse 336 | } 337 | v, err := r.ReadByte() 338 | if err != nil || (v != 0 && v != 1) { 339 | return nil, ErrParse 340 | } 341 | debugf("<- ExtendedUploadOnly %v", v) 342 | return ExtendedUploadOnly{ExtUploadOnly, v == 1}, nil 343 | default: 344 | debugf("<- ExtendedUnknown %v %v", subtype, length-2) 345 | _, err := r.Discard(int(length - 2)) 346 | if err != nil { 347 | return nil, err 348 | } 349 | return ExtendedUnknown{subtype}, nil 350 | } 351 | } 352 | _, err = r.Discard(int(length) - 1) 353 | if err != nil { 354 | return nil, err 355 | } 356 | return Unknown{tpe}, nil 357 | } 358 | 359 | // Reader reads BitTorrent messages from c until it is closed. The 360 | // parameter init contains data that is prepended to the data received 361 | // from c. If l is not nil, then all messages read are logged. 362 | func Reader(c net.Conn, init []byte, l *log.Logger, ch chan<- Message, done <-chan struct{}) { 363 | defer close(ch) 364 | 365 | var r *bufio.Reader 366 | if len(init) == 0 { 367 | r = bufio.NewReader(c) 368 | } else { 369 | r = bufio.NewReader(io.MultiReader(bytes.NewReader(init), c)) 370 | } 371 | for { 372 | var m Message 373 | err := c.SetReadDeadline(time.Now().Add(6 * time.Minute)) 374 | if err == nil { 375 | m, err = Read(r, l) 376 | } 377 | if err != nil { 378 | m = Error{err} 379 | } 380 | select { 381 | case ch <- m: 382 | case <-done: 383 | return 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /storrent.go: -------------------------------------------------------------------------------- 1 | // STorrent is a BitTorrent implementation that is optimised for streaming 2 | // media. 3 | package main 4 | 5 | import ( 6 | "context" 7 | crand "crypto/rand" 8 | "errors" 9 | "flag" 10 | "fmt" 11 | "io" 12 | "log" 13 | "math/rand/v2" 14 | "net" 15 | "net/netip" 16 | "os" 17 | "os/signal" 18 | "path/filepath" 19 | "runtime" 20 | "runtime/pprof" 21 | "syscall" 22 | "time" 23 | 24 | "github.com/jech/portmap" 25 | 26 | "github.com/jech/storrent/config" 27 | "github.com/jech/storrent/crypto" 28 | "github.com/jech/storrent/dht" 29 | "github.com/jech/storrent/fuse" 30 | "github.com/jech/storrent/http" 31 | "github.com/jech/storrent/peer" 32 | "github.com/jech/storrent/physmem" 33 | "github.com/jech/storrent/rundht" 34 | "github.com/jech/storrent/tor" 35 | ) 36 | 37 | func main() { 38 | var proxyURL, mountpoint string 39 | var cpuprofile, memprofile, mutexprofile, dhtMode string 40 | var doPortmap string 41 | 42 | mem, err := physmem.Total() 43 | if err != nil { 44 | log.Printf("Couldn't determine physical memory: %v", err) 45 | mem = 2 * 1024 * 1024 * 1024 46 | } 47 | 48 | fmt.Fprintf(os.Stderr, "STorrent 0.0 by Juliusz Chroboczek\n") 49 | 50 | flag.IntVar(&config.ProtocolPort, "port", 23222, 51 | "TCP and UDP `port` used for BitTorrent and DHT traffic") 52 | flag.StringVar(&config.HTTPAddr, "http", "[::1]:8088", 53 | "Web server `address`") 54 | flag.Int64Var(&config.MemoryMark, "mem", mem/2, 55 | "Target memory usage in `bytes`") 56 | flag.StringVar(&cpuprofile, "cpuprofile", "", 57 | "CPU profile `filename`") 58 | flag.StringVar(&memprofile, "memprofile", "", 59 | "Memory profile `filename`") 60 | flag.StringVar(&mutexprofile, "mutexprofile", "", 61 | "Mutex profile `filename`") 62 | flag.StringVar(&proxyURL, "proxy", "", 63 | "`URL` of a proxy to use for BitTorrent and tracker traffic.\n"+ 64 | "For tor, use \"socks5://127.0.0.1:9050\" and disable the DHT") 65 | flag.StringVar(&mountpoint, "mountpoint", "", 66 | "FUSE `mountpoint`") 67 | if dht.Available() { 68 | flag.StringVar(&dhtMode, "dht", "normal", "DHT mode") 69 | } 70 | flag.BoolVar(&config.DefaultUseTrackers, "use-trackers", false, 71 | "Use trackers (if available)") 72 | flag.BoolVar(&config.DefaultUseWebseeds, "use-webseeds", false, 73 | "Use webseeds (if available)") 74 | flag.Float64Var(&config.PrefetchRate, "prefetch-rate", 768*1024, 75 | "Prefetch `rate` in bytes per second") 76 | flag.BoolVar(&config.PreferEncryption, "prefer-encryption", true, 77 | "Prefer encrytion") 78 | flag.BoolVar(&config.ForceEncryption, "force-encryption", false, 79 | "Force encryption") 80 | flag.StringVar(&doPortmap, "portmap", "auto", "NAT port mappping `protocol` (natpmp, upnp, auto, or off)") 81 | flag.BoolVar(&config.MultipathTCP, "mptcp", false, 82 | "Use MP-TCP for peer-to-peer connections") 83 | flag.BoolVar(&config.Debug, "debug", false, 84 | "Log all BitTorrent messages") 85 | flag.Parse() 86 | 87 | if dht.Available() { 88 | m, err := config.ParseDhtMode(dhtMode) 89 | if err != nil { 90 | log.Fatalf("%v", err) 91 | } 92 | config.DefaultDhtMode = m 93 | } 94 | 95 | err = config.SetDefaultProxy(proxyURL) 96 | if err != nil { 97 | log.Printf("SetDefaultProxy: %v", err) 98 | return 99 | } 100 | 101 | if cpuprofile != "" { 102 | f, err := os.Create(cpuprofile) 103 | if err != nil { 104 | log.Printf("Create(cpuprofile): %v", err) 105 | return 106 | } 107 | pprof.StartCPUProfile(f) 108 | defer func() { 109 | pprof.StopCPUProfile() 110 | f.Close() 111 | }() 112 | } 113 | 114 | if memprofile != "" { 115 | defer func() { 116 | f, err := os.Create(memprofile) 117 | if err != nil { 118 | log.Printf("Create(memprofile): %v", err) 119 | return 120 | } 121 | pprof.WriteHeapProfile(f) 122 | f.Close() 123 | }() 124 | } 125 | 126 | if mutexprofile != "" { 127 | runtime.SetMutexProfileFraction(1) 128 | defer func() { 129 | f, err := os.Create(mutexprofile) 130 | if err != nil { 131 | log.Printf("Create(mutexprofile): %v", err) 132 | return 133 | } 134 | pprof.Lookup("mutex").WriteTo(f, 0) 135 | f.Close() 136 | }() 137 | } 138 | 139 | config.SetExternalIPv4Port(config.ProtocolPort, true) 140 | config.SetExternalIPv4Port(config.ProtocolPort, false) 141 | 142 | peer.UploadEstimator.Init(3 * time.Second) 143 | peer.UploadEstimator.Start() 144 | 145 | peer.DownloadEstimator.Init(3 * time.Second) 146 | peer.DownloadEstimator.Start() 147 | 148 | terminate := make(chan os.Signal, 1) 149 | signal.Notify(terminate, syscall.SIGINT) 150 | 151 | if mountpoint != "" { 152 | err := fuse.Serve(mountpoint) 153 | if err != nil { 154 | log.Fatalf("Couldn't mount directory: %v", err) 155 | } 156 | defer func(mountpoint string) { 157 | err := fuse.Close(mountpoint) 158 | if err != nil { 159 | log.Printf("Couldn't unmount directory: %v", err) 160 | } 161 | }(mountpoint) 162 | } 163 | 164 | ctx, cancelCtx := context.WithCancel(context.Background()) 165 | portmapdone := make(chan struct{}) 166 | defer func(portmapdone <-chan struct{}) { 167 | cancelCtx() 168 | log.Printf("Shutting down...") 169 | timer := time.NewTimer(4 * time.Second) 170 | select { 171 | case <-portmapdone: 172 | timer.Stop() 173 | case <-timer.C: 174 | } 175 | }(portmapdone) 176 | 177 | pmkind := 0 178 | switch doPortmap { 179 | case "off": 180 | pmkind = 0 181 | case "natpmp": 182 | pmkind = portmap.NATPMP 183 | case "upnp": 184 | pmkind = portmap.UPNP 185 | case "auto": 186 | pmkind = portmap.All 187 | default: 188 | log.Printf("Unknown portmapping kind %v", doPortmap) 189 | } 190 | 191 | if pmkind != 0 { 192 | go func() { 193 | err := portmap.Map(ctx, "STorrent", 194 | uint16(config.ProtocolPort), pmkind, 195 | func(proto string, status portmap.Status, err error) { 196 | if err != nil { 197 | log.Printf( 198 | "Port mapping: %v", err, 199 | ) 200 | } else if status.Lifetime > 0 { 201 | log.Printf( 202 | "Mapped %v %v->%v (%v)", 203 | proto, 204 | status.Internal, 205 | status.External, 206 | status.Lifetime, 207 | ) 208 | } else { 209 | log.Printf( 210 | "Unmapped %v %v", 211 | proto, 212 | status.Internal, 213 | ) 214 | } 215 | e := status.External 216 | if e == 0 { 217 | e = status.Internal 218 | } 219 | config.SetExternalIPv4Port( 220 | int(e), proto == "tcp", 221 | ) 222 | }, 223 | ) 224 | if err != nil { 225 | log.Println(err) 226 | } 227 | close(portmapdone) 228 | }() 229 | } else { 230 | close(portmapdone) 231 | } 232 | 233 | var dhtaddrs []netip.AddrPort 234 | var id []byte 235 | if config.DHTBootstrap != "" { 236 | id, dhtaddrs, err = rundht.Read(config.DHTBootstrap) 237 | if err != nil { 238 | log.Printf("Couldn't read %v: %v", 239 | config.DHTBootstrap, err) 240 | } 241 | } 242 | defer func() { 243 | if config.DHTBootstrap != "" { 244 | dir := filepath.Dir(config.DHTBootstrap) 245 | os.MkdirAll(dir, 0700) // ignore errors 246 | err := rundht.Write(config.DHTBootstrap, id) 247 | if err != nil { 248 | log.Printf("Couldn't write %v: %v", 249 | config.DHTBootstrap, err) 250 | } 251 | } 252 | }() 253 | 254 | config.DhtID = make([]byte, 20) 255 | if id != nil { 256 | copy(config.DhtID, id) 257 | } else { 258 | _, err := crand.Read(config.DhtID) 259 | if err != nil { 260 | log.Printf("Random: %v", err) 261 | return 262 | } 263 | } 264 | 265 | dhtevent, err := rundht.Run(ctx, config.DhtID, config.ProtocolPort) 266 | if err != nil { 267 | log.Printf("DHT: %v", err) 268 | return 269 | } 270 | 271 | go rundht.Handle(dhtevent) 272 | go rundht.Bootstrap(ctx, dhtaddrs) 273 | 274 | go func(args []string) { 275 | for _, arg := range args { 276 | proxy := config.DefaultProxy() 277 | t, err := tor.ReadMagnet(proxy, arg) 278 | if err != nil { 279 | log.Printf("Read magnet: %v", err) 280 | terminate <- nil 281 | return 282 | } 283 | if t == nil { 284 | t, err = tor.GetTorrent(ctx, proxy, arg) 285 | if err != nil { 286 | var perr tor.ParseURLError 287 | if !errors.As(err, &perr) { 288 | log.Printf("GetTorrent(%v): %v", 289 | arg, err) 290 | terminate <- nil 291 | return 292 | } 293 | } 294 | } 295 | if t == nil { 296 | torfile, err := os.Open(arg) 297 | if err != nil { 298 | log.Printf("Open(%v): %v", arg, err) 299 | terminate <- nil 300 | return 301 | } 302 | t, err = tor.ReadTorrent(proxy, torfile) 303 | if err != nil { 304 | log.Printf("%v: %v\n", arg, err) 305 | terminate <- nil 306 | torfile.Close() 307 | return 308 | } 309 | torfile.Close() 310 | } 311 | tor.AddTorrent(ctx, t) 312 | if t.InfoComplete() { 313 | t.Log.Printf("Added torrent %v (%v)\n", 314 | t.Name, t.Hash) 315 | } else { 316 | t.Log.Printf("Added torrent %v", t.Hash) 317 | } 318 | } 319 | }(flag.Args()) 320 | 321 | var lc net.ListenConfig 322 | if config.MultipathTCP { 323 | lc.SetMultipathTCP(true) 324 | } 325 | listener, err := 326 | lc.Listen(context.Background(), 327 | "tcp", fmt.Sprintf(":%v", config.ProtocolPort), 328 | ) 329 | if err != nil { 330 | log.Printf("Listen: %v", err) 331 | return 332 | } 333 | 334 | go listen(listener) 335 | 336 | err = http.Serve(config.HTTPAddr) 337 | if err != nil { 338 | log.Printf("Serve: %v", err) 339 | return 340 | } 341 | log.Printf("Listening on http://%v", config.HTTPAddr) 342 | 343 | go func() { 344 | min := 250 * time.Millisecond 345 | max := 16 * time.Second 346 | interval := max 347 | for { 348 | rc := tor.Expire() 349 | if rc < 0 { 350 | interval = interval / 2 351 | if interval < min { 352 | interval = min 353 | } 354 | } else if rc > 0 { 355 | interval = interval * 2 356 | if interval > max { 357 | interval = max 358 | } 359 | } 360 | time.Sleep(roughly(interval)) 361 | } 362 | }() 363 | 364 | <-terminate 365 | } 366 | 367 | func listen(listener net.Listener) { 368 | for { 369 | conn, err := listener.Accept() 370 | if err != nil { 371 | log.Printf("Accept: %v", err) 372 | time.Sleep(roughly(2 * time.Second)) 373 | continue 374 | } 375 | go func(conn net.Conn) { 376 | err = tor.Server(conn, crypto.DefaultOptions( 377 | config.PreferEncryption, 378 | config.ForceEncryption, 379 | )) 380 | if err != nil && err != io.EOF { 381 | log.Printf("Server: %v", err) 382 | } 383 | }(conn) 384 | } 385 | } 386 | 387 | func roughly(d time.Duration) time.Duration { 388 | r := d / 4 389 | if r > 2*time.Second { 390 | r = 2 * time.Second 391 | } 392 | m := rand.N(r) 393 | return d + m - r/2 394 | } 395 | --------------------------------------------------------------------------------