├── docs ├── ru │ ├── .keep │ └── readme.md ├── readme.md ├── en │ └── readme.md └── fr │ └── readme.md ├── debian ├── compat ├── xd.docs ├── xd.install ├── source │ └── format ├── xd.links ├── watch ├── .gitignore ├── changelog ├── xd.service ├── postinst ├── rules ├── postrm ├── prerm ├── control ├── preinst ├── copyright └── xd.init ├── .tool-versions ├── lib ├── dht │ ├── args.go │ ├── doc.go │ ├── find_node.go │ ├── context.go │ ├── node.go │ ├── serialize.go │ ├── payload.go │ ├── xdht.go │ ├── error.go │ └── message.go ├── util │ ├── doc.go │ ├── string.go │ ├── clientname.go │ ├── helpers.go │ ├── randbool.go │ ├── bw_test.go │ ├── ratio.go │ ├── string_compat.go │ ├── buffer.go │ ├── init.go │ ├── checkfile.go │ ├── ensuredir.go │ ├── helpers_unix.go │ ├── discard.go │ ├── randstr.go │ ├── zero.go │ ├── helpers_windows.go │ ├── writefull.go │ ├── bw.go │ ├── ensurefile.go │ └── rate.go ├── version │ ├── doc.go │ └── version.go ├── network │ ├── doc.go │ ├── i2p │ │ ├── doc.go │ │ ├── common.go │ │ ├── base.go │ │ ├── readline.go │ │ ├── addr.go │ │ ├── conn.go │ │ ├── session.go │ │ ├── listener.go │ │ ├── packetconn.go │ │ └── keyfile.go │ ├── inet │ │ ├── defaults.go │ │ ├── defaults_linux.go │ │ └── inet.go │ └── network.go ├── storage │ ├── doc.go │ ├── fs_settings.go │ ├── storage_test.go │ └── storage.go ├── constants │ └── gitprivacy.go ├── log │ ├── doc.go │ ├── log_windows.go │ ├── log_unix.go │ └── log.go ├── common │ ├── doc.go │ ├── infohash.go │ ├── piece.go │ └── peer.go ├── config │ ├── doc.go │ ├── defaults_lokinet.go │ ├── defaults.go │ ├── gnutella.go │ ├── log.go │ ├── lokinet.go │ ├── config.go │ ├── rpc.go │ ├── i2p.go │ ├── storage.go │ └── bittorrent.go ├── gnutella │ ├── handshake.go │ ├── swarm.go │ └── conn.go ├── tracker │ ├── doc.go │ ├── announce.go │ └── http.go ├── metainfo │ ├── test.torrent │ ├── doc.go │ ├── metainfo_test.go │ └── metainfo.go ├── bittorrent │ ├── doc.go │ ├── swarm │ │ ├── doc.go │ │ ├── swarm_test.go │ │ ├── piece_test.go │ │ ├── defaults.go │ │ ├── defaults_lokinet.go │ │ ├── event.go │ │ ├── list.go │ │ ├── pex.go │ │ ├── announce.go │ │ ├── holder.go │ │ └── status.go │ ├── extensions │ │ ├── dht.go │ │ ├── pex.go │ │ ├── ut_metadata.go │ │ └── extensions.go │ └── handshake.go ├── translate │ ├── vars_windows.go │ ├── vars_linux.go │ ├── vars_unix.go │ └── translate.go ├── rpc │ ├── transmission │ │ ├── handler.go │ │ ├── request.go │ │ ├── response.go │ │ ├── types.go │ │ ├── handler_notimpl.go │ │ ├── util.go │ │ ├── consts.go │ │ ├── token.go │ │ ├── handler_torrent_get.go │ │ └── rpc.go │ ├── consts.go │ ├── assets │ │ ├── noweb.go │ │ └── web.go │ ├── request.go │ ├── io.go │ ├── rpc_rpcerror.go │ ├── methods.go │ ├── rpc_swarmcount.go │ ├── response.go │ ├── rpc_listtorrentstatus.go │ ├── rpc_listtorrent.go │ ├── rpc_addtorrent.go │ ├── rpc_setopt.go │ ├── rpc_torrentstatus.go │ ├── rpc_changetorrent.go │ ├── client.go │ └── server.go ├── sync │ ├── sync.go │ └── sync_compat.go ├── fs │ ├── fs.go │ └── std.go ├── stats │ └── tracker.go ├── mktorrent │ └── mktorrent.go └── configparser │ ├── example_test.go │ └── README.md ├── contrib ├── hooks │ └── pre-commit ├── py │ ├── anodex_requirements.txt │ └── anodex.py ├── logos │ ├── xd_logo.png │ ├── xd_logo.xcf │ ├── xd_logo_8x8.png │ ├── xd_logo_16x16.png │ ├── xd_logo_22x22.png │ ├── xd_logo_24x24.png │ ├── xd_logo_32x32.png │ ├── xd_logo_48x48.png │ ├── xd_logo_128x128.png │ ├── xd_logo_256x256.png │ └── xd_logo_512x512.png ├── webui │ ├── .gitignore │ ├── readme.md │ ├── server.js │ ├── Makefile │ ├── package.json │ ├── docroot │ │ └── index.html │ ├── css │ │ └── main.css │ └── lib │ │ └── main.js ├── systemd │ └── xd.service ├── apparmor │ └── usr.bin.XD └── docker │ └── Dockerfile ├── config.cson ├── .vscode ├── settings.json └── tasks.json ├── .gitignore ├── .github ├── CONTRIBUTING.md └── issue_template.md ├── main.go ├── go.mod ├── .dir-locals.el ├── LICENSE ├── CODE_OF_CONDUCT.md ├── release.sh ├── Makefile ├── README.md ├── go.sum └── cmd └── rpc └── rpc.go /docs/ru/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/xd.docs: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.25.0 2 | -------------------------------------------------------------------------------- /debian/xd.install: -------------------------------------------------------------------------------- 1 | XD usr/bin 2 | -------------------------------------------------------------------------------- /lib/dht/args.go: -------------------------------------------------------------------------------- 1 | package dht 2 | -------------------------------------------------------------------------------- /lib/dht/doc.go: -------------------------------------------------------------------------------- 1 | package dht 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /contrib/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | make test -------------------------------------------------------------------------------- /lib/util/doc.go: -------------------------------------------------------------------------------- 1 | // various utilities 2 | package util 3 | -------------------------------------------------------------------------------- /lib/version/doc.go: -------------------------------------------------------------------------------- 1 | // version info 2 | package version 3 | -------------------------------------------------------------------------------- /config.cson: -------------------------------------------------------------------------------- 1 | "go-plus": 2 | gopath: "{{ProjectDir}}" 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.gopath": "${workspaceRoot}" 3 | } -------------------------------------------------------------------------------- /contrib/py/anodex_requirements.txt: -------------------------------------------------------------------------------- 1 | transmissionrpc 2 | requests 3 | -------------------------------------------------------------------------------- /lib/network/doc.go: -------------------------------------------------------------------------------- 1 | // network abstraction 2 | package network 3 | -------------------------------------------------------------------------------- /lib/dht/find_node.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | type FindNode struct { 4 | } 5 | -------------------------------------------------------------------------------- /lib/network/i2p/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | i2p connector 4 | */ 5 | package i2p 6 | -------------------------------------------------------------------------------- /lib/storage/doc.go: -------------------------------------------------------------------------------- 1 | // bittorrent data storage backend 2 | package storage 3 | -------------------------------------------------------------------------------- /debian/xd.links: -------------------------------------------------------------------------------- 1 | usr/bin/XD usr/bin/XD-cli 2 | var/lib/XD/xd.ini etc/XD/xd.ini 3 | -------------------------------------------------------------------------------- /lib/constants/gitprivacy.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const UseGitVersion = true 4 | -------------------------------------------------------------------------------- /lib/log/doc.go: -------------------------------------------------------------------------------- 1 | // Package log provides xd's minimal logger framework 2 | package log 3 | -------------------------------------------------------------------------------- /lib/common/doc.go: -------------------------------------------------------------------------------- 1 | // Package common provides common data structures 2 | package common 3 | -------------------------------------------------------------------------------- /lib/config/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | 4 | configuration loader/saver 5 | */ 6 | package config 7 | -------------------------------------------------------------------------------- /lib/gnutella/handshake.go: -------------------------------------------------------------------------------- 1 | package gnutella 2 | 3 | const Handshake = "GNUTELLA CONNECT/0.6" 4 | -------------------------------------------------------------------------------- /lib/tracker/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | bittorrent announce via tracker 4 | */ 5 | package tracker 6 | -------------------------------------------------------------------------------- /contrib/logos/xd_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo.png -------------------------------------------------------------------------------- /contrib/logos/xd_logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo.xcf -------------------------------------------------------------------------------- /lib/metainfo/test.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/lib/metainfo/test.torrent -------------------------------------------------------------------------------- /contrib/logos/xd_logo_8x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo_8x8.png -------------------------------------------------------------------------------- /contrib/logos/xd_logo_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo_16x16.png -------------------------------------------------------------------------------- /contrib/logos/xd_logo_22x22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo_22x22.png -------------------------------------------------------------------------------- /contrib/logos/xd_logo_24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo_24x24.png -------------------------------------------------------------------------------- /contrib/logos/xd_logo_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo_32x32.png -------------------------------------------------------------------------------- /contrib/logos/xd_logo_48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo_48x48.png -------------------------------------------------------------------------------- /lib/bittorrent/doc.go: -------------------------------------------------------------------------------- 1 | // Package bittorrent provides bittorrent related structures 2 | package bittorrent 3 | -------------------------------------------------------------------------------- /lib/dht/context.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | type Context interface { 4 | GetClosestNode(id []byte) Node 5 | } 6 | -------------------------------------------------------------------------------- /lib/metainfo/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | package for parsing bitorrent meta info objects 4 | */ 5 | package metainfo 6 | -------------------------------------------------------------------------------- /contrib/logos/xd_logo_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo_128x128.png -------------------------------------------------------------------------------- /contrib/logos/xd_logo_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo_256x256.png -------------------------------------------------------------------------------- /contrib/logos/xd_logo_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majestrate/XD/HEAD/contrib/logos/xd_logo_512x512.png -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # XD documentation 2 | 3 | * [English](en/) 4 | 5 | * [French](fr/) 6 | 7 | * [Russian](ru/) 8 | -------------------------------------------------------------------------------- /lib/dht/node.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | type Node interface { 4 | SendLowLevel([]byte) error 5 | ID() string 6 | } 7 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/doc.go: -------------------------------------------------------------------------------- 1 | // swarm package provides compelete bittorrent client that tracks 1 swarm 2 | package swarm 3 | -------------------------------------------------------------------------------- /lib/network/inet/defaults.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package inet 5 | 6 | const DefaultDNSAddr = "127.0.0.1:53" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *\#* 3 | 4 | # assets file is generated 5 | lib/rpc/assets/assets.go 6 | XD-* 7 | build-assets 8 | *.ini 9 | 10 | pkg/ -------------------------------------------------------------------------------- /lib/network/inet/defaults_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package inet 5 | 6 | const DefaultDNSAddr = "127.3.2.1:53" 7 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/swarm_test.go: -------------------------------------------------------------------------------- 1 | package swarm 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSwarm(t *testing.T) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /lib/util/string.go: -------------------------------------------------------------------------------- 1 | //go:build go1.8 2 | // +build go1.8 3 | 4 | package util 5 | 6 | import "strings" 7 | 8 | var StringCompare = strings.Compare 9 | -------------------------------------------------------------------------------- /lib/translate/vars_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package translate 5 | 6 | const Path = "translations" 7 | const env = "" 8 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | version=3 2 | opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/XD-$1\.tar\.gz/ \ 3 | https://github.com/majestrate/XD/tags .*/v?(\d\S+)\.tar\.gz 4 | -------------------------------------------------------------------------------- /lib/util/clientname.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func ClientNameFromID(id []byte) (name string) { 4 | // TODO: implement 5 | name = "idklol" 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /contrib/webui/.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | 4 | tmp/ 5 | docroot/xd.min.js 6 | docroot/xd.css 7 | docroot/contrib/ 8 | docroot/favicon.png 9 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | xd/ 2 | debhelper-build-stamp 3 | files 4 | xd.debhelper.log 5 | xd.postinst.debhelper 6 | xd.postrm.debhelper 7 | xd.prerm.debhelper 8 | xd.substvars 9 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | xd (0.1.0~pre4-1) unstable; urgency=low 2 | 3 | * initial release 0.1.0-pre4 4 | 5 | -- r4sas Fri, 16 Mar 2018 00:00:00 +0000 6 | -------------------------------------------------------------------------------- /lib/dht/serialize.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import "github.com/zeebo/bencode" 4 | 5 | type Serialize interface { 6 | bencode.Marshaler 7 | bencode.Unmarshaler 8 | } 9 | -------------------------------------------------------------------------------- /lib/translate/vars_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package translate 5 | 6 | const Path = "/usr/share/XD/translations" 7 | const env = "LC_ALL" 8 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | * be awesome to each other 3 | * fun is required, don't be a killjoy 4 | * `cp contrib/hooks/pre-commit .git/hooks/` 5 | * **vendor everything** 6 | -------------------------------------------------------------------------------- /lib/rpc/transmission/handler.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import "github.com/majestrate/XD/lib/bittorrent/swarm" 4 | 5 | type Handler func(*swarm.Swarm, Args) Response 6 | -------------------------------------------------------------------------------- /lib/dht/payload.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | type Payload interface { 4 | Serialize 5 | Process(ctx Context, ch chan *Message) // reply to this message by sending the reply down ch 6 | } 7 | -------------------------------------------------------------------------------- /lib/util/helpers.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "net/url" 4 | 5 | func SchemePath(u *url.URL) (scheme string, path string) { 6 | urlSchemePath(u, &scheme, &path) 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /lib/rpc/consts.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | const ParamInfohash = "infohash" 4 | const ParamURL = "url" 5 | const ParamN = "n" 6 | const ParamAction = "action" 7 | const ParamSwarms = "swarms" 8 | -------------------------------------------------------------------------------- /lib/translate/vars_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | package translate 5 | 6 | const Path = "/usr/local/XD/translations" 7 | const env = "LC_ALL" 8 | -------------------------------------------------------------------------------- /lib/util/randbool.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "math/rand" 4 | 5 | func RandBoolPercent(percent uint8) bool { 6 | return rand.Float32()*float32(100-percent) > float32(percent) 7 | } 8 | -------------------------------------------------------------------------------- /lib/log/log_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package log 5 | 6 | var colorReset string 7 | 8 | func (l logLevel) Color() string { 9 | return "" 10 | } 11 | -------------------------------------------------------------------------------- /lib/rpc/transmission/request.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | type Request struct { 4 | Method string `json:"method"` 5 | Args Args `json:"arguments"` 6 | Tag Tag `json:"tag"` 7 | } 8 | -------------------------------------------------------------------------------- /lib/rpc/transmission/response.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | type Response struct { 4 | Result string `json:"result"` 5 | Args Args `json:"arguments"` 6 | Tag Tag `json:"tag"` 7 | } 8 | -------------------------------------------------------------------------------- /lib/rpc/assets/noweb.go: -------------------------------------------------------------------------------- 1 | //go:build !webui 2 | // +build !webui 3 | 4 | package assets 5 | 6 | import ( 7 | "net/http" 8 | ) 9 | 10 | func GetAssets() http.FileSystem { 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /lib/util/bw_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestFormatRate(t *testing.T) { 6 | 7 | rate := float64(1000000.5) 8 | t.Logf("rate %f %s", rate, FormatRate(rate)) 9 | } 10 | -------------------------------------------------------------------------------- /lib/rpc/transmission/types.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | type Args map[string]interface{} 4 | type Tag int64 5 | 6 | type TorrentID int64 7 | type TorrentIDArray []TorrentID 8 | 9 | type TorrentFields []string 10 | -------------------------------------------------------------------------------- /lib/util/ratio.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "math" 4 | 5 | func Ratio(tx, rx float64) (r float64) { 6 | if rx > 0 { 7 | r = tx / rx 8 | } else if tx > 0 { 9 | r = math.Inf(1) 10 | } 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /lib/util/string_compat.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.8 2 | // +build !go1.8 3 | 4 | package util 5 | 6 | import "bytes" 7 | 8 | func StringCompare(a, b string) int { 9 | return bytes.Compare([]byte(a), []byte(b)) 10 | } 11 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/piece_test.go: -------------------------------------------------------------------------------- 1 | package swarm 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/log" 5 | "testing" 6 | ) 7 | 8 | func TestPieceRequester(t *testing.T) { 9 | log.SetLevel("debug") 10 | 11 | } 12 | -------------------------------------------------------------------------------- /lib/util/buffer.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type Buffer struct { 8 | bytes.Buffer 9 | } 10 | 11 | // Close implements io.Closer 12 | func (b *Buffer) Close() error { 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /lib/util/init.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var startTime time.Time 8 | 9 | func init() { 10 | startTime = time.Now() 11 | } 12 | 13 | func StartedAt() time.Time { 14 | return startTime 15 | } 16 | -------------------------------------------------------------------------------- /contrib/webui/readme.md: -------------------------------------------------------------------------------- 1 | # WebUI 2 | 3 | XD web ui source code 4 | 5 | 6 | ## dependencies 7 | 8 | * nodejs 9 | * GNU Make 10 | 11 | ## building 12 | 13 | when the webui is ready you can build it by running: 14 | 15 | $ make 16 | -------------------------------------------------------------------------------- /lib/bittorrent/extensions/dht.go: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | // bittorrent extension for i2p's dht variant 4 | const I2PDHT = Extension("i2p_dht") 5 | 6 | // bittorrent extension for XD's dht variant over wire protocol 7 | const XDHT = Extension("xdht") 8 | -------------------------------------------------------------------------------- /lib/util/checkfile.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // CheckFile returns true if a file exists 8 | func CheckFile(fpath string) (exists bool) { 9 | _, err := os.Stat(fpath) 10 | exists = !os.IsNotExist(err) 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /lib/util/ensuredir.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // ensure a directory is made 8 | // returns error if it can't be made 9 | func EnsureDir(fpath string) (err error) { 10 | err = os.MkdirAll(fpath, os.ModePerm) 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /lib/util/helpers_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package util 4 | 5 | import ( 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | func urlSchemePath(u *url.URL, scheme, path *string) { 11 | *scheme = strings.ToLower(u.Scheme) 12 | *path = u.Path 13 | } 14 | -------------------------------------------------------------------------------- /lib/sync/sync.go: -------------------------------------------------------------------------------- 1 | //go:build go1.9 2 | // +build go1.9 3 | 4 | package sync 5 | 6 | import ( 7 | "sync" 8 | ) 9 | 10 | type Mutex = sync.Mutex 11 | type RWMutex = sync.RWMutex 12 | type WaitGroup = sync.WaitGroup 13 | type Map = sync.Map 14 | type Pool = sync.Pool 15 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/defaults.go: -------------------------------------------------------------------------------- 1 | //go:build !lokinet 2 | // +build !lokinet 3 | 4 | package swarm 5 | 6 | import "github.com/majestrate/XD/lib/bittorrent/extensions" 7 | 8 | const DefaultMaxParallelRequests = 4 9 | const DefaultPEXDialect = extensions.I2PPeerExchange 10 | -------------------------------------------------------------------------------- /lib/util/discard.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type discard struct { 4 | } 5 | 6 | func (d discard) Write(data []byte) (n int, err error) { 7 | n = len(data) 8 | return 9 | } 10 | 11 | func (d discard) Close() (err error) { 12 | return 13 | } 14 | 15 | var Discard discard 16 | -------------------------------------------------------------------------------- /contrib/systemd/xd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Standalone I2P BitTorrent Client 3 | 4 | [Service] 5 | User=xd 6 | Group=xd 7 | WorkingDirectory=/var/lib/XD 8 | ExecStart=/usr/bin/XD /var/lib/XD/xd.ini 9 | Restart=on-failure 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /lib/util/randstr.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base32" 6 | "io" 7 | ) 8 | 9 | func RandStr(l int) string { 10 | buff := make([]byte, l) 11 | io.ReadFull(rand.Reader, buff) 12 | return base32.StdEncoding.EncodeToString(buff)[:l] 13 | } 14 | -------------------------------------------------------------------------------- /debian/xd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Standalone I2P BitTorrent Client 3 | 4 | [Service] 5 | User=debian-xd 6 | Group=debian-xd 7 | WorkingDirectory=/var/lib/XD 8 | ExecStart=/usr/bin/XD /var/lib/XD/xd.ini 9 | Restart=on-failure 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/defaults_lokinet.go: -------------------------------------------------------------------------------- 1 | //go:build lokinet 2 | // +build lokinet 3 | 4 | package swarm 5 | 6 | import "github.com/majestrate/XD/lib/bittorrent/extensions" 7 | 8 | const DefaultMaxParallelRequests = 48 9 | const DefaultPEXDialect = extensions.LokinetPeerExchange 10 | -------------------------------------------------------------------------------- /lib/network/i2p/common.go: -------------------------------------------------------------------------------- 1 | package i2p 2 | 3 | // default address of i2p interface 4 | const DEFAULT_ADDRESS = "127.0.0.1:7656" 5 | 6 | // default path to private keys 7 | const DEFAULT_KEYFILE = "xd-privkey.dat" 8 | 9 | // default i2p session name 10 | const DEFAULT_NAME = "XD" 11 | -------------------------------------------------------------------------------- /lib/rpc/transmission/handler_notimpl.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/bittorrent/swarm" 5 | ) 6 | 7 | func NotImplemented(sw *swarm.Swarm, args Args) (resp Response) { 8 | resp.Result = notImplemented 9 | resp.Args = make(Args) 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /lib/config/defaults_lokinet.go: -------------------------------------------------------------------------------- 1 | //go:build lokinet 2 | // +build lokinet 3 | 4 | package config 5 | 6 | const DisableLokinetByDefault = false 7 | const DisableI2PByDefault = true 8 | 9 | var DefaultOpenTrackers = map[string]string{ 10 | "default": "http://probably.loki:6680/announce", 11 | } 12 | -------------------------------------------------------------------------------- /lib/util/zero.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type zeroReader struct { 4 | } 5 | 6 | func (z *zeroReader) Read(d []byte) (int, error) { 7 | i := 0 8 | for i < len(d) { 9 | d[i] = 0 10 | i++ 11 | } 12 | return len(d), nil 13 | } 14 | 15 | // reader that reads zeros 16 | var Zero = new(zeroReader) 17 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | XDHOME='/var/lib/XD' 6 | XDUSER='debian-xd' 7 | 8 | if [ "$1" != "configure" ]; then 9 | exit 0 10 | fi 11 | 12 | if [ ! -e "${XDHOME}/xd.ini" ]; then 13 | cd $XDHOME && su -p $XDUSER -c "XD --genconf ${XDHOME}/xd.ini" 14 | fi 15 | 16 | #DEBHELPER# 17 | 18 | exit 0 19 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/event.go: -------------------------------------------------------------------------------- 1 | package swarm 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/common" 5 | ) 6 | 7 | // an event triggered when we get an inbound wire message from a peer we are connected with on this torrent asking for a piece 8 | type pieceEvent struct { 9 | c *PeerConn 10 | r *common.PieceRequest 11 | } 12 | -------------------------------------------------------------------------------- /lib/bittorrent/extensions/pex.go: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | // I2PPeerExchange is a BitTorrent Extension indicating we support PEX variant for i2p 4 | const I2PPeerExchange = Extension("i2p_pex") 5 | 6 | // LokinetPeerExchange is a Bittorrent Extension indication we support Lokinet PEX 7 | const LokinetPeerExchange = Extension("ln_pex") 8 | -------------------------------------------------------------------------------- /lib/network/i2p/base.go: -------------------------------------------------------------------------------- 1 | package i2p 2 | 3 | import ( 4 | "encoding/base32" 5 | "encoding/base64" 6 | ) 7 | 8 | var ( 9 | i2pB64enc *base64.Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~") 10 | i2pB32enc *base32.Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567") 11 | ) 12 | -------------------------------------------------------------------------------- /lib/network/i2p/readline.go: -------------------------------------------------------------------------------- 1 | package i2p 2 | 3 | import "io" 4 | 5 | func readLine(r io.Reader, buff []byte) (line string, err error) { 6 | var n int 7 | for err == nil { 8 | n, err = r.Read(buff[:]) 9 | if err == nil { 10 | line += string(buff[:]) 11 | if buff[n-1] == 10 { 12 | break 13 | } 14 | } 15 | } 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /lib/util/helpers_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package util 4 | 5 | import ( 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | func urlSchemePath(u *url.URL, scheme, path *string) { 11 | sch := strings.ToLower(u.Scheme) 12 | if len(sch) == 1 { 13 | // something like C:/wahtever 14 | sch = "file" 15 | } 16 | *path = u.String() 17 | *scheme = sch 18 | } 19 | -------------------------------------------------------------------------------- /lib/log/log_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package log 5 | 6 | var colorReset = "\x1b[0;0m" 7 | 8 | func (l logLevel) Color() string { 9 | switch l { 10 | case debug: 11 | return "\x1b[37;0m" 12 | case info: 13 | return "\x1b[37;1m" 14 | case warn: 15 | return "\x1b[33;1m" 16 | default: 17 | return "\x1b[31;1m" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/rpc/request.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/bittorrent/swarm" 5 | ) 6 | 7 | type BaseRequest struct { 8 | Swarm string `json:"-"` 9 | } 10 | 11 | type Request interface { 12 | // handle request on server 13 | ProcessRequest(sw *swarm.Swarm, w *ResponseWriter) 14 | // convert request to json 15 | MarshalJSON() ([]byte, error) 16 | } 17 | -------------------------------------------------------------------------------- /lib/rpc/io.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type rpcIO struct { 8 | w io.Writer 9 | r io.ReadCloser 10 | } 11 | 12 | func (r *rpcIO) Close() error { 13 | return r.r.Close() 14 | } 15 | 16 | func (r *rpcIO) Write(d []byte) (int, error) { 17 | return r.w.Write(d) 18 | } 19 | 20 | func (r *rpcIO) Read(d []byte) (int, error) { 21 | return r.r.Read(d) 22 | } 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/majestrate/XD/cmd/rpc" 5 | "github.com/majestrate/XD/cmd/xd" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | func main() { 12 | exename := filepath.Base(strings.ToUpper(os.Args[0])) 13 | docli := exename == "XD-CLI" || exename == "XD-CLI.EXE" 14 | if docli { 15 | rpc.Run() 16 | } else { 17 | xd.Run() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // a network session 8 | type Network interface { 9 | Dial(n, a string) (net.Conn, error) 10 | Accept() (net.Conn, error) 11 | ReadFrom([]byte) (int, net.Addr, error) 12 | WriteTo([]byte, net.Addr) (int, error) 13 | Open() error 14 | Close() error 15 | Addr() net.Addr 16 | Lookup(name, port string) (net.Addr, error) 17 | } 18 | -------------------------------------------------------------------------------- /lib/rpc/assets/web.go: -------------------------------------------------------------------------------- 1 | //go:build webui 2 | // +build webui 3 | 4 | package assets 5 | 6 | import ( 7 | "embed" 8 | "net/http" 9 | ) 10 | 11 | // content holds our static web server content. 12 | // 13 | //go:embed favicon.png 14 | //go:embed xd.min.js 15 | //go:embed xd.css 16 | //go:embed index.html 17 | var content embed.FS 18 | 19 | func GetAssets() http.FileSystem { 20 | return http.FS(content) 21 | } 22 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ## info 2 | 3 | git revision / version: [git revision or version here] 4 | 5 | OS: [os here] 6 | 7 | Architecture: [architecture here] 8 | 9 | ## problem 10 | 11 | [insert description of problem here] 12 | 13 | ## backtrace / error messages 14 | 15 | Error messages: [yes/no] 16 | 17 | [insert any error messages here] 18 | 19 | Backtrace: [yes/no] 20 | 21 | [insert any backtraces here] 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/majestrate/XD 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/pkg/sftp v1.13.5 7 | github.com/zeebo/bencode v1.0.0 8 | golang.org/x/crypto v0.45.0 9 | gopkg.in/leonelquinteros/gotext.v1 v1.3.1 10 | ) 11 | 12 | require ( 13 | github.com/kr/fs v0.1.0 // indirect 14 | github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect 15 | golang.org/x/sys v0.38.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | #export DH_VERBOSE = 1 5 | 6 | PREFIX=/usr 7 | 8 | %: 9 | dh $@ 10 | 11 | override_dh_auto_install: 12 | mkdir -p debian/xd/etc/apparmor.d 13 | cp contrib/apparmor/usr.bin.XD debian/xd/etc/apparmor.d/usr.bin.XD 14 | dh_apparmor --profile-name=usr.bin.XD -pxd 15 | dh_auto_install -------------------------------------------------------------------------------- /debian/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | case "$1" in 6 | purge) 7 | rm -rf /var/lib/XD /etc/XD 8 | deluser --remove-home debian-xd 9 | ;; 10 | 11 | remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) 12 | ;; 13 | 14 | *) 15 | echo "postrm called with unknown argument \`$1'" >&2 16 | exit 1 17 | ;; 18 | esac 19 | 20 | #DEBHELPER# 21 | 22 | exit 0 23 | -------------------------------------------------------------------------------- /lib/gnutella/swarm.go: -------------------------------------------------------------------------------- 1 | package gnutella 2 | 3 | type Swarm struct { 4 | activeConns []*Conn 5 | } 6 | 7 | func (sw *Swarm) AddInboundPeer(conn *Conn) { 8 | sw.activeConns = append(sw.activeConns, conn) 9 | } 10 | 11 | func (sw *Swarm) Close() error { 12 | for _, conn := range sw.activeConns { 13 | conn.Close() 14 | } 15 | sw.activeConns = []*Conn{} 16 | return nil 17 | } 18 | 19 | func NewSwarm() *Swarm { 20 | return &Swarm{} 21 | } 22 | -------------------------------------------------------------------------------- /lib/util/writefull.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/log" 5 | "io" 6 | ) 7 | 8 | // ensure a byteslices is written in full 9 | func WriteFull(w io.Writer, d []byte) (err error) { 10 | var n int 11 | l := len(d) 12 | for n < l { 13 | var o int 14 | o, err = w.Write(d[n:]) 15 | if err == nil { 16 | log.Debugf("wrote %d of %d", o, l) 17 | n += o 18 | } else { 19 | break 20 | } 21 | } 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /lib/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "github.com/majestrate/XD/lib/constants" 6 | ) 7 | 8 | const Name = "XD" 9 | 10 | var Major = "0" 11 | 12 | var Minor = "4" 13 | 14 | var Patch = "7" 15 | 16 | var Git string 17 | 18 | func Version() string { 19 | v := fmt.Sprintf("%s-%s.%s.%s", Name, Major, Minor, Patch) 20 | if len(Git) > 0 && constants.UseGitVersion { 21 | v += fmt.Sprintf("-%s", Git) 22 | } 23 | return v 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "type": "shell", 9 | "command": "make" 10 | }, 11 | { 12 | "label": "clean", 13 | "type": "shell", 14 | "command": "make clean" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /lib/bittorrent/swarm/list.go: -------------------------------------------------------------------------------- 1 | package swarm 2 | 3 | import "github.com/majestrate/XD/lib/util" 4 | 5 | type InfohashList []string 6 | 7 | func (l InfohashList) Len() int { 8 | return len(l) 9 | } 10 | 11 | func (l InfohashList) Less(i, j int) bool { 12 | return util.StringCompare(l[i], l[j]) < 0 13 | } 14 | 15 | func (l *InfohashList) Swap(i, j int) { 16 | (*l)[i], (*l)[j] = (*l)[j], (*l)[i] 17 | } 18 | 19 | type TorrentsList struct { 20 | Infohashes InfohashList 21 | } 22 | -------------------------------------------------------------------------------- /lib/rpc/rpc_rpcerror.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | ) 7 | 8 | type rpcError struct { 9 | message string 10 | } 11 | 12 | func (e *rpcError) MarshalJSON() (data []byte, err error) { 13 | data, err = json.Marshal(map[string]string{ 14 | "error": e.message, 15 | }) 16 | return 17 | } 18 | 19 | func (e *rpcError) ProcessRequest(sw *swarm.Swarm, w *ResponseWriter) { 20 | w.SendError(e.message) 21 | } 22 | -------------------------------------------------------------------------------- /contrib/webui/server.js: -------------------------------------------------------------------------------- 1 | var docroot = "docroot"; 2 | var backend = "http://127.0.0.1:1488"; 3 | 4 | 5 | var express = require("express"); 6 | var httpProxy = require("http-proxy"); 7 | var apiProxy = httpProxy.createProxyServer(); 8 | 9 | var app = express(); 10 | app.post("/ecksdee/api", function(req, res) { 11 | apiProxy.web(req, res, {target: backend}); 12 | }); 13 | app.use("/", express.static(docroot)); 14 | 15 | app.listen(48000, function() { console.log("listening on 0.0.0.0:48000")}); 16 | -------------------------------------------------------------------------------- /lib/util/bw.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | var rateUnits = []string{"B", "KB", "MB", "GB", "TB", "PB"} 9 | 10 | // FormatRate formats a floating point b/s as string with closest unit 11 | func FormatRate(rate float64) (str string) { 12 | if math.IsInf(rate, 0) { 13 | str = "infinity" 14 | return 15 | } 16 | var rateIdx int 17 | for rate > 1024.0 { 18 | rate /= 1024.0 19 | rateIdx++ 20 | } 21 | str = fmt.Sprintf("%.2f%s/sec", rate, rateUnits[rateIdx]) 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;; thanks stack overflow 2 | ;; https://stackoverflow.com/questions/4012321/how-can-i-access-the-path-to-the-current-directory-in-an-emacs-directory-variabl 3 | ((nil . ((eval . (set (make-local-variable 'my-project-path) 4 | (file-name-directory 5 | (let ((d (dir-locals-find-file "."))) 6 | (if (stringp d) d (car d)))))) 7 | (eval . (setenv "GOPATH" my-project-path)) 8 | (eval . (message "Project directory set to `%s'." my-project-path))))) 9 | -------------------------------------------------------------------------------- /lib/rpc/methods.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | const RPCPath = "/ecksdee/api" 4 | const RPCName = "XD" 5 | 6 | const RPCListTorrents = RPCName + ".ListTorrents" 7 | const RPCListTorrentStatus = RPCName + ".SwarmStatus" 8 | const RPCTorrentStatus = RPCName + ".TorrentStatus" 9 | const RPCAddTorrent = RPCName + ".AddTorrent" 10 | const RPCDelTorrent = RPCName + ".DelTorrent" 11 | const RPCSetPieceWindow = RPCName + ".SetPieceWindow" 12 | const RPCChangeTorrent = RPCName + ".ChangeTorrent" 13 | const RPCSwarmCount = RPCName + ".SwarmCount" 14 | -------------------------------------------------------------------------------- /contrib/apparmor/usr.bin.XD: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | profile XD /usr/bin/{XD,XD-cli} { 4 | #include 5 | #include 6 | #include 7 | 8 | @{sys}/kernel/mm/transparent_hugepage/hpage_pmd_size r, 9 | @{PROC}/sys/net/core/somaxconn r, 10 | /etc/mime.types r, 11 | 12 | owner @{HOME}/.config/XD/** r, 13 | owner @{HOME}/.cache/XD/{**,} rw, 14 | owner @{HOME}/.config/XD/metadata/{**,} rw, 15 | 16 | #include if exists 17 | } 18 | -------------------------------------------------------------------------------- /lib/gnutella/conn.go: -------------------------------------------------------------------------------- 1 | package gnutella 2 | 3 | import ( 4 | "net" 5 | "net/textproto" 6 | ) 7 | 8 | type Conn struct { 9 | c net.Conn 10 | tpc *textproto.Conn 11 | } 12 | 13 | func (c *Conn) Handshake(reject bool) (err error) { 14 | 15 | if reject { 16 | return c.tpc.PrintfLine("GNUTELLA/0.6 503 Rejected") 17 | } 18 | return err 19 | } 20 | 21 | func (c *Conn) Close() error { 22 | return c.c.Close() 23 | } 24 | 25 | func NewConn(c net.Conn) *Conn { 26 | return &Conn{ 27 | c: c, 28 | tpc: textproto.NewConn(c), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contrib/webui/Makefile: -------------------------------------------------------------------------------- 1 | REPO := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 2 | CSS_ROOT = $(REPO)/css 3 | JS_ROOT = $(REPO)/lib 4 | DOC_ROOT = $(REPO)/docroot 5 | CSS = $(DOC_ROOT)/xd.css 6 | JS = $(DOC_ROOT)/xd.min.js 7 | 8 | CAT = cat 9 | RM = rm -f 10 | 11 | NODE = $(shell which node || true) 12 | 13 | all: build 14 | 15 | build: $(CSS) $(JS) 16 | 17 | $(CSS): 18 | $(CAT) $(CSS_ROOT)/*.css > $(CSS) 19 | 20 | $(JS): 21 | $(CAT) $(JS_ROOT)/*.js > $(JS) 22 | 23 | run: 24 | $(NODE) server.js 25 | 26 | clean: 27 | $(RM) $(CSS) $(JS) 28 | -------------------------------------------------------------------------------- /lib/translate/translate.go: -------------------------------------------------------------------------------- 1 | package translate 2 | 3 | import ( 4 | "gopkg.in/leonelquinteros/gotext.v1" 5 | "os" 6 | ) 7 | 8 | // default locale to use 9 | const DefaultLocale = "en_US" 10 | const Domain = "default" 11 | 12 | func init() { 13 | lc := os.Getenv(env) 14 | if lc == "" { 15 | lc = DefaultLocale 16 | } 17 | gotext.Configure(Path, lc, Domain) 18 | } 19 | 20 | var TN = gotext.GetN 21 | var T = gotext.Get 22 | 23 | /** convert error to string */ 24 | func E(err error) (str string) { 25 | if err != nil { 26 | str = T(err.Error()) 27 | } 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | case "$1" in 6 | remove|deconfigure|remove-in-favour|deconfigure-in-favour) 7 | if [ -x "/etc/init.d/xd" ]; then 8 | if [ -x "`which invoke-rc.d 2>/dev/null`" ]; then 9 | invoke-rc.d xd stop || exit $? 10 | else 11 | /etc/init.d/xd stop || exit $? 12 | fi 13 | fi 14 | ;; 15 | upgrade|failed-upgrade) 16 | ;; 17 | *) 18 | echo "prerm called with unknown argument \`$1'" >&2 19 | ;; 20 | esac 21 | 22 | #DEBHELPER# 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /lib/rpc/rpc_swarmcount.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | ) 7 | 8 | type SwarmCountRequest struct { 9 | BaseRequest 10 | N int 11 | } 12 | 13 | func (scr *SwarmCountRequest) ProcessRequest(_ *swarm.Swarm, w *ResponseWriter) { 14 | w.Return(map[string]interface{}{ 15 | ParamSwarms: scr.N, 16 | }) 17 | } 18 | 19 | func (scr *SwarmCountRequest) MarshalJSON() (data []byte, err error) { 20 | data, err = json.Marshal(map[string]interface{}{ 21 | ParamSwarms: scr.N, 22 | ParamMethod: RPCSwarmCount, 23 | }) 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /lib/metainfo/metainfo_test.go: -------------------------------------------------------------------------------- 1 | package metainfo 2 | 3 | import ( 4 | "github.com/zeebo/bencode" 5 | "os" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestLoadTorrent(t *testing.T) { 11 | f, err := os.Open("test.torrent") 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | defer f.Close() 16 | tf := new(TorrentFile) 17 | dec := bencode.NewDecoder(f) 18 | err = dec.Decode(tf) 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | 23 | if strings.ToUpper(tf.Infohash().Hex()) != "6BCDC07177EC43658C1B4D5450640059663A5214" { 24 | t.Error(tf.Infohash().Hex()) 25 | } 26 | // TODO: check members 27 | } 28 | -------------------------------------------------------------------------------- /lib/rpc/response.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type ResponseWriter struct { 9 | w http.ResponseWriter 10 | } 11 | 12 | func (rw *ResponseWriter) SendJSON(obj interface{}) { 13 | json.NewEncoder(rw.w).Encode(obj) 14 | } 15 | 16 | func (rw *ResponseWriter) SendError(msg string) { 17 | rw.SendJSON(map[string]string{ 18 | "error": msg, 19 | }) 20 | } 21 | 22 | func (rw *ResponseWriter) Return(obj interface{}) { 23 | rw.SendJSON(obj) 24 | /* 25 | rw.SendJSON(map[string]interface{}{ 26 | "error": nil, 27 | "result": obj, 28 | }) 29 | */ 30 | } 31 | -------------------------------------------------------------------------------- /lib/dht/xdht.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import ( 4 | "bytes" 5 | "github.com/majestrate/XD/lib/bittorrent/extensions" 6 | "github.com/majestrate/XD/lib/common" 7 | "github.com/zeebo/bencode" 8 | ) 9 | 10 | type XDHT struct { 11 | } 12 | 13 | func (dht *XDHT) HandleError(err *Error) { 14 | 15 | } 16 | 17 | func (dht *XDHT) HandleMessage(msg extensions.Message, src common.PeerID) (err error) { 18 | r := bytes.NewReader(msg.PayloadRaw) 19 | var dhtmsg Message 20 | err = bencode.NewDecoder(r).Decode(&dhtmsg) 21 | if err == nil { 22 | if dhtmsg.IsError() { 23 | dht.HandleError(dhtmsg.Err) 24 | } 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /contrib/webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xd-webui", 3 | "version": "0.0.0", 4 | "description": "web ui for XD bittorrent client", 5 | "repository": "https://github.com/majestrate/XD", 6 | "author": "Jeff", 7 | "license": "MIT", 8 | "dependencies": { 9 | "express": "^4.15.4", 10 | "http-server": "^0.10.0", 11 | "uglify-js": "^2.4.11" 12 | }, 13 | "scripts": { 14 | "clean": "rm -f docroot/xd.min.js", 15 | "build": "cat css/*.css > docroot/xd.css && cat lib/*.js | uglifyjs > docroot/xd.min.js", 16 | "build-debug": "cat css/*.css > docroot/xd.css && cat lib/*.js > docroot/xd.min.js", 17 | "server": "node server.js" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/rpc/rpc_listtorrentstatus.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | ) 7 | 8 | type ListTorrentStatusRequest struct { 9 | BaseRequest 10 | } 11 | 12 | func (req *ListTorrentStatusRequest) ProcessRequest(sw *swarm.Swarm, w *ResponseWriter) { 13 | status := make(swarm.SwarmStatus) 14 | sw.Torrents.ForEachTorrent(func(t *swarm.Torrent) { 15 | status[t.Infohash().Hex()] = t.GetStatus() 16 | }) 17 | w.Return(status) 18 | } 19 | 20 | func (req *ListTorrentStatusRequest) MarshalJSON() (data []byte, err error) { 21 | data, err = json.Marshal(map[string]interface{}{ 22 | ParamSwarm: req.Swarm, 23 | ParamMethod: RPCListTorrentStatus, 24 | }) 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: xd 2 | Section: net 3 | Priority: optional 4 | Maintainer: r4sas 5 | Build-Depends: debhelper (>= 9~), 6 | dh-apparmor, 7 | dpkg-dev (>= 1.16.1~), 8 | lsb-release, 9 | golang-go (>= 1.6) | golang-1.7-go | golang-1.6-go 10 | Standards-Version: 3.9.6 11 | Homepage: https://xd-torrent.github.io/ 12 | Vcs-Git: git://github.com/majestrate/XD.git 13 | Vcs-Browser: https://github.com/majestrate/XD 14 | 15 | Package: xd 16 | Architecture: any 17 | Depends: ${shlibs:Depends}, ${misc:Depends}, lsb-base (>= 3.0-6), adduser 18 | Suggests: i2pd, apparmor 19 | Description: I2P BitTorrent Client written in GO 20 | . 21 | This package provides the daemon. 22 | -------------------------------------------------------------------------------- /lib/rpc/rpc_listtorrent.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | ) 7 | 8 | type ListTorrentsRequest struct { 9 | BaseRequest 10 | } 11 | 12 | func (ltr *ListTorrentsRequest) ProcessRequest(sw *swarm.Swarm, w *ResponseWriter) { 13 | var swarms swarm.TorrentsList 14 | sw.Torrents.ForEachTorrent(func(t *swarm.Torrent) { 15 | swarms.Infohashes = append(swarms.Infohashes, t.MetaInfo().Infohash().Hex()) 16 | }) 17 | w.Return(swarms) 18 | } 19 | 20 | func (ltr *ListTorrentsRequest) MarshalJSON() (data []byte, err error) { 21 | data, err = json.Marshal(map[string]interface{}{ 22 | ParamSwarm: ltr.Swarm, 23 | ParamMethod: RPCListTorrents, 24 | }) 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /lib/util/ensurefile.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/log" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // ensure a file and its parent directory exists 11 | func EnsureFile(fpath string, size uint64) (err error) { 12 | d, _ := filepath.Split(fpath) 13 | if d != "" { 14 | err = EnsureDir(d) 15 | } 16 | if err == nil { 17 | _, err = os.Stat(fpath) 18 | if os.IsNotExist(err) { 19 | log.Debugf("create file %s", fpath) 20 | var f *os.File 21 | f, err = os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY, 0666) 22 | if err == nil { 23 | // fill with zeros 24 | if size > 0 { 25 | _, err = io.CopyN(f, Zero, int64(size)) 26 | } 27 | f.Close() 28 | } 29 | } 30 | } 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /lib/rpc/rpc_addtorrent.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | ) 7 | 8 | type AddTorrentRequest struct { 9 | BaseRequest 10 | URL string `json:"url"` 11 | } 12 | 13 | func (atr *AddTorrentRequest) ProcessRequest(sw *swarm.Swarm, w *ResponseWriter) { 14 | err := sw.AddRemoteTorrent(atr.URL) 15 | if err == nil { 16 | w.Return(map[string]interface{}{"error": nil}) 17 | } else { 18 | w.Return(map[string]interface{}{"error": err.Error()}) 19 | } 20 | } 21 | 22 | func (atr *AddTorrentRequest) MarshalJSON() (data []byte, err error) { 23 | data, err = json.Marshal(map[string]interface{}{ 24 | ParamSwarm: atr.Swarm, 25 | ParamURL: atr.URL, 26 | ParamMethod: RPCAddTorrent, 27 | }) 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /lib/config/defaults.go: -------------------------------------------------------------------------------- 1 | //go:build !lokinet 2 | // +build !lokinet 3 | 4 | package config 5 | 6 | const DisableLokinetByDefault = true 7 | const DisableI2PByDefault = false 8 | 9 | // TODO: idk if these are the right names but the URL are correct 10 | var DefaultOpenTrackers = map[string]string{ 11 | "dg-opentracker": "http://w7tpbzncbcocrqtwwm3nezhnnsw4ozadvi2hmvzdhrqzfxfum7wa.b32.i2p/a", 12 | "chudo-opentracker": "http://swhb5i7wcjcohmus3gbt3w6du6pmvl3isdvxvepuhdxxkfbzao6q.b32.i2p/a", 13 | "r4sas-opentracker": "http://punzipidirfqspstvzpj6gb4tkuykqp6quurj6e23bgxcxhdoe7q.b32.i2p/a", 14 | "thebland-opentracker": "http://s5ikrdyjwbcgxmqetxb3nyheizftms7euacuub2hic7defkh3xhq.b32.i2p/a", 15 | "skank-opentracker": "http://by7luzwhx733fhc5ug2o75dcaunblq2ztlshzd7qvptaoa73nqua.b32.i2p/a", 16 | } 17 | -------------------------------------------------------------------------------- /lib/rpc/transmission/util.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | func getTorrentIDs(getActiveIDs func() map[int64]string, args Args) (ids TorrentIDArray) { 4 | ids_i, ok := args["ids"] 5 | if ok { 6 | ids_slice, ok := ids_i.([]interface{}) 7 | if ok { 8 | for _, id := range ids_slice { 9 | tid, ok := id.(int64) 10 | if ok { 11 | ids = append(ids, TorrentID(tid)) 12 | } 13 | } 14 | } else { 15 | ids_str, ok := ids_i.(string) 16 | if ok { 17 | if ids_str == idRecentlyActive { 18 | tids := getActiveIDs() 19 | for tid := range tids { 20 | ids = append(ids, TorrentID(tid)) 21 | } 22 | } 23 | } else { 24 | ids_int, ok := ids_i.(int64) 25 | if ok { 26 | ids = append(ids, TorrentID(ids_int)) 27 | } 28 | } 29 | } 30 | } 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /contrib/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | LABEL authors "Darknet Villain " 3 | 4 | ENV HOME_DIR="/home/xd" 5 | ENV XD_HOME="$HOME_DIR/data" 6 | 7 | RUN mkdir -p "$HOME_DIR" "$XD_HOME" \ 8 | && adduser -S -h "$HOME_DIR" xd \ 9 | && chown -R xd:nobody "$HOME_DIR" \ 10 | && chmod -R 700 "$XD_HOME" 11 | 12 | RUN apk --no-cache add go build-base git yarn \ 13 | && git clone https://github.com/majestrate/XD /root/XD \ 14 | && cd /root/XD \ 15 | && make \ 16 | && mv /root/XD/XD "$XD_HOME" \ 17 | && chown xd "$XD_HOME"/XD && chmod +x "$XD_HOME"/XD \ 18 | && ln -s "$XD_HOME"/XD "$XD_HOME"/xd-cli \ 19 | && rm -rf /root/XD && apk --purge del go build-base git yarn 20 | 21 | EXPOSE 1188 22 | 23 | VOLUME "$XD_HOME" 24 | WORKDIR "$XD_HOME" 25 | USER xd 26 | 27 | CMD ./XD torrents.ini 28 | -------------------------------------------------------------------------------- /lib/rpc/rpc_setopt.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | ) 7 | 8 | type SetPieceWindowRequest struct { 9 | BaseRequest 10 | N int `json:"n"` 11 | } 12 | 13 | func (r *SetPieceWindowRequest) ProcessRequest(sw *swarm.Swarm, w *ResponseWriter) { 14 | if r.N > 0 { 15 | sw.Torrents.MaxReq = r.N 16 | sw.Torrents.ForEachTorrent(func(t *swarm.Torrent) { 17 | t.SetPieceWindow(r.N) 18 | }) 19 | w.Return(map[string]interface{}{"error": nil}) 20 | } else { 21 | w.SendError("N must be greater than zero") 22 | } 23 | } 24 | 25 | func (r *SetPieceWindowRequest) MarshalJSON() (data []byte, err error) { 26 | data, err = json.Marshal(map[string]interface{}{ 27 | ParamMethod: RPCSetPieceWindow, 28 | ParamN: r.N, 29 | ParamSwarm: r.Swarm, 30 | }) 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /lib/rpc/transmission/consts.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | // Success indicates result of success 4 | const Success = "success" 5 | 6 | // XSRFToken is the header name for the XSRF token 7 | const XSRFToken = "X-Transmission-Session-Id" 8 | 9 | // RPCPath is the url path for rpc 10 | const RPCPath = "/transmission/rpc" 11 | 12 | // ContentType is the content type for responses 13 | const ContentType = "text/json; encoding=UTF-8" 14 | 15 | const notImplemented = "Not Implemented" 16 | 17 | const idRecentlyActive = "recently-active" 18 | 19 | const tr_Status_Stopped = 0 20 | const tr_Status_CheckWait = 1 21 | const tr_Status_Check = 2 22 | const tr_Status_DownloadWait = 3 23 | const tr_Status_Download = 4 24 | const tr_Status_SeedWait = 5 25 | const tr_Status_Seed = 6 26 | 27 | const tr_Pri_Low = -1 28 | const tr_Pri_Norm = 0 29 | const tr_Pri_High = 1 30 | -------------------------------------------------------------------------------- /lib/rpc/transmission/token.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/util" 5 | "time" 6 | ) 7 | 8 | type xsrfToken struct { 9 | data string 10 | expires time.Time 11 | } 12 | 13 | func newToken() *xsrfToken { 14 | return &xsrfToken{ 15 | data: util.RandStr(10), 16 | expires: time.Now().Add(time.Minute), 17 | } 18 | } 19 | 20 | func (t *xsrfToken) Expired() bool { 21 | return time.Now().After(t.expires) 22 | } 23 | 24 | func (t *xsrfToken) Update() { 25 | if t.Expired() { 26 | t.Regen() 27 | } 28 | } 29 | 30 | func (t *xsrfToken) Token() string { 31 | return t.data 32 | } 33 | 34 | func (t *xsrfToken) Regen() { 35 | t.data = util.RandStr(10) 36 | t.expires = time.Now().Add(time.Minute) 37 | } 38 | 39 | func (t *xsrfToken) Check(tok string) bool { 40 | return t.data == tok && !t.Expired() 41 | } 42 | -------------------------------------------------------------------------------- /lib/storage/fs_settings.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/zeebo/bencode" 5 | "io" 6 | ) 7 | 8 | type fsSettings struct { 9 | Opts map[string]string `bencode:"settings"` 10 | } 11 | 12 | func createSettings() fsSettings { 13 | return fsSettings{ 14 | Opts: make(map[string]string), 15 | } 16 | } 17 | 18 | func (s *fsSettings) Put(key, val string) { 19 | s.Opts[key] = val 20 | } 21 | 22 | func (s *fsSettings) Get(key, fallback string) (val string) { 23 | var ok bool 24 | val, ok = s.Opts[key] 25 | if !ok { 26 | val = fallback 27 | } 28 | return 29 | } 30 | 31 | func (s *fsSettings) BDecode(r io.Reader) (err error) { 32 | dec := bencode.NewDecoder(r) 33 | err = dec.Decode(s) 34 | return 35 | } 36 | 37 | func (s *fsSettings) BEncode(w io.Writer) (err error) { 38 | enc := bencode.NewEncoder(w) 39 | err = enc.Encode(s) 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /lib/config/gnutella.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/configparser" 5 | "github.com/majestrate/XD/lib/gnutella" 6 | ) 7 | 8 | type G2Config struct { 9 | enabled bool 10 | } 11 | 12 | // DefaultEnableGnutella says if should we enable gnutella by default 13 | const DefaultEnableGnutella = false 14 | 15 | func (c *G2Config) Load(s *configparser.Section) error { 16 | c.enabled = DefaultEnableGnutella 17 | if s != nil { 18 | c.enabled = s.ValueOf("enabled") == "1" 19 | } 20 | return nil 21 | } 22 | 23 | func (c *G2Config) Save(s *configparser.Section) error { 24 | if s != nil { 25 | val := "0" 26 | if c.enabled { 27 | val = "1" 28 | } 29 | s.Add("enabled", val) 30 | } 31 | return nil 32 | } 33 | 34 | func (c *G2Config) LoadEnv() { 35 | 36 | } 37 | 38 | func (c *G2Config) CreateSwarm() *gnutella.Swarm { 39 | if c.enabled { 40 | return gnutella.NewSwarm() 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /lib/config/log.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/configparser" 5 | "os" 6 | ) 7 | 8 | const EnvLogLevel = "XD_LOG_LEVEL" 9 | const EnvLogPProf = "XD_PPROF" 10 | 11 | type LogConfig struct { 12 | Level string 13 | Pprof bool 14 | } 15 | 16 | func (cfg *LogConfig) Load(s *configparser.Section) error { 17 | 18 | cfg.Level = "info" 19 | if s != nil { 20 | cfg.Level = s.Get("level", "info") 21 | cfg.Pprof = s.Get("pprof", "0") == "1" 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func (cfg *LogConfig) Save(s *configparser.Section) error { 28 | lvl := "0" 29 | if cfg.Pprof { 30 | lvl = "1" 31 | } 32 | s.Add("level", cfg.Level) 33 | s.Add("pprof", lvl) 34 | return nil 35 | } 36 | 37 | func (cfg *LogConfig) LoadEnv() { 38 | lvl := os.Getenv(EnvLogLevel) 39 | if lvl != "" { 40 | cfg.Level = lvl 41 | } 42 | lvl = os.Getenv(EnvLogPProf) 43 | if lvl != "" { 44 | cfg.Pprof = lvl == "1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /debian/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | XDHOME='/var/lib/XD' 5 | XDUSER='debian-xd' 6 | 7 | xdadduser() { 8 | if ! getent group $XDUSER >/dev/null; then 9 | addgroup --system $XDUSER >/dev/null 10 | fi 11 | 12 | if ! getent passwd $XDUSER >/dev/null; then 13 | adduser \ 14 | --system \ 15 | --disabled-login \ 16 | --ingroup $XDUSER \ 17 | --home $XDHOME \ 18 | --gecos "XD client user" \ 19 | --shell /bin/false \ 20 | $XDUSER >/dev/null 21 | fi 22 | install --directory --group=$XDUSER --owner=$XDUSER /var/run/XD -m644 23 | } 24 | 25 | case "$1" in 26 | install) 27 | xdadduser 28 | ;; 29 | 30 | upgrade) 31 | xdadduser 32 | ;; 33 | 34 | abort-upgrade) 35 | ;; 36 | 37 | *) 38 | echo "preinst called with unknown argument \`$1'" >&2 39 | exit 0 40 | ;; 41 | esac 42 | 43 | #DEBHELPER# 44 | 45 | exit 0 46 | -------------------------------------------------------------------------------- /lib/dht/error.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | import ( 4 | "fmt" 5 | "github.com/zeebo/bencode" 6 | ) 7 | 8 | type Error struct { 9 | Code int64 10 | Message string 11 | } 12 | 13 | func (e *Error) MarshalBencode() ([]byte, error) { 14 | return bencode.EncodeBytes([]interface{}{ 15 | e.Code, 16 | e.Message, 17 | }) 18 | } 19 | 20 | func (e *Error) UnmarshalBencode(d []byte) error { 21 | var dec []interface{} 22 | err := bencode.DecodeBytes(d, &dec) 23 | if err != nil { 24 | return err 25 | } 26 | if len(dec) != 2 { 27 | return fmt.Errorf("bad size of error: %d", len(dec)) 28 | } 29 | var ok bool 30 | e.Code, ok = dec[0].(int64) 31 | if !ok { 32 | return fmt.Errorf("first element is not an int") 33 | } 34 | e.Message, ok = dec[1].(string) 35 | if !ok { 36 | return fmt.Errorf("second element is not a string") 37 | } 38 | return nil 39 | } 40 | 41 | const ErrCodeGeneric = 201 42 | const ErrCodeServer = 202 43 | const ErrCodeProtocol = 203 44 | const ErrCodeMethod = 204 45 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/pex.go: -------------------------------------------------------------------------------- 1 | package swarm 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/network/i2p" 5 | "github.com/majestrate/XD/lib/sync" 6 | "net" 7 | ) 8 | 9 | // PEXSwarmState manages PeerExchange state on a bittorrent swarm 10 | type PEXSwarmState struct { 11 | m sync.Map 12 | } 13 | 14 | func (p *PEXSwarmState) onNewPeer(addr net.Addr) { 15 | p.m.Store(addr.String(), true) 16 | } 17 | 18 | func (p *PEXSwarmState) onPeerDisconnected(addr net.Addr) { 19 | p.m.Store(addr.String(), false) 20 | } 21 | 22 | // PopDestHashList gets list of i2p destination hashes of currently active and disconnected peers 23 | func (p *PEXSwarmState) PopDestHashLists() (connected, disconnected []byte) { 24 | p.m.Range(func(k, v interface{}) bool { 25 | addr := k.(string) 26 | active := v.(bool) 27 | h := i2p.I2PAddr(addr).Base32Addr() 28 | if active { 29 | connected = append(connected, h[:]...) 30 | } else { 31 | disconnected = append(disconnected, h[:]...) 32 | p.m.Delete(k) 33 | } 34 | return false 35 | }) 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016-2018 Jeff Becker 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /lib/rpc/rpc_torrentstatus.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | "github.com/majestrate/XD/lib/common" 7 | ) 8 | 9 | type TorrentStatusRequest struct { 10 | BaseRequest 11 | Infohash string `json:"infohash"` 12 | } 13 | 14 | func (r *TorrentStatusRequest) ProcessRequest(sw *swarm.Swarm, w *ResponseWriter) { 15 | var status swarm.TorrentStatus 16 | var ih common.Infohash 17 | var err error 18 | ih, err = common.DecodeInfohash(r.Infohash) 19 | if err == nil { 20 | sw.Torrents.VisitTorrent(ih, func(t *swarm.Torrent) { 21 | if t == nil { 22 | err = ErrNoTorrent 23 | } else { 24 | status = t.GetStatus() 25 | } 26 | }) 27 | } 28 | if err == nil { 29 | w.Return(status) 30 | } else { 31 | w.SendError(err.Error()) 32 | } 33 | } 34 | 35 | func (r *TorrentStatusRequest) MarshalJSON() (data []byte, err error) { 36 | data, err = json.Marshal(map[string]interface{}{ 37 | ParamSwarm: r.Swarm, 38 | ParamMethod: RPCTorrentStatus, 39 | ParamInfohash: r.Infohash, 40 | }) 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /lib/common/infohash.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "errors" 7 | ) 8 | 9 | var ErrBadMagnetURI = errors.New("bad magnet URI") 10 | 11 | // ErrBadInfoHashLen is error indicating that the infohash is a bad size 12 | var ErrBadInfoHashLen = errors.New("bad infohash length") 13 | 14 | // Infohash is a bittorrent infohash buffer 15 | type Infohash [20]byte 16 | 17 | func (ih Infohash) Equal(other Infohash) bool { 18 | return bytes.Equal(ih.Bytes(), other.Bytes()) 19 | } 20 | 21 | // Hex gets hex representation of infohash 22 | func (ih Infohash) Hex() string { 23 | return hex.EncodeToString(ih.Bytes()) 24 | } 25 | 26 | // DecodeInfohash decodes infohash buffer from hex string 27 | func DecodeInfohash(hexstr string) (ih Infohash, err error) { 28 | var dec []byte 29 | dec, err = hex.DecodeString(hexstr) 30 | if len(dec) == 20 { 31 | copy(ih[:], dec[:]) 32 | } else { 33 | err = ErrBadInfoHashLen 34 | } 35 | return 36 | } 37 | 38 | // Bytes gets underlying byteslice of infohash buffer 39 | func (ih Infohash) Bytes() []byte { 40 | return ih[:] 41 | } 42 | -------------------------------------------------------------------------------- /lib/network/i2p/addr.go: -------------------------------------------------------------------------------- 1 | package i2p 2 | 3 | import ( 4 | "crypto/sha256" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | // implements net.Addr 10 | type Addr struct { 11 | addr string 12 | port string 13 | } 14 | 15 | func (a Addr) Network() string { 16 | return "i2p" 17 | } 18 | 19 | func (a Addr) String() string { 20 | return net.JoinHostPort(a.addr, a.port) 21 | } 22 | 23 | func I2PAddr(addr string) Addr { 24 | if strings.Count(addr, ":") > 0 { 25 | a, p, _ := net.SplitHostPort(addr) 26 | return Addr{ 27 | addr: a, 28 | port: p, 29 | } 30 | } 31 | return Addr{ 32 | addr: addr, 33 | } 34 | } 35 | 36 | // compute base32 address 37 | func (addr Addr) Base32Addr() (b32 Base32Addr) { 38 | a := []byte(addr.addr) 39 | buf := make([]byte, i2pB64enc.DecodedLen(len(a))) 40 | n, err := i2pB64enc.Decode(buf, a) 41 | if err != nil { 42 | return 43 | } 44 | h := sha256.Sum256(buf[:n]) 45 | copy(b32[:], h[:]) 46 | return 47 | } 48 | 49 | // i2p destination hash 50 | type Base32Addr [32]byte 51 | 52 | // get string version 53 | func (b32 Base32Addr) String() string { 54 | b32addr := make([]byte, 56) 55 | i2pB32enc.Encode(b32addr, b32[:]) 56 | return string(b32addr[:52]) + ".b32.i2p" 57 | } 58 | -------------------------------------------------------------------------------- /lib/fs/fs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type ReadFile interface { 9 | io.ReadCloser 10 | io.ReaderAt 11 | } 12 | 13 | type WriteFile interface { 14 | io.WriteCloser 15 | io.WriterAt 16 | Sync() error 17 | } 18 | 19 | type Driver interface { 20 | io.Closer 21 | // open any underlying contexts 22 | Open() error 23 | // open file ready only 24 | OpenFileReadOnly(fpath string) (ReadFile, error) 25 | // open file write only 26 | OpenFileWriteOnly(fpath string) (WriteFile, error) 27 | // return true if file exists 28 | FileExists(fpath string) bool 29 | // ensure a directory exists 30 | EnsureDir(fpath string) error 31 | // ensire a file exists and is of size sz 32 | EnsureFile(fpath string, sz uint64) error 33 | // filepath.Glob lookalike 34 | Glob(str string) ([]string, error) 35 | // remove single file 36 | Remove(fpath string) error 37 | // Remove all in filepath 38 | RemoveAll(fpath string) error 39 | // Join path 40 | Join(parts ...string) string 41 | // move file 42 | Move(oldPath, newPath string) error 43 | // split path into dirname, basename 44 | Split(path string) (string, string) 45 | // call stat() 46 | Stat(path string) (os.FileInfo, error) 47 | } 48 | -------------------------------------------------------------------------------- /lib/dht/message.go: -------------------------------------------------------------------------------- 1 | package dht 2 | 3 | const mFindNode = "find_node" 4 | const mGetPeers = "get_peers" 5 | const mAnnouncePeer = "announce_peer" 6 | 7 | const kQuery = "q" 8 | const kResponse = "r" 9 | const kError = "e" 10 | 11 | const vID = "id" 12 | const vTarget = "target" 13 | const vNodes = "nodes" 14 | 15 | type Message struct { 16 | Query string `bencode:"q",omitempty` 17 | TID string `bencode:"t"` 18 | Reply string `bencode:"y"` 19 | Err *Error `bencode:"e",omitempty` 20 | Args map[string]interface{} `bencode:"a",omitempty` 21 | } 22 | 23 | func (m *Message) IsError() bool { 24 | return m.Reply == kError 25 | } 26 | 27 | // NewError generates a new error reply message 28 | func NewError(txid string, code int, errMsg string) *Message { 29 | return &Message{ 30 | TID: txid, 31 | Reply: kError, 32 | Err: &Error{ 33 | Code: int64(code), 34 | Message: errMsg, 35 | }, 36 | } 37 | } 38 | 39 | func NewFindNodeRequest(txid, id, target string) *Message { 40 | return &Message{ 41 | TID: txid, 42 | Reply: kQuery, 43 | Query: mFindNode, 44 | Args: map[string]interface{}{ 45 | vID: id, 46 | vTarget: target, 47 | }, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/stats/tracker.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/util" 5 | "github.com/zeebo/bencode" 6 | "io" 7 | ) 8 | 9 | type Tracker struct { 10 | history int 11 | rates map[string]*util.Rate 12 | } 13 | 14 | func NewTracker() *Tracker { 15 | return &Tracker{ 16 | history: 128, 17 | rates: make(map[string]*util.Rate), 18 | } 19 | } 20 | 21 | func (t *Tracker) NewRate(name string) { 22 | t.rates[name] = util.NewRate(t.history) 23 | } 24 | 25 | func (t *Tracker) AddSample(name string, n uint64) { 26 | r, ok := t.rates[name] 27 | if ok { 28 | r.AddSample(n) 29 | } 30 | } 31 | 32 | func (t *Tracker) Rate(name string) (r *util.Rate) { 33 | r, _ = t.rates[name] 34 | return 35 | } 36 | 37 | func (t *Tracker) ForEach(v func(string, *util.Rate)) { 38 | for n, r := range t.rates { 39 | v(n, r) 40 | } 41 | } 42 | 43 | func (t *Tracker) Tick() { 44 | for _, r := range t.rates { 45 | r.Tick() 46 | } 47 | } 48 | 49 | func (t *Tracker) BEncode(w io.Writer) (err error) { 50 | e := bencode.NewEncoder(w) 51 | err = e.Encode(t.rates) 52 | return 53 | } 54 | 55 | func (t *Tracker) BDecode(r io.Reader) (err error) { 56 | d := bencode.NewDecoder(r) 57 | err = d.Decode(&t.rates) 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /lib/network/i2p/conn.go: -------------------------------------------------------------------------------- 1 | package i2p 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | // tcp/i2p connection 9 | // implements net.Conn 10 | type I2PConn struct { 11 | // underlying connection 12 | c net.Conn 13 | // our local address 14 | laddr Addr 15 | // remote peer's address 16 | raddr Addr 17 | } 18 | 19 | // implements net.Conn 20 | func (c *I2PConn) Read(d []byte) (n int, err error) { 21 | n, err = c.c.Read(d) 22 | return 23 | } 24 | 25 | // implements net.Conn 26 | func (c *I2PConn) Write(d []byte) (n int, err error) { 27 | n, err = c.c.Write(d) 28 | return 29 | } 30 | 31 | // implements net.Conn 32 | func (c *I2PConn) Close() error { 33 | return c.c.Close() 34 | } 35 | 36 | // implements net.Conn 37 | func (c *I2PConn) LocalAddr() net.Addr { 38 | return c.laddr 39 | } 40 | 41 | // implements net.Conn 42 | func (c *I2PConn) RemoteAddr() net.Addr { 43 | return c.raddr 44 | } 45 | 46 | // implements net.Conn 47 | func (c *I2PConn) SetDeadline(t time.Time) error { 48 | return c.c.SetDeadline(t) 49 | } 50 | 51 | // implements net.Conn 52 | func (c *I2PConn) SetReadDeadline(t time.Time) error { 53 | return c.c.SetReadDeadline(t) 54 | } 55 | 56 | // implements net.Conn 57 | func (c *I2PConn) SetWriteDeadline(t time.Time) error { 58 | return c.c.SetWriteDeadline(t) 59 | } 60 | -------------------------------------------------------------------------------- /lib/common/piece.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | ) 7 | 8 | // PieceData is a bittorrent piece response 9 | type PieceData struct { 10 | Index uint32 11 | Begin uint32 12 | Data []byte 13 | } 14 | 15 | func (pc *PieceData) Equals(other *PieceData) bool { 16 | return pc != nil && other != nil && pc.Index == other.Index && pc.Begin == other.Begin && bytes.Equal(pc.Data, other.Data) 17 | } 18 | 19 | // PieceRequest is a request for a bittorrent piece 20 | type PieceRequest struct { 21 | Index uint32 22 | Begin uint32 23 | Length uint32 24 | } 25 | 26 | func (pc *PieceRequest) Copy(r *PieceRequest) { 27 | pc.Index = r.Index 28 | pc.Begin = r.Begin 29 | pc.Length = r.Length 30 | } 31 | 32 | func (pc PieceRequest) Cancel() WireMessage { 33 | return NewCancel(pc.Index, pc.Begin, pc.Length) 34 | } 35 | 36 | // ErrInvalidPiece is an error for when a piece has invalid sha1sum 37 | var ErrInvalidPiece = errors.New("invalid piece") 38 | 39 | // return true if piecedata matches this piece request 40 | func (r *PieceRequest) Matches(d *PieceData) bool { 41 | return r.Length == uint32(len(d.Data)) && r.Begin == d.Begin && r.Index == d.Index 42 | } 43 | 44 | func (r *PieceRequest) Equals(other *PieceRequest) bool { 45 | return r.Index == other.Index && r.Length == other.Length && r.Begin == other.Begin 46 | } 47 | -------------------------------------------------------------------------------- /lib/config/lokinet.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/configparser" 5 | "github.com/majestrate/XD/lib/log" 6 | "github.com/majestrate/XD/lib/network/inet" 7 | "os" 8 | ) 9 | 10 | type LokiNetConfig struct { 11 | DNSAddr string 12 | Port string 13 | Disabled bool 14 | } 15 | 16 | func (cfg *LokiNetConfig) Load(section *configparser.Section) error { 17 | if section == nil { 18 | cfg.DNSAddr = inet.DefaultDNSAddr 19 | cfg.Port = inet.DefaultPort 20 | cfg.Disabled = DisableLokinetByDefault 21 | } else { 22 | cfg.Disabled = section.Get("disabled", "") == "1" 23 | cfg.DNSAddr = section.Get("dns", inet.DefaultDNSAddr) 24 | cfg.Port = section.Get("port", inet.DefaultPort) 25 | } 26 | return nil 27 | } 28 | 29 | func (cfg *LokiNetConfig) Save(s *configparser.Section) error { 30 | opts := make(map[string]string) 31 | opts["dns"] = cfg.DNSAddr 32 | if cfg.Disabled { 33 | opts["disabled"] = "1" 34 | } 35 | for k := range opts { 36 | s.Add(k, opts[k]) 37 | } 38 | return nil 39 | } 40 | 41 | // create a network session from this config 42 | func (cfg *LokiNetConfig) CreateSession() (*inet.Session, error) { 43 | log.Infof("create new session on lokinet") 44 | return inet.NewSession(cfg.Port, cfg.DNSAddr) 45 | } 46 | 47 | func (cfg *LokiNetConfig) LoadEnv() { 48 | addr := os.Getenv("LOKINET_DNS") 49 | if addr != "" { 50 | cfg.DNSAddr = addr 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/rpc/transmission/handler_torrent_get.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "fmt" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | ) 7 | 8 | func TorrentGet(sw *swarm.Swarm, args Args) (resp Response) { 9 | resp.Args = make(Args) 10 | i_fields, ok := args["fields"] 11 | var err error 12 | if ok { 13 | ids := getTorrentIDs(sw.Torrents.TorrentIDs, args) 14 | var torrents []tgResp 15 | 16 | for _, id := range ids { 17 | r := make(tgResp) 18 | f_slice, ok := i_fields.([]interface{}) 19 | if !ok { 20 | resp.Result = "fields is not an array" 21 | return 22 | } 23 | t := sw.Torrents.GetTorrentByID(int64(id)) 24 | if t != nil { 25 | for _, f := range f_slice { 26 | field, ok := f.(string) 27 | if ok { 28 | h, ok := tgFieldHandlers[field] 29 | if ok { 30 | err = h(field, t, &r) 31 | if err != nil { 32 | break 33 | } 34 | } else { 35 | resp.Result = fmt.Sprintf("field '%s' not implemented", field) 36 | return 37 | } 38 | } else { 39 | resp.Result = "field is not a string" 40 | return 41 | } 42 | } 43 | } 44 | if err == nil { 45 | torrents = append(torrents, r) 46 | } else { 47 | resp.Result = err.Error() 48 | return 49 | } 50 | } 51 | resp.Args["torrents"] = torrents 52 | resp.Result = Success 53 | } else { 54 | resp.Result = "no fields provided" 55 | } 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /lib/bittorrent/extensions/ut_metadata.go: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | import ( 4 | "bytes" 5 | "github.com/majestrate/XD/lib/util" 6 | "github.com/zeebo/bencode" 7 | ) 8 | 9 | // UTMetaData is the bittorrent extension for ut_metadata 10 | const UTMetaData = Extension("ut_metadata") 11 | 12 | // UTRequest msg_type for requests 13 | const UTRequest = 0 14 | 15 | // UTData msg_type for data 16 | const UTData = 1 17 | 18 | // UTReject msg_type for reject messages 19 | const UTReject = 2 20 | 21 | // MetaData ut_metadata extension message 22 | type MetaData struct { 23 | Type int `bencode:"msg_type"` 24 | Piece uint32 `bencode:"piece"` 25 | Size uint32 `bencode:"total_size"` 26 | Data []byte `bencode:"-"` 27 | } 28 | 29 | // ParseMetadata parses a MetaData from a byteslice 30 | func ParseMetadata(buff []byte) (md MetaData, err error) { 31 | r := bytes.NewReader(buff) 32 | d := bencode.NewDecoder(r) 33 | err = d.Decode(&md) 34 | if err == nil && md.Size > 0 { 35 | l := d.BytesParsed() 36 | md.Data = buff[l:] 37 | } 38 | return 39 | } 40 | 41 | // Bytes serializes a MetaData to byteslice 42 | func (md MetaData) Bytes() []byte { 43 | buff := new(util.Buffer) 44 | if md.Type == UTData { 45 | bencode.NewEncoder(buff).Encode(md) 46 | buff.Write(md.Data) 47 | } else { 48 | bencode.NewEncoder(buff).Encode(map[string]interface{}{ 49 | "msg_type": md.Type, 50 | "piece": md.Piece, 51 | }) 52 | } 53 | return buff.Bytes() 54 | } 55 | -------------------------------------------------------------------------------- /lib/tracker/announce.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/common" 5 | "github.com/majestrate/XD/lib/network" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type Event string 11 | 12 | const Started = Event("started") 13 | const Stopped = Event("stopped") 14 | const Completed = Event("completed") 15 | const Nop = Event("") 16 | 17 | func (ev Event) String() string { 18 | return string(ev) 19 | } 20 | 21 | type Request struct { 22 | Infohash common.Infohash 23 | PeerID common.PeerID 24 | Port int 25 | Uploaded uint64 26 | Downloaded uint64 27 | Left uint64 28 | Event Event 29 | NumWant int 30 | Compact bool 31 | GetNetwork func() network.Network 32 | } 33 | 34 | type Response struct { 35 | Interval int `bencode:"interval"` 36 | Peers []common.Peer `bencode:"peers"` 37 | Error string `bencode:"failure reason"` 38 | NextAnnounce time.Time `bencode:"-"` 39 | } 40 | 41 | // bittorrent announcer, gets peers and announces presence in swarm 42 | type Announcer interface { 43 | // announce and get peers 44 | Announce(req *Request) (*Response, error) 45 | // name of this tracker 46 | Name() string 47 | } 48 | 49 | // get announcer from url 50 | // returns nil if invalid url 51 | func FromURL(str string) Announcer { 52 | u, err := url.Parse(str) 53 | if err == nil { 54 | if u.Scheme == "http" { 55 | return NewHttpTracker(u) 56 | } 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: xd 3 | Upstream-Contact: Jeff Becker 4 | Source: https://github.com/majestrate/XD 5 | 6 | Files: * 7 | Copyright: 2016-2018, Jeff Becker 8 | License: MIT 9 | 10 | Files: debian/* 11 | Copyright: 2018, r4sas 12 | License: MIT 13 | 14 | License: MIT 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | . 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | . 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | -------------------------------------------------------------------------------- /lib/mktorrent/mktorrent.go: -------------------------------------------------------------------------------- 1 | package mktorrent 2 | 3 | import ( 4 | "crypto/sha1" 5 | "errors" 6 | "github.com/majestrate/XD/lib/fs" 7 | "github.com/majestrate/XD/lib/metainfo" 8 | "io" 9 | "path/filepath" 10 | ) 11 | 12 | func mkTorrentSingle(f fs.Driver, fpath string, pieceLength uint32) (*metainfo.TorrentFile, error) { 13 | var info metainfo.Info 14 | 15 | info.PieceLength = pieceLength 16 | info.Path = filepath.Base(fpath) 17 | 18 | r, err := f.OpenFileReadOnly(fpath) 19 | if err != nil { 20 | return nil, err 21 | } 22 | buff := make([]byte, info.PieceLength) 23 | for { 24 | n, err := io.ReadFull(r, buff) 25 | if err == io.ErrUnexpectedEOF { 26 | err = nil 27 | d := sha1.Sum(buff[0:n]) 28 | info.Pieces = append(info.Pieces, d[:]...) 29 | info.Length += uint64(n) 30 | break 31 | } else if err == nil { 32 | d := sha1.Sum(buff) 33 | info.Pieces = append(info.Pieces, d[:]...) 34 | info.Length += uint64(n) 35 | } else { 36 | return nil, err 37 | } 38 | } 39 | 40 | return metainfo.TorrentFileFromInfo(info) 41 | } 42 | 43 | func mkTorrentDir(f fs.Driver, fpath string, pieceLength uint32) (*metainfo.TorrentFile, error) { 44 | return nil, errors.New("not implemented") 45 | } 46 | 47 | func MakeTorrent(f fs.Driver, fpath string, pieceLength uint32) (*metainfo.TorrentFile, error) { 48 | st, err := f.Stat(fpath) 49 | if err != nil { 50 | return nil, err 51 | } 52 | if st.IsDir() { 53 | return mkTorrentDir(f, fpath, pieceLength) 54 | } 55 | return mkTorrentSingle(f, fpath, pieceLength) 56 | } 57 | -------------------------------------------------------------------------------- /lib/network/i2p/session.go: -------------------------------------------------------------------------------- 1 | package i2p 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // i2p network session 8 | type Session interface { 9 | 10 | // get session name 11 | Name() string 12 | // open a new control socket 13 | // does handshaske 14 | OpenControlSocket() (net.Conn, error) 15 | 16 | // get printable b32.i2p address 17 | B32Addr() string 18 | 19 | // implements network.Network 20 | Addr() net.Addr 21 | 22 | // implements net.PacketConn 23 | LocalAddr() net.Addr 24 | 25 | // implements network.Network 26 | ReadFrom([]byte) (int, net.Addr, error) 27 | 28 | // implements network.Network 29 | WriteTo([]byte, net.Addr) (int, error) 30 | 31 | // implements network.Network 32 | Accept() (net.Conn, error) 33 | 34 | // implements network.Session 35 | Lookup(name, port string) (net.Addr, error) 36 | 37 | // lookup an i2p address 38 | LookupI2P(name string) (Addr, error) 39 | 40 | // implements network.Network 41 | Dial(n, a string) (net.Conn, error) 42 | 43 | // dial out to a remote destination 44 | DialI2P(a Addr) (net.Conn, error) 45 | 46 | // open the session, generate keys, start up destination etc 47 | Open() error 48 | 49 | // close the session 50 | Close() error 51 | } 52 | 53 | // create a new i2p session 54 | func NewSession(name, addr, keyfile string, opts map[string]string) Session { 55 | return &samSession{ 56 | name: name, 57 | addr: addr, 58 | minversion: "3.0", 59 | maxversion: "3.0", 60 | keys: NewKeyfile(keyfile), 61 | opts: opts, 62 | lookup: make(chan *lookupReq, 18), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/sync/sync_compat.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.9 2 | // +build !go1.9 3 | 4 | package sync 5 | 6 | import ( 7 | "sync" 8 | ) 9 | 10 | type Pool struct { 11 | sync.Pool 12 | } 13 | 14 | type WaitGroup struct { 15 | sync.WaitGroup 16 | } 17 | 18 | type Mutex struct { 19 | sync.Mutex 20 | } 21 | 22 | type RWMutex struct { 23 | sync.RWMutex 24 | } 25 | 26 | type Map struct { 27 | data map[interface{}]interface{} 28 | access Mutex 29 | } 30 | 31 | func (m *Map) ensure() { 32 | if m.data == nil { 33 | m.data = make(map[interface{}]interface{}) 34 | } 35 | } 36 | 37 | func (m *Map) Delete(key interface{}) { 38 | m.access.Lock() 39 | m.ensure() 40 | delete(m.data, key) 41 | m.access.Unlock() 42 | } 43 | 44 | func (m *Map) Load(key interface{}) (actual interface{}, ok bool) { 45 | m.access.Lock() 46 | m.ensure() 47 | actual, ok = m.data[key] 48 | m.access.Unlock() 49 | return 50 | } 51 | 52 | func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) { 53 | m.access.Lock() 54 | m.ensure() 55 | actual, loaded = m.data[key] 56 | if !loaded { 57 | m.data[key] = value 58 | actual = value 59 | } 60 | m.access.Unlock() 61 | return 62 | } 63 | 64 | func (m *Map) Range(f func(key, value interface{}) bool) { 65 | mcopy := make(map[interface{}]interface{}) 66 | m.access.Lock() 67 | m.ensure() 68 | for k := range m.data { 69 | mcopy[k] = m.data[k] 70 | } 71 | m.access.Unlock() 72 | for k := range mcopy { 73 | if !f(k, mcopy[k]) { 74 | return 75 | } 76 | } 77 | } 78 | 79 | func (m *Map) Store(key, value interface{}) { 80 | m.access.Lock() 81 | m.ensure() 82 | m.data[key] = value 83 | m.access.Unlock() 84 | } 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | **You have been visited by the CoC(K)** 3 | 4 | 5 | _.+._ 6 | (^\/^\/^) 7 | \@☆@☆@/ 8 | CoC(k) {_____} 9 | Code Of Conduct Killer _oOPPYbo. 10 | _,ooO8O' `Ob 11 | _,ooOPP"' Ob dO 12 | _oooOP"'' `Oo. ,O[ 13 | Ob _,ooOPP'' `YYboOP 14 | `O[ _ooOP"'' _,oOPP"' 15 | YOooooOP' _ooOP"' 16 | '' ,ooOP'' 17 | ,odPP'' 18 | _,oOP' 19 | ooOP"' 20 | _oOP'o 21 | ,OP YOL 22 | ,O. ,OP Yb contribute code or you're a racist 23 | dO' " Yb get offended as a responsible adult 24 | ]O. dO spread this like happy herpes 25 | Ob _,o. dOP 26 | `Ooo___ooOP'`YbooodPP just imagine what would happen 27 | '`"""'' `''' if we all decided to understand 28 | 29 | 30 | Sweet blessing be apon you but **ONLY** if comment `ebin` to this commit. 31 | -------------------------------------------------------------------------------- /lib/fs/std.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/util" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type stdFs struct{} 10 | 11 | var STD stdFs 12 | 13 | func (f stdFs) Open() error { 14 | return nil 15 | } 16 | 17 | func (f stdFs) Close() error { 18 | return nil 19 | } 20 | 21 | func (f stdFs) EnsureDir(fname string) error { 22 | return util.EnsureDir(fname) 23 | } 24 | 25 | func (f stdFs) EnsureFile(fname string, sz uint64) error { 26 | return util.EnsureFile(fname, sz) 27 | } 28 | 29 | func (f stdFs) FileExists(fname string) bool { 30 | return util.CheckFile(fname) 31 | } 32 | 33 | func (f stdFs) Glob(glob string) ([]string, error) { 34 | return filepath.Glob(glob) 35 | } 36 | 37 | func (f stdFs) OpenFileReadOnly(fname string) (ReadFile, error) { 38 | return os.Open(fname) 39 | } 40 | 41 | func (f stdFs) OpenFileWriteOnly(fname string) (WriteFile, error) { 42 | return os.OpenFile(fname, os.O_WRONLY|os.O_CREATE, 0755) 43 | } 44 | 45 | func (f stdFs) RemoveAll(fname string) error { 46 | return os.RemoveAll(fname) 47 | } 48 | 49 | func (f stdFs) Remove(fname string) error { 50 | return os.Remove(fname) 51 | } 52 | 53 | func (f stdFs) Join(parts ...string) string { 54 | return filepath.Join(parts...) 55 | } 56 | 57 | func (f stdFs) Move(oldpath, newpath string) (err error) { 58 | dir, _ := f.Split(newpath) 59 | err = f.EnsureDir(dir) 60 | if err == nil { 61 | err = os.Rename(oldpath, newpath) 62 | } 63 | return 64 | } 65 | 66 | func (f stdFs) Split(path string) (base, file string) { 67 | base, file = filepath.Split(path) 68 | return 69 | } 70 | 71 | func (f stdFs) Stat(path string) (os.FileInfo, error) { 72 | return os.Stat(path) 73 | } 74 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/announce.go: -------------------------------------------------------------------------------- 1 | package swarm 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/log" 5 | "github.com/majestrate/XD/lib/sync" 6 | "github.com/majestrate/XD/lib/tracker" 7 | "net" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | const DefaultAnnounceNumWant = 10 13 | const DefaultAnnouncePort = 6881 14 | 15 | type torrentAnnounce struct { 16 | access sync.Mutex 17 | next time.Time 18 | fails time.Duration 19 | announce tracker.Announcer 20 | t *Torrent 21 | } 22 | 23 | func (a *torrentAnnounce) tryAnnounce(ev tracker.Event) (err error) { 24 | a.access.Lock() 25 | if time.Now().After(a.next) { 26 | la := a.t.Network().Addr() 27 | if la.Network() == "i2p" { 28 | } 29 | req := &tracker.Request{ 30 | Infohash: a.t.st.Infohash(), 31 | PeerID: a.t.id, 32 | Event: ev, 33 | NumWant: DefaultAnnounceNumWant, 34 | Downloaded: a.t.st.DownloadedSize(), 35 | Left: a.t.st.DownloadRemaining(), 36 | Uploaded: a.t.tx, 37 | GetNetwork: a.t.Network, 38 | } 39 | if la.Network() == "i2p" { 40 | req.Port = DefaultAnnouncePort 41 | } else { 42 | var port string 43 | _, port, err = net.SplitHostPort(la.String()) 44 | req.Port, err = strconv.Atoi(port) 45 | if err != nil { 46 | return 47 | } 48 | } 49 | if ev == tracker.Stopped { 50 | req.NumWant = 0 51 | } 52 | var resp *tracker.Response 53 | log.Infof("announcing to %s", a.announce.Name()) 54 | resp, err = a.announce.Announce(req) 55 | backoff := a.fails * time.Minute 56 | a.next = resp.NextAnnounce.Add(backoff) 57 | if err == nil && ev != tracker.Stopped { 58 | a.t.addPeers(resp.Peers) 59 | } 60 | } 61 | a.access.Unlock() 62 | return 63 | } 64 | -------------------------------------------------------------------------------- /lib/rpc/rpc_changetorrent.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/majestrate/XD/lib/bittorrent/swarm" 7 | "github.com/majestrate/XD/lib/common" 8 | ) 9 | 10 | const TorrentChangeStart = "start" 11 | const TorrentChangeStop = "stop" 12 | const TorrentChangeRemove = "remove" 13 | const TorrentChangeDelete = "delete" 14 | 15 | var ErrInvalidAction = errors.New("invalid torrent action") 16 | 17 | type ChangeTorrentRequest struct { 18 | BaseRequest 19 | Infohash string `json:"infohash"` 20 | Action string `json:"action"` 21 | } 22 | 23 | func (r *ChangeTorrentRequest) ProcessRequest(sw *swarm.Swarm, w *ResponseWriter) { 24 | var ih common.Infohash 25 | var err error 26 | ih, err = common.DecodeInfohash(r.Infohash) 27 | if err == nil { 28 | sw.Torrents.VisitTorrent(ih, func(t *swarm.Torrent) { 29 | if t == nil { 30 | err = ErrNoTorrent 31 | } else { 32 | switch r.Action { 33 | case TorrentChangeStart: 34 | err = t.Start() 35 | case TorrentChangeStop: 36 | err = t.Stop() 37 | case TorrentChangeRemove: 38 | err = t.Remove() 39 | case TorrentChangeDelete: 40 | err = t.Delete() 41 | default: 42 | err = ErrInvalidAction 43 | } 44 | } 45 | }) 46 | } 47 | if err == nil { 48 | w.Return(map[string]interface{}{"error": nil}) 49 | } else { 50 | w.Return(map[string]interface{}{"error": err.Error()}) 51 | } 52 | } 53 | 54 | func (r *ChangeTorrentRequest) MarshalJSON() (data []byte, err error) { 55 | data, err = json.Marshal(map[string]interface{}{ 56 | ParamSwarm: r.Swarm, 57 | ParamInfohash: r.Infohash, 58 | ParamAction: r.Action, 59 | ParamMethod: RPCChangeTorrent, 60 | }) 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /lib/common/peer.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "github.com/majestrate/XD/lib/log" 7 | "github.com/majestrate/XD/lib/network" 8 | "github.com/majestrate/XD/lib/network/i2p" 9 | "github.com/majestrate/XD/lib/network/inet" 10 | "github.com/majestrate/XD/lib/version" 11 | "io" 12 | "net" 13 | "net/url" 14 | "strings" 15 | ) 16 | 17 | // PeerID is a buffer for bittorrent peerid 18 | type PeerID [20]byte 19 | 20 | // Bytes gets buffer as byteslice 21 | func (id PeerID) Bytes() []byte { 22 | return id[:] 23 | } 24 | 25 | // GeneratePeerID generates a new peer id for XD 26 | func GeneratePeerID() (id PeerID) { 27 | io.ReadFull(rand.Reader, id[:]) 28 | id[0] = '-' 29 | v := version.Name + version.Major + version.Minor + version.Patch + "0-" 30 | copy(id[1:], []byte(v[:])) 31 | return 32 | } 33 | 34 | // encode to string 35 | func (id PeerID) String() string { 36 | return url.QueryEscape(string(id.Bytes())) 37 | } 38 | 39 | // Peer provides info for a bittorrent swarm peer 40 | type Peer struct { 41 | Compact i2p.Base32Addr `bencode:"-"` 42 | IP string `bencode:"ip"` 43 | Port int `bencode:"port"` 44 | ID PeerID `bencode:"peer id"` 45 | } 46 | 47 | // Resolve resolves network address of peer 48 | func (p *Peer) Resolve(n network.Network) (a net.Addr, err error) { 49 | la := n.Addr() 50 | if la.Network() == "i2p" { 51 | if len(p.IP) > 0 { 52 | // prefer ip 53 | parts := strings.Split(p.IP, ".i2p") 54 | a = i2p.I2PAddr(parts[0]) 55 | 56 | } else { 57 | // try compact 58 | a, err = n.Lookup(p.Compact.String(), fmt.Sprintf("%d", p.Port)) 59 | } 60 | } else { 61 | log.Debugf("%q", p) 62 | a = inet.NewAddr(p.IP, fmt.Sprintf("%d", p.Port)) 63 | } 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /lib/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/configparser" 5 | ) 6 | 7 | type Config struct { 8 | LokiNet LokiNetConfig 9 | I2P I2PConfig 10 | Storage StorageConfig 11 | RPC RPCConfig 12 | Log LogConfig 13 | Bittorrent BittorrentConfig 14 | Gnutella G2Config 15 | } 16 | 17 | // Configurable interface for entity serializable to/from config parser section 18 | type Configurable interface { 19 | Load(s *configparser.Section) error 20 | Save(c *configparser.Section) error 21 | LoadEnv() 22 | } 23 | 24 | // Load loads a config from file by filename 25 | func (cfg *Config) Load(fname string) (err error) { 26 | sects := map[string]Configurable{ 27 | "lokinet": &cfg.LokiNet, 28 | "i2p": &cfg.I2P, 29 | "storage": &cfg.Storage, 30 | "rpc": &cfg.RPC, 31 | "log": &cfg.Log, 32 | "bittorrent": &cfg.Bittorrent, 33 | "gnutella": &cfg.Gnutella, 34 | } 35 | var c *configparser.Configuration 36 | c, err = configparser.Read(fname) 37 | for sect, conf := range sects { 38 | if c == nil { 39 | err = conf.Load(nil) 40 | } else { 41 | s, _ := c.Section(sect) 42 | err = conf.Load(s) 43 | } 44 | conf.LoadEnv() 45 | if err != nil { 46 | return 47 | } 48 | } 49 | return 50 | } 51 | 52 | // Save saves a loaded config to file by filename 53 | func (cfg *Config) Save(fname string) (err error) { 54 | sects := map[string]Configurable{ 55 | "lokinet": &cfg.LokiNet, 56 | "i2p": &cfg.I2P, 57 | "storage": &cfg.Storage, 58 | "rpc": &cfg.RPC, 59 | "log": &cfg.Log, 60 | "bittorrent": &cfg.Bittorrent, 61 | "gnutella": &cfg.Gnutella, 62 | } 63 | c := configparser.NewConfiguration() 64 | for sect, conf := range sects { 65 | s := c.NewSection(sect) 66 | err = conf.Save(s) 67 | if err != nil { 68 | return 69 | } 70 | } 71 | err = configparser.Save(c, fname) 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /lib/config/rpc.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/configparser" 5 | "os" 6 | ) 7 | 8 | type RPCConfig struct { 9 | Enabled bool 10 | Bind string 11 | ExpectedHost string 12 | Auth bool 13 | Username string 14 | Password string 15 | } 16 | 17 | const DefaultRPCAddr = "127.0.0.1:1776" 18 | const DefaultRPCHost = "127.0.0.1" 19 | const DefaultRPCAuth = "0" 20 | 21 | func (cfg *RPCConfig) Load(s *configparser.Section) error { 22 | if s != nil { 23 | cfg.ExpectedHost = s.Get("host", DefaultRPCHost) 24 | cfg.Bind = s.Get("bind", DefaultRPCAddr) 25 | cfg.Enabled = s.Get("enabled", "1") == "1" 26 | cfg.Auth = s.Get("auth", DefaultRPCAuth) == "1" 27 | cfg.Username = s.Get("username", "") 28 | cfg.Password = s.Get("password", "") 29 | } 30 | if cfg.Bind == "" { 31 | cfg.Bind = DefaultRPCAddr 32 | cfg.Enabled = true 33 | } 34 | if cfg.ExpectedHost == "" { 35 | cfg.ExpectedHost = DefaultRPCHost 36 | } 37 | return nil 38 | } 39 | 40 | func (cfg *RPCConfig) Save(s *configparser.Section) error { 41 | enabled := "1" 42 | if !cfg.Enabled { 43 | enabled = "0" 44 | } 45 | opts := map[string]string{ 46 | "enabled": enabled, 47 | } 48 | if cfg.Bind != "" { 49 | opts["bind"] = cfg.Bind 50 | } 51 | if cfg.ExpectedHost != "" { 52 | opts["host"] = cfg.ExpectedHost 53 | } 54 | 55 | if cfg.Auth && cfg.Username != "" && cfg.Password != "" { 56 | opts["auth"] = "1" 57 | opts["username"] = cfg.Username 58 | opts["password"] = cfg.Password 59 | } 60 | 61 | for k := range opts { 62 | s.Add(k, opts[k]) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | const EnvRPCAddr = "XD_RPC_ADDRESS" 69 | const EnvRPCHost = "XD_RPC_HOST" 70 | 71 | func (cfg *RPCConfig) LoadEnv() { 72 | addr := os.Getenv(EnvRPCAddr) 73 | if addr != "" { 74 | cfg.Bind = addr 75 | } 76 | host := os.Getenv(EnvRPCHost) 77 | if host != "" { 78 | cfg.ExpectedHost = host 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/network/i2p/listener.go: -------------------------------------------------------------------------------- 1 | package i2p 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strings" 9 | ) 10 | 11 | type i2pListener struct { 12 | // parent session 13 | session Session 14 | // local address 15 | laddr Addr 16 | } 17 | 18 | // implements net.Listener 19 | func (l *i2pListener) Addr() net.Addr { 20 | return l.laddr 21 | } 22 | 23 | // implements net.Listener 24 | func (l *i2pListener) Close() error { 25 | l.session = nil 26 | return nil 27 | } 28 | 29 | // implements net.Listener 30 | func (l *i2pListener) Accept() (c net.Conn, err error) { 31 | if l.session == nil { 32 | err = errors.New("session closed") 33 | return 34 | } 35 | readbuf := make([]byte, 1) 36 | var nc net.Conn 37 | nc, err = l.session.OpenControlSocket() 38 | if err == nil { 39 | _, err = fmt.Fprintf(nc, "STREAM ACCEPT ID=%s SILENT=false\n", l.session.Name()) 40 | if err == nil { 41 | var line string 42 | // read response line 43 | line, err = readLine(nc, readbuf) 44 | if err == nil { 45 | sc := bufio.NewScanner(strings.NewReader(line)) 46 | sc.Split(bufio.ScanWords) 47 | for sc.Scan() { 48 | text := sc.Text() 49 | upper := strings.ToUpper(text) 50 | if upper == "STREAM" { 51 | continue 52 | } 53 | if upper == "STATUS" { 54 | continue 55 | } 56 | if upper == "RESULT=OK" { 57 | // we good 58 | break 59 | } 60 | // error 61 | err = errors.New(line) 62 | } 63 | } 64 | if err == nil { 65 | // read address line 66 | line, err = readLine(nc, readbuf) 67 | if err == nil { 68 | // we got a new connection yeeeeh 69 | _ = nc.(*net.TCPConn).SetKeepAlive(false) 70 | c = &I2PConn{ 71 | c: nc, 72 | laddr: l.laddr, 73 | raddr: I2PAddr(line[:len(line)-1]), 74 | } 75 | } 76 | } 77 | } 78 | if c == nil { 79 | // we didn't get a connection 80 | nc.Close() 81 | } 82 | } 83 | return 84 | } 85 | -------------------------------------------------------------------------------- /docs/ru/readme.md: -------------------------------------------------------------------------------- 1 | # Начало работы 2 | 3 | После того как вы собрали и запустили клиент, веб-интерфейс будет доступен по адресу: http://127.0.0.1:1488/ 4 | Пользователям Windows рекомендуется использовать веб-интерфейс, если они не используют командную строку 5 | 6 | ## Командная строка (Unix) 7 | 8 | Чтобы использовать командную строку, вы должны создать символьную ссылку к `XD-cli`: 9 | 10 | $ ln -s XD XD-cli 11 | 12 | Добавить торрент через i2p: 13 | 14 | $ XD-cli add http://somesite.i2p/some/url/to/a/torrent.torrent 15 | 16 | Список активных торрентов: 17 | 18 | $ XD-cli list 19 | 20 | Увеличить количество параллельных запросов (может быть удалено в будущем): 21 | 22 | $ XD-cli set-piece-window 10 23 | 24 | ## Командная строка (Windows) 25 | 26 | В Windows: сделайте копию файла с именем `XD-cli.exe` 27 | Все команды выполняются таким же образом, как и в `Unix`, но зависят от используемого терминала. 28 | 29 | ## Конфигурация 30 | 31 | XD использует формат файла ini для конфигурации, основной файл конфигурации называется torrents.ini и автогенерируется со значениями по умолчанию. 32 | 33 | ## Конфигурация хранилища SFTP 34 | 35 | XD может использовать удаленную файловую систему, доступную через `sftp`. 36 | 37 | ### Пример настройки: 38 | 39 | [storage] 40 | rootdir=/mnt/storage/XD/ 41 | metadata=/mnt/storage/XD/metadata 42 | downloads=/mnt/storage/XD/downloads 43 | sftp_host=remote.server.tld 44 | sftp_port=22 45 | sftp_user=your_ssh_user 46 | sftp_remotekey=base64dserverpublickeygoeshere 47 | sftp_keyfile=/path/to/ssh/private/key/to/login/with/id_rsa 48 | sftp=1 49 | 50 | Это позволит подключиться к `remote.server.tld:22` пользователю `your_ssh_user` с помощью (незашифрованного) закрытого ключа и использовать `/mnt/storage/XD/` на удаленном сервере в качестве хранилища для торрентов и метаданных. 51 | 52 | Открытый ключ сервера обычно находится `/etc/ssh/ssh_host_*.pub` в формате: `ssh-whatever base64goeshere root@hostname` вы будете использовать значение base64 в `sftp_remotekey`. 53 | -------------------------------------------------------------------------------- /docs/en/readme.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Once you have built or obtained a release, a webui will be enabled by default at http://127.0.0.1:1488/ 4 | 5 | Windows users are encouraged to use the webui if they can't use the command line tool 6 | 7 | ## Command Line (unix) 8 | 9 | To use the command line tool you must symlink the `XD` binary to `XD-cli` 10 | 11 | $ ln -s XD XD-cli 12 | 13 | Adding torrents from seed file over i2p: 14 | 15 | XD-cli add http://somesite.i2p/some/url/to/a/torrent.torrent 16 | 17 | Listing active torrents: 18 | 19 | XD-cli list 20 | 21 | To increase how many pieces to request in parallel use `set-piece-window` command (may be removed in future): 22 | 23 | XD-cli set-piece-window 10 24 | 25 | 26 | ## Command Line (windows) 27 | 28 | On Windows: make a copy of the file called `XD-cli.exe` 29 | 30 | All commands are done in the same manner as in unix, except `/` needs escaping depending on what terminal in use. 31 | 32 | TODO: add more docs for windows 33 | 34 | # Configuration 35 | 36 | XD uses ini file format for configuration, the main config file is `torrents.ini` and is autogenerated with default values if not present 37 | 38 | 39 | ## SFTP storage config 40 | 41 | XD can use a remote filesystem accessed via sftp, to use this behavior it must be configured. 42 | 43 | Example config: 44 | 45 | [storage] 46 | rootdir=/mnt/storage/XD/ 47 | metadata=/mnt/storage/XD/metadata 48 | downloads=/mnt/storage/XD/downloads 49 | sftp_host=remote.server.tld 50 | sftp_port=22 51 | sftp_user=your_ssh_user 52 | sftp_remotekey=base64dserverpublickeygoeshere 53 | sftp_keyfile=/path/to/ssh/private/key/to/login/with/id_rsa 54 | sftp=1 55 | 56 | This will login to `remote.server.tld:22` with user `your_ssh_user` using (unencrypted) private key and use `/mnt/storage/XD/` on the remote server as the storage for torrents and metadata. 57 | 58 | The server's public key is usually located at `/etc/ssh/ssh_host_*.pub` in the form: `ssh-whatever base64goeshere root@hostname`, you want to use the base64 value in `sftp_remotekey` . 59 | -------------------------------------------------------------------------------- /lib/storage/storage_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "crypto/rand" 5 | "github.com/majestrate/XD/lib/common" 6 | "github.com/majestrate/XD/lib/fs" 7 | "github.com/majestrate/XD/lib/log" 8 | "github.com/majestrate/XD/lib/metainfo" 9 | "github.com/majestrate/XD/lib/mktorrent" 10 | "io" 11 | "testing" 12 | ) 13 | 14 | const testPieceLen = 65536 15 | 16 | func createRandomTorrent(testFname string) (*metainfo.TorrentFile, error) { 17 | f, err := fs.STD.OpenFileWriteOnly(testFname) 18 | if err != nil { 19 | return nil, err 20 | } 21 | _, err = io.CopyN(f, rand.Reader, (testPieceLen*8)+128) 22 | f.Sync() 23 | f.Close() 24 | 25 | return mktorrent.MakeTorrent(fs.STD, testFname, testPieceLen) 26 | } 27 | 28 | func TestStorage(t *testing.T) { 29 | 30 | log.SetLevel("debug") 31 | 32 | st := &FsStorage{ 33 | MetaDir: "storage", 34 | DataDir: "data", 35 | SeedingDir: "seeding", 36 | FS: fs.STD, 37 | } 38 | 39 | err := st.Init() 40 | if err != nil { 41 | t.Log("failed to init storage") 42 | t.Fail() 43 | return 44 | } 45 | fname := st.FS.Join(st.DataDir, "test.bin") 46 | meta, err := createRandomTorrent(fname) 47 | if err != nil { 48 | t.Logf("failed to make torrent: %s", err.Error()) 49 | t.Fail() 50 | return 51 | } 52 | 53 | torrent, err := st.OpenTorrent(meta) 54 | if err != nil { 55 | t.Log("failed to open torrent") 56 | t.Fail() 57 | return 58 | } 59 | err = torrent.VerifyAll() 60 | if err != nil { 61 | t.Log("verify all failed") 62 | t.Fail() 63 | return 64 | } 65 | var pc common.PieceData 66 | err = torrent.GetPiece(common.PieceRequest{ 67 | Index: 1, 68 | Begin: 0, 69 | Length: 16384, 70 | }, &pc) 71 | 72 | if err != nil { 73 | t.Log(err.Error()) 74 | t.Fail() 75 | return 76 | } 77 | 78 | log.Infof("put chunk: idx=%d offset=%d", pc.Index, pc.Begin) 79 | 80 | err = torrent.PutChunk(&pc) 81 | if err != nil { 82 | t.Log(err.Error()) 83 | t.Fail() 84 | return 85 | } 86 | 87 | log.Infof("verify piece 1") 88 | err = torrent.VerifyPiece(1) 89 | if err != nil { 90 | t.Log(err.Error()) 91 | t.Fail() 92 | return 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /lib/configparser/example_test.go: -------------------------------------------------------------------------------- 1 | package configparser_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/majestrate/XD/lib/configparser" 6 | "log" 7 | ) 8 | 9 | // Read and modify a configuration file 10 | func Example() { 11 | config, err := configparser.Read("/etc/config.ini") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | // Print the full configuration 16 | fmt.Println(config) 17 | 18 | // get a section 19 | section, err := config.Section("MYSQLD DEFAULT") 20 | if err != nil { 21 | log.Fatal(err) 22 | } else { 23 | fmt.Printf("TotalSendBufferMemory=%s\n", section.ValueOf("TotalSendBufferMemory")) 24 | 25 | // set new value 26 | var oldValue = section.SetValueFor("TotalSendBufferMemory", "256M") 27 | fmt.Printf("TotalSendBufferMemory=%s, old value=%s\n", section.ValueOf("TotalSendBufferMemory"), oldValue) 28 | 29 | // delete option 30 | oldValue = section.Delete("DefaultOperationRedoProblemAction") 31 | fmt.Println("Deleted DefaultOperationRedoProblemAction: " + oldValue) 32 | 33 | // add new options 34 | section.Add("innodb_buffer_pool_size", "64G") 35 | section.Add("innodb_buffer_pool_instances", "8") 36 | } 37 | 38 | // add a new section and options 39 | section = config.NewSection("NDBD MGM") 40 | section.Add("NodeId", "2") 41 | section.Add("HostName", "10.10.10.10") 42 | section.Add("PortNumber", "1186") 43 | section.Add("ArbitrationRank", "1") 44 | 45 | // find all sections ending with .webservers 46 | sections, err := config.Find(".webservers$") 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | for _, section := range sections { 51 | fmt.Print(section) 52 | } 53 | // or 54 | config.PrintSection("dc1.webservers") 55 | 56 | sections, err = config.Delete("NDB_MGMD DEFAULT") 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | // deleted sections 61 | for _, section := range sections { 62 | fmt.Print(section) 63 | } 64 | 65 | options := section.Options() 66 | fmt.Println(options["HostName"]) 67 | 68 | // save the new config. the original will be renamed to /etc/config.ini.bak 69 | err = configparser.Save(config, "/etc/config.ini") 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | version="$(git describe)" 4 | git clean -xdf 5 | key="${SIGNER:-jeff@lokinet.io}" 6 | 7 | _build_release() 8 | { 9 | exe="$1" 10 | builddir="$2" 11 | key="$3" 12 | for os in linux freebsd ; do 13 | for arch in amd64 arm ppc64 ; do 14 | export XD=$builddir/$exe-$os-$arch 15 | GOOS=$os GOARCH=$arch make clean $XD && gpg -u $key --sign --detach $XD 16 | done 17 | done 18 | export XD=$builddir/$exe-darwin 19 | GOOS=darwin GOARCH=amd64 make clean $XD && gpg -u $key --sign --detach $XD 20 | export XD=$builddir/$exe-windows.exe 21 | GOOS=windows GOARCH=amd64 make clean $XD && gpg -u $key --sign --detach $XD 22 | } 23 | 24 | 25 | export GIT_VERSION="" 26 | build=XD-$version 27 | mkdir -p $build 28 | # build i2p version 29 | export LOKINET=0 30 | _build_release XD-i2p-$version $build $key 31 | # build lokinet version 32 | export LOKINET=1 33 | _build_release XD-lokinet-$version $build $key 34 | 35 | # verify sigs and makes hashes 36 | for sig in $build/*.sig ; do 37 | gpg --verify $sig && b2sum -b $(echo $sig | sed s/\\.sig//) >> $build/HASHES.txt 38 | done 39 | 40 | # check hashes 41 | b2sum -c $build/HASHES.txt || exit 1 42 | 43 | rm -f $build/README.txt 44 | echo "To verify the integrity of XD $version use:" >> $build/README.txt 45 | echo "gpg --verify XD-$version.tar.xz.sig && tar -xJvf XD-$version.tar.xz && b2sum -c $build/HASHES.txt" >> $build/README.txt 46 | echo "" >> $build/README.txt 47 | echo "release hashes:" >> $build/README.txt 48 | echo "" >> $build/README.txt 49 | cat $build/HASHES.txt >> $build/README.txt 50 | 51 | gpg -u $key --clearsign --detach $build/README.txt 52 | mv $build/README.txt.asc $build/README.txt 53 | 54 | # make release tarball 55 | tar -cJvf XD-$version.tar.xz $build 56 | gpg -u $key --sign --detach XD-$version.tar.xz 57 | 58 | # make preformatted release notes 59 | echo '```' >> notes-$version 60 | cat $build/README.txt >> notes-$version 61 | echo '```' >> notes-$version 62 | 63 | # verify sig and upload release 64 | gpg --verify XD-$version.tar.xz.sig && gh release create --notes "XD $version" -R majestrate/XD -F notes-$version $version XD-$version.tar.xz{,.sig} 65 | 66 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPO := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 2 | LOGOS = $(REPO)/contrib/logos 3 | WEBUI_DIR = contrib/webui 4 | WEBUI = ./$(WEBUI_DIR) 5 | GO_ASSETS = $(REPO)/build-assets 6 | DOCROOT = $(WEBUI)/docroot 7 | WEBUI_LOGO = $(DOCROOT)/favicon.png 8 | WEB_FILES = $(DOCROOT)/index.html 9 | WEB_FILES += $(DOCROOT)/xd.min.js 10 | WEB_FILES += $(DOCROOT)/xd.css 11 | WEB_FILES += $(WEBUI_LOGO) 12 | WEBUI_CORE = $(DOCROOT)/xd.min.js 13 | WEBUI_CORE += $(DOCROOT)/xd.css 14 | WEBUI_PREFIX = /contrib/webui/docroot 15 | ASSETS = $(REPO)/lib/rpc/assets/assets.go 16 | 17 | TAGS ?= webui 18 | LOKINET ?= 0 19 | ifeq ($(LOKINET),1) 20 | TAGS += lokinet 21 | endif 22 | 23 | ifeq ($(GOOS),windows) 24 | BINEXT = .exe 25 | endif 26 | 27 | MKDIR = mkdir -p 28 | RM = rm -f 29 | CP = cp 30 | CPLINK = cp -P 31 | INSTALL = install 32 | LINK = ln -s 33 | CHMOD = chmod 34 | 35 | GIT_VERSION ?= $(shell test -e .git && git rev-parse --short HEAD || true) 36 | 37 | GO = go 38 | 39 | XD ?= XD$(BINEXT) 40 | CLI ?= XD-CLI$(BINEXT) 41 | 42 | build: $(CLI) 43 | 44 | 45 | $(XD): $(WEBUI_CORE) 46 | $(GO) build -a -ldflags "-X xd/lib/version.Git=$(GIT_VERSION)" -tags='$(TAGS)' -o $(XD) 47 | 48 | dev: $(WEBUI_CORE) 49 | $(GO) build -race -v -a -ldflags "-X xd/lib/version.Git=$(GIT_VERSION)" -tags='$(TAGS)' -o $(XD) 50 | 51 | $(CLI): $(XD) 52 | $(RM) $(CLI) 53 | $(LINK) $(XD) $(CLI) 54 | $(CHMOD) 755 $(CLI) 55 | 56 | test: 57 | $(GO) test ./... 58 | 59 | clean: webui-clean go-clean 60 | $(RM) $(CLI) 61 | 62 | distclean: clean clean-assets 63 | 64 | clean-assets: 65 | $(RM) $(ASSETS) 66 | 67 | webui-clean: 68 | $(RM) $(WEBUI_LOGO) 69 | $(MAKE) -C $(WEBUI) clean 70 | 71 | go-clean: 72 | $(GO) clean 73 | 74 | $(WEBUI_LOGO): 75 | $(CP) $(LOGOS)/xd_logo.png $(WEBUI_LOGO) 76 | 77 | $(WEBUI_CORE): $(WEBUI_LOGO) 78 | $(MAKE) -C $(WEBUI) 79 | $(CP) $(WEB_FILES) $(REPO)/lib/rpc/assets/ 80 | 81 | webui: $(WEBUI_CORE) 82 | 83 | no-webui: 84 | $(GO) build -ldflags "-X xd/lib/version.Git=$(GIT_VERSION)" -o $(XD) 85 | 86 | install: $(XD) $(CLI) 87 | $(MKDIR) $(PREFIX)/bin 88 | $(INSTALL) $(XD) $(PREFIX)/bin 89 | $(CPLINK) $(CLI) $(PREFIX)/bin 90 | 91 | uninstall: 92 | $(RM) $(PREFIX)/bin/$(XD) 93 | $(RM) $(PREFIX)/bin/$(CLI) 94 | 95 | .PHONY: build dev test clean distclean clean-assets webui-clean go-clean webui no-webui install uninstall 96 | -------------------------------------------------------------------------------- /lib/config/i2p.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/configparser" 5 | "github.com/majestrate/XD/lib/log" 6 | "github.com/majestrate/XD/lib/network/i2p" 7 | "github.com/majestrate/XD/lib/util" 8 | "os" 9 | ) 10 | 11 | type I2PConfig struct { 12 | Addr string 13 | Keyfile string 14 | Name string 15 | nameWasProvided bool 16 | I2CPOptions map[string]string 17 | Disabled bool 18 | } 19 | 20 | func (cfg *I2PConfig) Load(section *configparser.Section) error { 21 | cfg.I2CPOptions = make(map[string]string) 22 | cfg.I2CPOptions["i2cp.leaseSetEncType"] = "4,0" 23 | if section == nil { 24 | cfg.Addr = i2p.DEFAULT_ADDRESS 25 | cfg.Keyfile = "" 26 | cfg.Name = util.RandStr(5) 27 | cfg.Disabled = DisableI2PByDefault 28 | } else { 29 | cfg.Disabled = section.Get("disabled", "") == "1" 30 | cfg.Addr = section.Get("address", i2p.DEFAULT_ADDRESS) 31 | cfg.Keyfile = section.Get("keyfile", "") 32 | gen := util.RandStr(5) 33 | cfg.Name = section.Get("session", gen) 34 | cfg.nameWasProvided = cfg.Name != gen 35 | opts := section.Options() 36 | for k, v := range opts { 37 | if k == "address" || k == "keyfile" || k == "session" || k == "disabled" { 38 | continue 39 | } 40 | cfg.I2CPOptions[k] = v 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func (cfg *I2PConfig) Save(s *configparser.Section) error { 47 | opts := make(map[string]string) 48 | if cfg.I2CPOptions != nil { 49 | for k, v := range cfg.I2CPOptions { 50 | opts[k] = v 51 | } 52 | } 53 | opts["address"] = cfg.Addr 54 | if cfg.Keyfile != "" { 55 | opts["keyfile"] = cfg.Keyfile 56 | } 57 | if cfg.nameWasProvided { 58 | opts["session"] = cfg.Name 59 | } 60 | if cfg.Disabled { 61 | opts["disabled"] = "1" 62 | } else { 63 | opts["disabled"] = "0" 64 | } 65 | for k := range opts { 66 | s.Add(k, opts[k]) 67 | } 68 | return nil 69 | } 70 | 71 | // create an i2p session from this config 72 | func (cfg *I2PConfig) CreateSession() i2p.Session { 73 | log.Infof("create new i2p session with %s", cfg.Addr) 74 | return i2p.NewSession(util.RandStr(5), cfg.Addr, cfg.Keyfile, cfg.I2CPOptions) 75 | } 76 | 77 | // EnvI2PAddress is the name of the environmental variable to set the i2p address for XD 78 | const EnvI2PAddress = "XD_I2P_ADDRESS" 79 | 80 | func (cfg *I2PConfig) LoadEnv() { 81 | addr := os.Getenv(EnvI2PAddress) 82 | if addr != "" { 83 | cfg.Addr = addr 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/util/rate.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/zeebo/bencode" 5 | "io" 6 | "time" 7 | ) 8 | 9 | // (magnitude, time) 10 | type RateSample [2]uint64 11 | 12 | func (s RateSample) Value() uint64 { 13 | return s[0] 14 | } 15 | 16 | func (s RateSample) Time() time.Time { 17 | return time.Unix(int64(s[1]), 0) 18 | } 19 | 20 | func (s *RateSample) Clear() { 21 | s.Set(0) 22 | } 23 | 24 | func (s *RateSample) Set(n uint64) { 25 | (*s)[0] = n 26 | (*s)[1] = uint64(time.Now().Unix()) 27 | } 28 | 29 | func (s *RateSample) Add(n uint64) { 30 | (*s)[0] += n 31 | } 32 | 33 | type Rate struct { 34 | Samples []RateSample 35 | lastSampleIdx int 36 | } 37 | 38 | func NewRate(sampleLen int) *Rate { 39 | return &Rate{ 40 | Samples: make([]RateSample, sampleLen), 41 | } 42 | } 43 | 44 | func (r *Rate) BEncode(w io.Writer) (err error) { 45 | e := bencode.NewEncoder(w) 46 | err = e.Encode(r) 47 | return 48 | } 49 | 50 | func (r *Rate) BDecode(rd io.Reader) (err error) { 51 | d := bencode.NewDecoder(rd) 52 | err = d.Decode(r) 53 | return 54 | } 55 | 56 | func (r *Rate) Tick() { 57 | r.lastSampleIdx = (r.lastSampleIdx + 1) % len(r.Samples) 58 | r.Samples[r.lastSampleIdx].Clear() 59 | } 60 | 61 | func (r *Rate) AddSample(n uint64) { 62 | r.Samples[r.lastSampleIdx].Add(n) 63 | } 64 | 65 | func (r *Rate) Max() (max uint64) { 66 | for idx := range r.Samples { 67 | val := r.Samples[idx].Value() 68 | if val > max { 69 | max = val 70 | } 71 | } 72 | return 73 | } 74 | 75 | func (r *Rate) Current() (cur uint64) { 76 | cur = r.Samples[r.lastSampleIdx].Value() 77 | return 78 | } 79 | 80 | func (r *Rate) Min() (min uint64) { 81 | min = ^uint64(0) 82 | for idx := range r.Samples { 83 | val := r.Samples[idx].Value() 84 | if val < min { 85 | min = val 86 | } 87 | } 88 | return 89 | } 90 | 91 | func (r *Rate) PrevTickTime() time.Time { 92 | if r.lastSampleIdx == 0 { 93 | return r.Samples[len(r.Samples)-1].Time() 94 | } 95 | return r.Samples[r.lastSampleIdx-1].Time() 96 | } 97 | 98 | func (r *Rate) Mean() float64 { 99 | lastTick := r.PrevTickTime().Unix() 100 | sum := uint64(0) 101 | for idx := range r.Samples { 102 | sum += r.Samples[idx].Value() 103 | } 104 | sum /= uint64(len(r.Samples)) 105 | now := float64(time.Now().Unix() - lastTick) 106 | if now <= 0 { 107 | now = 1.0 108 | } 109 | return float64(sum) / now 110 | } 111 | -------------------------------------------------------------------------------- /docs/fr/readme.md: -------------------------------------------------------------------------------- 1 | # Pour débuter 2 | 3 | Une fois que vous avez compilé ou obtenu une version, une interface web est 4 | disponible par défaut à http://127.0.0.1:1488/. 5 | 6 | Les utilisateurs de Windows sont encouragés à utiliser l'interface web s'ils ne 7 | peuvent pas utiliser l'outil en ligne de commande. 8 | 9 | ## Ligne de commande (Unix) 10 | 11 | Pour utiliser l'outil en ligne de commande, vous devez établir un lien 12 | symbolique de `XD` vers `XD-cli`: 13 | 14 | $ ln -s XD XD-cli 15 | 16 | Pour ajouter des torrents depuis un fichier de semence à travers i2p: 17 | 18 | XD-cli add http://somesite.i2p/some/url/to/a/torrent.torrent 19 | 20 | Pour lister les torrents actifs: 21 | 22 | XD-cli list 23 | 24 | Pour augmenter le nombre de morceaux à demander en parallèle, utilisez la 25 | commande `set-piece-window` (celle-ci sera peut-être retirée dans le futur): 26 | 27 | XD-cli set-piece-window 10 28 | 29 | ## Ligne de commande (Windows) 30 | 31 | Sous Windows, faites une copie de `XD.exe` et appelez-la `XD-cli.exe`. 32 | 33 | Toutes les commandes sont comme sous Unix, mis à part que `/` peut avoir besoin 34 | d'être échappé, dépendant du terminal utilisé. 35 | 36 | TODO: ajouter plus de documentation pour Windows 37 | 38 | # Configuration 39 | 40 | XD utilise le format ini pour sa configuration. Le fichier de configuration 41 | principal est `torrents.ini` et est autogénéré avec les valeurs par défaut s'il 42 | n'est pas présent. 43 | 44 | ## Configuration pour stockage SFTP 45 | 46 | XD peut utiliser un système de fichier distant accédé par SFTP. Pour utiliser 47 | cette fonctionnalité, elle doit être configurée. 48 | 49 | Exemple de configuration: 50 | 51 | [storage] 52 | rootdir=/mnt/storage/XD/ 53 | metadata=/mnt/storage/XD/metadata 54 | downloads=/mnt/storage/XD/downloads 55 | sftp_host=remote.server.tld 56 | sftp_port=22 57 | sftp_user=votre_usager_ssh 58 | sftp_remotekey=cle_publique_du_serveur_en_base64 59 | sftp_keyfile=/chemin/vers/cle/ssh/privee/id_rsa 60 | sftp=1 61 | 62 | Cet exemple établit une connection à `remote.server.tld:22` avec l'usager 63 | `votre_usager_ssh` en utilisant la clé privée (non-chiffrée) et utilise 64 | `/mnt/storage/XD/` sur le serveur distant pour stocker les torrents et les 65 | méta-données. 66 | 67 | La clé publique du serveur est habituellement située dans 68 | `/etc/ssh/ssh_host_*.pub` sous la forme 69 | `ssh-quelque-chose la_cle_est_ecrite_ici_en_base64 root@hostname`. 70 | La valeur en base64 doit être utilisée pour `sftp_remotekey`. 71 | 72 | -------------------------------------------------------------------------------- /lib/network/i2p/packetconn.go: -------------------------------------------------------------------------------- 1 | package i2p 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "time" 7 | ) 8 | 9 | // tcp/i2p connection 10 | // implements net.Conn 11 | type I2PPacketConn struct { 12 | // underlying connection 13 | c net.PacketConn 14 | // our local address 15 | laddr Addr 16 | // remote sam addr 17 | samaddr net.Addr 18 | // sam version 19 | version string 20 | } 21 | 22 | // implements net.PacketConn 23 | func (c *I2PPacketConn) ReadFrom(d []byte) (n int, from net.Addr, err error) { 24 | var buff [65336]byte 25 | for err == nil { 26 | n, from, err = c.c.ReadFrom(buff[:]) 27 | if err == nil { 28 | if from.String() != c.samaddr.String() { 29 | // drop silent because source missmatch 30 | continue 31 | } 32 | idx := bytes.IndexByte(buff[:n], 10) 33 | if idx <= 0 { 34 | // drop silent because invalid format 35 | continue 36 | } 37 | parts := bytes.SplitN(buff[:idx-1], []byte{' '}, 2) 38 | if len(parts) < 2 { 39 | // drop silent because invalid format 40 | continue 41 | } 42 | parts = bytes.Split(parts[1], []byte{' '}) 43 | 44 | from = I2PAddr(string(parts[len(parts)-1])) 45 | data := buff[idx+1 : n] 46 | n -= 1 + idx 47 | if len(d) < n { 48 | // drop silent because too big for caller 49 | continue 50 | } 51 | 52 | copy(d, data) 53 | break 54 | } 55 | } 56 | return 57 | } 58 | 59 | // implements net.PacketConn 60 | func (c *I2PPacketConn) WriteTo(d []byte, to net.Addr) (n int, err error) { 61 | tostr := c.version + " " + to.String() 62 | tolen := len(tostr) 63 | buff := make([]byte, len(d)+tolen+1) 64 | copy(buff, tostr) 65 | buff[tolen] = '\n' 66 | copy(buff[:tolen+1], d) 67 | n, err = c.c.WriteTo(buff, c.samaddr) 68 | if err == nil { 69 | n = len(d) 70 | } 71 | return 72 | } 73 | 74 | // implements net.PacketConn 75 | func (c *I2PPacketConn) Close() error { 76 | if c.c == nil { 77 | return nil 78 | } 79 | return c.c.Close() 80 | } 81 | 82 | // implements net.PacketConn 83 | func (c *I2PPacketConn) LocalAddr() net.Addr { 84 | return c.laddr 85 | } 86 | 87 | // implements net.PacketConn 88 | func (c *I2PPacketConn) SetDeadline(t time.Time) error { 89 | return c.c.SetDeadline(t) 90 | } 91 | 92 | // implements net.PacketConn 93 | func (c *I2PPacketConn) SetReadDeadline(t time.Time) error { 94 | return c.c.SetReadDeadline(t) 95 | } 96 | 97 | // implements net.PacketConn 98 | func (c *I2PPacketConn) SetWriteDeadline(t time.Time) error { 99 | return c.c.SetWriteDeadline(t) 100 | } 101 | -------------------------------------------------------------------------------- /lib/network/i2p/keyfile.go: -------------------------------------------------------------------------------- 1 | package i2p 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | const SigType = 7 13 | 14 | // a destination keypair file 15 | type Keyfile struct { 16 | privkey string 17 | pubkey string 18 | fname string 19 | } 20 | 21 | // save to filesystem 22 | func (k *Keyfile) Store() (err error) { 23 | if len(k.fname) > 0 { 24 | var f *os.File 25 | f, err = os.OpenFile(k.fname, os.O_CREATE|os.O_WRONLY, 0600) 26 | if err == nil { 27 | err = k.write(f) 28 | f.Close() 29 | } 30 | } 31 | return 32 | } 33 | 34 | // load from filesystem 35 | func (k *Keyfile) Load() (err error) { 36 | if len(k.fname) > 0 { 37 | var f *os.File 38 | f, err = os.Open(k.fname) 39 | if err == nil { 40 | err = k.read(f) 41 | f.Close() 42 | } 43 | } 44 | return 45 | } 46 | 47 | func (k *Keyfile) write(w io.Writer) (err error) { 48 | _, err = fmt.Fprintf(w, "%s\n%s\n", k.privkey, k.pubkey) 49 | return 50 | } 51 | 52 | func (k *Keyfile) read(r io.Reader) (err error) { 53 | br := bufio.NewReader(r) 54 | k.privkey, err = br.ReadString(10) 55 | k.pubkey, err = br.ReadString(10) 56 | k.privkey = strings.Trim(k.privkey, "\n") 57 | k.pubkey = strings.Trim(k.pubkey, "\n") 58 | return 59 | } 60 | 61 | func (k *Keyfile) Addr() Addr { 62 | return I2PAddr(k.pubkey) 63 | } 64 | 65 | // ensure keys are created using a control socket 66 | func (k *Keyfile) ensure(nc net.Conn) (err error) { 67 | if len(k.fname) > 0 { 68 | _, err = os.Stat(k.fname) 69 | } 70 | if os.IsNotExist(err) || len(k.fname) == 0 { 71 | // no keyfile 72 | _, err = fmt.Fprintf(nc, "DEST GENERATE SIGNATURE_TYPE=%d\n", SigType) 73 | r := bufio.NewReader(nc) 74 | var line string 75 | line, err = r.ReadString(10) 76 | if err == nil { 77 | sc := bufio.NewScanner(strings.NewReader(line)) 78 | sc.Split(bufio.ScanWords) 79 | for sc.Scan() { 80 | txt := sc.Text() 81 | upper := strings.ToUpper(txt) 82 | if upper == "DEST" { 83 | continue 84 | } 85 | if upper == "REPLY" { 86 | continue 87 | } 88 | if strings.HasPrefix(upper, "PUB=") { 89 | k.pubkey = txt[4:] 90 | continue 91 | } 92 | if strings.HasPrefix(upper, "PRIV=") { 93 | k.privkey = txt[5:] 94 | continue 95 | } 96 | } 97 | // store new keys 98 | err = k.Store() 99 | return 100 | } 101 | } 102 | // load keys 103 | err = k.Load() 104 | return 105 | } 106 | 107 | // create new keyfile given filepath 108 | func NewKeyfile(f string) *Keyfile { 109 | if strings.ToUpper(f) == "TRANSIENT" { 110 | f = "" 111 | } 112 | return &Keyfile{ 113 | fname: f, 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XD 2 | 3 | BitTorrent Client written in GO (as a joke) 4 | 5 | ![XD](contrib/logos/xd_logo_256x256.png) 6 | 7 | [![Packaging status](https://repology.org/badge/vertical-allrepos/xd-torrent.svg)](https://repology.org/metapackage/xd-torrent) 8 | 9 | ![Downloads](https://img.shields.io/github/downloads/majestrate/XD/total.svg) 10 | 11 | ![MIT License](https://img.shields.io/github/license/majestrate/XD.svg) 12 | ![Logo is ebin](https://img.shields.io/badge/logo-ebin-brightgreen.svg) 13 | 14 | ## Features 15 | 16 | Current: 17 | 18 | * i2p only, no chances of cross network contamination, aka no way to leak IP. 19 | * works with [i2pd](https://github.com/purplei2p/i2pd) and Java I2P using the SAM api 20 | * Magnet URIs 21 | * memes 22 | 23 | Soon: 24 | 25 | * transmission compatible RPC 26 | 27 | Eventually: 28 | 29 | * DHT Support 30 | * Maggot Support 31 | 32 | ## Dependencies 33 | 34 | * GNU Make 35 | * GO 1.24 or higher 36 | 37 | 38 | ## Building 39 | 40 | right now the best way to build is with `make` 41 | 42 | $ git clone https://github.com/majestrate/XD 43 | $ cd XD 44 | $ make 45 | 46 | if you do not want to build with embedded webui instead run: 47 | 48 | $ make no-webui 49 | 50 | you can build with go get using: 51 | 52 | $ go get -u -v github.com/majestrate/XD 53 | 54 | please note that using `go get` disables the webui. 55 | 56 | or use `go get`: 57 | 58 | $ go get -u -v -tags lokinet github.com/majestrate/XD 59 | 60 | ### cross compile for Raspberry PI 61 | 62 | Set `GOARCH` and `GOOS` when building with make: 63 | 64 | $ make GOARCH=arm GOOS=linux 65 | 66 | ## Usage 67 | 68 | To autogenerate a new config and start: 69 | 70 | $ ./XD torrents.ini 71 | 72 | after started put torrent files into `./storage/downloads/` to start downloading 73 | 74 | to seed torrents put data files into `./storage/downloads/` first then add torrent files 75 | 76 | if you compiled with web ui it will be up at http://127.0.0.1:1776/ 77 | 78 | To use the RPC Tool symlink `XD` to `XD-CLI` 79 | 80 | $ ln -s XD XD-CLI 81 | 82 | to list torrents run: 83 | 84 | $ ./XD-CLI list 85 | 86 | to add a torrent from http server: 87 | 88 | $ ./XD-CLI add http://somehwere.i2p/some_torrent_that_is_not_fake.torrent 89 | 90 | Optionally on non windows systems you can install XD to `/usr/local/` 91 | 92 | # make install 93 | 94 | Or your home directory, make sure `$HOME/bin` is in your $PATH 95 | 96 | $ make install PREFIX=$HOME 97 | 98 | Tunnel length and quanity along with all other i2cp options are set in the i2p section of the configuration: 99 | ``` 100 | [i2p] 101 | inbound.length=1 102 | outbound.length=1 103 | ``` 104 | -------------------------------------------------------------------------------- /lib/bittorrent/handshake.go: -------------------------------------------------------------------------------- 1 | package bittorrent 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "github.com/majestrate/XD/lib/common" 7 | "github.com/majestrate/XD/lib/util" 8 | "io" 9 | ) 10 | 11 | const handshakeV1 = "BitTorrent protocol" 12 | 13 | // Reserved is reserved data in handshake 14 | type Reserved struct { 15 | data [8]uint8 16 | } 17 | 18 | // ReservedBit is a bit set in reserved data 19 | type ReservedBit uint8 20 | 21 | func (b ReservedBit) mask() uint8 { 22 | return 1 << (7 - (uint8(b-1) % 8)) 23 | } 24 | 25 | func (b ReservedBit) index() uint8 { 26 | return uint8(b-1) / 8 27 | } 28 | 29 | // Has returns true if reserved bit is set 30 | func (r Reserved) Has(bit ReservedBit) bool { 31 | return r.data[bit.index()]&bit.mask() == bit.mask() 32 | } 33 | 34 | // Set sets a reserved bit 35 | func (r *Reserved) Set(bit ReservedBit) { 36 | r.data[bit.index()] |= bit.mask() 37 | } 38 | 39 | // Intersect clears bits in `r` that are not set in `other` 40 | func (r *Reserved) Intersect(other Reserved) { 41 | for i := range r.data { 42 | r.data[i] &= other.data[i] 43 | } 44 | } 45 | 46 | // Extension is ReservedBit for bittorrent extensions 47 | const Extension = ReservedBit(44) 48 | 49 | // DHT is ReservedBit for BT DHT 50 | const DHT = ReservedBit(64) 51 | 52 | // ErrInvalidHandshake is returned when a handshake contained invalid format 53 | var ErrInvalidHandshake = errors.New("invalid bittorrent handshake") 54 | 55 | // Handshake is a bittorrent protocol handshake info 56 | type Handshake struct { 57 | Reserved Reserved 58 | Infohash common.Infohash 59 | PeerID common.PeerID 60 | } 61 | 62 | // FromBytes parses bittorrent handshake from byteslice 63 | func (h *Handshake) FromBytes(data []byte) (err error) { 64 | if len(data) < 68 { 65 | err = ErrInvalidHandshake 66 | } else { 67 | buff := data[:68] 68 | if buff[0] == 19 && bytes.Equal(buff[1:20], []byte(handshakeV1)) { 69 | copy(h.Reserved.data[:], buff[20:28]) 70 | copy(h.Infohash[:], buff[28:48]) 71 | copy(h.PeerID[:], buff[48:68]) 72 | } else { 73 | err = ErrInvalidHandshake 74 | } 75 | } 76 | return 77 | } 78 | 79 | // Recv reads handshake via reader 80 | func (h *Handshake) Recv(r io.Reader) (err error) { 81 | var buff [68]byte 82 | _, err = io.ReadFull(r, buff[:]) 83 | if err == nil { 84 | err = h.FromBytes(buff[:]) 85 | } 86 | return 87 | } 88 | 89 | // Send sends handshake via writer 90 | func (h *Handshake) Send(w io.Writer) (err error) { 91 | var buff [68]byte 92 | buff[0] = 19 93 | copy(buff[1:], []byte(handshakeV1)) 94 | copy(buff[20:28], h.Reserved.data[:]) 95 | copy(buff[28:48], h.Infohash[:]) 96 | copy(buff[48:68], h.PeerID[:]) 97 | err = util.WriteFull(w, buff[:]) 98 | return 99 | } 100 | -------------------------------------------------------------------------------- /debian/xd.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: XD 4 | # Required-Start: $network $local_fs $remote_fs 5 | # Required-Stop: $remote_fs 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Standalone I2P BitTorrent Client 9 | ### END INIT INFO 10 | 11 | # Author: r4sas 12 | 13 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 14 | DESC="Standalone I2P BitTorrent Client" # Introduce a short description here 15 | NAME=XD # Introduce the short server's name here 16 | DAEMON=/usr/bin/$NAME # Introduce the server's location here 17 | PIDFILE=/var/run/$NAME/$NAME.pid 18 | XDCONF=/etc/$NAME/xd.ini 19 | XDHOME=/var/lib/XD 20 | USER="debian-xd" 21 | 22 | # Exit if the package is not installed 23 | [ -x $DAEMON ] || exit 0 24 | 25 | . /lib/init/vars.sh 26 | . /lib/lsb/init-functions 27 | 28 | # Function that starts the daemon/service 29 | do_start() 30 | { 31 | # Return 32 | # 0 if daemon has been started 33 | # 1 if daemon was already running 34 | # 2 if daemon could not be started 35 | 36 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --chuid "$USER" --test > /dev/null \ 37 | || return 1 38 | start-stop-daemon --start --quiet --background --make-pidfile --pidfile $PIDFILE --exec $DAEMON --chuid "$USER" --chdir $XDHOME -- \ 39 | $XDCONF > /dev/null 2>&1 \ 40 | || return 2 41 | return $? 42 | } 43 | 44 | # Function that stops the daemon/service 45 | do_stop() 46 | { 47 | # Return 48 | # 0 if daemon has been stopped 49 | # 1 if daemon was already stopped 50 | # 2 if daemon could not be stopped 51 | # other if a failure occurred 52 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --exec $DAEMON --name $NAME 53 | return "$?" 54 | } 55 | 56 | 57 | case "$1" in 58 | start) 59 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" 60 | do_start 61 | case "$?" in 62 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 63 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 64 | esac 65 | ;; 66 | stop) 67 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 68 | do_stop 69 | case "$?" in 70 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 71 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 72 | esac 73 | ;; 74 | restart|reload|force-reload) 75 | log_daemon_msg "Restarting $DESC" "$NAME" 76 | do_stop 77 | case "$?" in 78 | 0|1) 79 | do_start 80 | case "$?" in 81 | 0) log_end_msg 0 ;; 82 | 1) log_end_msg 1 ;; # Old process is still running 83 | *) log_end_msg 1 ;; # Failed to start 84 | esac 85 | ;; 86 | *) 87 | # Failed to stop 88 | log_end_msg 1 89 | ;; 90 | esac 91 | ;; 92 | *) 93 | echo "Usage: $0 {start|stop|restart}" >&2 94 | exit 3 95 | ;; 96 | esac 97 | 98 | : 99 | -------------------------------------------------------------------------------- /lib/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | "github.com/majestrate/XD/lib/bittorrent" 6 | "github.com/majestrate/XD/lib/common" 7 | "github.com/majestrate/XD/lib/metainfo" 8 | "github.com/majestrate/XD/lib/stats" 9 | ) 10 | 11 | var ErrNoMetaInfo = errors.New("no torrent file") 12 | var ErrMetaInfoMissmatch = errors.New("torrent infohash does not match") 13 | 14 | // storage session for 1 torrent 15 | type Torrent interface { 16 | 17 | // allocate all files for download 18 | Allocate() error 19 | 20 | // verify all piece data 21 | VerifyAll() error 22 | 23 | // return true if we are currently doing a deep check 24 | Checking() bool 25 | 26 | // put a chunk of data 27 | PutChunk(pc *common.PieceData) error 28 | 29 | // visit a piece from storage 30 | GetPiece(r common.PieceRequest, pc *common.PieceData) error 31 | 32 | // verify a piece by index 33 | VerifyPiece(idx uint32) error 34 | 35 | // get metainfo 36 | MetaInfo() *metainfo.TorrentFile 37 | 38 | // get infohash 39 | Infohash() common.Infohash 40 | 41 | // get bitfield, if cached return cache otherwise compute and cache 42 | Bitfield() *bittorrent.Bitfield 43 | 44 | // get number of bytes we already downloaded 45 | DownloadedSize() uint64 46 | 47 | // get number of bytes remaining we need to download 48 | DownloadRemaining() uint64 49 | 50 | // flush bitfield to disk 51 | Flush() error 52 | 53 | // get name of this torrent 54 | Name() string 55 | 56 | // delete all files and metadata for this torrent 57 | Delete() error 58 | 59 | // save torrent stats 60 | SaveStats(s *stats.Tracker) error 61 | 62 | // get a list of files for this torrent 63 | // returns absolute path of all downloaded files 64 | FileList() []string 65 | 66 | // move data files to other directory, blocks for a LONG time 67 | MoveTo(other string) error 68 | 69 | // verify data and move to seeding directory 70 | Seed() (bool, error) 71 | 72 | // set metainfo for empty torrent 73 | PutInfoBytes(info []byte) error 74 | 75 | // get directory for data files 76 | DownloadDir() string 77 | } 78 | 79 | // torrent storage driver 80 | type Storage interface { 81 | 82 | // Close and flush storage backend 83 | Close() error 84 | 85 | // create a torrent with no meta info 86 | EmptyTorrent(ih common.Infohash) Torrent 87 | 88 | // open a storage session for a torrent 89 | // does not verify any piece data 90 | OpenTorrent(info *metainfo.TorrentFile) (Torrent, error) 91 | 92 | // open all torrents tracked by this storage 93 | // does not verify any piece data 94 | OpenAllTorrents() ([]Torrent, error) 95 | 96 | // intialize backend 97 | Init() error 98 | 99 | // returns nil if we have no new torrents added from backend 100 | // returns next new torrents added to storage 101 | PollNewTorrents() []Torrent 102 | 103 | // run mainloop 104 | Run() 105 | } 106 | -------------------------------------------------------------------------------- /lib/configparser/README.md: -------------------------------------------------------------------------------- 1 | configparser 2 | ============ 3 | Package configparser provides a simple parser for reading/writing configuration (INI) files. 4 | 5 | Supports reading/writing the INI file format in addition to: 6 | 7 | - Reading/writing duplicate section names (ex: MySQL NDB engine's config.ini) 8 | - Options without values (ex: can be used to group a set of hostnames) 9 | - Options without a named section (ex: a simple option=value file) 10 | - Find sections with regexp pattern matching on section names, ex: dc1.east.webservers where regex is '.webservers' 11 | - # or ; as comment delimiter 12 | - = or : as value delimiter 13 | 14 | ```go 15 | package configparser_test 16 | 17 | import ( 18 | "fmt" 19 | "github.com/alyu/configparser" 20 | "log" 21 | ) 22 | 23 | // Read and modify a configuration file 24 | func Example() { 25 | config, err := configparser.Read("/etc/config.ini") 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | // Print the full configuration 30 | fmt.Println(config) 31 | 32 | // get a section 33 | section, err := config.Section("MYSQLD DEFAULT") 34 | if err != nil { 35 | log.Fatal(err) 36 | } else { 37 | fmt.Printf("TotalSendBufferMemory=%s\n", section.ValueOf("TotalSendBufferMemory")) 38 | 39 | // set new value 40 | var oldValue = section.SetValueFor("TotalSendBufferMemory", "256M") 41 | fmt.Printf("TotalSendBufferMemory=%s, old value=%s\n", section.ValueOf("TotalSendBufferMemory"), oldValue) 42 | 43 | // delete option 44 | oldValue = section.Delete("DefaultOperationRedoProblemAction") 45 | fmt.Println("Deleted DefaultOperationRedoProblemAction: " + oldValue) 46 | 47 | // add new options 48 | section.Add("innodb_buffer_pool_size", "64G") 49 | section.Add("innodb_buffer_pool_instances", "8") 50 | } 51 | 52 | // add a new section and options 53 | section = config.NewSection("NDBD MGM") 54 | section.Add("NodeId", "2") 55 | section.Add("HostName", "10.10.10.10") 56 | section.Add("PortNumber", "1186") 57 | section.Add("ArbitrationRank", "1") 58 | 59 | // find all sections ending with .webservers 60 | sections, err := config.Find(".webservers$") 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | for _, section := range sections { 65 | fmt.Print(section) 66 | } 67 | // or 68 | config.PrintSection("dc1.webservers") 69 | 70 | sections, err = config.Delete("NDB_MGMD DEFAULT") 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | // deleted sections 75 | for _, section := range sections { 76 | fmt.Print(section) 77 | } 78 | 79 | options := section.Options() 80 | fmt.Println(options["HostName"]) 81 | 82 | // save the new config. the original will be renamed to /etc/config.ini.bak 83 | err = configparser.Save(config, "/etc/config.ini") 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | } 88 | ``` -------------------------------------------------------------------------------- /lib/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "github.com/majestrate/XD/lib/sync" 6 | "io" 7 | "os" 8 | "strings" 9 | "time" 10 | //t "github.com/majestrate/XD/lib/translate" 11 | ) 12 | 13 | var mtx sync.Mutex 14 | 15 | type logLevel int 16 | 17 | const ( 18 | debug = logLevel(0) 19 | info = logLevel(1) 20 | warn = logLevel(2) 21 | err = logLevel(3) 22 | fatal = logLevel(4) 23 | ) 24 | 25 | func (l logLevel) Int() int { 26 | return int(l) 27 | } 28 | 29 | func (l logLevel) Name() string { 30 | 31 | switch l { 32 | case debug: 33 | return "DBG" 34 | case info: 35 | return "NFO" 36 | case warn: 37 | return "WRN" 38 | case err: 39 | return "ERR" 40 | case fatal: 41 | return "FTL" 42 | default: 43 | return "???" 44 | } 45 | 46 | } 47 | 48 | var level = info 49 | 50 | // SetLevel sets global logger level 51 | func SetLevel(l string) { 52 | l = strings.ToLower(l) 53 | if l == "debug" { 54 | level = debug 55 | } else if l == "info" { 56 | level = info 57 | } else if l == "warn" { 58 | level = warn 59 | } else if l == "err" { 60 | level = err 61 | } else if l == "fatal" { 62 | level = fatal 63 | } else { 64 | panic(fmt.Sprintf("invalid log level: '%s'", l)) 65 | } 66 | } 67 | 68 | var out io.Writer = os.Stdout 69 | 70 | // SetOutput sets logging to output to a writer 71 | func SetOutput(w io.Writer) { 72 | out = w 73 | } 74 | 75 | func accept(lvl logLevel) bool { 76 | return lvl.Int() >= level.Int() 77 | } 78 | 79 | func log(lvl logLevel, f string, args ...interface{}) { 80 | if accept(lvl) { 81 | m := fmt.Sprintf(f, args...) 82 | t := time.Now() 83 | mtx.Lock() 84 | fmt.Fprintf(out, "%s[%s] %s\t%s%s", lvl.Color(), lvl.Name(), t, m, colorReset) 85 | fmt.Fprintln(out) 86 | mtx.Unlock() 87 | if lvl == fatal { 88 | panic(m) 89 | } 90 | } 91 | } 92 | 93 | // Debug prints debug message 94 | func Debug(msg string) { 95 | log(debug, msg) 96 | } 97 | 98 | // Debugf prints formatted debug message 99 | func Debugf(f string, args ...interface{}) { 100 | log(debug, f, args...) 101 | } 102 | 103 | // Info prints info log message 104 | func Info(msg string) { 105 | log(info, msg) 106 | } 107 | 108 | // Infof prints formatted info log message 109 | func Infof(f string, args ...interface{}) { 110 | log(info, f, args...) 111 | } 112 | 113 | // Warn prints warn log message 114 | func Warn(msg string) { 115 | log(warn, msg) 116 | } 117 | 118 | // Warnf prints formatted warn log message 119 | func Warnf(f string, args ...interface{}) { 120 | log(warn, f, args...) 121 | } 122 | 123 | // Error prints error log message 124 | func Error(msg string) { 125 | log(err, msg) 126 | } 127 | 128 | // Errorf prints formatted error log message 129 | func Errorf(f string, args ...interface{}) { 130 | log(err, f, args...) 131 | } 132 | 133 | // Fatal print fatal error and panic 134 | func Fatal(msg string) { 135 | log(fatal, msg) 136 | } 137 | 138 | // Fatalf print formatted fatal error and panic 139 | func Fatalf(f string, args ...interface{}) { 140 | log(fatal, f, args...) 141 | } 142 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 4 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 5 | github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= 6 | github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= 7 | github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= 8 | github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 13 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/zeebo/bencode v1.0.0 h1:zgop0Wu1nu4IexAZeCZ5qbsjU4O1vMrfCrVgUjbHVuA= 15 | github.com/zeebo/bencode v1.0.0/go.mod h1:Ct7CkrWIQuLWAy9M3atFHYq4kG9Ao/SsY5cdtCXmp9Y= 16 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 17 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 18 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 19 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 20 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 21 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 25 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 26 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 27 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 28 | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 29 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 30 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= 33 | gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 35 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/holder.go: -------------------------------------------------------------------------------- 1 | package swarm 2 | 3 | import ( 4 | "github.com/majestrate/XD/lib/common" 5 | "github.com/majestrate/XD/lib/network" 6 | "github.com/majestrate/XD/lib/storage" 7 | "github.com/majestrate/XD/lib/sync" 8 | ) 9 | 10 | // torrent swarm container 11 | type Holder struct { 12 | closing bool 13 | st storage.Storage 14 | torrents sync.Map 15 | torrentsByID sync.Map 16 | MaxReq int 17 | QueueSize int 18 | } 19 | 20 | func (h *Holder) TorrentIDs() (ids map[int64]string) { 21 | ids = make(map[int64]string) 22 | h.ForEachTorrent(func(t *Torrent) { 23 | ids[t.TID] = t.Infohash().Hex() 24 | }) 25 | return 26 | } 27 | 28 | func (h *Holder) GetTorrentByID(id int64) (t *Torrent) { 29 | tr, ok := h.torrentsByID.Load(id) 30 | if ok { 31 | t = tr.(*Torrent) 32 | } 33 | return 34 | } 35 | 36 | func (h *Holder) addTorrent(t storage.Torrent, getNet func() network.Network) { 37 | if h.closing { 38 | return 39 | } 40 | tr := newTorrent(t, getNet) 41 | tr.MaxRequests = h.MaxReq 42 | h.torrents.Store(t.Infohash().Hex(), tr) 43 | h.torrentsByID.Store(tr.TID, tr) 44 | } 45 | 46 | func (h *Holder) addMagnet(ih common.Infohash, getNet func() network.Network) { 47 | if h.closing { 48 | return 49 | } 50 | tr := newTorrent(h.st.EmptyTorrent(ih), getNet) 51 | tr.MaxRequests = h.MaxReq 52 | h.torrents.Store(ih.Hex(), tr) 53 | h.torrentsByID.Store(tr.TID, tr) 54 | } 55 | 56 | func (h *Holder) removeTorrent(ih common.Infohash) { 57 | if h.closing { 58 | return 59 | } 60 | tr, ok := h.torrents.Load(ih.Hex()) 61 | if ok { 62 | h.torrents.Delete(ih.Hex()) 63 | h.torrentsByID.Delete(tr.(*Torrent).TID) 64 | } 65 | } 66 | 67 | func (h *Holder) forEachTorrent(visit func(*Torrent), fork bool) { 68 | if h.closing { 69 | return 70 | } 71 | h.torrents.Range(func(_, v interface{}) bool { 72 | t := v.(*Torrent) 73 | if fork { 74 | go visit(t) 75 | } else { 76 | visit(t) 77 | } 78 | return true 79 | }) 80 | } 81 | 82 | func (h *Holder) ForEachTorrent(visit func(*Torrent)) { 83 | h.forEachTorrent(visit, false) 84 | } 85 | 86 | func (h *Holder) ForEachTorrentParallel(visit func(*Torrent)) { 87 | h.forEachTorrent(visit, true) 88 | } 89 | 90 | // find a torrent by infohash 91 | // returns nil if we don't have a torrent with this infohash 92 | func (h *Holder) GetTorrent(ih common.Infohash) (t *Torrent) { 93 | v, ok := h.torrents.Load(ih.Hex()) 94 | if ok { 95 | t = v.(*Torrent) 96 | } 97 | return 98 | } 99 | 100 | func (h *Holder) VisitTorrent(ih common.Infohash, visit func(*Torrent)) { 101 | visit(h.GetTorrent(ih)) 102 | } 103 | 104 | func (h *Holder) Close(announce bool) { 105 | if h.closing { 106 | return 107 | } 108 | var wg sync.WaitGroup 109 | h.closing = true 110 | h.torrentsByID.Range(func(k, _ interface{}) bool { 111 | h.torrentsByID.Delete(k) 112 | return false 113 | }) 114 | h.torrents.Range(func(k, v interface{}) bool { 115 | t := v.(*Torrent) 116 | wg.Add(1) 117 | go func() { 118 | if announce { 119 | t.Stop() 120 | } else { 121 | t.StopAnnouncing(false) 122 | t.Close() 123 | } 124 | h.torrents.Delete(k) 125 | wg.Add(-1) 126 | }() 127 | return false 128 | }) 129 | wg.Wait() 130 | return 131 | } 132 | -------------------------------------------------------------------------------- /lib/rpc/transmission/rpc.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/majestrate/XD/lib/bittorrent/swarm" 7 | "github.com/majestrate/XD/lib/log" 8 | "github.com/majestrate/XD/lib/sync" 9 | "github.com/majestrate/XD/lib/util" 10 | "io" 11 | "net" 12 | "net/http" 13 | ) 14 | 15 | type Server struct { 16 | sw *swarm.Swarm 17 | tokens sync.Map 18 | nextToken *xsrfToken 19 | handlers map[string]Handler 20 | } 21 | 22 | func (s *Server) Error(w http.ResponseWriter, err error, tag Tag) { 23 | w.WriteHeader(http.StatusOK) 24 | log.Warnf("trpc error: %s", err.Error()) 25 | json.NewEncoder(w).Encode(Response{ 26 | Tag: tag, 27 | Result: err.Error(), 28 | }) 29 | } 30 | 31 | func (s *Server) getToken(addr string) *xsrfToken { 32 | a, _, _ := net.SplitHostPort(addr) 33 | if a != "" { 34 | addr = a 35 | } 36 | tok, loaded := s.tokens.LoadOrStore(addr, s.nextToken) 37 | if !loaded { 38 | s.nextToken = newToken() 39 | } 40 | return tok.(*xsrfToken) 41 | } 42 | 43 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 44 | 45 | xsrf := r.Header.Get(XSRFToken) 46 | tok := s.getToken(r.RemoteAddr) 47 | if !tok.Check(xsrf) { 48 | tok.Update() 49 | w.Header().Set(XSRFToken, tok.Token()) 50 | w.WriteHeader(http.StatusConflict) 51 | return 52 | } 53 | tok.Update() 54 | var req Request 55 | var resp Response 56 | err := json.NewDecoder(r.Body).Decode(&req) 57 | if err == nil { 58 | log.Debugf("trpc request: %q", req) 59 | h, ok := s.handlers[req.Method] 60 | if ok { 61 | resp = h(s.sw, req.Args) 62 | if resp.Result != Success { 63 | log.Warnf("trpc handler non success: %s", resp.Result) 64 | } 65 | } 66 | resp.Tag = req.Tag 67 | } 68 | if err == nil { 69 | buff := new(util.Buffer) 70 | w.Header().Set("Content-Type", ContentType) 71 | json.NewEncoder(buff).Encode(resp) 72 | log.Debugf("trpc response: %s", buff.String()) 73 | w.Header().Set("Content-Length", fmt.Sprintf("%d", buff.Len())) 74 | io.Copy(w, buff) 75 | } else { 76 | s.Error(w, err, req.Tag) 77 | } 78 | r.Body.Close() 79 | } 80 | 81 | func NewHandler(sw *swarm.Swarm) http.Handler { 82 | return &Server{ 83 | sw: sw, 84 | nextToken: newToken(), 85 | handlers: map[string]Handler{ 86 | "torrent-start": NotImplemented, 87 | "torrent-start-now": NotImplemented, 88 | "torrent-stop": NotImplemented, 89 | "torrent-verify": NotImplemented, 90 | "torrent-reannounce": NotImplemented, 91 | "torrent-get": TorrentGet, 92 | "torrent-set": NotImplemented, 93 | "torrent-add": NotImplemented, 94 | "torrent-remove": NotImplemented, 95 | "torrent-set-location": NotImplemented, 96 | "torrent-rename-path": NotImplemented, 97 | "session-set": NotImplemented, 98 | "session-stats": NotImplemented, 99 | "blocklist-update": NotImplemented, 100 | "port-test": NotImplemented, 101 | "session-close": NotImplemented, 102 | "queue-move-top": NotImplemented, 103 | "queue-move-up": NotImplemented, 104 | "queue-move-down": NotImplemented, 105 | "queue-move-bottom": NotImplemented, 106 | "free-space": NotImplemented, 107 | }, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/config/storage.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/majestrate/XD/lib/configparser" 6 | "github.com/majestrate/XD/lib/fs" 7 | "github.com/majestrate/XD/lib/storage" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | // EnvRootDir is the name of the environmental variable to set the root storage directory at runtime 13 | const EnvRootDir = "XD_HOME" 14 | 15 | type SFTPConfig struct { 16 | Enabled bool 17 | Username string 18 | Hostname string 19 | Keyfile string 20 | RemotePubkey string 21 | Port int 22 | } 23 | 24 | func (cfg *SFTPConfig) Load(s *configparser.Section) error { 25 | cfg.Username = s.Get("sftp_user", "") 26 | cfg.Hostname = s.Get("sftp_host", "") 27 | cfg.Keyfile = s.Get("sftp_keyfile", "") 28 | cfg.RemotePubkey = s.Get("sftp_remotekey", "") 29 | cfg.Port = s.GetInt("sftp_port", 22) 30 | return nil 31 | } 32 | 33 | func (cfg *SFTPConfig) Save(s *configparser.Section) error { 34 | return nil 35 | } 36 | 37 | func (cfg *SFTPConfig) LoadEnv() { 38 | 39 | } 40 | 41 | func (cfg *SFTPConfig) ToFS() fs.Driver { 42 | return fs.SFTP(cfg.Username, cfg.Hostname, cfg.Keyfile, cfg.RemotePubkey, cfg.Port) 43 | } 44 | 45 | type StorageConfig struct { 46 | // downloads directory 47 | Downloads string 48 | // completed directory 49 | Completed string 50 | // metadata directory 51 | Meta string 52 | // root directory 53 | Root string 54 | // number of io threads 55 | Workers int 56 | // number of buffered iops when using pooled io 57 | IOPBufferSize int 58 | // sftp config 59 | SFTP SFTPConfig 60 | } 61 | 62 | func (cfg *StorageConfig) Load(s *configparser.Section) error { 63 | 64 | if cfg.Root == "" { 65 | cfg.Root = "storage" 66 | if s != nil { 67 | cfg.Root = s.Get("rootdir", cfg.Root) 68 | } 69 | } 70 | 71 | if s != nil { 72 | cfg.Workers = s.GetInt("workers", 0) 73 | cfg.IOPBufferSize = s.GetInt("iop_buffer_size", 256) 74 | } 75 | 76 | cfg.setSubpaths(s) 77 | 78 | if s != nil { 79 | cfg.SFTP.Enabled = s.Get("sftp", "0") == "1" 80 | } 81 | if cfg.SFTP.Enabled { 82 | return cfg.SFTP.Load(s) 83 | } 84 | return nil 85 | 86 | } 87 | 88 | func (cfg *StorageConfig) setSubpaths(s *configparser.Section) { 89 | cfg.Meta = filepath.Join(cfg.Root, "metadata") 90 | 91 | cfg.Downloads = filepath.Join(cfg.Root, "downloads") 92 | cfg.Completed = filepath.Join(cfg.Root, "seeding") 93 | if s != nil { 94 | cfg.Downloads = s.Get("downloads", cfg.Downloads) 95 | cfg.Completed = s.Get("completed", cfg.Completed) 96 | } 97 | 98 | } 99 | 100 | func (cfg *StorageConfig) Save(s *configparser.Section) error { 101 | 102 | s.Add("rootdir", cfg.Root) 103 | s.Add("metadata", cfg.Meta) 104 | s.Add("downloads", cfg.Downloads) 105 | s.Add("completed", cfg.Completed) 106 | s.Add("workers", fmt.Sprintf("%d", cfg.Workers)) 107 | s.Add("iop_buffer_size", fmt.Sprintf("%d", cfg.IOPBufferSize)) 108 | return nil 109 | } 110 | 111 | func (cfg *StorageConfig) LoadEnv() { 112 | dir := os.Getenv(EnvRootDir) 113 | if dir != "" { 114 | cfg.Root = dir 115 | cfg.setSubpaths(nil) 116 | } 117 | } 118 | 119 | func (cfg *StorageConfig) CreateStorage() storage.Storage { 120 | 121 | st := &storage.FsStorage{ 122 | SeedingDir: cfg.Completed, 123 | DataDir: cfg.Downloads, 124 | MetaDir: cfg.Meta, 125 | FS: fs.STD, 126 | IOPBufferSize: cfg.IOPBufferSize, 127 | Workers: cfg.Workers, 128 | } 129 | if cfg.SFTP.Enabled { 130 | st.FS = cfg.SFTP.ToFS() 131 | } 132 | return st 133 | } 134 | -------------------------------------------------------------------------------- /lib/rpc/client.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/majestrate/XD/lib/bittorrent/swarm" 8 | t "github.com/majestrate/XD/lib/translate" 9 | "io" 10 | "net" 11 | "net/http" 12 | "strings" 13 | ) 14 | 15 | type Client struct { 16 | url string 17 | swarmno string 18 | } 19 | 20 | func NewClient(url string, swarmno int) *Client { 21 | return &Client{ 22 | url: url, 23 | swarmno: fmt.Sprintf("%d", swarmno), 24 | } 25 | } 26 | 27 | func (cl *Client) doRPC(r interface{}, h func(r io.Reader) error) (err error) { 28 | var buf bytes.Buffer 29 | err = json.NewEncoder(&buf).Encode(r) 30 | if err == nil { 31 | var resp *http.Response 32 | var httpcl *http.Client 33 | var reqURL string 34 | if strings.HasPrefix(cl.url, "unix:") { 35 | httpcl = &http.Client{ 36 | Transport: &http.Transport{ 37 | Dial: func(_, _ string) (net.Conn, error) { 38 | return net.Dial("unix", cl.url[5:]) 39 | }, 40 | }, 41 | } 42 | reqURL = "http://unix" + RPCPath 43 | } else { 44 | httpcl = http.DefaultClient 45 | reqURL = cl.url 46 | } 47 | resp, err = httpcl.Post(reqURL, RPCContentType, &buf) 48 | if err == nil { 49 | err = h(resp.Body) 50 | resp.Body.Close() 51 | } 52 | } 53 | return 54 | } 55 | 56 | func (cl *Client) torrentAction(ih, action string) (err error) { 57 | err = cl.doRPC(&ChangeTorrentRequest{BaseRequest{cl.swarmno}, ih, action}, func(r io.Reader) error { 58 | var response map[string]interface{} 59 | e := json.NewDecoder(r).Decode(&response) 60 | if e == nil { 61 | emsg, has := response["error"] 62 | if has { 63 | if emsg != nil { 64 | return fmt.Errorf("%s", t.T(fmt.Sprintf("%s", emsg))) 65 | } 66 | } 67 | } 68 | return e 69 | }) 70 | return 71 | } 72 | 73 | func (cl *Client) StopTorrent(ih string) error { 74 | return cl.torrentAction(ih, TorrentChangeStop) 75 | } 76 | 77 | func (cl *Client) StartTorrent(ih string) error { 78 | return cl.torrentAction(ih, TorrentChangeStart) 79 | } 80 | 81 | func (cl *Client) RemoveTorrent(ih string) error { 82 | return cl.torrentAction(ih, TorrentChangeRemove) 83 | } 84 | 85 | func (cl *Client) DeleteTorrent(ih string) error { 86 | return cl.torrentAction(ih, TorrentChangeDelete) 87 | } 88 | 89 | func (cl *Client) ListTorrents() (torrents swarm.TorrentsList, err error) { 90 | err = cl.doRPC(&ListTorrentsRequest{BaseRequest{cl.swarmno}}, func(r io.Reader) error { 91 | return json.NewDecoder(r).Decode(&torrents) 92 | }) 93 | return 94 | } 95 | 96 | func (cl *Client) GetSwarmStatus() (status swarm.SwarmStatus, err error) { 97 | err = cl.doRPC(&ListTorrentStatusRequest{BaseRequest{cl.swarmno}}, func(r io.Reader) error { 98 | return json.NewDecoder(r).Decode(&status) 99 | }) 100 | return 101 | } 102 | 103 | func (cl *Client) SetPieceWindow(n int) (err error) { 104 | err = cl.doRPC(&SetPieceWindowRequest{BaseRequest{cl.swarmno}, n}, func(r io.Reader) error { 105 | var response interface{} 106 | return json.NewDecoder(r).Decode(&response) 107 | }) 108 | return 109 | } 110 | 111 | func (cl *Client) AddTorrent(url string) (err error) { 112 | err = cl.doRPC(&AddTorrentRequest{BaseRequest{cl.swarmno}, url}, func(r io.Reader) error { 113 | var response interface{} 114 | return json.NewDecoder(r).Decode(&response) 115 | }) 116 | return 117 | } 118 | 119 | func (cl *Client) SwarmStatus(ih string) (st swarm.TorrentStatus, err error) { 120 | err = cl.doRPC(&TorrentStatusRequest{BaseRequest{cl.swarmno}, ih}, func(r io.Reader) error { 121 | return json.NewDecoder(r).Decode(&st) 122 | }) 123 | return 124 | } 125 | -------------------------------------------------------------------------------- /lib/config/bittorrent.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | "github.com/majestrate/XD/lib/configparser" 7 | "github.com/majestrate/XD/lib/gnutella" 8 | "github.com/majestrate/XD/lib/storage" 9 | "github.com/majestrate/XD/lib/util" 10 | "os" 11 | "strconv" 12 | ) 13 | 14 | const DefaultTorrentQueueSize = 0 15 | const DefaultOpentrackerFilename = "trackers.ini" 16 | 17 | type TrackerConfig struct { 18 | Trackers map[string]string 19 | FileName string 20 | } 21 | 22 | func (c *TrackerConfig) Save() (err error) { 23 | if c.Trackers == nil || len(c.Trackers) == 0 { 24 | c.Trackers = DefaultOpenTrackers 25 | } 26 | cfg := configparser.NewConfiguration() 27 | for sect := range c.Trackers { 28 | s := cfg.NewSection(sect) 29 | s.Add("url", c.Trackers[sect]) 30 | } 31 | err = configparser.Save(cfg, c.FileName) 32 | return 33 | } 34 | 35 | func (c *TrackerConfig) Load() (err error) { 36 | 37 | if len(c.FileName) == 0 { 38 | c.FileName = DefaultOpentrackerFilename 39 | } 40 | 41 | // create defaults 42 | if !util.CheckFile(c.FileName) { 43 | err = c.Save() 44 | } 45 | 46 | if err == nil { 47 | var cfg *configparser.Configuration 48 | cfg, err = configparser.Read(c.FileName) 49 | if err == nil { 50 | var sects []*configparser.Section 51 | sects, err = cfg.AllSections() 52 | if err == nil { 53 | if c.Trackers == nil { 54 | c.Trackers = make(map[string]string) 55 | } 56 | for idx := range sects { 57 | if sects[idx].Exists("url") { 58 | c.Trackers[sects[idx].Name()] = sects[idx].ValueOf("url") 59 | } 60 | } 61 | } 62 | } 63 | } 64 | return 65 | } 66 | 67 | type BittorrentConfig struct { 68 | DHT bool 69 | PEX bool 70 | OpenTrackers TrackerConfig 71 | PieceWindowSize int 72 | Swarms int 73 | TorrentQueueSize int 74 | } 75 | 76 | func (c *BittorrentConfig) Load(s *configparser.Section) error { 77 | c.OpenTrackers.FileName = DefaultOpentrackerFilename 78 | c.PieceWindowSize = swarm.DefaultMaxParallelRequests 79 | c.TorrentQueueSize = DefaultTorrentQueueSize 80 | c.PEX = true 81 | c.Swarms = 1 82 | if s != nil { 83 | c.DHT = s.Get("dht", "0") == "1" 84 | c.PEX = s.Get("pex", "1") == "1" 85 | c.OpenTrackers.FileName = s.Get("tracker-config", c.OpenTrackers.FileName) 86 | var e error 87 | c.PieceWindowSize, e = strconv.Atoi(s.Get("piece-window", fmt.Sprintf("%d", swarm.DefaultMaxParallelRequests))) 88 | if e != nil { 89 | c.PieceWindowSize = swarm.DefaultMaxParallelRequests 90 | } 91 | c.Swarms, e = strconv.Atoi(s.Get("swarms", "1")) 92 | if e != nil { 93 | return e 94 | } 95 | c.TorrentQueueSize, e = strconv.Atoi(s.Get("max-torrents", "0")) 96 | if e != nil { 97 | return e 98 | } 99 | } 100 | return c.OpenTrackers.Load() 101 | } 102 | 103 | func (c *BittorrentConfig) Save(s *configparser.Section) error { 104 | if c.PEX { 105 | s.Add("pex", "1") 106 | } else { 107 | s.Add("pex", "0") 108 | } 109 | 110 | if c.DHT { 111 | s.Add("dht", "1") 112 | } else { 113 | s.Add("dht", "0") 114 | } 115 | 116 | s.Add("swarms", fmt.Sprintf("%d", c.Swarms)) 117 | 118 | s.Add("tracker-config", c.OpenTrackers.FileName) 119 | 120 | s.Add("max-torrents", fmt.Sprintf("%d", c.TorrentQueueSize)) 121 | 122 | return c.OpenTrackers.Save() 123 | } 124 | 125 | const EnvOpenTracker = "XD_OPENTRACKER_URL" 126 | 127 | func (cfg *BittorrentConfig) LoadEnv() { 128 | url := os.Getenv(EnvOpenTracker) 129 | if url != "" { 130 | cfg.OpenTrackers.Trackers = map[string]string{ 131 | "default": url, 132 | } 133 | } 134 | } 135 | 136 | func (c *BittorrentConfig) CreateSwarm(st storage.Storage, gnutella *gnutella.Swarm) *swarm.Swarm { 137 | sw := swarm.NewSwarm(st, gnutella) 138 | for name := range c.OpenTrackers.Trackers { 139 | sw.AddOpenTracker(c.OpenTrackers.Trackers[name]) 140 | } 141 | sw.Torrents.MaxReq = c.PieceWindowSize 142 | sw.Torrents.QueueSize = c.TorrentQueueSize 143 | return sw 144 | } 145 | -------------------------------------------------------------------------------- /contrib/webui/docroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | XD WebUI 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

