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 // io.write_byte(w, ' ') or_return // switch t in rline.target { case string: io.write_string(w, t) or_return // case URL: request_path_write(w, t) or_return // } io.write_byte(w, ' ') or_return // version_write(w, rline.version) or_return // io.write_string(w, "\r\n") or_return // // 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): // `, :: 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): // `, :: 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) } }