Skip to content

Commit 32a5dfd

Browse files
authored
Merge pull request #74 from robin-schoch/feature/47-set-and-parse-cookie-server
Feature/47 cookie support
2 parents 8b2a426 + 9950871 commit 32a5dfd

14 files changed

+576
-18
lines changed

lightbug_http/__init__.mojo

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from lightbug_http.http import HTTPRequest, HTTPResponse, OK, NotFound
22
from lightbug_http.uri import URI
33
from lightbug_http.header import Header, Headers, HeaderKey
4+
from lightbug_http.cookie import Cookie, RequestCookieJar, ResponseCookieJar
45
from lightbug_http.service import HTTPService, Welcome
56
from lightbug_http.server import Server
67
from lightbug_http.strings import to_string

lightbug_http/cookie/__init__.mojo

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .cookie import *
2+
from .duration import *
3+
from .same_site import *
4+
from .expiration import *
5+
from .request_cookie_jar import *
6+
from .response_cookie_jar import *

lightbug_http/cookie/cookie.mojo

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from collections import Optional
2+
from lightbug_http.header import HeaderKey
3+
4+
struct Cookie(CollectionElement):
5+
alias EXPIRES = "Expires"
6+
alias MAX_AGE = "Max-Age"
7+
alias DOMAIN = "Domain"
8+
alias PATH = "Path"
9+
alias SECURE = "Secure"
10+
alias HTTP_ONLY = "HttpOnly"
11+
alias SAME_SITE = "SameSite"
12+
alias PARTITIONED = "Partitioned"
13+
14+
alias SEPERATOR = "; "
15+
alias EQUAL = "="
16+
17+
var name: String
18+
var value: String
19+
var expires: Expiration
20+
var secure: Bool
21+
var http_only: Bool
22+
var partitioned: Bool
23+
var same_site: Optional[SameSite]
24+
var domain: Optional[String]
25+
var path: Optional[String]
26+
var max_age: Optional[Duration]
27+
28+
29+
@staticmethod
30+
fn from_set_header(header_str: String) raises -> Self:
31+
var parts = header_str.split(Cookie.SEPERATOR)
32+
if len(parts) < 1:
33+
raise Error("invalid Cookie")
34+
35+
var cookie = Cookie("", parts[0], path=str("/"))
36+
if Cookie.EQUAL in parts[0]:
37+
var name_value = parts[0].split(Cookie.EQUAL)
38+
cookie.name = name_value[0]
39+
cookie.value = name_value[1]
40+
41+
for i in range(1, len(parts)):
42+
var part = parts[i]
43+
if part == Cookie.PARTITIONED:
44+
cookie.partitioned = True
45+
elif part == Cookie.SECURE:
46+
cookie.secure = True
47+
elif part == Cookie.HTTP_ONLY:
48+
cookie.http_only = True
49+
elif part.startswith(Cookie.SAME_SITE):
50+
cookie.same_site = SameSite.from_string(part.removeprefix(Cookie.SAME_SITE + Cookie.EQUAL))
51+
elif part.startswith(Cookie.DOMAIN):
52+
cookie.domain = part.removeprefix(Cookie.DOMAIN + Cookie.EQUAL)
53+
elif part.startswith(Cookie.PATH):
54+
cookie.path = part.removeprefix(Cookie.PATH + Cookie.EQUAL)
55+
elif part.startswith(Cookie.MAX_AGE):
56+
cookie.max_age = Duration.from_string(part.removeprefix(Cookie.MAX_AGE + Cookie.EQUAL))
57+
elif part.startswith(Cookie.EXPIRES):
58+
var expires = Expiration.from_string(part.removeprefix(Cookie.EXPIRES + Cookie.EQUAL))
59+
if expires:
60+
cookie.expires = expires.value()
61+
62+
return cookie
63+
64+
fn __init__(
65+
inout self,
66+
name: String,
67+
value: String,
68+
expires: Expiration = Expiration.session(),
69+
max_age: Optional[Duration] = Optional[Duration](None),
70+
domain: Optional[String] = Optional[String](None),
71+
path: Optional[String] = Optional[String](None),
72+
same_site: Optional[SameSite] = Optional[SameSite](None),
73+
secure: Bool = False,
74+
http_only: Bool = False,
75+
partitioned: Bool = False,
76+
):
77+
self.name = name
78+
self.value = value
79+
self.expires = expires
80+
self.max_age = max_age
81+
self.domain = domain
82+
self.path = path
83+
self.secure = secure
84+
self.http_only = http_only
85+
self.same_site = same_site
86+
self.partitioned = partitioned
87+
88+
fn __str__(self) -> String:
89+
return "Name: " + self.name + " Value: " + self.value
90+
91+
fn __copyinit__(inout self: Cookie, existing: Cookie):
92+
self.name = existing.name
93+
self.value = existing.value
94+
self.max_age = existing.max_age
95+
self.expires = existing.expires
96+
self.domain = existing.domain
97+
self.path = existing.path
98+
self.secure = existing.secure
99+
self.http_only = existing.http_only
100+
self.same_site = existing.same_site
101+
self.partitioned = existing.partitioned
102+
103+
fn __moveinit__(inout self: Cookie, owned existing: Cookie):
104+
self.name = existing.name
105+
self.value = existing.value
106+
self.max_age = existing.max_age
107+
self.expires = existing.expires
108+
self.domain = existing.domain
109+
self.path = existing.path
110+
self.secure = existing.secure
111+
self.http_only = existing.http_only
112+
self.same_site = existing.same_site
113+
self.partitioned = existing.partitioned
114+
115+
fn clear_cookie(inout self):
116+
self.max_age = Optional[Duration](None)
117+
self.expires = Expiration.invalidate()
118+
119+
fn to_header(self) -> Header:
120+
return Header(HeaderKey.SET_COOKIE, self.build_header_value())
121+
122+
fn build_header_value(self) -> String:
123+
124+
var header_value = self.name + Cookie.EQUAL + self.value
125+
if self.expires.is_datetime():
126+
var v = self.expires.http_date_timestamp()
127+
if v:
128+
header_value += Cookie.SEPERATOR + Cookie.EXPIRES + Cookie.EQUAL + v.value()
129+
if self.max_age:
130+
header_value += Cookie.SEPERATOR + Cookie.MAX_AGE + Cookie.EQUAL + str(self.max_age.value().total_seconds)
131+
if self.domain:
132+
header_value += Cookie.SEPERATOR + Cookie.DOMAIN + Cookie.EQUAL + self.domain.value()
133+
if self.path:
134+
header_value += Cookie.SEPERATOR + Cookie.PATH + Cookie.EQUAL + self.path.value()
135+
if self.secure:
136+
header_value += Cookie.SEPERATOR + Cookie.SECURE
137+
if self.http_only:
138+
header_value += Cookie.SEPERATOR + Cookie.HTTP_ONLY
139+
if self.same_site:
140+
header_value += Cookie.SEPERATOR + Cookie.SAME_SITE + Cookie.EQUAL + str(self.same_site.value())
141+
if self.partitioned:
142+
header_value += Cookie.SEPERATOR + Cookie.PARTITIONED
143+
return header_value

