mirror of
https://github.com/ap-pauloafonso/ratio-spoof.git
synced 2026-05-22 17:42:07 +00:00
Compare commits
No commits in common. "master" and "v1.0" have entirely different histories.
32 changed files with 446 additions and 1805 deletions
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
|
|
@ -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
3
.gitignore
vendored
|
|
@ -130,6 +130,3 @@ dmypy.json
|
||||||
|
|
||||||
#vscode folder
|
#vscode folder
|
||||||
/.vscode
|
/.vscode
|
||||||
|
|
||||||
out/
|
|
||||||
.idea/
|
|
||||||
15
Makefile
15
Makefile
|
|
@ -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 .
|
|
||||||
41
README.md
41
README.md
|
|
@ -1,59 +1,56 @@
|
||||||
# ratio-spoof
|
# ratio-spoof
|
||||||
Ratio-spoof is a cross-platform, free and open source tool to spoof the download/upload amount on private bittorrent trackers.
|
Ratio-spoof is a cross-platform, free and open source tool to spoof the download/upload amount on private bittorrent trackers.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Motivation
|
## 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?
|
## 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.
|
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.
|
Ratio-spoof acts like a normal bittorrent client but without downloading or uploading anything, in fact it just tricks the tracker pretending that.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
With a recent python3 version installed, you will be able to run it on linux/macos/windows.
|
||||||
```
|
```
|
||||||
usage:
|
usage: ratio-spoof.py -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> <UPLOAD_SPEED>
|
||||||
./ratio-spoof -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> -ds <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> -us <UPLOAD_SPEED>
|
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h show this help message and exit
|
-h, --help 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:
|
required arguments:
|
||||||
-t <TORRENT_PATH>
|
-t <TORRENT_PATH> path .torrent file
|
||||||
-d <INITIAL_DOWNLOADED>
|
-d <INITIAL_DOWNLOADED> <DOWNLOAD_SPEED>
|
||||||
-ds <DOWNLOAD_SPEED>
|
required download arg values
|
||||||
-u <INITIAL_UPLOADED>
|
-u <INITIAL_UPLOADED> <UPLOAD_SPEED>
|
||||||
-us <UPLOAD_SPEED>
|
required upload arg values
|
||||||
|
|
||||||
<INITIAL_DOWNLOADED> and <INITIAL_UPLOADED> must be in %, b, kb, mb, gb, tb
|
<INITIAL_DOWNLOADED> and <INITIAL_UPLOADED> must be in %, b, kb, mb, gb, tb
|
||||||
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> must be in kbps, mbps
|
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> must be in kbps
|
||||||
[CLIENT_CODE] options: qbit-4.0.3, qbit-4.3.3
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
./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 "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.
|
* 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 "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 start "uploading" with the initial value of 1gb uplodead at 1024kbps (aka 1mb/s) indefinitely.
|
||||||
|
|
||||||
## Will I get caught using it ?
|
## 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.
|
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
|
## 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
|
## Resources
|
||||||
http://www.bittorrent.org/beps/bep_0003.html
|
http://www.bittorrent.org/beps/bep_0003.html
|
||||||
|
|
||||||
https://wiki.theory.org/BitTorrentSpecification
|
https://wiki.theory.org/index.php/BitTorrentSpecification
|
||||||
|
|
||||||
|
|
|
||||||
BIN
assets/demo.gif
BIN
assets/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 108 KiB |
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Binary file not shown.
92
bencode_parser.py
Normal file
92
bencode_parser.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
16
go.mod
|
|
@ -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
33
go.sum
|
|
@ -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=
|
|
||||||
188
input/input.go
188
input/input.go
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
74
main.go
|
|
@ -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
BIN
media/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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
333
ratio-spoof.py
Executable 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)
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue