Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions bbot/core/helpers/web/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,18 @@ def _build_blasthttp_kwargs(self, url, **kwargs):
headers = kwargs.pop("headers", None) or {}
body = kwargs.pop("body", None)
data = kwargs.pop("data", None)
files = kwargs.pop("files", None)
json_body = kwargs.pop("json", None)

body_sources = [
name
for name, val in (("body", body), ("data", data), ("json", json_body), ("files", files))
if val is not None
]
if len(body_sources) > 1:
raise ValueError(
f"request() got conflicting body kwargs {body_sources}; pass at most one of body, data, json, files"
)
timeout = kwargs.pop("timeout", self._http_timeout)
follow_redirects = kwargs.pop("follow_redirects", None)
max_redirects = kwargs.pop("max_redirects", None)
Expand Down Expand Up @@ -191,7 +202,9 @@ def _build_blasthttp_kwargs(self, url, **kwargs):
}

if body is not None:
blast_kwargs["body"] = str(body)
blast_kwargs["body"] = body if isinstance(body, (bytes, bytearray)) else str(body)
if files is not None:
blast_kwargs["files"] = files
if follow_redirects is not None:
blast_kwargs["follow_redirects"] = follow_redirects
if max_redirects is not None:
Expand Down Expand Up @@ -253,8 +266,6 @@ async def request(self, *args, **kwargs):
kwargs.pop("cache_for", None)
kwargs.pop("client", None)
kwargs.pop("stream", None)
if kwargs.pop("files", None) is not None:
log.warning("blasthttp does not support multipart file uploads (files= kwarg)")

# allow vs follow
allow_redirects = kwargs.pop("allow_redirects", None)
Expand Down
57 changes: 36 additions & 21 deletions bbot/test/mock_blasthttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async def handle_engine_request(self, web_helper_self, *args, **kwargs):
into a body, then dispatches via ``self._inner.request(...)``.
"""
raise_error = kwargs.pop("raise_error", False)
for k in ("cache_for", "client", "stream", "files"):
for k in ("cache_for", "client", "stream"):
kwargs.pop(k, None)

allow_redirects = kwargs.pop("allow_redirects", None)
Expand All @@ -86,7 +86,17 @@ async def handle_engine_request(self, web_helper_self, *args, **kwargs):
headers = kwargs.pop("headers", None) or {}
body = kwargs.pop("body", None)
data = kwargs.pop("data", None)
files = kwargs.pop("files", None)
json_body = kwargs.pop("json", None)
body_sources = [
name
for name, val in (("body", body), ("data", data), ("json", json_body), ("files", files))
if val is not None
]
if len(body_sources) > 1:
raise ValueError(
f"request() got conflicting body kwargs {body_sources}; pass at most one of body, data, json, files"
)
cookies = kwargs.pop("cookies", None)
auth = kwargs.pop("auth", None)
# Drop kwargs that don't apply to mock dispatch but are valid on WebHelper.
Expand All @@ -106,28 +116,33 @@ async def handle_engine_request(self, web_helper_self, *args, **kwargs):
if cookies:
headers["Cookie"] = "; ".join(f"{ck}={cv}" for ck, cv in cookies.items())

# Assemble body for handler-side matching/dispatch.
body_str = ""
if json_body is not None:
body_str = _json.dumps(json_body)
headers.setdefault("Content-Type", "application/json")
elif data is not None:
if isinstance(data, dict):
body_str = urlencode(data)
headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
else:
body_str = str(data)
elif body is not None:
body_str = str(body)
# Assemble body for handler-side matching/dispatch. files= wins (multipart),
# then json, then data, then raw body.
final_body = b""
if files is None:
if json_body is not None:
final_body = _json.dumps(json_body)
headers.setdefault("Content-Type", "application/json")
elif data is not None:
if isinstance(data, dict):
final_body = urlencode(data)
headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
else:
final_body = data if isinstance(data, (bytes, bytearray)) else str(data)
elif body is not None:
final_body = body if isinstance(body, (bytes, bytearray)) else str(body)

try:
return await self._inner.request(
url,
method=method,
headers=headers,
body=body_str,
follow_redirects=follow_redirects,
)
inner_kwargs = {
"method": method,
"headers": headers,
"follow_redirects": follow_redirects,
}
if files is not None:
inner_kwargs["files"] = files
else:
inner_kwargs["body"] = final_body
return await self._inner.request(url, **inner_kwargs)
except Exception as e:
import logging

Expand Down
60 changes: 60 additions & 0 deletions bbot/test/test_step_1/test_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,66 @@ def server_handler(request):
await scan._cleanup()


@pytest.mark.asyncio
async def test_web_request_files_multipart(bbot_scanner, bbot_httpserver):
"""httpx-style files= kwarg builds a multipart/form-data body."""
captured = {}

def server_handler(request):
from werkzeug.wrappers import Response

captured["body"] = request.get_data()
captured["content_type"] = request.headers.get("Content-Type", "")
return Response("ok")

bbot_httpserver.expect_request(uri="/upload", method="POST").respond_with_handler(server_handler)
url = bbot_httpserver.url_for("/upload")

scan = bbot_scanner()
await scan._prep()

response = await scan.helpers.request(
url,
method="POST",
files={
"field": (None, "value"),
"f": ("blob", b"\x00\x01\x02hello", "application/octet-stream"),
},
)
assert response.status_code == 200

ct = captured["content_type"]
assert ct.startswith("multipart/form-data; boundary=")
body = captured["body"]
assert b'name="field"' in body
assert b"value" in body
assert b'filename="blob"' in body
assert b"\x00\x01\x02hello" in body

await scan._cleanup()


@pytest.mark.asyncio
async def test_web_request_rejects_conflicting_body_kwargs(bbot_scanner):
scan = bbot_scanner()
await scan._prep()
url = "http://example.com/"

pairs = [
{"json": {"a": 1}, "files": {"f": ("x", b"x")}},
{"json": {"a": 1}, "data": {"a": "b"}},
{"body": "raw", "json": {"a": 1}},
{"body": "raw", "data": {"a": "b"}},
{"body": "raw", "files": {"f": ("x", b"x")}},
{"data": {"a": "b"}, "files": {"f": ("x", b"x")}},
]
for kwargs in pairs:
with pytest.raises(ValueError, match="conflicting body kwargs"):
await scan.helpers.request(url, method="POST", **kwargs)

await scan._cleanup()


@pytest.mark.asyncio
async def test_web_helpers(bbot_scanner, bbot_httpserver, blasthttp_mock):
# json conversion
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ dependencies = [
"ansible-core>=2.17,<3",
"tldextract>=5.3.0,<6",
"cloudcheck>=10.0.0,<11",
"blasthttp>=0.5.1",
"blasthttp>=0.6.0",
"blastdns>=1.9.0,<2",
]

Expand Down
Loading
Loading