Compare commits

...

61 commits
v1.0 ... master

Author SHA1 Message Date
paulo
032536eda4 img 2023-10-25 18:16:16 -03:00
paulo
6c69b9ec45 readme2 2023-10-24 23:51:26 -03:00
paulo
662252950f readme 2023-10-24 23:44:53 -03:00
paulo
4cd38be3ec grammar 2023-07-03 19:48:19 -03:00
paulo
6c1fb14a25 rm println on testfile 2023-07-03 15:47:05 -03:00
paulo
5f5a6195b5 readme update2 2023-07-03 15:24:20 -03:00
paulo
c2146e3a31 change go version github ci2 2023-07-03 15:13:53 -03:00
paulo
986583c677 change go version github ci 2023-07-03 15:11:58 -03:00
paulo
1544c0d568 no cache make test 2023-07-03 15:10:32 -03:00
paulo
5dd3724a53 outdated torrent samples update, fix readme link 2023-07-03 15:05:45 -03:00
ap-pauloafonso
00a239e02e
Merge pull request #21 from ap-pauloafonso/removal-stop-printer-channel
removal of stop printer channel
2021-03-23 19:27:27 -03:00
ap-pauloafonso
aabcc229d1 update readme 2021-03-22 22:26:24 -03:00
ap-pauloafonso
c94fd60f77 removal of stop printer channel 2021-03-22 22:09:38 -03:00
ap-pauloafonso
2bbf97d854
Merge pull request #20 from ap-pauloafonso/channel-removal
estimatedTime to tracker.go proposal
2021-03-19 20:05:27 -03:00
ap-pauloafonso
6cb7064828 camelCase method reciver 2021-03-19 18:52:34 -03:00
ap-pauloafonso
4fd2969d82 estimatedTime to tracker.go 2021-03-18 20:18:59 -03:00
ap-pauloafonso
8a630643bb
Merge pull request #18 from ap-pauloafonso/multiple-clients
Multiple clients
2021-03-16 19:06:04 -03:00
ap-pauloafonso
2b108990dc qbit 4.3.3 2021-03-15 18:46:58 -03:00
ap-pauloafonso
7821028a9c embed interfaces and readme 2021-03-15 00:30:48 -03:00
ap-pauloafonso
66a16b3cf1 ci go-version upgrade 2021-03-15 00:06:56 -03:00
ap-pauloafonso
84f621707f cleanup 2021-03-14 23:58:06 -03:00
ap-pauloafonso
42ec97200b moved interface on the file that uses it 2021-03-14 23:44:35 -03:00
ap-pauloafonso
5a461e3564 f close 2021-03-14 22:13:28 -03:00
ap-pauloafonso
f05c12bb19 round rename 2021-03-13 15:19:37 -03:00
ap-pauloafonso
840525eb32 uppercase makefile 2021-03-12 23:25:07 -03:00
ap-pauloafonso
422a2c05b5 Merge branch 'master' into multiple-clients 2021-03-12 23:19:52 -03:00
ap-pauloafonso
c1a86a8091 initial multiple-client build 2021-03-12 23:15:18 -03:00
ap-pauloafonso
b976c39e43
Merge pull request #17 from ap-pauloafonso/time-untill-printer
usage of time.until on printer.go instead of manually decrementing the the interval on the ratiospoof.go
2021-03-07 14:40:35 -03:00
ap-pauloafonso
0dc1a674c7 :q
:Merge branch 'master' into time-untill-printer
2021-03-05 20:47:15 -03:00
ap-pauloafonso
1f3dee1642 usage of time.until on printer insted of manually decrement the value 2021-03-05 20:44:30 -03:00
ap-pauloafonso
2b98b4ae5c
Merge pull request #14 from ap-pauloafonso/refactor2
Refactor proposal
2021-02-17 00:19:15 -03:00
ap-pauloafonso
804fe5c0de some corrections and tests2 2021-02-11 23:50:37 -03:00
ap-pauloafonso
f9ca05394d some corrections and tests 2021-02-11 23:50:09 -03:00
ap-pauloafonso
fa01ae1241 refactor 2021-02-09 20:32:32 -03:00
ap-pauloafonso
41b4ac9757
Create ci.yml 2021-01-22 18:42:56 -03:00
ap-pauloafonso
7c7bc6d94d
Merge pull request #13 from ap-pauloafonso/fix-bencode-typo
fix typo
2021-01-22 18:33:32 -03:00
ap-pauloafonso
2a6293e699 fix typo 2021-01-22 18:32:08 -03:00
ap-pauloafonso
2ad448ccc8
Merge pull request #12 from ap-pauloafonso/code-cleanup
Code cleanup
2021-01-22 17:41:18 -03:00
ap-pauloafonso
c978a5d054 merge master 2021-01-22 17:27:24 -03:00
ap-pauloafonso
bf8f931386
Merge pull request #11 from ap-pauloafonso/fix-interval
fix interval
2021-01-20 12:49:10 -03:00
ap-pauloafonso
e96353a248 fix interval 2021-01-20 12:48:08 -03:00
ap-pauloafonso
b7f0fbaa85
Merge pull request #10 from ap-pauloafonso/fix-invalid-infohash
fix info hash
2021-01-19 20:03:51 -03:00
ap-pauloafonso
d16bfcdded fix info hash 2021-01-19 20:02:13 -03:00
ap-pauloafonso
74e841efdd mod update 2021-01-19 00:58:06 -03:00
ap-pauloafonso
bcd8f91ebb initial clean up 2021-01-19 00:53:30 -03:00
ap-pauloafonso
fe5c6bde7a
Merge pull request #9 from ap-pauloafonso/qbit-refactor
qbit emulation refactor
2021-01-18 22:18:06 -03:00
ap-pauloafonso
4713a9e894 qbit emulation refactor 2021-01-18 22:16:30 -03:00
ap-pauloafonso
1ab6b86dd2
Merge pull request #6 from ap-pauloafonso/usage-print-refactor
print usage refactor
2021-01-13 00:15:05 -03:00
ap-pauloafonso
b4ae3705a1 print usage refactor 2021-01-13 00:07:42 -03:00
ap-pauloafonso
144c8adf3c quick refactor 2020-12-12 19:52:32 -03:00
ap-pauloafonso
8c3dc91ce5 readme fix 2020-12-06 01:30:37 -03:00
ap-pauloafonso
7f87d5d1b5 helper update 2020-12-06 01:21:21 -03:00
ap-pauloafonso
6c3746af94 retry announce 2020-12-06 01:00:48 -03:00
ap-pauloafonso
7302228c43 mbps speed and qbittorrent announceReport numbers 2020-12-05 23:35:53 -03:00
ap-pauloafonso
b3c03d6dac rand and empty result tracker gracefuly exit 2020-12-01 20:58:06 -03:00
ap-pauloafonso
02ad394ac4 fix sh 2020-11-30 21:05:36 -03:00
ap-pauloafonso
715184d5c5 improve sh script 2020-11-30 20:59:21 -03:00
ap-pauloafonso
6f8436d1ea gracefully exit and empty status fix 2020-11-30 20:28:56 -03:00
ap-pauloafonso
16be853e07 fix gzip reader with already flushed buffer 2020-11-29 23:34:47 -03:00
ap-pauloafonso
3e52da8f14 port to golang 2020-11-29 21:10:48 -03:00
ap-pauloafonso
465a5a2e00 added optional arg for port number 2020-05-03 15:54:35 -03:00
32 changed files with 1805 additions and 446 deletions

