예제 #1
0
    def parse_extended_message(self):
        extended_message_type = self.field[0]
        message = self.field[1:]

        if extended_message_type == 0:
            try:
                message = bdecode(message)
            except DecodingError:
                self.error.set()
                return

            if b'm' not in message:
                logger.debug('"m" not in extended handshake.')
                self.error.set()
                return

            self.extended_message_types = message[b'm']

            if b'ut_metadata' not in self.extended_message_types:
                logger.debug('Peer does not support metadata protocol.')
                self.error.set()
                return

            if b'metadata_size' not in message:
                logger.debug('Peer did not send "metadata_size" in extended handshake.')
                self.error.set()
                return

            self.metadata_size = message[b'metadata_size']
            logger.info('metadata size: {}'.format(self.metadata_size))
            self.extended_handshake_complete.set()

            self.write_message(15, b'') # have none
            logger.debug('Sent HAVE NONE.')
            self.write_message(0, b'') # choke
            logger.debug('Sent CHOKE.')
            self.write_message(3, b'') # not interesete
            logger.debug('Sent NOT INTERESTED.')
        elif extended_message_type == self.extended_message_types[b'ut_metadata']:
            original_message = message
            try:
                message = bdecode(message)
            except DecodingError:
                self.error.set()
                return

            if message[b'msg_type'] == 0:
                reply = {
                    'msg_type': 2,
                    'piece': message[b'piece']
                }
            elif message[b'msg_type'] == 2:
                logger.debug('Request for metadata rejected.')
                return
            elif message[b'msg_type'] == 1:
                size = len(original_message) - len(bencode(message))
                logger.debug('Got a metadata block of size: {}'.format(size))

                self.metadata_block = original_message[-size:]
                self.metadata_block_received.set()
예제 #2
0
    def parse_extended_message(self):
        extended_message_type = self.field[0]
        message = self.field[1:]

        if extended_message_type == 0:
            try:
                message = bdecode(message)
            except DecodingError:
                self.error.set()
                return

            if b'm' not in message:
                logger.debug('"m" not in extended handshake.')
                self.error.set()
                return

            self.extended_message_types = message[b'm']

            if b'ut_metadata' not in self.extended_message_types:
                logger.debug('Peer does not support metadata protocol.')
                self.error.set()
                return

            if b'metadata_size' not in message:
                logger.debug('Peer did not send "metadata_size" in extended handshake.')
                self.error.set()
                return

            self.metadata_size = message[b'metadata_size']
            logger.info('metadata size: {}'.format(self.metadata_size))
            self.extended_handshake_complete.set()

            self.write_message(15, b'') # have none
            logger.debug('Sent HAVE NONE.')
            self.write_message(0, b'') # choke
            logger.debug('Sent CHOKE.')
            self.write_message(3, b'') # not interesete
            logger.debug('Sent NOT INTERESTED.')
        elif extended_message_type == self.extended_message_types[b'ut_metadata']:
            original_message = message
            try:
                message = bdecode(message)
            except DecodingError:
                self.error.set()
                return

            if message[b'msg_type'] == 0:
                reply = {
                    'msg_type': 2,
                    'piece': message[b'piece']
                }
            elif message[b'msg_type'] == 2:
                logger.debug('Request for metadata rejected.')
                return
            elif message[b'msg_type'] == 1:
                size = len(original_message) - len(bencode(message))
                logger.debug('Got a metadata block of size: {}'.format(size))

                self.metadata_block = original_message[-size:]
                self.metadata_block_received.set()
예제 #3
0
def test_decode_parameter():
    """Ensure non-strings raise an exception."""
    # TODO: BTL implementation currently chokes on this type of input
    # self.assertRaises(BTFailure, bdecode, 0)
    # self.assertRaises(BTFailure, bdecode, None)
    # self.assertRaises(BTFailure, bdecode, 1.0)
    with pytest.raises(BencodeDecodeError):
        bdecode([1, 2])

    with pytest.raises(BencodeDecodeError):
        bdecode({'foo': 'bar'})
