Compare commits

..

No commits in common. "master" and "v1.0" have entirely different histories.
master ... v1.0

32 changed files with 446 additions and 1805 deletions

View file

@ -1,25 +0,0 @@
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.20'
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

3
.gitignore vendored
View file

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

View file

@ -1,15 +0,0 @@
test:
go test ./... -count=1 --cover
torrent-test:
go run main.go -c qbit-4.3.3 -t bencode/torrent_files_test/debian-12.0.0-amd64-DVD-1.iso.torrent -d 0% -ds 100kbps -u 0% -us 100kbps
release:
@if test -z "$(rsversion)"; then echo "usage: make release rsversion=v1.2"; exit 1; fi
rm -rf ./out
env GOOS=darwin GOARCH=amd64 go build -v -o ./out/mac/ratio-spoof .
env GOOS=linux GOARCH=amd64 go build -v -o ./out/linux/ratio-spoof .
env GOOS=windows GOARCH=amd64 go build -v -o ./out/windows/ratio-spoof.exe .
cd out/ ; zip ratio-spoof-$(rsversion)\(linux-mac-windows\).zip -r .

View file

@ -1,59 +1,56 @@
# ratio-spoof
Ratio-spoof is a cross-platform, free and open source tool to spoof the download/upload amount on private bittorrent trackers.
![](./assets/demo.gif)
![](./media/demo.gif)
## Motivation
Here in Brazil, not everybody has a great upload speed, and most private trackers require a ratio greater than or equal to 1. For example, if you downloaded 1GB, you must also upload 1GB in order to survive. Additionally, I have always been fascinated by the BitTorrent protocol. In fact, [I even made a BitTorrent web client to learn more about it](https://github.com/ap-pauloafonso/rwTorrent). So, if you have a bad internet connection, feel free to use this tool. Otherwise, please consider seeding the files with a real torrent client.
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.
## How does it work?
![Diagram](./assets/how-it-works.png)
## How does it works?
Bittorrent protocol works in such a way that there is no way that a tracker knows how much certain peer have downloaded or uploaded, so the tracker depends on the peer itself telling the amounts.
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 -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> -ds <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> -us <UPLOAD_SPEED>
usage: ratio-spoof.py -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> <UPLOAD_SPEED>
optional arguments:
-h show this help message and exit
-p [PORT] change the port number, default: 8999
-c [CLIENT_CODE] change the client emulation, default: qbit-4.0.3
-h, --help show this help message and exit
required arguments:
-t <TORRENT_PATH>
-d <INITIAL_DOWNLOADED>
-ds <DOWNLOAD_SPEED>
-u <INITIAL_UPLOADED>
-us <UPLOAD_SPEED>
-t <TORRENT_PATH> path .torrent file
-d <INITIAL_DOWNLOADED> <DOWNLOAD_SPEED>
required download arg values
-u <INITIAL_UPLOADED> <UPLOAD_SPEED>
required upload arg values
<INITIAL_DOWNLOADED> and <INITIAL_UPLOADED> must be in %, b, kb, mb, gb, tb
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> must be in kbps, mbps
[CLIENT_CODE] options: qbit-4.0.3, qbit-4.3.3
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> must be in kbps
```
```
./ratio-spoof -d 90% -ds 100kbps -u 0% -us 1024kbps -t (torrentfile_path)
./ratio-spoof -d 90% 100kbps -u 0% 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 -ds 500kbps -u 1gb -us 1024kbps -t (torrentfile_path)
./ratio-spoof -d 2gb 500kbps -u 1gb 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.
## Will I get caught using it ?
Depends on whether you use it carefully, It's a hard task to catch cheaters, but if you start uploading crazy amounts out of nowhere or seeding something with no active leecher on the swarm you may be in risk.
## Will i get caught using it ?
Depends wether you use it carefuly, Its a hard task to catch cheaters, but if you start uploading crazy amounts out of nowhere or seeding something with no active leecher on the swarm you may be in risk.
## Bittorrent client supported
The default client emulation is qbittorrent v4.0.3, however you can change it by using the -c argument
The currently emulation is hard coded to be a popular and accepted client qbittorrent v4.0.3.
## Resources
http://www.bittorrent.org/beps/bep_0003.html
https://wiki.theory.org/BitTorrentSpecification
https://wiki.theory.org/index.php/BitTorrentSpecification

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

View file

@ -1,201 +0,0 @@
package bencode
import (
"bytes"
"crypto/sha1"
"fmt"
"regexp"
"strconv"
)
const (
dictToken = byte('d')
numberToken = byte('i')
listToken = byte('l')
endOfCollectionToken = byte('e')
lengthValueStringSeparatorToken = byte(':')
torrentInfoKey = "info"
torrentNameKey = "name"
torrentPieceLengthKey = "piece length"
torrentLengthKey = "length"
torrentFilesKey = "files"
mainAnnounceKey = "announce"
announceListKey = "announce-list"
torrentDictOffsetsKey = "byte_offsets"
)
// TorrentInfo contains all relevant information extracted from a bencode file
type TorrentInfo struct {
Name string
PieceSize int
TotalSize int
TrackerInfo *TrackerInfo
InfoHashURLEncoded string
}
//TrackerInfo contains http urls from the tracker
type TrackerInfo struct {
Main string
Urls []string
}
type torrentDict struct {
resultMap map[string]interface{}
}
//TorrentDictParse decodes the bencoded bytes and builds the torrentInfo file
func TorrentDictParse(dat []byte) (torrent *TorrentInfo, err error) {
defer func() {
if e := recover(); e != nil {
err = e.(error)
}
}()
dict, _ := mapParse(0, &dat)
torrentMap := torrentDict{resultMap: dict}
return &TorrentInfo{
Name: torrentMap.resultMap[torrentInfoKey].(map[string]interface{})[torrentNameKey].(string),
PieceSize: torrentMap.resultMap[torrentInfoKey].(map[string]interface{})[torrentPieceLengthKey].(int),
TotalSize: torrentMap.extractTotalSize(),
TrackerInfo: torrentMap.extractTrackerInfo(),
InfoHashURLEncoded: torrentMap.extractInfoHashURLEncoded(dat),
}, err
}
func (t *torrentDict) extractInfoHashURLEncoded(rawData []byte) string {
byteOffsets := t.resultMap["info"].(map[string]interface{})["byte_offsets"].([]int)
h := sha1.New()
h.Write([]byte(rawData[byteOffsets[0]:byteOffsets[1]]))
ret := h.Sum(nil)
var buf bytes.Buffer
re := regexp.MustCompile(`[a-zA-Z0-9\.\-\_\~]`)
for _, b := range ret {
if re.Match([]byte{b}) {
buf.WriteByte(b)
} else {
buf.WriteString(fmt.Sprintf("%%%02x", b))
}
}
return buf.String()
}
func (t *torrentDict) extractTotalSize() int {
if value, ok := t.resultMap[torrentInfoKey].(map[string]interface{})[torrentLengthKey]; ok {
return value.(int)
}
var total int
for _, file := range t.resultMap[torrentInfoKey].(map[string]interface{})[torrentFilesKey].([]interface{}) {
total += file.(map[string]interface{})[torrentLengthKey].(int)
}
return total
}
func (t *torrentDict) extractTrackerInfo() *TrackerInfo {
uniqueUrls := make(map[string]int)
currentCount := 0
if main, ok := t.resultMap[mainAnnounceKey]; ok {
if _, found := uniqueUrls[main.(string)]; !found {
uniqueUrls[main.(string)] = currentCount
currentCount++
}
}
if list, ok := t.resultMap[announceListKey]; ok {
for _, innerList := range list.([]interface{}) {
for _, item := range innerList.([]interface{}) {
if _, found := uniqueUrls[item.(string)]; !found {
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]
return &trackerInfo
}
//Decode accepts a byte slice and returns a map with information parsed.
func Decode(data []byte) (dataMap map[string]interface{}, err error) {
defer func() {
if e := recover(); e != nil {
err = e.(error)
}
}()
result, _ := findParse(0, &data)
return result.(map[string]interface{}), err
}
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 bencode")
}
}
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] != lengthValueStringSeparatorToken {
current++
}
sizeStr, _ := strconv.Atoi(string(((*data)[startIdx:current])))
result = string((*data)[current+1 : current+1+int(sizeStr)])
nextIdx = current + 1 + int(sizeStr)
return
}

