Compare commits

...

51 commits

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

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

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

3
.gitignore vendored
View file

@ -131,4 +131,5 @@ dmypy.json
#vscode folder
/.vscode
out/
out/
.idea/

15
Makefile Normal file
View file

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

View file

@ -4,9 +4,10 @@ Ratio-spoof is a cross-platform, free and open source tool to spoof the download
![](./assets/demo.gif)
## Motivation
Here in brazil, not everybody has a great upload speed, and most of the private trackers requires a ratio to be greater than or equal to 1 (e.g. if you downloaded 1gb you must upload 1gb as well) in order to survive. Plus, i have always been fascinated by the bittorrent protocol, [i even made a bittorrent webclient to learn a bit about it ](https://github.com/ap-pauloafonso/rwTorrent) so with the current global covid-19 lockdown i got some free time and decided to code my own simple cli tool to spoof bittorrent trackers.
Here in Brazil, not everybody has a great upload speed, and most private trackers require a ratio greater than or equal to 1. For example, if you downloaded 1GB, you must also upload 1GB in order to survive. Additionally, I have always been fascinated by the BitTorrent protocol. In fact, [I even made a BitTorrent web client to learn more about it](https://github.com/ap-pauloafonso/rwTorrent). So, if you have a bad internet connection, feel free to use this tool. Otherwise, please consider seeding the files with a real torrent client.
## How does it works?
## How does it work?
![Diagram](./assets/how-it-works.png)
Bittorrent protocol works in such a way that there is no way that a tracker knows how much certain peer have downloaded or uploaded, so the tracker depends on the peer itself telling the amounts.
Ratio-spoof acts like a normal bittorrent client but without downloading or uploading anything, in fact it just tricks the tracker pretending that.
@ -18,7 +19,8 @@ usage:
optional arguments:
-h show this help message and exit
-p [PORT] change the port number, the default is 8999
-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>
@ -28,7 +30,8 @@ required arguments:
-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
```
```
@ -43,14 +46,14 @@ required arguments:
* 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/how-it-works.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View file

@ -1,86 +0,0 @@
package beencode
import (
"strconv"
)
const (
dictToken = byte('d')
numberToken = byte('i')
listToken = byte('l')
endOfCollectionToken = byte('e')
lengthValueStringSeparator = byte(':')
)
//Decode accepts a byte slice and returns a map with information parsed.(panic if it fails)
func Decode(data []byte) map[string]interface{} {
result, _ := findParse(0, &data)
return result.(map[string]interface{})
}
func findParse(currentIdx int, data *[]byte) (result interface{}, nextIdx int) {
token := (*data)[currentIdx : currentIdx+1][0]
switch {
case token == dictToken:
return mapParse(currentIdx, data)
case token == numberToken:
return numberParse(currentIdx, data)
case token == listToken:
return listParse(currentIdx, data)
case token >= byte('0') || token <= byte('9'):
return stringParse(currentIdx, data)
default:
panic("Error decoding beencode")
}
}
func mapParse(startIdx int, data *[]byte) (result map[string]interface{}, nextIdx int) {
result = make(map[string]interface{})
initialMapIndex := startIdx
current := startIdx + 1
for (*data)[current : current+1][0] != endOfCollectionToken {
mapKey, next := findParse(current, data)
current = next
mapValue, next := findParse(current, data)
current = next
result[mapKey.(string)] = mapValue
}
current++
result["byte_offsets"] = []int{initialMapIndex, current}
nextIdx = current
return
}
func listParse(startIdx int, data *[]byte) (result []interface{}, nextIdx int) {
current := startIdx + 1
for (*data)[current : current+1][0] != endOfCollectionToken {
value, next := findParse(current, data)
result = append(result, value)
current = next
}
current++
nextIdx = current
return
}
func numberParse(startIdx int, data *[]byte) (result int, nextIdx int) {
current := startIdx
for (*data)[current : current+1][0] != endOfCollectionToken {
current++
}
value, _ := strconv.Atoi(string((*data)[startIdx+1 : current]))
result = value
nextIdx = current + 1
return
}
func stringParse(startIdx int, data *[]byte) (result string, nextIdx int) {
current := startIdx
for (*data)[current : current+1][0] != lengthValueStringSeparator {
current++
}
sizeStr, _ := strconv.Atoi(string(((*data)[startIdx:current])))
result = string((*data)[current+1 : current+1+int(sizeStr)])
nextIdx = current + 1 + int(sizeStr)
return
}

201
bencode/bencode.go Normal file
View file

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

View file

@ -1,8 +1,8 @@
package beencode
package bencode
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
)
@ -99,14 +99,15 @@ func TestMapParse(T *testing.T) {
func TestDecode(T *testing.T) {
files, err := ioutil.ReadDir("./torrent_files_test")
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, _ := ioutil.ReadFile("./torrent_files_test/" + f.Name())
t.Log(Decode(data)["info"].(map[string]interface{})["name"])
data, _ := os.ReadFile("./torrent_files_test/" + f.Name())
result, _ := Decode(data)
t.Log(result["info"].(map[string]interface{})["name"])
})
}