예제 #4
0
def verify_torrent_contents(torrent_file, path):
    orig_path = path

    if hasattr(torrent_file, 'seek') and hasattr(torrent_file, 'read'):
        torrent_file.seek(0)
        torrent = bdecode(torrent_file.read())
    else:
        try:
            with open(torrent_file, 'rb') as f:
                torrent = bdecode(f.read())
        except (IOError, TypeError):
            torrent = bdecode(torrent_file)

    path = path_join(path, torrent[b'info'][b'name'].decode('utf-8'))
    is_a_file = False
    try:
        with open(path, 'rb'):
            is_a_file = True
    except IOError:
        pass

    if not isdir(path) and not is_a_file:
        raise IOError('Path specified for torrent data is invalid')

    piece_length = torrent[b'info'][b'piece length']
    piece_hashes = torrent[b'info'][b'pieces']
    piece_hashes = struct.unpack('<{}B'.format(len(piece_hashes)),
                                 piece_hashes)
    piece_hashes = _chunks(piece_hashes, 20)

    try:
        filenames = ['/'.join([y.decode('utf-8') for y in x[b'path']])
                     for x in torrent[b'info'][b'files']]
    except KeyError as e:
        if e.args[0] != b'files':
            raise e

        # Single file torrent, never has a directory
        filenames = [path]
        path = orig_path

    pieces = _get_torrent_pieces(filenames, path, piece_length)

    for known_hash, piece in zip(piece_hashes, pieces):
        try:
            file_hash = sha1(piece).digest()
        except TypeError:
            raise VerificationError('Unable to get hash for piece')

        if not compare_digest(bytes(known_hash), file_hash):
            raise VerificationError('Computed hash does not match torrent file\'s hash')
예제 #5
0
def scrape_http(parsed_tracker, hashes):
    qs = []
    for hash in hashes:
        url_param = binascii.a2b_hex(hash)
        qs.append(("info_hash", url_param))
    qs = urlencode(qs)
    pt = parsed_tracker
    url = urlunsplit((pt.scheme, pt.netloc, pt.path, qs, pt.fragment))
    try:
        handle = urlopen(url, timeout=5)
    except Exception:
        return {}
    if handle.getcode() is not 200:
        raise RuntimeError("%s status code returned" % handle.getcode())
    decoded = bdecode(handle.read())
    decoded = {k.decode('utf8'): v for k, v in decoded.items()}
    if 'files' not in decoded:
        return {}
    ret = {}
    for hash, stats in decoded['files'].items():
        nice_hash = binascii.b2a_hex(hash).decode('utf8')
        stats = {k.decode('utf8'): v for k, v in stats.items()}

        s = None if 'complete' not in stats else stats["complete"]
        p = None if 'incomplete' not in stats else stats["incomplete"]
        c = None if 'downloaded' not in stats else stats["downloaded"]
        ret[nice_hash] = {"seeds": s, "peers": p, "complete": c}
    return ret
예제 #6
0
    def datagram_received(self, data, addr):
        try:
            message = bdecode(data)
        except DecodingError:
            logger.debug('Received invalid bencoding in reply. Discarded.')
            return

        if b't' not in message:
            logger.debug('Received invalid reply. Discarded')
            return

        if message[b't'] != self.tid:
            logger.debug(
                'Received reply with invalid transaction ID. Discarded.')
            return

        if b'r' not in message or b'id' not in message[b'r']:
            logger.debug('Received invalid reply. Discarded.')
            return

        logger.debug('Received DHT reply from {}:{} with node ID {}.'.format(
            addr[0], addr[1],
            hexlify(message[b'r'][b'id']).decode()))

        self.reply = message[b'r']
        self.reply_received.set()
예제 #7
0
 def tracker_info(response):
     info = bdecode(response)
     failure = info.get(b'failure reason')
     if not failure:
         return info
     raise Tracker.TrackerError(
         "Tracker return with error: {}".format(failure)
     )
예제 #8
0
    def _decode(self, torrent_file):
        with open(torrent_file, 'rb') as f:
            self.torrent = f.read()
        odict = bdecode(self.torrent)

        self.announce = odict[b'announce'].decode('utf-8')
        self.info = odict[b'info']
        self.length = self._length(self.info)
        self.hash = sha1(bencode(self.info)).digest()
예제 #9
0
def test_decode_dict():
    """Ensure bytes can be decoded."""
    value = bdecode('d5:title7:Examplee')

    # Ensure a dict is returned
    assert isinstance(value, dict)

    # Validate items
    assert value == {b'title': b'Example'}
예제 #10
0
def test_decode_errors():
    """Illegally formatted strings should raise an exception when decoded."""
    with pytest.raises(BencodeDecodeError):
        bdecode("foo")

    with pytest.raises(BencodeDecodeError):
        bdecode("x:foo")

    with pytest.raises(BencodeDecodeError):
        bdecode("x42e")