View file

@ -1,114 +0,0 @@
package bencode
import (
"log"
"os"
"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 := os.ReadDir("./torrent_files_test")
if err != nil {
log.Fatal(err)
}
for _, f := range files {
T.Run(f.Name(), func(t *testing.T) {
data, _ := os.ReadFile("./torrent_files_test/" + f.Name())
result, _ := Decode(data)
t.Log(result["info"].(map[string]interface{})["name"])
})
}
}

92
bencode_parser.py Normal file
View file

@ -0,0 +1,92 @@
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

View file

@ -1,95 +0,0 @@
package emulation
import (
"embed"
"encoding/json"
generator2 "github.com/ap-pauloafonso/ratio-spoof/generator"
"io"
)
type ClientInfo struct {
Name string `json:"name"`
PeerID struct {
Generator string `json:"generator"`
Regex string `json:"regex"`
} `json:"peerId"`
Key struct {
Generator string `json:"generator"`
Regex string `json:"regex"`
} `json:"key"`
Rounding struct {
Generator string `json:"generator"`
Regex string `json:"regex"`
} `json:"rounding"`
Query string `json:"query"`
Headers map[string]string `json:"headers"`
}
type KeyGenerator interface {
Key() string
}
type PeerIdGenerator interface {
PeerId() string
}
type RoundingGenerator interface {
Round(downloadCandidateNextAmount, uploadCandidateNextAmount, leftCandidateNextAmount, pieceSize int) (downloaded, uploaded, left int)
}
type Emulation struct {
PeerIdGenerator
KeyGenerator
Query string
Name string
Headers map[string]string
RoundingGenerator
}
func NewEmulation(code string) (*Emulation, error) {
c, err := extractClient(code)
if err != nil {
return nil, err
}
peerG, err := generator2.NewRegexPeerIdGenerator(c.PeerID.Regex)
if err != nil {
return nil, err
}
keyG, err := generator2.NewDefaultKeyGenerator()
if err != nil {
return nil, err
}
roudingG, err := generator2.NewDefaultRoudingGenerator()
if err != nil {
return nil, err
}
return &Emulation{PeerIdGenerator: peerG, KeyGenerator: keyG, RoundingGenerator: roudingG,
Headers: c.Headers, Name: c.Name, Query: c.Query}, nil
}
//go:embed static
var staticFiles embed.FS
func extractClient(code string) (*ClientInfo, error) {
f, err := staticFiles.Open("static/" + code + ".json")
if err != nil {
return nil, err
}
defer f.Close()
bytes, err := io.ReadAll(f)
if err != nil {
return nil, err
}
var client ClientInfo
json.Unmarshal(bytes, &client)
return &client, nil
}

View file

