Compare commits

...

17 commits

Author SHA1 Message Date
paulo
032536eda4 img 2023-10-25 18:16:16 -03:00
paulo
6c69b9ec45 readme2 2023-10-24 23:51:26 -03:00
paulo
662252950f readme 2023-10-24 23:44:53 -03:00
paulo
4cd38be3ec grammar 2023-07-03 19:48:19 -03:00
paulo
6c1fb14a25 rm println on testfile 2023-07-03 15:47:05 -03:00
paulo
5f5a6195b5 readme update2 2023-07-03 15:24:20 -03:00
paulo
c2146e3a31 change go version github ci2 2023-07-03 15:13:53 -03:00
paulo
986583c677 change go version github ci 2023-07-03 15:11:58 -03:00
paulo
1544c0d568 no cache make test 2023-07-03 15:10:32 -03:00
paulo
5dd3724a53 outdated torrent samples update, fix readme link 2023-07-03 15:05:45 -03:00
ap-pauloafonso
00a239e02e
Merge pull request #21 from ap-pauloafonso/removal-stop-printer-channel
removal of stop printer channel
2021-03-23 19:27:27 -03:00
ap-pauloafonso
aabcc229d1 update readme 2021-03-22 22:26:24 -03:00
ap-pauloafonso
c94fd60f77 removal of stop printer channel 2021-03-22 22:09:38 -03:00
ap-pauloafonso
2bbf97d854
Merge pull request #20 from ap-pauloafonso/channel-removal
estimatedTime to tracker.go proposal
2021-03-19 20:05:27 -03:00
ap-pauloafonso
6cb7064828 camelCase method reciver 2021-03-19 18:52:34 -03:00
ap-pauloafonso
4fd2969d82 estimatedTime to tracker.go 2021-03-18 20:18:59 -03:00
ap-pauloafonso
8a630643bb
Merge pull request #18 from ap-pauloafonso/multiple-clients
Multiple clients
2021-03-16 19:06:04 -03:00
32 changed files with 355 additions and 342 deletions

View file

@ -16,7 +16,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.16 go-version: '1.20'
- name: Build - name: Build
run: go build -v ./... run: go build -v ./...

3
.gitignore vendored
View file

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

View file

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

View file