예제 #11
0
def scrape_http(parsed_tracker, hashes):
    print("Scraping HTTP: %s for %s hashes" %
          (parsed_tracker.geturl(), len(hashes)))
    qs = []
    for hash in hashes:
        url_param = binascii.a2b_hex(hash)
        qs.append(("info_hash", url_param))
    qs = urlencode(qs)
    pt = parsed_tracker
    url = urlunsplit((pt.scheme, pt.netloc, pt.path, qs, pt.fragment))
    handle = requests.get(url)
    if handle.status_code != 200:
        raise RuntimeError("%s status code returned" % handle.status_code)
    decoded_dict = bencodepy.bdecode(handle.content)
    decoded = get_decoded_dict(decoded_dict)

    ret = {}
    for hash, stats in decoded['files'].items():
        nice_hash = binascii.b2a_hex(hash).decode('utf-8')
        s = stats["complete"]
        p = stats["incomplete"]
        c = stats["downloaded"]
        ret[nice_hash] = {"seeds": s, "peers": p, "complete": c}
    return ret
예제 #12
0
    def datagram_received(self, data, addr):
        try:
            message = bdecode(data)
        except DecodingError:
            logger.debug('Received invalid bencoding in reply. Discarded.')
            return

        if b't' not in message:
            logger.debug('Received invalid reply. Discarded')
            return

        if message[b't'] != self.tid:
            logger.debug('Received reply with invalid transaction ID. Discarded.')
            return

        if b'r' not in message or b'id' not in message[b'r']:
            logger.debug('Received invalid reply. Discarded.')
            return

        logger.debug('Received DHT reply from {}:{} with node ID {}.'.format(
            addr[0], addr[1], hexlify(message[b'r'][b'id']).decode()))

        self.reply = message[b'r']
        self.reply_received.set()
예제 #13
0
async def get_metadata(host, port, infohash):
    global metadata, metadata_size, keep_running, full_metadata, get_metadatas_in_progress

    if not keep_running:
        return True

    get_metadatas_in_progress += 1

    try:
        logger.info('Getting metadata from: {}:{}'.format(host, port))

        loop = asyncio.get_running_loop()
        try:
            transport, protocol = await loop.create_connection(
                lambda: BitTorrentProtocol(infohash, nodeid), host, port)
        except OSError as e:
            logger.debug('Connection error: {}'.format(e))
            return False

        logger.debug('Connected to peer: {}:{}'.format(host, port))

        done, pending = await asyncio.wait(
            [asyncio.create_task(protocol.handshake_complete.wait()),
             asyncio.create_task(protocol.error.wait())],
            return_when=FIRST_COMPLETED,
            timeout=TIMEOUT)

        for task in pending:
            task.cancel()

        if not done or protocol.error.is_set():
            logger.debug('Error communicating with the peer while waiting for the handshake.')
            transport.close()
            return False

        done, pending = await asyncio.wait(
            [asyncio.create_task(protocol.extended_handshake_complete.wait()),
             asyncio.create_task(protocol.error.wait())],
            return_when=FIRST_COMPLETED,
            timeout=TIMEOUT)

        for task in pending:
            task.cancel()

        if not done or protocol.error.is_set():
            logger.debug('Error communicating with the peer while waiting for the extended handshake.')
            transport.close()
            return False

        if metadata_size > 0 and metadata_size != protocol.metadata_size:
            logger.warning('Inconsistent metadata size received.')

        metadata_size = protocol.metadata_size
        metadata_nblocks = int(metadata_size / (16 * 1024))
        metadata_nblocks += 0 if metadata_size % (16 * 1024) == 0 else 1

        while keep_running:
            protocol.metadata_block_received.clear()

            try:
                i = next(i for i in range(metadata_nblocks)
                         if i not in [m[0] for m in metadata])
            except StopIteration as e:
                transport.close()
                return True

            protocol.get_metadata_block(i)

            done, pending = await asyncio.wait(
                [asyncio.create_task(protocol.metadata_block_received.wait()),
                 asyncio.create_task(protocol.error.wait())],
                return_when=FIRST_COMPLETED,
                timeout=TIMEOUT)

            for task in pending:
                task.cancel()

            if not done or protocol.error.is_set():
                logger.debug('Error communicating with the peer while waiting for metadata block.')
                transport.close()
                return False

            metadata.add((i, protocol.metadata_block))

            if {m[0] for m in metadata} == set(range(metadata_nblocks)):
                # metadata complete. hash check.
                m = hashlib.sha1()
                full_metadata = b''
                for i, b in sorted(metadata, key=lambda m: m[0]):
                    full_metadata += b
                    m.update(b)
                if m.digest() != infohash:
                    logger.debug('Invalid metadata received. Hash does not checkout. Discarding.')
                    metadata_size = 0
                    metadata = set()
                    return False

                logger.info('Metadata received.')
                full_metadata = bdecode(full_metadata)
                keep_running = False
    finally:
        get_metadatas_in_progress  -= 1

    return True
