lph-11/odin-http/cookie.odin
2025-03-13 18:14:21 +13:00

432 lines
8.8 KiB
Odin

package http
import "core:io"
import "core:strconv"
import "core:strings"
import "core:time"
Cookie_Same_Site :: enum {
Unspecified,
None,
Strict,
Lax,
}
Cookie :: struct {
name: string,
value: string,
domain: Maybe(string),
expires_gmt: Maybe(time.Time),
max_age_secs: Maybe(int),
path: Maybe(string),
http_only: bool,
partitioned: bool,
secure: bool,
same_site: Cookie_Same_Site,
}
// Builds the Set-Cookie header string representation of the given cookie.
cookie_write :: proc(w: io.Writer, c: Cookie) -> io.Error {
// odinfmt:disable
io.write_string(w, "set-cookie: ") or_return
write_escaped_newlines(w, c.name) or_return
io.write_byte(w, '=') or_return
write_escaped_newlines(w, c.value) or_return
if d, ok := c.domain.(string); ok {
io.write_string(w, "; Domain=") or_return
write_escaped_newlines(w, d) or_return
}
if e, ok := c.expires_gmt.(time.Time); ok {
io.write_string(w, "; Expires=") or_return
date_write(w, e) or_return
}
if a, ok := c.max_age_secs.(int); ok {
io.write_string(w, "; Max-Age=") or_return
io.write_int(w, a) or_return
}
if p, ok := c.path.(string); ok {
io.write_string(w, "; Path=") or_return
write_escaped_newlines(w, p) or_return
}
switch c.same_site {
case .None: io.write_string(w, "; SameSite=None") or_return
case .Lax: io.write_string(w, "; SameSite=Lax") or_return
case .Strict: io.write_string(w, "; SameSite=Strict") or_return
case .Unspecified: // no-op.
}
// odinfmt:enable
if c.secure {
io.write_string(w, "; Secure") or_return
}
if c.partitioned {
io.write_string(w, "; Partitioned") or_return
}
if c.http_only {
io.write_string(w, "; HttpOnly") or_return
}
return nil
}
// Builds the Set-Cookie header string representation of the given cookie.
cookie_string :: proc(c: Cookie, allocator := context.allocator) -> string {
b: strings.Builder
strings.builder_init(&b, 0, 20, allocator)
cookie_write(strings.to_writer(&b), c)
return strings.to_string(b)
}
// TODO: check specific whitespace requirements in RFC.
//
// Allocations are done to check case-insensitive attributes but they are deleted right after.
// So, all the returned strings (inside cookie) are slices into the given value string.
cookie_parse :: proc(value: string, allocator := context.allocator) -> (cookie: Cookie, ok: bool) {
value := value
eq := strings.index_byte(value, '=')
if eq < 1 do return
cookie.name = value[:eq]
value = value[eq + 1:]
semi := strings.index_byte(value, ';')
switch semi {
case -1:
cookie.value = value
ok = true
return
case 0:
return
case:
cookie.value = value[:semi]
value = value[semi + 1:]
}
parse_part :: proc(cookie: ^Cookie, part: string, allocator := context.temp_allocator) -> (ok: bool) {
eq := strings.index_byte(part, '=')
switch eq {
case -1:
key := strings.to_lower(part, allocator)
defer delete(key)
switch key {
case "httponly":
cookie.http_only = true
case "partitioned":
cookie.partitioned = true
case "secure":
cookie.secure = true
case:
return
}
case 0:
return
case:
key := strings.to_lower(part[:eq], allocator)
defer delete(key)
value := part[eq + 1:]
switch key {
case "domain":
cookie.domain = value
case "expires":
cookie.expires_gmt = cookie_date_parse(value) or_return
case "max-age":
cookie.max_age_secs = strconv.parse_int(value, 10) or_return
case "path":
cookie.path = value
case "samesite":
switch value {
case "lax", "Lax", "LAX":
cookie.same_site = .Lax
case "none", "None", "NONE":
cookie.same_site = .None
case "strict", "Strict", "STRICT":
cookie.same_site = .Strict
case:
return
}
case:
return
}
}
return true
}
for semi = strings.index_byte(value, ';'); semi != -1; semi = strings.index_byte(value, ';') {
part := strings.trim_left_space(value[:semi])
value = value[semi + 1:]
parse_part(&cookie, part, allocator) or_return
}
part := strings.trim_left_space(value)
if part == "" {
ok = true
return
}
parse_part(&cookie, part, allocator) or_return
ok = true
return
}
/*
Implementation of the algorithm described in RFC 6265 section 5.1.1.
*/
cookie_date_parse :: proc(value: string) -> (t: time.Time, ok: bool) {
iter_delim :: proc(value: ^string) -> (token: string, ok: bool) {
start := -1
start_loop: for ch, i in transmute([]byte)value^ {
switch ch {
case 0x09, 0x20..=0x2F, 0x3B..=0x40, 0x5B..=0x60, 0x7B..=0x7E:
case:
start = i
break start_loop
}
}
if start == -1 {
return
}
token = value[start:]
length := len(token)
end_loop: for ch, i in transmute([]byte)token {
switch ch {
case 0x09, 0x20..=0x2F, 0x3B..=0x40, 0x5B..=0x60, 0x7B..=0x7E:
length = i
break end_loop
}
}
ok = true
token = token[:length]
value^ = value[start+length:]
return
}
parse_digits :: proc(value: string, min, max: int, trailing_ok: bool) -> (int, bool) {
count: int
for ch in transmute([]byte)value {
if ch <= 0x2f || ch >= 0x3a {
break
}
count += 1
}
if count < min || count > max {
return 0, false
}
if !trailing_ok && len(value) != count {
return 0, false
}
return strconv.parse_int(value[:count], 10)
}
parse_time :: proc(token: string) -> (t: Time, ok: bool) {
hours, match1, tail := strings.partition(token, ":")
if match1 != ":" { return }
minutes, match2, seconds := strings.partition(tail, ":")
if match2 != ":" { return }
t.hours = parse_digits(hours, 1, 2, false) or_return
t.minutes = parse_digits(minutes, 1, 2, false) or_return
t.seconds = parse_digits(seconds, 1, 2, true) or_return
ok = true
return
}
parse_month :: proc(token: string) -> (month: int) {
if len(token) < 3 {
return
}
lower: [3]byte
for &ch, i in lower {
#no_bounds_check orig := token[i]
switch orig {
case 'A'..='Z':
ch = orig + 32
case:
ch = orig
}
}
switch string(lower[:]) {
case "jan":
return 1
case "feb":
return 2
case "mar":
return 3
case "apr":
return 4
case "may":
return 5
case "jun":
return 6
case "jul":
return 7
case "aug":
return 8
case "sep":
return 9
case "oct":
return 10
case "nov":
return 11
case "dec":
return 12
case:
return
}
}
Time :: struct {
hours, minutes, seconds: int,
}
clock: Maybe(Time)
day_of_month, month, year: Maybe(int)
value := value
for token in iter_delim(&value) {
if _, has_time := clock.?; !has_time {
if t, tok := parse_time(token); tok {
clock = t
continue
}
}
if _, has_day_of_month := day_of_month.?; !has_day_of_month {
if dom, dok := parse_digits(token, 1, 2, true); dok {
day_of_month = dom
continue
}
}
if _, has_month := month.?; !has_month {
if mon := parse_month(token); mon > 0 {
month = mon
continue
}
}
if _, has_year := year.?; !has_year {
if yr, yrok := parse_digits(token, 2, 4, true); yrok {
if yr >= 70 && yr <= 99 {
yr += 1900
} else if yr >= 0 && yr <= 69 {
yr += 2000
}
year = yr
continue
}
}
}
c := clock.? or_return
y := year.? or_return
if y < 1601 {
return
}
t = time.datetime_to_time(
y,
month.? or_return,
day_of_month.? or_return,
c.hours,
c.minutes,
c.seconds,
) or_return
ok = true
return
}
/*
Retrieves the cookie with the given `key` out of the requests `Cookie` header.
If the same key is in the header multiple times the last one is returned.
*/
request_cookie_get :: proc(r: ^Request, key: string) -> (value: string, ok: bool) {
cookies := headers_get_unsafe(r.headers, "cookie") or_return
for k, v in request_cookies_iter(&cookies) {
if key == k do return v, true
}
return
}
/*
Allocates a map with the given allocator and puts all cookie pairs from the requests `Cookie` header into it.
If the same key is in the header multiple times the last one is returned.
*/
request_cookies :: proc(r: ^Request, allocator := context.temp_allocator) -> (res: map[string]string) {
res.allocator = allocator
cookies := headers_get_unsafe(r.headers, "cookie") or_else ""
for k, v in request_cookies_iter(&cookies) {
// Don't overwrite, the iterator goes from right to left and we want the last.
if k in res do continue
res[k] = v
}
return
}
/*
Iterates the cookies from right to left.
*/
request_cookies_iter :: proc(cookies: ^string) -> (key: string, value: string, ok: bool) {
end := len(cookies)
eq := -1
for i := end-1; i >= 0; i-=1 {
b := cookies[i]
start := i == 0
sep := start || b == ' ' && cookies[i-1] == ';'
if sep {
defer end = i - 1
// Invalid.
if eq < 0 {
continue
}
off := 0 if start else 1
key = cookies[i+off:eq]
value = cookies[eq+1:end]
cookies^ = cookies[:i-off]
return key, value, true
} else if b == '=' {
eq = i
}
}
return
}