diff --git a/ox/torrent/__init__.py b/ox/torrent/__init__.py index ce8b0de..9e96bad 100644 --- a/ox/torrent/__init__.py +++ b/ox/torrent/__init__.py @@ -5,15 +5,19 @@ from threading import Event from hashlib import sha1 import os +from six import PY2 -from .bencode import bencode, bdecode +if PY2: + from .bencode import bencode, bdecode +else: + from .bencode3 import bencode, bdecode __all__ = ['create_torrent', 'get_info_hash', 'get_torrent_info', 'get_files', 'get_torrent_size'] def create_torrent(file, url, params = {}, flag = Event(), progress = lambda x: None, progress_percent = 1): "Creates a torrent for a given file, using url as tracker url" - from makemetafile import make_meta_file + from .makemetafile import make_meta_file return make_meta_file(file, url, params, flag, progress, progress_percent) def get_info_hash(torrentFile): diff --git a/ox/torrent/bencode3.py b/ox/torrent/bencode3.py new file mode 100644 index 0000000..d2a2906 --- /dev/null +++ b/ox/torrent/bencode3.py @@ -0,0 +1,151 @@ +## +# +# bencode.py python3 compatable bencode / bdecode +# +## + +def _decode_int(data): + """ + decode integer from bytearray + return int, remaining data + """ + data = data[1:] + end = data.index(b'e') + return int(data[:end],10), data[end+1:] + +def _decode_str(data): + """ + decode string from bytearray + return string, remaining data + """ + start = data.index(b':') + l = int(data[:start].decode(),10) + if l <= 0: + raise Exception('invalid string size: %d'%d) + start += 1 + ret = bytes(data[start:start+l]) + data = data[start+l:] + return ret, data + +def _decode_list(data): + """ + decode list from bytearray + return list, remaining data + """ + ls = [] + data = data[1:] + while data[0] != ord(b'e'): + elem, data = _decode(data) + ls.append(elem) + return ls, data[1:] + +def _decode_dict(data): + """ + decode dict from bytearray + return dict, remaining data + """ + d = {} + data = data[1:] + while data[0] != ord(b'e'): + k, data = _decode_str(data) + v, data = _decode(data) + d[k.decode()] = v + return d, data[1:] + +def _decode(data): + """ + decode a bytearray + return deserialized object, remaining data + """ + ch = chr(data[0]) + if ch == 'l': + return _decode_list(data) + elif ch == 'i': + return _decode_int(data) + elif ch == 'd': + return _decode_dict(data) + elif ch.isdigit(): + return _decode_str(data) + else: + raise Exception('could not deserialize data: %s'%data) + +def bdecode(data): + """ + decode a bytearray + return deserialized object + """ + obj , data = _decode(data) + if len(data) > 0: + raise Exception('failed to deserialize, extra data: %s'%data) + return obj + +def _encode_str(s,buff): + """ + encode string to a buffer + """ + s = bytearray(s) + l = len(s) + buff.append(bytearray(str(l)+':','utf-8')) + buff.append(s) + +def _encode_int(i,buff): + """ + encode integer to a buffer + """ + buff.append(b'i') + buff.append(bytearray(str(i),'ascii')) + buff.append(b'e') + +def _encode_list(l,buff): + """ + encode list of elements to a buffer + """ + buff.append(b'l') + for i in l: + _encode(i,buff) + buff.append(b'e') + +def _encode_dict(d,buff): + """ + encode dict + """ + buff.append(b'd') + l = list(d.keys()) + l.sort() + for k in l: + _encode(str(k),buff) + _encode(d[k],buff) + buff.append(b'e') + +def _encode(obj,buff): + """ + encode element obj to a buffer buff + """ + if isinstance(obj,str): + _encode_str(bytearray(obj,'utf-8'),buff) + elif isinstance(obj,bytes): + _encode_str(bytearray(obj),buff) + elif isinstance(obj,bytearray): + _encode_str(obj,buff) + elif str(obj).isdigit(): + _encode_int(obj,buff) + elif isinstance(obj,list): + _encode_list(obj,buff) + elif hasattr(obj,'keys') and hasattr(obj,'values'): + _encode_dict(obj,buff) + elif str(obj) in ['True','False']: + _encode_int(int(obj and '1' or '0'),buff) + else: + raise Exception('non serializable object: %s'%obj) + + +def bencode(obj): + """ + bencode element, return bytearray + """ + buff = [] + _encode(obj,buff) + ret = bytearray() + for ba in buff: + ret += ba + return bytes(ret) diff --git a/ox/torrent/btformats.py b/ox/torrent/btformats.py deleted file mode 100644 index 825b87d..0000000 --- a/ox/torrent/btformats.py +++ /dev/null @@ -1,100 +0,0 @@ -# Written by Bram Cohen -# see LICENSE.txt for license information - -from types import StringType, LongType, IntType, ListType, DictType -from re import compile - -reg = compile(r'^[^/\\.~][^/\\]*$') - -ints = (LongType, IntType) - -def check_info(info): - if type(info) != DictType: - raise ValueError, 'bad metainfo - not a dictionary' - pieces = info.get('pieces') - if type(pieces) != StringType or len(pieces) % 20 != 0: - raise ValueError, 'bad metainfo - bad pieces key' - piecelength = info.get('piece length') - if type(piecelength) not in ints or piecelength <= 0: - raise ValueError, 'bad metainfo - illegal piece length' - name = info.get('name') - if type(name) != StringType: - raise ValueError, 'bad metainfo - bad name' - if not reg.match(name): - raise ValueError, 'name %s disallowed for security reasons' % name - if info.has_key('files') == info.has_key('length'): - raise ValueError, 'single/multiple file mix' - if info.has_key('length'): - length = info.get('length') - if type(length) not in ints or length < 0: - raise ValueError, 'bad metainfo - bad length' - else: - files = info.get('files') - if type(files) != ListType: - raise ValueError - for f in files: - if type(f) != DictType: - raise ValueError, 'bad metainfo - bad file value' - length = f.get('length') - if type(length) not in ints or length < 0: - raise ValueError, 'bad metainfo - bad length' - path = f.get('path') - if type(path) != ListType or path == []: - raise ValueError, 'bad metainfo - bad path' - for p in path: - if type(p) != StringType: - raise ValueError, 'bad metainfo - bad path dir' - if not reg.match(p): - raise ValueError, 'path %s disallowed for security reasons' % p - for i in xrange(len(files)): - for j in xrange(i): - if files[i]['path'] == files[j]['path']: - raise ValueError, 'bad metainfo - duplicate path' - -def check_message(message): - if type(message) != DictType: - raise ValueError - check_info(message.get('info')) - if type(message.get('announce')) != StringType: - raise ValueError - -def check_peers(message): - if type(message) != DictType: - raise ValueError - if message.has_key('failure reason'): - if type(message['failure reason']) != StringType: - raise ValueError - return - peers = message.get('peers') - if type(peers) == ListType: - for p in peers: - if type(p) != DictType: - raise ValueError - if type(p.get('ip')) != StringType: - raise ValueError - port = p.get('port') - if type(port) not in ints or p <= 0: - raise ValueError - if p.has_key('peer id'): - id = p['peer id'] - if type(id) != StringType or len(id) != 20: - raise ValueError - elif type(peers) != StringType or len(peers) % 6 != 0: - raise ValueError - interval = message.get('interval', 1) - if type(interval) not in ints or interval <= 0: - raise ValueError - minint = message.get('min interval', 1) - if type(minint) not in ints or minint <= 0: - raise ValueError - if type(message.get('tracker id', '')) != StringType: - raise ValueError - npeers = message.get('num peers', 0) - if type(npeers) not in ints or npeers < 0: - raise ValueError - dpeers = message.get('done peers', 0) - if type(dpeers) not in ints or dpeers < 0: - raise ValueError - last = message.get('last', 0) - if type(last) not in ints or last < 0: - raise ValueError diff --git a/ox/torrent/makemetafile.py b/ox/torrent/makemetafile.py index cc9cce1..f4b0ca0 100644 --- a/ox/torrent/makemetafile.py +++ b/ox/torrent/makemetafile.py @@ -6,9 +6,13 @@ from os.path import getsize, split, join, abspath, isdir from os import listdir from hashlib import sha1 as sha from copy import copy -from string import strip -from bencode import bencode -from btformats import check_info +import re + +from six import PY2 +if PY2: + from .bencode import bencode +else: + from .bencode3 import bencode from threading import Event from time import time from traceback import print_exc @@ -57,14 +61,63 @@ def print_announcelist_details(): print ('') print (' httpseeds = optional list of http-seed URLs, in the format:') print (' url[|url...]') + +reg = re.compile(r'^[^/\\.~][^/\\]*$') + +def is_number(value): + return isinstance(value, int) or isinstance(value,float) + + +def check_info(info): + if not isinstance(info, dict): + raise ValueError('bad metainfo - not a dictionary') + pieces = info.get('pieces') + if not isinstance(pieces, bytes) or len(pieces) % 20 != 0: + raise ValueError('bad metainfo - bad pieces key') + piecelength = info.get('piece length') + if not is_number(piecelength) or piecelength <= 0: + raise ValueError('bad metainfo - illegal piece length') + name = info.get('name') + if not isinstance(name, bytes): + raise ValueError('bad metainfo - bad name') + if not reg.match(name.decode('utf-8')): + raise ValueError('name %s disallowed for security reasons' % name) + if ('files' in info) == ('length' in info): + raise ValueError('single/multiple file mix') + if 'length' in info: + length = info.get('length') + if not is_number(length) or length < 0: + raise ValueError('bad metainfo - bad length') + else: + files = info.get('files') + if not isinstance(files, list): + raise ValueError + for f in files: + if not isinstance(f, dict): + raise ValueError('bad metainfo - bad file value') + length = f.get('length') + if not is_number(length) or length < 0: + raise ValueError('bad metainfo - bad length') + path = f.get('path') + if not isinstance(path, list) or path == []: + raise ValueError('bad metainfo - bad path') + for p in path: + if not isinstance(p, bytes): + raise ValueError('bad metainfo - bad path dir') + if not reg.match(p.decode('utf-8')): + raise ValueError('path %s disallowed for security reasons' % p) + for i in range(len(files)): + for j in range(i): + if files[i]['path'] == files[j]['path']: + raise ValueError('bad metainfo - duplicate path') def make_meta_file(file, url, params = {}, flag = Event(), progress = lambda x: None, progress_percent = 1): - if params.has_key('piece_size_pow2'): + if 'piece_size_pow2' in params: piece_len_exp = params['piece_size_pow2'] else: piece_len_exp = default_piece_len_exp - if params.has_key('target') and params['target'] != '': + if 'target' in params and params['target'] != '': f = params['target'] else: a, b = split(file) @@ -75,7 +128,7 @@ def make_meta_file(file, url, params = {}, flag = Event(), if piece_len_exp == 0: # automatic size = calcsize(file) - if size > 8L*1024*1024*1024: # > 8 gig = + if size > 8*1024*1024*1024: # > 8 gig = piece_len_exp = 21 # 2 meg pieces elif size > 2*1024*1024*1024: # > 2 gig = piece_len_exp = 20 # 1 meg pieces @@ -92,7 +145,7 @@ def make_meta_file(file, url, params = {}, flag = Event(), piece_length = 2 ** piece_len_exp encoding = None - if params.has_key('filesystem_encoding'): + if 'filesystem_encoding' in params: encoding = params['filesystem_encoding'] if not encoding: encoding = ENCODING @@ -104,28 +157,28 @@ def make_meta_file(file, url, params = {}, flag = Event(), return check_info(info) h = open(f, 'wb') - data = {'info': info, 'announce': strip(url), 'creation date': long(time())} + data = {'info': info, 'announce': url.strip(), 'creation date': int(time())} - if params.has_key('comment') and params['comment']: + if 'comment' in params and params['comment']: data['comment'] = params['comment'] - if params.has_key('real_announce_list'): # shortcut for progs calling in from outside + if 'real_announce_list' in params: # shortcut for progs calling in from outside data['announce-list'] = params['real_announce_list'] - elif params.has_key('announce_list') and params['announce_list']: + elif 'announce_list' in params and params['announce_list']: l = [] for tier in params['announce_list'].split('|'): l.append(tier.split(',')) data['announce-list'] = l - if params.has_key('real_httpseeds'): # shortcut for progs calling in from outside + if 'real_httpseeds' in params: # shortcut for progs calling in from outside data['httpseeds'] = params['real_httpseeds'] - elif params.has_key('httpseeds') and params['httpseeds']: + elif 'httpseeds' in params and params['httpseeds']: data['httpseeds'] = params['httpseeds'].split('|') - if params.has_key('url-list') and params['url-list']: + if 'url-list' in params and params['url-list']: data['url-list'] = params['url-list'].split('|') - if params.has_key('playtime') and params['playtime']: + if 'playtime' in params and params['playtime']: data['info']['playtime'] = params['playtime'] h.write(bencode(data)) @@ -134,7 +187,7 @@ def make_meta_file(file, url, params = {}, flag = Event(), def calcsize(file): if not isdir(file): return getsize(file) - total = 0L + total = 0 for s in subfiles(abspath(file)): total += getsize(s[1]) return total @@ -151,8 +204,8 @@ def uniconvertl(l, e): def uniconvert(s, e): try: - if s.__class__.__name__ != 'unicode': - s = unicode(s,e) + if isinstance(s, bytes): + s = s.decode(e) except UnicodeError: raise UnicodeError('bad filename: '+s) return s.encode('utf-8') @@ -164,15 +217,15 @@ def makeinfo(file, piece_length, encoding, flag, progress, progress_percent=1): subs.sort() pieces = [] sh = sha() - done = 0L + done = 0 fs = [] totalsize = 0.0 - totalhashed = 0L + totalhashed = 0 for p, f in subs: totalsize += getsize(f) for p, f in subs: - pos = 0L + pos = 0 size = getsize(f) fs.append({'length': size, 'path': uniconvertl(p, encoding)}) h = open(f, 'rb') @@ -196,13 +249,13 @@ def makeinfo(file, piece_length, encoding, flag, progress, progress_percent=1): h.close() if done > 0: pieces.append(sh.digest()) - return {'pieces': ''.join(pieces), + return {'pieces': b''.join(pieces), 'piece length': piece_length, 'files': fs, 'name': uniconvert(split(file)[1], encoding) } else: size = getsize(file) pieces = [] - p = 0L + p = 0 h = open(file, 'rb') while p < size: x = h.read(min(piece_length, size - p)) @@ -217,7 +270,7 @@ def makeinfo(file, piece_length, encoding, flag, progress, progress_percent=1): else: progress(min(piece_length, size - p)) h.close() - return {'pieces': ''.join(pieces), + return {'pieces': b''.join(pieces), 'piece length': piece_length, 'length': size, 'name': uniconvert(split(file)[1], encoding) } @@ -240,7 +293,7 @@ def completedir(dir, url, params = {}, flag = Event(), files = listdir(dir) files.sort() ext = '.torrent' - if params.has_key('target'): + if 'target' in params: target = params['target'] else: target = ''