mirror of
https://github.com/ap-pauloafonso/ratio-spoof.git
synced 2026-07-27 15:02:15 +00:00
Compare commits
51 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 |
37 changed files with 1609 additions and 1073 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
|
|
@ -131,4 +131,5 @@ dmypy.json
|
|||
#vscode folder
|
||||
/.vscode
|
||||
|
||||
out/
|
||||
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 .
|
||||
19
README.md
19
README.md
|
|
@ -4,9 +4,10 @@ Ratio-spoof is a cross-platform, free and open source tool to spoof the download
|
|||

|
||||
|
||||
## 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.
|
||||
|
|
@ -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
BIN
assets/how-it-works.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
|
|
@ -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
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
|
||||
}
|
||||
|
|
@ -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"])
|
||||
})
|
||||
}
|
||||
|
||||
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.
17
build.sh
17
build.sh
|
|
@ -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
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)
|
||||
}
|
||||
}
|
||||
11
go.mod
11
go.mod
|
|
@ -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
29
go.sum
|
|
@ -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
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
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