Skip to content

Commit 4ec5f9a

Browse files
committed
Added cookies handling and example for it
1 parent 46c22bf commit 4ec5f9a

File tree

4 files changed

+151
-2
lines changed

4 files changed

+151
-2
lines changed

adafruit_httpserver/request.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def __repr__(self) -> str:
221221
return f"{class_name}({repr(self._storage)}, files={repr(self.files._storage)})"
222222

223223

224-
class Request:
224+
class Request: # pylint: disable=too-many-instance-attributes
225225
"""
226226
Incoming request, constructed from raw incoming bytes.
227227
It is passed as first argument to all route handlers.
@@ -292,6 +292,7 @@ def __init__(
292292
self.client_address = client_address
293293
self.raw_request = raw_request
294294
self._form_data = None
295+
self._cookies = None
295296

296297
if raw_request is None:
297298
raise ValueError("raw_request cannot be None")
@@ -316,6 +317,36 @@ def body(self) -> bytes:
316317
def body(self, body: bytes) -> None:
317318
self.raw_request = self._raw_header_bytes + b"\r\n\r\n" + body
318319

320+
@staticmethod
321+
def _parse_cookies(cookie_header: str) -> None:
322+
"""Parse cookies from headers."""
323+
if cookie_header is None:
324+
return {}
325+
326+
return {
327+
name: value.strip('"')
328+
for name, value in [
329+
cookie.strip().split("=", 1) for cookie in cookie_header.split(";")
330+
]
331+
}
332+
333+
@property
334+
def cookies(self) -> Dict[str, str]:
335+
"""
336+
Cookies sent with the request.
337+
338+
Example::
339+
340+
request.headers["Cookie"]
341+
# "foo=bar; baz=qux; foo=quux"
342+
343+
request.cookies
344+
# {"foo": "quux", "baz": "qux"}
345+
"""
346+
if self._cookies is None:
347+
self._cookies = self._parse_cookies(self.headers.get("Cookie"))
348+
return self._cookies
349+
319350
@property
320351
def form_data(self) -> Union[FormData, None]:
321352
"""

adafruit_httpserver/response.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,15 @@ def __init__( # pylint: disable=too-many-arguments
6060
*,
6161
status: Union[Status, Tuple[int, str]] = OK_200,
6262
headers: Union[Headers, Dict[str, str]] = None,
63+
cookies: Dict[str, str] = None,
6364
content_type: str = None,
6465
) -> None:
6566
"""
6667
:param Request request: Request that this is a response to.
6768
:param str body: Body of response. Defaults to empty string.
6869
:param Status status: Status code and text. Defaults to 200 OK.
6970
:param Headers headers: Headers to include in response. Defaults to empty dict.
71+
:param Dict[str, str] cookies: Cookies to be sent with the response.
7072
:param str content_type: Content type of response. Defaults to None.
7173
"""
7274

