def _redirect(self, new_url, old_query): """Update announce URL from HTTP redirect""" if self._redirects >= self.MAX_REDIRECTS: raise IOError(('http error', 500, 'Internal Server Error: Redirect Recursion')) scheme, netloc, path, query, frag = urllib.parse.urlsplit(new_url) # If we got our own query string back, remove it oq_dict = urllib.parse.parse_qs(old_query) new_query = urllib.parse.urlencode( (key, val) for key, val in urllib.parse.parse_qsl(query) if val != oq_dict.get(key, '')) tracker_url = urllib.parse.urlunsplit( (scheme, netloc, path, new_query, frag)) with self.announcer_lock: # It's possible to have the same redirect happen if tracker_url in self.announcers and \ self.announcers[tracker_url] == self: return self._redirects += 1 self.announcers[tracker_url] = self self.stream = SharedStream(tracker_url) self.basequery = path + '?' + new_query + '&'[:bool(new_query)]
def _redirect(self, new_url, old_query): """Update announce URL from HTTP redirect""" if self._redirects >= self.MAX_REDIRECTS: raise IOError(('http error', 500, 'Internal Server Error: Redirect Recursion')) scheme, netloc, path, query, frag = urllib.parse.urlsplit(new_url) # If we got our own query string back, remove it oq_dict = urllib.parse.parse_qs(old_query) new_query = urllib.parse.urlencode( (key, val) for key, val in urllib.parse.parse_qsl(query) if val != oq_dict.get(key, '')) tracker_url = urllib.parse.urlunsplit((scheme, netloc, path, new_query, frag)) with self.announcer_lock: # It's possible to have the same redirect happen if tracker_url in self.announcers and \ self.announcers[tracker_url] == self: return self._redirects += 1 self.announcers[tracker_url] = self self.stream = SharedStream(tracker_url) self.basequery = path + '?' + new_query + '&'[:bool(new_query)]
def __new__(cls, tracker_url, port, *args, **kwargs): """Return the Announcer associated with the tracker URL, creating a new one, if needed. Client port is made available to Announcer objects, but is assumed not to vary within a program, so calling with the same URL and two different ports will return the same object.""" # Use Announcer() to allow scheme to decide on subclass if cls is Announcer: scheme = urllib.parse.urlsplit(tracker_url)[0] return cls.subclasses[scheme](tracker_url, port, *args, **kwargs) with cls.announcer_lock: # Assume port will not change within single Python instance # If this changes, change key to (tracker_url, port) if tracker_url not in cls.announcers: announcer = super(Announcer, cls).__new__(cls) # Keys allow trackers to recognize clients with IP changes announcer.key = random.randint(0, 0xffffffff).to_bytes(4, 'big') announcer.stream = SharedStream(tracker_url) cls.announcers[tracker_url] = announcer return cls.announcers[tracker_url]
class HTTPAnnouncer(Announcer): """Announce peer status over HTTP(S) This object maintains tracker-specific client state and handles HTTP redirects. New attempts to use the original tracker URL will instead use the redirected URL. This permits transparent HTTP->HTTPS upgrades without revealing client information more than once, as well as reducing the effective latency of announces. Implements BEP 3 Original request format for trackers BEP 7 IPv6 response entry ``peers6'' BEP 23 Compact peer lists Non-BEP standards MSE Announces supportcrypto, requirecrypto, cryptoport no_peer_id Request tracker to exclude peer IDs from responses Additional otherwise undocumented BitTornado extensions implemented: seed_id Dedicated seed IDs may be sent to trackers """ class_lock = threading.Lock() trackerid = None events = ['empty', 'started', 'completed', 'stopped'] _redirects = 0 MAX_REDIRECTS = 10 def __init__(self, tracker_url, port, ip=0, seed_id=None, supportcrypto=True, requirecrypto=False, cryptostealth=False, no_peer_id=True, compact=True): """Retrieve the announcer object associated with tracker_url On the first call, initializes object according to parameters. On later calls, does nothing. Parameters may be set on existing objects with set_options(). """ with self.class_lock: if self.initialized: return super(HTTPAnnouncer, self).__init__() self.redirect_lock = threading.Lock() _, _, path, query, _ = urllib.parse.urlsplit(tracker_url) # a[:bool(b)] == (a if b else '') self.basequery = path + '?' + query + '&'[:bool(query)] self.set_options(port, ip, seed_id, supportcrypto, requirecrypto, cryptostealth, no_peer_id, compact) self.initialized = True def set_options(self, port, ip=0, seed_id=None, supportcrypto=True, requirecrypto=False, cryptostealth=False, no_peer_id=True, compact=True): """Prepare query parameters according to state variables. Crypto parameters will be set to the *least secure* logically consistent configuration. Called during __init__(), so only use this if changing parameters DURING program execution.""" # Enforce logical consistency requirecrypto &= supportcrypto cryptostealth &= requirecrypto # Port is required; IP if specified; seed_id if specified self.client = [('ip', str(IPv4(ip)))][:bool(ip)] + \ [('port', port if not cryptostealth else 0)] + \ [('seed_id', seed_id)][:bool(seed_id)] # Compact precludes peer_id, so don't bother with no_peer_id self.peer_options = [('compact', int(compact))] + \ [('no_peer_id', 1)][:(no_peer_id and not compact)] self.crypto_options = [('supportcrypto', int(supportcrypto)), ('requirecrypto', int(requirecrypto))] + \ [('cryptoport', port)][:cryptostealth] def _redirect(self, new_url, old_query): """Update announce URL from HTTP redirect""" if self._redirects >= self.MAX_REDIRECTS: raise IOError(('http error', 500, 'Internal Server Error: Redirect Recursion')) scheme, netloc, path, query, frag = urllib.parse.urlsplit(new_url) # If we got our own query string back, remove it oq_dict = urllib.parse.parse_qs(old_query) new_query = urllib.parse.urlencode( (key, val) for key, val in urllib.parse.parse_qsl(query) if val != oq_dict.get(key, '')) tracker_url = urllib.parse.urlunsplit((scheme, netloc, path, new_query, frag)) with self.announcer_lock: # It's possible to have the same redirect happen if tracker_url in self.announcers and \ self.announcers[tracker_url] == self: return self._redirects += 1 self.announcers[tracker_url] = self self.stream = SharedStream(tracker_url) self.basequery = path + '?' + new_query + '&'[:bool(new_query)] def announce(self, infohash, peer_id, event=0, downloaded=0, uploaded=0, left=0, num_want=-1, snoop=False): """Send an announce request Arguments: infohash bytes[20] SHA1 hash of bencoded Info dictionary peer_id bytes unique peer ID event int Code indicating purpose of request 0 (empty/update statistics) 1 (download started) 2 (download completed) 3 (download stoped) downloaded int number of bytes downloaded uploaded int number of bytes uploaded left int number of bytes left to download num_want int number of peers to request (optional) snoop bool query tracker without affecting stats Returns: {'interval': int, number of seconds to wait to reannounce 'complete': int, number of seeders 'incomplete': int, number of leechers 'peers': [{'ip': str, Peer IPv4 address 'port': int}] Peer port number 'peers6': [{'ip': str, Peer IPv6 address 'port':int}] Peer port number 'crypto_flags': bytes, crypto capabilities of each peer b'\\x00' Prefers plaintext b'\\x01' Requires encryption 'min interval': int, strict reannounce interval 'tracker id': bytes, unique tracker ID 'warning message': str} message from tracker OR {'failure reason': str} error message from tracker If called with infohash and peer_id, a 'stopped' event is sent, and most trackers will respond with an empty list of peers. """ if snoop: options = [('info_hash', infohash), ('peer_id', peer_id), ('event', 'stopped'), ('port', 0), ('compact', True), ('uploaded', 0), ('downloaded', 0), ('left', 1), ('tracker', True), ('numwant', num_want)] else: # a[:bool(b)] == (a if b else '') basic = [('info_hash', infohash), ('peer_id', peer_id)] + \ [('event', self.events[event])][:bool(event)] stats = [('uploaded', uploaded), ('downloaded', downloaded), ('left', left)] trackercomm = [('key', base64.urlsafe_b64encode(self.key))] + \ [('trackerid', self.trackerid)][:bool(self.trackerid)] + \ [('numwant', num_want)][:(num_want >= 0)] options = basic + self.client + self.peer_options + stats + \ self.crypto_options + trackercomm # In Python 3.5, we can switch to the urlencode line. In the meantime, # keep using RequestURL query = str(RequestURL(options)) # query = urllib.parse.urlencode(options, quote_via=urllib.parse.quote) response, raw = self.send_query(query) if response.status == 200: ret = Response(bdecode(raw)) if 'trackerid' in ret: self.trackerid = ret['trackerid'] return ret try: return Response(bdecode(raw)) except ValueError: raise IOError(('http error', response.status, response.reason)) def send_query(self, query): """Send query, redirecting as needed""" with self.redirect_lock: response, raw = self.stream.request(self.basequery + query) while response.status in (301, 302): self._redirect(raw, query) response, raw = self.stream.request(self.basequery + query) return response, raw def forward_query(self, query, password=None): """Send a pre-formed query, optionally appending password Do not attempt to check errors or parse a response.""" if password is not None: query += '&password=' + password try: self.send_query(query) except IOError: pass
class HTTPAnnouncer(Announcer): """Announce peer status over HTTP(S) This object maintains tracker-specific client state and handles HTTP redirects. New attempts to use the original tracker URL will instead use the redirected URL. This permits transparent HTTP->HTTPS upgrades without revealing client information more than once, as well as reducing the effective latency of announces. Implements BEP 3 Original request format for trackers BEP 7 IPv6 response entry ``peers6'' BEP 23 Compact peer lists Non-BEP standards MSE Announces supportcrypto, requirecrypto, cryptoport no_peer_id Request tracker to exclude peer IDs from responses Additional otherwise undocumented BitTornado extensions implemented: seed_id Dedicated seed IDs may be sent to trackers """ class_lock = threading.Lock() trackerid = None events = ['empty', 'started', 'completed', 'stopped'] _redirects = 0 MAX_REDIRECTS = 10 def __init__(self, tracker_url, port, ip=0, seed_id=None, supportcrypto=True, requirecrypto=False, cryptostealth=False, no_peer_id=True, compact=True): """Retrieve the announcer object associated with tracker_url On the first call, initializes object according to parameters. On later calls, does nothing. Parameters may be set on existing objects with set_options(). """ with self.class_lock: if self.initialized: return super(HTTPAnnouncer, self).__init__() self.redirect_lock = threading.Lock() _, _, path, query, _ = urllib.parse.urlsplit(tracker_url) # a[:bool(b)] == (a if b else '') self.basequery = path + '?' + query + '&'[:bool(query)] self.set_options(port, ip, seed_id, supportcrypto, requirecrypto, cryptostealth, no_peer_id, compact) self.initialized = True def set_options(self, port, ip=0, seed_id=None, supportcrypto=True, requirecrypto=False, cryptostealth=False, no_peer_id=True, compact=True): """Prepare query parameters according to state variables. Crypto parameters will be set to the *least secure* logically consistent configuration. Called during __init__(), so only use this if changing parameters DURING program execution.""" # Enforce logical consistency requirecrypto &= supportcrypto cryptostealth &= requirecrypto # Port is required; IP if specified; seed_id if specified self.client = [('ip', str(IPv4(ip)))][:bool(ip)] + \ [('port', port if not cryptostealth else 0)] + \ [('seed_id', seed_id)][:bool(seed_id)] # Compact precludes peer_id, so don't bother with no_peer_id self.peer_options = [('compact', int(compact))] + \ [('no_peer_id', 1)][:(no_peer_id and not compact)] self.crypto_options = [('supportcrypto', int(supportcrypto)), ('requirecrypto', int(requirecrypto))] + \ [('cryptoport', port)][:cryptostealth] def _redirect(self, new_url, old_query): """Update announce URL from HTTP redirect""" if self._redirects >= self.MAX_REDIRECTS: raise IOError(('http error', 500, 'Internal Server Error: Redirect Recursion')) scheme, netloc, path, query, frag = urllib.parse.urlsplit(new_url) # If we got our own query string back, remove it oq_dict = urllib.parse.parse_qs(old_query) new_query = urllib.parse.urlencode( (key, val) for key, val in urllib.parse.parse_qsl(query) if val != oq_dict.get(key, '')) tracker_url = urllib.parse.urlunsplit( (scheme, netloc, path, new_query, frag)) with self.announcer_lock: # It's possible to have the same redirect happen if tracker_url in self.announcers and \ self.announcers[tracker_url] == self: return self._redirects += 1 self.announcers[tracker_url] = self self.stream = SharedStream(tracker_url) self.basequery = path + '?' + new_query + '&'[:bool(new_query)] def announce(self, infohash, peer_id, event=0, downloaded=0, uploaded=0, left=0, num_want=-1, snoop=False): """Send an announce request Arguments: infohash bytes[20] SHA1 hash of bencoded Info dictionary peer_id bytes unique peer ID event int Code indicating purpose of request 0 (empty/update statistics) 1 (download started) 2 (download completed) 3 (download stoped) downloaded int number of bytes downloaded uploaded int number of bytes uploaded left int number of bytes left to download num_want int number of peers to request (optional) snoop bool query tracker without affecting stats Returns: {'interval': int, number of seconds to wait to reannounce 'complete': int, number of seeders 'incomplete': int, number of leechers 'peers': [{'ip': str, Peer IPv4 address 'port': int}] Peer port number 'peers6': [{'ip': str, Peer IPv6 address 'port':int}] Peer port number 'crypto_flags': bytes, crypto capabilities of each peer b'\\x00' Prefers plaintext b'\\x01' Requires encryption 'min interval': int, strict reannounce interval 'tracker id': bytes, unique tracker ID 'warning message': str} message from tracker OR {'failure reason': str} error message from tracker If called with infohash and peer_id, a 'stopped' event is sent, and most trackers will respond with an empty list of peers. """ if snoop: options = [('info_hash', infohash), ('peer_id', peer_id), ('event', 'stopped'), ('port', 0), ('compact', True), ('uploaded', 0), ('downloaded', 0), ('left', 1), ('tracker', True), ('numwant', num_want)] else: # a[:bool(b)] == (a if b else '') basic = [('info_hash', infohash), ('peer_id', peer_id)] + \ [('event', self.events[event])][:bool(event)] stats = [('uploaded', uploaded), ('downloaded', downloaded), ('left', left)] trackercomm = [('key', base64.urlsafe_b64encode(self.key))] + \ [('trackerid', self.trackerid)][:bool(self.trackerid)] + \ [('numwant', num_want)][:(num_want >= 0)] options = basic + self.client + self.peer_options + stats + \ self.crypto_options + trackercomm # In Python 3.5, we can switch to the urlencode line. In the meantime, # keep using RequestURL query = str(RequestURL(options)) #query = urllib.parse.urlencode(options, quote_via=urllib.parse.quote) response, raw = self.send_query(query) if response.status == 200: ret = Response(bdecode(raw)) if 'trackerid' in ret: self.trackerid = ret['trackerid'] return ret try: return Response(bdecode(raw)) except ValueError: raise IOError(('http error', response.status, response.reason)) def send_query(self, query): """Send query, redirecting as needed""" with self.redirect_lock: response, raw = self.stream.request(self.basequery + query) while response.status in (301, 302): self._redirect(raw, query) response, raw = self.stream.request(self.basequery + query) return response, raw def forward_query(self, query, password=None): """Send a pre-formed query, optionally appending password Do not attempt to check errors or parse a response.""" if password is not None: query += '&password=' + password try: self.send_query(query) except IOError: pass