25
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,25 @@
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,3 +130,6 @@ dmypy.json
#vscode folder
/.vscode
out/
.idea/

15
Makefile Normal file
View file

@ -0,0 +1,15 @@
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,56 +1,59 @@
# 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.
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.
## How does it works?
## How does it work?
![Diagram](./assets/how-it-works.png)
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.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
-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> 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
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> must be in kbps, mbps
[CLIENT_CODE] options: qbit-4.0.3, qbit-4.3.3
```
```
./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.
## 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.
## 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.
## Bittorrent client supported
The currently emulation is hard coded to be a popular and accepted client qbittorrent v4.0.3.
The default client emulation is qbittorrent v4.0.3, however you can change it by using the -c argument
## Resources
http://www.bittorrent.org/beps/bep_0003.html
https://wiki.theory.org/index.php/BitTorrentSpecification
https://wiki.theory.org/BitTorrentSpecification

BIN
assets/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
assets/how-it-works.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

201
bencode/bencode.go Normal file
View file

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

114
bencode/bencode_test.go Normal file
View file

@ -0,0 +1,114 @@
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"])
})
}
}

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

95
emulation/emulation.go Normal file
View file

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

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

@ -0,0 +1,17 @@
{
"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

@ -0,0 +1,17 @@
{
"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"
}
}

23
generator/key.go Normal file
View file

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

14
generator/key_test.go Normal file
View file

@ -0,0 +1,14 @@
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")
}
})
}

21
generator/peerId.go Normal file
View file

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

16
generator/rouding.go Normal file
View file

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

@ -0,0 +1,21 @@
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 Normal file
View file

@ -0,0 +1,16 @@
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 Normal file
View file

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

188
input/input.go Normal file
View file

@ -0,0 +1,188 @@
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")
}
}

245
input/input_test.go Normal file
View file

@ -0,0 +1,245 @@
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 Normal file
View file

@ -0,0 +1,74 @@
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()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

118
printer/printer.go Normal file
View file

@ -0,0 +1,118 @@
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)
}

31
printer/printer_test.go Normal file
View file

@ -0,0 +1,31 @@
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)
}
})
}
}

View file

@ -1,333 +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):
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)

195
ratiospoof/ratiospoof.go Normal file
View file

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

@ -0,0 +1,15 @@
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)
}
}

153
tracker/tracker.go Normal file
View file

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

57
tracker/tracker_test.go Normal file
View file

@ -0,0 +1,57 @@
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)
}
})
}