@ -0,0 +1,22 @@ | |||
# Ratio.py | |||
Ratio.py is a small command line RatioMaster.Net like in Python3. It fakes upload stats of a torrent. | |||
Current emulators available are: | |||
* Transmission 2.92 | |||
## Requirements: | |||
1. Python 3.x | |||
2. pip install -r requirements.txt | |||
## Usage: | |||
```console | |||
foo@bar:~/ratio.py$ python ratio.py -c configuration.json | |||
``` | |||
## Configuration example | |||
```js | |||
{ | |||
"torrent": "<Torrent file path>", | |||
"upload": "<Upload speed (kB/s)>" | |||
} | |||
``` |
@ -0,0 +1,90 @@ | |||
import re | |||
import logging | |||
import binascii | |||
class bencoding(): | |||
def __init__(self): | |||
self.decimal_match = re.compile('\d') | |||
self.data = b'' | |||
self.dict = {} | |||
def get_dict(self, key): | |||
if key not in self.dict: | |||
return '' | |||
start = self.dict[key][0] | |||
end = self.dict[key][1] | |||
return self.data[start:end] | |||
def get_item(self, chunks): | |||
item = chunks[self.i] | |||
self.i += 1 | |||
if not type(item) == str: | |||
item = bytes([item]) | |||
try: | |||
item = item.decode('utf-8') | |||
except: | |||
item = '\\x{}'.format(binascii.hexlify(item)) | |||
return item | |||
def decoding_byte_string(self, chunks, item): | |||
# logging.debug('decoding string') | |||
num = '' | |||
while self.decimal_match.search(item): | |||
num += item | |||
item = self.get_item(chunks) | |||
line = '' | |||
for i in range(int(num)): | |||
line += self.get_item(chunks) | |||
return line | |||
def decoding_integer(self, chunks): | |||
# logging.debug('decoding integer') | |||
item = self.get_item(chunks) | |||
num = '' | |||
while item != 'e': | |||
num += item | |||
item = self.get_item(chunks) | |||
return int(num) | |||
def decoding_list(self, chunks): | |||
# logging.debug('decoding list') | |||
item = self.get_item(chunks) | |||
list = [] | |||
while item != 'e': | |||
self.i -= 1 | |||
list.append(self._dechunk(chunks)) | |||
item = self.get_item(chunks) | |||
return list | |||
def decoding_dictionnary(self, chunks): | |||
# logging.debug('decoding dictionnary') | |||
item = self.get_item(chunks) | |||
hash = {} | |||
while item != 'e': | |||
self.i -= 1 | |||
key = self._dechunk(chunks) | |||
start = self.i | |||
hash[key] = self._dechunk(chunks) | |||
end = self.i | |||
self.dict[key] = (start, end) | |||
item = self.get_item(chunks) | |||
return hash | |||
def _dechunk(self, chunks): | |||
item = self.get_item(chunks) | |||
if item == 'd': | |||
return self.decoding_dictionnary(chunks) | |||
elif item == 'l': | |||
return self.decoding_list(chunks) | |||
elif item == 'i': | |||
return self.decoding_integer(chunks) | |||
elif self.decimal_match.search(item): | |||
return self.decoding_byte_string(chunks, item) | |||
raise "Invalid input!" | |||
def bdecode(self, data): | |||
self.data = data | |||
chunks = list(self.data) | |||
self.i = 0 | |||
root = self._dechunk(chunks) | |||
return root |
@ -0,0 +1,28 @@ | |||
import requests | |||
from pprint import pformat | |||
def get_headers(headers): | |||
res = '' | |||
for k, v in headers.items(): | |||
res += '{}: {}\n'.format(k, v) | |||
return res | |||
def pretty_GET(url, headers, params): | |||
req = requests.Request('GET', url, headers=headers, params=params) | |||
s = requests.Session() | |||
prepared = s.prepare_request(req) | |||
p = '-----START-----\n' | |||
p +=('{} {}\n{}'.format(prepared.method, prepared.url, | |||
get_headers(prepared.headers), | |||
) | |||
) | |||
if prepared.body: | |||
pi += prepared.body | |||
p += '------END------' | |||
return p | |||
def pretty_data(data): | |||
return pformat(data) |
@ -0,0 +1,118 @@ | |||
from code.decoding_bencoded import bencoding | |||
from code.torrentclientfactory import Transmission292 | |||
from code.pretty import pretty_data, pretty_GET | |||
from hashlib import sha1 | |||
from urllib.parse import quote_plus | |||
import requests | |||
import logging | |||
import random | |||
from tqdm import tqdm | |||
from time import sleep | |||
from struct import unpack | |||
logging.basicConfig(level=logging.DEBUG) | |||
class process_torrent(): | |||
def __init__(self, configuration): | |||
self.configuration = configuration | |||
self.open_torrent() | |||
self.torrentclient = Transmission292(self.tracker_info_hash()) | |||
def open_torrent(self): | |||
torrent_file = self.configuration['torrent'] | |||
with open(torrent_file, 'rb') as tf: | |||
data = tf.read() | |||
self.b_enc = bencoding() | |||
self.metainfo = self.b_enc.bdecode(data) | |||
self.info = self.metainfo['info'] | |||
if 'length' not in self.info: | |||
self.info['length'] = 0 | |||
for file in self.info['files']: | |||
self.info['length'] += file['length'] | |||
print(pretty_data(self.info['files'])) | |||
def tracker_info_hash(self): | |||
raw_info = self.b_enc.get_dict('info') | |||
hash_factory = sha1() | |||
hash_factory.update(raw_info) | |||
hashed = hash_factory.hexdigest() | |||
sha = bytearray.fromhex(hashed) | |||
return str(quote_plus(sha)) | |||
def send_request(self, params, headers): | |||
url = self.metainfo['announce'] | |||
print(pretty_GET(url, headers, params)) | |||
while True: | |||
try: | |||
r = requests.get(url, params=params, headers=headers) | |||
except requests.exceptions.ConnectionError as e: | |||
sleep(1) | |||
continue | |||
break | |||
return r.content | |||
def tracker_start_request(self): | |||
tc = self.torrentclient | |||
headers = tc.get_headers() | |||
params = tc.get_query(uploaded=0, | |||
downloaded=0, | |||
event='started') | |||
print('----------- First Command to Tracker --------') | |||
content = self.send_request(params, headers) | |||
self.tracker_response_parser(content) | |||
def tracker_response_parser(self, tr_response): | |||
b_enc = bencoding() | |||
response = b_enc.bdecode(tr_response) | |||
print('----------- Received Tracker Response --------') | |||
print(pretty_data(response)) | |||
raw_peers = b_enc.get_dict('peers') | |||
i = 0 | |||
peers = [] | |||
while i<len(raw_peers)-6: | |||
peer = raw_peers[i:i+6] | |||
i+=6 | |||
unpacked_ip = unpack('BBBB', peer[0:4]) | |||
ip = ".".join(str(i) for i in unpacked_ip) | |||
unpacked_port = unpack('!H', peer[4:6]) | |||
port = unpacked_port[0] | |||
peers.append((ip, port)) | |||
self.interval = response['interval'] | |||
def wait(self): | |||
pbar = tqdm(total=self.interval) | |||
print('sleep: {}'.format(self.interval)) | |||
t = 0 | |||
while t < self.interval: | |||
t += 1 | |||
pbar.update(1) | |||
sleep(1) | |||
pbar.close() | |||
def tracker_process(self): | |||
while True: | |||
self.tracker_start_request() | |||
print('----------- Sending Command to Tracker --------') | |||
# get upload | |||
min_up = self.interval-(self.interval*0.1) | |||
max_up = self.interval | |||
randomize_upload = random.randint(min_up, max_up) | |||
uploaded = int(self.configuration['upload'])*1000*randomize_upload | |||
# get download | |||
downloaded = 0 | |||
tc = self.torrentclient | |||
headers = tc.get_headers() | |||
params = tc.get_query(uploaded=uploaded, | |||
downloaded=downloaded, | |||
event='stopped') | |||
content = self.send_request(params, headers) | |||
self.tracker_response_parser(content) | |||
self.wait() |
@ -0,0 +1,60 @@ | |||
import random | |||
import string | |||
class Transmission292(): | |||
def __init__(self, info_hash): | |||
self.name = "Transmission 2.92 (14714)" | |||
parameters = {} | |||
# urlencoded 20-byte SHA1 hash of the value of the info key from the Metainfo file | |||
parameters['info_hash'] = info_hash | |||
# urlencoded 20-byte string used as a unique ID for the client | |||
parameters["peer_id"] = self.generate_peer_id() | |||
# The port number that the client is listening on | |||
parameters["port"] = random.randint(1025, 65535) | |||
# Number of peers that the client would like to receive from the tracker | |||
parameters["numwant"] = 80 | |||
# An additional identification that is not shared with any other peers | |||
parameters["key"] = self.generate_key() | |||
# Setting this to 1 indicates that the client accepts a compact response | |||
parameters["compact"] = 0 | |||
# Setting this to 1 indicates that the client accepts crypto | |||
parameters["supportcrypto"] = 1 | |||
self.parameters = parameters | |||
def get_headers(self): | |||
headers = {} | |||
headers['User-Agent'] = 'Transmission/2.92' | |||
headers['Accept'] = '*/*' | |||
headers['Accept-Encoding'] = 'Accept-Encoding: gzip;q=1.0, deflate, identity' | |||
return headers | |||
def get_query(self, uploaded, downloaded, left=0, event=None): | |||
# The total amount uploaded (since the client sent the 'started' event) | |||
self.parameters["uploaded"] = uploaded | |||
# The total amount downloaded (since the client sent the 'started' event) | |||
self.parameters["downloaded"] = downloaded | |||
# The number of bytes this client still has to download | |||
self.parameters["left"] = left | |||
# If specified, must be one of started, completed, stopped | |||
if event: | |||
self.parameters["event"] = event | |||
params = '&'.join('{}={}'.format(k, v) | |||
for k, v in self.parameters.items()) | |||
return params | |||
def id_generator(self, chars, size): | |||
id = '' | |||
for _ in range(size): | |||
id += random.choice(chars) | |||
return id | |||
def generate_peer_id(self): | |||
chars = string.ascii_lowercase + string.digits | |||
rand_id = self.id_generator(chars, 12) | |||
peer_id = "-TR2920-" + rand_id | |||
return peer_id | |||
def generate_key(self): | |||
chars = 'ABCDEF' + string.digits | |||
key = self.id_generator(chars, 8) | |||
return key |
@ -0,0 +1,34 @@ | |||
from code.process_torrent import process_torrent | |||
import argparse | |||
import json | |||
import sys | |||
def parse_args(): | |||
"""Create the arguments""" | |||
parser = argparse.ArgumentParser('\nratio.py -c <configuration-file.json>') | |||
parser.add_argument("-c", "--configuration", help="Configuration file") | |||
return parser.parse_args() | |||
def load_configuration(configuration_file): | |||
with open(configuration_file) as f: | |||
configuration = json.load(f) | |||
if 'torrent' not in configuration: | |||
return None | |||
return configuration | |||
if __name__ == "__main__": | |||
args = parse_args() | |||
if args.configuration: | |||
configuration = load_configuration(args.configuration) | |||
else: | |||
sys.exit() | |||
if not configuration: | |||
sys.exit() | |||
to = process_torrent(configuration) | |||
to.tracker_process() | |||
@ -0,0 +1,2 @@ | |||
requests | |||
tqdm |