View file

@ -1,17 +0,0 @@
#!/bin/sh
version=$1
if [ -z "$version" ]; then
echo "usage: $0 <version>"
exit 1
fi
rm -rf ./out
env GOOS=darwin GOARCH=amd64 go build -v -o ./out/mac/ratio-spoof github.com/ap-pauloafonso/ratio-spoof/cmd
env GOOS=linux GOARCH=amd64 go build -v -o ./out/linux/ratio-spoof github.com/ap-pauloafonso/ratio-spoof/cmd
env GOOS=windows GOARCH=amd64 go build -v -o ./out/windows/ratio-spoof.exe github.com/ap-pauloafonso/ratio-spoof/cmd
cd out/
zip ratio-spoof-$version\(linux-mac-windows\).zip -r .

95
emulation/emulation.go Normal file
View file

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

View file

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

View file

@ -0,0 +1,17 @@
{
"name":"qBittorrent v4.0.3",
"peerId":{
"regex":"-qB4030-[A-Za-z0-9_~\\(\\)\\!\\.\\*-]{12}"
},
"key": {
"generator":"defaultKeyGenerator"
},
"rounding": {
"generator":"defaultRoudingGenerator"
},
"query":"info_hash={infohash}&peer_id={peerid}&port={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&key={key}&event={event}&numwant={numwant}&compact=1&no_peer_id=1&supportcrypto=1&redundant=0",
"headers":{
"User-Agent" :"qBittorrent/4.0.3",
"Accept-Encoding": "gzip"
}
}

View file

@ -0,0 +1,17 @@
{
"name":"qBittorrent v4.3.3",
"peerId":{
"regex":"-qB4330-[A-Za-z0-9_~\\(\\)\\!\\.\\*-]{12}"
},
"key": {
"generator":"defaultKeyGenerator"
},
"rounding": {
"generator":"defaultRoudingGenerator"
},
"query":"info_hash={infohash}&peer_id={peerid}&port={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&key={key}&event={event}&numwant={numwant}&compact=1&no_peer_id=1&supportcrypto=1&redundant=0",
"headers":{
"User-Agent" :"qBittorrent/4.3.3",
"Accept-Encoding": "gzip"
}
}

23
generator/key.go Normal file
View file

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

14
generator/key_test.go Normal file
View file

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

21
generator/peerId.go Normal file
View file

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

16
generator/rouding.go Normal file
View file

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

View file

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

11
go.mod
View file

@ -1,9 +1,16 @@
module github.com/ap-pauloafonso/ratio-spoof
go 1.15
go 1.20
require (
github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
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
)

29
go.sum
View file

@ -1,14 +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/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/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/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

188
input/input.go Normal file
View file

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

245
input/input_test.go Normal file
View file

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

View file

