|
6 | 6 | """ |
7 | 7 |
|
8 | 8 | import binascii |
| 9 | +import calendar |
| 10 | +import email.utils |
9 | 11 | import inspect |
10 | 12 | import io |
11 | 13 | import logging |
|
119 | 121 | from lib.core.settings import PLAIN_TEXT_CONTENT_TYPE |
120 | 122 | from lib.core.settings import RANDOM_INTEGER_MARKER |
121 | 123 | from lib.core.settings import RANDOM_STRING_MARKER |
| 124 | +from lib.core.settings import RATE_LIMIT_DEFAULT_DELAY |
| 125 | +from lib.core.settings import RATE_LIMIT_DELAY_STEP |
| 126 | +from lib.core.settings import RATE_LIMIT_MAX_DELAY |
122 | 127 | from lib.core.settings import REPLACEMENT_MARKER |
123 | 128 | from lib.core.settings import SAFE_HEX_MARKER |
124 | 129 | from lib.core.settings import TEXT_CONTENT_TYPE_REGEX |
| 130 | +from lib.core.settings import TOO_MANY_REQUESTS_HTTP_CODE |
125 | 131 | from lib.core.settings import UNENCODED_ORIGINAL_VALUE |
126 | 132 | from lib.core.settings import UNICODE_ENCODING |
127 | 133 | from lib.core.settings import URI_HTTP_HEADER |
@@ -223,6 +229,49 @@ def _retryProxy(**kwargs): |
223 | 229 | kwargs['retrying'] = True |
224 | 230 | return Connect._getPageProxy(**kwargs) |
225 | 231 |
|
| 232 | + @staticmethod |
| 233 | + def _parseRetryAfter(responseHeaders): |
| 234 | + """ |
| 235 | + Parses a 'Retry-After' response header (RFC 7231 delta-seconds or an HTTP-date) into a number |
| 236 | + of seconds to wait, or None when it is absent or unparseable. |
| 237 | + """ |
| 238 | + |
| 239 | + value = (responseHeaders.get(HTTP_HEADER.RETRY_AFTER) if responseHeaders else None) or "" |
| 240 | + value = value.strip() |
| 241 | + |
| 242 | + if value.isdigit(): |
| 243 | + return float(value) |
| 244 | + |
| 245 | + parsed = email.utils.parsedate(value) |
| 246 | + return max(0.0, calendar.timegm(parsed) - time.time()) if parsed else None |
| 247 | + |
| 248 | + @staticmethod |
| 249 | + def _rateLimitRetry(responseHeaders, code, **kwargs): |
| 250 | + """ |
| 251 | + Handles a rate-limited response by honoring its 'Retry-After' (capped), adaptively raising the |
| 252 | + inter-request delay so subsequent requests self-throttle under the limit, then re-issuing the |
| 253 | + request. Returns the retried (page, headers, code) or None when the retry budget is exhausted, |
| 254 | + so the caller can surface the rate-limited response as-is. |
| 255 | + """ |
| 256 | + |
| 257 | + threadData = getCurrentThreadData() |
| 258 | + if threadData.retriesCount >= conf.retries or kb.threadException: |
| 259 | + return None |
| 260 | + |
| 261 | + retryAfter = Connect._parseRetryAfter(responseHeaders) |
| 262 | + backoff = min(retryAfter if retryAfter is not None else RATE_LIMIT_DEFAULT_DELAY, RATE_LIMIT_MAX_DELAY) |
| 263 | + |
| 264 | + # additive-increase throttle: nudge the inter-request delay up toward a sustainable pace. The |
| 265 | + # auto-throttle is capped, but a larger user-set '--delay' is never lowered. It is monotonic, |
| 266 | + # so a lost concurrent update across threads self-heals on the next hit. |
| 267 | + conf.delay = max(conf.delay or 0, min(RATE_LIMIT_MAX_DELAY, (conf.delay or 0) + RATE_LIMIT_DELAY_STEP)) |
| 268 | + |
| 269 | + singleTimeWarnMessage("target appears to be rate-limiting requests; sqlmap is backing off and throttling accordingly (consider raising '--delay' or lowering '--threads')") |
| 270 | + logger.debug("rate-limited (HTTP %d)%s; sleeping %.1f second(s), inter-request delay now %.1f second(s)" % (code, " honoring 'Retry-After'" if retryAfter is not None else "", backoff, conf.delay)) |
| 271 | + |
| 272 | + time.sleep(backoff) |
| 273 | + return Connect._retryProxy(**kwargs) |
| 274 | + |
226 | 275 | @staticmethod |
227 | 276 | def _connReadProxy(conn): |
228 | 277 | parts = [] |
@@ -844,7 +893,13 @@ class _(dict): |
844 | 893 | raise SystemExit |
845 | 894 |
|
846 | 895 | if ex.code not in (conf.ignoreCode or []): |
847 | | - if ex.code == _http_client.UNAUTHORIZED: |
| 896 | + if ex.code == TOO_MANY_REQUESTS_HTTP_CODE or (ex.code == _http_client.SERVICE_UNAVAILABLE and Connect._parseRetryAfter(responseHeaders) is not None): |
| 897 | + retried = Connect._rateLimitRetry(responseHeaders, ex.code, **kwargs) |
| 898 | + if retried is not None: |
| 899 | + return retried |
| 900 | + debugMsg = "target kept rate-limiting after %d retries (%d)" % (conf.retries, code) |
| 901 | + logger.debug(debugMsg) |
| 902 | + elif ex.code == _http_client.UNAUTHORIZED: |
848 | 903 | errMsg = "not authorized, try to provide right HTTP " |
849 | 904 | errMsg += "authentication type and valid credentials (%d). " % code |
850 | 905 | errMsg += "If this is intended, try to rerun by providing " |
|
0 commit comments