From 5907664d4c4626a3033b082614382f5d6929b937 Mon Sep 17 00:00:00 2001 From: dolfies Date: Sat, 1 Apr 2023 15:26:26 -0400 Subject: [PATCH] Fix captcha handling with form param payloads (fixes #484) --- discord/http.py | 61 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/discord/http.py b/discord/http.py index db8ef0e59..dbb2b1b01 100644 --- a/discord/http.py +++ b/discord/http.py @@ -113,14 +113,6 @@ if TYPE_CHECKING: Response = Coroutine[Any, Any, T] MessageableChannel = Union[TextChannel, Thread, DMChannel, GroupChannel, PartialMessageable, VoiceChannel, ForumChannel] -CAPTCHA_VALUES = { - 'incorrect-captcha', - 'response-already-used', - 'captcha-required', - 'invalid-input-response', - 'invalid-response', - 'You need to update your app', # Discord moment -} INTERNAL_API_VERSION = 9 _log = logging.getLogger(__name__) @@ -634,7 +626,7 @@ class HTTPClient: route: Route, *, files: Optional[Sequence[File]] = None, - form: Optional[Iterable[Dict[str, Any]]] = None, + form: Optional[List[Dict[str, Any]]] = None, **kwargs: Any, ) -> Any: method = route.method @@ -682,7 +674,8 @@ class HTTPClient: if reason: headers['X-Audit-Log-Reason'] = _uriquote(reason) - if (payload := kwargs.pop('json', None)) is not None: + payload = kwargs.pop('json', None) + if payload is not None: headers['Content-Type'] = 'application/json' kwargs['data'] = utils._to_json(payload) @@ -707,6 +700,7 @@ class HTTPClient: response: Optional[aiohttp.ClientResponse] = None data: Optional[Union[Dict[str, Any], str]] = None + failed = 0 # Number of 500'd requests async with ratelimit: for tries in range(5): if files: @@ -720,6 +714,9 @@ class HTTPClient: form_data.add_field(**params) kwargs['data'] = form_data + if failed: + kwargs['headers']['X-Failed-Requests'] = str(failed) + try: async with self.__session.request(method, url, **kwargs) as response: _log.debug('%s %s with %s has returned %s.', method, url, kwargs.get('data'), response.status) @@ -826,7 +823,8 @@ class HTTPClient: continue # Unconditional retry - if response.status in {500, 502, 504, 524}: + if response.status in {500, 502, 504, 507, 522, 523, 524}: + failed += 1 await asyncio.sleep(1 + tries * 2) continue @@ -846,26 +844,53 @@ class HTTPClient: except OSError as e: # Connection reset by peer if tries < 4 and e.errno in (54, 10054): + failed += 1 await asyncio.sleep(1 + tries * 2) continue raise # Captcha handling except CaptchaRequired as e: - values = [i for i in e.json['captcha_key'] if any(value in i for value in CAPTCHA_VALUES)] + # The way captcha handling works is completely transparent + # The user is expected to provide a handler that will be called to return a solution + # Then, we just insert the solution + rqtoken (if applicable) into the payload and retry the request if captcha_handler is None or tries == 4: raise - elif not values: - raise else: - previous = payload or {} + # We use the payload_json form field if we're not sending a JSON payload + payload_json = None + previous = payload + if form: + payload_json = utils.find(lambda f: f['name'] == 'payload_json', form) + if payload_json: + previous = utils._from_json(payload_json['value']) + if previous is None: + previous = {} + previous['captcha_key'] = await captcha_handler.fetch_token(e.json, self.proxy, self.proxy_auth) - if (rqtoken := e.json.get('captcha_rqtoken')) is not None: + rqtoken = e.json.get('captcha_rqtoken') + if rqtoken: previous['captcha_rqtoken'] = rqtoken if 'nonce' in previous: + # I don't want to step on users' toes + # But the nonce is regenerated on requests retried after a captcha + # So I'm going to do the same here, as there's no good way to differentiate + # a library-generated nonce and a manually user-provided nonce previous['nonce'] = utils._generate_nonce() - kwargs['headers']['Content-Type'] = 'application/json' - kwargs['data'] = utils._to_json(previous) + + # Reinsert the updated payload to the form, or update the JSON payload + data = utils._to_json(previous) + if payload: + kwargs['data'] = data + elif form: + if payload_json: + payload_json['value'] = data + else: + form.append({'name': 'payload_json', 'value': data}) + else: + # We were not sending a payload in the first place + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['data'] = data if response is not None: # We've run out of retries, raise