@ -3,11 +3,11 @@ package main
import (
"flag"
"fmt"
"net/http"
"runtime"
"github.com/ap-pauloafonso/ratio-spoof/qbittorrent"
"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() {
@ -22,21 +22,15 @@ func main() {
//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() {
var osExecutableSuffix string
if runtime.GOOS == "windows" {
fmt.Println(`usage:
ratio-spoof.exe -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> -ds <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> -us <UPLOAD_SPEED>`, osExecutableSuffix)
} else {
fmt.Println(`usage:
./ratio-spoof -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> -ds <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> -us <UPLOAD_SPEED>`, osExecutableSuffix)
}
fmt.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, the default is 8999
-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>
@ -47,6 +41,7 @@ required arguments:
<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
`)
}
@ -57,9 +52,8 @@ required arguments:
return
}
qbit := qbittorrent.NewQbitTorrent()
r, err := ratiospoof.NewRatioSPoofState(
ratiospoof.InputArgs{
r, err := ratiospoof.NewRatioSpoofState(
input.InputArgs{
TorrentPath: *torrentPath,
InitialDownloaded: *initialDownload,
DownloadSpeed: *downloadSpeed,
@ -67,13 +61,14 @@ required arguments:
UploadSpeed: *uploadSpeed,
Port: *port,
Debug: *debug,
},
qbit,
http.DefaultClient)
Client: *client,
})
if err != nil {
panic(err)
log.Fatalln(err)
}
go printer.PrintState(r)
r.Run()
}

118
printer/printer.go Normal file
View file

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

31
printer/printer_test.go Normal file
View file

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

View file

@ -1,83 +0,0 @@
package qbittorrent
import (
"encoding/hex"
"math/rand"
"strings"
"time"
)
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
name = "qBittorrent v4.03"
query = "info_hash={infohash}&peer_id={peerid}&port={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&key={key}&event={event}&numwant={numwant}&compact=1&no_peer_id=1&supportcrypto=1&redundant=0"
)
type TypeTest struct {
}
type qbitTorrent struct {
name string
query string
dictHeaders map[string]string
key string
peerID string
}
func NewQbitTorrent() *qbitTorrent {
return &qbitTorrent{
name: name,
query: query,
dictHeaders: generateHeaders(),
key: generateKey(),
peerID: generatePeerID(),
}
}
func (qb *qbitTorrent) Name() string {
return qb.name
}
func (qb *qbitTorrent) PeerID() string {
return qb.peerID
}
func (qb *qbitTorrent) Key() string {
return qb.key
}
func (qb *qbitTorrent) Query() string {
return query
}
func (qb *qbitTorrent) Headers() map[string]string {
return qb.dictHeaders
}
func (qb *qbitTorrent) NextAmountReport(downloadCandidateNextAmount, uploadCandidateNextAmount, leftCandidateNextAmount, pieceSize int) (downloaded, uploaded, left int) {
d := downloadCandidateNextAmount
u := uploadCandidateNextAmount - (uploadCandidateNextAmount % (16 * 1024))
l := leftCandidateNextAmount - (leftCandidateNextAmount % pieceSize)
return d, u, l
}
func generateHeaders() map[string]string {
return map[string]string{"User-Agent": "qBittorrent/4.0.3", "Accept-Encoding": "gzip"}
}
func generatePeerID() string {
return "-qB4030-" + randStringBytes(12)
}
func randStringBytes(n int) string {
rand.Seed(time.Now().UnixNano())
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
func generateKey() string {
randomBytes := make([]byte, 4)
rand.Read(randomBytes)
str := hex.EncodeToString(randomBytes)
return strings.ToUpper(str)
}

View file

@ -1,57 +0,0 @@
package qbittorrent
import (
"strings"
"testing"
)
func TestGenerateRandomPeerId(T *testing.T) {
T.Run("PeerIds are different", func(t *testing.T) {
keys := make(map[string]bool)
for i := 0; i < 10; i++ {
obj := NewQbitTorrent()
key := obj.PeerID()
t.Log(key)
if _, ok := keys[key]; ok {
t.Error("peerId must be random")
break
}
keys[key] = true
}
})
}
func TestGenerateRandomKey(T *testing.T) {
T.Run("Keys are different", func(t *testing.T) {
keys := make(map[string]bool)
for i := 0; i < 10; i++ {
obj := NewQbitTorrent()
key := obj.Key()
t.Log(key)
if _, ok := keys[key]; ok {
t.Error("Keys must be random")
break
}
keys[key] = true
}
})
T.Run("Key has 8 length", func(t *testing.T) {
obj := NewQbitTorrent()
key := obj.Key()
if len(key) != 8 {
t.Error("Keys must have length of 8")
}
})
T.Run("Key must be uppercase", func(t *testing.T) {
obj := NewQbitTorrent()
key := obj.Key()
if strings.ToUpper(key) != key {
t.Error("Keys must be uppercase")
}
})
}

View file

@ -1,647 +0,0 @@
package ratiospoof
import (
"bytes"
"compress/gzip"
"crypto/sha1"
"errors"
"fmt"
"io/ioutil"
"math"
"math/rand"
"net/http"
"net/url"
"os"
"os/exec"
"os/signal"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/ap-pauloafonso/ratio-spoof/beencode"
"github.com/gammazero/deque"
"github.com/olekukonko/ts"
)
const (
maxAnnounceHistory = 10
)
var validInitialSufixes = [...]string{"%", "b", "kb", "mb", "gb", "tb"}
var validSpeedSufixes = [...]string{"kbps", "mbps"}
type ratioSPoofState struct {
mutex *sync.Mutex
httpClient HttpClient
torrentInfo *torrentInfo
input *inputParsed
bitTorrentClient TorrentClient
currentAnnounceTimer int
announceInterval int
numWant int
seeders int
leechers int
announceCount int
status string
announceHistory announceHistory
lastAnounceRequest string
lastTackerResponse string
retryAttempt int
}
type InputArgs struct {
TorrentPath string
InitialDownloaded string
DownloadSpeed string
InitialUploaded string
UploadSpeed string
Port int
Debug bool
}
type inputParsed struct {
torrentPath string
initialDownloaded int
downloadSpeed int
initialUploaded int
uploadSpeed int
port int
debug bool
}
type torrentInfo struct {
name string
pieceSize int
totalSize int
trackerInfo trackerInfo
infoHashURLEncoded string
}
func extractTorrentInfo(torrentPath string) (*torrentInfo, error) {
dat, err := ioutil.ReadFile(torrentPath)
if err != nil {
return nil, err
}
torrentMap := beencode.Decode(dat)
return &torrentInfo{
name: torrentMap["info"].(map[string]interface{})["name"].(string),
pieceSize: torrentMap["info"].(map[string]interface{})["piece length"].(int),
totalSize: extractTotalSize(torrentMap),
trackerInfo: extractTrackerInfo(torrentMap),
infoHashURLEncoded: extractInfoHashURLEncoded(dat, torrentMap),
}, nil
}
func (I *InputArgs) parseInput(torrentInfo *torrentInfo) (*inputParsed, error) {
downloaded, err := extractInputInitialByteCount(I.InitialDownloaded, torrentInfo.totalSize, true)
if err != nil {
return nil, err
}
uploaded, err := extractInputInitialByteCount(I.InitialUploaded, torrentInfo.totalSize, false)
if err != nil {
return nil, err
}
downloadSpeed, err := extractInputByteSpeed(I.DownloadSpeed)
if err != nil {
return nil, err
}
uploadSpeed, err := extractInputByteSpeed(I.UploadSpeed)
if err != nil {
return nil, err
}
if I.Port < 1 || I.Port > 65535 {
return nil, errors.New("port number must be between 1 and 65535")
}
return &inputParsed{initialDownloaded: downloaded,
downloadSpeed: downloadSpeed,
initialUploaded: uploaded,
uploadSpeed: uploadSpeed,
debug: I.Debug,
port: I.Port,
}, nil
}
func NewRatioSPoofState(input InputArgs, torrentClient TorrentClient, httpclient HttpClient) (*ratioSPoofState, error) {
torrentInfo, err := extractTorrentInfo(input.TorrentPath)
if err != nil {
panic(err)
}
inputParsed, err := input.parseInput(torrentInfo)
if err != nil {
panic(err)
}
return &ratioSPoofState{
bitTorrentClient: torrentClient,
httpClient: httpclient,
torrentInfo: torrentInfo,
input: inputParsed,
numWant: 200,
status: "started",
mutex: &sync.Mutex{},
}, nil
}
func checkSpeedSufix(input string) (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 := strSize2ByteSize(initialSizeInput, totalBytes)
if errorIfHigher && byteCount > totalBytes {
return 0, errors.New("initial downloaded can not be higher than the torrent size")
}
if byteCount < 0 {
return 0, errors.New("initial value can not be negative")
}
return byteCount, nil
}
func extractInputByteSpeed(initialSpeedInput string) (int, error) {
ok, suffix := checkSpeedSufix(initialSpeedInput)
if !ok {
return 0, fmt.Errorf("speed must be in %v", validSpeedSufixes)
}
number, _ := strconv.ParseFloat(initialSpeedInput[:len(initialSpeedInput)-4], 64)
if number < 0 {
return 0, errors.New("speed can not be negative")
}
if suffix == "kbps" {
number *= 1024
} else {
number = number * 1024 * 1024
}
ret := int(number)
return ret, nil
}
type trackerInfo struct {
main string
urls []string
}
func (T *trackerInfo) SwapFirst(currentIdx int) {
aux := T.urls[0]
T.urls[0] = T.urls[currentIdx]
T.urls[currentIdx] = aux
}
type trackerResponse struct {
minInterval int
interval int
seeders int
leechers int
}
type TorrentClient interface {
PeerID() string
Key() string
Query() string
Name() string
Headers() map[string]string
NextAmountReport(DownloadCandidateNextAmount, UploadCandidateNextAmount, leftCandidateNextAmount, pieceSize int) (downloaded, uploaded, left int)
}
type HttpClient interface {
Do(req *http.Request) (*http.Response, error)
}
type announceEntry struct {
count int
downloaded int
percentDownloaded float32
uploaded int
left int
}
type announceHistory struct {
deque.Deque
}
func (A *announceHistory) pushValueHistory(value announceEntry) {
if A.Len() >= maxAnnounceHistory {
A.PopFront()
}
A.PushBack(value)
}
func (R *ratioSPoofState) gracefullyExit() {
fmt.Printf("\nGracefully exiting...\n")
R.status = "stopped"
R.numWant = 0
R.fireAnnounce(false)
}
func (R *ratioSPoofState) Run() {
rand.Seed(time.Now().UnixNano())
sigCh := make(chan os.Signal)
stopPrintCh := make(chan string)
signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
R.firstAnnounce()
go R.decreaseTimer()
go R.printState(stopPrintCh)
go func() {
for {
R.generateNextAnnounce()
time.Sleep(time.Duration(R.announceInterval) * time.Second)
R.fireAnnounce(true)
}
}()
<-sigCh
stopPrintCh <- "exit print"
R.gracefullyExit()
}
func (R *ratioSPoofState) firstAnnounce() {
println("Trying to connect to the tracker...")
R.addAnnounce(R.input.initialDownloaded, R.input.initialUploaded, calculateBytesLeft(R.input.initialDownloaded, R.torrentInfo.totalSize), (float32(R.input.initialDownloaded)/float32(R.torrentInfo.totalSize))*100)
R.fireAnnounce(false)
}
func (R *ratioSPoofState) updateInterval(resp trackerResponse) {
if resp.minInterval > 0 {
R.announceInterval = resp.minInterval
} else {
R.announceInterval = resp.interval
}
}
func (R *ratioSPoofState) updateSeedersAndLeechers(resp trackerResponse) {
R.seeders = resp.seeders
R.leechers = resp.leechers
}
func (R *ratioSPoofState) addAnnounce(currentDownloaded, currentUploaded, currentLeft int, percentDownloaded float32) {
R.announceCount++
R.announceHistory.pushValueHistory(announceEntry{count: R.announceCount, downloaded: currentDownloaded, uploaded: currentUploaded, left: currentLeft, percentDownloaded: percentDownloaded})
}
func (R *ratioSPoofState) fireAnnounce(retry bool) {
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())
var trackerResp *trackerResponse
if retry {
retryDelay := 30 * time.Second
for {
exit := false
func() {
defer func() {
if err := recover(); err != nil {
R.changeCurrentTimer(int(retryDelay.Seconds()))
R.retryAttempt++
time.Sleep(retryDelay)
retryDelay *= 2
if retryDelay.Seconds() > 900 {
retryDelay = 900
}
}
}()
trackerResp = R.tryMakeRequest(query)
exit = true
}()
if exit {
break
}
}
} else {
trackerResp = R.tryMakeRequest(query)
}
R.retryAttempt = 0
if trackerResp != nil {
R.updateSeedersAndLeechers(*trackerResp)
R.updateInterval(*trackerResp)
}
}
func (R *ratioSPoofState) generateNextAnnounce() {
R.changeCurrentTimer(R.announceInterval)
lastAnnounce := R.announceHistory.Back().(announceEntry)
currentDownloaded := lastAnnounce.downloaded
var downloadCandidate int
if currentDownloaded < R.torrentInfo.totalSize {
downloadCandidate = calculateNextTotalSizeByte(R.input.downloadSpeed, currentDownloaded, R.torrentInfo.pieceSize, R.currentAnnounceTimer, R.torrentInfo.totalSize)
} else {
downloadCandidate = R.torrentInfo.totalSize
}
currentUploaded := lastAnnounce.uploaded
uploadCandidate := calculateNextTotalSizeByte(R.input.uploadSpeed, currentUploaded, R.torrentInfo.pieceSize, R.currentAnnounceTimer, 0)
leftCandidate := calculateBytesLeft(downloadCandidate, R.torrentInfo.totalSize)
d, u, l := R.bitTorrentClient.NextAmountReport(downloadCandidate, uploadCandidate, leftCandidate, R.torrentInfo.pieceSize)
R.addAnnounce(d, u, l, (float32(d)/float32(R.torrentInfo.totalSize))*100)
}
func (R *ratioSPoofState) decreaseTimer() {
for {
time.Sleep(1 * time.Second)
R.mutex.Lock()
if R.currentAnnounceTimer > 0 {
R.currentAnnounceTimer--
}
R.mutex.Unlock()
}
}
func (R *ratioSPoofState) printState(exitedCH <-chan string) {
terminalSize := func() int {
size, _ := ts.GetSize()
width := size.Col()
if width < 40 {
width = 40
}
return width
}
clear := func() {
if runtime.GOOS == "windows" {
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
cmd.Run()
} else {
fmt.Print("\033c")
}
}
center := func(s string, n int, fill string) string {
div := n / 2
return strings.Repeat(fill, div) + s + strings.Repeat(fill, div)
}
humanReadableSize := func(byteSize float64) string {
var unitFound string
for _, unit := range []string{"B", "KiB", "MiB", "GiB", "TiB"} {
if byteSize < 1024.0 {
unitFound = unit
break
}
byteSize /= 1024.0
}
return fmt.Sprintf("%.2f%v", byteSize, unitFound)
}
fmtDuration := func(seconds int) string {
d := time.Duration(seconds) * time.Second
return fmt.Sprintf("%s", d)
}
exit := false
go func() {
_ = <-exitedCH
exit = true
}()
for {
if exit {
break
}
width := terminalSize()
clear()
if R.announceHistory.Len() > 0 {
seedersStr := fmt.Sprint(R.seeders)
leechersStr := fmt.Sprint(R.leechers)
if R.seeders == 0 {
seedersStr = "not informed"
}
if R.leechers == 0 {
leechersStr = "not informed"
}
var retryStr string
if R.retryAttempt > 0 {
retryStr = fmt.Sprintf("(*Retry %v - check your connection)", R.retryAttempt)
}
fmt.Println(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`, R.torrentInfo.name, R.torrentInfo.trackerInfo.main, seedersStr, leechersStr, humanReadableSize(float64(R.input.downloadSpeed)),
humanReadableSize(float64(R.input.uploadSpeed)), humanReadableSize(float64(R.torrentInfo.totalSize)), R.bitTorrentClient.Name(), R.input.port)
fmt.Println()
fmt.Println()
fmt.Println(center(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF ", width-len(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF "), "#"))
fmt.Println()
for i := 0; i <= R.announceHistory.Len()-2; i++ {
dequeItem := R.announceHistory.At(i).(announceEntry)
fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | announced", dequeItem.count, humanReadableSize(float64(dequeItem.downloaded)), dequeItem.percentDownloaded, humanReadableSize(float64(dequeItem.left)), humanReadableSize(float64(dequeItem.uploaded)))
fmt.Println()
}
lastDequeItem := R.announceHistory.At(R.announceHistory.Len() - 1).(announceEntry)
fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | next announce in: %v %v", lastDequeItem.count,
humanReadableSize(float64(lastDequeItem.downloaded)),
lastDequeItem.percentDownloaded,
humanReadableSize(float64(lastDequeItem.left)),
humanReadableSize(float64(lastDequeItem.uploaded)),
fmtDuration(R.currentAnnounceTimer),
retryStr)
if R.input.debug {
fmt.Println()
fmt.Println()
fmt.Println(center(" DEBUG ", width-len(" DEBUG "), "#"))
fmt.Println()
fmt.Print(R.lastAnounceRequest)
fmt.Println()
fmt.Println()
fmt.Print(R.lastTackerResponse)
}
time.Sleep(1 * time.Second)
}
}
}
func (R *ratioSPoofState) changeCurrentTimer(newAnnounceRate int) {
R.mutex.Lock()
R.currentAnnounceTimer = newAnnounceRate
R.mutex.Unlock()
}
func (R *ratioSPoofState) tryMakeRequest(query string) *trackerResponse {
for idx, url := range R.torrentInfo.trackerInfo.urls {
completeURL := url + "?" + strings.TrimLeft(query, "?")
R.lastAnounceRequest = completeURL
req, _ := http.NewRequest("GET", completeURL, nil)
for header, value := range R.bitTorrentClient.Headers() {
req.Header.Add(header, value)
}
resp, err := R.httpClient.Do(req)
if err == nil {
if resp.StatusCode == http.StatusOK {
bytesR, _ := ioutil.ReadAll(resp.Body)
if len(bytesR) == 0 {
return nil
}
mimeType := http.DetectContentType(bytesR)
if mimeType == "application/x-gzip" {
gzipReader, _ := gzip.NewReader(bytes.NewReader(bytesR))
bytesR, _ = ioutil.ReadAll(gzipReader)
gzipReader.Close()
}
R.lastTackerResponse = string(bytesR)
decodedResp := beencode.Decode(bytesR)
if idx != 0 {
R.torrentInfo.trackerInfo.SwapFirst(idx)
}
ret := extractTrackerResponse(decodedResp)
return &ret
}
resp.Body.Close()
}
}
panic("Connection error with the tracker")
}
func calculateNextTotalSizeByte(speedBytePerSecond, currentByte, pieceSizeByte, seconds, limitTotalBytes int) int {
if speedBytePerSecond == 0 {
return currentByte
}
totalCandidate := currentByte + (speedBytePerSecond * seconds)
randomPieces := rand.Intn(10-1) + 1
totalCandidate = totalCandidate + (pieceSizeByte * randomPieces)
if limitTotalBytes != 0 && totalCandidate > limitTotalBytes {
return limitTotalBytes
}
return totalCandidate
}
func extractInfoHashURLEncoded(rawData []byte, torrentData map[string]interface{}) string {
byteOffsets := torrentData["info"].(map[string]interface{})["byte_offsets"].([]int)
h := sha1.New()
h.Write([]byte(rawData[byteOffsets[0]:byteOffsets[1]]))
ret := h.Sum(nil)
return url.QueryEscape(string(ret))
}
func extractTotalSize(torrentData map[string]interface{}) int {
if value, ok := torrentData["info"].(map[string]interface{})["length"]; ok {
return value.(int)
}
var total int
for _, file := range torrentData["info"].(map[string]interface{})["files"].([]interface{}) {
total += file.(map[string]interface{})["length"].(int)
}
return total
}
func extractTrackerInfo(torrentData map[string]interface{}) trackerInfo {
uniqueUrls := make(map[string]int)
currentCount := 0
if main, ok := torrentData["announce"]; ok && strings.HasPrefix(main.(string), "http") {
if _, found := uniqueUrls[main.(string)]; !found {
uniqueUrls[main.(string)] = currentCount
currentCount++
}
}
if list, ok := torrentData["announce-list"]; ok {
for _, innerList := range list.([]interface{}) {
for _, item := range innerList.([]interface{}) {
if _, found := uniqueUrls[item.(string)]; !found && strings.HasPrefix(item.(string), "http") {
uniqueUrls[item.(string)] = currentCount
currentCount++
}
}
}
}
trackerInfo := trackerInfo{urls: make([]string, len(uniqueUrls))}
for key, value := range uniqueUrls {
trackerInfo.urls[value] = key
}
trackerInfo.main = trackerInfo.urls[0]
if len(trackerInfo.urls) == 0 {
panic("No tcp/http tracker url announce found'")
}
return trackerInfo
}
func extractTrackerResponse(datatrackerResponse map[string]interface{}) trackerResponse {
var result trackerResponse
if v, ok := datatrackerResponse["failure reason"].(string); ok && len(v) > 0 {
panic(errors.New(v))
}
result.minInterval, _ = datatrackerResponse["min interval"].(int)
result.interval, _ = datatrackerResponse["interval"].(int)
result.seeders, _ = datatrackerResponse["complete"].(int)
result.leechers, _ = datatrackerResponse["incomplete"].(int)
return result
}
func calculateBytesLeft(currentBytes, totalBytes int) int {
return totalBytes - currentBytes
}
func strSize2ByteSize(input string, totalSize int) int {
lowerInput := strings.ToLower(input)
parseStrNumberFn := func(strWithSufix string, sufixLength, n int) int {
v, _ := strconv.ParseFloat(strWithSufix[:len(lowerInput)-sufixLength], 64)
result := v * math.Pow(1024, float64(n))
return int(result)
}
switch {
case strings.HasSuffix(lowerInput, "kb"):
{
return parseStrNumberFn(lowerInput, 2, 1)
}
case strings.HasSuffix(lowerInput, "mb"):
{
return parseStrNumberFn(lowerInput, 2, 2)
}
case strings.HasSuffix(lowerInput, "gb"):
{
return parseStrNumberFn(lowerInput, 2, 3)
}
case strings.HasSuffix(lowerInput, "tb"):
{
return parseStrNumberFn(lowerInput, 2, 4)
}
case strings.HasSuffix(lowerInput, "b"):
{
return parseStrNumberFn(lowerInput, 1, 0)
}
case strings.HasSuffix(lowerInput, "%"):
{
v, _ := strconv.ParseFloat(lowerInput[:len(lowerInput)-1], 64)
if v < 0 || v > 100 {
panic("percent value must be in (0-100)")
}
result := int(float64(v/100) * float64(totalSize))
return result
}
default:
panic("Size not found")
}
}