@ -1,74 +0,0 @@
package emulation
import (
"io/fs"
"strings"
"testing"
)
func TestNewEmulation(t *testing.T) {
var counter int
fs.WalkDir(staticFiles, ".", func(path string, d fs.DirEntry, err error) error {
if counter > 1 {
code := strings.TrimRight(strings.TrimLeft(path, "static/"), ".json")
e, err := NewEmulation(code)
if err != nil {
t.Error("should not return error ")
}
peerId := e.PeerId()
key := e.Key()
d, u, l := e.Round(2*1024*1024*1024, 1024*1024*1024, 3*1024*1024*1024, 1024)
if peerId == "" {
t.Errorf("%s.json should be able to generate PeerId", code)
}
if key == "" {
t.Errorf("%s.json should be able to generate Key", code)
}
if d <= 0 || u <= 0 || l <= 0 {
t.Errorf("%s.json should be able to round candidates", code)
}
}
counter++
return nil
})
}
func TestExtractClient(t *testing.T) {
var counter int
fs.WalkDir(staticFiles, ".", func(path string, d fs.DirEntry, err error) error {
if counter > 1 {
code := strings.TrimRight(strings.TrimLeft(path, "static/"), ".json")
c, e := extractClient(code)
if e != nil || err != nil {
t.Error("should not return error")
}
if c.Key.Generator == "" && c.Key.Regex == "" {
t.Errorf("%s.json should have key generator properties", code)
}
if c.PeerID.Generator == "" && c.PeerID.Regex == "" {
t.Errorf("%s.json should have PeerId generator properties", code)
}
if c.Rounding.Generator == "" && c.Rounding.Regex == "" {
t.Errorf("%s.json should have rouding generator properties", code)
}
if c.Name == "" {
t.Errorf("%s.json should have a name", code)
}
if c.Query == "" {
t.Errorf("%s.json should have a query", code)
}
if len(c.Headers) == 0 {
t.Errorf("%s.json should have headers", code)
}
}
counter++
return nil
})
}

View file

@ -1,17 +0,0 @@
{
"name":"qBittorrent v4.0.3",
"peerId":{
"regex":"-qB4030-[A-Za-z0-9_~\\(\\)\\!\\.\\*-]{12}"
},
"key": {
"generator":"defaultKeyGenerator"
},
"rounding": {
"generator":"defaultRoudingGenerator"
},
"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",
"headers":{
"User-Agent" :"qBittorrent/4.0.3",
"Accept-Encoding": "gzip"
}
}

View file

@ -1,17 +0,0 @@
{
"name":"qBittorrent v4.3.3",
"peerId":{
"regex":"-qB4330-[A-Za-z0-9_~\\(\\)\\!\\.\\*-]{12}"
},
"key": {
"generator":"defaultKeyGenerator"
},
"rounding": {
"generator":"defaultRoudingGenerator"
},
"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",
"headers":{
"User-Agent" :"qBittorrent/4.3.3",
"Accept-Encoding": "gzip"
}
}

View file

@ -1,23 +0,0 @@
package generator
import (
"crypto/rand"
"encoding/hex"
"strings"
)
func NewDefaultKeyGenerator() (*DefaultKeyGenerator, error) {
randomBytes := make([]byte, 4)
rand.Read(randomBytes)
str := hex.EncodeToString(randomBytes)
result := strings.ToUpper(str)
return &DefaultKeyGenerator{generated: result}, nil
}
type DefaultKeyGenerator struct {
generated string
}
func (d *DefaultKeyGenerator) Key() string {
return d.generated
}

View file

@ -1,14 +0,0 @@
package generator
import "testing"
func TestDeaultKeyGenerator(t *testing.T) {
t.Run("Key has 8 length", func(t *testing.T) {
obj, _ := NewDefaultKeyGenerator()
key := obj.Key()
if len(key) != 8 {
t.Error("Keys must have length of 8")
}
})
}

View file

@ -1,21 +0,0 @@
package generator
import (
regen "github.com/zach-klippenstein/goregen"
)
type RegexPeerIdGenerator struct {
generated string
}
func NewRegexPeerIdGenerator(pattern string) (*RegexPeerIdGenerator, error) {
result, err := regen.Generate(pattern)
if err != nil {
return nil, err
}
return &RegexPeerIdGenerator{generated: result}, nil
}
func (d *RegexPeerIdGenerator) PeerId() string {
return d.generated
}

View file

@ -1,16 +0,0 @@
package generator
type DefaultRoundingGenerator struct{}
func NewDefaultRoudingGenerator() (*DefaultRoundingGenerator, error) {
return &DefaultRoundingGenerator{}, nil
}
func (d *DefaultRoundingGenerator) Round(downloadCandidateNextAmount, uploadCandidateNextAmount, leftCandidateNextAmount, pieceSize int) (downloaded, uploaded, left int) {
down := downloadCandidateNextAmount
up := uploadCandidateNextAmount - (uploadCandidateNextAmount % (16 * 1024))
l := leftCandidateNextAmount - (leftCandidateNextAmount % pieceSize)
return down, up, l
}

View file

@ -1,21 +0,0 @@
package generator
import "testing"
func TestDefaultRounding(t *testing.T) {
r, _ := NewDefaultRoudingGenerator()
d, u, l := r.Round(656497856, 46479878, 7879879, 1024)
//same
if d != 656497856 {
t.Errorf("[download]got %v want %v", d, 656497856)
}
//16kb round
if u != 46465024 {
t.Errorf("[upload]got %v want %v", u, 46465024)
}
//piece size round
if l != 7879680 {
t.Errorf("[left]got %v want %v", l, 7879680)
}
}

16
go.mod
View file

@ -1,16 +0,0 @@
module github.com/ap-pauloafonso/ratio-spoof
go 1.20
require (
github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0
github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
)
require (
github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.7.0 // indirect
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
)

33
go.sum
View file

