mirror of
https://github.com/ap-pauloafonso/ratio-spoof.git
synced 2026-04-21 08:31:56 +00:00
commit
2b98b4ae5c
16 changed files with 872 additions and 554 deletions
19
cmd/main.go
19
cmd/main.go
|
|
@ -3,11 +3,13 @@ package main
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/ap-pauloafonso/ratio-spoof/emulation"
|
"github.com/ap-pauloafonso/ratio-spoof/internal/emulation"
|
||||||
"github.com/ap-pauloafonso/ratio-spoof/ratiospoof"
|
"github.com/ap-pauloafonso/ratio-spoof/internal/input"
|
||||||
|
"github.com/ap-pauloafonso/ratio-spoof/internal/printer"
|
||||||
|
"github.com/ap-pauloafonso/ratio-spoof/internal/ratiospoof"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -50,8 +52,8 @@ required arguments:
|
||||||
}
|
}
|
||||||
|
|
||||||
qbit := emulation.NewQbitTorrent()
|
qbit := emulation.NewQbitTorrent()
|
||||||
r, err := ratiospoof.NewRatioSPoofState(
|
r, err := ratiospoof.NewRatioSpoofState(
|
||||||
ratiospoof.InputArgs{
|
input.InputArgs{
|
||||||
TorrentPath: *torrentPath,
|
TorrentPath: *torrentPath,
|
||||||
InitialDownloaded: *initialDownload,
|
InitialDownloaded: *initialDownload,
|
||||||
DownloadSpeed: *downloadSpeed,
|
DownloadSpeed: *downloadSpeed,
|
||||||
|
|
@ -60,12 +62,13 @@ required arguments:
|
||||||
Port: *port,
|
Port: *port,
|
||||||
Debug: *debug,
|
Debug: *debug,
|
||||||
},
|
},
|
||||||
qbit,
|
qbit)
|
||||||
http.DefaultClient)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go printer.PrintState(r)
|
||||||
r.Run()
|
r.Run()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,13 @@ type torrentDict struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
//TorrentDictParse decodes the bencoded bytes and builds the torrentInfo file
|
//TorrentDictParse decodes the bencoded bytes and builds the torrentInfo file
|
||||||
func TorrentDictParse(dat []byte) (*TorrentInfo, error) {
|
func TorrentDictParse(dat []byte) (torrent *TorrentInfo, err error) {
|
||||||
|
defer func() {
|
||||||
|
if e := recover(); e != nil {
|
||||||
|
err = e.(error)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
dict, _ := mapParse(0, &dat)
|
dict, _ := mapParse(0, &dat)
|
||||||
torrentMap := torrentDict{resultMap: dict}
|
torrentMap := torrentDict{resultMap: dict}
|
||||||
return &TorrentInfo{
|
return &TorrentInfo{
|
||||||
|
|
@ -54,7 +60,7 @@ func TorrentDictParse(dat []byte) (*TorrentInfo, error) {
|
||||||
TotalSize: torrentMap.extractTotalSize(),
|
TotalSize: torrentMap.extractTotalSize(),
|
||||||
TrackerInfo: torrentMap.extractTrackerInfo(),
|
TrackerInfo: torrentMap.extractTrackerInfo(),
|
||||||
InfoHashURLEncoded: torrentMap.extractInfoHashURLEncoded(dat),
|
InfoHashURLEncoded: torrentMap.extractInfoHashURLEncoded(dat),
|
||||||
}, nil
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (T *torrentDict) extractInfoHashURLEncoded(rawData []byte) string {
|
func (T *torrentDict) extractInfoHashURLEncoded(rawData []byte) string {
|
||||||
|
|
@ -115,10 +121,16 @@ func (T *torrentDict) extractTrackerInfo() *TrackerInfo {
|
||||||
return &trackerInfo
|
return &trackerInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
//Decode accepts a byte slice and returns a map with information parsed.(panic if it fails)
|
//Decode accepts a byte slice and returns a map with information parsed.
|
||||||
func Decode(data []byte) map[string]interface{} {
|
func Decode(data []byte) (dataMap map[string]interface{}, err error) {
|
||||||
|
defer func() {
|
||||||
|
if e := recover(); e != nil {
|
||||||
|
err = e.(error)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
result, _ := findParse(0, &data)
|
result, _ := findParse(0, &data)
|
||||||
return result.(map[string]interface{})
|
return result.(map[string]interface{}), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func findParse(currentIdx int, data *[]byte) (result interface{}, nextIdx int) {
|
func findParse(currentIdx int, data *[]byte) (result interface{}, nextIdx int) {
|
||||||
|
|
@ -106,7 +106,8 @@ func TestDecode(T *testing.T) {
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
T.Run(f.Name(), func(t *testing.T) {
|
T.Run(f.Name(), func(t *testing.T) {
|
||||||
data, _ := ioutil.ReadFile("./torrent_files_test/" + f.Name())
|
data, _ := ioutil.ReadFile("./torrent_files_test/" + f.Name())
|
||||||
t.Log(Decode(data)["info"].(map[string]interface{})["name"])
|
result, _ := Decode(data)
|
||||||
|
t.Log(result["info"].(map[string]interface{})["name"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
188
internal/input/input.go
Normal file
188
internal/input/input.go
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ap-pauloafonso/ratio-spoof/internal/bencode"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minPortNumber = 1
|
||||||
|
maxPortNumber = 65535
|
||||||
|
speedSuffixLength = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
internal/input/input_test.go
Normal file
245
internal/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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ratiospoof
|
package printer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -8,13 +8,14 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ap-pauloafonso/ratio-spoof/internal/ratiospoof"
|
||||||
"github.com/olekukonko/ts"
|
"github.com/olekukonko/ts"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (R *ratioSpoofState) PrintState(exitedCH <-chan string) {
|
func PrintState(state *ratiospoof.RatioSpoof) {
|
||||||
exit := false
|
exit := false
|
||||||
go func() {
|
go func() {
|
||||||
_ = <-exitedCH
|
_ = <-state.StopPrintCH
|
||||||
exit = true
|
exit = true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -24,19 +25,25 @@ func (R *ratioSpoofState) PrintState(exitedCH <-chan string) {
|
||||||
}
|
}
|
||||||
width := terminalSize()
|
width := terminalSize()
|
||||||
clear()
|
clear()
|
||||||
if R.announceHistory.Len() > 0 {
|
|
||||||
seedersStr := fmt.Sprint(R.seeders)
|
if state.AnnounceCount == 1 {
|
||||||
leechersStr := fmt.Sprint(R.leechers)
|
println("Trying to connect to the tracker...")
|
||||||
if R.seeders == 0 {
|
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"
|
seedersStr = "not informed"
|
||||||
}
|
}
|
||||||
|
|
||||||
if R.leechers == 0 {
|
if state.Leechers == 0 {
|
||||||
leechersStr = "not informed"
|
leechersStr = "not informed"
|
||||||
}
|
}
|
||||||
var retryStr string
|
var retryStr string
|
||||||
if R.retryAttempt > 0 {
|
if state.Tracker.RetryAttempt > 0 {
|
||||||
retryStr = fmt.Sprintf("(*Retry %v - check your connection)", R.retryAttempt)
|
retryStr = fmt.Sprintf("(*Retry %v - check your connection)", state.Tracker.RetryAttempt)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s\n", center(" RATIO-SPOOF ", width-len(" RATIO-SPOOF "), "#"))
|
fmt.Printf("%s\n", center(" RATIO-SPOOF ", width-len(" RATIO-SPOOF "), "#"))
|
||||||
fmt.Printf(`
|
fmt.Printf(`
|
||||||
|
|
@ -47,25 +54,25 @@ func (R *ratioSpoofState) PrintState(exitedCH <-chan string) {
|
||||||
Download Speed: %v/s
|
Download Speed: %v/s
|
||||||
Upload Speed: %v/s
|
Upload Speed: %v/s
|
||||||
Size: %v
|
Size: %v
|
||||||
Emulation: %v | Port: %v`, R.torrentInfo.Name, R.torrentInfo.TrackerInfo.Main, seedersStr, leechersStr, humanReadableSize(float64(R.input.downloadSpeed)),
|
Emulation: %v | Port: %v`, state.TorrentInfo.Name, state.TorrentInfo.TrackerInfo.Main, seedersStr, leechersStr, humanReadableSize(float64(state.Input.DownloadSpeed)),
|
||||||
humanReadableSize(float64(R.input.uploadSpeed)), humanReadableSize(float64(R.torrentInfo.TotalSize)), R.bitTorrentClient.Name(), R.input.port)
|
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 "), "#"))
|
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 <= R.announceHistory.Len()-2; i++ {
|
for i := 0; i <= state.AnnounceHistory.Len()-2; i++ {
|
||||||
dequeItem := R.announceHistory.At(i).(announceEntry)
|
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)))
|
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 := R.announceHistory.At(R.announceHistory.Len() - 1).(announceEntry)
|
lastDequeItem := state.AnnounceHistory.At(state.AnnounceHistory.Len() - 1).(ratiospoof.AnnounceEntry)
|
||||||
fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | next announce in: %v %v\n", lastDequeItem.count,
|
fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | next announce in: %v %v\n", lastDequeItem.Count,
|
||||||
humanReadableSize(float64(lastDequeItem.downloaded)),
|
humanReadableSize(float64(lastDequeItem.Downloaded)),
|
||||||
lastDequeItem.percentDownloaded,
|
lastDequeItem.PercentDownloaded,
|
||||||
humanReadableSize(float64(lastDequeItem.left)),
|
humanReadableSize(float64(lastDequeItem.Left)),
|
||||||
humanReadableSize(float64(lastDequeItem.uploaded)),
|
humanReadableSize(float64(lastDequeItem.Uploaded)),
|
||||||
fmtDuration(R.currentAnnounceTimer),
|
fmtDuration(state.CurrentAnnounceTimer),
|
||||||
retryStr)
|
retryStr)
|
||||||
|
|
||||||
if R.input.debug {
|
if state.Input.Debug {
|
||||||
fmt.Printf("\n%s\n", center(" DEBUG ", width-len(" DEBUG "), "#"))
|
fmt.Printf("\n%s\n", center(" DEBUG ", width-len(" DEBUG "), "#"))
|
||||||
fmt.Printf("\n%s\n\n%s", R.lastAnounceRequest, R.lastTackerResponse)
|
fmt.Printf("\n%s\n\n%s", state.Tracker.LastAnounceRequest, state.Tracker.LastTackerResponse)
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ratiospoof
|
package printer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
237
internal/ratiospoof/ratiospoof.go
Normal file
237
internal/ratiospoof/ratiospoof.go
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
package ratiospoof
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ap-pauloafonso/ratio-spoof/internal/bencode"
|
||||||
|
"github.com/ap-pauloafonso/ratio-spoof/internal/input"
|
||||||
|
"github.com/ap-pauloafonso/ratio-spoof/internal/tracker"
|
||||||
|
"github.com/gammazero/deque"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxAnnounceHistory = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
type RatioSpoof struct {
|
||||||
|
mutex *sync.Mutex
|
||||||
|
TorrentInfo *bencode.TorrentInfo
|
||||||
|
Input *input.InputParsed
|
||||||
|
Tracker *tracker.HttpTracker
|
||||||
|
BitTorrentClient TorrentClientEmulation
|
||||||
|
CurrentAnnounceTimer int
|
||||||
|
AnnounceInterval int
|
||||||
|
NumWant int
|
||||||
|
Seeders int
|
||||||
|
Leechers int
|
||||||
|
AnnounceCount int
|
||||||
|
Status string
|
||||||
|
AnnounceHistory announceHistory
|
||||||
|
timerUpdateCh chan int
|
||||||
|
StopPrintCH chan interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorrentClientEmulation interface {
|
||||||
|
PeerID() string
|
||||||
|
Key() string
|
||||||
|
Query() string
|
||||||
|
Name() string
|
||||||
|
Headers() map[string]string
|
||||||
|
NextAmountReport(DownloadCandidateNextAmount, UploadCandidateNextAmount, leftCandidateNextAmount, pieceSize int) (downloaded, uploaded, left int)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnnounceEntry struct {
|
||||||
|
Count int
|
||||||
|
Downloaded int
|
||||||
|
PercentDownloaded float32
|
||||||
|
Uploaded int
|
||||||
|
Left int
|
||||||
|
}
|
||||||
|
|
||||||
|
type announceHistory struct {
|
||||||
|
deque.Deque
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRatioSpoofState(input input.InputArgs, torrentClient TorrentClientEmulation) (*RatioSpoof, error) {
|
||||||
|
changeTimerCh := make(chan int)
|
||||||
|
stopPrintCh := make(chan interface{})
|
||||||
|
dat, err := ioutil.ReadFile(input.TorrentPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentInfo, err := bencode.TorrentDictParse(dat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to parse the torrent file")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpTracker, err := tracker.NewHttpTracker(torrentInfo, changeTimerCh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inputParsed, err := input.ParseInput(torrentInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RatioSpoof{
|
||||||
|
BitTorrentClient: torrentClient,
|
||||||
|
TorrentInfo: torrentInfo,
|
||||||
|
Tracker: httpTracker,
|
||||||
|
Input: inputParsed,
|
||||||
|
NumWant: 200,
|
||||||
|
Status: "started",
|
||||||
|
mutex: &sync.Mutex{},
|
||||||
|
timerUpdateCh: changeTimerCh,
|
||||||
|
StopPrintCH: stopPrintCh,
|
||||||
|
}, 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() {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
sigCh := make(chan os.Signal)
|
||||||
|
|
||||||
|
signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
R.firstAnnounce()
|
||||||
|
go R.decreaseTimer()
|
||||||
|
go R.updateTimer()
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
R.generateNextAnnounce()
|
||||||
|
time.Sleep(time.Duration(R.AnnounceInterval) * time.Second)
|
||||||
|
R.fireAnnounce(true)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
<-sigCh
|
||||||
|
R.StopPrintCH <- "exit print"
|
||||||
|
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) updateInterval(resp tracker.TrackerResponse) {
|
||||||
|
if resp.Interval > 0 {
|
||||||
|
R.AnnounceInterval = resp.Interval
|
||||||
|
} else {
|
||||||
|
R.AnnounceInterval = 1800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, R.timerUpdateCh)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to reach the tracker:\n%s ", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if trackerResp != nil {
|
||||||
|
R.updateSeedersAndLeechers(*trackerResp)
|
||||||
|
R.updateInterval(*trackerResp)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (R *RatioSpoof) generateNextAnnounce() {
|
||||||
|
R.timerUpdateCh <- 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.AnnounceInterval, R.TorrentInfo.TotalSize)
|
||||||
|
} else {
|
||||||
|
downloadCandidate = R.TorrentInfo.TotalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUploaded := lastAnnounce.Uploaded
|
||||||
|
uploadCandidate := calculateNextTotalSizeByte(R.Input.UploadSpeed, currentUploaded, R.TorrentInfo.PieceSize, R.AnnounceInterval, 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 *RatioSpoof) decreaseTimer() {
|
||||||
|
for {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
R.mutex.Lock()
|
||||||
|
if R.CurrentAnnounceTimer > 0 {
|
||||||
|
R.CurrentAnnounceTimer--
|
||||||
|
}
|
||||||
|
R.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (R *RatioSpoof) updateTimer() {
|
||||||
|
for {
|
||||||
|
newValue := <-R.timerUpdateCh
|
||||||
|
R.mutex.Lock()
|
||||||
|
R.CurrentAnnounceTimer = newValue
|
||||||
|
R.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 calculateBytesLeft(currentBytes, totalBytes int) int {
|
||||||
|
return totalBytes - currentBytes
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package ratiospoof
|
package ratiospoof
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -12,34 +11,6 @@ func assertAreEqual(t *testing.T, got, want interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStrSize2ByteSize(T *testing.T) {
|
|
||||||
|
|
||||||
data := []struct {
|
|
||||||
in string
|
|
||||||
inTotalSize int
|
|
||||||
out int
|
|
||||||
}{
|
|
||||||
{"100kb", 100, 102400},
|
|
||||||
{"1kb", 0, 1024},
|
|
||||||
{"1mb", 0, 1048576},
|
|
||||||
{"1gb", 0, 1073741824},
|
|
||||||
{"1.5gb", 0, 1610612736},
|
|
||||||
{"1tb", 0, 1099511627776},
|
|
||||||
{"1b", 0, 1},
|
|
||||||
{"100%", 10737418240, 10737418240},
|
|
||||||
{"55%", 943718400, 519045120},
|
|
||||||
}
|
|
||||||
|
|
||||||
for idx, td := range data {
|
|
||||||
T.Run(fmt.Sprint(idx), func(t *testing.T) {
|
|
||||||
got := strSize2ByteSize(td.in, td.inTotalSize)
|
|
||||||
if got != td.out {
|
|
||||||
t.Errorf("got %v, want %v", got, td.out)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClculateNextTotalSizeByte(T *testing.T) {
|
func TestClculateNextTotalSizeByte(T *testing.T) {
|
||||||
|
|
||||||
got := calculateNextTotalSizeByte(100*1024, 0, 512, 30, 87979879)
|
got := calculateNextTotalSizeByte(100*1024, 0, 512, 30, 87979879)
|
||||||
140
internal/tracker/tracker.go
Normal file
140
internal/tracker/tracker.go
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ap-pauloafonso/ratio-spoof/internal/bencode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpTracker struct {
|
||||||
|
Urls []string
|
||||||
|
RetryAttempt int
|
||||||
|
LastAnounceRequest string
|
||||||
|
LastTackerResponse string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackerResponse struct {
|
||||||
|
MinInterval int
|
||||||
|
Interval int
|
||||||
|
Seeders int
|
||||||
|
Leechers int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpTracker(torrentInfo *bencode.TorrentInfo, timerChangeChannel chan<- int) (*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) Announce(query string, headers map[string]string, retry bool, timerUpdateChannel chan<- int) (*TrackerResponse, error) {
|
||||||
|
defer func() {
|
||||||
|
T.RetryAttempt = 0
|
||||||
|
}()
|
||||||
|
if retry {
|
||||||
|
retryDelay := 30 * time.Second
|
||||||
|
for {
|
||||||
|
trackerResp, err := T.tryMakeRequest(query, headers)
|
||||||
|
if err != nil {
|
||||||
|
timerUpdateChannel <- int(retryDelay.Seconds())
|
||||||
|
T.RetryAttempt++
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
retryDelay *= 2
|
||||||
|
if retryDelay.Seconds() > 900 {
|
||||||
|
retryDelay = 900
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return trackerResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
resp, err := T.tryMakeRequest(query, headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
if len(bytesR) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mimeType := http.DetectContentType(bytesR)
|
||||||
|
if mimeType == "application/x-gzip" {
|
||||||
|
gzipReader, _ := gzip.NewReader(bytes.NewReader(bytesR))
|
||||||
|
bytesR, _ = ioutil.ReadAll(gzipReader)
|
||||||
|
gzipReader.Close()
|
||||||
|
}
|
||||||
|
t.LastTackerResponse = string(bytesR)
|
||||||
|
decodedResp, err := bencode.Decode(bytesR)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret, err := extractTrackerResponse(decodedResp)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx != 0 {
|
||||||
|
t.SwapFirst(idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ret, nil
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("Connection error with the tracker")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFullUrl(baseurl, query string) string {
|
||||||
|
if len(strings.Split(baseurl, "?")) > 1 {
|
||||||
|
return baseurl + "&" + strings.TrimLeft(query, "&")
|
||||||
|
}
|
||||||
|
return baseurl + "?" + strings.TrimLeft(query, "?")
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTrackerResponse(datatrackerResponse map[string]interface{}) (TrackerResponse, error) {
|
||||||
|
var result TrackerResponse
|
||||||
|
if v, ok := datatrackerResponse["failure reason"].(string); ok && len(v) > 0 {
|
||||||
|
return result, errors.New(v)
|
||||||
|
}
|
||||||
|
result.MinInterval, _ = datatrackerResponse["min interval"].(int)
|
||||||
|
result.Interval, _ = datatrackerResponse["interval"].(int)
|
||||||
|
result.Seeders, _ = datatrackerResponse["complete"].(int)
|
||||||
|
result.Leechers, _ = datatrackerResponse["incomplete"].(int)
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,486 +0,0 @@
|
||||||
package ratiospoof
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ap-pauloafonso/ratio-spoof/bencode"
|
|
||||||
"github.com/gammazero/deque"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 *bencode.TorrentInfo
|
|
||||||
input *inputParsed
|
|
||||||
trackerState *httpTracker
|
|
||||||
bitTorrentClient TorrentClientEmulation
|
|
||||||
currentAnnounceTimer int
|
|
||||||
announceInterval int
|
|
||||||
numWant int
|
|
||||||
seeders int
|
|
||||||
leechers int
|
|
||||||
announceCount int
|
|
||||||
status string
|
|
||||||
announceHistory announceHistory
|
|
||||||
lastAnounceRequest string
|
|
||||||
lastTackerResponse string
|
|
||||||
retryAttempt int
|
|
||||||
}
|
|
||||||
type httpTracker struct {
|
|
||||||
urls []string
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 < 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 TorrentClientEmulation, httpclient HttpClient) (*ratioSpoofState, error) {
|
|
||||||
|
|
||||||
dat, err := ioutil.ReadFile(input.TorrentPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
torrentInfo, err := bencode.TorrentDictParse(dat)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
httpTracker, err := newHttpTracker(torrentInfo)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
inputParsed, err := input.parseInput(torrentInfo)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ratioSpoofState{
|
|
||||||
bitTorrentClient: torrentClient,
|
|
||||||
httpClient: httpclient,
|
|
||||||
torrentInfo: torrentInfo,
|
|
||||||
trackerState: httpTracker,
|
|
||||||
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 trackerResponse struct {
|
|
||||||
minInterval int
|
|
||||||
interval int
|
|
||||||
seeders int
|
|
||||||
leechers int
|
|
||||||
}
|
|
||||||
|
|
||||||
type TorrentClientEmulation 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.interval > 0 {
|
|
||||||
R.announceInterval = resp.interval
|
|
||||||
} else {
|
|
||||||
R.announceInterval = 1800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) changeCurrentTimer(newAnnounceRate int) {
|
|
||||||
R.mutex.Lock()
|
|
||||||
R.currentAnnounceTimer = newAnnounceRate
|
|
||||||
R.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (R *ratioSpoofState) tryMakeRequest(query string) *trackerResponse {
|
|
||||||
for idx, baseUrl := range R.trackerState.urls {
|
|
||||||
completeURL := buildFullUrl(baseUrl, 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 := bencode.Decode(bytesR)
|
|
||||||
if idx != 0 {
|
|
||||||
R.trackerState.SwapFirst(idx)
|
|
||||||
}
|
|
||||||
ret := extractTrackerResponse(decodedResp)
|
|
||||||
return &ret
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic("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 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 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue