class UnixControlSocket(_ControlSocket): def create_socket(self): filename = self.socket_filename if os.path.exists(filename): # If the file already exists, then either the last shutdown was # not clean, or there is another Anomos client running. try: # Check if another client is listening on the socket by # trying to send it a command. self.send_command('no-op') except BTFailure: pass else: raise BTFailure("Could not create control socket: already in use") try: os.unlink(filename) except OSError, e: raise BTFailure("Could not remove old control socket filename:" + str(e)) try: self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.socket.setblocking(0) self.bind(filename) except socket.error, e: raise BTFailure("Could not create control socket: "+str(e))
def _restore_state(self): def decode_line(line): hashtext = line[:40] try: infohash = hashtext.decode('hex') except: raise BTFailure("Invalid state file contents") if len(infohash) != 20: raise BTFailure("Invalid state file contents") try: path = os.path.join(self.config['data_dir'], 'metainfo', hashtext) f = file(path, 'rb') data = f.read() f.close() except Exception, e: try: f.close() except: pass self.global_error( "ERROR", "Error reading file %s (%s), \ cannot restore state completely" % (path, str(e))) return None if infohash in self.torrents: raise BTFailure("Invalid state file (duplicate entry)") t = Torrent(infohash) self.torrents[infohash] = t try: t.metainfo = ConvertedMetainfo(bdecode(data)) except Exception, e: self.global_error( "ERROR", "Corrupt data in " + path + " , cannot restore torrent (" + str(e) + ")") return None
def run(self, scrwin): def reread(): self.multitorrent.schedule(0, self.reread_config) self.d = CursesDisplayer(scrwin, self.errlist, self.doneflag, reread) try: self.multitorrent = Multitorrent(self.config, self.doneflag) # raises BTFailure if bad metainfo = ConvertedMetainfo(bdecode(self.metainfo)) torrent_name = metainfo.name_fs if config['save_as']: if config['save_in']: raise BTFailure('You cannot specify both --save_as and ' '--save_in') saveas = config['save_as'] elif config['save_in']: saveas = os.path.join(config['save_in'], torrent_name) else: saveas = torrent_name self.d.set_torrent_values(metainfo.name, os.path.abspath(saveas), metainfo.file_size, len(metainfo.hashes)) self.torrent = self.multitorrent.start_torrent( metainfo, self.config, self, saveas) except BTFailure, e: errlist.append(str(e)) return
def check_fastresume(self, resumefile, return_filelist=False, piece_size=None, numpieces=None, allfiles=None): filenames = [name for _, _, name in self.ranges] if resumefile is not None: version = resumefile.readline() if version != 'Anomos resume state file, version 1\n': raise BTFailure('Unsupported fastresume file format, ' 'maybe from another client version') amount_done = int(resumefile.readline()) else: amount_done = size = mtime = 0 for filename in filenames: if resumefile is not None: line = resumefile.readline() size, mtime = line.split()[:2] # allow adding extra fields size = int(size) if os.path.exists(filename): fsize = os.path.getsize(filename) else: fsize = 0 # Bram cast this to float, then to int for some reason, but that's # why this didn't work - leaving them as str makes this actually # work. Ass burgers. if fsize > 0 and str(mtime) != str(os.path.getmtime(filename)): raise BTFailure("Fastresume info doesn't match file " "modification time") if size != fsize: raise BTFailure("Fastresume data doesn't match actual " "filesize") if not return_filelist: return amount_done if resumefile is None: return None if numpieces < 32768: typecode = 'h' else: typecode = 'l' try: r = array(typecode) r.fromfile(resumefile, numpieces) except Exception, e: raise BTFailure("Couldn't read fastresume data: " + str(e))
def parse_options(defaults, newvalues): for key, value in newvalues.iteritems(): if not defaults.has_key(key): raise BTFailure('unknown key ' + format_key(key)) try: t = type(defaults[key]) if t is NoneType or t is StringType: newvalues[key] = value elif t in (IntType, LongType): newvalues[key] = int(value) elif t is FloatType: newvalues[key] = float(value) else: assert False except ValueError, e: raise BTFailure('wrong format of %s - %s' % (format_key(key), str(e)))
def read(self, pos, amount): r = [] for filename, pos, end in self._intervals(pos, amount): h = self._get_file_handle(filename, False) h.seek(pos) r.append(h.read(end - pos)) r = ''.join(r) if len(r) != amount: raise BTFailure('Short read - something truncated files?') return r
def add_files(self, files, torrent): for filename in files: if filename in self.allfiles: raise BTFailure('File ' + filename + ' belongs to another running ' 'torrent') for filename in files: self.allfiles[filename] = torrent if self.handlebuffer is None and \ len(self.allfiles) > self.max_files_open: self.handlebuffer = self.handles.keys()
def send_command(self, action, data=''): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.connect(('127.0.0.1', 56881)) s.send(tobinary(len(action))) s.send(action) s.send(tobinary(len(data))) s.send(data) s.close() except socket.error, e: s.close() raise BTFailure('Could not send command: ' + str(e))
def send_command(self, action, data=''): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) filename = self.socket_filename try: s.connect(filename) s.send(tobinary(len(action))) s.send(action) s.send(tobinary(len(data))) s.send(data) s.close() except socket.error, e: s.close() raise BTFailure('Could not send command: ' + str(e))
def decode_line(line): hashtext = line[:40] try: infohash = hashtext.decode('hex') except: raise BTFailure("Invalid state file contents") if len(infohash) != 20: raise BTFailure("Invalid state file contents") try: path = os.path.join(self.config['data_dir'], 'metainfo', hashtext) f = file(path, 'rb') data = f.read() f.close() except Exception, e: try: f.close() except: pass self.global_error( "ERROR", "Error reading file %s (%s), \ cannot restore state completely" % (path, str(e))) return None
def create_socket(self): try: reuse = True tos = 0 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if reuse and os.name != 'nt': self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setblocking(0) if tos != 0: try: self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, tos) except: pass self.bind(('127.0.0.1', 56881)) self.listen(5) except socket.error, e: raise BTFailure("Could not create control socket: "+str(e))
def parseargs(argv, options, minargs=None, maxargs=None, presets=None): config = {} for option in options: longname, default, doc = option config[longname] = default args = [] pos = 0 if presets is None: presets = {} else: presets = presets.copy() while pos < len(argv): if argv[pos][:1] != '-': # not a cmdline option args.append(argv[pos]) pos += 1 else: key, value = None, None if argv[pos][:2] == '--': # --aaa 1 if pos == len(argv) - 1: usage('parameter passed in at end with no value') key, value = argv[pos][2:], argv[pos + 1] pos += 2 elif argv[pos][:1] == '-': key = argv[pos][1:2] if len(argv[pos]) > 2: # -a1 value = argv[pos][2:] pos += 1 else: # -a 1 if pos == len(argv) - 1: usage('parameter passed in at end with no value') value = argv[pos + 1] pos += 2 else: raise BTFailure('command line parsing failed at ' + argv[pos]) presets[key] = value parse_options(config, presets) config.update(presets) for key, value in config.items(): if value is None: usage("Option %s is required." % format_key(key)) if minargs is not None and len(args) < minargs: usage("Must supply at least %d args." % minargs) if maxargs is not None and len(args) > maxargs: usage("Too many args - %d max." % maxargs) return (config, args)
def _move_piece(self, oldpos, newpos): assert self.rplaces[newpos] < 0 assert self.rplaces[oldpos] >= 0 data = self.storage.read(self.piece_size * oldpos, self._piecelen(newpos)) self.storage.write(self.piece_size * newpos, data) if self.rplaces[newpos] == UNALLOCATED: self.storage.allocated(self.piece_size * newpos, len(data)) piece = self.rplaces[oldpos] self.places[piece] = newpos self.rplaces[oldpos] = ALLOCATED self.rplaces[newpos] = piece if not self.have[piece]: return data = data[:self._piecelen(piece)] if hashlib.sha1(data).digest() != self.hashes[piece]: raise BTFailure('data corrupted on disk - ' 'maybe you have two copies running?')
def __init__(self, config, filepool, files, check_only=False): self.filepool = filepool self.config = config self.ranges = [] self.myfiles = {} self.tops = {} self.undownloaded = {} self.unallocated = {} total = 0 for filename, length in files: self.unallocated[filename] = length self.undownloaded[filename] = length if length > 0: self.ranges.append((total, total + length, filename)) self.myfiles[filename] = None total += length if os.path.exists(filename): if not os.path.isfile(filename): raise BTFailure('File ' + filename + ' already exists, but ' 'is not a regular file') l = os.path.getsize(filename) if l > length and not check_only: h = file(filename, 'rb+') h.truncate(length) h.close() l = length self.tops[filename] = l elif not check_only: f = os.path.split(filename)[0] if f != '' and not os.path.exists(f): os.makedirs(f) file(filename, 'wb').close() self.begins = [i[0] for i in self.ranges] self.total_length = total if check_only: return self.handles = filepool.handles self.whandles = filepool.whandles # Rather implement this as an ugly hack here than change all the # individual calls. Affects all torrent instances using this module. if config['enable_bad_libc_workaround']: bad_libc_workaround()
def handle_accept(self): assert self.callback is not None try: sock, addr = self.socket.accept() except socket.error, e: raise BTFailure("Could not create control socket: "+str(e))
def check_info(info, check_paths=True): if type(info) != dict: raise BTFailure('bad metainfo - not a dictionary') pieces = info.get('pieces') if type(pieces) != str or len(pieces) % 20 != 0: raise BTFailure('bad metainfo - bad pieces key') piecelength = info.get('piece length') if type(piecelength) not in ints or piecelength <= 0: raise BTFailure('bad metainfo - illegal piece length') name = info.get('name') if type(name) != str: raise BTFailure('bad metainfo - bad name') if not allowed_path_re.match(name): raise BTFailure('name %s disallowed for security reasons' % name) if info.has_key('files') == info.has_key('length'): raise BTFailure('single/multiple file mix') if info.has_key('length'): length = info.get('length') if type(length) not in ints or length < 0: raise BTFailure('bad metainfo - bad length') else: files = info.get('files') if type(files) != list: raise BTFailure('bad metainfo - "files" is not a list of files') for f in files: if type(f) != dict: raise BTFailure('bad metainfo - bad file value') length = f.get('length') if type(length) not in ints or length < 0: raise BTFailure('bad metainfo - bad length') path = f.get('path') if type(path) != list or path == []: raise BTFailure('bad metainfo - bad path') for p in path: if type(p) != str: raise BTFailure('bad metainfo - bad path dir') if check_paths and not allowed_path_re.match(p): raise BTFailure('path %s disallowed for security reasons' % p) f = ['/'.join(x['path']) for x in files] f.sort() i = iter(f) try: name2 = i.next() while True: name1 = name2 name2 = i.next() if name2.startswith(name1): if name1 == name2: raise BTFailure('bad metainfo - duplicate path') elif name2[len(name1)] == '/': raise BTFailure('bad metainfo - name used as both ' 'file and subdirectory name') except StopIteration: pass
if args: if config['responsefile']: raise BTFailure, 'must have responsefile as arg or ' \ 'parameter, not both' config['responsefile'] = args[0] try: if config['responsefile']: h = file(config['responsefile'], 'rb') metainfo = h.read() h.close() elif config['url']: h = urlopen(config['url']) metainfo = h.read() h.close() else: raise BTFailure('you need to specify a .torrent file') except IOError, e: raise BTFailure('Error reading .torrent file: ', str(e)) except BTFailure, e: print str(e) sys.exit(1) errlist = [] dl = DL(metainfo, config, errlist) curses_wrapper(dl.run) if errlist: print "These messages were logged during execution:" for error in errlist: print error
def message(self, s): print "### " + s def exception(self, s): exceptions.append(s) self.message('SYSTEM ERROR - EXCEPTION GENERATED') if __name__ == '__main__': uiname = 'anonlaunchmany' defaults = get_defaults(uiname) try: if len(sys.argv) < 2: printHelp(uiname, defaults) sys.exit(1) config, args = configfile.parse_configuration_and_args( defaults, uiname, sys.argv[1:], 0, 1) if args: config['torrent_dir'] = args[0] if not os.path.isdir(config['torrent_dir']): raise BTFailure("Warning: " + args[0] + " is not a directory") except BTFailure, e: print 'error: ' + str( e) + '\nrun with no args for parameter explanations' sys.exit(1) LaunchMany(config, HeadlessDisplayer(), 'anonlaunchmany') if exceptions: print '\nEXCEPTION:' print exceptions[0]
def check_message(message, check_paths=True): if type(message) != dict: raise BTFailure('bad metainfo - wrong object type') check_info(message.get('info'), check_paths) if type(message.get('announce')) != str: raise BTFailure('bad metainfo - no announce URL string')
def add_torrent(self, infohash, torrent): if infohash in self.torrents: raise BTFailure("Can't start two separate instances of the same " "torrent") self.torrents[infohash] = torrent
"ERROR", "Corrupt data in " + path + " , cannot restore torrent (" + str(e) + ")") return None if len(line) == 41: t.dlpath = None return infohash, t try: if version < 2: t.dlpath = line[41:-1].decode('string_escape') else: up, down, dlpath = line[41:-1].split(' ', 2) t.uptotal = t.uptotal_old = int(up) t.downtotal = t.downtotal_old = int(down) t.dlpath = dlpath.decode('string_escape') except ValueError: # unpack, int(), decode() raise BTFailure('Invalid state file (bad entry)') return infohash, t filename = os.path.join(self.config['data_dir'], 'ui_state') if not os.path.exists(filename): return f = None try: f = file(filename, 'rb') lines = f.readlines() f.close() except Exception, e: if f is not None: f.close() raise BTFailure(str(e)) i = iter(lines)
def check_peers(message): if type(message) != dict: raise BTFailure if message.has_key('failure reason'): if type(message['failure reason']) != str: raise BTFailure('non-text failure reason') return if message.has_key('warning message'): if type(message['warning message']) != str: raise BTFailure('non-text warning message') peers = message.get('peers', []) if type(peers) != list: raise BTFailure('invalid peer list') for p in peers: if type(p) != dict: raise BTFailure('invalid entry in peer list') if type(p.get('ip')) != str: raise BTFailure('invalid entry in peer list') port = p.get('port') if type(port) not in ints or p <= 0: raise BTFailure('invalid entry in peer list') if p.has_key('nid'): nid = p.get('nid') if type(nid) != str or len(nid) != 1: raise BTFailure('invalid entry in peer list') #PeerID only used in BitTorrent #if p.has_key('peer id'): # peerid = p.get('peer id') # if type(peerid) != str or len(peerid) != 20: # raise BTFailure('invalid entry in peer list') interval = message.get('interval', 1) if type(interval) not in ints or interval <= 0: raise BTFailure('invalid announce interval') minint = message.get('min interval', 1) if type(minint) not in ints or minint <= 0: raise BTFailure('invalid min announce interval') tcodes = message.get('tracking codes', []) if type(tcodes) != list: raise BTFailure('invalid tracking code list') for t in tcodes: if type(t) != list: raise BTFailure('invalid entry in tracking code list') if len(t) != 2: raise BTFailure('invalid tracking code entry length') if type(t[0]) != str or type(t[1]) != str: raise BTFailure('invalid format for tracking code components')
def usage(str): raise BTFailure(str)
def __init__(self, storage, config, hashes, piece_size, finished, statusfunc, flag, data_flunked, infohash, resumefile): self.numpieces = len(hashes) self.storage = storage self.config = config check_hashes = config['check_hashes'] self.hashes = hashes self.piece_size = piece_size self.data_flunked = data_flunked self.total_length = storage.get_total_length() self.amount_left = self.total_length self.partial_mark = "Anomos - this part has not been "+\ "downloaded yet."+infohash+\ tobinary(config['download_slice_size']) if self.total_length <= piece_size * (self.numpieces - 1): raise BTFailure, 'bad data in responsefile - total too small' if self.total_length > piece_size * self.numpieces: raise BTFailure, 'bad data in responsefile - total too big' self.finished = finished self.numactive = array('H', [0] * self.numpieces) self.inactive_requests = [1] * self.numpieces self.amount_inactive = self.total_length self.endgame = False self.have = Bitfield(self.numpieces) self.waschecked = Bitfield(self.numpieces) if self.numpieces < 32768: typecode = 'h' else: typecode = 'l' self.places = array(typecode, [NO_PLACE] * self.numpieces) if not check_hashes: self.rplaces = array(typecode, range(self.numpieces)) fastresume = True else: self.rplaces = self._load_fastresume(resumefile, typecode) if self.rplaces is not None: fastresume = True else: self.rplaces = array(typecode, [UNALLOCATED] * self.numpieces) fastresume = False self.holepos = 0 self.stat_numfound = 0 self.stat_numflunked = 0 self.stat_numdownloaded = 0 self.stat_active = {} self.stat_new = {} self.stat_dirty = {} self.download_history = {} self.failed_pieces = {} if self.numpieces == 0: return targets = {} total = 0 if not fastresume: for i in xrange(self.numpieces): if self._waspre(i): self.rplaces[i] = ALLOCATED total += 1 else: targets[hashes[i]] = i if total and check_hashes: statusfunc('checking existing file', 0) def markgot(piece, pos): if self.have[piece]: if piece != pos: return self.rplaces[self.places[pos]] = ALLOCATED self.places[pos] = self.rplaces[pos] = pos return self.places[piece] = pos self.rplaces[pos] = piece self.have[piece] = True self.amount_left -= self._piecelen(piece) self.amount_inactive -= self._piecelen(piece) self.inactive_requests[piece] = None if not fastresume: self.waschecked[piece] = True self.stat_numfound += 1 lastlen = self._piecelen(self.numpieces - 1) partials = {} for i in xrange(self.numpieces): if not self._waspre(i): if self.rplaces[i] != UNALLOCATED: raise BTFailure("--check_hashes 0 or fastresume info " "doesn't match file state (missing data)") continue elif fastresume: t = self.rplaces[i] if t >= 0: markgot(t, i) continue if t == UNALLOCATED: raise BTFailure("Bad fastresume info (files contain more " "data)") if t == ALLOCATED: continue if t!= FASTRESUME_PARTIAL: raise BTFailure("Bad fastresume info (illegal value)") data = self.storage.read(self.piece_size * i, self._piecelen(i)) self._check_partial(i, partials, data) self.rplaces[i] = ALLOCATED else: data = self.storage.read(piece_size * i, self._piecelen(i)) sh = hashlib.sha1(buffer(data, 0, lastlen)) sp = sh.digest() sh.update(buffer(data, lastlen)) s = sh.digest() if s == hashes[i]: markgot(i, i) elif s in targets and self._piecelen(i) == self._piecelen(targets[s]): markgot(targets[s], i) elif not self.have[self.numpieces - 1] and sp == hashes[-1] and (i == self.numpieces - 1 or not self._waspre(self.numpieces - 1)): markgot(self.numpieces - 1, i) else: self._check_partial(i, partials, data) statusfunc(fractionDone = 1 - float(self.amount_left) / self.total_length) if flag.isSet(): return self.amount_left_with_partials = self.amount_left for piece in partials: if self.places[piece] < 0: pos = partials[piece][0] self.places[piece] = pos self.rplaces[pos] = piece self._make_partial(piece, partials[piece][1]) for i in xrange(self.numpieces): if self.rplaces[i] != UNALLOCATED: self.storage.allocated(piece_size * i, self._piecelen(i)) if self.have[i]: self.storage.downloaded(piece_size * i, self._piecelen(i))
def __init__(self, metainfo): self.bad_torrent_wrongfield = False self.bad_torrent_unsolvable = False self.bad_torrent_noncharacter = False self.bad_conversion = False self.bad_windows = False self.bad_path = False self.reported_errors = False self.is_batch = False self.orig_files = None self.files_fs = None self.file_size = 0 self.sizes = [] self.anon = None if metainfo.has_key('anon'): self.anon = bool(metainfo['anon']) btformats.check_message(metainfo, check_paths=False) info = metainfo['info'] if info.has_key('length'): self.file_size = info['length'] self.sizes.append(self.file_size) else: self.is_batch = True r = [] self.orig_files = [] self.sizes = [] i = 0 for f in info['files']: l = f['length'] self.file_size += l self.sizes.append(l) path = self._get_attr_utf8(f, 'path') for x in path: if not btformats.allowed_path_re.match(x): if l > 0: raise BTFailure("Bad file path component: "+x) # BitComet makes bad .torrent files with empty # filename part self.bad_path = True break else: path = [(self._enforce_utf8(x), x) for x in path] self.orig_files.append('/'.join([x[0] for x in path])) r.append(([(self._to_fs_2(u), u, o) for u, o in path], i)) i += 1 # If two or more file/subdirectory names in the same directory # would map to the same name after encoding conversions + Windows # workarounds, change them. Files are changed as # 'a.b.c'->'a.b.0.c', 'a.b.1.c' etc, directories or files without # '.' as 'a'->'a.0', 'a.1' etc. If one of the multiple original # names was a "clean" conversion, that one is always unchanged # and the rest are adjusted. r.sort() self.files_fs = [None] * len(r) prev = [None] res = [] stack = [{}] for x in r: j = 0 x, i = x while x[j] == prev[j]: j += 1 del res[j:] del stack[j+1:] name = x[j][0][1] if name in stack[-1]: for name in generate_names(x[j][1], j != len(x) - 1): name = self._to_fs(name) if name not in stack[-1]: break stack[-1][name] = None res.append(name) for j in range(j + 1, len(x)): name = x[j][0][1] stack.append({name: None}) res.append(name) self.files_fs[i] = os.path.join(*res) prev = x self.name = self._get_field_utf8(info, 'name') self.name_fs = self._to_fs(self.name) self.piece_length = info['piece length'] self.announce = metainfo['announce'] self.hashes = [info['pieces'][x:x+20] for x in xrange(0, len(info['pieces']), 20)] self.infohash = hashlib.sha1(bencode(info)).digest()