@ -1,33 +0,0 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 h1:OL2d27ueTKnlQJoqLW2fc9pWYulFnJYLWzomGV7HqZo=
github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4/go.mod h1:Pw1H1OjSNHiqeuxAduB1BKYXIwFtsyrY47nEqSgEiCM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
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=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea h1:CyhwejzVGvZ3Q2PSbQ4NRRYn+ZWv5eS1vlaEusT+bAI=
github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea/go.mod h1:eNr558nEUjP8acGw8FFjTeWvSgU1stO7FAO6eknhHe4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,188 +0,0 @@
package input
import (
"errors"
"fmt"
"github.com/ap-pauloafonso/ratio-spoof/bencode"
"math"
"strconv"
"strings"
)
const (
minPortNumber = 1
maxPortNumber = 65535
speedSuffixLength = 4
)
type InputArgs struct {
TorrentPath string
InitialDownloaded string
DownloadSpeed string
InitialUploaded string
Client string
UploadSpeed string
Port int
Debug bool
}
type InputParsed struct {
TorrentPath string
InitialDownloaded int
DownloadSpeed int
InitialUploaded int
UploadSpeed int
Port int
Debug bool
}
var validInitialSufixes = [...]string{"%", "b", "kb", "mb", "gb", "tb"}
var validSpeedSufixes = [...]string{"kbps", "mbps"}
func (i *InputArgs) ParseInput(torrentInfo *bencode.TorrentInfo) (*InputParsed, error) {
downloaded, err := extractInputInitialByteCount(i.InitialDownloaded, torrentInfo.TotalSize, true)
if err != nil {
return nil, err
}
uploaded, err := extractInputInitialByteCount(i.InitialUploaded, torrentInfo.TotalSize, false)
if err != nil {
return nil, err
}
downloadSpeed, err := extractInputByteSpeed(i.DownloadSpeed)
if err != nil {
return nil, err
}
uploadSpeed, err := extractInputByteSpeed(i.UploadSpeed)
if err != nil {
return nil, err
}
if i.Port < minPortNumber || i.Port > maxPortNumber {
return nil, errors.New(fmt.Sprint("port number must be between %i and %i", minPortNumber, maxPortNumber))
}
return &InputParsed{InitialDownloaded: downloaded,
DownloadSpeed: downloadSpeed,
InitialUploaded: uploaded,
UploadSpeed: uploadSpeed,
Debug: i.Debug,
Port: i.Port,
}, nil
}
func checkSpeedSufix(input string) (valid bool, suffix string) {
for _, v := range validSpeedSufixes {
if strings.HasSuffix(strings.ToLower(input), v) {
return true, input[len(input)-4:]
}
}
return false, ""
}
func extractInputInitialByteCount(initialSizeInput string, totalBytes int, errorIfHigher bool) (int, error) {
byteCount, err := strSize2ByteSize(initialSizeInput, totalBytes)
if err != nil {
return 0, err
}
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
}
// Takes an dirty speed input and returns the bytes per second based on the suffixes
// example 1kbps(string) > 1024 bytes per second (int)
func extractInputByteSpeed(initialSpeedInput string) (int, error) {
ok, suffix := checkSpeedSufix(initialSpeedInput)
if !ok {
return 0, fmt.Errorf("speed must be in %v", validSpeedSufixes)
}
speedVal, err := strconv.ParseFloat(initialSpeedInput[:len(initialSpeedInput)-speedSuffixLength], 64)
if err != nil {
return 0, errors.New("invalid speed number")
}
if speedVal < 0 {
return 0, errors.New("speed can not be negative")
}
if suffix == "kbps" {
speedVal *= 1024
} else {
speedVal = speedVal * 1024 * 1024
}
ret := int(speedVal)
return ret, nil
}
func extractByteSizeNumber(strWithSufix string, sufixLength, power int) (int, error) {
v, err := strconv.ParseFloat(strWithSufix[:len(strWithSufix)-sufixLength], 64)
if err != nil {
return 0, err
}
result := v * math.Pow(1024, float64(power))
return int(result), nil
}
func strSize2ByteSize(input string, totalSize int) (int, error) {
lowerInput := strings.ToLower(input)
invalidSizeError := errors.New("invalid input size")
switch {
case strings.HasSuffix(lowerInput, "kb"):
{
v, err := extractByteSizeNumber(lowerInput, 2, 1)
if err != nil {
return 0, invalidSizeError
}
return v, nil
}
case strings.HasSuffix(lowerInput, "mb"):
{
v, err := extractByteSizeNumber(lowerInput, 2, 2)
if err != nil {
return 0, invalidSizeError
}
return v, nil
}
case strings.HasSuffix(lowerInput, "gb"):
{
v, err := extractByteSizeNumber(lowerInput, 2, 3)
if err != nil {
return 0, invalidSizeError
}
return v, nil
}
case strings.HasSuffix(lowerInput, "tb"):
{
v, err := extractByteSizeNumber(lowerInput, 2, 4)
if err != nil {
return 0, invalidSizeError
}
return v, nil
}
case strings.HasSuffix(lowerInput, "b"):
{
v, err := extractByteSizeNumber(lowerInput, 1, 0)
if err != nil {
return 0, invalidSizeError
}
return v, nil
}
case strings.HasSuffix(lowerInput, "%"):
{
v, err := strconv.ParseFloat(lowerInput[:len(lowerInput)-1], 64)
if v < 0 || v > 100 || err != nil {
return 0, errors.New("percent value must be in (0-100)")
}
result := int(float64(v/100) * float64(totalSize))
return result, nil
}
default:
return 0, errors.New("Size not found")
}
}

View file

