port to golang

This commit is contained in:
ap-pauloafonso 2020-11-29 21:10:48 -03:00
parent 465a5a2e00
commit 3e52da8f14
18 changed files with 1152 additions and 444 deletions

2
.gitignore vendored
View file

@ -130,3 +130,5 @@ dmypy.json
#vscode folder
/.vscode
out/

View file

@ -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 <TORRENT_PATH> -d <INITIAL_DOWNLOADED> <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> <UPLOAD_SPEED>
usage:
./ratio-spoof -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> -ds <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> -us <UPLOAD_SPEED>
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 <TORRENT_PATH> path .torrent file
-d <INITIAL_DOWNLOADED> <DOWNLOAD_SPEED>
required download arg values
-u <INITIAL_UPLOADED> <UPLOAD_SPEED>
required upload arg values
-t <TORRENT_PATH>
-d <INITIAL_DOWNLOADED>
-ds <DOWNLOAD_SPEED>
-u <INITIAL_UPLOADED>
-us <UPLOAD_SPEED>
<INITIAL_DOWNLOADED> and <INITIAL_UPLOADED> must be in %, b, kb, mb, gb, tb
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> 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.

View file

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

86
beencode/beencode.go Normal file
View file

@ -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
}

113
beencode/beencode_test.go Normal file
View file

@ -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"])
})
}
}

View file

@ -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

3
build.sh Executable file
View file

@ -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

79
cmd/main.go Normal file
View file

@ -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 <TORRENT_PATH> -d <INITIAL_DOWNLOADED> -ds <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> -us <UPLOAD_SPEED>`, osExecutableSuffix)
} else {
fmt.Println(`usage:
./ratio-spoof -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> -ds <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> -us <UPLOAD_SPEED>`, 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 <TORRENT_PATH>
-d <INITIAL_DOWNLOADED>
-ds <DOWNLOAD_SPEED>
-u <INITIAL_UPLOADED>
-us <UPLOAD_SPEED>
<INITIAL_DOWNLOADED> and <INITIAL_UPLOADED> must be in %, b, kb, mb, gb, tb
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> 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()
}

9
go.mod Normal file
View file

@ -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
)

14
go.sum Normal file
View file

@ -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=

View file

@ -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)
}

View file

@ -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")
}
})
}

View file

@ -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 = """
<INITIAL_DOWNLOADED> and <INITIAL_UPLOADED> must be in %, b, kb, mb, gb, tb
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> 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=('<TORRENT_PATH>'), help='path .torrent file' , type=argparse.FileType('rb'))
group.add_argument('-d', required=True,help='required download arg values', nargs=2 ,metavar=('<INITIAL_DOWNLOADED>', '<DOWNLOAD_SPEED>'))
group.add_argument('-u',required=True,help='required upload arg values ', nargs=2 ,metavar=('<INITIAL_UPLOADED>', '<UPLOAD_SPEED>'))
args = parser.parse_args()
read_file(args.t, args.d, args.u,args.p)

570
ratiospoof/ratio-spoof.go Normal file
View file

@ -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")
}
}

View file

@ -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)
}