Skip to content

Fix duplicate header issue #149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 5, 2024
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
81 changes: 51 additions & 30 deletions adafruit_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,16 @@ def _send(socket: SocketType, data: bytes):
raise OSError(errno.EIO)
total_sent += sent

def _send_as_bytes(self, socket: SocketType, data: str):
return self._send(socket, bytes(data, "utf-8"))

def _send_header(self, socket, header, value):
self._send_as_bytes(socket, header)
self._send(socket, b": ")
self._send_as_bytes(socket, value)
self._send(socket, b"\r\n")

# pylint: disable=too-many-arguments
def _send_request(
self,
socket: SocketType,
Expand All @@ -561,40 +571,51 @@ def _send_request(
data: Any,
json: Any,
):
# pylint: disable=too-many-arguments
self._send(socket, bytes(method, "utf-8"))
self._send(socket, b" /")
self._send(socket, bytes(path, "utf-8"))
self._send(socket, b" HTTP/1.1\r\n")
if "Host" not in headers:
self._send(socket, b"Host: ")
self._send(socket, bytes(host, "utf-8"))
self._send(socket, b"\r\n")
if "User-Agent" not in headers:
self._send(socket, b"User-Agent: Adafruit CircuitPython\r\n")
# Iterate over keys to avoid tuple alloc
for k in headers:
self._send(socket, k.encode())
self._send(socket, b": ")
self._send(socket, headers[k].encode())
self._send(socket, b"\r\n")
# Convert data
content_type_header = None

# If json is sent, set content type header and convert to string
if json is not None:
assert data is None
content_type_header = "application/json"
data = json_module.dumps(json)
self._send(socket, b"Content-Type: application/json\r\n")
if data:
if isinstance(data, dict):
self._send(
socket, b"Content-Type: application/x-www-form-urlencoded\r\n"
)
_post_data = ""
for k in data:
_post_data = "{}&{}={}".format(_post_data, k, data[k])
data = _post_data[1:]
if isinstance(data, str):
data = bytes(data, "utf-8")
self._send(socket, b"Content-Length: %d\r\n" % len(data))

# If data is sent and it's a dict, set content type header and convert to string
if data and isinstance(data, dict):
content_type_header = "application/x-www-form-urlencoded"
_post_data = ""
for k in data:
_post_data = "{}&{}={}".format(_post_data, k, data[k])
# remove first "&" from concatenation
data = _post_data[1:]

# Convert str data to bytes
if data and isinstance(data, str):
data = bytes(data, "utf-8")

self._send_as_bytes(socket, method)
self._send(socket, b" /")
self._send_as_bytes(socket, path)
self._send(socket, b" HTTP/1.1\r\n")

# create lower-case supplied header list
supplied_headers = {header.lower() for header in headers}

# Send headers
if not "host" in supplied_headers:
self._send_header(socket, "Host", host)
if not "user-agent" in supplied_headers:
self._send_header(socket, "User-Agent", "Adafruit CircuitPython")
if content_type_header and not "content-type" in supplied_headers:
self._send_header(socket, "Content-Type", content_type_header)
if data and not "content-length" in supplied_headers:
self._send_header(socket, "Content-Length", str(len(data)))
# Iterate over keys to avoid tuple alloc
for header in headers:
self._send_header(socket, header, headers[header])
self._send(socket, b"\r\n")

# Send data
if data:
self._send(socket, bytes(data))

Expand Down
9 changes: 6 additions & 3 deletions tests/chunk_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def do_test_get_text(
)
sock.send.assert_has_calls(
[
mock.call(b"Host: "),
mock.call(b"Host"),
mock.call(b": "),
mock.call(b"wifitest.adafruit.com"),
]
)
Expand Down Expand Up @@ -105,7 +106,8 @@ def do_test_close_flush(
)
sock.send.assert_has_calls(
[
mock.call(b"Host: "),
mock.call(b"Host"),
mock.call(b": "),
mock.call(b"wifitest.adafruit.com"),
]
)
Expand Down Expand Up @@ -146,7 +148,8 @@ def do_test_get_text_extra_space(
)
sock.send.assert_has_calls(
[
mock.call(b"Host: "),
mock.call(b"Host"),
mock.call(b": "),
mock.call(b"wifitest.adafruit.com"),
]
)
Expand Down
6 changes: 4 additions & 2 deletions tests/concurrent_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ def test_second_connect_fails_memoryerror(): # pylint: disable=invalid-name

sock.send.assert_has_calls(
[
mock.call(b"Host: "),
mock.call(b"Host"),
mock.call(b": "),
mock.call(b"wifitest.adafruit.com"),
mock.call(b"\r\n"),
]
Expand Down Expand Up @@ -82,7 +83,8 @@ def test_second_connect_fails_oserror(): # pylint: disable=invalid-name

sock.send.assert_has_calls(
[
mock.call(b"Host: "),
mock.call(b"Host"),
mock.call(b": "),
mock.call(b"wifitest.adafruit.com"),
mock.call(b"\r\n"),
]
Expand Down
124 changes: 120 additions & 4 deletions tests/header_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,75 @@
RESPONSE_HEADERS = b"HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n"


def test_json():
def test_host():
pool = mocket.MocketPool()
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
sock = mocket.Mocket(RESPONSE_HEADERS)
pool.socket.return_value = sock
sent = []

def _send(data):
sent.append(data) # pylint: disable=no-member
return len(data)

sock.send.side_effect = _send

requests_session = adafruit_requests.Session(pool)
headers = {}
requests_session.get("http://" + HOST + "/get", headers=headers)

sock.connect.assert_called_once_with((IP, 80))
sent = b"".join(sent)
assert b"Host: httpbin.org\r\n" in sent


def test_host_replace():
pool = mocket.MocketPool()
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
sock = mocket.Mocket(RESPONSE_HEADERS)
pool.socket.return_value = sock
sent = []

def _send(data):
sent.append(data) # pylint: disable=no-member
return len(data)

sock.send.side_effect = _send

requests_session = adafruit_requests.Session(pool)
headers = {"host": IP}
requests_session.get("http://" + HOST + "/get", headers=headers)

sock.connect.assert_called_once_with((IP, 80))
sent = b"".join(sent)
assert b"host: 1.2.3.4\r\n" in sent
assert b"Host: httpbin.org\r\n" not in sent
assert sent.lower().count(b"host:") == 1


def test_user_agent():
pool = mocket.MocketPool()
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
sock = mocket.Mocket(RESPONSE_HEADERS)
pool.socket.return_value = sock
sent = []

def _send(data):
sent.append(data) # pylint: disable=no-member
return len(data)

sock.send.side_effect = _send

requests_session = adafruit_requests.Session(pool)
headers = {}
requests_session.get("http://" + HOST + "/get", headers=headers)

sock.connect.assert_called_once_with((IP, 80))
sent = b"".join(sent)
assert b"User-Agent: Adafruit CircuitPython\r\n" in sent


def test_user_agent_replace():
pool = mocket.MocketPool()
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
sock = mocket.Mocket(RESPONSE_HEADERS)
Expand All @@ -30,7 +98,55 @@ def _send(data):
requests_session.get("http://" + HOST + "/get", headers=headers)

sock.connect.assert_called_once_with((IP, 80))
sent = b"".join(sent).lower()
sent = b"".join(sent)
assert b"user-agent: blinka/1.0.0\r\n" in sent
# The current implementation sends two user agents. Fix it, and uncomment below.
# assert sent.count(b"user-agent:") == 1
assert b"User-Agent: Adafruit CircuitPython\r\n" not in sent
assert sent.lower().count(b"user-agent:") == 1


def test_content_type():
pool = mocket.MocketPool()
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
sock = mocket.Mocket(RESPONSE_HEADERS)
pool.socket.return_value = sock
sent = []

def _send(data):
sent.append(data) # pylint: disable=no-member
return len(data)

sock.send.side_effect = _send

requests_session = adafruit_requests.Session(pool)
headers = {}
data = {"test": True}
requests_session.post("http://" + HOST + "/get", data=data, headers=headers)

sock.connect.assert_called_once_with((IP, 80))
sent = b"".join(sent)
assert b"Content-Type: application/x-www-form-urlencoded\r\n" in sent


def test_content_type_replace():
pool = mocket.MocketPool()
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
sock = mocket.Mocket(RESPONSE_HEADERS)
pool.socket.return_value = sock
sent = []

def _send(data):
sent.append(data) # pylint: disable=no-member
return len(data)

sock.send.side_effect = _send

requests_session = adafruit_requests.Session(pool)
headers = {"content-type": "application/test"}
data = {"test": True}
requests_session.post("http://" + HOST + "/get", data=data, headers=headers)

sock.connect.assert_called_once_with((IP, 80))
sent = b"".join(sent)
assert b"content-type: application/test\r\n" in sent
assert b"Content-Type: application/x-www-form-urlencoded\r\n" not in sent
assert sent.lower().count(b"content-type:") == 1
27 changes: 22 additions & 5 deletions tests/post_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def test_method():
)
sock.send.assert_has_calls(
[
mock.call(b"Host: "),
mock.call(b"Host"),
mock.call(b": "),
mock.call(b"httpbin.org"),
]
)
Expand All @@ -64,10 +65,18 @@ def test_form():
pool.socket.return_value = sock

requests_session = adafruit_requests.Session(pool)
data = {"Date": "July 25, 2019"}
data = {"Date": "July 25, 2019", "Time": "12:00"}
requests_session.post("http://" + HOST + "/post", data=data)
sock.connect.assert_called_once_with((IP, 80))
sock.send.assert_called_with(b"Date=July 25, 2019")
sock.send.assert_has_calls(
[
mock.call(b"Content-Type"),
mock.call(b": "),
mock.call(b"application/x-www-form-urlencoded"),
mock.call(b"\r\n"),
]
)
sock.send.assert_called_with(b"Date=July 25, 2019&Time=12:00")


def test_json():
Expand All @@ -77,7 +86,15 @@ def test_json():
pool.socket.return_value = sock

requests_session = adafruit_requests.Session(pool)
json_data = {"Date": "July 25, 2019"}
json_data = {"Date": "July 25, 2019", "Time": "12:00"}
requests_session.post("http://" + HOST + "/post", json=json_data)
sock.connect.assert_called_once_with((IP, 80))
sock.send.assert_called_with(b'{"Date": "July 25, 2019"}')
sock.send.assert_has_calls(
[
mock.call(b"Content-Type"),
mock.call(b": "),
mock.call(b"application/json"),
mock.call(b"\r\n"),
]
)
sock.send.assert_called_with(b'{"Date": "July 25, 2019", "Time": "12:00"}')
9 changes: 6 additions & 3 deletions tests/protocol_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def test_get_https_text():
)
sock.send.assert_has_calls(
[
mock.call(b"Host: "),
mock.call(b"Host"),
mock.call(b": "),
mock.call(b"wifitest.adafruit.com"),
]
)
Expand Down Expand Up @@ -80,7 +81,8 @@ def test_get_http_text():
)
sock.send.assert_has_calls(
[
mock.call(b"Host: "),
mock.call(b"Host"),
mock.call(b": "),
mock.call(b"wifitest.adafruit.com"),
]
)
Expand Down Expand Up @@ -109,7 +111,8 @@ def test_get_close():
)
sock.send.assert_has_calls(
[
mock.call(b"Host: "),
mock.call(b"Host"),
mock.call(b": "),
mock.call(b"wifitest.adafruit.com"),
]
)
Expand Down
Loading