@ -1,245 +0,0 @@
package input
import (
"errors"
"testing"
)
func CheckError(out error, want error, t *testing.T) {
t.Helper()
if out == nil && want == nil {
return
}
if out != nil && want == nil {
t.Errorf("got %v, want %v", out.Error(), "")
}
if out == nil && want != nil {
t.Errorf("got %v, want %v", "", want.Error())
}
if out != nil && want != nil && out.Error() != want.Error() {
t.Errorf("got %v, want %v", out.Error(), want.Error())
}
}
func TestExtractInputInitialByteCount(T *testing.T) {
data := []struct {
name string
inSize string
inTotal int
inErrorIfHigher bool
err error
}{
{
name: "[Donwloaded - error if higher]100kb input with 200kb limit shouldn't return error test",
inSize: "100kb",
inTotal: 204800,
inErrorIfHigher: true,
},
{
name: "[Donwloaded - error if higher]300kb input with 200kb limit should return error test",
inSize: "300kb",
inTotal: 204800,
inErrorIfHigher: true,
err: errors.New("initial downloaded can not be higher than the torrent size"),
},
{
name: "[Uploaded]100kb input with 200kb limit shouldn't return error test",
inSize: "100kb",
inTotal: 204800,
inErrorIfHigher: false,
},
{
name: "[Uploaded]300kb input with 200kb limit shouldn't return error test",
inSize: "300kb",
inTotal: 204800,
inErrorIfHigher: false,
},
{
name: "[Donwloaded] -100kb should return negative number error test",
inSize: "-100kb",
inTotal: 204800,
inErrorIfHigher: true,
err: errors.New("initial value can not be negative"),
},
{
name: "[Uploaded] -100kb should return negative number error test",
inSize: "-100kb",
inTotal: 204800,
inErrorIfHigher: false,
err: errors.New("initial value can not be negative"),
},
}
for _, td := range data {
T.Run(td.name, func(t *testing.T) {
_, err := extractInputInitialByteCount(td.inSize, td.inTotal, td.inErrorIfHigher)
CheckError(err, td.err, t)
})
}
}
func TestStrSize2ByteSize(T *testing.T) {
data := []struct {
name string
in string
inTotalSize int
out int
err error
}{
{
name: "100kb test",
in: "100kb",
inTotalSize: 100,
out: 102400,
},
{
name: "1kb test",
in: "1kb",
inTotalSize: 0,
out: 1024,
},
{
name: "1mb test",
in: "1mb",
inTotalSize: 0,
out: 1048576,
},
{
name: "1gb test",
in: "1gb",
inTotalSize: 0,
out: 1073741824,
},
{
name: "1.5gb test",
in: "1.5gb",
inTotalSize: 0,
out: 1610612736,
},
{
name: "1tb test",
in: "1tb",
inTotalSize: 0,
out: 1099511627776,
},
{
name: "1b test",
in: "1b",
inTotalSize: 0,
out: 1,
},
{
name: "10xb test",
in: "10xb",
inTotalSize: 0,
err: errors.New("invalid input size"),
},
{
name: `100% test`,
in: "100%",
inTotalSize: 10737418240,
out: 10737418240,
},
{
name: `55% test`,
in: "55%",
inTotalSize: 943718400,
out: 519045120,
},
{
name: `5kg test`,
in: "5kg",
err: errors.New("Size not found"),
},
{
name: `-1% test`,
in: "-1%",
err: errors.New("percent value must be in (0-100)"),
},
{
name: `101% test`,
in: "101%",
err: errors.New("percent value must be in (0-100)"),
},
{
name: `a% test`,
in: "a%",
err: errors.New("percent value must be in (0-100)"),
},
}
for _, td := range data {
T.Run(td.name, func(t *testing.T) {
got, err := strSize2ByteSize(td.in, td.inTotalSize)
if td.err != nil {
if td.err.Error() != err.Error() {
t.Errorf("got %v, want %v", err.Error(), td.err.Error())
}
}
if got != td.out {
t.Errorf("got %v, want %v", got, td.out)
}
})
}
}
func TestExtractInputByteSpeed(T *testing.T) {
data := []struct {
name string
speed string
expected int
err error
}{
{
name: "1kbps test",
speed: "1kbps",
expected: 1024,
},
{
name: "1024kbps test",
speed: "1024kbps",
expected: 1048576,
},
{
name: "1mbps test",
speed: "1mbps",
expected: 1048576,
},
{
name: "2.5mbps test",
speed: "2.5mbps",
expected: 2621440,
},
{
name: "2.5tbps test",
speed: "2.5tbps",
err: errors.New("speed must be in [kbps mbps]"),
},
{
name: "-akbps test",
speed: "-akbps",
err: errors.New("invalid speed number"),
},
{
name: "-10kbps test",
speed: "-10kbps",
err: errors.New("speed can not be negative"),
},
}
for _, td := range data {
T.Run(td.name, func(t *testing.T) {
got, err := extractInputByteSpeed(td.speed)
if td.err != nil {
if td.err.Error() != err.Error() {
t.Errorf("got %v, want %v", err.Error(), td.err.Error())
}
}
if got != td.expected {
t.Errorf("got %v, want %v", got, td.expected)
}
})
}
}

74
main.go
View file

@ -1,74 +0,0 @@
package main
import (
"flag"
"fmt"
"github.com/ap-pauloafonso/ratio-spoof/input"
"github.com/ap-pauloafonso/ratio-spoof/printer"
"github.com/ap-pauloafonso/ratio-spoof/ratiospoof"
"log"
"os"
)
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, "")
client := flag.String("c", "qbit-4.0.3", "emulated client")
flag.Usage = func() {
fmt.Printf("usage: %s -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> -ds <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> -us <UPLOAD_SPEED>\n", os.Args[0])
fmt.Print(`
optional arguments:
-h show this help message and exit
-p [PORT] change the port number, default: 8999
-c [CLIENT_CODE] change the client emulation, default: qbit-4.0.3
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, mbps
[CLIENT_CODE] options: qbit-4.0.3, qbit-4.3.3
`)
}
flag.Parse()
if *torrentPath == "" || *initialDownload == "" || *downloadSpeed == "" || *initialUpload == "" || *uploadSpeed == "" {
flag.Usage()
return
}
r, err := ratiospoof.NewRatioSpoofState(
input.InputArgs{
TorrentPath: *torrentPath,
InitialDownloaded: *initialDownload,
DownloadSpeed: *downloadSpeed,
InitialUploaded: *initialUpload,
UploadSpeed: *uploadSpeed,
Port: *port,
Debug: *debug,
Client: *client,
})
if err != nil {
log.Fatalln(err)
}
go printer.PrintState(r)
r.Run()
}

