Skip to content

Commit da68c05

Browse files
Merge pull request #2778 from blacklanternsecurity/download-api-key-cycle
Add api_download() helper
2 parents 02c0f59 + a774fda commit da68c05

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

bbot/modules/base.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from sys import exc_info
55
from contextlib import suppress
66

7-
from ..errors import ValidationError
87
from ..core.helpers.misc import get_size # noqa
8+
from ..errors import ValidationError, WebError
99
from ..core.helpers.async_helpers import TaskCounter, ShuffleQueue
1010

1111

@@ -1249,6 +1249,24 @@ async def api_request(self, *args, **kwargs):
12491249

12501250
return r
12511251

1252+
async def api_download(self, url, **kwargs):
1253+
"""
1254+
A wrapper around the `download()` web helper that incorporates API key cycling.
1255+
"""
1256+
error = None
1257+
raise_error = kwargs.pop("raise_error", False)
1258+
for _ in range(self.api_retries):
1259+
new_url, kwargs = self.prepare_api_request(url, kwargs)
1260+
if "raise_error" not in kwargs:
1261+
kwargs["raise_error"] = True
1262+
try:
1263+
return await self.helpers.download(new_url, **kwargs)
1264+
except WebError as e:
1265+
error = e
1266+
self.cycle_api_key()
1267+
if raise_error:
1268+
raise error
1269+
12521270
def _get_retry_after(self, r):
12531271
# try to get retry_after from headers first
12541272
headers = getattr(r, "headers", {})

bbot/test/test_step_1/test_web.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,3 +498,49 @@ def echo_cookies_handler(request):
498498
assert r1 is not None, "Request to self-signed SSL server went through even with ssl_verify=True"
499499
assert "bar" in r1.text
500500
await scan1._cleanup()
501+
502+
503+
@pytest.mark.asyncio
504+
async def test_api_download_api_key_cycle(bbot_scanner, bbot_httpserver):
505+
from werkzeug.wrappers import Response
506+
from bbot.modules.base import BaseModule
507+
508+
endpoint = "/api_download_cycle_one_test"
509+
url = bbot_httpserver.url_for(endpoint)
510+
511+
seen_auth = []
512+
n_request = 0
513+
514+
# First key should trigger 500, second key should succeed with 200
515+
def handler(request):
516+
nonlocal n_request
517+
n_request += 1
518+
auth = request.headers.get("Authorization", "")
519+
seen_auth.append(auth)
520+
if auth == "Bearer k1":
521+
if n_request == 1:
522+
return Response("ok_k1", status=200)
523+
return Response("fail_k1", status=500)
524+
elif auth == "Bearer k2":
525+
return Response("ok_k2", status=200)
526+
return Response("unexpected_key", status=400)
527+
528+
bbot_httpserver.expect_request(uri=endpoint).respond_with_handler(handler)
529+
530+
scan = bbot_scanner("127.0.0.1")
531+
module = BaseModule(scan)
532+
module.api_key = ["k1", "k2"]
533+
534+
filename = await module.api_download(url)
535+
assert filename is not None
536+
with open(filename) as f:
537+
assert f.read() == "ok_k1"
538+
539+
assert seen_auth == ["Bearer k1"]
540+
541+
filename = await module.api_download(url)
542+
543+
# verify the requests occurred in expected order with expected API keys
544+
assert seen_auth == ["Bearer k1", "Bearer k1", "Bearer k2"]
545+
546+
await scan._cleanup()

0 commit comments

Comments
 (0)