예제 #14
0
def test_decode_bytes():
    """Ensure bytes can be decoded."""
    assert bdecode(b'1:\x9c') == b'\x9c'
예제 #15
0
def test_encode_roundtrip():
    """Consecutive calls to decode and encode should deliver the original data again."""
    for plain, encoded in ENCODE:
        assert encoded == bencode(bdecode(encoded))
예제 #16
0
def test_decode_roundtrip():
    """Consecutive calls to encode and decode should deliver the original data again."""
    for plain, encoded in VALUES:
        assert plain == bdecode(bencode(plain))
예제 #17
0
def test_decode():
    """Decode should give known result with known input."""
    for plain, encoded in DECODE:
        assert plain == bdecode(encoded)
예제 #18
0
def get_metadata(loop, host, port, infohash):
    global metadata, metadata_size, keep_running, full_metadata, get_metadatas_in_progress

    if not keep_running:
        return True

    get_metadatas_in_progress += 1

    try:
        logger.info('Getting metadata from: {}:{}'.format(host, port))

        try:
            transport, protocol = yield from loop.create_connection(
                lambda: BitTorrentProtocol(infohash, nodeid), host, port)
        except OSError as e:
            logger.debug('Connection error: {}'.format(e))
            return False

        logger.debug('Connected to peer: {}:{}'.format(host, port))

        done, pending = yield from asyncio.wait(
            [protocol.handshake_complete.wait(),
             protocol.error.wait()],
            return_when=FIRST_COMPLETED,
            timeout=TIMEOUT)

        for task in pending:
            task.cancel()

        if not done or protocol.error.is_set():
            logger.debug('Error communicating with the peer while waiting for the handshake.')
            transport.close()
            return False

        done, pending = yield from asyncio.wait(
            [protocol.extended_handshake_complete.wait(),
             protocol.error.wait()],
            return_when=FIRST_COMPLETED,
            timeout=TIMEOUT)

        for task in pending:
            task.cancel()

        if not done or protocol.error.is_set():
            logger.debug('Error communicating with the peer while waiting for the extended handshake.')
            transport.close()
            return False

        if metadata_size > 0 and metadata_size != protocol.metadata_size:
            logger.warning('Inconsistent metadata size received.')

        metadata_size = protocol.metadata_size
        metadata_nblocks = int(metadata_size / (16 * 1024))
        metadata_nblocks += 0 if metadata_size % (16 * 1024) == 0 else 1

        while keep_running:
            protocol.metadata_block_received.clear()

            try:
                i = next(i for i in range(metadata_nblocks)
                         if i not in [m[0] for m in metadata])
            except StopIteration as e:
                transport.close()
                return True

            protocol.get_metadata_block(i)

            done, pending = yield from asyncio.wait(
                [protocol.metadata_block_received.wait(),
                 protocol.error.wait()],
                return_when=FIRST_COMPLETED,
                timeout=TIMEOUT)

            for task in pending:
                task.cancel()

            if not done or protocol.error.is_set():
                logger.debug('Error communicating with the peer while waiting for metadata block.')
                transport.close()
                return False

            metadata.add((i, protocol.metadata_block))

            if {m[0] for m in metadata} == set(range(metadata_nblocks)):
                # metadata complete. hash check.
                m = hashlib.sha1()
                full_metadata = b''
                for i, b in sorted(metadata, key=lambda m: m[0]):
                    full_metadata += b
                    m.update(b)
                if m.digest() != infohash:
                    logger.debug('Invalid metadata received. Hash does not checkout. Discarding.')
                    metadata_size = 0
                    metadata = set()
                    return False

                logger.info('Metadata received.')
                full_metadata = bdecode(full_metadata)
                keep_running = False
    finally:
        get_metadatas_in_progress  -= 1

    return True