@@ -76,6 +78,7 @@ def __init__( # pylint: disable=too-many-arguments
7678
self._headers = (
7779
headers.copy() if isinstance(headers, Headers) else Headers(headers)
7880
)
81+
self._cookies = cookies.copy() if cookies else {}
7982
self._content_type = content_type
8083
self._size = 0
8184

@@ -96,6 +99,9 @@ def _send_headers(
9699
headers.setdefault("Content-Length", content_length)
97100
headers.setdefault("Connection", "close")
98101

102+
for cookie_name, cookie_value in self._cookies.items():
103+
headers.add("Set-Cookie", f"{cookie_name}={cookie_value}")
104+
99105
for header, value in headers.items():
100106
if value is not None:
101107
response_message_header += f"{header}: {value}\r\n"
@@ -166,6 +172,7 @@ def __init__( # pylint: disable=too-many-arguments
166172
*,
167173
status: Union[Status, Tuple[int, str]] = OK_200,
168174
headers: Union[Headers, Dict[str, str]] = None,
175+
cookies: Dict[str, str] = None,
169176
content_type: str = None,
170177
as_attachment: bool = False,
171178
download_filename: str = None,
@@ -180,6 +187,7 @@ def __init__( # pylint: disable=too-many-arguments
180187
server's ``root_path``.
181188
:param Status status: Status code and text. Defaults to ``200 OK``.
182189
:param Headers headers: Headers to include in response.
190+
:param Dict[str, str] cookies: Cookies to be sent with the response.
183191
:param str content_type: Content type of response.
184192
:param bool as_attachment: If ``True``, the file will be sent as an attachment.
185193
:param str download_filename: Name of the file to send as an attachment.
@@ -193,6 +201,7 @@ def __init__( # pylint: disable=too-many-arguments
193201
super().__init__(
194202
request=request,
195203
headers=headers,
204+
cookies=cookies,
196205
content_type=content_type,
197206
status=status,
198207
)
@@ -293,19 +302,22 @@ def __init__( # pylint: disable=too-many-arguments
293302
*,
294303
status: Union[Status, Tuple[int, str]] = OK_200,
295304
headers: Union[Headers, Dict[str, str]] = None,
305+
cookies: Dict[str, str] = None,
296306
content_type: str = None,
297307
) -> None:
298308
"""
299309
:param Request request: Request object
300310
:param Generator body: Generator that yields chunks of data.
301311
:param Status status: Status object or tuple with code and message.
302312
:param Headers headers: Headers to be sent with the response.
313+
:param Dict[str, str] cookies: Cookies to be sent with the response.
303314
:param str content_type: Content type of the response.
304315
"""
305316

306317
super().__init__(
307318
request=request,
308319
headers=headers,
320+
cookies=cookies,
309321
status=status,
310322
content_type=content_type,
311323
)
@@ -352,17 +364,20 @@ def __init__(
352364
data: Dict[Any, Any],
353365
*,
354366
headers: Union[Headers, Dict[str, str]] = None,
367+
cookies: Dict[str, str] = None,
355368
status: Union[Status, Tuple[int, str]] = OK_200,
356369
) -> None:
357370
"""
358371
:param Request request: Request that this is a response to.
359372
:param dict data: Data to be sent as JSON.
360373
:param Headers headers: Headers to include in response.
374+
:param Dict[str, str] cookies: Cookies to be sent with the response.
361375
:param Status status: Status code and text. Defaults to 200 OK.
362376
"""
363377
super().__init__(
364378
request=request,
365379
headers=headers,
380+
cookies=cookies,
366381
status=status,
367382
)
368383
self._data = data
@@ -398,6 +413,7 @@ def __init__(
398413
preserve_method: bool = False,
399414
status: Union[Status, Tuple[int, str]] = None,
400415
headers: Union[Headers, Dict[str, str]] = None,
416+
cookies: Dict[str, str] = None,
401417
) -> None:
402418
"""
403419
By default uses ``permament`` and ``preserve_method`` to determine the ``status`` code to
@@ -415,6 +431,7 @@ def __init__(
415431
:param bool preserve_method: Whether to preserve the method of the request.
416432
:param Status status: Status object or tuple with code and message.
417433
:param Headers headers: Headers to include in response.
434+
:param Dict[str, str] cookies: Cookies to be sent with the response.
418435
"""
419436

420437
if status is not None and (permanent or preserve_method):
@@ -428,7 +445,7 @@ def __init__(
428445
else:
429446
status = MOVED_PERMANENTLY_301 if permanent else FOUND_302
430447

431-
super().__init__(request, status=status, headers=headers)
448+
super().__init__(request, status=status, headers=headers, cookies=cookies)
432449
self._headers.update({"Location": url})
433450

434451
def _send(self) -> None:
@@ -474,14 +491,17 @@ def __init__( # pylint: disable=too-many-arguments
474491
self,
475492
request: Request,
476493
headers: Union[Headers, Dict[str, str]] = None,
494+
cookies: Dict[str, str] = None,
477495
) -> None:
478496
"""
479497
:param Request request: Request object
480498
:param Headers headers: Headers to be sent with the response.
499+
:param Dict[str, str] cookies: Cookies to be sent with the response.
481500
"""
482501
super().__init__(
483502
request=request,
484503
headers=headers,
504+
cookies=cookies,
485505
content_type="text/event-stream",
486506
)
487507
self._headers.setdefault("Cache-Control", "no-cache")
@@ -606,11 +626,13 @@ def __init__( # pylint: disable=too-many-arguments
606626
self,
607627
request: Request,
608628
headers: Union[Headers, Dict[str, str]] = None,
629+
cookies: Dict[str, str] = None,
609630
buffer_size: int = 1024,
610631
) -> None:
611632
"""
612633
:param Request request: Request object
613634
:param Headers headers: Headers to be sent with the response.
635+
:param Dict[str, str] cookies: Cookies to be sent with the response.
614636
:param int buffer_size: Size of the buffer used to send and receive messages.
615637
"""
616638
self._check_request_initiates_handshake(request)
@@ -621,6 +643,7 @@ def __init__( # pylint: disable=too-many-arguments
621643
request=request,
622644
status=SWITCHING_PROTOCOLS_101,
623645
headers=headers,
646+
cookies=cookies,
624647
)
625648
self._headers.setdefault("Upgrade", "websocket")
626649
self._headers.setdefault("Connection", "Upgrade")

docs/examples.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,20 @@ return only the first one.
185185
:emphasize-lines: 32,47,50
186186
:linenos:
187187

188+
Cookies
189+
---------------------
190+
191+
You can use cookies to store data on the client side, that will be sent back to the server with every request.
192+
They are often used to store authentication tokens, session IDs, but also to user preferences e.g. theme.
193+
194+
To access cookies, use ``request.cookies`` dictionary.
195+
In order to set cookies, pass ``cookies`` dictionary to ``Response`` constructor.
196+
197+
.. literalinclude:: ../examples/httpserver_cookies.py
198+
:caption: examples/httpserver_cookies.py
199+
:emphasize-lines: 70,77
200+
:linenos:
201+
188202
Chunked response
189203
----------------
190204

examples/httpserver_cookies.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# SPDX-FileCopyrightText: 2023 Michał Pokusa
2+
#
3+
# SPDX-License-Identifier: Unlicense
4+
5+
import socketpool
6+
import wifi
7+
8+
from adafruit_httpserver import Server, Request, Response, GET
9+
10+
11+
pool = socketpool.SocketPool(wifi.radio)
12+
server = Server(pool, debug=True)
13+
14+
15+
THEMES = {
16+
"dark": {
17+
"background-color": "#1c1c1c",
18+
"color": "white",
19+
"button-color": "#181818",
20+
},
21+
"light": {
22+
"background-color": "white",
23+
"color": "#1c1c1c",
24+
"button-color": "white",
25+
},
26+
}
27+
28+
29+
def themed_template(user_preferred_theme: str):
30+
theme = THEMES[user_preferred_theme]
31+
32+
return f"""
33+
<html>
34+
<head>
35+
<title>Cookie Example</title>
36+
<style>
37+
body {{
38+
background-color: {theme['background-color']};
39+
color: {theme['color']};
40+
}}
41+
42+
button {{
43+
background-color: {theme['button-color']};
44+
color: {theme['color']};
45+
border: 1px solid {theme['color']};
46+
padding: 10px;
47+
margin: 10px;
48+
}}
49+
</style>
50+
</head>
51+
<body>
52+
<a href="/?theme=dark"><button>Dark theme</button></a>
53+
<a href="/?theme=light"><button>Light theme</button></a>
54+
<br />
55+
<p>
56+
After changing the theme, close the tab and open again.
57+
Notice that theme stays the same.
58+
</p>
59+
</body>
60+
</html>
61+
"""
62+
63+
64+
@server.route("/", GET)
65+
def themed_from_cookie(request: Request):
66+
"""
67+
Serve a simple themed page, based on the user's cookie.
68+
"""
69+
70+
user_theme = request.cookies.get("theme", "light")
71+
wanted_theme = request.query_params.get("theme", user_theme)
72+
73+
return Response(
74+
request,
75+
themed_template(wanted_theme),
76+
content_type="text/html",
77+
cookies={} if user_theme == wanted_theme else {"theme": wanted_theme},
78+
)
79+
80+
81+
server.serve_forever(str(wifi.radio.ipv4_address))

0 commit comments

Comments
 (0)