BIN
media/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View file

@ -1,118 +0,0 @@
package printer
import (
"fmt"
"github.com/ap-pauloafonso/ratio-spoof/ratiospoof"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/olekukonko/ts"
)
func PrintState(state *ratiospoof.RatioSpoof) {
for {
if !state.Print {
break
}
width := terminalSize()
clear()
if state.AnnounceCount == 1 {
println("Trying to connect to the tracker...")
time.Sleep(1 * time.Second)
continue
}
if state.AnnounceHistory.Len() > 0 {
seedersStr := fmt.Sprint(state.Seeders)
leechersStr := fmt.Sprint(state.Leechers)
if state.Seeders == 0 {
seedersStr = "not informed"
}
if state.Leechers == 0 {
leechersStr = "not informed"
}
var retryStr string
if state.Tracker.RetryAttempt > 0 {
retryStr = fmt.Sprintf("(*Retry %v - check your connection)", state.Tracker.RetryAttempt)
}
fmt.Printf("%s\n", center(" RATIO-SPOOF ", width-len(" RATIO-SPOOF "), "#"))
fmt.Printf(`
Torrent: %v
Tracker: %v
Seeders: %v
Leechers:%v
Download Speed: %v/s
Upload Speed: %v/s
Size: %v
Emulation: %v | Port: %v`, state.TorrentInfo.Name, state.TorrentInfo.TrackerInfo.Main, seedersStr, leechersStr, humanReadableSize(float64(state.Input.DownloadSpeed)),
humanReadableSize(float64(state.Input.UploadSpeed)), humanReadableSize(float64(state.TorrentInfo.TotalSize)), state.BitTorrentClient.Name, state.Input.Port)
fmt.Printf("\n\n%s\n\n", center(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF ", width-len(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF "), "#"))
for i := 0; i <= state.AnnounceHistory.Len()-2; i++ {
dequeItem := state.AnnounceHistory.At(i).(ratiospoof.AnnounceEntry)
fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | announced\n", dequeItem.Count, humanReadableSize(float64(dequeItem.Downloaded)), dequeItem.PercentDownloaded, humanReadableSize(float64(dequeItem.Left)), humanReadableSize(float64(dequeItem.Uploaded)))
}
lastDequeItem := state.AnnounceHistory.At(state.AnnounceHistory.Len() - 1).(ratiospoof.AnnounceEntry)
remaining := time.Until(state.Tracker.EstimatedTimeToAnnounce)
fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | next announce in: %v %v\n", lastDequeItem.Count,
humanReadableSize(float64(lastDequeItem.Downloaded)),
lastDequeItem.PercentDownloaded,
humanReadableSize(float64(lastDequeItem.Left)),
humanReadableSize(float64(lastDequeItem.Uploaded)),
fmtDuration(remaining),
retryStr)
if state.Input.Debug {
fmt.Printf("\n%s\n", center(" DEBUG ", width-len(" DEBUG "), "#"))
fmt.Printf("\n%s\n\n%s", state.Tracker.LastAnounceRequest, state.Tracker.LastTackerResponse)
}
time.Sleep(1 * time.Second)
}
}
}
func terminalSize() int {
size, _ := ts.GetSize()
width := size.Col()
if width < 40 {
width = 40
}
return width
}
func clear() {
if runtime.GOOS == "windows" {
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
cmd.Run()
} else {
fmt.Print("\033c")
}
}
func center(s string, n int, fill string) string {
div := n / 2
return strings.Repeat(fill, div) + s + strings.Repeat(fill, div)
}
func humanReadableSize(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)
}
func fmtDuration(d time.Duration) string {
if d.Seconds() < 0 {
return fmt.Sprintf("%s", 0*time.Second)
}
return fmt.Sprintf("%s", time.Duration(int(d.Seconds()))*time.Second)
}

View file

@ -1,31 +0,0 @@
package printer
import (
"fmt"
"testing"
)
func TestHumanReadableSize(T *testing.T) {
data := []struct {
in float64
out string
}{
{1536, "1.50KiB"},
{379040563, "361.48MiB"},
{6291456, "6.00MiB"},
{372749107, "355.48MiB"},
{10485760, "10.00MiB"},
{15728640, "15.00MiB"},
{363311923, "346.48MiB"},
{16777216, "16.00MiB"},
{379040563, "361.48MiB"},
}
for idx, td := range data {
T.Run(fmt.Sprint(idx), func(t *testing.T) {
got := humanReadableSize(td.in)
if got != td.out {
t.Errorf("got %q, want %q", got, td.out)
}
})
}
}

333
ratio-spoof.py Executable file
View file

