mirror of
https://github.com/ap-pauloafonso/ratio-spoof.git
synced 2026-04-19 23:52:04 +00:00
port to golang
This commit is contained in:
parent
465a5a2e00
commit
3e52da8f14
18 changed files with 1152 additions and 444 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -130,3 +130,5 @@ dmypy.json
|
|||
|
||||
#vscode folder
|
||||
/.vscode
|
||||
|
||||
out/
|
||||
30
README.md
30
README.md
|
|
@ -1,7 +1,7 @@
|
|||
# ratio-spoof
|
||||
Ratio-spoof is a cross-platform, free and open source tool to spoof the download/upload amount on private bittorrent trackers.
|
||||
|
||||

|
||||

|
||||
|
||||
## Motivation
|
||||
Here in brazil, not everybody has a great upload speed, and most of the private trackers requires a ratio to be greater than or equal to 1 (e.g. if you downloaded 1gb you must upload 1gb as well) in order to survive. Plus, i have always been fascinated by the bittorrent protocol, [i even made a bittorrent webclient to learn a bit about it ](https://github.com/ap-pauloafonso/rwTorrent) so with the current global covid-19 lockdown i got some free time and decided to code my own simple cli tool to spoof bittorrent trackers.
|
||||
|
|
@ -12,35 +12,33 @@ Bittorrent protocol works in such a way that there is no way that a tracker know
|
|||
Ratio-spoof acts like a normal bittorrent client but without downloading or uploading anything, in fact it just tricks the tracker pretending that.
|
||||
|
||||
## Usage
|
||||
With a recent python3 version installed, you will be able to run it on linux/macos/windows.
|
||||
```
|
||||
usage: ratio-spoof.py -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> <UPLOAD_SPEED>
|
||||
|
||||
usage:
|
||||
./ratio-spoof -t <TORRENT_PATH> -d <INITIAL_DOWNLOADED> -ds <DOWNLOAD_SPEED> -u <INITIAL_UPLOADED> -us <UPLOAD_SPEED>
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-p [PORT] change the port number, the default is 8999
|
||||
|
||||
|
||||
-h show this help message and exit
|
||||
-p [PORT] change the port number, the default is 8999
|
||||
|
||||
required arguments:
|
||||
-t <TORRENT_PATH> path .torrent file
|
||||
-d <INITIAL_DOWNLOADED> <DOWNLOAD_SPEED>
|
||||
required download arg values
|
||||
-u <INITIAL_UPLOADED> <UPLOAD_SPEED>
|
||||
required upload arg values
|
||||
|
||||
-t <TORRENT_PATH>
|
||||
-d <INITIAL_DOWNLOADED>
|
||||
-ds <DOWNLOAD_SPEED>
|
||||
-u <INITIAL_UPLOADED>
|
||||
-us <UPLOAD_SPEED>
|
||||
|
||||
<INITIAL_DOWNLOADED> and <INITIAL_UPLOADED> must be in %, b, kb, mb, gb, tb
|
||||
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> must be in kbps
|
||||
```
|
||||
|
||||
```
|
||||
./ratio-spoof -d 90% 100kbps -u 0% 1024kbps -t (torrentfile_path)
|
||||
./ratio-spoof -d 90% -ds 100kbps -u 0% -us 1024kbps -t (torrentfile_path)
|
||||
```
|
||||
* Will start "downloading" with the initial value of 90% of the torrent total size at 100 kbps speed until it reaches 100% mark.
|
||||
* Will start "uploading" with the initial value of 0% of the torrent total size at 1024kbps (aka 1mb/s) indefinitely.
|
||||
|
||||
```
|
||||
./ratio-spoof -d 2gb 500kbps -u 1gb 1024kbps -t (torrentfile_path)
|
||||
./ratio-spoof -d 2gb -ds 500kbps -u 1gb -us 1024kbps -t (torrentfile_path)
|
||||
```
|
||||
* Will start "downloading" with the initial value of 2gb downloaded if possible at 500kbps speed until it reaches 100% mark.
|
||||
* Will start "uploading" with the initial value of 1gb uplodead at 1024kbps (aka 1mb/s) indefinitely.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
86
beencode/beencode.go
Normal file
86
beencode/beencode.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
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
|
||||
}
|
||||
113
beencode/beencode_test.go
Normal file
113
beencode/beencode_test.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package beencode
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assertAreEqual(t *testing.T, got, want interface{}) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("got: %v want: %v", got, want)
|
||||
}
|
||||
}
|
||||
func assertAreEqualDeep(t *testing.T, got, want interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got: %v want: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumberParse(T *testing.T) {
|
||||
|
||||
T.Run("Positive number", func(t *testing.T) {
|
||||
input := []byte("i322ed:5:")
|
||||
gotValue, gotNextIdx := numberParse(0, &input)
|
||||
wantValue, wantNextIdx := 322, 5
|
||||
|
||||
assertAreEqual(t, gotValue, wantValue)
|
||||
assertAreEqual(t, gotNextIdx, wantNextIdx)
|
||||
|
||||
})
|
||||
T.Run("Negative number", func(t *testing.T) {
|
||||
input := []byte("i-322ed:5:")
|
||||
gotValue, gotNextIdx := numberParse(0, &input)
|
||||
wantValue, wantNextIdx := -322, 6
|
||||
|
||||
assertAreEqual(t, gotValue, wantValue)
|
||||
assertAreEqual(t, gotNextIdx, wantNextIdx)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringParse(T *testing.T) {
|
||||
|
||||
T.Run("String test 1", func(t *testing.T) {
|
||||
input := []byte("5:color4:blue")
|
||||
gotValue, gotNextIdx := stringParse(0, &input)
|
||||
wantValue, wantNextIdx := "color", 7
|
||||
|
||||
assertAreEqual(t, gotValue, wantValue)
|
||||
assertAreEqual(t, gotNextIdx, wantNextIdx)
|
||||
|
||||
})
|
||||
T.Run("String test 2", func(t *testing.T) {
|
||||
input := []byte("15:metallica_rocksd:4:color")
|
||||
gotValue, gotNextIdx := stringParse(0, &input)
|
||||
wantValue, wantNextIdx := "metallica_rocks", 18
|
||||
|
||||
assertAreEqual(t, gotValue, wantValue)
|
||||
assertAreEqual(t, gotNextIdx, wantNextIdx)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListParse(T *testing.T) {
|
||||
T.Run("list of strings", func(t *testing.T) {
|
||||
input := []byte("l4:spam4:eggsed:5color")
|
||||
gotValue, gotNextIdx := listParse(0, &input)
|
||||
var wantValue []interface{}
|
||||
wantValue = append(wantValue, "spam", "eggs")
|
||||
wantNextIdx := 14
|
||||
assertAreEqualDeep(t, gotValue, wantValue)
|
||||
assertAreEqual(t, gotNextIdx, wantNextIdx)
|
||||
})
|
||||
T.Run("list of numbers", func(t *testing.T) {
|
||||
input := []byte("li322ei400eed:5color")
|
||||
gotValue, gotNextIdx := listParse(0, &input)
|
||||
var wantValue []interface{}
|
||||
wantValue = append(wantValue, 322, 400)
|
||||
wantNextIdx := 12
|
||||
assertAreEqualDeep(t, gotValue, wantValue)
|
||||
assertAreEqual(t, gotNextIdx, wantNextIdx)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapParse(T *testing.T) {
|
||||
T.Run("map with string and list inside", func(t *testing.T) {
|
||||
input := []byte("d13:favorite_band4:tool6:othersl5:qotsaee5:color")
|
||||
gotValue, gotNextIdx := mapParse(0, &input)
|
||||
wantValue := make(map[string]interface{})
|
||||
wantValue["favorite_band"] = "tool"
|
||||
wantValue["others"] = []interface{}{"qotsa"}
|
||||
wantValue["byte_offsets"] = []int{0, 41}
|
||||
wantNextIdx := 41
|
||||
assertAreEqualDeep(t, gotValue, wantValue)
|
||||
assertAreEqual(t, gotNextIdx, wantNextIdx)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDecode(T *testing.T) {
|
||||
|
||||
files, err := ioutil.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"])
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,92 +0,0 @@
|
|||
import socket
|
||||
import struct
|
||||
import sys
|
||||
from enum import Enum
|
||||
import os
|
||||
import json
|
||||
import hashlib
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
|
||||
class BencodeKeys(Enum):
|
||||
dic = 'd'
|
||||
number= 'i'
|
||||
arr = 'l'
|
||||
end_of_collecion = 'e'
|
||||
length_and_value_string_separator = ':'
|
||||
|
||||
|
||||
def string_parse(startIdx, data:bytes):
|
||||
current = startIdx
|
||||
while data[current:current +1].decode() != BencodeKeys.length_and_value_string_separator.value:
|
||||
current= current + 1
|
||||
size = data[startIdx:current].decode()
|
||||
string_nextidx = current+1 + int(size)
|
||||
data_slice = data[current+1:current+1 + int(size)]
|
||||
return (str(data_slice, 'utf-8', 'replace'), string_nextidx)
|
||||
|
||||
def number_parse(startIdx, data:bytes):
|
||||
current = startIdx
|
||||
while data[current:current +1].decode() != BencodeKeys.end_of_collecion.value:
|
||||
current = current +1
|
||||
number_nextidx = current +1
|
||||
data_slice = data[startIdx +1:current]
|
||||
return (int(data_slice), number_nextidx)
|
||||
|
||||
def find_parse(startIdx,data:bytes):
|
||||
c = data[startIdx:startIdx +1].decode()
|
||||
if(c == BencodeKeys.number.value):
|
||||
return number_parse(startIdx, data)
|
||||
elif(c == BencodeKeys.dic.value):
|
||||
return dic_parse(startIdx,data)
|
||||
elif(c == BencodeKeys.arr.value):
|
||||
return list_parse(startIdx,data)
|
||||
elif(str(c).isdigit()):
|
||||
return string_parse(startIdx, data)
|
||||
else:
|
||||
raise Exception('Error parse')
|
||||
|
||||
|
||||
def list_parse(startIdx, data):
|
||||
result = []
|
||||
current = startIdx +1
|
||||
while current < len(data):
|
||||
value, nextIdx = find_parse(current, data)
|
||||
result.append(value)
|
||||
current = nextIdx
|
||||
if (data[current: current+1].decode()== BencodeKeys.end_of_collecion.value):
|
||||
current = current +1
|
||||
break
|
||||
list_nextidx = current
|
||||
return (result, list_nextidx)
|
||||
|
||||
|
||||
def dic_parse(startIdx,data):
|
||||
dic = {}
|
||||
initial_dict_idx = startIdx
|
||||
current = startIdx +1
|
||||
|
||||
while current < len(data):
|
||||
key, nextIdx = find_parse(current, data)
|
||||
current = nextIdx
|
||||
value,nextIdx = find_parse(current, data)
|
||||
dic[key] = value
|
||||
current = nextIdx
|
||||
if (data[current: current+1].decode()==BencodeKeys.end_of_collecion.value):
|
||||
current = current +1
|
||||
final_dict_idx = current
|
||||
dic['byte_offsets'] = [initial_dict_idx,final_dict_idx]
|
||||
break
|
||||
dic_nextidx = current
|
||||
return dic, dic_nextidx
|
||||
|
||||
|
||||
def decode(data:bytes):
|
||||
result,_ = find_parse(0,data)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
3
build.sh
Executable file
3
build.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
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
|
||||
79
cmd/main.go
Normal file
79
cmd/main.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/ap-pauloafonso/ratio-spoof/qbittorrent"
|
||||
"github.com/ap-pauloafonso/ratio-spoof/ratiospoof"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
//required
|
||||
torrentPath := flag.String("t", "", "torrent path")
|
||||
initialDownload := flag.String("d", "", "a INITIAL_DOWNLOADED")
|
||||
downloadSpeed := flag.String("ds", "", "a DOWNLOAD_SPEED")
|
||||
initialUpload := flag.String("u", "", "a INITIAL_UPLOADED")
|
||||
uploadSpeed := flag.String("us", "", "a UPLOAD_SPEED")
|
||||
|
||||
//optional
|
||||
port := flag.Int("p", 8999, "a PORT")
|
||||
debug := flag.Bool("debug", false, "")
|
||||
|
||||
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.Print(`
|
||||
optional arguments:
|
||||
-h show this help message and exit
|
||||
-p [PORT] change the port number, the default is 8999
|
||||
|
||||
required arguments:
|
||||
-t <TORRENT_PATH>
|
||||
-d <INITIAL_DOWNLOADED>
|
||||
-ds <DOWNLOAD_SPEED>
|
||||
-u <INITIAL_UPLOADED>
|
||||
-us <UPLOAD_SPEED>
|
||||
|
||||
<INITIAL_DOWNLOADED> and <INITIAL_UPLOADED> must be in %, b, kb, mb, gb, tb
|
||||
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> must be in kbps
|
||||
`)
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *torrentPath == "" || *initialDownload == "" || *downloadSpeed == "" || *initialUpload == "" || *uploadSpeed == "" {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
qbit := qbittorrent.NewQbitTorrent()
|
||||
r, err := ratiospoof.NewRatioSPoofState(
|
||||
ratiospoof.InputArgs{
|
||||
TorrentPath: *torrentPath,
|
||||
InitialDownloaded: *initialDownload,
|
||||
DownloadSpeed: *downloadSpeed,
|
||||
InitialUploaded: *initialUpload,
|
||||
UploadSpeed: *uploadSpeed,
|
||||
Port: *port,
|
||||
Debug: *debug,
|
||||
},
|
||||
qbit,
|
||||
http.DefaultClient)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r.Run()
|
||||
|
||||
}
|
||||
9
go.mod
Normal file
9
go.mod
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
module github.com/ap-pauloafonso/ratio-spoof
|
||||
|
||||
go 1.15
|
||||
|
||||
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
|
||||
)
|
||||
14
go.sum
Normal file
14
go.sum
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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/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=
|
||||
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/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=
|
||||
76
qbittorrent/qbittorrent.go
Normal file
76
qbittorrent/qbittorrent.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
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 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)
|
||||
}
|
||||
57
qbittorrent/qbittorrent_test.go
Normal file
57
qbittorrent/qbittorrent_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
336
ratio-spoof.py
336
ratio-spoof.py
|
|
@ -1,336 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import bencode_parser
|
||||
import sys
|
||||
import hashlib
|
||||
import urllib.parse
|
||||
import json
|
||||
import random
|
||||
import base64
|
||||
import os
|
||||
import uuid
|
||||
import argparse
|
||||
import time
|
||||
from collections import deque
|
||||
import subprocess
|
||||
import platform
|
||||
import datetime
|
||||
import threading
|
||||
import urllib.request
|
||||
import http.client
|
||||
import gzip
|
||||
import shutil
|
||||
|
||||
|
||||
|
||||
class RatioSpoofState():
|
||||
def __init__(self, torrent_name,download_speed, upload_speed, \
|
||||
announce_rate, current_downloaded, current_uploaded,\
|
||||
piece_size, total_size, announce_info, info_hash_urlencoded, port):
|
||||
self.__lock = threading.Lock()
|
||||
self.torrent_name = torrent_name
|
||||
self.download_speed = download_speed
|
||||
self.upload_speed = upload_speed
|
||||
self.announce_rate = announce_rate
|
||||
self.announce_current_timer = self.announce_rate
|
||||
self.piece_size = piece_size
|
||||
self.total_size = total_size
|
||||
self.announce_info = announce_info
|
||||
self.peer_id = peer_id()
|
||||
self.key = key()
|
||||
self.info_hash_urlencoded = info_hash_urlencoded
|
||||
self.announce_history_deq = deque(maxlen=10)
|
||||
self.deq_count = 0
|
||||
self.numwant = 200
|
||||
self.seeders = None
|
||||
self.leechers = None
|
||||
self.port = port
|
||||
self.__add_announce(current_downloaded, current_uploaded ,next_announce_left_b(current_downloaded, total_size))
|
||||
|
||||
def start_announcing(self):
|
||||
announce_interval = self.__announce('started')
|
||||
threading.Thread(daemon = True, target = (lambda: self.__decrease_timer())).start()
|
||||
threading.Thread(daemon = True, target = (lambda: self.__print_state())).start()
|
||||
while True:
|
||||
self.__generate_next_announce(announce_interval)
|
||||
time.sleep(announce_interval)
|
||||
self.__announce()
|
||||
|
||||
def __add_announce(self, current_downloaded, current_uploaded, left):
|
||||
self.deq_count +=1
|
||||
self.announce_history_deq.append({'count': self.deq_count, 'downloaded':current_downloaded, 'percent': round((current_downloaded/self.total_size) *100) , 'uploaded':current_uploaded,'left': left })
|
||||
|
||||
def __generate_next_announce(self, announce_rate):
|
||||
self.__reset_timer(announce_rate)
|
||||
current_downloaded = self.announce_history_deq[-1]['downloaded']
|
||||
if(self.announce_history_deq[-1]['downloaded'] < self.total_size):
|
||||
current_downloaded = next_announce_total_b(self.download_speed,self.announce_history_deq[-1]['downloaded'], self.piece_size, self.announce_rate, self.total_size)
|
||||
else:
|
||||
self.numwant = 0
|
||||
current_uploaded = next_announce_total_b(self.upload_speed,self.announce_history_deq[-1]['uploaded'], self.piece_size, self.announce_rate)
|
||||
current_left = next_announce_left_b(current_downloaded, self.total_size)
|
||||
self.__add_announce(current_downloaded,current_uploaded,current_left)
|
||||
|
||||
def __announce(self, event = None):
|
||||
last_announce_data = self.announce_history_deq[-1]
|
||||
query_dict = build_query_string(self, last_announce_data, event, self.port)
|
||||
|
||||
error =''
|
||||
|
||||
if (len(self.announce_info['list_of_lists']) > 0):
|
||||
for tier_list in self.announce_info['list_of_lists']:
|
||||
for item in tier_list:
|
||||
try:
|
||||
announce_response = tracker_announce_request(item, query_dict)
|
||||
self.__update_seeders_and_leechers(announce_response)
|
||||
return announce_response['interval']
|
||||
except Exception as e : error = str(e)
|
||||
|
||||
else:
|
||||
url = self.announce_info['main']
|
||||
try:
|
||||
announce_response = tracker_announce_request(url, query_dict)
|
||||
self.__update_seeders_and_leechers(announce_response)
|
||||
return announce_response['interval']
|
||||
except Exception as e : error = str(e)
|
||||
|
||||
raise Exception(f'Connection error with the tracker: {error}')
|
||||
|
||||
def __update_seeders_and_leechers(self, dict):
|
||||
self.seeders = dict['seeders']
|
||||
self.leechers = dict['leechers']
|
||||
|
||||
def __decrease_timer(self):
|
||||
while True:
|
||||
time.sleep(1)
|
||||
with self.__lock:
|
||||
self.announce_current_timer = self.announce_current_timer - 1 if self.announce_current_timer > 0 else 0
|
||||
|
||||
def __reset_timer(self, new_announce_rate = None):
|
||||
if new_announce_rate != None:
|
||||
self.announce_rate = new_announce_rate
|
||||
with self.__lock:
|
||||
self.announce_current_timer = self.announce_rate
|
||||
|
||||
def __print_state(self):
|
||||
while True:
|
||||
clear_screen()
|
||||
print(' RATIO-SPOOF '.center(shutil.get_terminal_size().columns,'#'))
|
||||
print(f"""
|
||||
Torrent: {self.torrent_name}
|
||||
Tracker: {self.announce_info['main']}
|
||||
Seeders: {self.seeders if self.seeders !=None else 'not informed'}
|
||||
Leechers: {self.leechers if self.leechers !=None else 'not informed'}
|
||||
Download Speed: {self.download_speed}KB/s
|
||||
Upload Speed: {self.upload_speed}KB/s
|
||||
Size: {human_readable_size(self.total_size)}
|
||||
Emulation: qBittorrent v4.03 | Port: {self.port}
|
||||
""")
|
||||
print(' GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF '.center(shutil.get_terminal_size().columns, '#'))
|
||||
print()
|
||||
for item in list(self.announce_history_deq)[:len(self.announce_history_deq)-1]:
|
||||
print(f'#{item["count"]} downloaded: {human_readable_size(item["downloaded"])}({item["percent"]}%) | left: {human_readable_size(item["left"])} | uploaded: {human_readable_size(item["uploaded"])} | announced')
|
||||
print(f'#{self.announce_history_deq[-1]["count"]} downloaded: {human_readable_size(self.announce_history_deq[-1]["downloaded"])}({self.announce_history_deq[-1]["percent"]}%) | left: {human_readable_size(self.announce_history_deq[-1]["left"])} | uploaded: {human_readable_size(self.announce_history_deq[-1]["uploaded"])} | next announce in :{str(datetime.timedelta(seconds=self.announce_current_timer))}')
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def human_readable_size(size, decimal_places=2):
|
||||
for unit in ['B','KiB','MiB','GiB','TiB']:
|
||||
if size < 1024.0:
|
||||
break
|
||||
size /= 1024.0
|
||||
return f"{size:.{decimal_places}f}{unit}"
|
||||
|
||||
def clear_screen():
|
||||
if platform.system() == "Windows":
|
||||
subprocess.Popen("cls", shell=True).communicate()
|
||||
else:
|
||||
print("\033c", end="")
|
||||
|
||||
def t_total_size(data):
|
||||
if ('length' in data['info']):
|
||||
return data['info']['length']
|
||||
|
||||
return sum(map(lambda x : x['length'] , data['info']['files']))
|
||||
|
||||
def t_infohash_urlencoded(data, raw_data):
|
||||
info_offsets= data['info']['byte_offsets']
|
||||
info_bytes = hashlib.sha1(raw_data[info_offsets[0]:info_offsets[1]]).digest()
|
||||
return urllib.parse.quote_plus(info_bytes)
|
||||
|
||||
def t_piecesize_b(data):
|
||||
return data['info']['piece length']
|
||||
|
||||
def next_announce_total_b(speed_kbps, b_current, b_piece_size,s_time, b_total_limit = None):
|
||||
if(speed_kbps == 0): return b_current
|
||||
|
||||
total = b_current + (speed_kbps *1024 *s_time)
|
||||
closest_piece_number = int(total / b_piece_size)
|
||||
closest_piece_number = closest_piece_number + random.randint(1,10)
|
||||
next_announce = closest_piece_number *b_piece_size
|
||||
|
||||
if(b_total_limit is not None and next_announce > b_total_limit):
|
||||
return b_total_limit
|
||||
return next_announce
|
||||
|
||||
def next_announce_left_b(b_current, b_total_size):
|
||||
return b_total_size - b_current
|
||||
|
||||
def peer_id():
|
||||
return f'-qB4030-{base64.urlsafe_b64encode(uuid.uuid4().bytes)[:12].decode()}'
|
||||
|
||||
def key():
|
||||
return hex(random.getrandbits(32))[2:].upper()
|
||||
|
||||
def find_approx_current(b_total_size, piece_size, percent):
|
||||
if( percent <= 0): return 0
|
||||
total = (percent/100) * b_total_size
|
||||
current_approx = int(total / piece_size) * piece_size
|
||||
return current_approx
|
||||
|
||||
def build_announce_info(data):
|
||||
announce_info = {'main':data['announce'], 'list_of_lists':data['announce-list'] if 'announce-list' in data else []}
|
||||
tcp_list_of_lists = []
|
||||
for _list in announce_info['list_of_lists']:
|
||||
aux = list(filter(lambda x: x.lower().startswith('http'),_list))
|
||||
if len(aux) >0:
|
||||
tcp_list_of_lists.append(aux)
|
||||
announce_info['list_of_lists'] = tcp_list_of_lists
|
||||
if (not announce_info['main'].startswith('udp')):
|
||||
announce_info['list_of_lists'].insert(-1,[announce_info['main']])
|
||||
|
||||
if(len(announce_info['list_of_lists']) == 0): raise Exception('No tcp/http tracker url announce found')
|
||||
|
||||
return announce_info
|
||||
|
||||
def tracker_announce_request(url, query_string):
|
||||
request = urllib.request.Request(url = f'{url}?{query_string}', headers= {'User-Agent' :'qBittorrent/4.0.3', 'Accept-Encoding':'gzip'})
|
||||
response = urllib.request.urlopen(request).read()
|
||||
try:
|
||||
response = gzip.decompress(response)
|
||||
except:pass
|
||||
|
||||
decoded_response = bencode_parser.decode(response)
|
||||
|
||||
interval = decoded_response.get('min interval',None)
|
||||
if(interval is None):
|
||||
interval = decoded_response.get('interval',None)
|
||||
|
||||
if interval is not None:
|
||||
return { 'interval': int(decoded_response['interval']), 'seeders': decoded_response.get('complete'), 'leechers': decoded_response.get('incomplete') }
|
||||
else: raise Exception(json.dumps(decoded_response))
|
||||
|
||||
def build_query_string(state:RatioSpoofState, curent_info,event, port):
|
||||
query = {
|
||||
'peer_id':state.peer_id,
|
||||
'port':port,
|
||||
'uploaded':curent_info['uploaded'],
|
||||
'downloaded':curent_info['downloaded'],
|
||||
'left':curent_info['left'],
|
||||
'corrupt': 0,
|
||||
'key':state.key,
|
||||
'event':event,
|
||||
'numwant':state.numwant,
|
||||
'compact':1,
|
||||
'no_peer_id': 1,
|
||||
'supportcrypto':1,
|
||||
'redundant':0
|
||||
}
|
||||
|
||||
if(event == None):
|
||||
del(query['event'])
|
||||
|
||||
result = f'info_hash={state.info_hash_urlencoded}&' + urllib.parse.urlencode(query)
|
||||
return result
|
||||
|
||||
def check_initial_value_suffix(input:str, attribute_name):
|
||||
valid_suffixs = ('%', 'b','kb','mb','gb','tb')
|
||||
if not input.lower().endswith(valid_suffixs):
|
||||
raise Exception(f'initial {attribute_name} must be in {valid_suffixs}')
|
||||
|
||||
def check_speed_value_suffix(input:str, attribute_name):
|
||||
valid_suffixs = ('kbps')
|
||||
if not input.lower().endswith(valid_suffixs):
|
||||
raise Exception(f'{attribute_name} speed must be in {valid_suffixs}')
|
||||
|
||||
def percent_validation(n):
|
||||
if n not in range (0, 101):
|
||||
raise Exception ('percent value must be in (0-100)')
|
||||
|
||||
def input_size_2_byte_size(input, total_size ):
|
||||
if input.lower().endswith('kb'):
|
||||
return int((float(input[:-2])) * 1024)
|
||||
elif input.lower().endswith('mb'):
|
||||
return int((float(input[:-2])) * (1024 **2))
|
||||
elif input.lower().endswith('gb'):
|
||||
return int((float(input[:-2])) * (1024 **3))
|
||||
elif input.lower().endswith('tb'):
|
||||
return int((float(input[:-2])) * (1024 **4))
|
||||
elif input.lower().endswith('b'):
|
||||
return int(float(input[:-1]))
|
||||
elif input.endswith('%'):
|
||||
percent_validation(int(float(input[:-1])))
|
||||
return int((float(input[:-1])/100 ) * total_size)
|
||||
else:
|
||||
raise Exception('Size not found')
|
||||
|
||||
def check_downloaded_initial_value(input, total_size_b):
|
||||
size_b =input_size_2_byte_size(input,total_size_b)
|
||||
if size_b > total_size_b:
|
||||
raise Exception('initial downloaded can not be higher than the torrent size')
|
||||
return size_b
|
||||
|
||||
def check_uploaded_initial_value(input, total_size_b):
|
||||
size_b =input_size_2_byte_size(input, total_size_b)
|
||||
return size_b
|
||||
|
||||
def check_speed(input):
|
||||
return int(float(input[:-4]))
|
||||
|
||||
|
||||
def validate_download_args(downloaded_arg, download_speed_arg,total_size_b):
|
||||
check_initial_value_suffix(downloaded_arg,'download')
|
||||
check_speed_value_suffix(download_speed_arg, 'download')
|
||||
|
||||
donwloaded_b = check_downloaded_initial_value(downloaded_arg, total_size_b)
|
||||
speed_kbps = check_speed(download_speed_arg)
|
||||
|
||||
return (donwloaded_b, speed_kbps)
|
||||
|
||||
def validate_upload_args(uploaded_arg, upload_speed_arg, total_size_b):
|
||||
check_initial_value_suffix(uploaded_arg,'upload')
|
||||
check_speed_value_suffix(upload_speed_arg, 'upload')
|
||||
|
||||
uploaded_b = check_uploaded_initial_value(uploaded_arg,total_size_b )
|
||||
speed_kbps = check_speed(upload_speed_arg)
|
||||
return (uploaded_b, speed_kbps)
|
||||
|
||||
|
||||
def read_file(f, args_download, args_upload, port):
|
||||
raw_data = f.read()
|
||||
result = bencode_parser.decode(raw_data)
|
||||
total_size = t_total_size(result)
|
||||
piece_size = t_piecesize_b(result)
|
||||
|
||||
downloaded, download_speed_kbps = validate_download_args(args_download[0], args_download[1], total_size)
|
||||
uploaded_b, upload_speed_kbps = validate_upload_args(args_upload[0], args_upload[1], total_size)
|
||||
|
||||
state = RatioSpoofState(result['info']['name'],download_speed_kbps,upload_speed_kbps,0,\
|
||||
downloaded, uploaded_b, piece_size,total_size,
|
||||
build_announce_info(result),t_infohash_urlencoded(result, raw_data), port)
|
||||
|
||||
state.start_announcing()
|
||||
|
||||
|
||||
tip = """
|
||||
<INITIAL_DOWNLOADED> and <INITIAL_UPLOADED> must be in %, b, kb, mb, gb, tb
|
||||
<DOWNLOAD_SPEED> and <UPLOAD_SPEED> must be in kbps
|
||||
"""
|
||||
parser = argparse.ArgumentParser(epilog=tip, description='ratio-spoof is a open source tool to trick private trackers',formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('-p' ,required=False ,type=int, default=8999, choices=range(1, 65535) ,help='change the port number, the default is 8999' , metavar='[PORT]')
|
||||
group = parser.add_argument_group('required arguments')
|
||||
group.add_argument('-t', required=True, metavar=('<TORRENT_PATH>'), help='path .torrent file' , type=argparse.FileType('rb'))
|
||||
group.add_argument('-d', required=True,help='required download arg values', nargs=2 ,metavar=('<INITIAL_DOWNLOADED>', '<DOWNLOAD_SPEED>'))
|
||||
group.add_argument('-u',required=True,help='required upload arg values ', nargs=2 ,metavar=('<INITIAL_UPLOADED>', '<UPLOAD_SPEED>'))
|
||||
args = parser.parse_args()
|
||||
|
||||
read_file(args.t, args.d, args.u,args.p)
|
||||
570
ratiospoof/ratio-spoof.go
Normal file
570
ratiospoof/ratio-spoof.go
Normal file
|
|
@ -0,0 +1,570 @@
|
|||
package ratiospoof
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"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"}
|
||||
|
||||
type ratioSPoofState struct {
|
||||
mutex *sync.Mutex
|
||||
httpClient HttpClient
|
||||
torrentInfo *torrentInfo
|
||||
input *inputParsed
|
||||
bitTorrentClient TorrentClient
|
||||
announceRate int
|
||||
currentAnnounceTimer int
|
||||
announceInterval int
|
||||
numWant int
|
||||
seeders int
|
||||
leechers int
|
||||
announceCount int
|
||||
status string
|
||||
announceHistory announceHistory
|
||||
lastAnounceRequest string
|
||||
lastTackerResponse string
|
||||
}
|
||||
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 := extractInputKbpsSpeed(I.DownloadSpeed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploadSpeed, err := extractInputKbpsSpeed(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) bool {
|
||||
for _, v := range validSpeedSufixes {
|
||||
|
||||
if strings.HasSuffix(strings.ToLower(input), v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
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 extractInputKbpsSpeed(initialSpeedInput string) (int, error) {
|
||||
if !checkSpeedSufix(initialSpeedInput) {
|
||||
return 0, errors.New("speed must be in kbps")
|
||||
}
|
||||
number, _ := strconv.ParseFloat(initialSpeedInput[:len(initialSpeedInput)-4], 64)
|
||||
|
||||
ret := int(number)
|
||||
|
||||
if ret < 0 {
|
||||
return 0, errors.New("speed can not be negative")
|
||||
}
|
||||
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
|
||||
}
|
||||
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) Run() {
|
||||
R.firstAnnounce()
|
||||
go R.decreaseTimer()
|
||||
go R.printState()
|
||||
for {
|
||||
R.generateNextAnnounce()
|
||||
time.Sleep(time.Duration(R.announceInterval) * time.Second)
|
||||
R.fireAnnounce()
|
||||
}
|
||||
|
||||
}
|
||||
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()
|
||||
R.AfterFirstRequestState()
|
||||
}
|
||||
|
||||
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() {
|
||||
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 := R.tryMakeRequest(query)
|
||||
R.updateSeedersAndLeechers(trackerResp)
|
||||
R.updateInterval(trackerResp)
|
||||
}
|
||||
func (R *ratioSPoofState) generateNextAnnounce() {
|
||||
R.resetTimer(R.announceInterval)
|
||||
lastAnnounce := R.announceHistory.Back().(announceEntry)
|
||||
currentDownloaded := lastAnnounce.downloaded
|
||||
var downloaded int
|
||||
if currentDownloaded < R.torrentInfo.totalSize {
|
||||
downloaded = calculateNextTotalSizeByte(R.input.downloadSpeed, currentDownloaded, R.torrentInfo.pieceSize, R.currentAnnounceTimer, R.torrentInfo.totalSize)
|
||||
} else {
|
||||
downloaded = R.torrentInfo.totalSize
|
||||
}
|
||||
|
||||
currentUploaded := lastAnnounce.uploaded
|
||||
uploaded := calculateNextTotalSizeByte(R.input.uploadSpeed, currentUploaded, R.torrentInfo.pieceSize, R.currentAnnounceTimer, 0)
|
||||
|
||||
left := calculateBytesLeft(downloaded, R.torrentInfo.totalSize)
|
||||
R.addAnnounce(downloaded, uploaded, left, (float32(downloaded)/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() {
|
||||
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)
|
||||
}
|
||||
|
||||
for {
|
||||
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"
|
||||
}
|
||||
fmt.Println(center(" RATIO-SPOOF ", width-len(" RATIO-SPOOF "), "#"))
|
||||
fmt.Printf(`
|
||||
Torrent: %v
|
||||
Tracker: %v
|
||||
Seeders: %v
|
||||
Leechers:%v
|
||||
Download Speed: %vKB/s
|
||||
Upload Speed: %vKB/s
|
||||
Size: %v
|
||||
Emulation: %v | Port: %v`, R.torrentInfo.name, R.torrentInfo.trackerInfo.main, seedersStr, leechersStr, R.input.downloadSpeed,
|
||||
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", lastDequeItem.count, humanReadableSize(float64(lastDequeItem.downloaded)), lastDequeItem.percentDownloaded, humanReadableSize(float64(lastDequeItem.left)), humanReadableSize(float64(lastDequeItem.uploaded)), fmtDuration(R.currentAnnounceTimer))
|
||||
|
||||
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) resetTimer(newAnnounceRate int) {
|
||||
R.announceRate = newAnnounceRate
|
||||
R.mutex.Lock()
|
||||
defer R.mutex.Unlock()
|
||||
R.currentAnnounceTimer = R.announceRate
|
||||
}
|
||||
|
||||
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 {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
bytes, _ := ioutil.ReadAll(resp.Body)
|
||||
mimeType := http.DetectContentType(bytes)
|
||||
if mimeType == "application/x-gzip" {
|
||||
gzipReader, _ := gzip.NewReader(resp.Body)
|
||||
defer gzipReader.Close()
|
||||
bytes, _ = ioutil.ReadAll(gzipReader)
|
||||
}
|
||||
R.lastTackerResponse = string(bytes)
|
||||
decodedResp := beencode.Decode(bytes)
|
||||
if idx != 0 {
|
||||
R.torrentInfo.trackerInfo.SwapFirst(idx)
|
||||
}
|
||||
ret := extractTrackerResponse(decodedResp)
|
||||
return ret
|
||||
}
|
||||
}
|
||||
}
|
||||
panic("Connection error with the tracker")
|
||||
|
||||
}
|
||||
func (R *ratioSPoofState) AfterFirstRequestState() {
|
||||
R.status = ""
|
||||
}
|
||||
|
||||
func calculateNextTotalSizeByte(speedKbps, currentByte, pieceSizeByte, seconds, limitTotalBytes int) int {
|
||||
if speedKbps == 0 {
|
||||
return currentByte
|
||||
}
|
||||
total := currentByte + (speedKbps * 1024 * seconds)
|
||||
closestPieceNumber := int(total / pieceSizeByte)
|
||||
closestPieceNumber += 5
|
||||
nextTotal := closestPieceNumber * pieceSizeByte
|
||||
|
||||
if limitTotalBytes != 0 && nextTotal > limitTotalBytes {
|
||||
return limitTotalBytes
|
||||
}
|
||||
return nextTotal
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
129
ratiospoof/ratio-spoof_test.go
Normal file
129
ratiospoof/ratio-spoof_test.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
package ratiospoof
|
||||
|
||||
import "testing"
|
||||
|
||||
func assertAreEqual(t *testing.T, got, want interface{}) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("got: %v 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)
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue