diff --git a/.gitignore b/.gitignore index 0a1a487..2561303 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ dmypy.json #vscode folder /.vscode + +out/ \ No newline at end of file diff --git a/README.md b/README.md index e70a55d..001b4fd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ratio-spoof Ratio-spoof is a cross-platform, free and open source tool to spoof the download/upload amount on private bittorrent trackers. -![](./media/demo.gif) +![](./assets/demo.gif) ## Motivation Here in brazil, not everybody has a great upload speed, and most of the private trackers requires a ratio to be greater than or equal to 1 (e.g. if you downloaded 1gb you must upload 1gb as well) in order to survive. Plus, i have always been fascinated by the bittorrent protocol, [i even made a bittorrent webclient to learn a bit about it ](https://github.com/ap-pauloafonso/rwTorrent) so with the current global covid-19 lockdown i got some free time and decided to code my own simple cli tool to spoof bittorrent trackers. @@ -12,35 +12,33 @@ Bittorrent protocol works in such a way that there is no way that a tracker know Ratio-spoof acts like a normal bittorrent client but without downloading or uploading anything, in fact it just tricks the tracker pretending that. ## Usage -With a recent python3 version installed, you will be able to run it on linux/macos/windows. ``` -usage: ratio-spoof.py -t -d -u - +usage: + ./ratio-spoof -t -d -ds -u -us optional arguments: - -h, --help show this help message and exit - -p [PORT] change the port number, the default is 8999 - - + -h show this help message and exit + -p [PORT] change the port number, the default is 8999 + required arguments: - -t path .torrent file - -d - required download arg values - -u - required upload arg values - + -t + -d + -ds + -u + -us + and must be in %, b, kb, mb, gb, tb and must be in kbps ``` ``` -./ratio-spoof -d 90% 100kbps -u 0% 1024kbps -t (torrentfile_path) +./ratio-spoof -d 90% -ds 100kbps -u 0% -us 1024kbps -t (torrentfile_path) ``` * Will start "downloading" with the initial value of 90% of the torrent total size at 100 kbps speed until it reaches 100% mark. * Will start "uploading" with the initial value of 0% of the torrent total size at 1024kbps (aka 1mb/s) indefinitely. ``` -./ratio-spoof -d 2gb 500kbps -u 1gb 1024kbps -t (torrentfile_path) +./ratio-spoof -d 2gb -ds 500kbps -u 1gb -us 1024kbps -t (torrentfile_path) ``` * Will start "downloading" with the initial value of 2gb downloaded if possible at 500kbps speed until it reaches 100% mark. * Will start "uploading" with the initial value of 1gb uplodead at 1024kbps (aka 1mb/s) indefinitely. diff --git a/media/demo.gif b/assets/demo.gif similarity index 100% rename from media/demo.gif rename to assets/demo.gif diff --git a/beencode/beencode.go b/beencode/beencode.go new file mode 100644 index 0000000..9654659 --- /dev/null +++ b/beencode/beencode.go @@ -0,0 +1,86 @@ +package beencode + +import ( + "strconv" +) + +const ( + dictToken = byte('d') + numberToken = byte('i') + listToken = byte('l') + endOfCollectionToken = byte('e') + lengthValueStringSeparator = byte(':') +) + +//Decode accepts a byte slice and returns a map with information parsed.(panic if it fails) +func Decode(data []byte) map[string]interface{} { + result, _ := findParse(0, &data) + return result.(map[string]interface{}) +} + +func findParse(currentIdx int, data *[]byte) (result interface{}, nextIdx int) { + token := (*data)[currentIdx : currentIdx+1][0] + switch { + case token == dictToken: + return mapParse(currentIdx, data) + case token == numberToken: + return numberParse(currentIdx, data) + case token == listToken: + return listParse(currentIdx, data) + case token >= byte('0') || token <= byte('9'): + return stringParse(currentIdx, data) + default: + panic("Error decoding beencode") + } +} + +func mapParse(startIdx int, data *[]byte) (result map[string]interface{}, nextIdx int) { + result = make(map[string]interface{}) + initialMapIndex := startIdx + current := startIdx + 1 + for (*data)[current : current+1][0] != endOfCollectionToken { + mapKey, next := findParse(current, data) + current = next + mapValue, next := findParse(current, data) + current = next + result[mapKey.(string)] = mapValue + } + current++ + result["byte_offsets"] = []int{initialMapIndex, current} + nextIdx = current + return +} + +func listParse(startIdx int, data *[]byte) (result []interface{}, nextIdx int) { + current := startIdx + 1 + for (*data)[current : current+1][0] != endOfCollectionToken { + value, next := findParse(current, data) + result = append(result, value) + current = next + } + current++ + nextIdx = current + return +} + +func numberParse(startIdx int, data *[]byte) (result int, nextIdx int) { + current := startIdx + for (*data)[current : current+1][0] != endOfCollectionToken { + current++ + } + value, _ := strconv.Atoi(string((*data)[startIdx+1 : current])) + result = value + nextIdx = current + 1 + return +} + +func stringParse(startIdx int, data *[]byte) (result string, nextIdx int) { + current := startIdx + for (*data)[current : current+1][0] != lengthValueStringSeparator { + current++ + } + sizeStr, _ := strconv.Atoi(string(((*data)[startIdx:current]))) + result = string((*data)[current+1 : current+1+int(sizeStr)]) + nextIdx = current + 1 + int(sizeStr) + return +} diff --git a/beencode/beencode_test.go b/beencode/beencode_test.go new file mode 100644 index 0000000..1197d5b --- /dev/null +++ b/beencode/beencode_test.go @@ -0,0 +1,113 @@ +package beencode + +import ( + "io/ioutil" + "log" + "reflect" + "testing" +) + +func assertAreEqual(t *testing.T, got, want interface{}) { + t.Helper() + if got != want { + t.Errorf("got: %v want: %v", got, want) + } +} +func assertAreEqualDeep(t *testing.T, got, want interface{}) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Errorf("got: %v want: %v", got, want) + } +} + +func TestNumberParse(T *testing.T) { + + T.Run("Positive number", func(t *testing.T) { + input := []byte("i322ed:5:") + gotValue, gotNextIdx := numberParse(0, &input) + wantValue, wantNextIdx := 322, 5 + + assertAreEqual(t, gotValue, wantValue) + assertAreEqual(t, gotNextIdx, wantNextIdx) + + }) + T.Run("Negative number", func(t *testing.T) { + input := []byte("i-322ed:5:") + gotValue, gotNextIdx := numberParse(0, &input) + wantValue, wantNextIdx := -322, 6 + + assertAreEqual(t, gotValue, wantValue) + assertAreEqual(t, gotNextIdx, wantNextIdx) + }) +} + +func TestStringParse(T *testing.T) { + + T.Run("String test 1", func(t *testing.T) { + input := []byte("5:color4:blue") + gotValue, gotNextIdx := stringParse(0, &input) + wantValue, wantNextIdx := "color", 7 + + assertAreEqual(t, gotValue, wantValue) + assertAreEqual(t, gotNextIdx, wantNextIdx) + + }) + T.Run("String test 2", func(t *testing.T) { + input := []byte("15:metallica_rocksd:4:color") + gotValue, gotNextIdx := stringParse(0, &input) + wantValue, wantNextIdx := "metallica_rocks", 18 + + assertAreEqual(t, gotValue, wantValue) + assertAreEqual(t, gotNextIdx, wantNextIdx) + }) +} + +func TestListParse(T *testing.T) { + T.Run("list of strings", func(t *testing.T) { + input := []byte("l4:spam4:eggsed:5color") + gotValue, gotNextIdx := listParse(0, &input) + var wantValue []interface{} + wantValue = append(wantValue, "spam", "eggs") + wantNextIdx := 14 + assertAreEqualDeep(t, gotValue, wantValue) + assertAreEqual(t, gotNextIdx, wantNextIdx) + }) + T.Run("list of numbers", func(t *testing.T) { + input := []byte("li322ei400eed:5color") + gotValue, gotNextIdx := listParse(0, &input) + var wantValue []interface{} + wantValue = append(wantValue, 322, 400) + wantNextIdx := 12 + assertAreEqualDeep(t, gotValue, wantValue) + assertAreEqual(t, gotNextIdx, wantNextIdx) + }) +} + +func TestMapParse(T *testing.T) { + T.Run("map with string and list inside", func(t *testing.T) { + input := []byte("d13:favorite_band4:tool6:othersl5:qotsaee5:color") + gotValue, gotNextIdx := mapParse(0, &input) + wantValue := make(map[string]interface{}) + wantValue["favorite_band"] = "tool" + wantValue["others"] = []interface{}{"qotsa"} + wantValue["byte_offsets"] = []int{0, 41} + wantNextIdx := 41 + assertAreEqualDeep(t, gotValue, wantValue) + assertAreEqual(t, gotNextIdx, wantNextIdx) + }) +} + +func TestDecode(T *testing.T) { + + files, err := ioutil.ReadDir("./torrent_files_test") + if err != nil { + log.Fatal(err) + } + for _, f := range files { + T.Run(f.Name(), func(t *testing.T) { + data, _ := ioutil.ReadFile("./torrent_files_test/" + f.Name()) + t.Log(Decode(data)["info"].(map[string]interface{})["name"]) + }) + } + +} diff --git a/beencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent b/beencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent new file mode 100644 index 0000000..bd45cf0 Binary files /dev/null and b/beencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent differ diff --git a/beencode/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/beencode/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 new file mode 100644 index 0000000..98b6c9a Binary files /dev/null and b/beencode/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 differ diff --git a/beencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent b/beencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent new file mode 100644 index 0000000..3dcd393 Binary files /dev/null and b/beencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent differ diff --git a/bencode_parser.py b/bencode_parser.py deleted file mode 100644 index 43d73d7..0000000 --- a/bencode_parser.py +++ /dev/null @@ -1,92 +0,0 @@ -import socket -import struct -import sys -from enum import Enum -import os -import json -import hashlib -import os -import urllib.parse - - -class BencodeKeys(Enum): - dic = 'd' - number= 'i' - arr = 'l' - end_of_collecion = 'e' - length_and_value_string_separator = ':' - - -def string_parse(startIdx, data:bytes): - current = startIdx - while data[current:current +1].decode() != BencodeKeys.length_and_value_string_separator.value: - current= current + 1 - size = data[startIdx:current].decode() - string_nextidx = current+1 + int(size) - data_slice = data[current+1:current+1 + int(size)] - return (str(data_slice, 'utf-8', 'replace'), string_nextidx) - -def number_parse(startIdx, data:bytes): - current = startIdx - while data[current:current +1].decode() != BencodeKeys.end_of_collecion.value: - current = current +1 - number_nextidx = current +1 - data_slice = data[startIdx +1:current] - return (int(data_slice), number_nextidx) - -def find_parse(startIdx,data:bytes): - c = data[startIdx:startIdx +1].decode() - if(c == BencodeKeys.number.value): - return number_parse(startIdx, data) - elif(c == BencodeKeys.dic.value): - return dic_parse(startIdx,data) - elif(c == BencodeKeys.arr.value): - return list_parse(startIdx,data) - elif(str(c).isdigit()): - return string_parse(startIdx, data) - else: - raise Exception('Error parse') - - -def list_parse(startIdx, data): - result = [] - current = startIdx +1 - while current < len(data): - value, nextIdx = find_parse(current, data) - result.append(value) - current = nextIdx - if (data[current: current+1].decode()== BencodeKeys.end_of_collecion.value): - current = current +1 - break - list_nextidx = current - return (result, list_nextidx) - - -def dic_parse(startIdx,data): - dic = {} - initial_dict_idx = startIdx - current = startIdx +1 - - while current < len(data): - key, nextIdx = find_parse(current, data) - current = nextIdx - value,nextIdx = find_parse(current, data) - dic[key] = value - current = nextIdx - if (data[current: current+1].decode()==BencodeKeys.end_of_collecion.value): - current = current +1 - final_dict_idx = current - dic['byte_offsets'] = [initial_dict_idx,final_dict_idx] - break - dic_nextidx = current - return dic, dic_nextidx - - -def decode(data:bytes): - result,_ = find_parse(0,data) - return result - - - - - diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..7f561ab --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +env GOOS=darwin GOARCH=amd64 go build -v -o ./out/mac/ratio-spoof github.com/ap-pauloafonso/ratio-spoof/cmd +env GOOS=linux GOARCH=amd64 go build -v -o ./out/linux/ratio-spoof github.com/ap-pauloafonso/ratio-spoof/cmd +env GOOS=windows GOARCH=amd64 go build -v -o ./out/windows/ratio-spoof.exe github.com/ap-pauloafonso/ratio-spoof/cmd \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..ba15d61 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "flag" + "fmt" + "net/http" + "runtime" + + "github.com/ap-pauloafonso/ratio-spoof/qbittorrent" + "github.com/ap-pauloafonso/ratio-spoof/ratiospoof" +) + +func main() { + + //required + torrentPath := flag.String("t", "", "torrent path") + initialDownload := flag.String("d", "", "a INITIAL_DOWNLOADED") + downloadSpeed := flag.String("ds", "", "a DOWNLOAD_SPEED") + initialUpload := flag.String("u", "", "a INITIAL_UPLOADED") + uploadSpeed := flag.String("us", "", "a UPLOAD_SPEED") + + //optional + port := flag.Int("p", 8999, "a PORT") + debug := flag.Bool("debug", false, "") + + flag.Usage = func() { + var osExecutableSuffix string + if runtime.GOOS == "windows" { + fmt.Println(`usage: + ratio-spoof.exe -t -d -ds -u -us `, osExecutableSuffix) + } else { + fmt.Println(`usage: + ./ratio-spoof -t -d -ds -u -us `, osExecutableSuffix) + } + + fmt.Print(` +optional arguments: + -h show this help message and exit + -p [PORT] change the port number, the default is 8999 + +required arguments: + -t + -d + -ds + -u + -us + + and must be in %, b, kb, mb, gb, tb + and must be in kbps +`) + } + + flag.Parse() + + if *torrentPath == "" || *initialDownload == "" || *downloadSpeed == "" || *initialUpload == "" || *uploadSpeed == "" { + flag.Usage() + return + } + + qbit := qbittorrent.NewQbitTorrent() + r, err := ratiospoof.NewRatioSPoofState( + ratiospoof.InputArgs{ + TorrentPath: *torrentPath, + InitialDownloaded: *initialDownload, + DownloadSpeed: *downloadSpeed, + InitialUploaded: *initialUpload, + UploadSpeed: *uploadSpeed, + Port: *port, + Debug: *debug, + }, + qbit, + http.DefaultClient) + + if err != nil { + panic(err) + } + r.Run() + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f59dbeb --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/ap-pauloafonso/ratio-spoof + +go 1.15 + +require ( + github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc + github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 + golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f844619 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc h1:F7BbnLACph7UYiz9ZHi6npcROwKaZUyviDjsNERsoMM= +github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc/go.mod h1:IlBLfYXnuw9sspy1XS6ctu5exGb6WHGKQsyo4s7bOEA= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/qbittorrent/qbittorrent.go b/qbittorrent/qbittorrent.go new file mode 100644 index 0000000..5fc5f03 --- /dev/null +++ b/qbittorrent/qbittorrent.go @@ -0,0 +1,76 @@ +package qbittorrent + +import ( + "encoding/hex" + "math/rand" + "strings" + "time" +) + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" + name = "qBittorrent v4.03" + query = "info_hash={infohash}&peer_id={peerid}&port={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&key={key}&event={event}&numwant={numwant}&compact=1&no_peer_id=1&supportcrypto=1&redundant=0" +) + +type TypeTest struct { +} + +type qbitTorrent struct { + name string + query string + dictHeaders map[string]string + key string + peerID string +} + +func NewQbitTorrent() *qbitTorrent { + return &qbitTorrent{ + name: name, + query: query, + dictHeaders: generateHeaders(), + key: generateKey(), + peerID: generatePeerID(), + } +} + +func (qb *qbitTorrent) Name() string { + return qb.name +} +func (qb *qbitTorrent) PeerID() string { + return qb.peerID +} + +func (qb *qbitTorrent) Key() string { + return qb.key +} + +func (qb *qbitTorrent) Query() string { + return query +} +func (qb *qbitTorrent) Headers() map[string]string { + return qb.dictHeaders +} + +func generateHeaders() map[string]string { + return map[string]string{"User-Agent": "qBittorrent/4.0.3", "Accept-Encoding": "gzip"} +} + +func generatePeerID() string { + return "-qB4030-" + randStringBytes(12) +} + +func randStringBytes(n int) string { + rand.Seed(time.Now().UnixNano()) + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +} +func generateKey() string { + randomBytes := make([]byte, 4) + rand.Read(randomBytes) + str := hex.EncodeToString(randomBytes) + return strings.ToUpper(str) +} diff --git a/qbittorrent/qbittorrent_test.go b/qbittorrent/qbittorrent_test.go new file mode 100644 index 0000000..7826109 --- /dev/null +++ b/qbittorrent/qbittorrent_test.go @@ -0,0 +1,57 @@ +package qbittorrent + +import ( + "strings" + "testing" +) + +func TestGenerateRandomPeerId(T *testing.T) { + T.Run("PeerIds are different", func(t *testing.T) { + keys := make(map[string]bool) + for i := 0; i < 10; i++ { + obj := NewQbitTorrent() + key := obj.PeerID() + t.Log(key) + if _, ok := keys[key]; ok { + t.Error("peerId must be random") + break + } + keys[key] = true + } + + }) + +} + +func TestGenerateRandomKey(T *testing.T) { + T.Run("Keys are different", func(t *testing.T) { + keys := make(map[string]bool) + for i := 0; i < 10; i++ { + obj := NewQbitTorrent() + key := obj.Key() + t.Log(key) + if _, ok := keys[key]; ok { + t.Error("Keys must be random") + break + } + keys[key] = true + } + }) + T.Run("Key has 8 length", func(t *testing.T) { + obj := NewQbitTorrent() + key := obj.Key() + if len(key) != 8 { + t.Error("Keys must have length of 8") + } + + }) + T.Run("Key must be uppercase", func(t *testing.T) { + obj := NewQbitTorrent() + key := obj.Key() + if strings.ToUpper(key) != key { + t.Error("Keys must be uppercase") + } + + }) + +} diff --git a/ratio-spoof.py b/ratio-spoof.py deleted file mode 100755 index 6523f94..0000000 --- a/ratio-spoof.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env python3 -import bencode_parser -import sys -import hashlib -import urllib.parse -import json -import random -import base64 -import os -import uuid -import argparse -import time -from collections import deque -import subprocess -import platform -import datetime -import threading -import urllib.request -import http.client -import gzip -import shutil - - - -class RatioSpoofState(): - def __init__(self, torrent_name,download_speed, upload_speed, \ - announce_rate, current_downloaded, current_uploaded,\ - piece_size, total_size, announce_info, info_hash_urlencoded, port): - self.__lock = threading.Lock() - self.torrent_name = torrent_name - self.download_speed = download_speed - self.upload_speed = upload_speed - self.announce_rate = announce_rate - self.announce_current_timer = self.announce_rate - self.piece_size = piece_size - self.total_size = total_size - self.announce_info = announce_info - self.peer_id = peer_id() - self.key = key() - self.info_hash_urlencoded = info_hash_urlencoded - self.announce_history_deq = deque(maxlen=10) - self.deq_count = 0 - self.numwant = 200 - self.seeders = None - self.leechers = None - self.port = port - self.__add_announce(current_downloaded, current_uploaded ,next_announce_left_b(current_downloaded, total_size)) - - def start_announcing(self): - announce_interval = self.__announce('started') - threading.Thread(daemon = True, target = (lambda: self.__decrease_timer())).start() - threading.Thread(daemon = True, target = (lambda: self.__print_state())).start() - while True: - self.__generate_next_announce(announce_interval) - time.sleep(announce_interval) - self.__announce() - - def __add_announce(self, current_downloaded, current_uploaded, left): - self.deq_count +=1 - self.announce_history_deq.append({'count': self.deq_count, 'downloaded':current_downloaded, 'percent': round((current_downloaded/self.total_size) *100) , 'uploaded':current_uploaded,'left': left }) - - def __generate_next_announce(self, announce_rate): - self.__reset_timer(announce_rate) - current_downloaded = self.announce_history_deq[-1]['downloaded'] - if(self.announce_history_deq[-1]['downloaded'] < self.total_size): - current_downloaded = next_announce_total_b(self.download_speed,self.announce_history_deq[-1]['downloaded'], self.piece_size, self.announce_rate, self.total_size) - else: - self.numwant = 0 - current_uploaded = next_announce_total_b(self.upload_speed,self.announce_history_deq[-1]['uploaded'], self.piece_size, self.announce_rate) - current_left = next_announce_left_b(current_downloaded, self.total_size) - self.__add_announce(current_downloaded,current_uploaded,current_left) - - def __announce(self, event = None): - last_announce_data = self.announce_history_deq[-1] - query_dict = build_query_string(self, last_announce_data, event, self.port) - - error ='' - - if (len(self.announce_info['list_of_lists']) > 0): - for tier_list in self.announce_info['list_of_lists']: - for item in tier_list: - try: - announce_response = tracker_announce_request(item, query_dict) - self.__update_seeders_and_leechers(announce_response) - return announce_response['interval'] - except Exception as e : error = str(e) - - else: - url = self.announce_info['main'] - try: - announce_response = tracker_announce_request(url, query_dict) - self.__update_seeders_and_leechers(announce_response) - return announce_response['interval'] - except Exception as e : error = str(e) - - raise Exception(f'Connection error with the tracker: {error}') - - def __update_seeders_and_leechers(self, dict): - self.seeders = dict['seeders'] - self.leechers = dict['leechers'] - - def __decrease_timer(self): - while True: - time.sleep(1) - with self.__lock: - self.announce_current_timer = self.announce_current_timer - 1 if self.announce_current_timer > 0 else 0 - - def __reset_timer(self, new_announce_rate = None): - if new_announce_rate != None: - self.announce_rate = new_announce_rate - with self.__lock: - self.announce_current_timer = self.announce_rate - - def __print_state(self): - while True: - clear_screen() - print(' RATIO-SPOOF '.center(shutil.get_terminal_size().columns,'#')) - print(f""" - Torrent: {self.torrent_name} - Tracker: {self.announce_info['main']} - Seeders: {self.seeders if self.seeders !=None else 'not informed'} - Leechers: {self.leechers if self.leechers !=None else 'not informed'} - Download Speed: {self.download_speed}KB/s - Upload Speed: {self.upload_speed}KB/s - Size: {human_readable_size(self.total_size)} - Emulation: qBittorrent v4.03 | Port: {self.port} - """) - print(' GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF '.center(shutil.get_terminal_size().columns, '#')) - print() - for item in list(self.announce_history_deq)[:len(self.announce_history_deq)-1]: - print(f'#{item["count"]} downloaded: {human_readable_size(item["downloaded"])}({item["percent"]}%) | left: {human_readable_size(item["left"])} | uploaded: {human_readable_size(item["uploaded"])} | announced') - print(f'#{self.announce_history_deq[-1]["count"]} downloaded: {human_readable_size(self.announce_history_deq[-1]["downloaded"])}({self.announce_history_deq[-1]["percent"]}%) | left: {human_readable_size(self.announce_history_deq[-1]["left"])} | uploaded: {human_readable_size(self.announce_history_deq[-1]["uploaded"])} | next announce in :{str(datetime.timedelta(seconds=self.announce_current_timer))}') - time.sleep(1) - - -def human_readable_size(size, decimal_places=2): - for unit in ['B','KiB','MiB','GiB','TiB']: - if size < 1024.0: - break - size /= 1024.0 - return f"{size:.{decimal_places}f}{unit}" - -def clear_screen(): - if platform.system() == "Windows": - subprocess.Popen("cls", shell=True).communicate() - else: - print("\033c", end="") - -def t_total_size(data): - if ('length' in data['info']): - return data['info']['length'] - - return sum(map(lambda x : x['length'] , data['info']['files'])) - -def t_infohash_urlencoded(data, raw_data): - info_offsets= data['info']['byte_offsets'] - info_bytes = hashlib.sha1(raw_data[info_offsets[0]:info_offsets[1]]).digest() - return urllib.parse.quote_plus(info_bytes) - -def t_piecesize_b(data): - return data['info']['piece length'] - -def next_announce_total_b(speed_kbps, b_current, b_piece_size,s_time, b_total_limit = None): - if(speed_kbps == 0): return b_current - - total = b_current + (speed_kbps *1024 *s_time) - closest_piece_number = int(total / b_piece_size) - closest_piece_number = closest_piece_number + random.randint(1,10) - next_announce = closest_piece_number *b_piece_size - - if(b_total_limit is not None and next_announce > b_total_limit): - return b_total_limit - return next_announce - -def next_announce_left_b(b_current, b_total_size): - return b_total_size - b_current - -def peer_id(): - return f'-qB4030-{base64.urlsafe_b64encode(uuid.uuid4().bytes)[:12].decode()}' - -def key(): - return hex(random.getrandbits(32))[2:].upper() - -def find_approx_current(b_total_size, piece_size, percent): - if( percent <= 0): return 0 - total = (percent/100) * b_total_size - current_approx = int(total / piece_size) * piece_size - return current_approx - -def build_announce_info(data): - announce_info = {'main':data['announce'], 'list_of_lists':data['announce-list'] if 'announce-list' in data else []} - tcp_list_of_lists = [] - for _list in announce_info['list_of_lists']: - aux = list(filter(lambda x: x.lower().startswith('http'),_list)) - if len(aux) >0: - tcp_list_of_lists.append(aux) - announce_info['list_of_lists'] = tcp_list_of_lists - if (not announce_info['main'].startswith('udp')): - announce_info['list_of_lists'].insert(-1,[announce_info['main']]) - - if(len(announce_info['list_of_lists']) == 0): raise Exception('No tcp/http tracker url announce found') - - return announce_info - -def tracker_announce_request(url, query_string): - request = urllib.request.Request(url = f'{url}?{query_string}', headers= {'User-Agent' :'qBittorrent/4.0.3', 'Accept-Encoding':'gzip'}) - response = urllib.request.urlopen(request).read() - try: - response = gzip.decompress(response) - except:pass - - decoded_response = bencode_parser.decode(response) - - interval = decoded_response.get('min interval',None) - if(interval is None): - interval = decoded_response.get('interval',None) - - if interval is not None: - return { 'interval': int(decoded_response['interval']), 'seeders': decoded_response.get('complete'), 'leechers': decoded_response.get('incomplete') } - else: raise Exception(json.dumps(decoded_response)) - -def build_query_string(state:RatioSpoofState, curent_info,event, port): - query = { - 'peer_id':state.peer_id, - 'port':port, - 'uploaded':curent_info['uploaded'], - 'downloaded':curent_info['downloaded'], - 'left':curent_info['left'], - 'corrupt': 0, - 'key':state.key, - 'event':event, - 'numwant':state.numwant, - 'compact':1, - 'no_peer_id': 1, - 'supportcrypto':1, - 'redundant':0 - } - - if(event == None): - del(query['event']) - - result = f'info_hash={state.info_hash_urlencoded}&' + urllib.parse.urlencode(query) - return result - -def check_initial_value_suffix(input:str, attribute_name): - valid_suffixs = ('%', 'b','kb','mb','gb','tb') - if not input.lower().endswith(valid_suffixs): - raise Exception(f'initial {attribute_name} must be in {valid_suffixs}') - -def check_speed_value_suffix(input:str, attribute_name): - valid_suffixs = ('kbps') - if not input.lower().endswith(valid_suffixs): - raise Exception(f'{attribute_name} speed must be in {valid_suffixs}') - -def percent_validation(n): - if n not in range (0, 101): - raise Exception ('percent value must be in (0-100)') - -def input_size_2_byte_size(input, total_size ): - if input.lower().endswith('kb'): - return int((float(input[:-2])) * 1024) - elif input.lower().endswith('mb'): - return int((float(input[:-2])) * (1024 **2)) - elif input.lower().endswith('gb'): - return int((float(input[:-2])) * (1024 **3)) - elif input.lower().endswith('tb'): - return int((float(input[:-2])) * (1024 **4)) - elif input.lower().endswith('b'): - return int(float(input[:-1])) - elif input.endswith('%'): - percent_validation(int(float(input[:-1]))) - return int((float(input[:-1])/100 ) * total_size) - else: - raise Exception('Size not found') - -def check_downloaded_initial_value(input, total_size_b): - size_b =input_size_2_byte_size(input,total_size_b) - if size_b > total_size_b: - raise Exception('initial downloaded can not be higher than the torrent size') - return size_b - -def check_uploaded_initial_value(input, total_size_b): - size_b =input_size_2_byte_size(input, total_size_b) - return size_b - -def check_speed(input): - return int(float(input[:-4])) - - -def validate_download_args(downloaded_arg, download_speed_arg,total_size_b): - check_initial_value_suffix(downloaded_arg,'download') - check_speed_value_suffix(download_speed_arg, 'download') - - donwloaded_b = check_downloaded_initial_value(downloaded_arg, total_size_b) - speed_kbps = check_speed(download_speed_arg) - - return (donwloaded_b, speed_kbps) - -def validate_upload_args(uploaded_arg, upload_speed_arg, total_size_b): - check_initial_value_suffix(uploaded_arg,'upload') - check_speed_value_suffix(upload_speed_arg, 'upload') - - uploaded_b = check_uploaded_initial_value(uploaded_arg,total_size_b ) - speed_kbps = check_speed(upload_speed_arg) - return (uploaded_b, speed_kbps) - - -def read_file(f, args_download, args_upload, port): - raw_data = f.read() - result = bencode_parser.decode(raw_data) - total_size = t_total_size(result) - piece_size = t_piecesize_b(result) - - downloaded, download_speed_kbps = validate_download_args(args_download[0], args_download[1], total_size) - uploaded_b, upload_speed_kbps = validate_upload_args(args_upload[0], args_upload[1], total_size) - - state = RatioSpoofState(result['info']['name'],download_speed_kbps,upload_speed_kbps,0,\ - downloaded, uploaded_b, piece_size,total_size, - build_announce_info(result),t_infohash_urlencoded(result, raw_data), port) - - state.start_announcing() - - -tip = """ - and must be in %, b, kb, mb, gb, tb - and must be in kbps -""" -parser = argparse.ArgumentParser(epilog=tip, description='ratio-spoof is a open source tool to trick private trackers',formatter_class=argparse.RawDescriptionHelpFormatter) -parser.add_argument('-p' ,required=False ,type=int, default=8999, choices=range(1, 65535) ,help='change the port number, the default is 8999' , metavar='[PORT]') -group = parser.add_argument_group('required arguments') -group.add_argument('-t', required=True, metavar=(''), help='path .torrent file' , type=argparse.FileType('rb')) -group.add_argument('-d', required=True,help='required download arg values', nargs=2 ,metavar=('', '')) -group.add_argument('-u',required=True,help='required upload arg values ', nargs=2 ,metavar=('', '')) -args = parser.parse_args() - -read_file(args.t, args.d, args.u,args.p) diff --git a/ratiospoof/ratio-spoof.go b/ratiospoof/ratio-spoof.go new file mode 100644 index 0000000..111c9c5 --- /dev/null +++ b/ratiospoof/ratio-spoof.go @@ -0,0 +1,570 @@ +package ratiospoof + +import ( + "compress/gzip" + "crypto/sha1" + "errors" + "fmt" + "io/ioutil" + "math" + "net/http" + "net/url" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "github.com/ap-pauloafonso/ratio-spoof/beencode" + "github.com/gammazero/deque" + "github.com/olekukonko/ts" +) + +const ( + maxAnnounceHistory = 10 +) + +var validInitialSufixes = [...]string{"%", "b", "kb", "mb", "gb", "tb"} +var validSpeedSufixes = [...]string{"kbps"} + +type ratioSPoofState struct { + mutex *sync.Mutex + httpClient HttpClient + torrentInfo *torrentInfo + input *inputParsed + bitTorrentClient TorrentClient + announceRate int + currentAnnounceTimer int + announceInterval int + numWant int + seeders int + leechers int + announceCount int + status string + announceHistory announceHistory + lastAnounceRequest string + lastTackerResponse string +} +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 +} + +type torrentInfo struct { + name string + pieceSize int + totalSize int + trackerInfo trackerInfo + infoHashURLEncoded string +} + +func extractTorrentInfo(torrentPath string) (*torrentInfo, error) { + dat, err := ioutil.ReadFile(torrentPath) + if err != nil { + return nil, err + } + torrentMap := beencode.Decode(dat) + return &torrentInfo{ + name: torrentMap["info"].(map[string]interface{})["name"].(string), + pieceSize: torrentMap["info"].(map[string]interface{})["piece length"].(int), + totalSize: extractTotalSize(torrentMap), + trackerInfo: extractTrackerInfo(torrentMap), + infoHashURLEncoded: extractInfoHashURLEncoded(dat, torrentMap), + }, nil +} +func (I *InputArgs) parseInput(torrentInfo *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 := extractInputKbpsSpeed(I.DownloadSpeed) + if err != nil { + return nil, err + } + uploadSpeed, err := extractInputKbpsSpeed(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 TorrentClient, httpclient HttpClient) (*ratioSPoofState, error) { + + torrentInfo, err := extractTorrentInfo(input.TorrentPath) + if err != nil { + panic(err) + } + + inputParsed, err := input.parseInput(torrentInfo) + if err != nil { + panic(err) + } + + return &ratioSPoofState{ + bitTorrentClient: torrentClient, + httpClient: httpclient, + torrentInfo: torrentInfo, + input: inputParsed, + numWant: 200, + status: "started", + mutex: &sync.Mutex{}, + }, nil +} + +func checkSpeedSufix(input string) bool { + for _, v := range validSpeedSufixes { + + if strings.HasSuffix(strings.ToLower(input), v) { + return true + } + } + 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 extractInputKbpsSpeed(initialSpeedInput string) (int, error) { + if !checkSpeedSufix(initialSpeedInput) { + return 0, errors.New("speed must be in kbps") + } + number, _ := strconv.ParseFloat(initialSpeedInput[:len(initialSpeedInput)-4], 64) + + ret := int(number) + + if ret < 0 { + return 0, errors.New("speed can not be negative") + } + return ret, nil +} + +type trackerInfo struct { + main string + urls []string +} + +func (T *trackerInfo) SwapFirst(currentIdx int) { + aux := T.urls[0] + T.urls[0] = T.urls[currentIdx] + T.urls[currentIdx] = aux +} + +type trackerResponse struct { + minInterval int + interval int + seeders int + leechers int +} + +type TorrentClient interface { + PeerID() string + Key() string + Query() string + Name() string + Headers() map[string]string +} +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) Run() { + R.firstAnnounce() + go R.decreaseTimer() + go R.printState() + for { + R.generateNextAnnounce() + time.Sleep(time.Duration(R.announceInterval) * time.Second) + R.fireAnnounce() + } + +} +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() + R.AfterFirstRequestState() +} + +func (R *ratioSPoofState) updateInterval(resp trackerResponse) { + if resp.minInterval > 0 { + R.announceInterval = resp.minInterval + } else { + R.announceInterval = resp.interval + } +} + +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() { + 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.tryMakeRequest(query) + R.updateSeedersAndLeechers(trackerResp) + R.updateInterval(trackerResp) +} +func (R *ratioSPoofState) generateNextAnnounce() { + R.resetTimer(R.announceInterval) + lastAnnounce := R.announceHistory.Back().(announceEntry) + currentDownloaded := lastAnnounce.downloaded + var downloaded int + if currentDownloaded < R.torrentInfo.totalSize { + downloaded = calculateNextTotalSizeByte(R.input.downloadSpeed, currentDownloaded, R.torrentInfo.pieceSize, R.currentAnnounceTimer, R.torrentInfo.totalSize) + } else { + downloaded = R.torrentInfo.totalSize + } + + currentUploaded := lastAnnounce.uploaded + uploaded := calculateNextTotalSizeByte(R.input.uploadSpeed, currentUploaded, R.torrentInfo.pieceSize, R.currentAnnounceTimer, 0) + + left := calculateBytesLeft(downloaded, R.torrentInfo.totalSize) + R.addAnnounce(downloaded, uploaded, left, (float32(downloaded)/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) printState() { + terminalSize := func() int { + size, _ := ts.GetSize() + width := size.Col() + if width < 40 { + width = 40 + } + return width + } + clear := func() { + if runtime.GOOS == "windows" { + cmd := exec.Command("cmd", "/c", "cls") + cmd.Stdout = os.Stdout + cmd.Run() + } else { + fmt.Print("\033c") + } + } + + center := func(s string, n int, fill string) string { + div := n / 2 + return strings.Repeat(fill, div) + s + strings.Repeat(fill, div) + } + + humanReadableSize := func(byteSize float64) string { + var unitFound string + for _, unit := range []string{"B", "KiB", "MiB", "GiB", "TiB"} { + if byteSize < 1024.0 { + unitFound = unit + break + } + byteSize /= 1024.0 + } + return fmt.Sprintf("%.2f%v", byteSize, unitFound) + } + + fmtDuration := func(seconds int) string { + d := time.Duration(seconds) * time.Second + return fmt.Sprintf("%s", d) + } + + for { + width := terminalSize() + clear() + if R.announceHistory.Len() > 0 { + seedersStr := fmt.Sprint(R.seeders) + leechersStr := fmt.Sprint(R.leechers) + if R.seeders == 0 { + seedersStr = "not informed" + } + + if R.leechers == 0 { + leechersStr = "not informed" + } + fmt.Println(center(" RATIO-SPOOF ", width-len(" RATIO-SPOOF "), "#")) + fmt.Printf(` + Torrent: %v + Tracker: %v + Seeders: %v + Leechers:%v + Download Speed: %vKB/s + Upload Speed: %vKB/s + Size: %v + Emulation: %v | Port: %v`, R.torrentInfo.name, R.torrentInfo.trackerInfo.main, seedersStr, leechersStr, R.input.downloadSpeed, + R.input.uploadSpeed, humanReadableSize(float64(R.torrentInfo.totalSize)), R.bitTorrentClient.Name(), R.input.port) + fmt.Println() + fmt.Println() + fmt.Println(center(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF ", width-len(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF "), "#")) + fmt.Println() + 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", dequeItem.count, humanReadableSize(float64(dequeItem.downloaded)), dequeItem.percentDownloaded, humanReadableSize(float64(dequeItem.left)), humanReadableSize(float64(dequeItem.uploaded))) + fmt.Println() + + } + lastDequeItem := R.announceHistory.At(R.announceHistory.Len() - 1).(announceEntry) + fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | next announce in: %v", lastDequeItem.count, humanReadableSize(float64(lastDequeItem.downloaded)), lastDequeItem.percentDownloaded, humanReadableSize(float64(lastDequeItem.left)), humanReadableSize(float64(lastDequeItem.uploaded)), fmtDuration(R.currentAnnounceTimer)) + + if R.input.debug { + fmt.Println() + fmt.Println() + fmt.Println(center(" DEBUG ", width-len(" DEBUG "), "#")) + fmt.Println() + fmt.Print(R.lastAnounceRequest) + fmt.Println() + fmt.Println() + fmt.Print(R.lastTackerResponse) + } + time.Sleep(1 * time.Second) + } + } +} + +func (R *ratioSPoofState) resetTimer(newAnnounceRate int) { + R.announceRate = newAnnounceRate + R.mutex.Lock() + defer R.mutex.Unlock() + R.currentAnnounceTimer = R.announceRate +} + +func (R *ratioSPoofState) tryMakeRequest(query string) trackerResponse { + for idx, url := range R.torrentInfo.trackerInfo.urls { + completeURL := url + "?" + strings.TrimLeft(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 { + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + bytes, _ := ioutil.ReadAll(resp.Body) + mimeType := http.DetectContentType(bytes) + if mimeType == "application/x-gzip" { + gzipReader, _ := gzip.NewReader(resp.Body) + defer gzipReader.Close() + bytes, _ = ioutil.ReadAll(gzipReader) + } + R.lastTackerResponse = string(bytes) + decodedResp := beencode.Decode(bytes) + if idx != 0 { + R.torrentInfo.trackerInfo.SwapFirst(idx) + } + ret := extractTrackerResponse(decodedResp) + return ret + } + } + } + panic("Connection error with the tracker") + +} +func (R *ratioSPoofState) AfterFirstRequestState() { + R.status = "" +} + +func calculateNextTotalSizeByte(speedKbps, currentByte, pieceSizeByte, seconds, limitTotalBytes int) int { + if speedKbps == 0 { + return currentByte + } + total := currentByte + (speedKbps * 1024 * seconds) + closestPieceNumber := int(total / pieceSizeByte) + closestPieceNumber += 5 + nextTotal := closestPieceNumber * pieceSizeByte + + if limitTotalBytes != 0 && nextTotal > limitTotalBytes { + return limitTotalBytes + } + return nextTotal +} + +func extractInfoHashURLEncoded(rawData []byte, torrentData map[string]interface{}) string { + byteOffsets := torrentData["info"].(map[string]interface{})["byte_offsets"].([]int) + h := sha1.New() + h.Write([]byte(rawData[byteOffsets[0]:byteOffsets[1]])) + ret := h.Sum(nil) + return url.QueryEscape(string(ret)) + +} +func extractTotalSize(torrentData map[string]interface{}) int { + if value, ok := torrentData["info"].(map[string]interface{})["length"]; ok { + return value.(int) + } + + var total int + + for _, file := range torrentData["info"].(map[string]interface{})["files"].([]interface{}) { + total += file.(map[string]interface{})["length"].(int) + } + return total +} + +func extractTrackerInfo(torrentData map[string]interface{}) trackerInfo { + uniqueUrls := make(map[string]int) + currentCount := 0 + if main, ok := torrentData["announce"]; ok && strings.HasPrefix(main.(string), "http") { + if _, found := uniqueUrls[main.(string)]; !found { + uniqueUrls[main.(string)] = currentCount + currentCount++ + } + } + if list, ok := torrentData["announce-list"]; ok { + for _, innerList := range list.([]interface{}) { + for _, item := range innerList.([]interface{}) { + if _, found := uniqueUrls[item.(string)]; !found && strings.HasPrefix(item.(string), "http") { + uniqueUrls[item.(string)] = currentCount + currentCount++ + } + } + } + + } + trackerInfo := trackerInfo{urls: make([]string, len(uniqueUrls))} + for key, value := range uniqueUrls { + trackerInfo.urls[value] = key + } + + trackerInfo.main = trackerInfo.urls[0] + + if len(trackerInfo.urls) == 0 { + panic("No tcp/http tracker url announce found'") + } + return trackerInfo +} + +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") + } +} diff --git a/ratiospoof/ratio-spoof_test.go b/ratiospoof/ratio-spoof_test.go new file mode 100644 index 0000000..1d37b81 --- /dev/null +++ b/ratiospoof/ratio-spoof_test.go @@ -0,0 +1,129 @@ +package ratiospoof + +import "testing" + +func assertAreEqual(t *testing.T, got, want interface{}) { + t.Helper() + if got != want { + t.Errorf("got: %v want: %v", got, want) + } +} + +// func TestStrSize2ByteSize(T *testing.T) { +// T.Run("100kb", func(t *testing.T) { +// got := strSize2ByteSize("100kb", 100) +// want := 102400 +// assertAreEqual(t, got, want) +// }) +// T.Run("1kb", func(t *testing.T) { +// got := strSize2ByteSize("1kb", 0) +// want := 1024 +// assertAreEqual(t, got, want) +// }) + +// T.Run("1mb", func(t *testing.T) { +// got := strSize2ByteSize("1mb", 0) +// want := 1048576 +// assertAreEqual(t, got, want) +// }) +// T.Run("1gb", func(t *testing.T) { +// got := strSize2ByteSize("1gb", 0) +// want := 1073741824 +// assertAreEqual(t, got, want) +// }) +// T.Run("1.5gb", func(t *testing.T) { +// got := strSize2ByteSize("1.5gb", 0) +// want := 1610612736 +// assertAreEqual(t, got, want) +// }) +// T.Run("1tb", func(t *testing.T) { +// got := strSize2ByteSize("1tb", 0) +// want := 1099511627776 +// assertAreEqual(t, got, want) +// }) +// T.Run("1b", func(t *testing.T) { +// got := strSize2ByteSize("1b", 0) +// want := 1 +// assertAreEqual(t, got, want) +// }) +// T.Run("100%% of 10gb ", func(t *testing.T) { +// got := strSize2ByteSize("100%", 10737418240) +// want := 10737418240 +// assertAreEqual(t, got, want) +// }) +// T.Run("55%% of 900mb ", func(t *testing.T) { +// got := strSize2ByteSize("55%", 943718400) +// want := 519045120 +// assertAreEqual(t, got, want) +// }) +// T.Run("55%% of 900mb ", func(t *testing.T) { +// got := strSize2ByteSize("55%", 943718400) +// want := 519045120 +// assertAreEqual(t, got, want) +// }) +// } + +// func TestHumanReadableSize(T *testing.T) { + +// T.Run("#1", func(t *testing.T) { +// got := humanReadableSize(1536, true) +// want := "1.50KiB" + +// assertAreEqual(t, got, want) +// }) +// T.Run("#2", func(t *testing.T) { +// got := humanReadableSize(379040563, true) +// want := "1.50KiB" + +// assertAreEqual(t, got, want) +// }) +// T.Run("#3", func(t *testing.T) { +// got := humanReadableSize(6291456, true) +// want := "1.50KiB" + +// assertAreEqual(t, got, want) +// }) +// T.Run("#4", func(t *testing.T) { +// got := humanReadableSize(372749107, true) +// want := "1.50KiB" + +// assertAreEqual(t, got, want) +// }) +// T.Run("#5", func(t *testing.T) { +// got := humanReadableSize(10485760, true) +// want := "1.50KiB" + +// assertAreEqual(t, got, want) +// }) +// T.Run("#6", func(t *testing.T) { +// got := humanReadableSize(15728640, true) +// want := "1.50KiB" +// assertAreEqual(t, got, want) +// }) +// T.Run("#7", func(t *testing.T) { +// got := humanReadableSize(363311923, true) +// want := "1.50KiB" +// assertAreEqual(t, got, want) +// }) +// T.Run("#8", func(t *testing.T) { +// got := humanReadableSize(16777216, true) +// want := "1.50KiB" + +// assertAreEqual(t, got, want) +// }) +// T.Run("#9", func(t *testing.T) { +// got := humanReadableSize(379040563, true) +// want := "1.50KiB" + +// assertAreEqual(t, got, want) +// }) +// } + +func TestClculateNextTotalSizeByte(T *testing.T) { + + got := calculateNextTotalSizeByte(100, 0, 512, 30, 87979879) + want := 3074560 + + assertAreEqual(T, got, want) + +}