440 lines
11 KiB
Odin
440 lines
11 KiB
Odin
package http
|
|
|
|
import "base:runtime"
|
|
|
|
import "core:io"
|
|
import "core:slice"
|
|
import "core:strconv"
|
|
import "core:strings"
|
|
import "core:sync"
|
|
import "core:time"
|
|
|
|
Requestline_Error :: enum {
|
|
None,
|
|
Method_Not_Implemented,
|
|
Not_Enough_Fields,
|
|
Invalid_Version_Format,
|
|
}
|
|
|
|
Requestline :: struct {
|
|
method: Method,
|
|
target: union {
|
|
string,
|
|
URL,
|
|
},
|
|
version: Version,
|
|
}
|
|
|
|
// A request-line begins with a method token, followed by a single space
|
|
// (SP), the request-target, another single space (SP), the protocol
|
|
// version, and ends with CRLF.
|
|
//
|
|
// This allocates a clone of the target, because this is intended to be used with a scanner,
|
|
// which has a buffer that changes every read.
|
|
requestline_parse :: proc(s: string, allocator := context.temp_allocator) -> (line: Requestline, err: Requestline_Error) {
|
|
s := s
|
|
|
|
next_space := strings.index_byte(s, ' ')
|
|
if next_space == -1 do return line, .Not_Enough_Fields
|
|
|
|
ok: bool
|
|
line.method, ok = method_parse(s[:next_space])
|
|
if !ok do return line, .Method_Not_Implemented
|
|
s = s[next_space + 1:]
|
|
|
|
next_space = strings.index_byte(s, ' ')
|
|
if next_space == -1 do return line, .Not_Enough_Fields
|
|
|
|
line.target = strings.clone(s[:next_space], allocator)
|
|
s = s[len(line.target.(string)) + 1:]
|
|
|
|
line.version, ok = version_parse(s)
|
|
if !ok do return line, .Invalid_Version_Format
|
|
|
|
return
|
|
}
|
|
|
|
requestline_write :: proc(w: io.Writer, rline: Requestline) -> io.Error {
|
|
// odinfmt:disable
|
|
io.write_string(w, method_string(rline.method)) or_return // <METHOD>
|
|
io.write_byte(w, ' ') or_return // <METHOD> <SP>
|
|
|
|
switch t in rline.target {
|
|
case string: io.write_string(w, t) or_return // <METHOD> <SP> <TARGET>
|
|
case URL: request_path_write(w, t) or_return // <METHOD> <SP> <TARGET>
|
|
}
|
|
|
|
io.write_byte(w, ' ') or_return // <METHOD> <SP> <TARGET> <SP>
|
|
version_write(w, rline.version) or_return // <METHOD> <SP> <TARGET> <SP> <VERSION>
|
|
io.write_string(w, "\r\n") or_return // <METHOD> <SP> <TARGET> <SP> <VERSION> <CRLF>
|
|
// odinfmt:enable
|
|
|
|
return nil
|
|
}
|
|
|
|
Version :: struct {
|
|
major: u8,
|
|
minor: u8,
|
|
}
|
|
|
|
// Parses an HTTP version string according to RFC 7230, section 2.6.
|
|
version_parse :: proc(s: string) -> (version: Version, ok: bool) {
|
|
switch len(s) {
|
|
case 8:
|
|
(s[6] == '.') or_return
|
|
version.minor = u8(int(s[7]) - '0')
|
|
fallthrough
|
|
case 6:
|
|
(s[:5] == "HTTP/") or_return
|
|
version.major = u8(int(s[5]) - '0')
|
|
case:
|
|
return
|
|
}
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
version_write :: proc(w: io.Writer, v: Version) -> io.Error {
|
|
io.write_string(w, "HTTP/") or_return
|
|
io.write_rune(w, '0' + rune(v.major)) or_return
|
|
if v.minor > 0 {
|
|
io.write_rune(w, '.')
|
|
io.write_rune(w, '0' + rune(v.minor))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
version_string :: proc(v: Version, allocator := context.allocator) -> string {
|
|
buf := make([]byte, 8, allocator)
|
|
|
|
b: strings.Builder
|
|
b.buf = slice.into_dynamic(buf)
|
|
|
|
version_write(strings.to_writer(&b), v)
|
|
|
|
return strings.to_string(b)
|
|
}
|
|
|
|
Method :: enum {
|
|
Get,
|
|
Post,
|
|
Delete,
|
|
Patch,
|
|
Put,
|
|
Head,
|
|
Connect,
|
|
Options,
|
|
Trace,
|
|
}
|
|
|
|
_method_strings := [?]string{"GET", "POST", "DELETE", "PATCH", "PUT", "HEAD", "CONNECT", "OPTIONS", "TRACE"}
|
|
|
|
method_string :: proc(m: Method) -> string #no_bounds_check {
|
|
if m < .Get || m > .Trace do return ""
|
|
return _method_strings[m]
|
|
}
|
|
|
|
method_parse :: proc(m: string) -> (method: Method, ok: bool) #no_bounds_check {
|
|
// PERF: I assume this is faster than a map with this amount of items.
|
|
|
|
for r in Method {
|
|
if _method_strings[r] == m {
|
|
return r, true
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// Parses the header and adds it to the headers if valid. The given string is copied.
|
|
header_parse :: proc(headers: ^Headers, line: string, allocator := context.temp_allocator) -> (key: string, ok: bool) {
|
|
// Preceding spaces should not be allowed.
|
|
(len(line) > 0 && line[0] != ' ') or_return
|
|
|
|
colon := strings.index_byte(line, ':')
|
|
(colon > 0) or_return
|
|
|
|
// There must not be a space before the colon.
|
|
(line[colon - 1] != ' ') or_return
|
|
|
|
// TODO/PERF: only actually relevant/needed if the key is one of these.
|
|
has_host := headers_has_unsafe(headers^, "host")
|
|
cl, has_cl := headers_get_unsafe(headers^, "content-length")
|
|
|
|
value := strings.clone(strings.trim_space(line[colon + 1:]), allocator)
|
|
key = headers_set(headers, line[:colon], value)
|
|
|
|
// RFC 7230 5.4: Server MUST respond with 400 to any request
|
|
// with multiple "Host" header fields.
|
|
if key == "host" && has_host {
|
|
return
|
|
}
|
|
|
|
// RFC 7230 3.3.3: If a message is received without Transfer-Encoding and with
|
|
// either multiple Content-Length header fields having differing
|
|
// field-values or a single Content-Length header field having an
|
|
// invalid value, then the message framing is invalid and the
|
|
// recipient MUST treat it as an unrecoverable error.
|
|
if key == "content-length" && has_cl && cl != value {
|
|
return
|
|
}
|
|
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
// Returns if this is a valid trailer header.
|
|
//
|
|
// RFC 7230 4.1.2:
|
|
// A sender MUST NOT generate a trailer that contains a field necessary
|
|
// for message framing (e.g., Transfer-Encoding and Content-Length),
|
|
// routing (e.g., Host), request modifiers (e.g., controls and
|
|
// conditionals in Section 5 of [RFC7231]), authentication (e.g., see
|
|
// [RFC7235] and [RFC6265]), response control data (e.g., see Section
|
|
// 7.1 of [RFC7231]), or determining how to process the payload (e.g.,
|
|
// Content-Encoding, Content-Type, Content-Range, and Trailer).
|
|
header_allowed_trailer :: proc(key: string) -> bool {
|
|
// odinfmt:disable
|
|
return (
|
|
// Message framing:
|
|
key != "transfer-encoding" &&
|
|
key != "content-length" &&
|
|
// Routing:
|
|
key != "host" &&
|
|
// Request modifiers:
|
|
key != "if-match" &&
|
|
key != "if-none-match" &&
|
|
key != "if-modified-since" &&
|
|
key != "if-unmodified-since" &&
|
|
key != "if-range" &&
|
|
// Authentication:
|
|
key != "www-authenticate" &&
|
|
key != "authorization" &&
|
|
key != "proxy-authenticate" &&
|
|
key != "proxy-authorization" &&
|
|
key != "cookie" &&
|
|
key != "set-cookie" &&
|
|
// Control data:
|
|
key != "age" &&
|
|
key != "cache-control" &&
|
|
key != "expires" &&
|
|
key != "date" &&
|
|
key != "location" &&
|
|
key != "retry-after" &&
|
|
key != "vary" &&
|
|
key != "warning" &&
|
|
// How to process:
|
|
key != "content-encoding" &&
|
|
key != "content-type" &&
|
|
key != "content-range" &&
|
|
key != "trailer")
|
|
// odinfmt:enable
|
|
}
|
|
|
|
@(private)
|
|
DATE_LENGTH :: len("Fri, 05 Feb 2023 09:01:10 GMT")
|
|
|
|
// Formats a time in the HTTP header format (no timezone conversion is done, GMT expected):
|
|
// `<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT`
|
|
date_write :: proc(w: io.Writer, t: time.Time) -> io.Error {
|
|
year, month, day := time.date(t)
|
|
hour, minute, second := time.clock_from_time(t)
|
|
wday := time.weekday(t)
|
|
|
|
// odinfmt:disable
|
|
io.write_string(w, DAYS[wday]) or_return // 'Fri, '
|
|
write_padded_int(w, day) or_return // 'Fri, 05'
|
|
io.write_string(w, MONTHS[month]) or_return // 'Fri, 05 Feb '
|
|
io.write_int(w, year) or_return // 'Fri, 05 Feb 2023'
|
|
io.write_byte(w, ' ') or_return // 'Fri, 05 Feb 2023 '
|
|
write_padded_int(w, hour) or_return // 'Fri, 05 Feb 2023 09'
|
|
io.write_byte(w, ':') or_return // 'Fri, 05 Feb 2023 09:'
|
|
write_padded_int(w, minute) or_return // 'Fri, 05 Feb 2023 09:01'
|
|
io.write_byte(w, ':') or_return // 'Fri, 05 Feb 2023 09:01:'
|
|
write_padded_int(w, second) or_return // 'Fri, 05 Feb 2023 09:01:10'
|
|
io.write_string(w, " GMT") or_return // 'Fri, 05 Feb 2023 09:01:10 GMT'
|
|
// odinfmt:enable
|
|
|
|
return nil
|
|
}
|
|
|
|
// Formats a time in the HTTP header format (no timezone conversion is done, GMT expected):
|
|
// `<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT`
|
|
date_string :: proc(t: time.Time, allocator := context.allocator) -> string {
|
|
b: strings.Builder
|
|
|
|
buf := make([]byte, DATE_LENGTH, allocator)
|
|
b.buf = slice.into_dynamic(buf)
|
|
|
|
date_write(strings.to_writer(&b), t)
|
|
|
|
return strings.to_string(b)
|
|
}
|
|
|
|
date_parse :: proc(value: string) -> (t: time.Time, ok: bool) #no_bounds_check {
|
|
if len(value) != DATE_LENGTH do return
|
|
|
|
// Remove 'Fri, '
|
|
value := value
|
|
value = value[5:]
|
|
|
|
// Parse '05'
|
|
day := strconv.parse_i64_of_base(value[:2], 10) or_return
|
|
value = value[2:]
|
|
|
|
// Parse ' Feb ' or '-Feb-' (latter is a deprecated format but should still be parsed).
|
|
month_index := -1
|
|
month_str := value[1:4]
|
|
value = value[5:]
|
|
for month, i in MONTHS[1:] {
|
|
if month_str == month[1:4] {
|
|
month_index = i
|
|
break
|
|
}
|
|
}
|
|
month_index += 1
|
|
if month_index <= 0 do return
|
|
|
|
year := strconv.parse_i64_of_base(value[:4], 10) or_return
|
|
value = value[4:]
|
|
|
|
hour := strconv.parse_i64_of_base(value[1:3], 10) or_return
|
|
value = value[4:]
|
|
|
|
minute := strconv.parse_i64_of_base(value[:2], 10) or_return
|
|
value = value[3:]
|
|
|
|
seconds := strconv.parse_i64_of_base(value[:2], 10) or_return
|
|
value = value[3:]
|
|
|
|
// Should have only 'GMT' left now.
|
|
if value != "GMT" do return
|
|
|
|
t = time.datetime_to_time(int(year), int(month_index), int(day), int(hour), int(minute), int(seconds)) or_return
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
request_path_write :: proc(w: io.Writer, target: URL) -> io.Error {
|
|
// TODO: maybe net.percent_encode.
|
|
|
|
if target.path == "" {
|
|
io.write_byte(w, '/') or_return
|
|
} else {
|
|
io.write_string(w, target.path) or_return
|
|
}
|
|
|
|
if len(target.query) > 0 {
|
|
io.write_byte(w, '?') or_return
|
|
io.write_string(w, target.query) or_return
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
request_path :: proc(target: URL, allocator := context.allocator) -> (rq_path: string) {
|
|
res := strings.builder_make(0, len(target.path), allocator)
|
|
request_path_write(strings.to_writer(&res), target)
|
|
return strings.to_string(res)
|
|
}
|
|
|
|
_dynamic_unwritten :: proc(d: [dynamic]$E) -> []E {
|
|
return (cast([^]E)raw_data(d))[len(d):cap(d)]
|
|
}
|
|
|
|
_dynamic_add_len :: proc(d: ^[dynamic]$E, len: int) {
|
|
(transmute(^runtime.Raw_Dynamic_Array)d).len += len
|
|
}
|
|
|
|
@(private)
|
|
write_padded_int :: proc(w: io.Writer, i: int) -> io.Error {
|
|
if i < 10 {
|
|
io.write_string(w, PADDED_NUMS[i]) or_return
|
|
return nil
|
|
}
|
|
|
|
_, err := io.write_int(w, i)
|
|
return err
|
|
}
|
|
|
|
@(private)
|
|
write_escaped_newlines :: proc(w: io.Writer, v: string) -> io.Error {
|
|
for c in v {
|
|
if c == '\n' {
|
|
io.write_string(w, "\\n") or_return
|
|
} else {
|
|
io.write_rune(w, c) or_return
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
@(private)
|
|
PADDED_NUMS := [10]string{"00", "01", "02", "03", "04", "05", "06", "07", "08", "09"}
|
|
|
|
@(private)
|
|
DAYS := [7]string{"Sun, ", "Mon, ", "Tue, ", "Wed, ", "Thu, ", "Fri, ", "Sat, "}
|
|
|
|
@(private)
|
|
MONTHS := [13]string {
|
|
" ", // Jan is 1, so 0 should never be accessed.
|
|
" Jan ",
|
|
" Feb ",
|
|
" Mar ",
|
|
" Apr ",
|
|
" May ",
|
|
" Jun ",
|
|
" Jul ",
|
|
" Aug ",
|
|
" Sep ",
|
|
" Oct ",
|
|
" Nov ",
|
|
" Dec ",
|
|
}
|
|
|
|
@(private)
|
|
Atomic :: struct($T: typeid) {
|
|
raw: T,
|
|
}
|
|
|
|
@(private)
|
|
atomic_store :: #force_inline proc(a: ^Atomic($T), val: T) {
|
|
sync.atomic_store(&a.raw, val)
|
|
}
|
|
|
|
@(private)
|
|
atomic_load :: #force_inline proc(a: ^Atomic($T)) -> T {
|
|
return sync.atomic_load(&a.raw)
|
|
}
|
|
|
|
import "core:testing"
|
|
|
|
@(test)
|
|
test_dynamic_unwritten :: proc(t: ^testing.T) {
|
|
{
|
|
d := make([dynamic]int, 4, 8)
|
|
du := _dynamic_unwritten(d)
|
|
|
|
testing.expect(t, len(du) == 4)
|
|
}
|
|
|
|
{
|
|
d := slice.into_dynamic([]int{1, 2, 3, 4, 5})
|
|
_dynamic_add_len(&d, 3)
|
|
du := _dynamic_unwritten(d)
|
|
|
|
testing.expect(t, len(d) == 3)
|
|
testing.expect(t, len(du) == 2)
|
|
testing.expect(t, du[0] == 4)
|
|
testing.expect(t, du[1] == 5)
|
|
}
|
|
|
|
{
|
|
d := slice.into_dynamic([]int{})
|
|
du := _dynamic_unwritten(d)
|
|
|
|
testing.expect(t, len(du) == 0)
|
|
}
|
|
}
|
|
|