lightbug_http/cookie/duration.mojo

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@value
2+
struct Duration():
3+
var total_seconds: Int
4+
5+
fn __init__(
6+
inout self,
7+
seconds: Int = 0,
8+
minutes: Int = 0,
9+
hours: Int = 0,
10+
days: Int = 0
11+
):
12+
self.total_seconds = seconds
13+
self.total_seconds += minutes * 60
14+
self.total_seconds += hours * 60 * 60
15+
self.total_seconds += days * 24 * 60 * 60
16+
17+
@staticmethod
18+
fn from_string(str: String) -> Optional[Self]:
19+
try:
20+
return Duration(seconds=int(str))
21+
except:
22+
return Optional[Self](None)

lightbug_http/cookie/expiration.mojo

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
alias HTTP_DATE_FORMAT = "ddd, DD MMM YYYY HH:mm:ss ZZZ"
2+
alias TZ_GMT = TimeZone(0, "GMT")
3+
4+
@value
5+
struct Expiration:
6+
var variant: UInt8
7+
var datetime: Optional[SmallTime]
8+
9+
@staticmethod
10+
fn session() -> Self:
11+
return Self(variant=0, datetime=None)
12+
13+
@staticmethod
14+
fn from_datetime(time: SmallTime) -> Self:
15+
return Self(variant=1, datetime=time)
16+
17+
@staticmethod
18+
fn from_string(str: String) -> Optional[Self]:
19+
try:
20+
return Self.from_datetime(strptime(str, HTTP_DATE_FORMAT, TZ_GMT))
21+
except:
22+
return None
23+
24+
@staticmethod
25+
fn invalidate() -> Self:
26+
return Self(variant=1, datetime=SmallTime(1970, 1, 1, 0, 0, 0, 0))
27+
28+
fn is_session(self) -> Bool:
29+
return self.variant == 0
30+
31+
fn is_datetime(self) -> Bool:
32+
return self.variant == 1
33+
34+
fn http_date_timestamp(self) -> Optional[String]:
35+
if not self.datetime:
36+
return Optional[String](None)
37+
38+
# TODO fix this it breaks time and space (replacing timezone might add or remove something sometimes)
39+
var dt = self.datetime.value()
40+
dt.tz = TZ_GMT
41+
return Optional[String](dt.format(HTTP_DATE_FORMAT))
42+
43+
fn __eq__(self, other: Self) -> Bool:
44+
if self.variant != other.variant:
45+
return False
46+
if self.variant == 1:
47+
return self.datetime == other.datetime
48+
return True
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from collections import Optional, List, Dict
2+
from small_time import SmallTime, TimeZone
3+
from small_time.small_time import strptime
4+
from lightbug_http.strings import to_string, lineBreak
5+
from lightbug_http.header import HeaderKey, write_header
6+
from lightbug_http.utils import ByteReader, ByteWriter, is_newline, is_space
7+
8+
@value
9+
struct RequestCookieJar(Formattable, Stringable):
10+
var _inner: Dict[String, String]
11+
12+
fn __init__(inout self):
13+
self._inner = Dict[String, String]()
14+
15+
fn __init__(inout self, *cookies: Cookie):
16+
self._inner = Dict[String, String]()
17+
for cookie in cookies:
18+
self._inner[cookie[].name] = cookie[].value
19+
20+
fn parse_cookies(inout self, headers: Headers) raises:
21+
var cookie_header = headers[HeaderKey.COOKIE]
22+
if not cookie_header:
23+
return None
24+
var cookie_strings = cookie_header.split("; ")
25+
26+
for chunk in cookie_strings:
27+
var key = String("")
28+
var value = chunk[]
29+
if "=" in chunk[]:
30+
var key_value = chunk[].split("=")
31+
key = key_value[0]
32+
value = key_value[1]
33+
34+
# TODO value must be "unquoted"
35+
self._inner[key] = value
36+
37+
38+
@always_inline
39+
fn empty(self) -> Bool:
40+
return len(self._inner) == 0
41+
42+
@always_inline
43+
fn __contains__(self, key: String) -> Bool:
44+
return key in self._inner
45+
46+
fn __contains__(self, key: Cookie) -> Bool:
47+
return key.name in self
48+
49+
@always_inline
50+
fn __getitem__(self, key: String) raises -> String:
51+
return self._inner[key.lower()]
52+
53+
fn get(self, key: String) -> Optional[String]:
54+
try:
55+
return self[key]
56+
except:
57+
return Optional[String](None)
58+
59+
fn to_header(self) -> Optional[Header]:
60+
alias equal = "="
61+
if len(self._inner) == 0:
62+
return None
63+
64+
var header_value = List[String]()
65+
for cookie in self._inner.items():
66+
header_value.append(cookie[].key + equal + cookie[].value)
67+
return Header(HeaderKey.COOKIE, "; ".join(header_value))
68+
69+
fn encode_to(inout self, inout writer: ByteWriter):
70+
var header = self.to_header()
71+
if header:
72+
write_header(writer, header.value().key, header.value().value)
73+
74+
fn format_to(self, inout writer: Formatter):
75+
var header = self.to_header()
76+
if header:
77+
write_header(writer, header.value().key, header.value().value)
78+
79+
fn __str__(self) -> String:
80+
return to_string(self)

0 commit comments

Comments
 (0)