Remove torrent?

17 |

Delete local files

18 |

Don't show this dialog again

19 |
20 |
21 | 23 |
24 |
25 | 27 |
28 |
29 |
30 |
31 | 32 |
33 | 59 | 60 |
61 |
62 |
63 |
64 | 65 |
66 |
- %
67 |
68 |
peers connected.
69 |
70 | 71 |
72 | 73 |
74 |
75 | 76 |
77 |
78 |
79 |
80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /lib/bittorrent/swarm/status.go: -------------------------------------------------------------------------------- 1 | package swarm 2 | 3 | import ( 4 | "fmt" 5 | "github.com/majestrate/XD/lib/bittorrent" 6 | "github.com/majestrate/XD/lib/metainfo" 7 | "github.com/majestrate/XD/lib/util" 8 | ) 9 | 10 | type TorrentFileInfo struct { 11 | FileInfo metainfo.FileInfo 12 | Progress float64 13 | } 14 | 15 | func (i TorrentFileInfo) Length() int64 { 16 | return int64(i.FileInfo.Length) 17 | } 18 | 19 | func (i TorrentFileInfo) Name() string { 20 | return i.FileInfo.Path.FilePath("") 21 | } 22 | 23 | func (i TorrentFileInfo) BytesCompleted() int64 { 24 | return int64(float64(i.FileInfo.Length) * i.Progress) 25 | } 26 | 27 | type TorrentPeers []*PeerConnStats 28 | 29 | func (p TorrentPeers) RX() (rx float64) { 30 | for idx := range p { 31 | if p[idx] != nil { 32 | rx += p[idx].RX 33 | } 34 | } 35 | return 36 | } 37 | 38 | func (p TorrentPeers) TX() (tx float64) { 39 | for idx := range p { 40 | if p[idx] != nil { 41 | tx += p[idx].TX 42 | } 43 | } 44 | return 45 | } 46 | 47 | func (p TorrentPeers) Len() int { 48 | return len(p) 49 | } 50 | 51 | func (p TorrentPeers) Less(i, j int) bool { 52 | return p[i].Less(p[j]) 53 | } 54 | 55 | func (p *TorrentPeers) Swap(i, j int) { 56 | (*p)[i], (*p)[j] = (*p)[j], (*p)[i] 57 | } 58 | 59 | // connection statistics 60 | type PeerConnStats struct { 61 | TX float64 62 | RX float64 63 | ID string 64 | Client string 65 | Addr string 66 | UsInterested bool 67 | UsChoking bool 68 | ThemInterested bool 69 | ThemChoking bool 70 | Downloading bool 71 | Inbound bool 72 | Uploading bool 73 | Bitfield bittorrent.Bitfield 74 | } 75 | 76 | func (p *PeerConnStats) Less(o *PeerConnStats) bool { 77 | return util.StringCompare(p.ID, o.ID) < 0 78 | } 79 | 80 | type TorrentState string 81 | 82 | const Seeding = TorrentState("seeding") 83 | const Checking = TorrentState("checking") 84 | const Stopped = TorrentState("stopped") 85 | const Downloading = TorrentState("downloading") 86 | 87 | func (t TorrentState) String() string { 88 | return string(t) 89 | } 90 | 91 | // immutable status of torrent 92 | type TorrentStatus struct { 93 | Files []TorrentFileInfo 94 | Peers TorrentPeers 95 | Us PeerConnStats 96 | Name string 97 | State TorrentState 98 | Infohash string 99 | Progress float64 100 | TX uint64 101 | RX uint64 102 | } 103 | 104 | func (t TorrentStatus) Ratio() (r float64) { 105 | r = util.Ratio(float64(t.TX), float64(t.RX)) 106 | return 107 | } 108 | 109 | type TorrentStatusList []TorrentStatus 110 | 111 | func (l TorrentStatusList) TX() (tx float64) { 112 | for idx := range l { 113 | tx += l[idx].Peers.TX() 114 | } 115 | return 116 | } 117 | 118 | func (l TorrentStatusList) Ratio() (r float64) { 119 | var tx, rx uint64 120 | for idx := range l { 121 | tx += l[idx].TX 122 | rx += l[idx].RX 123 | } 124 | r = util.Ratio(float64(tx), float64(rx)) 125 | return 126 | } 127 | 128 | func (l TorrentStatusList) RX() (rx float64) { 129 | for idx := range l { 130 | rx += l[idx].Peers.RX() 131 | } 132 | return 133 | } 134 | 135 | func (l TorrentStatusList) Len() int { 136 | return len(l) 137 | } 138 | func (l TorrentStatusList) Less(i, j int) bool { 139 | return util.StringCompare(l[i].Name, l[j].Name) < 0 140 | } 141 | 142 | func (l *TorrentStatusList) Swap(i, j int) { 143 | (*l)[i], (*l)[j] = (*l)[j], (*l)[i] 144 | } 145 | 146 | // SwarmBandwidth is a string tuple for bandwith 147 | type SwarmBandwidth struct { 148 | Upload string 149 | Download string 150 | } 151 | 152 | func (sb SwarmBandwidth) String() string { 153 | return fmt.Sprintf("Upload: %s Download: %s", sb.Upload, sb.Download) 154 | } 155 | 156 | // infohash -> torrent status map 157 | type SwarmStatus map[string]TorrentStatus 158 | 159 | func (sw SwarmStatus) TotalSpeed() (tx, rx float64) { 160 | for ih := range sw { 161 | tx += sw[ih].Peers.TX() 162 | rx += sw[ih].Peers.RX() 163 | } 164 | return 165 | } 166 | 167 | func (sw SwarmStatus) Ratio() (r float64) { 168 | var tx, rx uint64 169 | for ih := range sw { 170 | tx += sw[ih].TX 171 | rx += sw[ih].RX 172 | } 173 | r = util.Ratio(float64(tx), float64(rx)) 174 | return 175 | } 176 | -------------------------------------------------------------------------------- /lib/rpc/server.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/majestrate/XD/lib/bittorrent/swarm" 8 | "github.com/majestrate/XD/lib/rpc/assets" 9 | "github.com/majestrate/XD/lib/rpc/transmission" 10 | "net" 11 | "net/http" 12 | "strconv" 13 | ) 14 | 15 | const ParamMethod = "method" 16 | const ParamSwarm = "swarm" 17 | 18 | var ErrNoTorrent = errors.New("no such torrent") 19 | 20 | const RPCContentType = "text/json; encoding=UTF-8" 21 | 22 | // Bittorrent Swarm RPC Handler 23 | type Server struct { 24 | sw []*swarm.Swarm 25 | fileserver http.Handler 26 | expectedHost string 27 | trpc http.Handler 28 | } 29 | 30 | func NewServer(sw []*swarm.Swarm, host string) *Server { 31 | fs := assets.GetAssets() 32 | trpc := transmission.NewHandler(sw[0]) 33 | if fs == nil { 34 | return &Server{ 35 | sw: sw, 36 | expectedHost: host, 37 | trpc: trpc, 38 | } 39 | } else { 40 | return &Server{ 41 | sw: sw, 42 | expectedHost: host, 43 | fileserver: http.FileServer(fs), 44 | trpc: trpc, 45 | } 46 | } 47 | } 48 | 49 | func (r *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { 50 | 51 | if r.expectedHost != "" { 52 | host := req.Host 53 | 54 | h, _, err := net.SplitHostPort(host) 55 | if err == nil { 56 | host = h 57 | } 58 | 59 | if !(host == r.expectedHost || host == "localhost") { 60 | w.WriteHeader(http.StatusForbidden) 61 | fmt.Fprintf(w, "expected host %s but got %s", r.expectedHost, host) 62 | return 63 | } 64 | } 65 | 66 | if req.Method == "GET" && r.fileserver != nil { 67 | r.fileserver.ServeHTTP(w, req) 68 | } else if req.Method == "POST" { 69 | if req.URL.Path == RPCPath { 70 | defer req.Body.Close() 71 | w.Header().Set("Content-Type", RPCContentType) 72 | var body map[string]interface{} 73 | err := json.NewDecoder(req.Body).Decode(&body) 74 | rw := &ResponseWriter{ 75 | w: w, 76 | } 77 | if err == nil { 78 | var rr Request 79 | method := body[ParamMethod] 80 | swarmno, ok := body[ParamSwarm] 81 | swarmidx := 0 82 | if ok { 83 | swarmidx, err = strconv.Atoi(fmt.Sprintf("%s", swarmno)) 84 | } 85 | if err == nil { 86 | switch method { 87 | case RPCSwarmCount: 88 | rr = &SwarmCountRequest{ 89 | N: len(r.sw), 90 | } 91 | case RPCChangeTorrent: 92 | rr = &ChangeTorrentRequest{ 93 | Infohash: fmt.Sprintf("%s", body[ParamInfohash]), 94 | Action: fmt.Sprintf("%s", body[ParamAction]), 95 | } 96 | case RPCListTorrents: 97 | rr = &ListTorrentsRequest{} 98 | case RPCTorrentStatus: 99 | rr = &TorrentStatusRequest{ 100 | Infohash: fmt.Sprintf("%s", body[ParamInfohash]), 101 | } 102 | case RPCAddTorrent: 103 | rr = &AddTorrentRequest{ 104 | URL: fmt.Sprintf("%s", body[ParamURL]), 105 | } 106 | case RPCSetPieceWindow: 107 | n, ok := body[ParamN].(float64) 108 | if ok { 109 | rr = &SetPieceWindowRequest{ 110 | N: int(n), 111 | } 112 | } else { 113 | rr = &rpcError{ 114 | message: fmt.Sprintf("invalid value: %s", body[ParamN]), 115 | } 116 | } 117 | case RPCListTorrentStatus: 118 | rr = &ListTorrentStatusRequest{} 119 | default: 120 | rr = &rpcError{ 121 | message: fmt.Sprintf("no such method %s", method), 122 | } 123 | } 124 | } else { 125 | rr = &rpcError{ 126 | message: err.Error(), 127 | } 128 | } 129 | if swarmidx < len(r.sw) { 130 | if r.sw[swarmidx].IsOnline() { 131 | rr.ProcessRequest(r.sw[swarmidx], rw) 132 | } else { 133 | rr = &rpcError{ 134 | message: "swarm offline", 135 | } 136 | rr.ProcessRequest(nil, rw) 137 | } 138 | } else { 139 | rr = &rpcError{ 140 | message: "no such swarm", 141 | } 142 | rr.ProcessRequest(nil, rw) 143 | } 144 | } else { 145 | // TODO: whatever fix this later 146 | w.WriteHeader(http.StatusInternalServerError) 147 | } 148 | } else if req.URL.Path == transmission.RPCPath && r.trpc != nil { 149 | r.trpc.ServeHTTP(w, req) 150 | } else { 151 | w.WriteHeader(http.StatusNotFound) 152 | } 153 | } else { 154 | w.WriteHeader(http.StatusMethodNotAllowed) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /contrib/webui/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #2E3436; 3 | color: #D3D7CF; 4 | } 5 | 6 | .base.container { 7 | max-width: none; 8 | padding: 24px; 9 | } 10 | 11 | nav { 12 | padding-bottom: 20px; 13 | } 14 | 15 | nav > div.row { 16 | margin: 20px 0px; 17 | } 18 | 19 | .speed { 20 | color: #D3D7CF; 21 | } 22 | 23 | .speed:hover { 24 | color: #ccc; 25 | background-color: #222; 26 | cursor: pointer; 27 | } 28 | 29 | progress { 30 | width: 100%; 31 | } 32 | 33 | .logo img { 34 | width: 38px; 35 | } 36 | 37 | .button-remove { 38 | color: #D3D7CF; 39 | font-weight: bold; 40 | padding: 0px 10px; 41 | border-radius: 4px; 42 | border:1px #D3D7CF solid; 43 | } 44 | 45 | .button-remove:hover { 46 | cursor: pointer; 47 | background: #D3D7CF; 48 | color: #ccc; 49 | } 50 | 51 | .torrent-row { 52 | padding: 10px; 53 | font-size: 0.8em; 54 | border: 1px solid #222; 55 | border-top: none; 56 | } 57 | 58 | .torrent-row:first-child { 59 | border-top: 1px solid #222; 60 | } 61 | 62 | .torrent-button { 63 | text-align: right; 64 | padding-bottom: 1em; 65 | } 66 | 67 | .torrent-row:nth-child(odd) { 68 | background: #272C2D; 69 | } 70 | 71 | .torrent-row:hover { 72 | background: #222; 73 | } 74 | 75 | .torrent-row-downloading { 76 | color: #D3D7CF; 77 | } 78 | 79 | .torrent-row-seeding { 80 | color: #00BB00; 81 | } 82 | 83 | progress { 84 | -webkit-appearance: none; 85 | appearance: none; 86 | } 87 | 88 | .filter-buttons li { 89 | display: inline-block; 90 | font-size: 0.7em; 91 | height: 38px; 92 | padding: 0 10px; 93 | color: #D3D7CF; 94 | text-align: center; 95 | line-height: 38px; 96 | letter-spacing: .1rem; 97 | text-transform: uppercase; 98 | text-decoration: none; 99 | white-space: nowrap; 100 | background-color: #272C2D; 101 | border-radius: 4px; 102 | border: 1px solid #D3D7CF; 103 | cursor: pointer; 104 | margin: 0px 2px; 105 | box-sizing: border-box; 106 | } 107 | 108 | .filter-buttons li.active { 109 | background: #D3D7CF; 110 | color: #272C2D; 111 | } 112 | 113 | .filter-buttons li:hover { 114 | background: #D3D7CF; 115 | color: #272C2D; 116 | } 117 | 118 | .button.button-primary, 119 | button.button-primary, 120 | input[type="submit"].button-primary, 121 | input[type="reset"].button-primary, 122 | input[type="button"].button-primary { 123 | color: #272C2D !important; 124 | background-color: #D3D7CF !important; 125 | border-color: #D3D7CF !important; } 126 | .button.button-primary:hover, 127 | button.button-primary:hover, 128 | input[type="submit"].button-primary:hover, 129 | input[type="reset"].button-primary:hover, 130 | input[type="button"].button-primary:hover, 131 | .button.button-primary:focus, 132 | button.button-primary:focus, 133 | input[type="submit"].button-primary:focus, 134 | input[type="reset"].button-primary:focus, 135 | input[type="button"].button-primary:focus { 136 | color: #FFF !important; 137 | background-color: #00BB00 !important; 138 | border-color: #00BB00 !important; } 139 | 140 | .button.button-cancel:hover, 141 | button.button-cancel:hover, 142 | input[type="submit"].button-cancel:hover, 143 | input[type="reset"].button-cancel:hover, 144 | input[type="button"].button-cancel:hover, 145 | .button.button-cancel:focus, 146 | button.button-cancel:focus, 147 | input[type="submit"].button-cancel:focus, 148 | input[type="reset"].button-cancel:focus, 149 | input[type="button"].button-cancel:focus { 150 | color: #FFF !important; 151 | background-color: #CB2431 !important; 152 | border-color: #CB2431 !important; } 153 | 154 | input[type="email"], 155 | input[type="number"], 156 | input[type="search"], 157 | input[type="text"], 158 | input[type="tel"], 159 | input[type="url"], 160 | input[type="password"], 161 | textarea, 162 | select { 163 | background-color: #272C2D !important; 164 | border: 1px solid #D3D7CF !important; 165 | color: #D3D7CF !important; } 166 | input[type="email"]:focus, 167 | input[type="number"]:focus, 168 | input[type="search"]:focus, 169 | input[type="text"]:focus, 170 | input[type="tel"]:focus, 171 | input[type="url"]:focus, 172 | input[type="password"]:focus, 173 | textarea:focus, 174 | select:focus { 175 | border: 1px solid #00BB00 !important; 176 | color: #00BB00 !important; } 177 | 178 | /* The Modal (background) */ 179 | .confirmation-box { 180 | position: fixed; 181 | z-index: 1; 182 | left: 0; 183 | top: 0; 184 | width: 100%; 185 | height: 100%; 186 | overflow: auto; 187 | background-color: rgb(0,0,0); 188 | background-color: rgba(0,0,0,0.4); 189 | } 190 | 191 | /* Modal Content/Box */ 192 | .confirmation-content { 193 | background-color: #272C2D; 194 | color: #D3D7CF; 195 | margin: 15% auto; 196 | padding: 20px; 197 | border: 1px solid #888; 198 | width: 80%; 199 | } 200 | -------------------------------------------------------------------------------- /contrib/py/anodex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # anodex mirroring script for transmission to XD 4 | # 5 | # takes all horriblesubs torrents from transmission and 6 | # uploads them to anodex.i2p then adds them to XD for seeding 7 | # 8 | # uses ~/.netrc for authentication, see man 5 netrc 9 | # example entry: 10 | # machine anodex.i2p username youruser password yourpassword 11 | # 12 | # usage: 13 | # python3 -m venv v && v/bin/pip install -r anodex_requirements.txt 14 | # v/bin/python anodex.py 15 | 16 | import transmissionrpc as rpc 17 | import requests 18 | import shutil 19 | import sys 20 | import os 21 | 22 | 23 | # http proxy url 24 | proxy_url = "http://127.0.0.1:4444/" 25 | 26 | # xd downloads directory 27 | xd_dir = "/home/xd/storage/downloads/" 28 | 29 | # xd api endpoint 30 | xd_api = "http://127.0.0.1:1488/ecksdee/api" 31 | 32 | # anodex info 33 | anodexBaseURL = "http://anodex.i2p" 34 | anodexAnimeURL = "{}/c/3/?t=json".format(anodexBaseURL) 35 | 36 | # proxies for i2p 37 | proxies = { 38 | "http": proxy_url, 39 | "https": proxy_url 40 | } 41 | 42 | # transmission parameters 43 | rpc_host = '127.0.0.1' 44 | rpc_port = 9091 45 | rpc_user = None 46 | rpc_password = None 47 | 48 | 49 | def anodex_upload_torrent(torrent, tags, description='auto upload'): 50 | """ 51 | upload a torrent to andoex anime category 52 | returns torrent url 53 | """ 54 | j = None 55 | print("upload {}".format(torrent.name)) 56 | with open(torrent.torrentFile, 'rb') as f: 57 | r = requests.post(anodexAnimeURL, files={ 58 | 'torrent-file' : (os.path.basename(torrent.torrentFile), f, 'application/bittorrent'), 59 | }, data={ 60 | "torrent-name": torrent.name, 61 | "torrent-description": description, 62 | "tags": ','.join(tags) 63 | }, proxies=proxies) 64 | j = r.json() 65 | if j and 'URL' in j: 66 | return j['URL'] 67 | 68 | 69 | def copy_torrent_files(torrent, dest_dir): 70 | """ 71 | copy torrent data files from torrent into dest_dir 72 | """ 73 | files = torrent.files() 74 | for id in files: 75 | inf = os.path.join(torrent.downloadDir, files[id]['name']) 76 | outf = os.path.join(dest_dir, files[id]['name']) 77 | d = os.path.dirname(outf) 78 | if not os.path.exists(d): 79 | os.mkdir(d) 80 | if os.path.exists(inf): 81 | print("{} -> {}".format(inf, outf)) 82 | shutil.copyfile(inf, outf) 83 | 84 | def anodex_has_torrent(torrent): 85 | """ 86 | return true if anodex has a copy of this torrent 87 | """ 88 | r = requests.get("{}/dl/{}.torrent".format(anodexBaseURL, torrent.hashString), proxies=proxies) 89 | return r.status_code is 200 90 | 91 | 92 | def xd_has_torrent(torrent): 93 | """ 94 | return true if xd has this torrent already 95 | """ 96 | r = requests.post(xd_api, json={'method': 'XD.TorrentStatus', 'infohash': torrent.hashString}) 97 | j = r.json() 98 | return 'error' not in j or j['error'] is None 99 | 100 | def should_process(torrent): 101 | """ 102 | return true if we should process this torrent 103 | """ 104 | # only download torrents that have all the data 105 | if not torrent.isFinished: 106 | return False 107 | if xd_has_torrent(torrent): 108 | return False 109 | return torrent.name.lower().startswith("[horriblesubs]") 110 | 111 | def xd_add_torrent(url): 112 | """ 113 | add torrent to xd by url 114 | """ 115 | print('adding {}'.format(url)) 116 | r = requests.post(xd_api, json={'method': 'XD.AddTorrent', 'url': url}) 117 | j = r.json() 118 | 119 | 120 | def generate_tags(t): 121 | """ 122 | given a torrent generate anodex tags 123 | """ 124 | tags = list() 125 | name = t.name.lower() 126 | if name.startswith('[horriblesubs]'): 127 | tags.append('horriblesubs') 128 | if '[1080p]' in name: 129 | tags.append('1080p') 130 | elif '[720p]' in name: 131 | tags.append('720p') 132 | return tags 133 | 134 | def main(): 135 | cl = rpc.Client(rpc_host, port=rpc_port, user=rpc_user, password=rpc_password) 136 | torrents = cl.get_torrents() 137 | for t in torrents: 138 | if should_process(t): 139 | copy_torrent_files(t, xd_dir) 140 | if not anodex_has_torrent(t): 141 | tries = 10 142 | while tries > 0: 143 | u = anodex_upload_torrent(t, generate_tags(t)) 144 | if u: 145 | print('uploaded to {}'.format(u)) 146 | break 147 | else: 148 | print('upload failed, try again, {} tries left'.format(tries)) 149 | tries -= 1 150 | url = '{}/dl/{}.torrent'.format(anodexBaseURL, t.hashString) 151 | xd_add_torrent(url) 152 | 153 | 154 | if __name__ == "__main__": 155 | main() 156 | -------------------------------------------------------------------------------- /lib/metainfo/metainfo.go: -------------------------------------------------------------------------------- 1 | package metainfo 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "github.com/majestrate/XD/lib/common" 8 | "github.com/majestrate/XD/lib/log" 9 | "github.com/zeebo/bencode" 10 | "io" 11 | "path/filepath" 12 | ) 13 | 14 | type FilePath []string 15 | 16 | // get filepath 17 | func (f FilePath) FilePath(base string) string { 18 | if len(base) > 0 { 19 | path := []string{base} 20 | path = append(path, f...) 21 | return filepath.Join(path...) 22 | } 23 | return filepath.Join(f...) 24 | } 25 | 26 | type FileInfo struct { 27 | // length of file 28 | Length uint64 `bencode:"length"` 29 | // relative path of file 30 | Path FilePath `bencode:"path"` 31 | // md5sum 32 | Sum []byte `bencode:"md5sum,omitempty"` 33 | } 34 | 35 | // info section of torrent file 36 | type Info struct { 37 | // length of pices in bytes 38 | PieceLength uint32 `bencode:"piece length"` 39 | // piece data 40 | Pieces []byte `bencode:"pieces"` 41 | // name of root file 42 | Path string `bencode:"name"` 43 | // file metadata 44 | Files []FileInfo `bencode:"files,omitempty"` 45 | // private torrent 46 | Private *uint64 `bencode:"private,omitempty"` 47 | // length of file in signle file mode 48 | Length uint64 `bencode:"length,omitempty"` 49 | // md5sum 50 | Sum []byte `bencode:"md5sum,omitempty"` 51 | } 52 | 53 | // get fileinfos from this info section 54 | func (i Info) GetFiles() (infos []FileInfo) { 55 | if i.Length > 0 { 56 | infos = append(infos, FileInfo{ 57 | Length: i.Length, 58 | Path: FilePath([]string{i.Path}), 59 | Sum: i.Sum, 60 | }) 61 | } else { 62 | infos = append(infos, i.Files...) 63 | } 64 | return 65 | } 66 | 67 | // check if a piece is valid against the pieces in this info section 68 | func (i Info) CheckPiece(p *common.PieceData) bool { 69 | idx := p.Index * 20 70 | if i.NumPieces() > p.Index { 71 | h := sha1.Sum(p.Data) 72 | expected := i.Pieces[idx : idx+20] 73 | if bytes.Equal(h[:], expected) { 74 | return true 75 | } 76 | log.Warnf("piece missmatch: %s != %s", hex.EncodeToString(h[:]), hex.EncodeToString(expected)) 77 | return false 78 | } 79 | log.Error("piece index out of bounds") 80 | return false 81 | } 82 | 83 | func (i Info) NumPieces() uint32 { 84 | return uint32(len(i.Pieces) / 20) 85 | } 86 | 87 | // a torrent file 88 | type TorrentFile struct { 89 | Info Info `bencode:"-"` 90 | RawInfo bencode.RawMessage `bencode:"info"` 91 | Announce string `bencode:"announce"` 92 | AnnounceList [][]string `bencode:"announce-list"` 93 | Created int64 `bencode:"created"` 94 | Comment []byte `bencode:"comment"` 95 | CreatedBy []byte `bencode:"created by"` 96 | Encoding []byte `bencode:"encoding"` 97 | } 98 | 99 | func (tf *TorrentFile) LengthOfPiece(idx uint32) (l uint32) { 100 | i := tf.Info 101 | np := i.NumPieces() 102 | if np == idx+1 { 103 | sz := tf.TotalSize() 104 | l64 := uint64(i.PieceLength) - ((uint64(np) * uint64(i.PieceLength)) - sz) 105 | l = uint32(l64) 106 | } else { 107 | l = i.PieceLength 108 | } 109 | return 110 | } 111 | 112 | // get total size of files from torrent info section 113 | func (tf *TorrentFile) TotalSize() uint64 { 114 | if tf.IsSingleFile() { 115 | return tf.Info.Length 116 | } 117 | total := uint64(0) 118 | for _, f := range tf.Info.Files { 119 | total += f.Length 120 | } 121 | return total 122 | } 123 | 124 | func (tf *TorrentFile) GetAllAnnounceURLS() (l []string) { 125 | if len(tf.Announce) > 0 { 126 | l = append(l, tf.Announce) 127 | } 128 | for _, al := range tf.AnnounceList { 129 | for _, a := range al { 130 | if len(a) > 0 { 131 | l = append(l, a) 132 | } 133 | } 134 | } 135 | return 136 | } 137 | 138 | func (tf *TorrentFile) TorrentName() string { 139 | return tf.Info.Path 140 | } 141 | 142 | // calculate infohash 143 | func (tf *TorrentFile) Infohash() (ih common.Infohash) { 144 | d := sha1.Sum(tf.RawInfo) 145 | copy(ih[:], d[:]) 146 | return 147 | } 148 | 149 | // return true if this torrent is for a single file 150 | func (tf *TorrentFile) IsSingleFile() bool { 151 | return tf.Info.Length > 0 152 | } 153 | 154 | // bencode this file via an io.Writer 155 | func (tf *TorrentFile) BEncode(w io.Writer) (err error) { 156 | enc := bencode.NewEncoder(w) 157 | err = enc.Encode(tf) 158 | return 159 | } 160 | 161 | // load from an io.Reader 162 | func (tf *TorrentFile) BDecode(r io.Reader) (err error) { 163 | dec := bencode.NewDecoder(r) 164 | err = dec.Decode(tf) 165 | if err != nil { 166 | return 167 | } 168 | err = bencode.DecodeBytes(tf.RawInfo, &tf.Info) 169 | return 170 | } 171 | 172 | // IsPrivate returns true if this torrent is a private torrent 173 | func (tf *TorrentFile) IsPrivate() bool { 174 | return tf.Info.Private != nil && *tf.Info.Private > 0 175 | } 176 | 177 | func TorrentFileFromInfoBytes(bytes []byte) (tf *TorrentFile, err error) { 178 | tf = &TorrentFile{ 179 | RawInfo: bytes, 180 | } 181 | err = bencode.DecodeBytes(tf.RawInfo, &tf.Info) 182 | if err != nil { 183 | tf = nil 184 | } 185 | return 186 | } 187 | 188 | func TorrentFileFromInfo(info Info) (tf *TorrentFile, err error) { 189 | tf = &TorrentFile{ 190 | Info: info, 191 | } 192 | tf.RawInfo, err = bencode.EncodeBytes(tf.Info) 193 | if err != nil { 194 | tf = nil 195 | } 196 | return 197 | } 198 | -------------------------------------------------------------------------------- /lib/tracker/http.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/majestrate/XD/lib/common" 7 | "github.com/majestrate/XD/lib/log" 8 | "github.com/majestrate/XD/lib/sync" 9 | "github.com/zeebo/bencode" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // http tracker 18 | type HttpTracker struct { 19 | u *url.URL 20 | // last time we resolved the remote address 21 | lastResolved time.Time 22 | // cached network address of tracker 23 | addr net.Addr 24 | // how often to resolve network address 25 | resolveInterval time.Duration 26 | // currently resolving the address ? 27 | resolving sync.Mutex 28 | } 29 | 30 | // create new http tracker from url 31 | func NewHttpTracker(u *url.URL) *HttpTracker { 32 | t := &HttpTracker{ 33 | u: u, 34 | resolveInterval: time.Hour, 35 | lastResolved: time.Unix(0, 0), 36 | } 37 | 38 | return t 39 | } 40 | 41 | func (t *HttpTracker) shouldResolve() bool { 42 | return t.lastResolved.Add(t.resolveInterval).Before(time.Now()) 43 | } 44 | 45 | // http compact response 46 | type compactHttpAnnounceResponse struct { 47 | Peers interface{} `bencode:"peers"` 48 | Interval int `bencode:"interval"` 49 | Error string `bencode:"failure reason"` 50 | } 51 | 52 | func (t *HttpTracker) Name() string { 53 | return t.u.String() 54 | } 55 | 56 | // send announce via http request 57 | func (t *HttpTracker) Announce(req *Request) (resp *Response, err error) { 58 | //if req == nil { 59 | // return 60 | //} 61 | // http client 62 | var client http.Client 63 | 64 | client.Transport = &http.Transport{ 65 | Dial: func(_, _ string) (c net.Conn, e error) { 66 | var a net.Addr 67 | t.resolving.Lock() 68 | if t.shouldResolve() { 69 | var h, p string 70 | // XXX: hack 71 | if strings.Index(t.u.Host, ":") == -1 { 72 | t.u.Host += ":80" 73 | } 74 | h, p, e = net.SplitHostPort(t.u.Host) 75 | if e == nil { 76 | a, e = req.GetNetwork().Lookup(h, p) 77 | if e == nil { 78 | t.addr = a 79 | t.lastResolved = time.Now() 80 | } 81 | } 82 | } else { 83 | a = t.addr 84 | } 85 | t.resolving.Unlock() 86 | if e == nil { 87 | c, e = req.GetNetwork().Dial(a.Network(), a.String()) 88 | } 89 | return 90 | }, 91 | } 92 | 93 | resp = new(Response) 94 | interval := 30 95 | // build query 96 | var u *url.URL 97 | u, err = url.Parse(t.u.String()) 98 | if err == nil { 99 | v := u.Query() 100 | n := req.GetNetwork() 101 | a := n.Addr() 102 | host, _, _ := net.SplitHostPort(a.String()) 103 | if a.Network() == "i2p" { 104 | host += ".i2p" 105 | req.Compact = true 106 | } 107 | v.Add("ip", host) 108 | v.Add("info_hash", string(req.Infohash.Bytes())) 109 | v.Add("peer_id", string(req.PeerID.Bytes())) 110 | v.Add("port", fmt.Sprintf("%d", req.Port)) 111 | v.Add("numwant", fmt.Sprintf("%d", req.NumWant)) 112 | v.Add("left", fmt.Sprintf("%d", req.Left)) 113 | if req.Event != Nop { 114 | v.Add("event", req.Event.String()) 115 | } 116 | v.Add("downloaded", fmt.Sprintf("%d", req.Downloaded)) 117 | v.Add("uploaded", fmt.Sprintf("%d", req.Uploaded)) 118 | 119 | // compact response 120 | if req.Compact || u.Path != "/a" { 121 | req.Compact = true 122 | v.Add("compact", "1") 123 | } 124 | u.RawQuery = v.Encode() 125 | var r *http.Response 126 | log.Debugf("%s announcing", t.Name()) 127 | r, err = client.Get(u.String()) 128 | if err == nil { 129 | defer r.Body.Close() 130 | dec := bencode.NewDecoder(r.Body) 131 | if req.Compact { 132 | cresp := new(compactHttpAnnounceResponse) 133 | err = dec.Decode(cresp) 134 | if err == nil { 135 | interval = cresp.Interval 136 | var cpeers string 137 | 138 | _, ok := cresp.Peers.(string) 139 | if ok { 140 | cpeers = cresp.Peers.(string) 141 | l := len(cpeers) / 32 142 | for l > 0 { 143 | var p common.Peer 144 | // TODO: bounds check 145 | copy(p.Compact[:], cpeers[(l-1)*32:l*32]) 146 | resp.Peers = append(resp.Peers, p) 147 | l-- 148 | } 149 | } else { 150 | fullpeers, ok := cresp.Peers.([]interface{}) 151 | if ok { 152 | for idx := range fullpeers { 153 | // XXX: this is horribad :DDDDDDDDD 154 | var peer map[string]interface{} 155 | peer, ok = fullpeers[idx].(map[string]interface{}) 156 | if ok { 157 | var p common.Peer 158 | p.IP = fmt.Sprintf("%s", peer["ip"]) 159 | port, ok := peer["port"].(int64) 160 | if ok { 161 | p.Port = int(port) 162 | } 163 | resp.Peers = append(resp.Peers, p) 164 | } 165 | } 166 | } 167 | } 168 | 169 | if len(cresp.Error) > 0 { 170 | err = errors.New(cresp.Error) 171 | } 172 | } 173 | } else { 174 | // decode non compact response 175 | err = dec.Decode(resp) 176 | interval = resp.Interval 177 | if len(resp.Error) > 0 { 178 | err = errors.New(resp.Error) 179 | } 180 | } 181 | } 182 | } 183 | 184 | if err == nil { 185 | log.Infof("%s got %d peers for %s", t.Name(), len(resp.Peers), req.Infohash.Hex()) 186 | } else { 187 | log.Warnf("%s got error while announcing: %s", t.Name(), err) 188 | } 189 | if interval == 0 { 190 | interval = 60 191 | } 192 | resp.NextAnnounce = time.Now().Add(time.Second * time.Duration(interval)) 193 | return 194 | } 195 | -------------------------------------------------------------------------------- /lib/bittorrent/extensions/extensions.go: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | import ( 4 | "errors" 5 | "github.com/majestrate/XD/lib/common" 6 | "github.com/majestrate/XD/lib/util" 7 | "github.com/majestrate/XD/lib/version" 8 | "github.com/zeebo/bencode" 9 | ) 10 | 11 | // Extension is a bittorrent extenension string 12 | type Extension string 13 | 14 | // String gets extension as string 15 | func (ex Extension) String() string { 16 | return string(ex) 17 | } 18 | 19 | // Message is a serializable BitTorrent extended options message 20 | type Message struct { 21 | ID uint8 `bencode:"-"` 22 | Version string `bencode:"v"` // handshake data 23 | Extensions map[string]uint32 `bencode:"m"` // handshake data 24 | Payload interface{} `bencode:"-"` 25 | PayloadRaw []byte `bencode:"-"` 26 | MetainfoSize *uint32 `bencode:"metadata_size,omitempty"` 27 | } 28 | 29 | // I2PPEX returns true if i2p PEX is supported 30 | func (opts Message) I2PPEX() bool { 31 | return opts.IsSupported(I2PPeerExchange.String()) 32 | } 33 | 34 | // LNPEX returns true if we support lokinet pex 35 | func (opts Message) LNPEX() bool { 36 | return opts.IsSupported(LokinetPeerExchange.String()) 37 | } 38 | 39 | // XDHT returns true if XHDT is supported 40 | func (opts Message) XDHT() bool { 41 | return opts.IsSupported(XDHT.String()) 42 | } 43 | 44 | // MetaData returns true if ut_metadata is supported 45 | func (opts Message) MetaData() bool { 46 | return opts.IsSupported(UTMetaData.String()) 47 | } 48 | 49 | // SetSupported sets a bittorrent extension as supported 50 | func (opts *Message) SetSupported(ext Extension) { 51 | // get next id 52 | nextId := uint32(1) 53 | for k, v := range opts.Extensions { 54 | if v >= nextId { 55 | nextId = v + 1 56 | } 57 | // already supported 58 | if k == ext.String() { 59 | return 60 | } 61 | } 62 | // set supported 63 | opts.Extensions[ext.String()] = nextId 64 | } 65 | 66 | // IsSupported returns true if an extension by its name is supported 67 | func (opts Message) IsSupported(ext string) (has bool) { 68 | if opts.Extensions != nil { 69 | _, has = opts.Extensions[ext] 70 | } 71 | return 72 | } 73 | 74 | // Lookup finds the extension name of the extension by id 75 | func (opts Message) Lookup(id uint8) (string, bool) { 76 | for k, v := range opts.Extensions { 77 | if v == uint32(id) { 78 | return k, true 79 | } 80 | } 81 | return "", false 82 | } 83 | 84 | // Copy makes a copy of this Message 85 | func (opts Message) Copy() Message { 86 | ext := make(map[string]uint32) 87 | for k, v := range opts.Extensions { 88 | ext[k] = v 89 | } 90 | m := Message{ 91 | ID: opts.ID, 92 | Version: opts.Version, 93 | Extensions: ext, 94 | Payload: opts.Payload, 95 | MetainfoSize: opts.MetainfoSize, 96 | } 97 | if opts.PayloadRaw != nil { 98 | m.PayloadRaw = make([]byte, len(opts.PayloadRaw)) 99 | copy(m.PayloadRaw, opts.PayloadRaw) 100 | } 101 | return m 102 | } 103 | 104 | // ToWireMessage serializes this ExtendedOptions to a BitTorrent wire message 105 | func (opts Message) ToWireMessage() common.WireMessage { 106 | var body []byte 107 | if opts.ID == 0 { 108 | var b util.Buffer 109 | bencode.NewEncoder(&b).Encode(opts) 110 | body = b.Bytes() 111 | } else if opts.Payload != nil { 112 | var b util.Buffer 113 | bencode.NewEncoder(&b).Encode(opts.Payload) 114 | body = b.Bytes() 115 | } else if opts.PayloadRaw != nil { 116 | body = opts.PayloadRaw 117 | } else { 118 | // wtf? invalid message 119 | return nil 120 | } 121 | return common.NewWireMessage(common.Extended, []byte{opts.ID}, body) 122 | } 123 | 124 | // New creates new valid Message instance 125 | func New() Message { 126 | return Message{ 127 | Version: version.Version(), 128 | Extensions: make(map[string]uint32), 129 | } 130 | } 131 | 132 | // NewOur creates a new Message instance with metadata size set 133 | func NewOur(sz uint32) Message { 134 | m := Message{ 135 | Version: version.Version(), 136 | Extensions: make(map[string]uint32), 137 | } 138 | if sz > 0 { 139 | m.MetainfoSize = &sz 140 | } 141 | return m 142 | } 143 | 144 | // NewI2PPEX creates a new PEX message for i2p peers 145 | func NewI2PPEX(id uint8, connected, disconnected []byte) Message { 146 | payload := map[string]interface{}{ 147 | "added": connected, 148 | "dropped": disconnected, 149 | } 150 | msg := New() 151 | msg.ID = id 152 | msg.Payload = payload 153 | return msg 154 | } 155 | 156 | // NewLNPex creates a new PEX message for lokinet peers 157 | func NewLNPEX(id uint8, connected, disconnected []common.Peer) Message { 158 | payload := map[string]interface{}{ 159 | "added": connected, 160 | "dropped": disconnected, 161 | } 162 | msg := New() 163 | msg.ID = id 164 | msg.Payload = payload 165 | return msg 166 | } 167 | 168 | var ErrInvalidSize = errors.New("invalid message size") 169 | var ErrInvalidMessageID = errors.New("invalid message id") 170 | 171 | // FromWireMessage loads an ExtendedOptions messgae from a BitTorrent wire message 172 | func FromWireMessage(msg common.WireMessage) (opts Message, err error) { 173 | if msg.MessageID() == common.Extended { 174 | payload := msg.Payload() 175 | if len(payload) > 1 { 176 | opts = Message{ 177 | ID: payload[0], 178 | PayloadRaw: payload[1:], 179 | } 180 | if opts.ID == 0 { 181 | // handshake 182 | bencode.DecodeBytes(opts.PayloadRaw, &opts) 183 | // clear out raw payload because handshake 184 | opts.PayloadRaw = nil 185 | } else { 186 | bencode.DecodeBytes(opts.PayloadRaw, &opts.Payload) 187 | } 188 | } else { 189 | err = ErrInvalidSize 190 | } 191 | } else { 192 | err = ErrInvalidMessageID 193 | } 194 | return 195 | } 196 | -------------------------------------------------------------------------------- /cmd/rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "fmt" 5 | "github.com/majestrate/XD/lib/bittorrent/swarm" 6 | "github.com/majestrate/XD/lib/config" 7 | "github.com/majestrate/XD/lib/log" 8 | "github.com/majestrate/XD/lib/rpc" 9 | t "github.com/majestrate/XD/lib/translate" 10 | "github.com/majestrate/XD/lib/util" 11 | "github.com/majestrate/XD/lib/version" 12 | "net/url" 13 | "os" 14 | "sort" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | func formatRate(r float64) string { 20 | str := util.FormatRate(r) 21 | for len(str) < 12 { 22 | str += " " 23 | } 24 | return str 25 | } 26 | 27 | // Run runs xd-cli main function 28 | func Run() { 29 | var args []string 30 | cmd := "help" 31 | fname := "torrents.ini" 32 | if len(os.Args) > 1 { 33 | cmd = os.Args[1] 34 | args = os.Args[2:] 35 | } 36 | cfg := new(config.Config) 37 | err := cfg.Load(fname) 38 | if err != nil { 39 | log.Errorf("error: %s", err) 40 | return 41 | } 42 | log.SetLevel(cfg.Log.Level) 43 | var rpcURL string 44 | if strings.HasPrefix(cfg.RPC.Bind, "unix:") { 45 | rpcURL = cfg.RPC.Bind 46 | } else { 47 | u := url.URL{ 48 | Scheme: "http", 49 | Host: cfg.RPC.Bind, 50 | Path: rpc.RPCPath, 51 | } 52 | rpcURL = u.String() 53 | } 54 | swarms := cfg.Bittorrent.Swarms 55 | count := 0 56 | switch strings.ToLower(cmd) { 57 | case "list": 58 | for count < swarms { 59 | c := rpc.NewClient(rpcURL, count) 60 | listTorrents(c) 61 | count++ 62 | } 63 | case "add": 64 | for count < swarms { 65 | c := rpc.NewClient(rpcURL, count) 66 | addTorrents(c, args...) 67 | count++ 68 | } 69 | case "start": 70 | for count < swarms { 71 | c := rpc.NewClient(rpcURL, count) 72 | startTorrents(c, args...) 73 | count++ 74 | } 75 | case "stop": 76 | for count < swarms { 77 | c := rpc.NewClient(rpcURL, count) 78 | stopTorrents(c, args...) 79 | count++ 80 | } 81 | case "remove": 82 | for count < swarms { 83 | c := rpc.NewClient(rpcURL, count) 84 | removeTorrents(c, args...) 85 | count++ 86 | } 87 | case "delete": 88 | for count < swarms { 89 | c := rpc.NewClient(rpcURL, count) 90 | deleteTorrents(c, args...) 91 | count++ 92 | } 93 | case "set-piece-window": 94 | for count < swarms { 95 | c := rpc.NewClient(rpcURL, count) 96 | setPieceWindow(c, args[0]) 97 | count++ 98 | } 99 | case "version": 100 | fmt.Println(version.Version()) 101 | case "help": 102 | printHelp(os.Args[0]) 103 | } 104 | } 105 | 106 | func printHelp(cmd string) { 107 | fmt.Println(t.T("usage: %s [help|version|list|add http://somesite.i2p/some.torrent|set-piece-window n|remove infohash|delete infohash|stop infohash|start infohash]", cmd)) 108 | } 109 | 110 | func setPieceWindow(c *rpc.Client, str string) { 111 | n, err := strconv.Atoi(str) 112 | if err != nil { 113 | log.Fatalf("error: %s", err.Error()) 114 | } 115 | c.SetPieceWindow(n) 116 | } 117 | 118 | func addTorrents(c *rpc.Client, urls ...string) { 119 | for idx := range urls { 120 | fmt.Println(t.T("fetch %s ... ", urls[idx])) 121 | err := c.AddTorrent(urls[idx]) 122 | if err == nil { 123 | fmt.Println(t.T("OK")) 124 | } else { 125 | fmt.Println(t.E(err)) 126 | } 127 | } 128 | } 129 | 130 | func startTorrents(c *rpc.Client, ih ...string) { 131 | for idx := range ih { 132 | fmt.Println(t.T("start %s ... ", ih[idx])) 133 | err := c.AddTorrent(ih[idx]) 134 | if err == nil { 135 | fmt.Println(t.T("OK")) 136 | } else { 137 | fmt.Println(t.E(err)) 138 | } 139 | } 140 | } 141 | 142 | func stopTorrents(c *rpc.Client, ih ...string) { 143 | for idx := range ih { 144 | fmt.Println(t.T("stop %s ... ", ih[idx])) 145 | err := c.StopTorrent(ih[idx]) 146 | if err == nil { 147 | fmt.Println(t.T("OK")) 148 | } else { 149 | fmt.Println(t.E(err)) 150 | } 151 | } 152 | } 153 | 154 | func removeTorrents(c *rpc.Client, ih ...string) { 155 | for idx := range ih { 156 | fmt.Println(t.T("remove %s ... ", ih[idx])) 157 | err := c.RemoveTorrent(ih[idx]) 158 | if err == nil { 159 | fmt.Println(t.T("OK")) 160 | } else { 161 | fmt.Println(t.E(err)) 162 | } 163 | } 164 | } 165 | 166 | func deleteTorrents(c *rpc.Client, ih ...string) { 167 | for idx := range ih { 168 | fmt.Println(t.T("delete %s ... ", ih[idx])) 169 | err := c.DeleteTorrent(ih[idx]) 170 | if err == nil { 171 | fmt.Println(t.T("OK")) 172 | } else { 173 | fmt.Println(t.E(err)) 174 | } 175 | } 176 | } 177 | 178 | func listTorrents(c *rpc.Client) { 179 | var err error 180 | var st swarm.SwarmStatus 181 | st, err = c.GetSwarmStatus() 182 | if err != nil { 183 | log.Errorf("rpc error: %s", err) 184 | return 185 | } 186 | 187 | var torrents swarm.TorrentStatusList 188 | for _, status := range st { 189 | torrents = append(torrents, status) 190 | } 191 | sort.Stable(&torrents) 192 | for _, status := range torrents { 193 | fmt.Printf("%s [%s] %s %.2f\n", status.Name, status.Infohash, t.T("progress:"), status.Progress*100) 194 | fmt.Println(t.T("peers:")) 195 | sort.Stable(&status.Peers) 196 | for _, peer := range status.Peers { 197 | pad := peer.ID 198 | 199 | for len(pad) < 65 { 200 | pad += " " 201 | } 202 | fmt.Printf("\t%stx=%s rx=%s\n", pad, formatRate(peer.TX), formatRate(peer.RX)) 203 | } 204 | fmt.Printf("%s tx=%s rx=%s (%s: %.2f)\n", status.State, formatRate(status.Peers.TX()), formatRate(status.Peers.RX()), t.T("ratio"), status.Ratio()) 205 | fmt.Println(t.T("files:")) 206 | for idx, f := range status.Files { 207 | fmt.Printf("\t[%d] %s (%s: %.2f)\n", idx, f.FileInfo.Path.FilePath(""), t.T("progress:"), f.Progress) 208 | } 209 | fmt.Println() 210 | } 211 | fmt.Println() 212 | tx, rx := st.TotalSpeed() 213 | fmt.Printf("%s: tx=%s rx=%s (%.2f ratio)\n", t.TN("%d torrent", "%d torrents", torrents.Len(), torrents.Len()), formatRate(tx), formatRate(rx), st.Ratio()) 214 | fmt.Println() 215 | fmt.Println() 216 | } 217 | -------------------------------------------------------------------------------- /lib/network/inet/inet.go: -------------------------------------------------------------------------------- 1 | package inet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const DefaultHostname = "localhost.loki" 12 | const DefaultPort = "6888" 13 | 14 | type Session struct { 15 | localIP net.IP 16 | localAddr string 17 | name string 18 | port string 19 | serv net.Listener 20 | packet net.PacketConn 21 | resolver net.Resolver 22 | } 23 | 24 | func NewSession(port, dns string) (s *Session, err error) { 25 | var found []net.IP 26 | found, err = net.LookupIP(DefaultHostname) 27 | if err != nil { 28 | return 29 | } 30 | localIP := found[0] 31 | ss := &Session{ 32 | port: port, 33 | localIP: localIP, 34 | localAddr: net.JoinHostPort(localIP.String(), port), 35 | resolver: net.Resolver{ 36 | Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { 37 | var d net.Dialer 38 | return d.DialContext(ctx, "udp", dns) 39 | }, 40 | }, 41 | } 42 | var names []string 43 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 44 | defer cancel() 45 | names, err = ss.resolver.LookupAddr(ctx, ss.localIP.String()) 46 | if err != nil { 47 | return 48 | } 49 | if len(names) == 0 { 50 | err = fmt.Errorf("we have no rdns record for %s", localIP) 51 | return 52 | } 53 | ss.name = strings.TrimSuffix(names[0], ".") 54 | s = ss 55 | return 56 | } 57 | 58 | func (s *Session) LocalName() string { 59 | return s.name 60 | } 61 | 62 | func (s *Session) Dial(_, a string) (net.Conn, error) { 63 | h, p, err := net.SplitHostPort(a) 64 | if err != nil { 65 | return nil, err 66 | } 67 | raddr, err := s.lookupTCP(h, p) 68 | if err != nil { 69 | return nil, err 70 | } 71 | localAddr := net.JoinHostPort(s.localIP.String(), "0") 72 | laddr, err := net.ResolveTCPAddr("tcp4", localAddr) 73 | if err != nil { 74 | return nil, err 75 | } 76 | c, err := net.DialTCP("tcp4", laddr, raddr) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return s.wrapConn(c) 81 | } 82 | 83 | func (s *Session) wrapConn(c net.Conn) (*Conn, error) { 84 | raddr := c.RemoteAddr() 85 | h, port, err := net.SplitHostPort(raddr.String()) 86 | if err != nil { 87 | c.Close() 88 | return nil, err 89 | } 90 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 91 | defer cancel() 92 | names, err := s.resolver.LookupAddr(ctx, h) 93 | if err != nil { 94 | c.Close() 95 | return nil, err 96 | } 97 | return &Conn{ 98 | c: c, 99 | laddr: &Addr{ 100 | name: s.name, 101 | port: s.port, 102 | }, 103 | raddr: &Addr{ 104 | name: strings.TrimSuffix(names[0], "."), 105 | port: port, 106 | }, 107 | }, nil 108 | } 109 | 110 | type Listener struct { 111 | l net.Listener 112 | laddr *Addr 113 | } 114 | 115 | func (l *Listener) Addr() net.Addr { 116 | return l.laddr 117 | } 118 | 119 | func (l *Listener) Close() error { 120 | return l.l.Close() 121 | } 122 | 123 | func (l *Listener) Accept() (net.Conn, error) { 124 | return l.l.Accept() 125 | } 126 | 127 | func NewAddr(n, p string) *Addr { 128 | return &Addr{ 129 | name: n, 130 | port: p, 131 | } 132 | } 133 | 134 | type Addr struct { 135 | name string 136 | port string 137 | } 138 | 139 | func (a *Addr) Network() string { 140 | return "tcp" 141 | } 142 | 143 | func (a *Addr) String() string { 144 | return net.JoinHostPort(a.name, a.port) 145 | } 146 | 147 | type Conn struct { 148 | c net.Conn 149 | laddr *Addr 150 | raddr *Addr 151 | } 152 | 153 | // implements net.Conn 154 | func (c *Conn) Read(d []byte) (n int, err error) { 155 | n, err = c.c.Read(d) 156 | return 157 | } 158 | 159 | // implements net.Conn 160 | func (c *Conn) Write(d []byte) (n int, err error) { 161 | n, err = c.c.Write(d) 162 | return 163 | } 164 | 165 | // implements net.Conn 166 | func (c *Conn) Close() error { 167 | return c.c.Close() 168 | } 169 | 170 | // implements net.Conn 171 | func (c *Conn) LocalAddr() net.Addr { 172 | return c.laddr 173 | } 174 | 175 | // implements net.Conn 176 | func (c *Conn) RemoteAddr() net.Addr { 177 | return c.raddr 178 | } 179 | 180 | // implements net.Conn 181 | func (c *Conn) SetDeadline(t time.Time) error { 182 | return c.c.SetDeadline(t) 183 | } 184 | 185 | // implements net.Conn 186 | func (c *Conn) SetReadDeadline(t time.Time) error { 187 | return c.c.SetReadDeadline(t) 188 | } 189 | 190 | // implements net.Conn 191 | func (c *Conn) SetWriteDeadline(t time.Time) error { 192 | return c.c.SetWriteDeadline(t) 193 | } 194 | 195 | func (s *Session) Accept() (net.Conn, error) { 196 | c, err := s.serv.Accept() 197 | if err != nil { 198 | return nil, err 199 | } 200 | return s.wrapConn(c) 201 | } 202 | 203 | func (s *Session) Open() error { 204 | l, err := net.Listen("tcp", s.localAddr) 205 | if err != nil { 206 | return err 207 | } 208 | _, port, err := net.SplitHostPort(l.Addr().String()) 209 | if err != nil { 210 | return err 211 | } 212 | s.serv = &Listener{ 213 | l: l, 214 | laddr: &Addr{ 215 | name: s.name, 216 | port: port, 217 | }, 218 | } 219 | return nil 220 | } 221 | 222 | func (s *Session) ReadFrom(d []byte) (n int, from net.Addr, err error) { 223 | return 224 | } 225 | 226 | func (s *Session) WriteTo(d []byte, to net.Addr) (n int, err error) { 227 | return 228 | } 229 | 230 | func (s *Session) Close() error { 231 | return s.serv.Close() 232 | } 233 | 234 | func (s *Session) Addr() net.Addr { 235 | if s.serv == nil { 236 | return nil 237 | } 238 | return s.serv.Addr() 239 | } 240 | 241 | func (s *Session) Lookup(name, port string) (addr net.Addr, err error) { 242 | return s.lookupTCP(name, port) 243 | } 244 | 245 | func (s *Session) lookupTCP(name, port string) (addr *net.TCPAddr, err error) { 246 | var ips []net.IPAddr 247 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 248 | defer cancel() 249 | ips, err = s.resolver.LookupIPAddr(ctx, name) 250 | if err == nil { 251 | for _, ip := range ips { 252 | tcpaddr := &net.TCPAddr{ 253 | IP: ip.IP, 254 | } 255 | tcpaddr.Port, err = net.LookupPort(tcpaddr.Network(), port) 256 | if err == nil { 257 | addr = tcpaddr 258 | return 259 | } 260 | } 261 | } 262 | return 263 | } 264 | -------------------------------------------------------------------------------- /contrib/webui/lib/main.js: -------------------------------------------------------------------------------- 1 | function bytesToSize(bytes) { 2 | var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; 3 | if (bytes == 0) return '0 B'; 4 | var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); 5 | return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]; 6 | } 7 | 8 | function formatFloat(f, eps) { 9 | if (!eps) eps = 2; 10 | eps = Math.pow(10, eps); 11 | return parseInt(f * eps) / eps; 12 | } 13 | 14 | function makeRatio(tx, rx) { 15 | var r = "0.0"; 16 | if ( rx > 0 ) { 17 | if ( tx > 0 ) { 18 | r = "" + formatFloat(tx / rx); 19 | } 20 | } else if ( tx > 0 ) { 21 | r = "\u221E"; 22 | } 23 | return r; 24 | } 25 | 26 | var Torrent = function(data) { 27 | this.Name = data.Name; 28 | this.State = data.State; 29 | this.Infohash = data.Infohash; 30 | this.Peers = function() { return data.Peers ? data.Peers.length : 0; }; 31 | this.Speed = function() { 32 | var tx = 0, rx = 0; 33 | if (data.Peers) 34 | data.Peers.forEach(function(p){tx += p.TX; rx += p.RX;}); 35 | return "\u2191 " + bytesToSize(tx) +"/s \u2193 " + bytesToSize(rx) + "/s"; 36 | }; 37 | this.RX = function() { 38 | var rx = 0; 39 | if(data.Peers) data.Peers.forEach(function(p) { rx += p.RX; }); 40 | return rx; 41 | }; 42 | this.TX = function() { 43 | var tx = 0; 44 | if(data.Peers) data.Peers.forEach(function(p) { tx += p.TX; }); 45 | return tx; 46 | }; 47 | this.Data = function() { 48 | return data; 49 | }; 50 | this.TotalSize = function() { 51 | var total_size = 0; 52 | if(data.Files) { 53 | data.Files.forEach(function(f){ total_size += f.FileInfo.Length }); 54 | } 55 | return bytesToSize(total_size); 56 | }; 57 | this.Progress = formatFloat(data.Progress * 100); 58 | this.Ratio = function() { 59 | return "("+makeRatio(data.TX, data.RX) + " ratio)"; 60 | }; 61 | 62 | this.changeTorrent = function(action) 63 | { 64 | viewModel._apicall({method: "XD.ChangeTorrent", action: action, infohash: this.Infohash, swarm: "0"}, function(data){ 65 | console.log(data); 66 | }); 67 | }.bind(this); 68 | 69 | this.remove = function() 70 | { 71 | if (viewModel.confirmation.silent()) { 72 | viewModel.deleteTorrent(this.Infohash, viewModel.confirmation.deleteFiles()); 73 | } else { 74 | viewModel.confirmation.Infohash(this.Infohash); 75 | viewModel.confirmation.show(true); 76 | } 77 | }.bind(this); 78 | 79 | this.start = function() 80 | { 81 | this.changeTorrent("start"); 82 | }.bind(this); 83 | 84 | this.stop = function() 85 | { 86 | this.changeTorrent("stop"); 87 | }.bind(this); 88 | 89 | this.toggle = function() 90 | { 91 | if(this.Stopped()) 92 | this.start(); 93 | else 94 | this.stop(); 95 | }.bind(this); 96 | 97 | this.Stopped = function() 98 | { 99 | return data.State == "stopped"; 100 | }; 101 | 102 | this.StatusButton = function() 103 | { 104 | if (this.Stopped()) 105 | { 106 | return "\u25BA"; 107 | } 108 | else 109 | { 110 | return "\u275A\u275A"; 111 | } 112 | }; 113 | } 114 | 115 | var viewModel = { 116 | _url: "ecksdee/api", 117 | _apicall: function(call, cb) 118 | { 119 | $.ajax({ 120 | type: "POST", 121 | url: this._url, 122 | contentType: "text/json; charset=UTF-8", 123 | data: JSON.stringify(call), 124 | success: function(j, text, xhr) { 125 | // console.log(call, j); 126 | cb(JSON.parse(j)); 127 | } 128 | }); 129 | }, 130 | torrents: ko.observableArray(), 131 | torrentURL: ko.observable(), 132 | torrentFilter: ko.observable('all').bind(this), 133 | setFilter: function(state) { 134 | this.torrentFilter(state); main(); }, 135 | addTorrent: function() 136 | { 137 | var _this = this; 138 | this._apicall({method: "XD.AddTorrent", swarm: "0", url: this.torrentURL()}, function(data){ 139 | if (!data.error) _this.torrentURL(""); 140 | }); 141 | }, 142 | deleteTorrent: function(Infohash, deleteFiles) 143 | { 144 | var action = deleteFiles ? "delete" : "remove"; 145 | this._apicall({ 146 | method: "XD.ChangeTorrent", 147 | action: action, 148 | infohash: Infohash, swarm: "0"}, 149 | function(data){ console.log(data); }); 150 | }, 151 | torrentStates: ['all', 'downloading', 'seeding'], 152 | globalInfo: function() 153 | { 154 | var rx = 0; 155 | var tx = 0; 156 | var peers = 0; 157 | var rtx = 0; 158 | var rrx = 0; 159 | var count = 0; 160 | this.torrents().forEach(function(t) { 161 | rx += t.RX(); 162 | tx += t.TX(); 163 | rrx += t.Data().RX; 164 | rtx += t.Data().TX; 165 | peers += t.Peers(); 166 | count ++; 167 | }); 168 | return peers+" peers connected on "+ count+ " torrents (" + makeRatio(rtx, rrx) + " ratio) \u2191 " + bytesToSize(tx) +"/s \u2193 " + bytesToSize(rx) + "/s"; 169 | }, 170 | 171 | // confirmation box 172 | confirmation: { 173 | Infohash: ko.observable(), 174 | show: ko.observable(false), silent: ko.observable(false), deleteFiles: ko.observable(false), 175 | close: function() { this.confirmation.Infohash(null); 176 | this.confirmation.silent(false); this.confirmation.show(false); }, 177 | confirmed: function() { 178 | this.deleteTorrent(this.confirmation.Infohash(), this.confirmation.deleteFiles()); 179 | this.confirmation.Infohash(null); this.confirmation.show(false); } 180 | }, 181 | }; 182 | 183 | function main() 184 | { 185 | viewModel._apicall({method: "XD.SwarmStatus"}, function(data){ 186 | viewModel.torrents.removeAll(); 187 | for (var prop in data) { 188 | if (viewModel.torrentFilter() != 'all' & viewModel.torrentFilter() != data[prop].State) 189 | continue; 190 | viewModel.torrents.push(new Torrent(data[prop])); 191 | } 192 | }); 193 | } 194 | 195 | window.onload = function() 196 | { 197 | ko.applyBindings(viewModel); 198 | main(); setInterval(main, 1000); 199 | } 200 | --------------------------------------------------------------------------------