432 lines
8.8 KiB
Odin
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
|
|
}
|