ratio-spoof/internal/ratiospoof/ratiospoof.go
2021-03-18 19:07:02 -03:00

210 lines
6 KiB
Go

package ratiospoof
import (
"errors"
"fmt"
"log"
"math/rand"
"os"
"os/signal"
"strings"
"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 {
TorrentInfo *bencode.TorrentInfo
Input *input.InputParsed
Tracker *tracker.HttpTracker
BitTorrentClient *emulation.Emulation
AnnounceInterval int
EstimatedTimeToAnnounce time.Time
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) {
stopPrintCh := make(chan interface{})
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",
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 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.EstimatedTimeToAnnounce = time.Now().Add(time.Duration(interval) * time.Second)
}
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.updateEstimatedTimeToAnnounce)
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
}