Skip to content

Implement RFC 9112 2.2-2 #259

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

Open
wants to merge 12 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 .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ jobs:
magic run integration_tests_py
magic run integration_tests_external
magic run integration_tests_udp
magic run rfc_tests
44 changes: 28 additions & 16 deletions lightbug_http/header.mojo
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections import Dict, Optional
from lightbug_http.io.bytes import Bytes, ByteReader, ByteWriter, is_newline, is_space
from lightbug_http.io.bytes import Bytes, ByteReader, ByteWriter, is_newline, is_space, ByteView, bytes_equal_ignore_case, bytes_to_lower_string
from lightbug_http.strings import BytesConstant
from lightbug_http._logger import logger
from lightbug_http.strings import rChar, nChar, lineBreak, to_string
Expand Down Expand Up @@ -38,21 +38,24 @@ fn write_header[T: Writer](mut writer: T, key: String, value: String):


@value
struct Headers(Writable, Stringable):
struct Headers[origin: Origin](Writable, Stringable):
"""Represents the header key/values in an http request/response.

Header keys are normalized to lowercase
Header keys are normalized to lowercase and stored as strings,
while values are stored as bytes to comply with RFC requirements.
"""

var _inner: Dict[String, String]
var _inner: Dict[String, Bytes]

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

fn __init__(out self, owned *headers: Header):
self._inner = Dict[String, String]()
self._inner = Dict[String, Bytes]()
for header in headers:
self[header[].key.lower()] = header[].value
var key_lower = header[].key.lower()
var value_bytes = Bytes(header[].value.as_bytes())
self._inner[key_lower] = value_bytes

@always_inline
fn empty(self) -> Bool:
Expand All @@ -65,25 +68,30 @@ struct Headers(Writable, Stringable):
@always_inline
fn __getitem__(self, key: String) raises -> String:
try:
return self._inner[key.lower()]
var value_bytes = self._inner[key.lower()]
return to_string(value_bytes)
except:
raise Error("KeyError: Key not found in headers: " + key)

@always_inline
fn get(self, key: String) -> Optional[String]:
return self._inner.get(key.lower())
var value_opt = self._inner.get(key.lower())
if value_opt:
return to_string(value_opt.value())
return None

@always_inline
fn __setitem__(mut self, key: String, value: String):
self._inner[key.lower()] = value
var value_bytes = Bytes(value.as_bytes())
self._inner[key.lower()] = value_bytes

fn content_length(self) -> Int:
try:
return Int(self[HeaderKey.CONTENT_LENGTH])
except:
return 0

fn parse_raw(mut self, mut r: ByteReader) raises -> (String, String, String, List[String]):
fn parse_raw[origin: Origin](mut self, mut r: ByteReader[origin]) raises -> (ByteView[origin], ByteView[origin], ByteView[origin], List[String]):
var first_byte = r.peek()
if not first_byte:
raise Error("Headers.parse_raw: Failed to read first byte from response header")
Expand All @@ -102,17 +110,21 @@ struct Headers(Writable, Stringable):
r.increment()
# TODO (bgreni): Handle possible trailing whitespace
var value = r.read_line()
var k = String(key).lower()
if k == HeaderKey.SET_COOKIE:

if bytes_equal_ignore_case(key, HeaderKey.SET_COOKIE):
cookies.append(String(value))
continue

self._inner[k] = String(value)
return (String(first), String(second), String(third), cookies)
var key_str = bytes_to_lower_string(key)
var value_bytes = value.to_bytes()
self._inner[key_str] = value_bytes

return (first, second, third, cookies)