@ -4,9 +4,10 @@ Ratio-spoof is a cross-platform, free and open source tool to spoof the download
![](./assets/demo.gif) ![](./assets/demo.gif)
## Motivation ## Motivation
Here in brazil, not everybody has a great upload speed, and most of the private trackers requires a ratio to be greater than or equal to 1 (e.g. if you downloaded 1gb you must upload 1gb as well) in order to survive. Plus, i have always been fascinated by the bittorrent protocol, [i even made a bittorrent webclient to learn a bit about it ](https://github.com/ap-pauloafonso/rwTorrent) so with the current global covid-19 lockdown i got some free time and decided to code my own simple cli tool to spoof bittorrent trackers. Here in Brazil, not everybody has a great upload speed, and most private trackers require a ratio greater than or equal to 1. For example, if you downloaded 1GB, you must also upload 1GB in order to survive. Additionally, I have always been fascinated by the BitTorrent protocol. In fact, [I even made a BitTorrent web client to learn more about it](https://github.com/ap-pauloafonso/rwTorrent). So, if you have a bad internet connection, feel free to use this tool. Otherwise, please consider seeding the files with a real torrent client.
## How does it works? ## How does it work?
![Diagram](./assets/how-it-works.png)
Bittorrent protocol works in such a way that there is no way that a tracker knows how much certain peer have downloaded or uploaded, so the tracker depends on the peer itself telling the amounts. Bittorrent protocol works in such a way that there is no way that a tracker knows how much certain peer have downloaded or uploaded, so the tracker depends on the peer itself telling the amounts.
Ratio-spoof acts like a normal bittorrent client but without downloading or uploading anything, in fact it just tricks the tracker pretending that. Ratio-spoof acts like a normal bittorrent client but without downloading or uploading anything, in fact it just tricks the tracker pretending that.
@ -45,14 +46,14 @@ required arguments:
* Will start "downloading" with the initial value of 2gb downloaded if possible at 500kbps speed until it reaches 100% mark. * Will start "downloading" with the initial value of 2gb downloaded if possible at 500kbps speed until it reaches 100% mark.
* Will start "uploading" with the initial value of 1gb uplodead at 1024kbps (aka 1mb/s) indefinitely. * Will start "uploading" with the initial value of 1gb uplodead at 1024kbps (aka 1mb/s) indefinitely.
## Will i get caught using it ? ## Will I get caught using it ?
Depends wether you use it carefuly, Its a hard task to catch cheaters, but if you start uploading crazy amounts out of nowhere or seeding something with no active leecher on the swarm you may be in risk. Depends on whether you use it carefully, It's a hard task to catch cheaters, but if you start uploading crazy amounts out of nowhere or seeding something with no active leecher on the swarm you may be in risk.
## Bittorrent client supported ## Bittorrent client supported
The currently emulation is hard coded to be a popular and accepted client qbittorrent v4.0.3. The default client emulation is qbittorrent v4.0.3, however you can change it by using the -c argument
## Resources ## Resources
http://www.bittorrent.org/beps/bep_0003.html http://www.bittorrent.org/beps/bep_0003.html
https://wiki.theory.org/index.php/BitTorrentSpecification https://wiki.theory.org/BitTorrentSpecification

BIN
assets/how-it-works.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View file

@ -63,8 +63,8 @@ func TorrentDictParse(dat []byte) (torrent *TorrentInfo, err error) {
}, err }, err
} }
func (T *torrentDict) extractInfoHashURLEncoded(rawData []byte) string { func (t *torrentDict) extractInfoHashURLEncoded(rawData []byte) string {
byteOffsets := T.resultMap["info"].(map[string]interface{})["byte_offsets"].([]int) byteOffsets := t.resultMap["info"].(map[string]interface{})["byte_offsets"].([]int)
h := sha1.New() h := sha1.New()
h.Write([]byte(rawData[byteOffsets[0]:byteOffsets[1]])) h.Write([]byte(rawData[byteOffsets[0]:byteOffsets[1]]))
ret := h.Sum(nil) ret := h.Sum(nil)
@ -80,28 +80,28 @@ func (T *torrentDict) extractInfoHashURLEncoded(rawData []byte) string {
return buf.String() return buf.String()
} }
func (T *torrentDict) extractTotalSize() int { func (t *torrentDict) extractTotalSize() int {
if value, ok := T.resultMap[torrentInfoKey].(map[string]interface{})[torrentLengthKey]; ok { if value, ok := t.resultMap[torrentInfoKey].(map[string]interface{})[torrentLengthKey]; ok {
return value.(int) return value.(int)
} }
var total int var total int
for _, file := range T.resultMap[torrentInfoKey].(map[string]interface{})[torrentFilesKey].([]interface{}) { for _, file := range t.resultMap[torrentInfoKey].(map[string]interface{})[torrentFilesKey].([]interface{}) {
total += file.(map[string]interface{})[torrentLengthKey].(int) total += file.(map[string]interface{})[torrentLengthKey].(int)
} }
return total return total
} }
func (T *torrentDict) extractTrackerInfo() *TrackerInfo { func (t *torrentDict) extractTrackerInfo() *TrackerInfo {
uniqueUrls := make(map[string]int) uniqueUrls := make(map[string]int)
currentCount := 0 currentCount := 0
if main, ok := T.resultMap[mainAnnounceKey]; ok { if main, ok := t.resultMap[mainAnnounceKey]; ok {
if _, found := uniqueUrls[main.(string)]; !found { if _, found := uniqueUrls[main.(string)]; !found {
uniqueUrls[main.(string)] = currentCount uniqueUrls[main.(string)] = currentCount
currentCount++ currentCount++
} }
} }
if list, ok := T.resultMap[announceListKey]; ok { if list, ok := t.resultMap[announceListKey]; ok {
for _, innerList := range list.([]interface{}) { for _, innerList := range list.([]interface{}) {
for _, item := range innerList.([]interface{}) { for _, item := range innerList.([]interface{}) {
if _, found := uniqueUrls[item.(string)]; !found { if _, found := uniqueUrls[item.(string)]; !found {

View file

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

View file

@ -3,9 +3,8 @@ package emulation
import ( import (
"embed" "embed"
"encoding/json" "encoding/json"
"io/ioutil" generator2 "github.com/ap-pauloafonso/ratio-spoof/generator"
"io"
"github.com/ap-pauloafonso/ratio-spoof/internal/generator"
) )
type ClientInfo struct { type ClientInfo struct {
@ -52,17 +51,17 @@ func NewEmulation(code string) (*Emulation, error) {
return nil, err return nil, err
} }
peerG, err := generator.NewRegexPeerIdGenerator(c.PeerID.Regex) peerG, err := generator2.NewRegexPeerIdGenerator(c.PeerID.Regex)
if err != nil { if err != nil {
return nil, err return nil, err
} }
keyG, err := generator.NewDefaultKeyGenerator() keyG, err := generator2.NewDefaultKeyGenerator()
if err != nil { if err != nil {
return nil, err return nil, err
} }
roudingG, err := generator.NewDefaultRoudingGenerator() roudingG, err := generator2.NewDefaultRoudingGenerator()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -83,7 +82,7 @@ func extractClient(code string) (*ClientInfo, error) {
} }
defer f.Close() defer f.Close()
bytes, err := ioutil.ReadAll(f) bytes, err := io.ReadAll(f)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -2,7 +2,7 @@ package generator
import "testing" import "testing"
func TestNextAmountReport(t *testing.T) { func TestDefaultRounding(t *testing.T) {
r, _ := NewDefaultRoudingGenerator() r, _ := NewDefaultRoudingGenerator()
d, u, l := r.Round(656497856, 46479878, 7879879, 1024) d, u, l := r.Round(656497856, 46479878, 7879879, 1024)

9
go.mod
View file

@ -1,13 +1,16 @@
module github.com/ap-pauloafonso/ratio-spoof module github.com/ap-pauloafonso/ratio-spoof
go 1.16 go 1.20
require ( require (
github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc github.com/gammazero/deque v0.0.0-20201010052221-3932da5530cc
github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 // indirect
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0
github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
)
require (
github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.7.0 // indirect github.com/stretchr/testify v1.7.0 // indirect
github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
) )

View file

@ -3,11 +3,10 @@ package input
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/ap-pauloafonso/ratio-spoof/bencode"
"math" "math"
"strconv" "strconv"
"strings" "strings"
"github.com/ap-pauloafonso/ratio-spoof/internal/bencode"
) )
const ( const (
@ -40,25 +39,25 @@ type InputParsed struct {
var validInitialSufixes = [...]string{"%", "b", "kb", "mb", "gb", "tb"} var validInitialSufixes = [...]string{"%", "b", "kb", "mb", "gb", "tb"}
var validSpeedSufixes = [...]string{"kbps", "mbps"} var validSpeedSufixes = [...]string{"kbps", "mbps"}
func (I *InputArgs) ParseInput(torrentInfo *bencode.TorrentInfo) (*InputParsed, error) { func (i *InputArgs) ParseInput(torrentInfo *bencode.TorrentInfo) (*InputParsed, error) {
downloaded, err := extractInputInitialByteCount(I.InitialDownloaded, torrentInfo.TotalSize, true) downloaded, err := extractInputInitialByteCount(i.InitialDownloaded, torrentInfo.TotalSize, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
uploaded, err := extractInputInitialByteCount(I.InitialUploaded, torrentInfo.TotalSize, false) uploaded, err := extractInputInitialByteCount(i.InitialUploaded, torrentInfo.TotalSize, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
downloadSpeed, err := extractInputByteSpeed(I.DownloadSpeed) downloadSpeed, err := extractInputByteSpeed(i.DownloadSpeed)
if err != nil { if err != nil {
return nil, err return nil, err
} }
uploadSpeed, err := extractInputByteSpeed(I.UploadSpeed) uploadSpeed, err := extractInputByteSpeed(i.UploadSpeed)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if I.Port < minPortNumber || I.Port > maxPortNumber { if i.Port < minPortNumber || i.Port > maxPortNumber {
return nil, errors.New(fmt.Sprint("port number must be between %i and %i", minPortNumber, maxPortNumber)) return nil, errors.New(fmt.Sprint("port number must be between %i and %i", minPortNumber, maxPortNumber))
} }
@ -66,8 +65,8 @@ func (I *InputArgs) ParseInput(torrentInfo *bencode.TorrentInfo) (*InputParsed,
DownloadSpeed: downloadSpeed, DownloadSpeed: downloadSpeed,
InitialUploaded: uploaded, InitialUploaded: uploaded,
UploadSpeed: uploadSpeed, UploadSpeed: uploadSpeed,
Debug: I.Debug, Debug: i.Debug,
Port: I.Port, Port: i.Port,
}, nil }, nil
} }
@ -95,7 +94,7 @@ func extractInputInitialByteCount(initialSizeInput string, totalBytes int, error
return byteCount, nil return byteCount, nil
} }
//Takes an dirty speed input and returns the bytes per second based on the suffixes // Takes an dirty speed input and returns the bytes per second based on the suffixes
// example 1kbps(string) > 1024 bytes per second (int) // example 1kbps(string) > 1024 bytes per second (int)
func extractInputByteSpeed(initialSpeedInput string) (int, error) { func extractInputByteSpeed(initialSpeedInput string) (int, error) {
ok, suffix := checkSpeedSufix(initialSpeedInput) ok, suffix := checkSpeedSufix(initialSpeedInput)

View file

@ -1,227 +0,0 @@
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/emulation"
"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 *emulation.Emulation
AnnounceInterval int
EstimatedTimeToAnnounce time.Time
EstimatedTimeToAnnounceUpdateCh chan int
NumWant int
Seeders int
Leechers int
AnnounceCount int
Status string
AnnounceHistory announceHistory
StopPrintCH chan interface{}
}
type AnnounceEntry struct {
Count int
Downloaded int
PercentDownloaded float32
Uploaded int
Left int
}
type announceHistory struct {
deque.Deque
}
func NewRatioSpoofState(input input.InputArgs) (*RatioSpoof, error) {
EstimatedTimeToAnnounceUpdateCh := make(chan int)
stopPrintCh := make(chan interface{})
dat, err := ioutil.ReadFile(input.TorrentPath)
if err != nil {
return nil, err
}
client, err := emulation.NewEmulation(input.Client)
if err != nil {
return nil, errors.New("Error building the emulated client with the code")
}
torrentInfo, err := bencode.TorrentDictParse(dat)
if err != nil {
return nil, errors.New("failed to parse the torrent file")
}
httpTracker, err := tracker.NewHttpTracker(torrentInfo)
if err != nil {
return nil, err
}
inputParsed, err := input.ParseInput(torrentInfo)
if err != nil {
return nil, err
}
return &RatioSpoof{
BitTorrentClient: client,
TorrentInfo: torrentInfo,
Tracker: httpTracker,
Input: inputParsed,
NumWant: 200,
Status: "started",
mutex: &sync.Mutex{},
StopPrintCH: stopPrintCh,
EstimatedTimeToAnnounceUpdateCh: EstimatedTimeToAnnounceUpdateCh,
}, 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.updateEstimatedTimeToAnnounceListener()
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(interval int) {
if interval > 0 {
R.AnnounceInterval = interval
} else {
R.AnnounceInterval = 1800
}
R.updateEstimatedTimeToAnnounce(R.AnnounceInterval)
}
func (R *RatioSpoof) updateEstimatedTimeToAnnounce(interval int) {
R.mutex.Lock()
defer R.mutex.Unlock()
R.EstimatedTimeToAnnounce = time.Now().Add(time.Duration(interval) * time.Second)
}
func (R *RatioSpoof) updateEstimatedTimeToAnnounceListener() {
for {
interval := <-R.EstimatedTimeToAnnounceUpdateCh
R.updateEstimatedTimeToAnnounce(interval)
}
}
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.EstimatedTimeToAnnounceUpdateCh)
if err != nil {
log.Fatalf("failed to reach the tracker:\n%s ", err.Error())
}
if trackerResp != nil {
R.updateSeedersAndLeechers(*trackerResp)
R.updateInterval(trackerResp.Interval)
}
return nil
}
func (R *RatioSpoof) generateNextAnnounce() {
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.Round(downloadCandidate, uploadCandidate, leftCandidate, R.TorrentInfo.PieceSize)
R.addAnnounce(d, u, l, (float32(d)/float32(R.TorrentInfo.TotalSize))*100)
}
func calculateNextTotalSizeByte(speedBytePerSecond, currentByte, pieceSizeByte, seconds, limitTotalBytes 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
}

View file

@ -1,36 +0,0 @@
package ratiospoof
import (
"testing"
)
func assertAreEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("\ngot : %v\nwant: %v", got, want)
}
}
func TestClculateNextTotalSizeByte(T *testing.T) {
got := calculateNextTotalSizeByte(100*1024, 0, 512, 30, 87979879)
want := 3075072
assertAreEqual(T, got, want)
}
// func TestUrlEncodeInfoHash(T *testing.T) {
// b, _ := ioutil.ReadFile("")
// got := extractInfoHashURLEncoded(b, bencode.Decode(b))
// want := "%60N%7d%1f%8b%3a%9bT%d5%fc%ad%d1%27%ab5%02%1c%fb%03%b0"
// assertAreEqual(T, got, want)
// }
// func TestUrlEncodeInfoHash2(T *testing.T) {
// b, _ := ioutil.ReadFile("")
// got := extractInfoHashURLEncoded(b, bencode.Decode(b))
// want := "%02r%fd%fe%bf%fbt%d0%0f%cf%d9%8c%e0%a9%97%f8%08%9b%00%b2"
// assertAreEqual(T, got, want)
// }

View file

@ -3,12 +3,11 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"github.com/ap-pauloafonso/ratio-spoof/input"
"github.com/ap-pauloafonso/ratio-spoof/printer"
"github.com/ap-pauloafonso/ratio-spoof/ratiospoof"
"log" "log"
"os" "os"
"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() {

View file

@ -2,25 +2,19 @@ package printer
import ( import (
"fmt" "fmt"
"github.com/ap-pauloafonso/ratio-spoof/ratiospoof"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"strings" "strings"
"time" "time"
"github.com/ap-pauloafonso/ratio-spoof/internal/ratiospoof"
"github.com/olekukonko/ts" "github.com/olekukonko/ts"
) )
func PrintState(state *ratiospoof.RatioSpoof) { func PrintState(state *ratiospoof.RatioSpoof) {
exit := false
go func() {
_ = <-state.StopPrintCH
exit = true
}()
for { for {
if exit { if !state.Print {
break break
} }
width := terminalSize() width := terminalSize()
@ -63,7 +57,7 @@ func PrintState(state *ratiospoof.RatioSpoof) {
} }
lastDequeItem := state.AnnounceHistory.At(state.AnnounceHistory.Len() - 1).(ratiospoof.AnnounceEntry) lastDequeItem := state.AnnounceHistory.At(state.AnnounceHistory.Len() - 1).(ratiospoof.AnnounceEntry)
remaining := time.Until(state.EstimatedTimeToAnnounce) remaining := time.Until(state.Tracker.EstimatedTimeToAnnounce)
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,

195
ratiospoof/ratiospoof.go Normal file
View file

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

View file

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

View file

@ -4,19 +4,19 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"errors" "errors"
"io/ioutil" "github.com/ap-pauloafonso/ratio-spoof/bencode"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/ap-pauloafonso/ratio-spoof/internal/bencode"
) )
type HttpTracker struct { type HttpTracker struct {
Urls []string Urls []string
RetryAttempt int RetryAttempt int
LastAnounceRequest string LastAnounceRequest string
LastTackerResponse string LastTackerResponse string
EstimatedTimeToAnnounce time.Time
} }
type TrackerResponse struct { type TrackerResponse struct {
@ -40,23 +40,34 @@ func NewHttpTracker(torrentInfo *bencode.TorrentInfo) (*HttpTracker, error) {
return &HttpTracker{Urls: torrentInfo.TrackerInfo.Urls}, nil return &HttpTracker{Urls: torrentInfo.TrackerInfo.Urls}, nil
} }
func (T *HttpTracker) SwapFirst(currentIdx int) { func (t *HttpTracker) swapFirst(currentIdx int) {
aux := T.Urls[0] aux := t.Urls[0]
T.Urls[0] = T.Urls[currentIdx] t.Urls[0] = t.Urls[currentIdx]
T.Urls[currentIdx] = aux t.Urls[currentIdx] = aux
} }
func (T *HttpTracker) Announce(query string, headers map[string]string, retry bool, estimatedTimeToAnnounceUpdateCh chan<- int) (*TrackerResponse, error) { func (t *HttpTracker) updateEstimatedTimeToAnnounce(interval int) {
t.EstimatedTimeToAnnounce = time.Now().Add(time.Duration(interval) * time.Second)
}
func (t *HttpTracker) handleSuccessfulResponse(resp *TrackerResponse) {
if resp.Interval <= 0 {
resp.Interval = 1800
}
t.updateEstimatedTimeToAnnounce(resp.Interval)
}
func (t *HttpTracker) Announce(query string, headers map[string]string, retry bool) (*TrackerResponse, error) {
defer func() { defer func() {
T.RetryAttempt = 0 t.RetryAttempt = 0
}() }()
if retry { if retry {
retryDelay := 30 retryDelay := 30
for { for {
trackerResp, err := T.tryMakeRequest(query, headers) trackerResp, err := t.tryMakeRequest(query, headers)
if err != nil { if err != nil {
estimatedTimeToAnnounceUpdateCh <- retryDelay t.updateEstimatedTimeToAnnounce(retryDelay)
T.RetryAttempt++ t.RetryAttempt++
time.Sleep(time.Duration(retryDelay) * time.Second) time.Sleep(time.Duration(retryDelay) * time.Second)
retryDelay *= 2 retryDelay *= 2
if retryDelay > 900 { if retryDelay > 900 {
@ -64,14 +75,16 @@ func (T *HttpTracker) Announce(query string, headers map[string]string, retry bo
} }
continue continue
} }
t.handleSuccessfulResponse(trackerResp)
return trackerResp, nil return trackerResp, nil
} }
} else { } else {
resp, err := T.tryMakeRequest(query, headers) resp, err := t.tryMakeRequest(query, headers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
t.handleSuccessfulResponse(resp)
return resp, nil return resp, nil
} }
} }
@ -87,14 +100,14 @@ func (t *HttpTracker) tryMakeRequest(query string, headers map[string]string) (*
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err == nil { if err == nil {
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
bytesR, _ := ioutil.ReadAll(resp.Body) bytesR, _ := io.ReadAll(resp.Body)
if len(bytesR) == 0 { if len(bytesR) == 0 {
continue continue
} }
mimeType := http.DetectContentType(bytesR) mimeType := http.DetectContentType(bytesR)
if mimeType == "application/x-gzip" { if mimeType == "application/x-gzip" {
gzipReader, _ := gzip.NewReader(bytes.NewReader(bytesR)) gzipReader, _ := gzip.NewReader(bytes.NewReader(bytesR))
bytesR, _ = ioutil.ReadAll(gzipReader) bytesR, _ = io.ReadAll(gzipReader)
gzipReader.Close() gzipReader.Close()
} }
t.LastTackerResponse = string(bytesR) t.LastTackerResponse = string(bytesR)
@ -107,7 +120,7 @@ func (t *HttpTracker) tryMakeRequest(query string, headers map[string]string) (*
continue continue
} }
if idx != 0 { if idx != 0 {
t.SwapFirst(idx) t.swapFirst(idx)
} }
return &ret, nil return &ret, nil

57
tracker/tracker_test.go Normal file
View file

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