diff --git a/cmd/main.go b/cmd/main.go index 658d244..0a66eea 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,11 +3,12 @@ package main import ( "flag" "fmt" - "net/http" "os" - "github.com/ap-pauloafonso/ratio-spoof/emulation" - "github.com/ap-pauloafonso/ratio-spoof/ratiospoof" + "github.com/ap-pauloafonso/ratio-spoof/internal/emulation" + "github.com/ap-pauloafonso/ratio-spoof/internal/input" + "github.com/ap-pauloafonso/ratio-spoof/internal/printer" + "github.com/ap-pauloafonso/ratio-spoof/internal/ratiospoof" ) func main() { @@ -50,8 +51,8 @@ required arguments: } qbit := emulation.NewQbitTorrent() - r, err := ratiospoof.NewRatioSPoofState( - ratiospoof.InputArgs{ + r, err := ratiospoof.NewRatioSpoofState( + input.InputArgs{ TorrentPath: *torrentPath, InitialDownloaded: *initialDownload, DownloadSpeed: *downloadSpeed, @@ -60,12 +61,13 @@ required arguments: Port: *port, Debug: *debug, }, - qbit, - http.DefaultClient) + qbit) if err != nil { panic(err) } + + go printer.PrintState(r) r.Run() } diff --git a/bencode/bencode.go b/internal/bencode/bencode.go similarity index 100% rename from bencode/bencode.go rename to internal/bencode/bencode.go diff --git a/bencode/bencode_test.go b/internal/bencode/bencode_test.go similarity index 100% rename from bencode/bencode_test.go rename to internal/bencode/bencode_test.go diff --git a/bencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent b/internal/bencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent similarity index 100% rename from bencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent rename to internal/bencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent diff --git a/bencode/torrent_files_test/Slackware 14.2 x86_64 DVD ISO (Includes everything except for source code -- see the Slackware 14.2 source code DVD ISO above for source code).torrent b/internal/bencode/torrent_files_test/Slackware142.torrent similarity index 100% rename from bencode/torrent_files_test/Slackware 14.2 x86_64 DVD ISO (Includes everything except for source code -- see the Slackware 14.2 source code DVD ISO above for source code).torrent rename to internal/bencode/torrent_files_test/Slackware142.torrent diff --git a/bencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent b/internal/bencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent similarity index 100% rename from bencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent rename to internal/bencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent diff --git a/emulation/qbittorrent.go b/internal/emulation/qbittorrent.go similarity index 100% rename from emulation/qbittorrent.go rename to internal/emulation/qbittorrent.go diff --git a/emulation/qbittorrent_test.go b/internal/emulation/qbittorrent_test.go similarity index 100% rename from emulation/qbittorrent_test.go rename to internal/emulation/qbittorrent_test.go diff --git a/internal/input/input.go b/internal/input/input.go new file mode 100644 index 0000000..592d47b --- /dev/null +++ b/internal/input/input.go @@ -0,0 +1,149 @@ +package input + +import ( + "errors" + "fmt" + "math" + "strconv" + "strings" + + "github.com/ap-pauloafonso/ratio-spoof/internal/bencode" +) + +type InputArgs struct { + TorrentPath string + InitialDownloaded string + DownloadSpeed string + InitialUploaded string + UploadSpeed string + Port int + Debug bool +} + +type InputParsed struct { + TorrentPath string + InitialDownloaded int + DownloadSpeed int + InitialUploaded int + UploadSpeed int + Port int + Debug bool +} + +var validInitialSufixes = [...]string{"%", "b", "kb", "mb", "gb", "tb"} +var validSpeedSufixes = [...]string{"kbps", "mbps"} + +func (I *InputArgs) ParseInput(torrentInfo *bencode.TorrentInfo) (*InputParsed, error) { + downloaded, err := extractInputInitialByteCount(I.InitialDownloaded, torrentInfo.TotalSize, true) + if err != nil { + return nil, err + } + uploaded, err := extractInputInitialByteCount(I.InitialUploaded, torrentInfo.TotalSize, false) + if err != nil { + return nil, err + } + downloadSpeed, err := extractInputByteSpeed(I.DownloadSpeed) + if err != nil { + return nil, err + } + uploadSpeed, err := extractInputByteSpeed(I.UploadSpeed) + if err != nil { + return nil, err + } + + if I.Port < 1 || I.Port > 65535 { + return nil, errors.New("port number must be between 1 and 65535") + } + + return &InputParsed{InitialDownloaded: downloaded, + DownloadSpeed: downloadSpeed, + InitialUploaded: uploaded, + UploadSpeed: uploadSpeed, + Debug: I.Debug, + Port: I.Port, + }, nil +} + +func checkSpeedSufix(input string) (valid bool, suffix string) { + for _, v := range validSpeedSufixes { + + if strings.HasSuffix(strings.ToLower(input), v) { + return true, input[len(input)-4:] + } + } + return false, "" +} + +func extractInputInitialByteCount(initialSizeInput string, totalBytes int, errorIfHigher bool) (int, error) { + byteCount := strSize2ByteSize(initialSizeInput, totalBytes) + if errorIfHigher && byteCount > totalBytes { + return 0, errors.New("initial downloaded can not be higher than the torrent size") + } + if byteCount < 0 { + return 0, errors.New("initial value can not be negative") + } + return byteCount, nil +} +func extractInputByteSpeed(initialSpeedInput string) (int, error) { + ok, suffix := checkSpeedSufix(initialSpeedInput) + if !ok { + return 0, fmt.Errorf("speed must be in %v", validSpeedSufixes) + } + number, _ := strconv.ParseFloat(initialSpeedInput[:len(initialSpeedInput)-4], 64) + if number < 0 { + return 0, errors.New("speed can not be negative") + } + + if suffix == "kbps" { + number *= 1024 + } else { + number = number * 1024 * 1024 + } + ret := int(number) + return ret, nil +} + +func strSize2ByteSize(input string, totalSize int) int { + lowerInput := strings.ToLower(input) + + parseStrNumberFn := func(strWithSufix string, sufixLength, n int) int { + v, _ := strconv.ParseFloat(strWithSufix[:len(lowerInput)-sufixLength], 64) + result := v * math.Pow(1024, float64(n)) + return int(result) + } + switch { + case strings.HasSuffix(lowerInput, "kb"): + { + return parseStrNumberFn(lowerInput, 2, 1) + } + case strings.HasSuffix(lowerInput, "mb"): + { + return parseStrNumberFn(lowerInput, 2, 2) + } + case strings.HasSuffix(lowerInput, "gb"): + { + return parseStrNumberFn(lowerInput, 2, 3) + } + case strings.HasSuffix(lowerInput, "tb"): + { + return parseStrNumberFn(lowerInput, 2, 4) + } + case strings.HasSuffix(lowerInput, "b"): + { + return parseStrNumberFn(lowerInput, 1, 0) + } + case strings.HasSuffix(lowerInput, "%"): + { + v, _ := strconv.ParseFloat(lowerInput[:len(lowerInput)-1], 64) + if v < 0 || v > 100 { + panic("percent value must be in (0-100)") + } + result := int(float64(v/100) * float64(totalSize)) + + return result + } + + default: + panic("Size not found") + } +} diff --git a/internal/input/input_test.go b/internal/input/input_test.go new file mode 100644 index 0000000..840afc2 --- /dev/null +++ b/internal/input/input_test.go @@ -0,0 +1,34 @@ +package input + +import ( + "fmt" + "testing" +) + +func TestStrSize2ByteSize(T *testing.T) { + + data := []struct { + in string + inTotalSize int + out int + }{ + {"100kb", 100, 102400}, + {"1kb", 0, 1024}, + {"1mb", 0, 1048576}, + {"1gb", 0, 1073741824}, + {"1.5gb", 0, 1610612736}, + {"1tb", 0, 1099511627776}, + {"1b", 0, 1}, + {"100%", 10737418240, 10737418240}, + {"55%", 943718400, 519045120}, + } + + for idx, td := range data { + T.Run(fmt.Sprint(idx), func(t *testing.T) { + got := strSize2ByteSize(td.in, td.inTotalSize) + if got != td.out { + t.Errorf("got %v, want %v", got, td.out) + } + }) + } +} diff --git a/ratiospoof/printstate.go b/internal/printer/printer.go similarity index 50% rename from ratiospoof/printstate.go rename to internal/printer/printer.go index ca71723..86c2f27 100644 --- a/ratiospoof/printstate.go +++ b/internal/printer/printer.go @@ -1,4 +1,4 @@ -package ratiospoof +package printer import ( "fmt" @@ -8,13 +8,14 @@ import ( "strings" "time" + "github.com/ap-pauloafonso/ratio-spoof/internal/ratiospoof" "github.com/olekukonko/ts" ) -func (R *ratioSpoofState) PrintState(exitedCH <-chan string) { +func PrintState(state *ratiospoof.RatioSpoof) { exit := false go func() { - _ = <-exitedCH + _ = <-state.StopPrintCH exit = true }() @@ -24,19 +25,25 @@ func (R *ratioSpoofState) PrintState(exitedCH <-chan string) { } width := terminalSize() clear() - if R.announceHistory.Len() > 0 { - seedersStr := fmt.Sprint(R.seeders) - leechersStr := fmt.Sprint(R.leechers) - if R.seeders == 0 { + + if state.AnnounceCount == 1 { + println("Trying to connect to the tracker...") + time.Sleep(1 * time.Second) + continue + } + if state.AnnounceHistory.Len() > 0 { + seedersStr := fmt.Sprint(state.Seeders) + leechersStr := fmt.Sprint(state.Leechers) + if state.Seeders == 0 { seedersStr = "not informed" } - if R.leechers == 0 { + if state.Leechers == 0 { leechersStr = "not informed" } var retryStr string - if R.retryAttempt > 0 { - retryStr = fmt.Sprintf("(*Retry %v - check your connection)", R.retryAttempt) + if state.Tracker.RetryAttempt > 0 { + retryStr = fmt.Sprintf("(*Retry %v - check your connection)", state.Tracker.RetryAttempt) } fmt.Printf("%s\n", center(" RATIO-SPOOF ", width-len(" RATIO-SPOOF "), "#")) fmt.Printf(` @@ -47,25 +54,25 @@ func (R *ratioSpoofState) PrintState(exitedCH <-chan string) { Download Speed: %v/s Upload Speed: %v/s Size: %v - Emulation: %v | Port: %v`, R.torrentInfo.Name, R.torrentInfo.TrackerInfo.Main, seedersStr, leechersStr, humanReadableSize(float64(R.input.downloadSpeed)), - humanReadableSize(float64(R.input.uploadSpeed)), humanReadableSize(float64(R.torrentInfo.TotalSize)), R.bitTorrentClient.Name(), R.input.port) + Emulation: %v | Port: %v`, state.TorrentInfo.Name, state.TorrentInfo.TrackerInfo.Main, seedersStr, leechersStr, humanReadableSize(float64(state.Input.DownloadSpeed)), + humanReadableSize(float64(state.Input.UploadSpeed)), humanReadableSize(float64(state.TorrentInfo.TotalSize)), state.BitTorrentClient.Name(), state.Input.Port) fmt.Printf("\n\n%s\n\n", center(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF ", width-len(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF "), "#")) - for i := 0; i <= R.announceHistory.Len()-2; i++ { - dequeItem := R.announceHistory.At(i).(announceEntry) - fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | announced\n", dequeItem.count, humanReadableSize(float64(dequeItem.downloaded)), dequeItem.percentDownloaded, humanReadableSize(float64(dequeItem.left)), humanReadableSize(float64(dequeItem.uploaded))) + for i := 0; i <= state.AnnounceHistory.Len()-2; i++ { + dequeItem := state.AnnounceHistory.At(i).(ratiospoof.AnnounceEntry) + fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | announced\n", dequeItem.Count, humanReadableSize(float64(dequeItem.Downloaded)), dequeItem.PercentDownloaded, humanReadableSize(float64(dequeItem.Left)), humanReadableSize(float64(dequeItem.Uploaded))) } - lastDequeItem := R.announceHistory.At(R.announceHistory.Len() - 1).(announceEntry) - fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | next announce in: %v %v\n", lastDequeItem.count, - humanReadableSize(float64(lastDequeItem.downloaded)), - lastDequeItem.percentDownloaded, - humanReadableSize(float64(lastDequeItem.left)), - humanReadableSize(float64(lastDequeItem.uploaded)), - fmtDuration(R.currentAnnounceTimer), + lastDequeItem := state.AnnounceHistory.At(state.AnnounceHistory.Len() - 1).(ratiospoof.AnnounceEntry) + fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | next announce in: %v %v\n", lastDequeItem.Count, + humanReadableSize(float64(lastDequeItem.Downloaded)), + lastDequeItem.PercentDownloaded, + humanReadableSize(float64(lastDequeItem.Left)), + humanReadableSize(float64(lastDequeItem.Uploaded)), + fmtDuration(state.CurrentAnnounceTimer), retryStr) - if R.input.debug { + if state.Input.Debug { fmt.Printf("\n%s\n", center(" DEBUG ", width-len(" DEBUG "), "#")) - fmt.Printf("\n%s\n\n%s", R.lastAnounceRequest, R.lastTackerResponse) + fmt.Printf("\n%s\n\n%s", state.Tracker.LastAnounceRequest, state.Tracker.LastTackerResponse) } time.Sleep(1 * time.Second) } diff --git a/ratiospoof/printstate_test.go b/internal/printer/printer_test.go similarity index 96% rename from ratiospoof/printstate_test.go rename to internal/printer/printer_test.go index f351802..ca52f62 100644 --- a/ratiospoof/printstate_test.go +++ b/internal/printer/printer_test.go @@ -1,4 +1,4 @@ -package ratiospoof +package printer import ( "fmt" diff --git a/internal/ratiospoof/ratiospoof.go b/internal/ratiospoof/ratiospoof.go new file mode 100644 index 0000000..61b2444 --- /dev/null +++ b/internal/ratiospoof/ratiospoof.go @@ -0,0 +1,229 @@ +package ratiospoof + +import ( + "fmt" + "io/ioutil" + "math/rand" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/ap-pauloafonso/ratio-spoof/internal/bencode" + "github.com/ap-pauloafonso/ratio-spoof/internal/input" + "github.com/ap-pauloafonso/ratio-spoof/internal/tracker" + "github.com/gammazero/deque" +) + +const ( + maxAnnounceHistory = 10 +) + +type RatioSpoof struct { + mutex *sync.Mutex + TorrentInfo *bencode.TorrentInfo + Input *input.InputParsed + Tracker *tracker.HttpTracker + BitTorrentClient TorrentClientEmulation + CurrentAnnounceTimer int + AnnounceInterval int + NumWant int + Seeders int + Leechers int + AnnounceCount int + Status string + AnnounceHistory announceHistory + timerUpdateCh chan int + StopPrintCH chan interface{} +} + +type TorrentClientEmulation interface { + PeerID() string + Key() string + Query() string + Name() string + Headers() map[string]string + NextAmountReport(DownloadCandidateNextAmount, UploadCandidateNextAmount, leftCandidateNextAmount, pieceSize int) (downloaded, uploaded, left int) +} + +type AnnounceEntry struct { + Count int + Downloaded int + PercentDownloaded float32 + Uploaded int + Left int +} + +type announceHistory struct { + deque.Deque +} + +func NewRatioSpoofState(input input.InputArgs, torrentClient TorrentClientEmulation) (*RatioSpoof, error) { + changeTimerCh := make(chan int) + stopPrintCh := make(chan interface{}) + dat, err := ioutil.ReadFile(input.TorrentPath) + if err != nil { + return nil, err + } + + torrentInfo, err := bencode.TorrentDictParse(dat) + if err != nil { + panic(err) + } + + httpTracker, err := tracker.NewHttpTracker(torrentInfo, changeTimerCh) + if err != nil { + panic(err) + } + + inputParsed, err := input.ParseInput(torrentInfo) + if err != nil { + panic(err) + } + + return &RatioSpoof{ + BitTorrentClient: torrentClient, + TorrentInfo: torrentInfo, + Tracker: httpTracker, + Input: inputParsed, + NumWant: 200, + Status: "started", + mutex: &sync.Mutex{}, + timerUpdateCh: changeTimerCh, + StopPrintCH: stopPrintCh, + }, nil +} + +func (A *announceHistory) pushValueHistory(value AnnounceEntry) { + if A.Len() >= maxAnnounceHistory { + A.PopFront() + } + A.PushBack(value) +} + +func (R *RatioSpoof) gracefullyExit() { + fmt.Printf("\nGracefully exiting...\n") + R.Status = "stopped" + R.NumWant = 0 + R.fireAnnounce(false) +} + +func (R *RatioSpoof) Run() { + rand.Seed(time.Now().UnixNano()) + sigCh := make(chan os.Signal) + + signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + R.firstAnnounce() + go R.decreaseTimer() + go R.updateTimer() + go func() { + for { + R.generateNextAnnounce() + time.Sleep(time.Duration(R.AnnounceInterval) * time.Second) + R.fireAnnounce(true) + } + }() + <-sigCh + R.StopPrintCH <- "exit print" + R.gracefullyExit() +} +func (R *RatioSpoof) firstAnnounce() { + R.addAnnounce(R.Input.InitialDownloaded, R.Input.InitialUploaded, calculateBytesLeft(R.Input.InitialDownloaded, R.TorrentInfo.TotalSize), (float32(R.Input.InitialDownloaded)/float32(R.TorrentInfo.TotalSize))*100) + R.fireAnnounce(false) +} + +func (R *RatioSpoof) updateInterval(resp tracker.TrackerResponse) { + if resp.Interval > 0 { + R.AnnounceInterval = resp.Interval + } else { + R.AnnounceInterval = 1800 + } +} + +func (R *RatioSpoof) updateSeedersAndLeechers(resp tracker.TrackerResponse) { + R.Seeders = resp.Seeders + R.Leechers = resp.Leechers +} +func (R *RatioSpoof) addAnnounce(currentDownloaded, currentUploaded, currentLeft int, percentDownloaded float32) { + R.AnnounceCount++ + R.AnnounceHistory.pushValueHistory(AnnounceEntry{Count: R.AnnounceCount, Downloaded: currentDownloaded, Uploaded: currentUploaded, Left: currentLeft, PercentDownloaded: percentDownloaded}) +} +func (R *RatioSpoof) fireAnnounce(retry bool) { + lastAnnounce := R.AnnounceHistory.Back().(AnnounceEntry) + replacer := strings.NewReplacer("{infohash}", R.TorrentInfo.InfoHashURLEncoded, + "{port}", fmt.Sprint(R.Input.Port), + "{peerid}", R.BitTorrentClient.PeerID(), + "{uploaded}", fmt.Sprint(lastAnnounce.Uploaded), + "{downloaded}", fmt.Sprint(lastAnnounce.Downloaded), + "{left}", fmt.Sprint(lastAnnounce.Left), + "{key}", R.BitTorrentClient.Key(), + "{event}", R.Status, + "{numwant}", fmt.Sprint(R.NumWant)) + query := replacer.Replace(R.BitTorrentClient.Query()) + trackerResp := R.Tracker.Announce(query, R.BitTorrentClient.Headers(), retry, R.timerUpdateCh) + + if trackerResp != nil { + R.updateSeedersAndLeechers(*trackerResp) + R.updateInterval(*trackerResp) + } +} +func (R *RatioSpoof) generateNextAnnounce() { + R.timerUpdateCh <- R.AnnounceInterval + lastAnnounce := R.AnnounceHistory.Back().(AnnounceEntry) + currentDownloaded := lastAnnounce.Downloaded + var downloadCandidate int + + if currentDownloaded < R.TorrentInfo.TotalSize { + downloadCandidate = calculateNextTotalSizeByte(R.Input.DownloadSpeed, currentDownloaded, R.TorrentInfo.PieceSize, R.AnnounceInterval, R.TorrentInfo.TotalSize) + } else { + downloadCandidate = R.TorrentInfo.TotalSize + } + + currentUploaded := lastAnnounce.Uploaded + uploadCandidate := calculateNextTotalSizeByte(R.Input.UploadSpeed, currentUploaded, R.TorrentInfo.PieceSize, R.AnnounceInterval, 0) + + leftCandidate := calculateBytesLeft(downloadCandidate, R.TorrentInfo.TotalSize) + + d, u, l := R.BitTorrentClient.NextAmountReport(downloadCandidate, uploadCandidate, leftCandidate, R.TorrentInfo.PieceSize) + + R.addAnnounce(d, u, l, (float32(d)/float32(R.TorrentInfo.TotalSize))*100) +} +func (R *RatioSpoof) decreaseTimer() { + for { + time.Sleep(1 * time.Second) + R.mutex.Lock() + if R.CurrentAnnounceTimer > 0 { + R.CurrentAnnounceTimer-- + } + R.mutex.Unlock() + } +} + +func (R *RatioSpoof) updateTimer() { + for { + newValue := <-R.timerUpdateCh + R.mutex.Lock() + R.CurrentAnnounceTimer = newValue + R.mutex.Unlock() + } +} + +func calculateNextTotalSizeByte(speedBytePerSecond, currentByte, pieceSizeByte, seconds, limitTotalBytes int) int { + if speedBytePerSecond == 0 { + return currentByte + } + totalCandidate := currentByte + (speedBytePerSecond * seconds) + randomPieces := rand.Intn(10-1) + 1 + totalCandidate = totalCandidate + (pieceSizeByte * randomPieces) + + if limitTotalBytes != 0 && totalCandidate > limitTotalBytes { + return limitTotalBytes + } + return totalCandidate +} + +func calculateBytesLeft(currentBytes, totalBytes int) int { + return totalBytes - currentBytes +} diff --git a/ratiospoof/ratiospoof_test.go b/internal/ratiospoof/ratiospoof_test.go similarity index 59% rename from ratiospoof/ratiospoof_test.go rename to internal/ratiospoof/ratiospoof_test.go index ab8fc8d..b742e3d 100644 --- a/ratiospoof/ratiospoof_test.go +++ b/internal/ratiospoof/ratiospoof_test.go @@ -1,7 +1,6 @@ package ratiospoof import ( - "fmt" "testing" ) @@ -12,34 +11,6 @@ func assertAreEqual(t *testing.T, got, want interface{}) { } } -func TestStrSize2ByteSize(T *testing.T) { - - data := []struct { - in string - inTotalSize int - out int - }{ - {"100kb", 100, 102400}, - {"1kb", 0, 1024}, - {"1mb", 0, 1048576}, - {"1gb", 0, 1073741824}, - {"1.5gb", 0, 1610612736}, - {"1tb", 0, 1099511627776}, - {"1b", 0, 1}, - {"100%", 10737418240, 10737418240}, - {"55%", 943718400, 519045120}, - } - - for idx, td := range data { - T.Run(fmt.Sprint(idx), func(t *testing.T) { - got := strSize2ByteSize(td.in, td.inTotalSize) - if got != td.out { - t.Errorf("got %v, want %v", got, td.out) - } - }) - } -} - func TestClculateNextTotalSizeByte(T *testing.T) { got := calculateNextTotalSizeByte(100*1024, 0, 512, 30, 87979879) diff --git a/internal/tracker/tracker.go b/internal/tracker/tracker.go new file mode 100644 index 0000000..5a65587 --- /dev/null +++ b/internal/tracker/tracker.go @@ -0,0 +1,138 @@ +package tracker + +import ( + "bytes" + "compress/gzip" + "errors" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/ap-pauloafonso/ratio-spoof/internal/bencode" +) + +type HttpTracker struct { + Urls []string + RetryAttempt int + LastAnounceRequest string + LastTackerResponse string +} + +type TrackerResponse struct { + MinInterval int + Interval int + Seeders int + Leechers int +} + +func NewHttpTracker(torrentInfo *bencode.TorrentInfo, timerChangeChannel chan<- int) (*HttpTracker, error) { + + var result []string + for _, url := range torrentInfo.TrackerInfo.Urls { + if strings.HasPrefix(url, "http") { + result = append(result, url) + } + } + if len(result) == 0 { + return nil, errors.New("No tcp/http tracker url announce found") + } + return &HttpTracker{Urls: torrentInfo.TrackerInfo.Urls}, nil +} + +func (T *HttpTracker) SwapFirst(currentIdx int) { + aux := T.Urls[0] + T.Urls[0] = T.Urls[currentIdx] + T.Urls[currentIdx] = aux +} + +func (T *HttpTracker) Announce(query string, headers map[string]string, retry bool, timerUpdateChannel chan<- int) *TrackerResponse { + var trackerResp *TrackerResponse + if retry { + retryDelay := 30 * time.Second + for { + exit := false + func() { + defer func() { + if err := recover(); err != nil { + timerUpdateChannel <- int(retryDelay.Seconds()) + T.RetryAttempt++ + time.Sleep(retryDelay) + retryDelay *= 2 + if retryDelay.Seconds() > 900 { + retryDelay = 900 + } + } + }() + trackerResp = T.tryMakeRequest(query, headers) + exit = true + }() + if exit { + break + } + } + + } else { + trackerResp = T.tryMakeRequest(query, headers) + } + T.RetryAttempt = 0 + + return trackerResp + +} + +func (t *HttpTracker) tryMakeRequest(query string, headers map[string]string) *TrackerResponse { + for idx, baseUrl := range t.Urls { + completeURL := buildFullUrl(baseUrl, query) + t.LastAnounceRequest = completeURL + req, _ := http.NewRequest("GET", completeURL, nil) + for header, value := range headers { + req.Header.Add(header, value) + } + resp, err := http.DefaultClient.Do(req) + if err == nil { + if resp.StatusCode == http.StatusOK { + bytesR, _ := ioutil.ReadAll(resp.Body) + if len(bytesR) == 0 { + return nil + } + mimeType := http.DetectContentType(bytesR) + if mimeType == "application/x-gzip" { + gzipReader, _ := gzip.NewReader(bytes.NewReader(bytesR)) + bytesR, _ = ioutil.ReadAll(gzipReader) + gzipReader.Close() + } + t.LastTackerResponse = string(bytesR) + decodedResp := bencode.Decode(bytesR) + if idx != 0 { + t.SwapFirst(idx) + } + ret := extractTrackerResponse(decodedResp) + return &ret + } + resp.Body.Close() + } + } + panic("Connection error with the tracker") + +} + +func buildFullUrl(baseurl, query string) string { + if len(strings.Split(baseurl, "?")) > 1 { + return baseurl + "&" + strings.TrimLeft(query, "&") + } + return baseurl + "?" + strings.TrimLeft(query, "?") +} + +func extractTrackerResponse(datatrackerResponse map[string]interface{}) TrackerResponse { + var result TrackerResponse + if v, ok := datatrackerResponse["failure reason"].(string); ok && len(v) > 0 { + panic(errors.New(v)) + } + result.MinInterval, _ = datatrackerResponse["min interval"].(int) + result.Interval, _ = datatrackerResponse["interval"].(int) + result.Seeders, _ = datatrackerResponse["complete"].(int) + result.Leechers, _ = datatrackerResponse["incomplete"].(int) + return result + +} diff --git a/ratiospoof/ratiospoof.go b/ratiospoof/ratiospoof.go deleted file mode 100644 index cf2c4e7..0000000 --- a/ratiospoof/ratiospoof.go +++ /dev/null @@ -1,486 +0,0 @@ -package ratiospoof - -import ( - "bytes" - "compress/gzip" - "errors" - "fmt" - "io/ioutil" - "math" - "math/rand" - "net/http" - "os" - "os/signal" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "github.com/ap-pauloafonso/ratio-spoof/bencode" - "github.com/gammazero/deque" -) - -const ( - maxAnnounceHistory = 10 -) - -var validInitialSufixes = [...]string{"%", "b", "kb", "mb", "gb", "tb"} -var validSpeedSufixes = [...]string{"kbps", "mbps"} - -type ratioSpoofState struct { - mutex *sync.Mutex - httpClient HttpClient - torrentInfo *bencode.TorrentInfo - input *inputParsed - trackerState *httpTracker - bitTorrentClient TorrentClientEmulation - currentAnnounceTimer int - announceInterval int - numWant int - seeders int - leechers int - announceCount int - status string - announceHistory announceHistory - lastAnounceRequest string - lastTackerResponse string - retryAttempt int -} -type httpTracker struct { - urls []string -} - -func newHttpTracker(torrentInfo *bencode.TorrentInfo) (*httpTracker, error) { - - var result []string - for _, url := range torrentInfo.TrackerInfo.Urls { - if strings.HasPrefix(url, "http") { - result = append(result, url) - } - } - if len(result) == 0 { - return nil, errors.New("No tcp/http tracker url announce found") - } - return &httpTracker{urls: torrentInfo.TrackerInfo.Urls}, nil -} - -func (T *httpTracker) SwapFirst(currentIdx int) { - aux := T.urls[0] - T.urls[0] = T.urls[currentIdx] - T.urls[currentIdx] = aux -} - -type InputArgs struct { - TorrentPath string - InitialDownloaded string - DownloadSpeed string - InitialUploaded string - UploadSpeed string - Port int - Debug bool -} - -type inputParsed struct { - torrentPath string - initialDownloaded int - downloadSpeed int - initialUploaded int - uploadSpeed int - port int - debug bool -} - -func (I *InputArgs) parseInput(torrentInfo *bencode.TorrentInfo) (*inputParsed, error) { - downloaded, err := extractInputInitialByteCount(I.InitialDownloaded, torrentInfo.TotalSize, true) - if err != nil { - return nil, err - } - uploaded, err := extractInputInitialByteCount(I.InitialUploaded, torrentInfo.TotalSize, false) - if err != nil { - return nil, err - } - downloadSpeed, err := extractInputByteSpeed(I.DownloadSpeed) - if err != nil { - return nil, err - } - uploadSpeed, err := extractInputByteSpeed(I.UploadSpeed) - if err != nil { - return nil, err - } - - if I.Port < 1 || I.Port > 65535 { - return nil, errors.New("port number must be between 1 and 65535") - } - - return &inputParsed{initialDownloaded: downloaded, - downloadSpeed: downloadSpeed, - initialUploaded: uploaded, - uploadSpeed: uploadSpeed, - debug: I.Debug, - port: I.Port, - }, nil -} - -func NewRatioSPoofState(input InputArgs, torrentClient TorrentClientEmulation, httpclient HttpClient) (*ratioSpoofState, error) { - - dat, err := ioutil.ReadFile(input.TorrentPath) - if err != nil { - return nil, err - } - - torrentInfo, err := bencode.TorrentDictParse(dat) - if err != nil { - panic(err) - } - - httpTracker, err := newHttpTracker(torrentInfo) - if err != nil { - panic(err) - } - - inputParsed, err := input.parseInput(torrentInfo) - if err != nil { - panic(err) - } - - return &ratioSpoofState{ - bitTorrentClient: torrentClient, - httpClient: httpclient, - torrentInfo: torrentInfo, - trackerState: httpTracker, - input: inputParsed, - numWant: 200, - status: "started", - mutex: &sync.Mutex{}, - }, nil -} - -func checkSpeedSufix(input string) (valid bool, suffix string) { - for _, v := range validSpeedSufixes { - - if strings.HasSuffix(strings.ToLower(input), v) { - return true, input[len(input)-4:] - } - } - return false, "" -} - -func extractInputInitialByteCount(initialSizeInput string, totalBytes int, errorIfHigher bool) (int, error) { - byteCount := strSize2ByteSize(initialSizeInput, totalBytes) - if errorIfHigher && byteCount > totalBytes { - return 0, errors.New("initial downloaded can not be higher than the torrent size") - } - if byteCount < 0 { - return 0, errors.New("initial value can not be negative") - } - return byteCount, nil -} -func extractInputByteSpeed(initialSpeedInput string) (int, error) { - ok, suffix := checkSpeedSufix(initialSpeedInput) - if !ok { - return 0, fmt.Errorf("speed must be in %v", validSpeedSufixes) - } - number, _ := strconv.ParseFloat(initialSpeedInput[:len(initialSpeedInput)-4], 64) - if number < 0 { - return 0, errors.New("speed can not be negative") - } - - if suffix == "kbps" { - number *= 1024 - } else { - number = number * 1024 * 1024 - } - ret := int(number) - return ret, nil -} - -type trackerResponse struct { - minInterval int - interval int - seeders int - leechers int -} - -type TorrentClientEmulation interface { - PeerID() string - Key() string - Query() string - Name() string - Headers() map[string]string - NextAmountReport(DownloadCandidateNextAmount, UploadCandidateNextAmount, leftCandidateNextAmount, pieceSize int) (downloaded, uploaded, left int) -} -type HttpClient interface { - Do(req *http.Request) (*http.Response, error) -} - -type announceEntry struct { - count int - downloaded int - percentDownloaded float32 - uploaded int - left int -} - -type announceHistory struct { - deque.Deque -} - -func (A *announceHistory) pushValueHistory(value announceEntry) { - if A.Len() >= maxAnnounceHistory { - A.PopFront() - } - A.PushBack(value) -} - -func (R *ratioSpoofState) gracefullyExit() { - fmt.Printf("\nGracefully exiting...\n") - R.status = "stopped" - R.numWant = 0 - R.fireAnnounce(false) -} - -func (R *ratioSpoofState) Run() { - rand.Seed(time.Now().UnixNano()) - sigCh := make(chan os.Signal) - stopPrintCh := make(chan string) - - signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - R.firstAnnounce() - go R.decreaseTimer() - go R.PrintState(stopPrintCh) - go func() { - for { - R.generateNextAnnounce() - time.Sleep(time.Duration(R.announceInterval) * time.Second) - R.fireAnnounce(true) - } - }() - <-sigCh - stopPrintCh <- "exit print" - R.gracefullyExit() -} -func (R *ratioSpoofState) firstAnnounce() { - println("Trying to connect to the tracker...") - R.addAnnounce(R.input.initialDownloaded, R.input.initialUploaded, calculateBytesLeft(R.input.initialDownloaded, R.torrentInfo.TotalSize), (float32(R.input.initialDownloaded)/float32(R.torrentInfo.TotalSize))*100) - R.fireAnnounce(false) -} - -func (R *ratioSpoofState) updateInterval(resp trackerResponse) { - if resp.interval > 0 { - R.announceInterval = resp.interval - } else { - R.announceInterval = 1800 - } -} - -func (R *ratioSpoofState) updateSeedersAndLeechers(resp trackerResponse) { - R.seeders = resp.seeders - R.leechers = resp.leechers -} -func (R *ratioSpoofState) addAnnounce(currentDownloaded, currentUploaded, currentLeft int, percentDownloaded float32) { - R.announceCount++ - R.announceHistory.pushValueHistory(announceEntry{count: R.announceCount, downloaded: currentDownloaded, uploaded: currentUploaded, left: currentLeft, percentDownloaded: percentDownloaded}) -} -func (R *ratioSpoofState) fireAnnounce(retry bool) { - lastAnnounce := R.announceHistory.Back().(announceEntry) - replacer := strings.NewReplacer("{infohash}", R.torrentInfo.InfoHashURLEncoded, - "{port}", fmt.Sprint(R.input.port), - "{peerid}", R.bitTorrentClient.PeerID(), - "{uploaded}", fmt.Sprint(lastAnnounce.uploaded), - "{downloaded}", fmt.Sprint(lastAnnounce.downloaded), - "{left}", fmt.Sprint(lastAnnounce.left), - "{key}", R.bitTorrentClient.Key(), - "{event}", R.status, - "{numwant}", fmt.Sprint(R.numWant)) - query := replacer.Replace(R.bitTorrentClient.Query()) - - var trackerResp *trackerResponse - if retry { - retryDelay := 30 * time.Second - for { - exit := false - func() { - defer func() { - if err := recover(); err != nil { - R.changeCurrentTimer(int(retryDelay.Seconds())) - R.retryAttempt++ - time.Sleep(retryDelay) - retryDelay *= 2 - if retryDelay.Seconds() > 900 { - retryDelay = 900 - } - } - }() - trackerResp = R.tryMakeRequest(query) - exit = true - }() - if exit { - break - } - } - - } else { - trackerResp = R.tryMakeRequest(query) - } - R.retryAttempt = 0 - if trackerResp != nil { - R.updateSeedersAndLeechers(*trackerResp) - R.updateInterval(*trackerResp) - } -} -func (R *ratioSpoofState) generateNextAnnounce() { - R.changeCurrentTimer(R.announceInterval) - lastAnnounce := R.announceHistory.Back().(announceEntry) - currentDownloaded := lastAnnounce.downloaded - var downloadCandidate int - - if currentDownloaded < R.torrentInfo.TotalSize { - downloadCandidate = calculateNextTotalSizeByte(R.input.downloadSpeed, currentDownloaded, R.torrentInfo.PieceSize, R.currentAnnounceTimer, R.torrentInfo.TotalSize) - } else { - downloadCandidate = R.torrentInfo.TotalSize - } - - currentUploaded := lastAnnounce.uploaded - uploadCandidate := calculateNextTotalSizeByte(R.input.uploadSpeed, currentUploaded, R.torrentInfo.PieceSize, R.currentAnnounceTimer, 0) - - leftCandidate := calculateBytesLeft(downloadCandidate, R.torrentInfo.TotalSize) - - d, u, l := R.bitTorrentClient.NextAmountReport(downloadCandidate, uploadCandidate, leftCandidate, R.torrentInfo.PieceSize) - - R.addAnnounce(d, u, l, (float32(d)/float32(R.torrentInfo.TotalSize))*100) -} -func (R *ratioSpoofState) decreaseTimer() { - for { - time.Sleep(1 * time.Second) - R.mutex.Lock() - if R.currentAnnounceTimer > 0 { - R.currentAnnounceTimer-- - } - R.mutex.Unlock() - } -} - -func (R *ratioSpoofState) changeCurrentTimer(newAnnounceRate int) { - R.mutex.Lock() - R.currentAnnounceTimer = newAnnounceRate - R.mutex.Unlock() -} - -func (R *ratioSpoofState) tryMakeRequest(query string) *trackerResponse { - for idx, baseUrl := range R.trackerState.urls { - completeURL := buildFullUrl(baseUrl, query) - R.lastAnounceRequest = completeURL - req, _ := http.NewRequest("GET", completeURL, nil) - for header, value := range R.bitTorrentClient.Headers() { - req.Header.Add(header, value) - } - resp, err := R.httpClient.Do(req) - if err == nil { - if resp.StatusCode == http.StatusOK { - bytesR, _ := ioutil.ReadAll(resp.Body) - if len(bytesR) == 0 { - return nil - } - mimeType := http.DetectContentType(bytesR) - if mimeType == "application/x-gzip" { - gzipReader, _ := gzip.NewReader(bytes.NewReader(bytesR)) - bytesR, _ = ioutil.ReadAll(gzipReader) - gzipReader.Close() - } - R.lastTackerResponse = string(bytesR) - decodedResp := bencode.Decode(bytesR) - if idx != 0 { - R.trackerState.SwapFirst(idx) - } - ret := extractTrackerResponse(decodedResp) - return &ret - } - resp.Body.Close() - } - } - panic("Connection error with the tracker") - -} - -func buildFullUrl(baseurl, query string) string { - if len(strings.Split(baseurl, "?")) > 1 { - return baseurl + "&" + strings.TrimLeft(query, "&") - } - return baseurl + "?" + strings.TrimLeft(query, "?") -} - -func calculateNextTotalSizeByte(speedBytePerSecond, currentByte, pieceSizeByte, seconds, limitTotalBytes int) int { - if speedBytePerSecond == 0 { - return currentByte - } - totalCandidate := currentByte + (speedBytePerSecond * seconds) - randomPieces := rand.Intn(10-1) + 1 - totalCandidate = totalCandidate + (pieceSizeByte * randomPieces) - - if limitTotalBytes != 0 && totalCandidate > limitTotalBytes { - return limitTotalBytes - } - return totalCandidate -} - -func extractTrackerResponse(datatrackerResponse map[string]interface{}) trackerResponse { - var result trackerResponse - if v, ok := datatrackerResponse["failure reason"].(string); ok && len(v) > 0 { - panic(errors.New(v)) - } - result.minInterval, _ = datatrackerResponse["min interval"].(int) - result.interval, _ = datatrackerResponse["interval"].(int) - result.seeders, _ = datatrackerResponse["complete"].(int) - result.leechers, _ = datatrackerResponse["incomplete"].(int) - return result - -} -func calculateBytesLeft(currentBytes, totalBytes int) int { - return totalBytes - currentBytes -} - -func strSize2ByteSize(input string, totalSize int) int { - lowerInput := strings.ToLower(input) - - parseStrNumberFn := func(strWithSufix string, sufixLength, n int) int { - v, _ := strconv.ParseFloat(strWithSufix[:len(lowerInput)-sufixLength], 64) - result := v * math.Pow(1024, float64(n)) - return int(result) - } - switch { - case strings.HasSuffix(lowerInput, "kb"): - { - return parseStrNumberFn(lowerInput, 2, 1) - } - case strings.HasSuffix(lowerInput, "mb"): - { - return parseStrNumberFn(lowerInput, 2, 2) - } - case strings.HasSuffix(lowerInput, "gb"): - { - return parseStrNumberFn(lowerInput, 2, 3) - } - case strings.HasSuffix(lowerInput, "tb"): - { - return parseStrNumberFn(lowerInput, 2, 4) - } - case strings.HasSuffix(lowerInput, "b"): - { - return parseStrNumberFn(lowerInput, 1, 0) - } - case strings.HasSuffix(lowerInput, "%"): - { - v, _ := strconv.ParseFloat(lowerInput[:len(lowerInput)-1], 64) - if v < 0 || v > 100 { - panic("percent value must be in (0-100)") - } - result := int(float64(v/100) * float64(totalSize)) - - return result - } - - default: - panic("Size not found") - } -}