First release, remove not used lines from gitignore
This commit is contained in:
parent
c0bfb3b062
commit
d28e1cb4a6
85 changed files with 7205 additions and 50 deletions
23
src/lib/subtl/LICENCE
Normal file
23
src/lib/subtl/LICENCE
Normal file
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2012, Packetloop. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Packetloop nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28
src/lib/subtl/README.md
Normal file
28
src/lib/subtl/README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# subtl
|
||||
|
||||
## Overview
|
||||
|
||||
SUBTL is a **s**imple **U**DP **B**itTorrent **t**racker **l**ibrary for Python, licenced under the modified BSD license.
|
||||
|
||||
## Example
|
||||
|
||||
This short example will list a few IP Addresses from a certain hash:
|
||||
|
||||
from subtl import UdpTrackerClient
|
||||
utc = UdpTrackerClient('tracker.openbittorrent.com', 80)
|
||||
utc.connect()
|
||||
if not utc.poll_once():
|
||||
raise Exception('Could not connect')
|
||||
print('Success!')
|
||||
|
||||
utc.announce(info_hash='089184ED52AA37F71801391C451C5D5ADD0D9501')
|
||||
data = utc.poll_once()
|
||||
if not data:
|
||||
raise Exception('Could not announce')
|
||||
for a in data['response']['peers']:
|
||||
print(a)
|
||||
|
||||
## Caveats
|
||||
|
||||
* There is no automatic retrying of sending packets yet.
|
||||
* This library won't download torrent files--it is simply a tracker client.
|
0
src/lib/subtl/__init__.py
Normal file
0
src/lib/subtl/__init__.py
Normal file
220
src/lib/subtl/subtl.py
Normal file
220
src/lib/subtl/subtl.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
'''
|
||||
Based on the specification at http://bittorrent.org/beps/bep_0015.html
|
||||
'''
|
||||
import random
|
||||
import struct
|
||||
import time
|
||||
import socket
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
__version__ = '0.0.1'
|
||||
|
||||
CONNECT = 0
|
||||
ANNOUNCE = 1
|
||||
SCRAPE = 2
|
||||
ERROR = 3
|
||||
|
||||
|
||||
def norm_info_hash(info_hash):
|
||||
if len(info_hash) == 40:
|
||||
info_hash = info_hash.decode('hex')
|
||||
if len(info_hash) != 20:
|
||||
raise UdpTrackerClientException(
|
||||
'info_hash length is not 20: {}'.format(len(info_hash)))
|
||||
return info_hash
|
||||
|
||||
|
||||
def info_hash_to_str(info_hash):
|
||||
return binascii.hexlify(info_hash)
|
||||
|
||||
|
||||
class UdpTrackerClientException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UdpTrackerClient:
|
||||
|
||||
def __init__(self, host, port):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.peer_port = 6881
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.conn_id = 0x41727101980
|
||||
self.transactions = {}
|
||||
self.peer_id = self._generate_peer_id()
|
||||
self.timeout = 2
|
||||
|
||||
def connect(self):
|
||||
return self._send(CONNECT)
|
||||
|
||||
def announce(self, **kwargs):
|
||||
if not kwargs:
|
||||
raise UdpTrackerClientException('arguments missing')
|
||||
args = {
|
||||
'peer_id': self.peer_id,
|
||||
'downloaded': 0,
|
||||
'left': 0,
|
||||
'uploaded': 0,
|
||||
'event': 0,
|
||||
'key': 0,
|
||||
'num_want': 10,
|
||||
'ip_address': 0,
|
||||
'port': self.peer_port,
|
||||
}
|
||||
args.update(kwargs)
|
||||
|
||||
fields = 'info_hash peer_id downloaded left uploaded event ' \
|
||||
'ip_address key num_want port'
|
||||
|
||||
# Check and raise if missing fields
|
||||
self._check_fields(args, fields)
|
||||
|
||||
# Humans tend to use hex representations of the hash. Wasteful humans.
|
||||
args['info_hash'] = norm_info_hash(args['info_hash'])
|
||||
|
||||
values = [args[a] for a in fields.split()]
|
||||
payload = struct.pack('!20s20sQQQLLLLH', *values)
|
||||
return self._send(ANNOUNCE, payload)
|
||||
|
||||
def scrape(self, info_hash_list):
|
||||
if len(info_hash_list) > 74:
|
||||
raise UdpTrackerClientException('Max info_hashes is 74')
|
||||
|
||||
payload = ''
|
||||
for info_hash in info_hash_list:
|
||||
info_hash = norm_info_hash(info_hash)
|
||||
payload += info_hash
|
||||
|
||||
trans = self._send(SCRAPE, payload)
|
||||
trans['sent_hashes'] = info_hash_list
|
||||
return trans
|
||||
|
||||
def poll_once(self):
|
||||
self.sock.settimeout(self.timeout)
|
||||
try:
|
||||
response = self.sock.recv(10240)
|
||||
except socket.timeout:
|
||||
return
|
||||
|
||||
header = response[:8]
|
||||
payload = response[8:]
|
||||
action, trans_id = struct.unpack('!LL', header)
|
||||
try:
|
||||
trans = self.transactions[trans_id]
|
||||
except KeyError:
|
||||
self.error('transaction_id not found')
|
||||
return
|
||||
trans['response'] = self._process_response(action, payload, trans)
|
||||
trans['completed'] = True
|
||||
del self.transactions[trans_id]
|
||||
return trans
|
||||
|
||||
def error(self, message):
|
||||
print('error: {}'.format(message))
|
||||
|
||||
def _send(self, action, payload=None):
|
||||
if not payload:
|
||||
payload = ''
|
||||
trans_id, header = self._request_header(action)
|
||||
self.transactions[trans_id] = trans = {
|
||||
'action': action,
|
||||
'time': time.time(),
|
||||
'payload': payload,
|
||||
'completed': False,
|
||||
}
|
||||
self.sock.sendto(header + payload, (self.host, self.port))
|
||||
return trans
|
||||
|
||||
def _request_header(self, action):
|
||||
trans_id = random.randint(0, (1 << 32) - 1)
|
||||
return trans_id, struct.pack('!QLL', self.conn_id, action, trans_id)
|
||||
|
||||
def _process_response(self, action, payload, trans):
|
||||
if action == CONNECT:
|
||||
return self._process_connect(payload, trans)
|
||||
elif action == ANNOUNCE:
|
||||
return self._process_announce(payload, trans)
|
||||
elif action == SCRAPE:
|
||||
return self._process_scrape(payload, trans)
|
||||
elif action == ERROR:
|
||||
return self._proecss_error(payload, trans)
|
||||
else:
|
||||
raise UdpTrackerClientException(
|
||||
'Unknown action response: {}'.format(action))
|
||||
|
||||
def _process_connect(self, payload, trans):
|
||||
self.conn_id = struct.unpack('!Q', payload)[0]
|
||||
return self.conn_id
|
||||
|
||||
def _process_announce(self, payload, trans):
|
||||
response = {}
|
||||
|
||||
info_struct = '!LLL'
|
||||
info_size = struct.calcsize(info_struct)
|
||||
info = payload[:info_size]
|
||||
interval, leechers, seeders = struct.unpack(info_struct, info)
|
||||
|
||||
peer_data = payload[info_size:]
|
||||
peer_struct = '!LH'
|
||||
peer_size = struct.calcsize(peer_struct)
|
||||
peer_count = len(peer_data) / peer_size
|
||||
peers = []
|
||||
|
||||
for peer_offset in xrange(peer_count):
|
||||
off = peer_size * peer_offset
|
||||
peer = peer_data[off:off + peer_size]
|
||||
addr, port = struct.unpack(peer_struct, peer)
|
||||
peers.append({
|
||||
'addr': socket.inet_ntoa(struct.pack('!L', addr)),
|
||||
'port': port,
|
||||
})
|
||||
|
||||
return {
|
||||
'interval': interval,
|
||||
'leechers': leechers,
|
||||
'seeders': seeders,
|
||||
'peers': peers,
|
||||
}
|
||||
|
||||
def _process_scrape(self, payload, trans):
|
||||
info_struct = '!LLL'
|
||||
info_size = struct.calcsize(info_struct)
|
||||
info_count = len(payload) / info_size
|
||||
hashes = trans['sent_hashes']
|
||||
response = {}
|
||||
for info_offset in xrange(info_count):
|
||||
off = info_size * info_offset
|
||||
info = payload[off:off + info_size]
|
||||
seeders, completed, leechers = struct.unpack(info_struct, info)
|
||||
response[hashes[info_offset]] = {
|
||||
'seeders': seeders,
|
||||
'completed': completed,
|
||||
'leechers': leechers,
|
||||
}
|
||||
return response
|
||||
|
||||
def _process_error(self, payload, trans):
|
||||
'''
|
||||
I haven't seen this action type be sent from a tracker, but I've left
|
||||
it here for the possibility.
|
||||
'''
|
||||
self.error(payload)
|
||||
return payload
|
||||
|
||||
def _generate_peer_id(self):
|
||||
'''http://www.bittorrent.org/beps/bep_0020.html'''
|
||||
peer_id = '-PU' + __version__.replace('.', '-') + '-'
|
||||
remaining = 20 - len(peer_id)
|
||||
numbers = [str(random.randint(0, 9)) for _ in xrange(remaining)]
|
||||
peer_id += ''.join(numbers)
|
||||
assert(len(peer_id) == 20)
|
||||
return peer_id
|
||||
|
||||
def _check_fields(self, args, fields):
|
||||
for f in fields:
|
||||
try:
|
||||
args.get(f)
|
||||
except KeyError:
|
||||
raise UdpTrackerClientException('field missing: {}'.format(f))
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue