def request_send(self, event_dispatcher, result_callback, error_callback): """Initiate announce sequence""" if not (self.state is None): raise TrackerRequestError("Request {0!a} is still pending.".format(self.sock)) self.result_callback = result_callback self.error_callback = error_callback self.ed = event_dispatcher try: tracker_addrinfo = random.choice(socket.getaddrinfo(*self.address_get())) except socket.error as exc: self.log(35, "Connection to tracker {0} failed; found no valid records. Faking timeout.".format(self.address_get())) self._timeout_fake() return tracker_AF = tracker_addrinfo[0] rsock = socket.socket(tracker_AF, socket.SOCK_DGRAM) self.sock = AsyncPacketSock(event_dispatcher, rsock) self.sock.process_input = self.frame_process self.sock.process_close = (lambda *args: None) self.transaction_id = self.tid_generate() self.state = 0 self.tracker_address = tracker_addrinfo[4][:2] try: self.frame_send(self.frame_build_init()) except socket.error as exc: # Ugly hack, but modeling explicitly reported send failure different # from a timeout isn't worth the hassle. self.log(30, 'Unable to contact tracker {0}: UDP sendto() failed with \'{1}\'. Faking timeout.'.format(self.address_get(),exc)) self._timeout_fake() return self.timeout_timer = event_dispatcher.set_timer(self.TIMEOUT, self.timeout_handle)
class UDPTrackerRequest(TrackerRequest): proto = b'udp' pm_in_init = '>llq' pm_in_announce = '>lllll' CONNECTION_ID_DEFAULT = 0x41727101980 ACTION_CONNECT = 0 ACTION_ANNOUNCE = 1 ACTION_SCRAPE = 2 ACTION_ERROR = 3 EXT_AUTH = 1 TIMEOUT = 50 EVENT_MAP = { None:0, b'completed':1, b'started':2, b'stopped':3 } def __init__(self, *args, **kwargs): TrackerRequest.__init__(self, *args, **kwargs) self.sock = None self.timeout_timer = None self.close() @staticmethod def tid_generate(): """Return a random 32bit signed integer""" return random.randint(-1*2**31,2**31-1) def frame_build_init(self, tid=None): """Build and return frame for initiating session to tracker""" if (tid is None): tid = self.transaction_id return struct.pack('>qll', self.CONNECTION_ID_DEFAULT, self.ACTION_CONNECT, tid) def numwant_get(self): if (self.numwant is None): return -1 return self.numwant def frame_build_announce(self, tid=None, ip_address=0, extensions=0): """Build and return announce frame""" if (tid is None): tid = self.transaction_id key = self.key if (key is None): key = '' key = key[-4:] return struct.pack('>qll20s20sqqqlL4slHH', self.connection_id, self.ACTION_ANNOUNCE, tid, self.info_hash, self.peer_id, self.downloaded, self.left, self.uploaded, self.EVENT_MAP[self.event], ip_address, key, self.numwant_get(), self.port, extensions) @staticmethod def frame_parsebody_init(data): """Parse init response fragment (packet minus initial 8 bytes) and return contents""" if (len(data) != 8): raise ValueError('Data {0!a} invalid; expected exactly 8 bytes.'.format(data,)) return struct.unpack('>q', data)[0] def frame_parsebody_announce(self, data): """Parse announce response body (packet minus initial 8 bytes) and return contents""" if (len(data) < 12): raise ValueError('Data {0!a} invalid; expected at least 12 bytes.'.format(data,)) if (((len(data) - 12) % 6) != 0): raise ValueError('Data {0!a} invalid; length {1} does not satisfy (((l - 12) % 6) == 0) condition.'.format(data, len(data))) (interval, seeders, leechers) = struct.unpack('>lll', data[:12]) peers = BTPeer.seq_build(data[12:]) peers_filtered = [] for peer in peers: if ((int(peer.ip) == 0) or (peer.port == 0)): self.log(30, '{0} for invalid BTPeer data {1!a}. Discarding.'.format(self, peer)) continue peers_filtered.append(peer) peers = peers_filtered # Build response data manually, since it doesn't exist at protocol level response_data = { b'peers': peers, b'interval':interval, b'complete':seeders, b'incomplete':leechers } return response_data def timeout_handle(self): """Handle session timeout.""" self.log(30, '{0!a} (tracker {1!a}) timeouted in state {2!a}.'.format(self, self.announce_url, self.state)) self.timeout_timer = None self.error_callback(TrackerResponseError('Session to tracker {0!a} timeouted in state {1!a}.'.format(self.announce_url, self.state))) self.close() def frame_process_init(self, data): """Process init response""" if (self.state != 0): raise TrackerResponseError('{0!a}.frame_process_init({1!a}) got called while state == {2!a}.'.format(self, data, self.state)) self.connection_id = self.frame_parsebody_init(data) self.transaction_id = self.tid_generate() self.state = 1 try: self.frame_send(self.frame_build_announce()) except socket.error as exc: self.log(30, 'Unable to contact tracker {0}: UDP sendto() failed with \'{1}\'. Faking timeout.'.format(self.address_get(),exc)) self._timeout_fake() return def frame_process_announce(self, data): """Process announce response""" if (self.state != 1): raise TrackerResponseError('{0!a}.frame_process_announce({1!a}) got called while state == {2!a}.'.format(self, data, self.state)) response_data = self.frame_parsebody_announce(data) self.result_callback(self, response_data) self.close() def frame_process_error(self, data): """Process error response""" self.log(30, '{0!a} got error {1!a} from tracker.'.format(self, data)) self.error_callback(TrackerResponseError('Tracker {0!a} returned failure reason {1!a}.'.format(self.announce_url, data))) self.close() FRAME_HANDLERS = { ACTION_CONNECT:frame_process_init, ACTION_ANNOUNCE:frame_process_announce, ACTION_ERROR:frame_process_error } def frame_process(self, data, source): """Process UDP frame received from tracker""" if (source != self.tracker_address): # Not what we're expecting. self.log(30, '{0!a} got unexpected udp frame {1!a} from {2!a}. Discarding.'.format(self, data, source)) return if (len(data) < 8): raise ValueError('data {0!a} invalid; expected at least 8 bytes.'.format(data,)) (action, tid) = struct.unpack('>ll', data[:8]) if (tid != self.transaction_id): # Not what we're expecting. self.log(30, '{0!a} got unexpected tid {1!a}; expected {2!a}. Discarding frame.'.format(self, tid, self.transaction_id)) return if (action in self.FRAME_HANDLERS): try: self.FRAME_HANDLERS[action](self, data[8:]) except (TrackerResponseError, ValueError) as exc: self.log(30, '{0!a} failed to parse udp frame. Discarding frame. Traceback:'.format(self), exc_info=True) else: self.log(30, '{0!a} got frame with unknown action {1!a}. Discarding frame.'.format(self, action)) def frame_send(self, data): """Send frame to tracker""" self.sock.send_bytes((data,), self.tracker_address) def _timeout_fake(self): if not (self.timeout_timer is None): self.timeout_timer.cancel() self.timeout_timer = self.ed.set_timer(0, self.timeout_handle) def request_send(self, event_dispatcher, result_callback, error_callback): """Initiate announce sequence""" if not (self.state is None): raise TrackerRequestError("Request {0!a} is still pending.".format(self.sock)) self.result_callback = result_callback self.error_callback = error_callback self.ed = event_dispatcher try: tracker_addrinfo = random.choice(socket.getaddrinfo(*self.address_get())) except socket.error as exc: self.log(35, "Connection to tracker {0} failed; found no valid records. Faking timeout.".format(self.address_get())) self._timeout_fake() return tracker_AF = tracker_addrinfo[0] rsock = socket.socket(tracker_AF, socket.SOCK_DGRAM) self.sock = AsyncPacketSock(event_dispatcher, rsock) self.sock.process_input = self.frame_process self.sock.process_close = (lambda *args: None) self.transaction_id = self.tid_generate() self.state = 0 self.tracker_address = tracker_addrinfo[4][:2] try: self.frame_send(self.frame_build_init()) except socket.error as exc: # Ugly hack, but modeling explicitly reported send failure different # from a timeout isn't worth the hassle. self.log(30, 'Unable to contact tracker {0}: UDP sendto() failed with \'{1}\'. Faking timeout.'.format(self.address_get(),exc)) self._timeout_fake() return self.timeout_timer = event_dispatcher.set_timer(self.TIMEOUT, self.timeout_handle) def close(self): """Close socket, if opened, and reset state variables""" self.ed = None if (self.sock): self.sock.close() self.sock = None if not (self.timeout_timer is None): self.timeout_timer.cancel() self.timeout_timer = None self.connection_id = None self.state = None self.transaction_id = None self.tracker_address = None self.result_callback = None self.error_callback = None