fn write_to[T: Writer, //](self, mut writer: T):
for header in self._inner.items():
write_header(writer, header[].key, header[].value)
var value_str = to_string(header[].value)
write_header(writer, header[].key, value_str)

fn __str__(self) -> String:
return String.write(self)
42 changes: 26 additions & 16 deletions lightbug_http/http/request.mojo
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from memory import Span
from lightbug_http.io.bytes import Bytes, bytes, ByteReader, ByteWriter
from lightbug_http.io.bytes import Bytes, bytes, ByteReader, ByteWriter, ByteView
from lightbug_http.header import Headers, HeaderKey, Header, write_header
from lightbug_http.cookie import RequestCookieJar
from lightbug_http.uri import URI
Expand All @@ -14,6 +14,7 @@ from lightbug_http.strings import (
nChar,
lineBreak,
to_string,
to_bytes,
)


Expand All @@ -30,29 +31,30 @@ struct RequestMethod:
alias options = RequestMethod("OPTIONS")


@value
struct HTTPRequest(Writable, Stringable):
var headers: Headers
struct HTTPRequest[origin: Origin](Writable, Stringable):
var headers: Headers[origin]
var cookies: RequestCookieJar
var uri: URI
var body_raw: Bytes

var method: String
var protocol: String
var method: Bytes
var protocol: Bytes

var server_is_tls: Bool
var timeout: Duration

@staticmethod
fn from_bytes(addr: String, max_body_size: Int, b: Span[Byte]) raises -> HTTPRequest:
fn from_bytes(addr: String, max_body_size: Int, b: Span[Byte]) raises -> HTTPRequest[origin]:
var reader = ByteReader(b)
var headers = Headers()
var method: String
var protocol: String
var uri: String
var headers = Headers[origin]()
var method: Bytes
var protocol: Bytes
var uri: Bytes
try:
var rest = headers.parse_raw(reader)
method, uri, protocol = rest[0], rest[1], rest[2]
var method = rest[0]
var uri = rest[1]
var protocol = rest[2]
except e:
raise Error("HTTPRequest.from_bytes: Failed to parse request headers: " + String(e))

Expand All @@ -67,7 +69,7 @@ struct HTTPRequest(Writable, Stringable):
raise Error("HTTPRequest.from_bytes: Request body too large.")

var request = HTTPRequest(
URI.parse(addr + uri), headers=headers, method=method, protocol=protocol, cookies=cookies
URI.parse(addr + to_string(uri)), headers=headers, method=to_string(method), protocol=to_string(protocol), cookies=cookies
)

if content_length > 0:
Expand All @@ -82,7 +84,7 @@ struct HTTPRequest(Writable, Stringable):
fn __init__(
out self,
uri: URI,
headers: Headers = Headers(),
headers: Headers[origin] = Headers[origin](),
cookies: RequestCookieJar = RequestCookieJar(),
method: String = "GET",
protocol: String = strHttp11,
Expand All @@ -92,8 +94,8 @@ struct HTTPRequest(Writable, Stringable):
):
self.headers = headers
self.cookies = cookies
self.method = method
self.protocol = protocol
self.method = to_bytes(method)
self.protocol = to_bytes(protocol)
self.uri = uri
self.body_raw = body
self.server_is_tls = server_is_tls
Expand All @@ -108,6 +110,14 @@ struct HTTPRequest(Writable, Stringable):
else:
self.headers[HeaderKey.HOST] = uri.host

fn __copyinit__(out self, existing: HTTPRequest[origin]):
self.headers = existing.headers
self.cookies = existing.cookies
self.uri = existing.uri
self.body_raw = existing.body_raw
self.method = existing.method
self.protocol = existing.protocol

fn get_body(self) -> StringSlice[__origin_of(self.body_raw)]:
return StringSlice(unsafe_from_utf8=Span(self.body_raw))

Expand Down
4 changes: 2 additions & 2 deletions lightbug_http/http/response.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ struct HTTPResponse(Writable, Stringable):

try:
var properties = headers.parse_raw(reader)
protocol, status_code, status_text = properties[0], properties[1], properties[2]
protocol, status_code, status_text = String(properties[0]), String(properties[1]), String(properties[2])
cookies.from_headers(properties[3])
reader.skip_carriage_return()
except e:
Expand Down Expand Up @@ -76,7 +76,7 @@ struct HTTPResponse(Writable, Stringable):

try:
var properties = headers.parse_raw(reader)
protocol, status_code, status_text = properties[0], properties[1], properties[2]
protocol, status_code, status_text = String(properties[0]), String(properties[1]), String(properties[2])
cookies.from_headers(properties[3])
reader.skip_carriage_return()
except e:
Expand Down
31 changes: 31 additions & 0 deletions lightbug_http/io/bytes.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,37 @@ fn is_space(b: Byte) -> Bool:
return b == BytesConstant.whitespace


fn bytes_equal_ignore_case(a: ByteView, b: String) -> Bool:
"""Compare ByteView with String case-insensitively without creating intermediate strings."""
if len(a) != len(b):
return False

for i in range(len(a)):
var byte_a = a[i]
var byte_b = ord(b[i])

# Convert to lowercase for comparison
if byte_a >= ord('A') and byte_a <= ord('Z'):
byte_a = byte_a + 32 # Convert to lowercase
if byte_b >= ord('A') and byte_b <= ord('Z'):
byte_b = byte_b + 32 # Convert to lowercase

if byte_a != byte_b:
return False
return True


fn bytes_to_lower_string(b: ByteView) -> String:
"""Convert ByteView to lowercase String."""
var result = Bytes()
for i in range(len(b)):
var byte_val = b[i]
if byte_val >= ord('A') and byte_val <= ord('Z'):
byte_val = byte_val + 32 # Convert to lowercase
result.append(byte_val)
return to_string(result^)


struct ByteWriter(Writer):
var _inner: Bytes

Expand Down
4 changes: 4 additions & 0 deletions lightbug_http/strings.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ fn to_string(owned bytes: Bytes) -> String:
return result^


fn to_bytes(s: String) -> Bytes:
return Bytes(s.as_bytes())


fn find_all(s: String, sub_str: String) -> List[Int]:
match_idxs = List[Int]()
var current_idx: Int = s.find(sub_str)
Expand Down
Loading
Loading