mirror of
https://github.com/ap-pauloafonso/ratio-spoof.git
synced 2026-05-17 15:21:51 +00:00
Compare commits
61 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
032536eda4 | ||
|
|
6c69b9ec45 | ||
|
|
662252950f | ||
|
|
4cd38be3ec | ||
|
|
6c1fb14a25 | ||
|
|
5f5a6195b5 | ||
|
|
c2146e3a31 | ||
|
|
986583c677 | ||
|
|
1544c0d568 | ||
|
|
5dd3724a53 | ||
|
|
00a239e02e | ||
|
|
aabcc229d1 | ||
|
|
c94fd60f77 | ||
|
|
2bbf97d854 | ||
|
|
6cb7064828 | ||
|
|
4fd2969d82 | ||
|
|
8a630643bb | ||
|
|
2b108990dc | ||
|
|
7821028a9c | ||
|
|
66a16b3cf1 | ||
|
|
84f621707f | ||
|
|
42ec97200b | ||
|
|
5a461e3564 | ||
|
|
f05c12bb19 | ||
|
|
840525eb32 | ||
|
|
422a2c05b5 | ||
|
|
c1a86a8091 | ||
|
|
b976c39e43 | ||
|
|
0dc1a674c7 | ||
|
|
1f3dee1642 | ||
|
|
2b98b4ae5c | ||
|
|
804fe5c0de | ||
|
|
f9ca05394d | ||
|
|
fa01ae1241 | ||
|
|
41b4ac9757 | ||
|
|
7c7bc6d94d | ||
|
|
2a6293e699 | ||
|
|
2ad448ccc8 | ||
|
|
c978a5d054 | ||
|
|
bf8f931386 | ||
|
|
e96353a248 | ||
|
|
b7f0fbaa85 | ||
|
|
d16bfcdded | ||
|
|
74e841efdd | ||
|
|
bcd8f91ebb | ||
|
|
fe5c6bde7a | ||
|
|
4713a9e894 | ||
|
|
1ab6b86dd2 | ||
|
|
b4ae3705a1 | ||
|
|
144c8adf3c | ||
|
|
8c3dc91ce5 | ||
|
|
7f87d5d1b5 | ||
|
|
6c3746af94 | ||
|
|
7302228c43 | ||
|
|
b3c03d6dac | ||
|
|
02ad394ac4 | ||
|
|
715184d5c5 | ||
|
|
6f8436d1ea | ||
|
|
16be853e07 | ||
|
|
3e52da8f14 | ||
|
|
465a5a2e00 |
32 changed files with 1805 additions and 446 deletions
25
.github/workflows/ci.yml
vendored
Normal file
25
.github/workflows/ci.yml
vendored
Normal 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
3
.gitignore
vendored
|
|
@ -130,3 +130,6 @@ dmypy.json
|
|||
|
||||
#vscode folder
|
||||
/.vscode
|
||||
|
||||
out/
|
||||
.idea/
|
||||
15
Makefile
Normal file
15
Makefile
Normal 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 .
|
||||
45
README.md
45
README.md
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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?
|
||||

|
||||
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
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
BIN
assets/how-it-works.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
201
bencode/bencode.go
Normal file
201
bencode/bencode.go
Normal 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
114
bencode/bencode_test.go
Normal 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"])
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
BIN
bencode/torrent_files_test/debian-12.0.0-amd64-DVD-1.iso.torrent
Normal file
BIN
bencode/torrent_files_test/debian-12.0.0-amd64-DVD-1.iso.torrent
Normal file
Binary file not shown.
|
|
@ -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
95
emulation/emulation.go
Normal 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
|
||||
}
|
||||
74
emulation/emulation_test.go
Normal file
74
emulation/emulation_test.go
Normal 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
|
||||
})
|
||||
|
||||
}
|
||||
17
emulation/static/qbit-4.0.3.json
Normal file
17
emulation/static/qbit-4.0.3.json
Normal 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"
|
||||
}
|
||||
}
|
||||
17
emulation/static/qbit-4.3.3.json
Normal file
17
emulation/static/qbit-4.3.3.json
Normal 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
23
generator/key.go
Normal 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
14
generator/key_test.go
Normal 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
21
generator/peerId.go
Normal 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
16
generator/rouding.go
Normal 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
|
||||
}
|
||||
21
generator/rounding_test.go
Normal file
21
generator/rounding_test.go
Normal 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
16
go.mod
Normal 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
33
go.sum
Normal 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
188
input/input.go
Normal 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
245
input/input_test.go
Normal 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
74
main.go
Normal 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()
|
||||
|
||||
}
|
||||
BIN
media/demo.gif
BIN
media/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB |
118
printer/printer.go
Normal file
118
printer/printer.go
Normal 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
31
printer/printer_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
333
ratio-spoof.py
333
ratio-spoof.py
|
|
@ -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
195
ratiospoof/ratiospoof.go
Normal 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
|
||||
}
|
||||
15
ratiospoof/ratiospoof_test.go
Normal file
15
ratiospoof/ratiospoof_test.go
Normal 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
153
tracker/tracker.go
Normal 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
57
tracker/tracker_test.go
Normal 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)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue