Skip to content
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

Feature/47 cookie support #74

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions lightbug_http/__init__.mojo
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from lightbug_http.http import HTTPRequest, HTTPResponse, OK, NotFound
from lightbug_http.uri import URI
from lightbug_http.header import Header, Headers, HeaderKey
from lightbug_http.cookie import Cookie, RequestCookieJar, ResponseCookieJar
from lightbug_http.service import HTTPService, Welcome
from lightbug_http.server import Server
from lightbug_http.strings import to_string
Expand Down
6 changes: 6 additions & 0 deletions lightbug_http/cookie/__init__.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .cookie import *
from .duration import *
from .same_site import *
from .expiration import *
from .request_cookie_jar import *
from .response_cookie_jar import *
143 changes: 143 additions & 0 deletions lightbug_http/cookie/cookie.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from collections import Optional
from lightbug_http.header import HeaderKey

struct Cookie(CollectionElement):
alias EXPIRES = "Expires"
alias MAX_AGE = "Max-Age"
alias DOMAIN = "Domain"
alias PATH = "Path"
alias SECURE = "Secure"
alias HTTP_ONLY = "HttpOnly"
alias SAME_SITE = "SameSite"
alias PARTITIONED = "Partitioned"

alias SEPERATOR = "; "
alias EQUAL = "="

var name: String
var value: String
var expires: Expiration
var secure: Bool
var http_only: Bool
var partitioned: Bool
var same_site: Optional[SameSite]
var domain: Optional[String]
var path: Optional[String]
var max_age: Optional[Duration]


@staticmethod
fn from_set_header(header_str: String) raises -> Self:
var parts = header_str.split(Cookie.SEPERATOR)
if len(parts) < 1:
raise Error("invalid Cookie")

var cookie = Cookie("", parts[0], path=str("/"))
if Cookie.EQUAL in parts[0]:
var name_value = parts[0].split(Cookie.EQUAL)
cookie.name = name_value[0]
cookie.value = name_value[1]

for i in range(1, len(parts)):
var part = parts[i]
if part == Cookie.PARTITIONED:
cookie.partitioned = True
elif part == Cookie.SECURE:
cookie.secure = True
elif part == Cookie.HTTP_ONLY:
cookie.http_only = True
elif part.startswith(Cookie.SAME_SITE):
cookie.same_site = SameSite.from_string(part.removeprefix(Cookie.SAME_SITE + Cookie.EQUAL))
elif part.startswith(Cookie.DOMAIN):
cookie.domain = part.removeprefix(Cookie.DOMAIN + Cookie.EQUAL)
elif part.startswith(Cookie.PATH):
cookie.path = part.removeprefix(Cookie.PATH + Cookie.EQUAL)
elif part.startswith(Cookie.MAX_AGE):
cookie.max_age = Duration.from_string(part.removeprefix(Cookie.MAX_AGE + Cookie.EQUAL))
elif part.startswith(Cookie.EXPIRES):
var expires = Expiration.from_string(part.removeprefix(Cookie.EXPIRES + Cookie.EQUAL))
if expires:
cookie.expires = expires.value()

return cookie

fn __init__(
inout self,
name: String,
value: String,
expires: Expiration = Expiration.session(),
max_age: Optional[Duration] = Optional[Duration](None),
domain: Optional[String] = Optional[String](None),
path: Optional[String] = Optional[String](None),
same_site: Optional[SameSite] = Optional[SameSite](None),
secure: Bool = False,
http_only: Bool = False,
partitioned: Bool = False,
):
self.name = name
self.value = value
self.expires = expires
self.max_age = max_age
self.domain = domain
self.path = path
self.secure = secure
self.http_only = http_only
self.same_site = same_site
self.partitioned = partitioned

fn __str__(self) -> String:
return "Name: " + self.name + " Value: " + self.value

fn __copyinit__(inout self: Cookie, existing: Cookie):
self.name = existing.name
self.value = existing.value
self.max_age = existing.max_age
self.expires = existing.expires
self.domain = existing.domain
self.path = existing.path
self.secure = existing.secure
self.http_only = existing.http_only
self.same_site = existing.same_site
self.partitioned = existing.partitioned

fn __moveinit__(inout self: Cookie, owned existing: Cookie):
self.name = existing.name
self.value = existing.value
self.max_age = existing.max_age
self.expires = existing.expires
self.domain = existing.domain
self.path = existing.path
self.secure = existing.secure
self.http_only = existing.http_only
self.same_site = existing.same_site
self.partitioned = existing.partitioned

fn clear_cookie(inout self):
self.max_age = Optional[Duration](None)
self.expires = Expiration.invalidate()

fn to_header(self) -> Header:
return Header(HeaderKey.SET_COOKIE, self.build_header_value())

fn build_header_value(self) -> String:

var header_value = self.name + Cookie.EQUAL + self.value
if self.expires.is_datetime():
var v = self.expires.http_date_timestamp()
if v:
header_value += Cookie.SEPERATOR + Cookie.EXPIRES + Cookie.EQUAL + v.value()
if self.max_age:
header_value += Cookie.SEPERATOR + Cookie.MAX_AGE + Cookie.EQUAL + str(self.max_age.value().total_seconds)
if self.domain:
header_value += Cookie.SEPERATOR + Cookie.DOMAIN + Cookie.EQUAL + self.domain.value()
if self.path:
header_value += Cookie.SEPERATOR + Cookie.PATH + Cookie.EQUAL + self.path.value()
if self.secure:
header_value += Cookie.SEPERATOR + Cookie.SECURE
if self.http_only:
header_value += Cookie.SEPERATOR + Cookie.HTTP_ONLY
if self.same_site:
header_value += Cookie.SEPERATOR + Cookie.SAME_SITE + Cookie.EQUAL + str(self.same_site.value())
if self.partitioned:
header_value += Cookie.SEPERATOR + Cookie.PARTITIONED
return header_value
22 changes: 22 additions & 0 deletions lightbug_http/cookie/duration.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@value
struct Duration():
var total_seconds: Int

fn __init__(
inout self,
seconds: Int = 0,
minutes: Int = 0,
hours: Int = 0,
days: Int = 0
):
self.total_seconds = seconds
self.total_seconds += minutes * 60
self.total_seconds += hours * 60 * 60
self.total_seconds += days * 24 * 60 * 60

@staticmethod
fn from_string(str: String) -> Optional[Self]:
try:
return Duration(seconds=int(str))
except:
return Optional[Self](None)
48 changes: 48 additions & 0 deletions lightbug_http/cookie/expiration.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
alias HTTP_DATE_FORMAT = "ddd, DD MMM YYYY HH:mm:ss ZZZ"
alias TZ_GMT = TimeZone(0, "GMT")

@value
struct Expiration:
var variant: UInt8
var datetime: Optional[SmallTime]

@staticmethod
fn session() -> Self:
return Self(variant=0, datetime=None)

@staticmethod
fn from_datetime(time: SmallTime) -> Self:
return Self(variant=1, datetime=time)

@staticmethod
fn from_string(str: String) -> Optional[Self]:
try:
return Self.from_datetime(strptime(str, HTTP_DATE_FORMAT, TZ_GMT))
except:
return None

@staticmethod
fn invalidate() -> Self:
return Self(variant=1, datetime=SmallTime(1970, 1, 1, 0, 0, 0, 0))

fn is_session(self) -> Bool:
return self.variant == 0

fn is_datetime(self) -> Bool:
return self.variant == 1

fn http_date_timestamp(self) -> Optional[String]:
if not self.datetime:
return Optional[String](None)

# TODO fix this it breaks time and space (replacing timezone might add or remove something sometimes)
var dt = self.datetime.value()
dt.tz = TZ_GMT
return Optional[String](dt.format(HTTP_DATE_FORMAT))

fn __eq__(self, other: Self) -> Bool:
if self.variant != other.variant:
return False
if self.variant == 1:
return self.datetime == other.datetime
return True
80 changes: 80 additions & 0 deletions lightbug_http/cookie/request_cookie_jar.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from collections import Optional, List, Dict
from small_time import SmallTime, TimeZone
from small_time.small_time import strptime
from lightbug_http.strings import to_string, lineBreak
from lightbug_http.header import HeaderKey, write_header
from lightbug_http.utils import ByteReader, ByteWriter, is_newline, is_space

@value
struct RequestCookieJar(Formattable, Stringable):
var _inner: Dict[String, String]

fn __init__(inout self):
self._inner = Dict[String, String]()

fn __init__(inout self, *cookies: Cookie):
self._inner = Dict[String, String]()
for cookie in cookies:
self._inner[cookie[].name] = cookie[].value

fn parse_cookies(inout self, headers: Headers) raises:
var cookie_header = headers[HeaderKey.COOKIE]
if not cookie_header:
return None
var cookie_strings = cookie_header.split("; ")

for chunk in cookie_strings:
var key = String("")
var value = chunk[]
if "=" in chunk[]:
var key_value = chunk[].split("=")
key = key_value[0]
value = key_value[1]

# TODO value must be "unquoted"
self._inner[key] = value


@always_inline
fn empty(self) -> Bool:
return len(self._inner) == 0

@always_inline
fn __contains__(self, key: String) -> Bool:
return key in self._inner

fn __contains__(self, key: Cookie) -> Bool:
return key.name in self

@always_inline
fn __getitem__(self, key: String) raises -> String:
return self._inner[key.lower()]

fn get(self, key: String) -> Optional[String]:
try:
return self[key]
except:
return Optional[String](None)

fn to_header(self) -> Optional[Header]:
alias equal = "="
if len(self._inner) == 0:
return None

var header_value = List[String]()
for cookie in self._inner.items():
header_value.append(cookie[].key + equal + cookie[].value)
return Header(HeaderKey.COOKIE, "; ".join(header_value))

fn encode_to(inout self, inout writer: ByteWriter):
var header = self.to_header()
if header:
write_header(writer, header.value().key, header.value().value)

fn format_to(self, inout writer: Formatter):
var header = self.to_header()
if header:
write_header(writer, header.value().key, header.value().value)

fn __str__(self) -> String:
return to_string(self)
Loading