View file

@ -1,141 +0,0 @@
package ratiospoof
import (
"io/ioutil"
"testing"
"github.com/ap-pauloafonso/ratio-spoof/beencode"
)
func assertAreEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("\ngot: %v\n want: %v", got, want)
}
}
// func TestStrSize2ByteSize(T *testing.T) {
// T.Run("100kb", func(t *testing.T) {
// got := strSize2ByteSize("100kb", 100)
// want := 102400
// assertAreEqual(t, got, want)
// })
// T.Run("1kb", func(t *testing.T) {
// got := strSize2ByteSize("1kb", 0)
// want := 1024
// assertAreEqual(t, got, want)
// })
// T.Run("1mb", func(t *testing.T) {
// got := strSize2ByteSize("1mb", 0)
// want := 1048576
// assertAreEqual(t, got, want)
// })
// T.Run("1gb", func(t *testing.T) {
// got := strSize2ByteSize("1gb", 0)
// want := 1073741824
// assertAreEqual(t, got, want)
// })
// T.Run("1.5gb", func(t *testing.T) {
// got := strSize2ByteSize("1.5gb", 0)
// want := 1610612736
// assertAreEqual(t, got, want)
// })
// T.Run("1tb", func(t *testing.T) {
// got := strSize2ByteSize("1tb", 0)
// want := 1099511627776
// assertAreEqual(t, got, want)
// })
// T.Run("1b", func(t *testing.T) {
// got := strSize2ByteSize("1b", 0)
// want := 1
// assertAreEqual(t, got, want)
// })
// T.Run("100%% of 10gb ", func(t *testing.T) {
// got := strSize2ByteSize("100%", 10737418240)
// want := 10737418240
// assertAreEqual(t, got, want)
// })
// T.Run("55%% of 900mb ", func(t *testing.T) {
// got := strSize2ByteSize("55%", 943718400)
// want := 519045120
// assertAreEqual(t, got, want)
// })
// T.Run("55%% of 900mb ", func(t *testing.T) {
// got := strSize2ByteSize("55%", 943718400)
// want := 519045120
// assertAreEqual(t, got, want)
// })
// }
// func TestHumanReadableSize(T *testing.T) {
// T.Run("#1", func(t *testing.T) {
// got := humanReadableSize(1536, true)
// want := "1.50KiB"
// assertAreEqual(t, got, want)
// })
// T.Run("#2", func(t *testing.T) {
// got := humanReadableSize(379040563, true)
// want := "1.50KiB"
// assertAreEqual(t, got, want)
// })
// T.Run("#3", func(t *testing.T) {
// got := humanReadableSize(6291456, true)
// want := "1.50KiB"
// assertAreEqual(t, got, want)
// })
// T.Run("#4", func(t *testing.T) {
// got := humanReadableSize(372749107, true)
// want := "1.50KiB"
// assertAreEqual(t, got, want)
// })
// T.Run("#5", func(t *testing.T) {
// got := humanReadableSize(10485760, true)
// want := "1.50KiB"
// assertAreEqual(t, got, want)
// })
// T.Run("#6", func(t *testing.T) {
// got := humanReadableSize(15728640, true)
// want := "1.50KiB"
// assertAreEqual(t, got, want)
// })
// T.Run("#7", func(t *testing.T) {
// got := humanReadableSize(363311923, true)
// want := "1.50KiB"
// assertAreEqual(t, got, want)
// })
// T.Run("#8", func(t *testing.T) {
// got := humanReadableSize(16777216, true)
// want := "1.50KiB"
// assertAreEqual(t, got, want)
// })
// T.Run("#9", func(t *testing.T) {
// got := humanReadableSize(379040563, true)
// want := "1.50KiB"
// assertAreEqual(t, got, want)
// })
// }
func TestClculateNextTotalSizeByte(T *testing.T) {
got := calculateNextTotalSizeByte(100, 0, 512, 30, 87979879)
want := 3074560
assertAreEqual(T, got, want)
}
func TestUrlEncodeInfoHash(T *testing.T) {
b, _ := ioutil.ReadFile("")
got := extractInfoHashURLEncoded(b, beencode.Decode(b))
want := "%60N%7d%1f%8b%3a%9bT%d5%fc%ad%d1%27%ab5%02%1c%fb%03%b0"
assertAreEqual(T, got, want)
}

195
ratiospoof/ratiospoof.go Normal file
View file

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

View file

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

153
tracker/tracker.go Normal file
View file

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

57
tracker/tracker_test.go Normal file
View file

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