@ -0,0 +1,333 @@
#!/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):
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.__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)
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)}
""")
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):
query = {
'peer_id':state.peer_id,
'port':8999,
'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):
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))
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)
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)

View file

@ -1,195 +0,0 @@
package ratiospoof
import (
"errors"
"fmt"
"github.com/ap-pauloafonso/ratio-spoof/bencode"
"github.com/ap-pauloafonso/ratio-spoof/emulation"
"github.com/ap-pauloafonso/ratio-spoof/input"
"github.com/ap-pauloafonso/ratio-spoof/tracker"
"log"
"math/rand"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/gammazero/deque"
)
const (
maxAnnounceHistory = 10
)
type RatioSpoof struct {
TorrentInfo *bencode.TorrentInfo
Input *input.InputParsed
Tracker *tracker.HttpTracker
BitTorrentClient *emulation.Emulation
AnnounceInterval int
NumWant int
Seeders int
Leechers int
AnnounceCount int
Status string
AnnounceHistory announceHistory
Print bool
}
type AnnounceEntry struct {
Count int
Downloaded int
PercentDownloaded float32
Uploaded int
Left int
}
type announceHistory struct {
deque.Deque
}
func NewRatioSpoofState(input input.InputArgs) (*RatioSpoof, error) {
dat, err := os.ReadFile(input.TorrentPath)
if err != nil {
return nil, err
}
client, err := emulation.NewEmulation(input.Client)
if err != nil {
return nil, errors.New("Error building the emulated client with the code")
}
torrentInfo, err := bencode.TorrentDictParse(dat)
if err != nil {
return nil, errors.New("failed to parse the torrent file")
}
httpTracker, err := tracker.NewHttpTracker(torrentInfo)
if err != nil {
return nil, err
}
inputParsed, err := input.ParseInput(torrentInfo)
if err != nil {
return nil, err
}
return &RatioSpoof{
BitTorrentClient: client,
TorrentInfo: torrentInfo,
Tracker: httpTracker,
Input: inputParsed,
NumWant: 200,
Status: "started",
Print: true,
}, nil
}
func (a *announceHistory) pushValueHistory(value AnnounceEntry) {
if a.Len() >= maxAnnounceHistory {
a.PopFront()
}
a.PushBack(value)
}
func (r *RatioSpoof) gracefullyExit() {
fmt.Printf("\nGracefully exiting...\n")
r.Status = "stopped"
r.NumWant = 0
r.fireAnnounce(false)
fmt.Printf("Gracefully exited successfully.\n")
}
func (r *RatioSpoof) Run() {
sigCh := make(chan os.Signal)
signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
r.firstAnnounce()
go func() {
for {
r.generateNextAnnounce()
time.Sleep(time.Duration(r.AnnounceInterval) * time.Second)
r.fireAnnounce(true)
}
}()
<-sigCh
r.Print = false
r.gracefullyExit()
}
func (r *RatioSpoof) firstAnnounce() {
r.addAnnounce(r.Input.InitialDownloaded, r.Input.InitialUploaded, calculateBytesLeft(r.Input.InitialDownloaded, r.TorrentInfo.TotalSize), (float32(r.Input.InitialDownloaded)/float32(r.TorrentInfo.TotalSize))*100)
r.fireAnnounce(false)
}
func (r *RatioSpoof) updateSeedersAndLeechers(resp tracker.TrackerResponse) {
r.Seeders = resp.Seeders
r.Leechers = resp.Leechers
}
func (r *RatioSpoof) addAnnounce(currentDownloaded, currentUploaded, currentLeft int, percentDownloaded float32) {
r.AnnounceCount++
r.AnnounceHistory.pushValueHistory(AnnounceEntry{Count: r.AnnounceCount, Downloaded: currentDownloaded, Uploaded: currentUploaded, Left: currentLeft, PercentDownloaded: percentDownloaded})
}
func (r *RatioSpoof) fireAnnounce(retry bool) error {
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, err := r.Tracker.Announce(query, r.BitTorrentClient.Headers, retry)
if err != nil {
log.Fatalf("failed to reach the tracker:\n%s ", err.Error())
}
if trackerResp != nil {
r.updateSeedersAndLeechers(*trackerResp)
r.AnnounceInterval = trackerResp.Interval
}
return nil
}
func (r *RatioSpoof) generateNextAnnounce() {
lastAnnounce := r.AnnounceHistory.Back().(AnnounceEntry)
currentDownloaded := lastAnnounce.Downloaded
var downloadCandidate int
if currentDownloaded < r.TorrentInfo.TotalSize {
randomPiecesDownload := rand.Intn(10-1) + 1
downloadCandidate = calculateNextTotalSizeByte(r.Input.DownloadSpeed, currentDownloaded, r.TorrentInfo.PieceSize, r.AnnounceInterval, r.TorrentInfo.TotalSize, randomPiecesDownload)
} else {
downloadCandidate = r.TorrentInfo.TotalSize
}
currentUploaded := lastAnnounce.Uploaded
randomPiecesUpload := rand.Intn(10-1) + 1
uploadCandidate := calculateNextTotalSizeByte(r.Input.UploadSpeed, currentUploaded, r.TorrentInfo.PieceSize, r.AnnounceInterval, 0, randomPiecesUpload)
leftCandidate := calculateBytesLeft(downloadCandidate, r.TorrentInfo.TotalSize)
d, u, l := r.BitTorrentClient.Round(downloadCandidate, uploadCandidate, leftCandidate, r.TorrentInfo.PieceSize)
r.addAnnounce(d, u, l, (float32(d)/float32(r.TorrentInfo.TotalSize))*100)
}
func calculateNextTotalSizeByte(speedBytePerSecond, currentByte, pieceSizeByte, seconds, limitTotalBytes, randomPieces int) int {
if speedBytePerSecond == 0 {
return currentByte
}
totalCandidate := currentByte + (speedBytePerSecond * seconds)
totalCandidate = totalCandidate + (pieceSizeByte * randomPieces)
if limitTotalBytes != 0 && totalCandidate > limitTotalBytes {
return limitTotalBytes
}
return totalCandidate
}
func calculateBytesLeft(currentBytes, totalBytes int) int {
return totalBytes - currentBytes
}

View file

@ -1,15 +0,0 @@
package ratiospoof
import (
"testing"
)
func TestCalculateNextTotalSizeByte(t *testing.T) {
randomPieces := 8
got := calculateNextTotalSizeByte(100*1024, 0, 512, 30, 87979879, randomPieces)
want := 3076096
if got != want {
t.Errorf("\ngot : %v\nwant: %v", got, want)
}
}

View file

@ -1,153 +0,0 @@
package tracker
import (
"bytes"
"compress/gzip"
"errors"
"github.com/ap-pauloafonso/ratio-spoof/bencode"
"io"
"net/http"
"strings"
"time"
)
type HttpTracker struct {
Urls []string
RetryAttempt int
LastAnounceRequest string
LastTackerResponse string
EstimatedTimeToAnnounce time.Time
}
type TrackerResponse struct {
MinInterval int
Interval int
Seeders int
Leechers int
}
func NewHttpTracker(torrentInfo *bencode.TorrentInfo) (*HttpTracker, error) {
var result []string
for _, url := range torrentInfo.TrackerInfo.Urls {
if strings.HasPrefix(url, "http") {
result = append(result, url)
}
}
if len(result) == 0 {
return nil, errors.New("No tcp/http tracker url announce found")
}
return &HttpTracker{Urls: torrentInfo.TrackerInfo.Urls}, nil
}
func (t *HttpTracker) swapFirst(currentIdx int) {
aux := t.Urls[0]
t.Urls[0] = t.Urls[currentIdx]
t.Urls[currentIdx] = aux
}
func (t *HttpTracker) updateEstimatedTimeToAnnounce(interval int) {
t.EstimatedTimeToAnnounce = time.Now().Add(time.Duration(interval) * time.Second)
}
func (t *HttpTracker) handleSuccessfulResponse(resp *TrackerResponse) {
if resp.Interval <= 0 {
resp.Interval = 1800
}
t.updateEstimatedTimeToAnnounce(resp.Interval)
}
func (t *HttpTracker) Announce(query string, headers map[string]string, retry bool) (*TrackerResponse, error) {
defer func() {
t.RetryAttempt = 0
}()
if retry {
retryDelay := 30
for {
trackerResp, err := t.tryMakeRequest(query, headers)
if err != nil {
t.updateEstimatedTimeToAnnounce(retryDelay)
t.RetryAttempt++
time.Sleep(time.Duration(retryDelay) * time.Second)
retryDelay *= 2
if retryDelay > 900 {
retryDelay = 900
}
continue
}
t.handleSuccessfulResponse(trackerResp)
return trackerResp, nil
}
} else {
resp, err := t.tryMakeRequest(query, headers)
if err != nil {
return nil, err
}
t.handleSuccessfulResponse(resp)
return resp, nil
}
}
func (t *HttpTracker) tryMakeRequest(query string, headers map[string]string) (*TrackerResponse, error) {
for idx, baseUrl := range t.Urls {
completeURL := buildFullUrl(baseUrl, query)
t.LastAnounceRequest = completeURL
req, _ := http.NewRequest("GET", completeURL, nil)
for header, value := range headers {
req.Header.Add(header, value)
}
resp, err := http.DefaultClient.Do(req)
if err == nil {
if resp.StatusCode == http.StatusOK {
bytesR, _ := io.ReadAll(resp.Body)
if len(bytesR) == 0 {
continue
}
mimeType := http.DetectContentType(bytesR)
if mimeType == "application/x-gzip" {
gzipReader, _ := gzip.NewReader(bytes.NewReader(bytesR))
bytesR, _ = io.ReadAll(gzipReader)
gzipReader.Close()
}
t.LastTackerResponse = string(bytesR)
decodedResp, err := bencode.Decode(bytesR)
if err != nil {
continue
}
ret, err := extractTrackerResponse(decodedResp)
if err != nil {
continue
}
if idx != 0 {
t.swapFirst(idx)
}
return &ret, nil
}
resp.Body.Close()
}
}
return nil, errors.New("Connection error with the tracker")
}
func buildFullUrl(baseurl, query string) string {
if len(strings.Split(baseurl, "?")) > 1 {
return baseurl + "&" + strings.TrimLeft(query, "&")
}
return baseurl + "?" + strings.TrimLeft(query, "?")
}
func extractTrackerResponse(datatrackerResponse map[string]interface{}) (TrackerResponse, error) {
var result TrackerResponse
if v, ok := datatrackerResponse["failure reason"].(string); ok && len(v) > 0 {
return result, 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, nil
}

View file

@ -1,57 +0,0 @@
package tracker
import (
"github.com/ap-pauloafonso/ratio-spoof/bencode"
"reflect"
"testing"
)
func TestNewHttpTracker(t *testing.T) {
_, err := NewHttpTracker(&bencode.TorrentInfo{TrackerInfo: &bencode.TrackerInfo{Urls: []string{"udp://url1", "udp://url2"}}})
got := err.Error()
want := "No tcp/http tracker url announce found"
if got != want {
t.Errorf("got: %v want %v", got, want)
}
}
func TestSwapFirst(t *testing.T) {
tracker, _ := NewHttpTracker(&bencode.TorrentInfo{TrackerInfo: &bencode.TrackerInfo{Urls: []string{"http://url1", "http://url2", "http://url3", "http://url4"}}})
tracker.swapFirst(3)
got := tracker.Urls
want := []string{"http://url4", "http://url2", "http://url3", "http://url1"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got: %v want %v", got, want)
}
}
func TestHandleSuccessfulResponse(t *testing.T) {
t.Run("Empty interval should be overided with 1800 ", func(t *testing.T) {
tracker, _ := NewHttpTracker(&bencode.TorrentInfo{TrackerInfo: &bencode.TrackerInfo{Urls: []string{"http://url1", "http://url2", "http://url3", "http://url4"}}})
r := TrackerResponse{}
tracker.handleSuccessfulResponse(&r)
got := r.Interval
want := 1800
if !reflect.DeepEqual(got, want) {
t.Errorf("got: %v want %v", got, want)
}
})
t.Run("Valid interval shouldn't be overwritten", func(t *testing.T) {
tracker, _ := NewHttpTracker(&bencode.TorrentInfo{TrackerInfo: &bencode.TrackerInfo{Urls: []string{"http://url1", "http://url2", "http://url3", "http://url4"}}})
r := TrackerResponse{Interval: 900}
tracker.handleSuccessfulResponse(&r)
got := r.Interval
want := 900
if !reflect.DeepEqual(got, want) {
t.Errorf("got: %v want %v", got, want)
}
})
}