From 9a7c8d7625fcd327a6998fd5b7e709c4acb44a68 Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Mon, 20 Jan 2025 20:30:43 -0600 Subject: [PATCH 01/13] wip --- lightbug_http/client.mojo | 11 ----------- lightbug_http/net.mojo | 15 +++++++++++++++ lightbug_http/socket.mojo | 10 ++++------ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index 2f655199..ebb18a63 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -1,16 +1,5 @@ from collections import Dict from memory import UnsafePointer -from lightbug_http.libc import ( - c_int, - AF_INET, - SOCK_STREAM, - socket, - connect, - send, - recv, - close, -) -from lightbug_http.strings import to_string from lightbug_http.net import default_buffer_size from lightbug_http.http import HTTPRequest, HTTPResponse, encode from lightbug_http.header import Headers, HeaderKey diff --git a/lightbug_http/net.mojo b/lightbug_http/net.mojo index 9b89d26e..e32cb15d 100644 --- a/lightbug_http/net.mojo +++ b/lightbug_http/net.mojo @@ -114,6 +114,15 @@ struct NoTLSListener: fn __moveinit__(out self, owned existing: Self): self.socket = existing.socket^ + fn __enter__(owned self) -> Self: + return self^ + + fn __del__(owned self): + try: + self.socket.teardown() + except e: + logger.debug("NoTLSListener.__del__: Failed to close socket on deletion:", e) + fn accept(self) raises -> TCPConnection: return TCPConnection(self.socket.accept()) @@ -199,6 +208,12 @@ struct TCPConnection: fn __moveinit__(out self, owned existing: Self): self.socket = existing.socket^ + fn __del__(owned self): + try: + self.socket.teardown() + except e: + logger.debug("TCPConnection.__del__: Failed to close socket on deletion:", e) + fn read(self, mut buf: Bytes) raises -> Int: try: return self.socket.receive(buf) diff --git a/lightbug_http/socket.mojo b/lightbug_http/socket.mojo index 92c7956c..31aefb2d 100644 --- a/lightbug_http/socket.mojo +++ b/lightbug_http/socket.mojo @@ -168,11 +168,9 @@ struct Socket[AddrType: Addr, address_family: Int = AF_INET](Representable, Stri logger.error("Socket.teardown: Failed to close socket.") raise e - fn __enter__(owned self) -> Self: - return self^ - - fn __exit__(mut self) raises: - self.teardown() + # TODO: Removed until we can determine why __del__ bugs out in the client flow, but not server flow? + # fn __enter__(owned self) -> Self: + # return self^ # TODO: Seems to be bugged if this is included. Mojo tries to delete a mystical 0 fd socket that was never initialized? # fn __del__(owned self): @@ -181,7 +179,7 @@ struct Socket[AddrType: Addr, address_family: Int = AF_INET](Representable, Stri # try: # self.teardown() # except e: - # logger.debug("Socket.__del__: Failed to close socket during deletion:", str(e)) + # logger.debug("Socket.__del__: Failed to close socket during deletion:", e) fn __str__(self) -> String: return String.write(self) From b93d808936b805081aa8bfc86decef0435a40d54 Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Wed, 22 Jan 2025 22:48:52 -0600 Subject: [PATCH 02/13] fix socket bug, enable __del__ and __enter__, and switch poolmanager to use tuple of host, port, and scheme --- lightbug_http/client.mojo | 91 +++++++++++++++------------- lightbug_http/pool_manager.mojo | 102 +++++++++++++++++++++++++------- lightbug_http/socket.mojo | 23 ++++--- 3 files changed, 145 insertions(+), 71 deletions(-) diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index ebb18a63..bc2d3013 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -1,4 +1,5 @@ from collections import Dict +from utils import StringSlice from memory import UnsafePointer from lightbug_http.net import default_buffer_size from lightbug_http.http import HTTPRequest, HTTPResponse, encode @@ -6,7 +7,29 @@ from lightbug_http.header import Headers, HeaderKey from lightbug_http.net import create_connection, TCPConnection from lightbug_http.io.bytes import Bytes from lightbug_http.utils import ByteReader, logger -from lightbug_http.pool_manager import PoolManager +from lightbug_http.pool_manager import PoolManager, Scheme, PoolKey + + +fn parse_host_and_port(source: String, is_tls: Bool) raises -> (String, UInt16): + """Parses the host and port from a given string. + + Args: + source: The host uri to parse. + is_tls: A boolean indicating whether the connection is secure. + + Returns: + A tuple containing the host and port. + """ + var port: UInt16 + if source.count(":") != 1: + port = 443 if is_tls else 80 + return source, port + + var host: String + var reader = ByteReader(source.as_bytes()) + host = StringSlice(unsafe_from_utf8=reader.read_until(ord(":"))) + port = atol(StringSlice(unsafe_from_utf8=reader.read_bytes()[1:])) + return host^, port struct Client: @@ -30,7 +53,7 @@ struct Client: self.allow_redirects = allow_redirects self._connections = PoolManager[TCPConnection](cached_connections) - fn do(mut self, owned req: HTTPRequest) raises -> HTTPResponse: + fn do(mut self, owned request: HTTPRequest) raises -> HTTPResponse: """The `do` method is responsible for sending an HTTP request to a server and receiving the corresponding response. It performs the following steps: @@ -43,7 +66,7 @@ struct Client: Note: The code assumes that the `HTTPRequest` object passed as an argument has a valid URI with a host and port specified. Args: - req: An `HTTPRequest` object representing the request to be sent. + request: An `HTTPRequest` object representing the request to be sent. Returns: The received response. @@ -51,73 +74,59 @@ struct Client: Raises: Error: If there is a failure in sending or receiving the message. """ - if req.uri.host == "": + if request.uri.host == "": raise Error("Client.do: Request failed because the host field is empty.") - var is_tls = False - if req.uri.is_https(): + var is_tls = False + var scheme = Scheme.HTTP + if request.uri.is_https(): is_tls = True + scheme = Scheme.HTTPS - var host_str: String - var port: Int - if ":" in req.uri.host: - var host_port: List[String] - try: - host_port = req.uri.host.split(":") - except: - raise Error("Client.do: Failed to split host and port.") - host_str = host_port[0] - port = atol(host_port[1]) - else: - host_str = req.uri.host - if is_tls: - port = 443 - else: - port = 80 - + host, port = parse_host_and_port(request.uri.host, is_tls) + var pool_key = PoolKey(host, port, scheme) var cached_connection = False var conn: TCPConnection try: - conn = self._connections.take(host_str) + conn = self._connections.take(pool_key) cached_connection = True except e: if str(e) == "PoolManager.take: Key not found.": - conn = create_connection(host_str, port) + conn = create_connection(host, port) else: logger.error(e) raise Error("Client.do: Failed to create a connection to host.") var bytes_sent: Int try: - bytes_sent = conn.write(encode(req)) + bytes_sent = conn.write(encode(request)) except e: # Maybe peer reset ungracefully, so try a fresh connection if str(e) == "SendError: Connection reset by peer.": logger.debug("Client.do: Connection reset by peer. Trying a fresh connection.") conn.teardown() if cached_connection: - return self.do(req^) + return self.do(request^) logger.error("Client.do: Failed to send message.") raise e # TODO: What if the response is too large for the buffer? We should read until the end of the response. (@thatstoasty) var new_buf = Bytes(capacity=default_buffer_size) - try: _ = conn.read(new_buf) except e: if str(e) == "EOF": conn.teardown() if cached_connection: - return self.do(req^) + return self.do(request^) raise Error("Client.do: No response received from the server.") else: logger.error(e) raise Error("Client.do: Failed to read response from peer.") - var res: HTTPResponse + var response: HTTPResponse try: - res = HTTPResponse.from_bytes(new_buf, conn) + response = HTTPResponse.from_bytes(new_buf, conn) except e: logger.error("Failed to parse a response...") try: @@ -127,19 +136,19 @@ struct Client: raise e # Redirects should not keep the connection alive, as redirects can send the client to a different server. - if self.allow_redirects and res.is_redirect(): + if self.allow_redirects and response.is_redirect(): conn.teardown() - return self._handle_redirect(req^, res^) + return self._handle_redirect(request^, response^) # Server told the client to close the connection, we can assume the server closed their side after sending the response. - elif res.connection_close(): + elif response.connection_close(): conn.teardown() # Otherwise, persist the connection by giving it back to the pool manager. else: - self._connections.give(host_str, conn^) - return res + self._connections.give(pool_key, conn^) + return response fn _handle_redirect( - mut self, owned original_req: HTTPRequest, owned original_response: HTTPResponse + mut self, owned original_request: HTTPRequest, owned original_response: HTTPResponse ) raises -> HTTPResponse: var new_uri: URI var new_location: String @@ -150,9 +159,9 @@ struct Client: if new_location and new_location.startswith("http"): new_uri = URI.parse(new_location) - original_req.headers[HeaderKey.HOST] = new_uri.host + original_request.headers[HeaderKey.HOST] = new_uri.host else: - new_uri = original_req.uri + new_uri = original_request.uri new_uri.path = new_location - original_req.uri = new_uri - return self.do(original_req^) + original_request.uri = new_uri + return self.do(original_request^) diff --git a/lightbug_http/pool_manager.mojo b/lightbug_http/pool_manager.mojo index a83072ef..d09cdbed 100644 --- a/lightbug_http/pool_manager.mojo +++ b/lightbug_http/pool_manager.mojo @@ -9,15 +9,76 @@ from lightbug_http.utils import logger from lightbug_http.owning_list import OwningList +@value +struct Scheme(Hashable, EqualityComparable, Representable, Stringable, Writable): + var value: String + alias HTTP = Self("http") + alias HTTPS = Self("https") + + fn __hash__(self) -> UInt: + return hash(self.value) + + fn __eq__(self, other: Self) -> Bool: + return self.value == other.value + + fn __ne__(self, other: Self) -> Bool: + return self.value != other.value + + fn write_to[W: Writer, //](self, mut writer: W) -> None: + writer.write("Scheme(value=", repr(self.value), ")") + + fn __repr__(self) -> String: + return String.write(self) + + fn __str__(self) -> String: + return self.value.upper() + + +@value +struct PoolKey(Hashable, KeyElement): + var host: String + var port: UInt16 + var scheme: Scheme + + fn __init__(out self, host: String, port: UInt16, scheme: Scheme): + self.host = host + self.port = port + self.scheme = scheme + + fn __hash__(self) -> UInt: + # TODO: Very rudimentary hash. We probably need to actually have an actual hash function here. + # Since Tuple doesn't have one. + return hash(self.host) + hash(self.port) + hash(self.scheme) + + fn __eq__(self, other: Self) -> Bool: + return self.host == other.host and self.port == other.port and self.scheme == other.scheme + + fn __ne__(self, other: Self) -> Bool: + return self.host != other.host or self.port != other.port or self.scheme != other.scheme + + fn __str__(self) -> String: + var result = String() + result.write(self.scheme.value, "://", self.host, ":", str(self.port)) + return result + + fn __repr__(self) -> String: + return String.write(self) + + fn write_to[W: Writer, //](self, mut writer: W) -> None: + writer.write( + "PoolKey(", "scheme=", repr(self.scheme.value), ", host=", repr(self.host), ", port=", str(self.port), ")" + ) + + struct PoolManager[ConnectionType: Connection](): var _connections: OwningList[ConnectionType] var _capacity: Int - var mapping: Dict[String, Int] + var mapping: Dict[PoolKey, Int] fn __init__(out self, capacity: Int = 10): self._connections = OwningList[ConnectionType](capacity=capacity) self._capacity = capacity - self.mapping = Dict[String, Int]() + self.mapping = Dict[PoolKey, Int]() fn __del__(owned self): logger.debug( @@ -25,24 +86,23 @@ struct PoolManager[ConnectionType: Connection](): ) self.clear() - fn give(mut self, host: String, owned value: ConnectionType) raises: - if host in self.mapping: - self._connections[self.mapping[host]] = value^ + fn give(mut self, key: PoolKey, owned value: ConnectionType) raises: + if key in self.mapping: + self._connections[self.mapping[key]] = value^ return if self._connections.size == self._capacity: raise Error("PoolManager.give: Cache is full.") - self._connections[self._connections.size] = value^ - self.mapping[host] = self._connections.size - self._connections.size += 1 - logger.debug("Checked in connection for peer:", host + ", at index:", self._connections.size) + self._connections.append(value^) + self.mapping[key] = self._connections.size - 1 + logger.debug("Checked in connection for peer:", str(key) + ", at index:", self._connections.size) - fn take(mut self, host: String) raises -> ConnectionType: + fn take(mut self, key: PoolKey) raises -> ConnectionType: var index: Int try: - index = self.mapping[host] - _ = self.mapping.pop(host) + index = self.mapping[key] + _ = self.mapping.pop(key) except: raise Error("PoolManager.take: Key not found.") @@ -52,7 +112,7 @@ struct PoolManager[ConnectionType: Connection](): if kv[].value > index: self.mapping[kv[].key] -= 1 - logger.debug("Checked out connection for peer:", host + ", from index:", self._connections.size + 1) + logger.debug("Checked out connection for peer:", str(key) + ", from index:", self._connections.size + 1) return connection^ fn clear(mut self): @@ -65,14 +125,14 @@ struct PoolManager[ConnectionType: Connection](): logger.error("Failed to tear down connection. Error:", e) self.mapping.clear() - fn __contains__(self, host: String) -> Bool: - return host in self.mapping + fn __contains__(self, key: PoolKey) -> Bool: + return key in self.mapping - fn __setitem__(mut self, host: String, owned value: ConnectionType) raises -> None: - if host in self.mapping: - self._connections[self.mapping[host]] = value^ + fn __setitem__(mut self, key: PoolKey, owned value: ConnectionType) raises -> None: + if key in self.mapping: + self._connections[self.mapping[key]] = value^ else: - self.give(host, value^) + self.give(key, value^) - fn __getitem__(self, host: String) raises -> ref [self._connections] ConnectionType: - return self._connections[self.mapping[host]] + fn __getitem__(self, key: PoolKey) raises -> ref [self._connections] ConnectionType: + return self._connections[self.mapping[key]] diff --git a/lightbug_http/socket.mojo b/lightbug_http/socket.mojo index 31aefb2d..68b4ea74 100644 --- a/lightbug_http/socket.mojo +++ b/lightbug_http/socket.mojo @@ -148,10 +148,16 @@ struct Socket[AddrType: Addr, address_family: Int = AF_INET](Representable, Stri self.fd = existing.fd self.socket_type = existing.socket_type self.protocol = existing.protocol + self._local_address = existing._local_address^ + existing._local_address = AddrType() self._remote_address = existing._remote_address^ + existing._remote_address = AddrType() + self._closed = existing._closed + existing._closed = True self._connected = existing._connected + existing._connected = False fn teardown(mut self) raises: """Close the socket and free the file descriptor.""" @@ -169,17 +175,16 @@ struct Socket[AddrType: Addr, address_family: Int = AF_INET](Representable, Stri raise e # TODO: Removed until we can determine why __del__ bugs out in the client flow, but not server flow? - # fn __enter__(owned self) -> Self: - # return self^ + fn __enter__(owned self) -> Self: + return self^ # TODO: Seems to be bugged if this is included. Mojo tries to delete a mystical 0 fd socket that was never initialized? - # fn __del__(owned self): - # """Close the socket when the object is deleted.""" - # logger.info("In socket del", self) - # try: - # self.teardown() - # except e: - # logger.debug("Socket.__del__: Failed to close socket during deletion:", e) + fn __del__(owned self): + """Close the socket when the object is deleted.""" + try: + self.teardown() + except e: + logger.debug("Socket.__del__: Failed to close socket during deletion:", e) fn __str__(self) -> String: return String.write(self) From 92f65206cf9c7bf43cd957004a7c71ad98042e2e Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Wed, 22 Jan 2025 22:53:08 -0600 Subject: [PATCH 03/13] fix socket bug, enable __del__ and __enter__, and switch poolmanager to use tuple of host, port, and scheme --- lightbug_http/client.mojo | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index bc2d3013..28240490 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -20,16 +20,12 @@ fn parse_host_and_port(source: String, is_tls: Bool) raises -> (String, UInt16): Returns: A tuple containing the host and port. """ - var port: UInt16 if source.count(":") != 1: - port = 443 if is_tls else 80 + var port: UInt16 = 443 if is_tls else 80 return source, port - var host: String - var reader = ByteReader(source.as_bytes()) - host = StringSlice(unsafe_from_utf8=reader.read_until(ord(":"))) - port = atol(StringSlice(unsafe_from_utf8=reader.read_bytes()[1:])) - return host^, port + var result = source.split(":") + return result[0], UInt16(atol(result[1])) struct Client: From 32ca13af92f998d424e01a46742e9b5d6c7c938e Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Wed, 22 Jan 2025 22:53:49 -0600 Subject: [PATCH 04/13] removed todos --- lightbug_http/socket.mojo | 2 -- 1 file changed, 2 deletions(-) diff --git a/lightbug_http/socket.mojo b/lightbug_http/socket.mojo index 68b4ea74..1b1641ae 100644 --- a/lightbug_http/socket.mojo +++ b/lightbug_http/socket.mojo @@ -174,11 +174,9 @@ struct Socket[AddrType: Addr, address_family: Int = AF_INET](Representable, Stri logger.error("Socket.teardown: Failed to close socket.") raise e - # TODO: Removed until we can determine why __del__ bugs out in the client flow, but not server flow? fn __enter__(owned self) -> Self: return self^ - # TODO: Seems to be bugged if this is included. Mojo tries to delete a mystical 0 fd socket that was never initialized? fn __del__(owned self): """Close the socket when the object is deleted.""" try: From 90e699882b0163dd6e64ce15d314b5fa7f37a4e9 Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Thu, 23 Jan 2025 08:57:27 -0600 Subject: [PATCH 05/13] removed __del__ from connection and listener --- lightbug_http/net.mojo | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/lightbug_http/net.mojo b/lightbug_http/net.mojo index e32cb15d..38ca9aea 100644 --- a/lightbug_http/net.mojo +++ b/lightbug_http/net.mojo @@ -114,15 +114,6 @@ struct NoTLSListener: fn __moveinit__(out self, owned existing: Self): self.socket = existing.socket^ - fn __enter__(owned self) -> Self: - return self^ - - fn __del__(owned self): - try: - self.socket.teardown() - except e: - logger.debug("NoTLSListener.__del__: Failed to close socket on deletion:", e) - fn accept(self) raises -> TCPConnection: return TCPConnection(self.socket.accept()) @@ -156,14 +147,13 @@ struct ListenConfig: logger.error(e) raise Error("ListenConfig.listen: Failed to create listener due to socket creation failure.") - try: - - @parameter - # TODO: do we want to reuse port on linux? currently doesn't work - if os_is_macos(): + @parameter + # TODO: do we want to reuse port on linux? currently doesn't work + if os_is_macos(): + try: socket.set_socket_option(SO_REUSEADDR, 1) - except e: - logger.warn("ListenConfig.listen: Failed to set socket as reusable", e) + except e: + logger.warn("ListenConfig.listen: Failed to set socket as reusable", e) var bind_success = False var bind_fail_logged = False @@ -208,12 +198,6 @@ struct TCPConnection: fn __moveinit__(out self, owned existing: Self): self.socket = existing.socket^ - fn __del__(owned self): - try: - self.socket.teardown() - except e: - logger.debug("TCPConnection.__del__: Failed to close socket on deletion:", e) - fn read(self, mut buf: Bytes) raises -> Int: try: return self.socket.receive(buf) From 34f31bd622a969f39cc221053e093c647c5bbabb Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Thu, 23 Jan 2025 11:44:09 -0600 Subject: [PATCH 06/13] hash of hashes --- lightbug_http/pool_manager.mojo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightbug_http/pool_manager.mojo b/lightbug_http/pool_manager.mojo index d09cdbed..6a2fdefe 100644 --- a/lightbug_http/pool_manager.mojo +++ b/lightbug_http/pool_manager.mojo @@ -48,7 +48,7 @@ struct PoolKey(Hashable, KeyElement): fn __hash__(self) -> UInt: # TODO: Very rudimentary hash. We probably need to actually have an actual hash function here. # Since Tuple doesn't have one. - return hash(self.host) + hash(self.port) + hash(self.scheme) + return hash(hash(self.host) + hash(self.port) + hash(self.scheme)) fn __eq__(self, other: Self) -> Bool: return self.host == other.host and self.port == other.port and self.scheme == other.scheme From 16fcc12a3be96e6bcc11bb84cf71873f848df12a Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Sat, 25 Jan 2025 12:40:23 -0600 Subject: [PATCH 07/13] update byte handling --- benchmark/bench.mojo | 50 ++-- lightbug_http/{libc.mojo => _libc.mojo} | 0 lightbug_http/_logger.mojo | 113 ++++++++ .../{owning_list.mojo => _owning_list.mojo} | 0 lightbug_http/client.mojo | 35 +-- lightbug_http/cookie/request_cookie_jar.mojo | 2 +- lightbug_http/cookie/response_cookie_jar.mojo | 2 +- lightbug_http/header.mojo | 12 +- lightbug_http/http/request.mojo | 14 +- lightbug_http/http/response.mojo | 18 +- lightbug_http/io/bytes.mojo | 257 +++++++++++++++++ lightbug_http/net.mojo | 6 +- lightbug_http/pool_manager.mojo | 30 +- lightbug_http/server.mojo | 4 +- lightbug_http/socket.mojo | 4 +- lightbug_http/uri.mojo | 146 +++++++--- lightbug_http/utils.mojo | 271 ------------------ .../integration/integration_test_client.mojo | 2 +- tests/lightbug_http/{ => http}/test_http.mojo | 34 +-- tests/lightbug_http/http/test_request.mojo | 2 +- .../{ => io}/test_byte_reader.mojo | 25 +- .../{ => io}/test_byte_writer.mojo | 3 +- tests/lightbug_http/test_header.mojo | 3 +- tests/lightbug_http/test_owning_list.mojo | 2 +- tests/lightbug_http/test_uri.mojo | 30 +- 25 files changed, 581 insertions(+), 484 deletions(-) rename lightbug_http/{libc.mojo => _libc.mojo} (100%) create mode 100644 lightbug_http/_logger.mojo rename lightbug_http/{owning_list.mojo => _owning_list.mojo} (100%) delete mode 100644 lightbug_http/utils.mojo rename tests/lightbug_http/{ => http}/test_http.mojo (75%) rename tests/lightbug_http/{ => io}/test_byte_reader.mojo (59%) rename tests/lightbug_http/{ => io}/test_byte_writer.mojo (91%) diff --git a/benchmark/bench.mojo b/benchmark/bench.mojo index accd1ad5..a64e3441 100644 --- a/benchmark/bench.mojo +++ b/benchmark/bench.mojo @@ -2,7 +2,7 @@ from memory import Span from benchmark import * from lightbug_http.io.bytes import bytes, Bytes from lightbug_http.header import Headers, Header -from lightbug_http.utils import ByteReader, ByteWriter +from lightbug_http.io.bytes import ByteReader, ByteWriter from lightbug_http.http import HTTPRequest, HTTPResponse, encode from lightbug_http.uri import URI @@ -11,9 +11,7 @@ alias headers = "GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mo alias body = "I am the body of an HTTP request" * 5 alias body_bytes = bytes(body) alias Request = "GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n" + body -alias Response = "HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type:" - " application/octet-stream\r\nconnection: keep-alive\r\ncontent-length:" - " 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n" + body +alias Response = "HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type: application/octet-stream\r\nconnection: keep-alive\r\ncontent-length: 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n" + body fn main(): @@ -26,24 +24,12 @@ fn run_benchmark(): config.verbose_timing = True config.tabular_view = True var m = Bench(config) - m.bench_function[lightbug_benchmark_header_encode]( - BenchId("HeaderEncode") - ) - m.bench_function[lightbug_benchmark_header_parse]( - BenchId("HeaderParse") - ) - m.bench_function[lightbug_benchmark_request_encode]( - BenchId("RequestEncode") - ) - m.bench_function[lightbug_benchmark_request_parse]( - BenchId("RequestParse") - ) - m.bench_function[lightbug_benchmark_response_encode]( - BenchId("ResponseEncode") - ) - m.bench_function[lightbug_benchmark_response_parse]( - BenchId("ResponseParse") - ) + m.bench_function[lightbug_benchmark_header_encode](BenchId("HeaderEncode")) + m.bench_function[lightbug_benchmark_header_parse](BenchId("HeaderParse")) + m.bench_function[lightbug_benchmark_request_encode](BenchId("RequestEncode")) + m.bench_function[lightbug_benchmark_request_parse](BenchId("RequestParse")) + m.bench_function[lightbug_benchmark_response_encode](BenchId("ResponseEncode")) + m.bench_function[lightbug_benchmark_response_parse](BenchId("ResponseParse")) m.dump_report() except: print("failed to start benchmark") @@ -100,12 +86,15 @@ fn lightbug_benchmark_request_encode(mut b: Bencher): @always_inline @parameter fn request_encode(): - var req = HTTPRequest( - URI.parse("http://127.0.0.1:8080/some-path"), - headers=headers_struct, - body=body_bytes, - ) - _ = encode(req^) + try: + var req = HTTPRequest( + URI.parse("http://127.0.0.1:8080/some-path"), + headers=headers_struct, + body=body_bytes, + ) + _ = encode(req^) + except e: + print("request_encode failed", e) b.iter[request_encode]() @@ -130,8 +119,7 @@ fn lightbug_benchmark_header_parse(mut b: Bencher): var header = Headers() var reader = ByteReader(headers.as_bytes()) _ = header.parse_raw(reader) - except: - print("failed") + except e: + print("failed", e) b.iter[header_parse]() - diff --git a/lightbug_http/libc.mojo b/lightbug_http/_libc.mojo similarity index 100% rename from lightbug_http/libc.mojo rename to lightbug_http/_libc.mojo diff --git a/lightbug_http/_logger.mojo b/lightbug_http/_logger.mojo new file mode 100644 index 00000000..7433df2e --- /dev/null +++ b/lightbug_http/_logger.mojo @@ -0,0 +1,113 @@ +from sys.param_env import env_get_string + + +struct LogLevel: + alias FATAL = 0 + alias ERROR = 1 + alias WARN = 2 + alias INFO = 3 + alias DEBUG = 4 + + +fn get_log_level() -> Int: + """Returns the log level based on the parameter environment variable `LOG_LEVEL`. + + Returns: + The log level. + """ + alias level = env_get_string["LB_LOG_LEVEL", "INFO"]() + if level == "INFO": + return LogLevel.INFO + elif level == "WARN": + return LogLevel.WARN + elif level == "ERROR": + return LogLevel.ERROR + elif level == "DEBUG": + return LogLevel.DEBUG + elif level == "FATAL": + return LogLevel.FATAL + else: + return LogLevel.INFO + + +alias LOG_LEVEL = get_log_level() +"""Logger level determined by the `LB_LOG_LEVEL` param environment variable. + +When building or running the application, you can set `LB_LOG_LEVEL` by providing the the following option: + +```bash +mojo build ... -D LB_LOG_LEVEL=DEBUG +# or +mojo ... -D LB_LOG_LEVEL=DEBUG +``` +""" + + +@value +struct Logger[level: Int]: + alias STDOUT = 1 + alias STDERR = 2 + + fn _log_message[event_level: Int](self, message: String): + @parameter + if level >= event_level: + + @parameter + if event_level < LogLevel.WARN: + # Write to stderr if FATAL or ERROR + print(message, file=Self.STDERR) + else: + print(message) + + fn info[*Ts: Writable](self, *messages: *Ts): + var msg = String.write("\033[36mINFO\033[0m - ") + + @parameter + fn write_message[T: Writable](message: T): + msg.write(message, " ") + + messages.each[write_message]() + self._log_message[LogLevel.INFO](msg) + + fn warn[*Ts: Writable](self, *messages: *Ts): + var msg = String.write("\033[33mWARN\033[0m - ") + + @parameter + fn write_message[T: Writable](message: T): + msg.write(message, " ") + + messages.each[write_message]() + self._log_message[LogLevel.WARN](msg) + + fn error[*Ts: Writable](self, *messages: *Ts): + var msg = String.write("\033[31mERROR\033[0m - ") + + @parameter + fn write_message[T: Writable](message: T): + msg.write(message, " ") + + messages.each[write_message]() + self._log_message[LogLevel.ERROR](msg) + + fn debug[*Ts: Writable](self, *messages: *Ts): + var msg = String.write("\033[34mDEBUG\033[0m - ") + + @parameter + fn write_message[T: Writable](message: T): + msg.write(message, " ") + + messages.each[write_message]() + self._log_message[LogLevel.DEBUG](msg) + + fn fatal[*Ts: Writable](self, *messages: *Ts): + var msg = String.write("\033[35mFATAL\033[0m - ") + + @parameter + fn write_message[T: Writable](message: T): + msg.write(message, " ") + + messages.each[write_message]() + self._log_message[LogLevel.FATAL](msg) + + +alias logger = Logger[LOG_LEVEL]() diff --git a/lightbug_http/owning_list.mojo b/lightbug_http/_owning_list.mojo similarity index 100% rename from lightbug_http/owning_list.mojo rename to lightbug_http/_owning_list.mojo diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index 28240490..8fbe5a1e 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -5,27 +5,10 @@ from lightbug_http.net import default_buffer_size from lightbug_http.http import HTTPRequest, HTTPResponse, encode from lightbug_http.header import Headers, HeaderKey from lightbug_http.net import create_connection, TCPConnection -from lightbug_http.io.bytes import Bytes -from lightbug_http.utils import ByteReader, logger -from lightbug_http.pool_manager import PoolManager, Scheme, PoolKey - - -fn parse_host_and_port(source: String, is_tls: Bool) raises -> (String, UInt16): - """Parses the host and port from a given string. - - Args: - source: The host uri to parse. - is_tls: A boolean indicating whether the connection is secure. - - Returns: - A tuple containing the host and port. - """ - if source.count(":") != 1: - var port: UInt16 = 443 if is_tls else 80 - return source, port - - var result = source.split(":") - return result[0], UInt16(atol(result[1])) +from lightbug_http.io.bytes import Bytes, ByteReader +from lightbug_http._logger import logger +from lightbug_http.pool_manager import PoolManager, PoolKey +from lightbug_http.uri import URI, Scheme struct Client: @@ -71,7 +54,9 @@ struct Client: Error: If there is a failure in sending or receiving the message. """ if request.uri.host == "": - raise Error("Client.do: Request failed because the host field is empty.") + raise Error("Client.do: Host must not be empty.") + if not request.uri.port: + raise Error("Client.do: You must specify the port to connect on.") var is_tls = False var scheme = Scheme.HTTP @@ -79,8 +64,8 @@ struct Client: is_tls = True scheme = Scheme.HTTPS - host, port = parse_host_and_port(request.uri.host, is_tls) - var pool_key = PoolKey(host, port, scheme) + var uri = URI.parse(request.uri.host) + var pool_key = PoolKey(uri.host, uri.port.value(), scheme) var cached_connection = False var conn: TCPConnection try: @@ -88,7 +73,7 @@ struct Client: cached_connection = True except e: if str(e) == "PoolManager.take: Key not found.": - conn = create_connection(host, port) + conn = create_connection(uri.host, uri.port.value()) else: logger.error(e) raise Error("Client.do: Failed to create a connection to host.") diff --git a/lightbug_http/cookie/request_cookie_jar.mojo b/lightbug_http/cookie/request_cookie_jar.mojo index 11e7b0fa..a4d89eef 100644 --- a/lightbug_http/cookie/request_cookie_jar.mojo +++ b/lightbug_http/cookie/request_cookie_jar.mojo @@ -3,7 +3,7 @@ from small_time import SmallTime, TimeZone from small_time.small_time import strptime from lightbug_http.strings import to_string, lineBreak from lightbug_http.header import HeaderKey, write_header -from lightbug_http.utils import ByteReader, ByteWriter, is_newline, is_space +from lightbug_http.io.bytes import ByteReader, ByteWriter, is_newline, is_space @value diff --git a/lightbug_http/cookie/response_cookie_jar.mojo b/lightbug_http/cookie/response_cookie_jar.mojo index 139a2e93..ec437f89 100644 --- a/lightbug_http/cookie/response_cookie_jar.mojo +++ b/lightbug_http/cookie/response_cookie_jar.mojo @@ -1,7 +1,7 @@ from collections import Optional, List, Dict, KeyElement from lightbug_http.strings import to_string from lightbug_http.header import HeaderKey, write_header -from lightbug_http.utils import ByteWriter +from lightbug_http.io.bytes import ByteWriter @value diff --git a/lightbug_http/header.mojo b/lightbug_http/header.mojo index 8c30a212..fc273ed9 100644 --- a/lightbug_http/header.mojo +++ b/lightbug_http/header.mojo @@ -1,8 +1,8 @@ from collections import Dict, Optional from memory import Span -from lightbug_http.io.bytes import Bytes, Byte +from lightbug_http.io.bytes import Bytes, ByteReader, ByteWriter, is_newline, is_space from lightbug_http.strings import BytesConstant -from lightbug_http.utils import ByteReader, ByteWriter, is_newline, is_space, logger +from lightbug_http._logger import logger from lightbug_http.strings import rChar, nChar, lineBreak, to_string @@ -103,13 +103,13 @@ struct Headers(Writable, Stringable): r.increment() # TODO (bgreni): Handle possible trailing whitespace var value = r.read_line() - var k = to_string(key).lower() + var k = str(key).lower() if k == HeaderKey.SET_COOKIE: - cookies.append(to_string(value)) + cookies.append(str(value)) continue - self._inner[k] = to_string(value) - return (to_string(first), to_string(second), to_string(third), cookies) + self._inner[k] = str(value) + return (str(first), str(second), str(third), cookies) fn write_to[T: Writer, //](self, mut writer: T): for header in self._inner.items(): diff --git a/lightbug_http/http/request.mojo b/lightbug_http/http/request.mojo index 83572e94..b6332519 100644 --- a/lightbug_http/http/request.mojo +++ b/lightbug_http/http/request.mojo @@ -1,9 +1,9 @@ from memory import Span -from lightbug_http.io.bytes import Bytes, bytes, Byte +from lightbug_http.io.bytes import Bytes, bytes, ByteReader, ByteWriter from lightbug_http.header import Headers, HeaderKey, Header, write_header from lightbug_http.cookie import RequestCookieJar from lightbug_http.uri import URI -from lightbug_http.utils import ByteReader, ByteWriter, logger +from lightbug_http._logger import logger from lightbug_http.io.sync import Duration from lightbug_http.strings import ( strHttp11, @@ -86,7 +86,11 @@ struct HTTPRequest(Writable, Stringable): if HeaderKey.CONNECTION not in self.headers: self.headers[HeaderKey.CONNECTION] = "keep-alive" if HeaderKey.HOST not in self.headers: - self.headers[HeaderKey.HOST] = uri.host + if uri.port: + var host = String.write(uri.host, ":", str(uri.port.value())) + self.headers[HeaderKey.HOST] = host + else: + self.headers[HeaderKey.HOST] = uri.host fn get_body(self) -> StringSlice[__origin_of(self.body_raw)]: return StringSlice(unsafe_from_utf8=Span(self.body_raw)) @@ -108,7 +112,7 @@ struct HTTPRequest(Writable, Stringable): if content_length > max_body_size: raise Error("Request body too large") - self.body_raw = r.read_bytes(content_length) + self.body_raw = r.read_bytes(content_length).to_bytes() self.set_content_length(content_length) fn write_to[T: Writer, //](self, mut writer: T): @@ -152,7 +156,7 @@ struct HTTPRequest(Writable, Stringable): lineBreak, ) writer.consuming_write(self^.body_raw) - return writer.consume() + return writer^.consume() fn __str__(self) -> String: return String.write(self) diff --git a/lightbug_http/http/response.mojo b/lightbug_http/http/response.mojo index 333ef494..a138e7e7 100644 --- a/lightbug_http/http/response.mojo +++ b/lightbug_http/http/response.mojo @@ -1,7 +1,6 @@ from small_time.small_time import now from lightbug_http.uri import URI -from lightbug_http.utils import ByteReader, ByteWriter -from lightbug_http.io.bytes import Bytes, bytes, Byte, byte +from lightbug_http.io.bytes import Bytes, bytes, byte, ByteReader, ByteWriter from lightbug_http.strings import ( strHttp11, strHttp, @@ -95,7 +94,7 @@ struct HTTPResponse(Writable, Stringable): var transfer_encoding = response.headers.get(HeaderKey.TRANSFER_ENCODING) if transfer_encoding and transfer_encoding.value() == "chunked": - var b = Bytes(reader.read_bytes()) + var b = reader.read_bytes().to_bytes() var buff = Bytes(capacity=default_buffer_size) try: while conn.read(buff) > 0: @@ -168,7 +167,7 @@ struct HTTPResponse(Writable, Stringable): self.status_code = status_code self.status_text = status_text self.protocol = protocol - self.body_raw = reader.read_bytes() + self.body_raw = reader.read_bytes().to_bytes() self.set_content_length(len(self.body_raw)) if HeaderKey.CONNECTION not in self.headers: self.set_connection_keep_alive() @@ -220,16 +219,16 @@ struct HTTPResponse(Writable, Stringable): @always_inline fn read_body(mut self, mut r: ByteReader) raises -> None: - self.body_raw = r.read_bytes(self.content_length()) + self.body_raw = r.read_bytes(self.content_length()).to_bytes() self.set_content_length(len(self.body_raw)) fn read_chunks(mut self, chunks: Span[Byte]) raises: var reader = ByteReader(chunks) while True: - var size = atol(StringSlice(unsafe_from_utf8=reader.read_line()), 16) + var size = atol(str(reader.read_line()), 16) if size == 0: break - var data = reader.read_bytes(size) + var data = reader.read_bytes(size).to_bytes() reader.skip_carriage_return() self.set_content_length(self.content_length() + len(data)) self.body_raw += data @@ -265,8 +264,9 @@ struct HTTPResponse(Writable, Stringable): except: pass writer.write(self.headers, self.cookies, lineBreak) - writer.consuming_write(self^.body_raw) - return writer.consume() + writer.consuming_write(self.body_raw^) + self.body_raw = Bytes() + return writer^.consume() fn __str__(self) -> String: return String.write(self) diff --git a/lightbug_http/io/bytes.mojo b/lightbug_http/io/bytes.mojo index 915bd911..85cb3c34 100644 --- a/lightbug_http/io/bytes.mojo +++ b/lightbug_http/io/bytes.mojo @@ -1,6 +1,23 @@ +from utils import StringSlice +from memory.span import Span, _SpanIter +from lightbug_http.net import default_buffer_size + + alias Bytes = List[Byte, True] +struct Constant: + alias WHITESPACE: UInt8 = ord(" ") + alias COLON: UInt8 = ord(":") + alias AT: UInt8 = ord("@") + alias CR: UInt8 = ord("\r") + alias LF: UInt8 = ord("\n") + alias SLASH: UInt8 = ord("/") + alias QUESTION: UInt8 = ord("?") + alias ZERO: UInt8 = ord("0") + alias NINE: UInt8 = ord("9") + + @always_inline fn byte(s: String) -> Byte: return ord(s) @@ -9,3 +26,243 @@ fn byte(s: String) -> Byte: @always_inline fn bytes(s: String) -> Bytes: return s.as_bytes() + + +@always_inline +fn is_newline(b: Byte) -> Bool: + return b == Constant.LF or b == Constant.CR + + +@always_inline +fn is_space(b: Byte) -> Bool: + return b == Constant.WHITESPACE + + +struct ByteWriter(Writer): + var _inner: Bytes + + fn __init__(out self, capacity: Int = default_buffer_size): + self._inner = Bytes(capacity=capacity) + + @always_inline + fn write_bytes(mut self, bytes: Span[Byte]) -> None: + """Writes the contents of `bytes` into the internal buffer. + + Args: + bytes: The bytes to write. + """ + self._inner.extend(bytes) + + fn write[*Ts: Writable](mut self, *args: *Ts) -> None: + """Write data to the `Writer`. + + Parameters: + Ts: The types of data to write. + + Args: + args: The data to write. + """ + + @parameter + fn write_arg[T: Writable](arg: T): + arg.write_to(self) + + args.each[write_arg]() + + @always_inline + fn consuming_write(mut self, owned b: Bytes): + self._inner.extend(b^) + + @always_inline + fn consuming_write(mut self, owned s: String): + # kind of cursed but seems to work? + _ = s._buffer.pop() + self._inner.extend(s._buffer^) + s._buffer = s._buffer_type() + + @always_inline + fn write_byte(mut self, b: Byte): + self._inner.append(b) + + fn consume(owned self) -> Bytes: + var ret = self._inner^ + self._inner = Bytes() + return ret^ + + +alias EndOfReaderError = "No more bytes to read." +alias OutOfBoundsError = "Tried to read past the end of the ByteReader." + + +@value +struct ByteView[origin: Origin](): + """Convenience wrapper around a Span of Bytes.""" + + var _inner: Span[Byte, origin] + + @implicit + fn __init__(out self, b: Span[Byte, origin]): + self._inner = b + + fn __len__(self) -> Int: + return len(self._inner) + + fn __contains__(self, b: Byte) -> Bool: + for i in range(len(self._inner)): + if self._inner[i] == b: + return True + return False + + fn __getitem__(self, index: Int) -> Byte: + return self._inner[index] + + fn __getitem__(self, slc: Slice) -> Self: + return Self(self._inner[slc]) + + fn __str__(self) -> String: + return String(StringSlice(unsafe_from_utf8=self._inner)) + + fn __eq__(self, other: Self) -> Bool: + # both empty + if not self._inner and not other._inner: + return True + if len(self) != len(other): + return False + + for i in range(len(self)): + if self[i] != other[i]: + return False + return True + + fn __eq__(self, other: Span[Byte]) -> Bool: + # both empty + if not self._inner and not other: + return True + if len(self) != len(other): + return False + + for i in range(len(self)): + if self[i] != other[i]: + return False + return True + + fn __ne__(self, other: Self) -> Bool: + return not self == other + + fn __ne__(self, other: Span[Byte]) -> Bool: + return not self == other + + fn __iter__(self) -> _SpanIter[Byte, origin]: + return self._inner.__iter__() + + fn find(self, target: Byte) -> Int: + """Finds the index of a byte in a byte span. + + Args: + target: The byte to find. + + Returns: + The index of the byte in the span, or -1 if not found. + """ + for i in range(len(self)): + if self[i] == target: + return i + + return -1 + + fn to_bytes(self) -> Bytes: + return Bytes(self._inner) + + +struct ByteReader[origin: Origin]: + var _inner: Span[Byte, origin] + var read_pos: Int + + fn __init__(out self, b: Span[Byte, origin]): + self._inner = b + self.read_pos = 0 + + fn __contains__(self, b: Byte) -> Bool: + for i in range(self.read_pos, len(self._inner)): + if self._inner[i] == b: + return True + return False + + @always_inline + fn available(self) -> Bool: + return self.read_pos < len(self._inner) + + fn __len__(self) -> Int: + return len(self._inner) - self.read_pos + + fn peek(self) raises -> Byte: + if not self.available(): + raise EndOfReaderError + return self._inner[self.read_pos] + + fn read_bytes(mut self, n: Int = -1) raises -> ByteView[origin]: + var count = n + var start = self.read_pos + if n == -1: + count = len(self) + + if start + count > len(self._inner): + raise OutOfBoundsError + + self.read_pos += count + return self._inner[start : start + count] + + fn read_until(mut self, char: Byte) -> ByteView[origin]: + var start = self.read_pos + for i in range(start, len(self._inner)): + if self._inner[i] == char: + break + self.increment() + + return self._inner[start : self.read_pos] + + @always_inline + fn read_word(mut self) -> ByteView[origin]: + return self.read_until(Constant.WHITESPACE) + + fn read_line(mut self) -> ByteView[origin]: + var start = self.read_pos + for i in range(start, len(self._inner)): + if is_newline(self._inner[i]): + break + self.increment() + + # If we are at the end of the buffer, there is no newline to check for. + var ret = self._inner[start : self.read_pos] + if not self.available(): + return ret + + if self._inner[self.read_pos] == Constant.CR: + self.increment(2) + else: + self.increment() + return ret + + @always_inline + fn skip_whitespace(mut self): + for i in range(self.read_pos, len(self._inner)): + if is_space(self._inner[i]): + self.increment() + else: + break + + @always_inline + fn skip_carriage_return(mut self): + for i in range(self.read_pos, len(self._inner)): + if self._inner[i] == Constant.CR: + self.increment(2) + else: + break + + @always_inline + fn increment(mut self, v: Int = 1): + self.read_pos += v + + @always_inline + fn consume(owned self, bytes_len: Int = -1) -> Bytes: + return self^._inner[self.read_pos : self.read_pos + len(self) + 1] diff --git a/lightbug_http/net.mojo b/lightbug_http/net.mojo index 38ca9aea..46e392ec 100644 --- a/lightbug_http/net.mojo +++ b/lightbug_http/net.mojo @@ -6,7 +6,7 @@ from sys.ffi import external_call, OpaquePointer from lightbug_http.strings import NetworkType, to_string from lightbug_http.io.bytes import Bytes, bytes from lightbug_http.io.sync import Duration -from lightbug_http.libc import ( +from lightbug_http._libc import ( c_void, c_int, c_uint, @@ -46,7 +46,7 @@ from lightbug_http.libc import ( INET_ADDRSTRLEN, INET6_ADDRSTRLEN, ) -from lightbug_http.utils import logger +from lightbug_http._logger import logger from lightbug_http.socket import Socket @@ -558,7 +558,7 @@ fn listen_udp(local_address: UDPAddr) raises -> UDPConnection: Raises: Error: If the address is invalid or failed to bind the socket. """ - socket = Socket[UDPAddr](socket_type=SOCK_DGRAM) + var socket = Socket[UDPAddr](socket_type=SOCK_DGRAM) socket.bind(local_address.ip, local_address.port) return UDPConnection(socket^) diff --git a/lightbug_http/pool_manager.mojo b/lightbug_http/pool_manager.mojo index 6a2fdefe..c34ba0e0 100644 --- a/lightbug_http/pool_manager.mojo +++ b/lightbug_http/pool_manager.mojo @@ -5,33 +5,9 @@ from memory import UnsafePointer, bitcast, memcpy from collections import Dict, Optional from collections.dict import RepresentableKeyElement from lightbug_http.net import create_connection, TCPConnection, Connection -from lightbug_http.utils import logger -from lightbug_http.owning_list import OwningList - - -@value -struct Scheme(Hashable, EqualityComparable, Representable, Stringable, Writable): - var value: String - alias HTTP = Self("http") - alias HTTPS = Self("https") - - fn __hash__(self) -> UInt: - return hash(self.value) - - fn __eq__(self, other: Self) -> Bool: - return self.value == other.value - - fn __ne__(self, other: Self) -> Bool: - return self.value != other.value - - fn write_to[W: Writer, //](self, mut writer: W) -> None: - writer.write("Scheme(value=", repr(self.value), ")") - - fn __repr__(self) -> String: - return String.write(self) - - fn __str__(self) -> String: - return self.value.upper() +from lightbug_http._logger import logger +from lightbug_http._owning_list import OwningList +from lightbug_http.uri import Scheme @value diff --git a/lightbug_http/server.mojo b/lightbug_http/server.mojo index d864e0c8..1832d413 100644 --- a/lightbug_http/server.mojo +++ b/lightbug_http/server.mojo @@ -1,8 +1,8 @@ from memory import Span from lightbug_http.io.sync import Duration -from lightbug_http.io.bytes import Bytes, bytes +from lightbug_http.io.bytes import Bytes, bytes, ByteReader from lightbug_http.strings import NetworkType -from lightbug_http.utils import ByteReader, logger +from lightbug_http._logger import logger from lightbug_http.net import NoTLSListener, default_buffer_size, TCPConnection, ListenConfig from lightbug_http.socket import Socket from lightbug_http.http import HTTPRequest, encode diff --git a/lightbug_http/socket.mojo b/lightbug_http/socket.mojo index 1b1641ae..732a1217 100644 --- a/lightbug_http/socket.mojo +++ b/lightbug_http/socket.mojo @@ -3,7 +3,7 @@ from utils import StaticTuple from sys import sizeof, external_call from sys.info import os_is_macos from memory import Pointer, UnsafePointer -from lightbug_http.libc import ( +from lightbug_http._libc import ( socket, connect, recv, @@ -54,7 +54,7 @@ from lightbug_http.net import ( addrinfo_macos, addrinfo_unix, ) -from lightbug_http.utils import logger +from lightbug_http._logger import logger alias SocketClosedError = "Socket: Socket is already closed" diff --git a/lightbug_http/uri.mojo b/lightbug_http/uri.mojo index d56295ea..b5d50c54 100644 --- a/lightbug_http/uri.mojo +++ b/lightbug_http/uri.mojo @@ -1,5 +1,7 @@ -from utils import Variant -from lightbug_http.io.bytes import Bytes, bytes +from utils import Variant, StringSlice +from memory import Span +from collections import Optional +from lightbug_http.io.bytes import Bytes, bytes, ByteReader, Constant from lightbug_http.strings import ( strSlash, strHttp11, @@ -11,6 +13,49 @@ from lightbug_http.strings import ( ) +@value +struct Scheme(Hashable, EqualityComparable, Representable, Stringable, Writable): + var value: String + alias HTTP = Self("http") + alias HTTPS = Self("https") + + fn __hash__(self) -> UInt: + return hash(self.value) + + fn __eq__(self, other: Self) -> Bool: + return self.value == other.value + + fn __ne__(self, other: Self) -> Bool: + return self.value != other.value + + fn write_to[W: Writer, //](self, mut writer: W) -> None: + writer.write("Scheme(value=", repr(self.value), ")") + + fn __repr__(self) -> String: + return String.write(self) + + fn __str__(self) -> String: + return self.value.upper() + + +fn parse_host_and_port(source: String, is_tls: Bool) raises -> (String, UInt16): + """Parses the host and port from a given string. + + Args: + source: The host uri to parse. + is_tls: A boolean indicating whether the connection is secure. + + Returns: + A tuple containing the host and port. + """ + if source.count(":") != 1: + var port: UInt16 = 443 if is_tls else 80 + return source, port + + var result = source.split(":") + return result[0], UInt16(atol(result[1])) + + @value struct URI(Writable, Stringable, Representable): var _original_path: String @@ -19,6 +64,7 @@ struct URI(Writable, Stringable, Representable): var query_string: String var _hash: String var host: String + var port: Optional[UInt16] var full_uri: String var request_uri: String @@ -27,58 +73,70 @@ struct URI(Writable, Stringable, Representable): var password: String @staticmethod - fn parse(uri: String) -> URI: - var proto_str = String(strHttp11) - var is_https = False - - var proto_end = uri.find("://") - var remainder_uri: String - if proto_end >= 0: - proto_str = uri[:proto_end] - if proto_str == https: - is_https = True - remainder_uri = uri[proto_end + 3 :] - else: - remainder_uri = uri + fn parse(owned uri: String) raises -> URI: + """Parses a URI which is defined using the following format. - var path_start = remainder_uri.find("/") - var host_and_port: String - var request_uri: String + `[scheme:][//[user_info@]host][/]path[?query][#fragment]` + """ + var reader = ByteReader(uri.as_bytes()) + + # Parse the scheme, if exists. + # Assume http if no scheme is provided, fairly safe given the context of lightbug. + var scheme: String = "http" + if Constant.COLON in reader: + scheme = str(reader.read_until(Constant.COLON)) + if reader.read_bytes(3) != "://".as_bytes(): + raise Error("URI.parse: Invalid URI format, scheme should be followed by `://`. Received: " + uri) + + # Parse the user info, if exists. + var user_info: String = "" + if Constant.AT in reader: + user_info = str(reader.read_until(Constant.AT)) + reader.increment(1) + + # TODOs (@thatstoasty) + # Handle ipv4 and ipv6 literal + # Handle string host + # A query right after the domain is a valid uri, but it's equivalent to example.com/?query + # so we should add the normalization of paths + var host_and_port = reader.read_until(Constant.SLASH) + colon = host_and_port.find(Constant.COLON) var host: String - if path_start >= 0: - host_and_port = remainder_uri[:path_start] - request_uri = remainder_uri[path_start:] - host = host_and_port[:path_start] + var port: Optional[UInt16] = None + if colon != -1: + host = str(host_and_port[:colon]) + var port_end = colon + 1 + # loop through the post colon chunk until we find a non-digit character + for b in host_and_port[colon + 1 :]: + if b[] < Constant.ZERO or b[] > Constant.NINE: + break + port_end += 1 + port = UInt16(atol(str(host_and_port[colon + 1 : port_end]))) else: - host_and_port = remainder_uri - request_uri = strSlash - host = host_and_port + host = str(host_and_port) - var scheme: String - if is_https: - scheme = https - else: - scheme = http - - var n = request_uri.find("?") - var original_path: String - var query_string: String - if n >= 0: - original_path = request_uri[:n] - query_string = request_uri[n + 1 :] - else: - original_path = request_uri - query_string = "" + # Parse the path + var path: String = "/" + if reader.available() and reader.peek() == Constant.SLASH: + # Read until the query string, or the end if there is none. + path = str(reader.read_until(Constant.QUESTION)) + + # Parse query + var query: String = "" + if reader.available() and reader.peek() == Constant.QUESTION: + # TODO: Handle fragments for anchors + query = str(reader.read_bytes()[1:]) return URI( - _original_path=original_path, + _original_path=path, scheme=scheme, - path=original_path, - query_string=query_string, + path=path, + query_string=query, _hash="", host=host, + port=port, full_uri=uri, - request_uri=request_uri, + request_uri=uri, username="", password="", ) diff --git a/lightbug_http/utils.mojo b/lightbug_http/utils.mojo deleted file mode 100644 index 4097568b..00000000 --- a/lightbug_http/utils.mojo +++ /dev/null @@ -1,271 +0,0 @@ -from memory import Span -from sys.param_env import env_get_string -from lightbug_http.io.bytes import Bytes, Byte -from lightbug_http.strings import BytesConstant -from lightbug_http.net import default_buffer_size - - -@always_inline -fn is_newline(b: Byte) -> Bool: - return b == BytesConstant.nChar or b == BytesConstant.rChar - - -@always_inline -fn is_space(b: Byte) -> Bool: - return b == BytesConstant.whitespace - - -struct ByteWriter(Writer): - var _inner: Bytes - - fn __init__(out self, capacity: Int = default_buffer_size): - self._inner = Bytes(capacity=capacity) - - @always_inline - fn write_bytes(mut self, bytes: Span[Byte]) -> None: - """Writes the contents of `bytes` into the internal buffer. - - Args: - bytes: The bytes to write. - """ - self._inner.extend(bytes) - - fn write[*Ts: Writable](mut self, *args: *Ts) -> None: - """Write data to the `Writer`. - - Parameters: - Ts: The types of data to write. - - Args: - args: The data to write. - """ - - @parameter - fn write_arg[T: Writable](arg: T): - arg.write_to(self) - - args.each[write_arg]() - - @always_inline - fn consuming_write(mut self, owned b: Bytes): - self._inner.extend(b^) - - @always_inline - fn consuming_write(mut self, owned s: String): - # kind of cursed but seems to work? - _ = s._buffer.pop() - self._inner.extend(s._buffer^) - s._buffer = s._buffer_type() - - @always_inline - fn write_byte(mut self, b: Byte): - self._inner.append(b) - - fn consume(mut self) -> Bytes: - var ret = self._inner^ - self._inner = Bytes() - return ret^ - - -alias EndOfReaderError = "No more bytes to read." -alias OutOfBoundsError = "Tried to read past the end of the ByteReader." - - -struct ByteReader[origin: Origin]: - var _inner: Span[Byte, origin] - var read_pos: Int - - fn __init__(out self, ref b: Span[Byte, origin]): - self._inner = b - self.read_pos = 0 - - @always_inline - fn available(self) -> Bool: - return self.read_pos < len(self._inner) - - fn __len__(self) -> Int: - return len(self._inner) - self.read_pos - - fn peek(self) raises -> Byte: - if not self.available(): - raise EndOfReaderError - return self._inner[self.read_pos] - - fn read_bytes(mut self, n: Int = -1) raises -> Span[Byte, origin]: - var count = n - var start = self.read_pos - if n == -1: - count = len(self) - - if start + count > len(self._inner): - raise OutOfBoundsError - - self.read_pos += count - return self._inner[start : start + count] - - fn read_until(mut self, char: Byte) -> Span[Byte, origin]: - var start = self.read_pos - for i in range(start, len(self._inner)): - if self._inner[i] == char: - break - self.increment() - - return self._inner[start : self.read_pos] - - @always_inline - fn read_word(mut self) -> Span[Byte, origin]: - return self.read_until(BytesConstant.whitespace) - - fn read_line(mut self) -> Span[Byte, origin]: - var start = self.read_pos - for i in range(start, len(self._inner)): - if is_newline(self._inner[i]): - break - self.increment() - - # If we are at the end of the buffer, there is no newline to check for. - var ret = self._inner[start : self.read_pos] - if not self.available(): - return ret - - if self._inner[self.read_pos] == BytesConstant.rChar: - self.increment(2) - else: - self.increment() - return ret - - @always_inline - fn skip_whitespace(mut self): - for i in range(self.read_pos, len(self._inner)): - if is_space(self._inner[i]): - self.increment() - else: - break - - @always_inline - fn skip_carriage_return(mut self): - for i in range(self.read_pos, len(self._inner)): - if self._inner[i] == BytesConstant.rChar: - self.increment(2) - else: - break - - @always_inline - fn increment(mut self, v: Int = 1): - self.read_pos += v - - @always_inline - fn consume(owned self, bytes_len: Int = -1) -> Bytes: - return self^._inner[self.read_pos : self.read_pos + len(self) + 1] - - -struct LogLevel: - alias FATAL = 0 - alias ERROR = 1 - alias WARN = 2 - alias INFO = 3 - alias DEBUG = 4 - - -fn get_log_level() -> Int: - """Returns the log level based on the parameter environment variable `LOG_LEVEL`. - - Returns: - The log level. - """ - alias level = env_get_string["LB_LOG_LEVEL", "INFO"]() - if level == "INFO": - return LogLevel.INFO - elif level == "WARN": - return LogLevel.WARN - elif level == "ERROR": - return LogLevel.ERROR - elif level == "DEBUG": - return LogLevel.DEBUG - elif level == "FATAL": - return LogLevel.FATAL - else: - return LogLevel.INFO - - -alias LOG_LEVEL = get_log_level() -"""Logger level determined by the `LB_LOG_LEVEL` param environment variable. - -When building or running the application, you can set `LB_LOG_LEVEL` by providing the the following option: - -```bash -mojo build ... -D LB_LOG_LEVEL=DEBUG -# or -mojo ... -D LB_LOG_LEVEL=DEBUG -``` -""" - - -@value -struct Logger[level: Int]: - alias STDOUT = 1 - alias STDERR = 2 - - fn _log_message[event_level: Int](self, message: String): - @parameter - if level >= event_level: - - @parameter - if event_level < LogLevel.WARN: - # Write to stderr if FATAL or ERROR - print(message, file=Self.STDERR) - else: - print(message) - - fn info[*Ts: Writable](self, *messages: *Ts): - var msg = String.write("\033[36mINFO\033[0m - ") - - @parameter - fn write_message[T: Writable](message: T): - msg.write(message, " ") - - messages.each[write_message]() - self._log_message[LogLevel.INFO](msg) - - fn warn[*Ts: Writable](self, *messages: *Ts): - var msg = String.write("\033[33mWARN\033[0m - ") - - @parameter - fn write_message[T: Writable](message: T): - msg.write(message, " ") - - messages.each[write_message]() - self._log_message[LogLevel.WARN](msg) - - fn error[*Ts: Writable](self, *messages: *Ts): - var msg = String.write("\033[31mERROR\033[0m - ") - - @parameter - fn write_message[T: Writable](message: T): - msg.write(message, " ") - - messages.each[write_message]() - self._log_message[LogLevel.ERROR](msg) - - fn debug[*Ts: Writable](self, *messages: *Ts): - var msg = String.write("\033[34mDEBUG\033[0m - ") - - @parameter - fn write_message[T: Writable](message: T): - msg.write(message, " ") - - messages.each[write_message]() - self._log_message[LogLevel.DEBUG](msg) - - fn fatal[*Ts: Writable](self, *messages: *Ts): - var msg = String.write("\033[35mFATAL\033[0m - ") - - @parameter - fn write_message[T: Writable](message: T): - msg.write(message, " ") - - messages.each[write_message]() - self._log_message[LogLevel.FATAL](msg) - - -alias logger = Logger[LOG_LEVEL]() diff --git a/tests/integration/integration_test_client.mojo b/tests/integration/integration_test_client.mojo index 210d2839..201476b7 100644 --- a/tests/integration/integration_test_client.mojo +++ b/tests/integration/integration_test_client.mojo @@ -1,7 +1,7 @@ from collections import Dict from lightbug_http import * from lightbug_http.client import Client -from lightbug_http.utils import logger +from lightbug_http._logger import logger from testing import * diff --git a/tests/lightbug_http/test_http.mojo b/tests/lightbug_http/http/test_http.mojo similarity index 75% rename from tests/lightbug_http/test_http.mojo rename to tests/lightbug_http/http/test_http.mojo index 35907256..6e7ac2aa 100644 --- a/tests/lightbug_http/test_http.mojo +++ b/tests/lightbug_http/http/test_http.mojo @@ -10,6 +10,7 @@ from lightbug_http.strings import to_string alias default_server_conn_string = "http://localhost:8080" + def test_encode_http_request(): var uri = URI.parse(default_server_conn_string + "/foobar?baz") var req = HTTPRequest( @@ -17,7 +18,7 @@ def test_encode_http_request(): body=String("Hello world!").as_bytes(), cookies=RequestCookieJar( Cookie(name="session_id", value="123", path=str("/"), secure=True, max_age=Duration(minutes=10)), - Cookie(name="token", value="abc", domain=str("localhost"), path=str("/api"), http_only=True) + Cookie(name="token", value="abc", domain=str("localhost"), path=str("/api"), http_only=True), ), headers=Headers(Header("Connection", "keep-alive")), ) @@ -25,20 +26,9 @@ def test_encode_http_request(): var as_str = str(req) var req_encoded = to_string(encode(req^)) + var expected = "GET /foobar?baz HTTP/1.1\r\nconnection: keep-alive\r\ncontent-length: 12\r\nhost: localhost:8080\r\ncookie: session_id=123; token=abc\r\n\r\nHello world!" - var expected = - "GET /foobar?baz HTTP/1.1\r\n" - "connection: keep-alive\r\n" - "content-length: 12\r\n" - "host: localhost:8080\r\n" - "cookie: session_id=123; token=abc\r\n" - "\r\n" - "Hello world!" - - testing.assert_equal( - req_encoded, - expected - ) + testing.assert_equal(req_encoded, expected) testing.assert_equal(req_encoded, as_str) @@ -49,25 +39,16 @@ def test_encode_http_response(): res.cookies = ResponseCookieJar( Cookie(name="session_id", value="123", path=str("/api"), secure=True), Cookie(name="session_id", value="abc", path=str("/"), secure=True, max_age=Duration(minutes=10)), - Cookie(name="token", value="123", domain=str("localhost"), path=str("/api"), http_only=True) + Cookie(name="token", value="123", domain=str("localhost"), path=str("/api"), http_only=True), ) var as_str = str(res) var res_encoded = to_string(encode(res^)) - var expected_full = - "HTTP/1.1 200 OK\r\n" - "server: lightbug_http\r\n" - "content-type: application/octet-stream\r\n" - "connection: keep-alive\r\ncontent-length: 13\r\n" - "date: 2024-06-02T13:41:50.766880+00:00\r\n" - "set-cookie: session_id=123; Path=/api; Secure\r\n" - "set-cookie: session_id=abc; Max-Age=600; Path=/; Secure\r\n" - "set-cookie: token=123; Domain=localhost; Path=/api; HttpOnly\r\n" - "\r\n" - "Hello, World!" + var expected_full = "HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type: application/octet-stream\r\nconnection: keep-alive\r\ncontent-length: 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\nset-cookie: session_id=123; Path=/api; Secure\r\nset-cookie: session_id=abc; Max-Age=600; Path=/; Secure\r\nset-cookie: token=123; Domain=localhost; Path=/api; HttpOnly\r\n\r\nHello, World!" testing.assert_equal(res_encoded, expected_full) testing.assert_equal(res_encoded, as_str) + def test_decoding_http_response(): var res = String( "HTTP/1.1 200 OK\r\n" @@ -91,6 +72,7 @@ def test_decoding_http_response(): assert_equal(200, response.status_code) assert_equal("OK", response.status_text) + def test_http_version_parse(): var v1 = HttpVersion("HTTP/1.1") testing.assert_equal(v1._v, 1) diff --git a/tests/lightbug_http/http/test_request.mojo b/tests/lightbug_http/http/test_request.mojo index d9e6fdfb..80f60eb2 100644 --- a/tests/lightbug_http/http/test_request.mojo +++ b/tests/lightbug_http/http/test_request.mojo @@ -8,7 +8,7 @@ def test_request_from_bytes(): var request = HTTPRequest.from_bytes("127.0.0.1", 4096, data.as_bytes()) testing.assert_equal(request.protocol, "HTTP/1.1") testing.assert_equal(request.method, "GET") - testing.assert_equal(request.uri.request_uri, "/redirect") + testing.assert_equal(request.uri.request_uri, "127.0.0.1/redirect") testing.assert_equal(request.headers["Host"], "127.0.0.1:8080") testing.assert_equal(request.headers["User-Agent"], "python-requests/2.32.3") diff --git a/tests/lightbug_http/test_byte_reader.mojo b/tests/lightbug_http/io/test_byte_reader.mojo similarity index 59% rename from tests/lightbug_http/test_byte_reader.mojo rename to tests/lightbug_http/io/test_byte_reader.mojo index 9a0ceb0b..401eee35 100644 --- a/tests/lightbug_http/test_byte_reader.mojo +++ b/tests/lightbug_http/io/test_byte_reader.mojo @@ -1,6 +1,5 @@ import testing -from lightbug_http.utils import ByteReader, EndOfReaderError -from lightbug_http.io.bytes import Bytes +from lightbug_http.io.bytes import Bytes, ByteReader, EndOfReaderError alias example = "Hello, World!" @@ -21,23 +20,23 @@ def test_peek(): def test_read_until(): var r = ByteReader(example.as_bytes()) testing.assert_equal(r.read_pos, 0) - testing.assert_equal(Bytes(r.read_until(ord(","))), Bytes(72, 101, 108, 108, 111)) + testing.assert_equal(r.read_until(ord(",")).to_bytes(), Bytes(72, 101, 108, 108, 111)) testing.assert_equal(r.read_pos, 5) def test_read_bytes(): var r = ByteReader(example.as_bytes()) - testing.assert_equal(Bytes(r.read_bytes()), Bytes(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)) + testing.assert_equal(r.read_bytes().to_bytes(), Bytes(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)) r = ByteReader(example.as_bytes()) - testing.assert_equal(Bytes(r.read_bytes(7)), Bytes(72, 101, 108, 108, 111, 44, 32)) - testing.assert_equal(Bytes(r.read_bytes()), Bytes(87, 111, 114, 108, 100, 33)) + testing.assert_equal(r.read_bytes(7).to_bytes(), Bytes(72, 101, 108, 108, 111, 44, 32)) + testing.assert_equal(r.read_bytes().to_bytes(), Bytes(87, 111, 114, 108, 100, 33)) def test_read_word(): var r = ByteReader(example.as_bytes()) testing.assert_equal(r.read_pos, 0) - testing.assert_equal(Bytes(r.read_word()), Bytes(72, 101, 108, 108, 111, 44)) + testing.assert_equal(r.read_word().to_bytes(), Bytes(72, 101, 108, 108, 111, 44)) testing.assert_equal(r.read_pos, 6) @@ -45,15 +44,15 @@ def test_read_line(): # No newline, go to end of line var r = ByteReader(example.as_bytes()) testing.assert_equal(r.read_pos, 0) - testing.assert_equal(Bytes(r.read_line()), Bytes(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)) + testing.assert_equal(r.read_line().to_bytes(), Bytes(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)) testing.assert_equal(r.read_pos, 13) # Newline, go to end of line. Should cover carriage return and newline var r2 = ByteReader("Hello\r\nWorld\n!".as_bytes()) testing.assert_equal(r2.read_pos, 0) - testing.assert_equal(Bytes(r2.read_line()), Bytes(72, 101, 108, 108, 111)) + testing.assert_equal(r2.read_line().to_bytes(), Bytes(72, 101, 108, 108, 111)) testing.assert_equal(r2.read_pos, 7) - testing.assert_equal(Bytes(r2.read_line()), Bytes(87, 111, 114, 108, 100)) + testing.assert_equal(r2.read_line().to_bytes(), Bytes(87, 111, 114, 108, 100)) testing.assert_equal(r2.read_pos, 13) @@ -61,16 +60,16 @@ def test_skip_whitespace(): var r = ByteReader(" Hola".as_bytes()) r.skip_whitespace() testing.assert_equal(r.read_pos, 1) - testing.assert_equal(Bytes(r.read_word()), Bytes(72, 111, 108, 97)) + testing.assert_equal(r.read_word().to_bytes(), Bytes(72, 111, 108, 97)) def test_skip_carriage_return(): var r = ByteReader("\r\nHola".as_bytes()) r.skip_carriage_return() testing.assert_equal(r.read_pos, 2) - testing.assert_equal(Bytes(r.read_bytes(4)), Bytes(72, 111, 108, 97)) + testing.assert_equal(r.read_bytes(4).to_bytes(), Bytes(72, 111, 108, 97)) def test_consume(): var r = ByteReader(example.as_bytes()) - testing.assert_equal(Bytes(r^.consume()), Bytes(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)) + testing.assert_equal(r^.consume(), Bytes(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)) diff --git a/tests/lightbug_http/test_byte_writer.mojo b/tests/lightbug_http/io/test_byte_writer.mojo similarity index 91% rename from tests/lightbug_http/test_byte_writer.mojo rename to tests/lightbug_http/io/test_byte_writer.mojo index 86d28e11..b0386364 100644 --- a/tests/lightbug_http/test_byte_writer.mojo +++ b/tests/lightbug_http/io/test_byte_writer.mojo @@ -1,6 +1,5 @@ import testing -from lightbug_http.utils import ByteWriter -from lightbug_http.io.bytes import Bytes +from lightbug_http.io.bytes import Bytes, ByteWriter def test_write_byte(): diff --git a/tests/lightbug_http/test_header.mojo b/tests/lightbug_http/test_header.mojo index cac3fd60..d7900062 100644 --- a/tests/lightbug_http/test_header.mojo +++ b/tests/lightbug_http/test_header.mojo @@ -1,8 +1,7 @@ from testing import assert_equal, assert_true from memory import Span -from lightbug_http.utils import ByteReader from lightbug_http.header import Headers, Header -from lightbug_http.io.bytes import Bytes, bytes +from lightbug_http.io.bytes import Bytes, bytes, ByteReader def test_header_case_insensitive(): diff --git a/tests/lightbug_http/test_owning_list.mojo b/tests/lightbug_http/test_owning_list.mojo index 0a486b60..2ec43d15 100644 --- a/tests/lightbug_http/test_owning_list.mojo +++ b/tests/lightbug_http/test_owning_list.mojo @@ -1,4 +1,4 @@ -from lightbug_http.owning_list import OwningList +from lightbug_http._owning_list import OwningList from sys.info import sizeof from memory import UnsafePointer, Span diff --git a/tests/lightbug_http/test_uri.mojo b/tests/lightbug_http/test_uri.mojo index 7f332841..2e6e05cc 100644 --- a/tests/lightbug_http/test_uri.mojo +++ b/tests/lightbug_http/test_uri.mojo @@ -15,10 +15,11 @@ def test_uri_no_parse_defaults(): def test_uri_parse_http_with_port(): var uri = URI.parse("http://example.com:8080/index.html") testing.assert_equal(uri.scheme, "http") - testing.assert_equal(uri.host, "example.com:8080") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.port.value(), 8080) testing.assert_equal(uri.path, "/index.html") testing.assert_equal(uri._original_path, "/index.html") - testing.assert_equal(uri.request_uri, "/index.html") + # testing.assert_equal(uri.request_uri, "http://example.com:8080/index.html") testing.assert_equal(uri.is_https(), False) testing.assert_equal(uri.is_http(), True) testing.assert_equal(uri.query_string, empty_string) @@ -27,10 +28,11 @@ def test_uri_parse_http_with_port(): def test_uri_parse_https_with_port(): var uri = URI.parse("https://example.com:8080/index.html") testing.assert_equal(uri.scheme, "https") - testing.assert_equal(uri.host, "example.com:8080") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.port.value(), 8080) testing.assert_equal(uri.path, "/index.html") testing.assert_equal(uri._original_path, "/index.html") - testing.assert_equal(uri.request_uri, "/index.html") + # testing.assert_equal(uri.request_uri, "https://example.com:8080/index.html") testing.assert_equal(uri.is_https(), True) testing.assert_equal(uri.is_http(), False) testing.assert_equal(uri.query_string, empty_string) @@ -42,7 +44,7 @@ def test_uri_parse_http_with_path(): testing.assert_equal(uri.host, "example.com") testing.assert_equal(uri.path, "/index.html") testing.assert_equal(uri._original_path, "/index.html") - testing.assert_equal(uri.request_uri, "/index.html") + # testing.assert_equal(uri.request_uri, "http://example.com/index.html") testing.assert_equal(uri.is_https(), False) testing.assert_equal(uri.is_http(), True) testing.assert_equal(uri.query_string, empty_string) @@ -54,7 +56,7 @@ def test_uri_parse_https_with_path(): testing.assert_equal(uri.host, "example.com") testing.assert_equal(uri.path, "/index.html") testing.assert_equal(uri._original_path, "/index.html") - testing.assert_equal(uri.request_uri, "/index.html") + # testing.assert_equal(uri.request_uri, "https://example.com/index.html") testing.assert_equal(uri.is_https(), True) testing.assert_equal(uri.is_http(), False) testing.assert_equal(uri.query_string, empty_string) @@ -66,7 +68,7 @@ def test_uri_parse_http_basic(): testing.assert_equal(uri.host, "example.com") testing.assert_equal(uri.path, "/") testing.assert_equal(uri._original_path, "/") - testing.assert_equal(uri.request_uri, "/") + # testing.assert_equal(uri.request_uri, "/") testing.assert_equal(uri.query_string, empty_string) @@ -76,7 +78,7 @@ def test_uri_parse_http_basic_www(): testing.assert_equal(uri.host, "www.example.com") testing.assert_equal(uri.path, "/") testing.assert_equal(uri._original_path, "/") - testing.assert_equal(uri.request_uri, "/") + # testing.assert_equal(uri.request_uri, "/") testing.assert_equal(uri.query_string, empty_string) @@ -86,9 +88,15 @@ def test_uri_parse_http_with_query_string(): testing.assert_equal(uri.host, "www.example.com") testing.assert_equal(uri.path, "/job") testing.assert_equal(uri._original_path, "/job") - testing.assert_equal(uri.request_uri, "/job?title=engineer") + # testing.assert_equal(uri.request_uri, "/job?title=engineer") testing.assert_equal(uri.query_string, "title=engineer") -def test_uri_parse_http_with_hash(): - ... +def test_uri_parse_no_scheme(): + var uri = URI.parse("www.example.com") + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.host, "www.example.com") + + +# def test_uri_parse_http_with_hash(): +# ... From 6102aa18eb3edf5c00361ce4738cd9e4b58911a3 Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Tue, 28 Jan 2025 09:30:19 -0600 Subject: [PATCH 08/13] fix up request uri --- lightbug_http/io/bytes.mojo | 3 ++ lightbug_http/uri.mojo | 6 +++- tests/lightbug_http/http/test_request.mojo | 2 +- tests/lightbug_http/io/test_byte_writer.mojo | 13 +++++---- tests/lightbug_http/test_uri.mojo | 29 +++++++++++++++----- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/lightbug_http/io/bytes.mojo b/lightbug_http/io/bytes.mojo index 85cb3c34..dc66da3c 100644 --- a/lightbug_http/io/bytes.mojo +++ b/lightbug_http/io/bytes.mojo @@ -182,6 +182,9 @@ struct ByteReader[origin: Origin]: self._inner = b self.read_pos = 0 + fn copy(self) -> Self: + return ByteReader(self._inner[self.read_pos :]) + fn __contains__(self, b: Byte) -> Bool: for i in range(self.read_pos, len(self._inner)): if self._inner[i] == b: diff --git a/lightbug_http/uri.mojo b/lightbug_http/uri.mojo index b5d50c54..aa5a90df 100644 --- a/lightbug_http/uri.mojo +++ b/lightbug_http/uri.mojo @@ -117,7 +117,11 @@ struct URI(Writable, Stringable, Representable): # Parse the path var path: String = "/" + var request_uri: String = "/" if reader.available() and reader.peek() == Constant.SLASH: + # Copy the remaining bytes to read the request uri. + var request_uri_reader = reader.copy() + request_uri = str(request_uri_reader.read_bytes()) # Read until the query string, or the end if there is none. path = str(reader.read_until(Constant.QUESTION)) @@ -136,7 +140,7 @@ struct URI(Writable, Stringable, Representable): host=host, port=port, full_uri=uri, - request_uri=uri, + request_uri=request_uri, username="", password="", ) diff --git a/tests/lightbug_http/http/test_request.mojo b/tests/lightbug_http/http/test_request.mojo index 80f60eb2..d9e6fdfb 100644 --- a/tests/lightbug_http/http/test_request.mojo +++ b/tests/lightbug_http/http/test_request.mojo @@ -8,7 +8,7 @@ def test_request_from_bytes(): var request = HTTPRequest.from_bytes("127.0.0.1", 4096, data.as_bytes()) testing.assert_equal(request.protocol, "HTTP/1.1") testing.assert_equal(request.method, "GET") - testing.assert_equal(request.uri.request_uri, "127.0.0.1/redirect") + testing.assert_equal(request.uri.request_uri, "/redirect") testing.assert_equal(request.headers["Host"], "127.0.0.1:8080") testing.assert_equal(request.headers["User-Agent"], "python-requests/2.32.3") diff --git a/tests/lightbug_http/io/test_byte_writer.mojo b/tests/lightbug_http/io/test_byte_writer.mojo index b0386364..50b2a441 100644 --- a/tests/lightbug_http/io/test_byte_writer.mojo +++ b/tests/lightbug_http/io/test_byte_writer.mojo @@ -5,9 +5,11 @@ from lightbug_http.io.bytes import Bytes, ByteWriter def test_write_byte(): var w = ByteWriter() w.write_byte(0x01) - testing.assert_equal(w.consume(), Bytes(0x01)) + testing.assert_equal(w^.consume(), Bytes(0x01)) + + w = ByteWriter() w.write_byte(2) - testing.assert_equal(w.consume(), Bytes(2)) + testing.assert_equal(w^.consume(), Bytes(2)) def test_consuming_write(): @@ -15,16 +17,17 @@ def test_consuming_write(): var my_string: String = "World" w.consuming_write("Hello ") w.consuming_write(my_string^) - testing.assert_equal(w.consume(), Bytes(72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100)) + testing.assert_equal(w^.consume(), Bytes(72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100)) + w = ByteWriter() var my_bytes = Bytes(72, 101, 108, 108, 111, 32) w.consuming_write(my_bytes^) w.consuming_write(Bytes(87, 111, 114, 108, 10)) - testing.assert_equal(w.consume(), Bytes(72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100)) + testing.assert_equal(w^.consume(), Bytes(72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100)) def test_write(): var w = ByteWriter() w.write("Hello", ", ") w.write_bytes("World!".as_bytes()) - testing.assert_equal(w.consume(), Bytes(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)) + testing.assert_equal(w^.consume(), Bytes(72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33)) diff --git a/tests/lightbug_http/test_uri.mojo b/tests/lightbug_http/test_uri.mojo index 2e6e05cc..1da34264 100644 --- a/tests/lightbug_http/test_uri.mojo +++ b/tests/lightbug_http/test_uri.mojo @@ -19,7 +19,7 @@ def test_uri_parse_http_with_port(): testing.assert_equal(uri.port.value(), 8080) testing.assert_equal(uri.path, "/index.html") testing.assert_equal(uri._original_path, "/index.html") - # testing.assert_equal(uri.request_uri, "http://example.com:8080/index.html") + testing.assert_equal(uri.request_uri, "/index.html") testing.assert_equal(uri.is_https(), False) testing.assert_equal(uri.is_http(), True) testing.assert_equal(uri.query_string, empty_string) @@ -32,7 +32,7 @@ def test_uri_parse_https_with_port(): testing.assert_equal(uri.port.value(), 8080) testing.assert_equal(uri.path, "/index.html") testing.assert_equal(uri._original_path, "/index.html") - # testing.assert_equal(uri.request_uri, "https://example.com:8080/index.html") + testing.assert_equal(uri.request_uri, "/index.html") testing.assert_equal(uri.is_https(), True) testing.assert_equal(uri.is_http(), False) testing.assert_equal(uri.query_string, empty_string) @@ -44,7 +44,7 @@ def test_uri_parse_http_with_path(): testing.assert_equal(uri.host, "example.com") testing.assert_equal(uri.path, "/index.html") testing.assert_equal(uri._original_path, "/index.html") - # testing.assert_equal(uri.request_uri, "http://example.com/index.html") + testing.assert_equal(uri.request_uri, "/index.html") testing.assert_equal(uri.is_https(), False) testing.assert_equal(uri.is_http(), True) testing.assert_equal(uri.query_string, empty_string) @@ -56,7 +56,7 @@ def test_uri_parse_https_with_path(): testing.assert_equal(uri.host, "example.com") testing.assert_equal(uri.path, "/index.html") testing.assert_equal(uri._original_path, "/index.html") - # testing.assert_equal(uri.request_uri, "https://example.com/index.html") + testing.assert_equal(uri.request_uri, "/index.html") testing.assert_equal(uri.is_https(), True) testing.assert_equal(uri.is_http(), False) testing.assert_equal(uri.query_string, empty_string) @@ -68,7 +68,7 @@ def test_uri_parse_http_basic(): testing.assert_equal(uri.host, "example.com") testing.assert_equal(uri.path, "/") testing.assert_equal(uri._original_path, "/") - # testing.assert_equal(uri.request_uri, "/") + testing.assert_equal(uri.request_uri, "/") testing.assert_equal(uri.query_string, empty_string) @@ -78,7 +78,7 @@ def test_uri_parse_http_basic_www(): testing.assert_equal(uri.host, "www.example.com") testing.assert_equal(uri.path, "/") testing.assert_equal(uri._original_path, "/") - # testing.assert_equal(uri.request_uri, "/") + testing.assert_equal(uri.request_uri, "/") testing.assert_equal(uri.query_string, empty_string) @@ -88,7 +88,7 @@ def test_uri_parse_http_with_query_string(): testing.assert_equal(uri.host, "www.example.com") testing.assert_equal(uri.path, "/job") testing.assert_equal(uri._original_path, "/job") - # testing.assert_equal(uri.request_uri, "/job?title=engineer") + testing.assert_equal(uri.request_uri, "/job?title=engineer") testing.assert_equal(uri.query_string, "title=engineer") @@ -98,5 +98,20 @@ def test_uri_parse_no_scheme(): testing.assert_equal(uri.host, "www.example.com") +def test_uri_ip_address_no_scheme(): + var uri = URI.parse("168.22.0.1/path/to/favicon.ico") + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.host, "168.22.0.1") + testing.assert_equal(uri.path, "/path/to/favicon.ico") + + +def test_uri_ip_address(): + var uri = URI.parse("http://168.22.0.1:8080/path/to/favicon.ico") + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.host, "168.22.0.1") + testing.assert_equal(uri.path, "/path/to/favicon.ico") + testing.assert_equal(uri.port.value(), 8080) + + # def test_uri_parse_http_with_hash(): # ... From 7bec60884267ee3abd1035ca5da5c2130d80a995 Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Wed, 29 Jan 2025 10:33:22 -0600 Subject: [PATCH 09/13] sync up changes --- lightbug_http/io/bytes.mojo | 3 +++ lightbug_http/uri.mojo | 31 +++++++++++-------------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/lightbug_http/io/bytes.mojo b/lightbug_http/io/bytes.mojo index dc66da3c..ec554f57 100644 --- a/lightbug_http/io/bytes.mojo +++ b/lightbug_http/io/bytes.mojo @@ -107,6 +107,9 @@ struct ByteView[origin: Origin](): fn __len__(self) -> Int: return len(self._inner) + fn __bool__(self) -> Bool: + return self._inner.__bool__() + fn __contains__(self, b: Byte) -> Bool: for i in range(len(self._inner)): if self._inner[i] == b: diff --git a/lightbug_http/uri.mojo b/lightbug_http/uri.mojo index 2f9dfa89..235129b7 100644 --- a/lightbug_http/uri.mojo +++ b/lightbug_http/uri.mojo @@ -115,24 +115,6 @@ struct Scheme(Hashable, EqualityComparable, Representable, Stringable, Writable) return self.value.upper() -fn parse_host_and_port(source: String, is_tls: Bool) raises -> (String, UInt16): - """Parses the host and port from a given string. - - Args: - source: The host uri to parse. - is_tls: A boolean indicating whether the connection is secure. - - Returns: - A tuple containing the host and port. - """ - if source.count(":") != 1: - var port: UInt16 = 443 if is_tls else 80 - return source, port - - var result = source.split(":") - return result[0], UInt16(atol(result[1])) - - @value struct URI(Writable, Stringable, Representable): var _original_path: String @@ -193,6 +175,15 @@ struct URI(Writable, Stringable, Representable): else: host = str(host_and_port) + # Reads until either the start of the query string, or the end of the uri. + var unquote_reader = reader.copy() + var original_path_bytes = unquote_reader.read_until(Constant.QUESTION) + var original_path: String + if not original_path_bytes: + original_path = "/" + else: + original_path = unquote(str(original_path_bytes), disallowed_escapes=List(str("/"))) + # Parse the path var path: String = "/" var request_uri: String = "/" @@ -201,7 +192,7 @@ struct URI(Writable, Stringable, Representable): var request_uri_reader = reader.copy() request_uri = str(request_uri_reader.read_bytes()) # Read until the query string, or the end if there is none. - path = str(reader.read_until(Constant.QUESTION)) + path = unquote(str(reader.read_until(Constant.QUESTION)), disallowed_escapes=List(str("/"))) # Parse query var query: String = "" @@ -223,7 +214,7 @@ struct URI(Writable, Stringable, Representable): queries[key] = unquote[expand_plus=True](key_val[1]) return URI( - _original_path=path, + _original_path=original_path, scheme=scheme, path=path, query_string=query, From 34aebdeddb306ee7e8e7ceacc66d13667010e080 Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Wed, 29 Jan 2025 12:41:52 -0600 Subject: [PATCH 10/13] remove constants from constant struct --- lightbug_http/io/bytes.mojo | 6 ------ lightbug_http/uri.mojo | 33 +++++++++++++++++++++------------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/lightbug_http/io/bytes.mojo b/lightbug_http/io/bytes.mojo index ec554f57..4f4c449f 100644 --- a/lightbug_http/io/bytes.mojo +++ b/lightbug_http/io/bytes.mojo @@ -8,14 +8,8 @@ alias Bytes = List[Byte, True] struct Constant: alias WHITESPACE: UInt8 = ord(" ") - alias COLON: UInt8 = ord(":") - alias AT: UInt8 = ord("@") alias CR: UInt8 = ord("\r") alias LF: UInt8 = ord("\n") - alias SLASH: UInt8 = ord("/") - alias QUESTION: UInt8 = ord("?") - alias ZERO: UInt8 = ord("0") - alias NINE: UInt8 = ord("9") @always_inline diff --git a/lightbug_http/uri.mojo b/lightbug_http/uri.mojo index 235129b7..5744ffb2 100644 --- a/lightbug_http/uri.mojo +++ b/lightbug_http/uri.mojo @@ -1,7 +1,7 @@ from utils import Variant, StringSlice from memory import Span from collections import Optional, Dict -from lightbug_http.io.bytes import Bytes, bytes, ByteReader, Constant +from lightbug_http.io.bytes import Bytes, bytes, ByteReader from lightbug_http.strings import ( find_all, strSlash, @@ -88,6 +88,15 @@ struct URIDelimiters: alias PATH = strSlash alias ROOT_PATH = strSlash alias CHAR_ESCAPE = "%" + alias AUTHORITY = "@" + alias QUERY = "?" + alias SCHEME = ":" + + +struct PortBounds: + # For port parsing + alias NINE: UInt8 = ord("9") + alias ZERO: UInt8 = ord("0") @value @@ -143,15 +152,15 @@ struct URI(Writable, Stringable, Representable): # Parse the scheme, if exists. # Assume http if no scheme is provided, fairly safe given the context of lightbug. var scheme: String = "http" - if Constant.COLON in reader: - scheme = str(reader.read_until(Constant.COLON)) + if ord(URIDelimiters.SCHEME) in reader: + scheme = str(reader.read_until(ord(URIDelimiters.SCHEME))) if reader.read_bytes(3) != "://".as_bytes(): raise Error("URI.parse: Invalid URI format, scheme should be followed by `://`. Received: " + uri) # Parse the user info, if exists. var user_info: String = "" - if Constant.AT in reader: - user_info = str(reader.read_until(Constant.AT)) + if ord(URIDelimiters.AUTHORITY) in reader: + user_info = str(reader.read_until(ord(URIDelimiters.AUTHORITY))) reader.increment(1) # TODOs (@thatstoasty) @@ -159,8 +168,8 @@ struct URI(Writable, Stringable, Representable): # Handle string host # A query right after the domain is a valid uri, but it's equivalent to example.com/?query # so we should add the normalization of paths - var host_and_port = reader.read_until(Constant.SLASH) - colon = host_and_port.find(Constant.COLON) + var host_and_port = reader.read_until(ord(URIDelimiters.PATH)) + colon = host_and_port.find(ord(URIDelimiters.SCHEME)) var host: String var port: Optional[UInt16] = None if colon != -1: @@ -168,7 +177,7 @@ struct URI(Writable, Stringable, Representable): var port_end = colon + 1 # loop through the post colon chunk until we find a non-digit character for b in host_and_port[colon + 1 :]: - if b[] < Constant.ZERO or b[] > Constant.NINE: + if b[] < PortBounds.ZERO or b[] > PortBounds.NINE: break port_end += 1 port = UInt16(atol(str(host_and_port[colon + 1 : port_end]))) @@ -177,7 +186,7 @@ struct URI(Writable, Stringable, Representable): # Reads until either the start of the query string, or the end of the uri. var unquote_reader = reader.copy() - var original_path_bytes = unquote_reader.read_until(Constant.QUESTION) + var original_path_bytes = unquote_reader.read_until(ord(URIDelimiters.QUERY)) var original_path: String if not original_path_bytes: original_path = "/" @@ -187,16 +196,16 @@ struct URI(Writable, Stringable, Representable): # Parse the path var path: String = "/" var request_uri: String = "/" - if reader.available() and reader.peek() == Constant.SLASH: + if reader.available() and reader.peek() == ord(URIDelimiters.PATH): # Copy the remaining bytes to read the request uri. var request_uri_reader = reader.copy() request_uri = str(request_uri_reader.read_bytes()) # Read until the query string, or the end if there is none. - path = unquote(str(reader.read_until(Constant.QUESTION)), disallowed_escapes=List(str("/"))) + path = unquote(str(reader.read_until(ord(URIDelimiters.QUERY))), disallowed_escapes=List(str("/"))) # Parse query var query: String = "" - if reader.available() and reader.peek() == Constant.QUESTION: + if reader.available() and reader.peek() == ord(URIDelimiters.QUERY): # TODO: Handle fragments for anchors query = str(reader.read_bytes()[1:]) From fc9b238816e3e67240fa0278f9e58872a5b8b966 Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Wed, 29 Jan 2025 16:58:58 -0600 Subject: [PATCH 11/13] fix scheme parsing bug and remove constants --- lightbug_http/client.mojo | 8 +++----- lightbug_http/io/bytes.mojo | 17 ++++++----------- lightbug_http/uri.mojo | 2 +- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index 12dc8db1..5ceb7288 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -55,8 +55,6 @@ struct Client: """ if request.uri.host == "": raise Error("Client.do: Host must not be empty.") - if not request.uri.port: - raise Error("Client.do: You must specify the port to connect on.") var is_tls = False var scheme = Scheme.HTTP @@ -64,8 +62,8 @@ struct Client: is_tls = True scheme = Scheme.HTTPS - var uri = URI.parse(request.uri.host) - var pool_key = PoolKey(uri.host, uri.port.value(), scheme) + port = request.uri.port.value() if request.uri.port else 80 + var pool_key = PoolKey(request.uri.host, port, scheme) var cached_connection = False var conn: TCPConnection try: @@ -73,7 +71,7 @@ struct Client: cached_connection = True except e: if str(e) == "PoolManager.take: Key not found.": - conn = create_connection(uri.host, uri.port.value()) + conn = create_connection(request.uri.host, port) else: logger.error(e) raise Error("Client.do: Failed to create a connection to host.") diff --git a/lightbug_http/io/bytes.mojo b/lightbug_http/io/bytes.mojo index 4f4c449f..007fbdf4 100644 --- a/lightbug_http/io/bytes.mojo +++ b/lightbug_http/io/bytes.mojo @@ -1,17 +1,12 @@ from utils import StringSlice from memory.span import Span, _SpanIter +from lightbug_http.strings import BytesConstant from lightbug_http.net import default_buffer_size alias Bytes = List[Byte, True] -struct Constant: - alias WHITESPACE: UInt8 = ord(" ") - alias CR: UInt8 = ord("\r") - alias LF: UInt8 = ord("\n") - - @always_inline fn byte(s: String) -> Byte: return ord(s) @@ -24,12 +19,12 @@ fn bytes(s: String) -> Bytes: @always_inline fn is_newline(b: Byte) -> Bool: - return b == Constant.LF or b == Constant.CR + return b == BytesConstant.nChar or b == BytesConstant.rChar @always_inline fn is_space(b: Byte) -> Bool: - return b == Constant.WHITESPACE + return b == BytesConstant.whitespace struct ByteWriter(Writer): @@ -223,7 +218,7 @@ struct ByteReader[origin: Origin]: @always_inline fn read_word(mut self) -> ByteView[origin]: - return self.read_until(Constant.WHITESPACE) + return self.read_until(BytesConstant.whitespace) fn read_line(mut self) -> ByteView[origin]: var start = self.read_pos @@ -237,7 +232,7 @@ struct ByteReader[origin: Origin]: if not self.available(): return ret - if self._inner[self.read_pos] == Constant.CR: + if self._inner[self.read_pos] == BytesConstant.rChar: self.increment(2) else: self.increment() @@ -254,7 +249,7 @@ struct ByteReader[origin: Origin]: @always_inline fn skip_carriage_return(mut self): for i in range(self.read_pos, len(self._inner)): - if self._inner[i] == Constant.CR: + if self._inner[i] == BytesConstant.rChar: self.increment(2) else: break diff --git a/lightbug_http/uri.mojo b/lightbug_http/uri.mojo index 5744ffb2..cb84c9f4 100644 --- a/lightbug_http/uri.mojo +++ b/lightbug_http/uri.mojo @@ -152,7 +152,7 @@ struct URI(Writable, Stringable, Representable): # Parse the scheme, if exists. # Assume http if no scheme is provided, fairly safe given the context of lightbug. var scheme: String = "http" - if ord(URIDelimiters.SCHEME) in reader: + if "://" in uri: scheme = str(reader.read_until(ord(URIDelimiters.SCHEME))) if reader.read_bytes(3) != "://".as_bytes(): raise Error("URI.parse: Invalid URI format, scheme should be followed by `://`. Received: " + uri) From 3690354c7c02e7dde6e42029b963b8e34da19bbd Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Wed, 29 Jan 2025 17:03:32 -0600 Subject: [PATCH 12/13] fix scheme parsing bug and remove constants --- lightbug_http/client.mojo | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index 5ceb7288..08d405fe 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -62,7 +62,17 @@ struct Client: is_tls = True scheme = Scheme.HTTPS - port = request.uri.port.value() if request.uri.port else 80 + var port: UInt16 + if request.uri.port: + port = request.uri.port.value() + else: + if request.uri.scheme == Scheme.HTTP.value: + port = 80 + elif request.uri.scheme == Scheme.HTTPS.value: + port = 443 + else: + raise Error("Client.do: Invalid scheme received in the URI.") + var pool_key = PoolKey(request.uri.host, port, scheme) var cached_connection = False var conn: TCPConnection From e0ef3d4675bf1f6c22fbe4c6af193c1fb11fb89c Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Thu, 30 Jan 2025 09:56:05 -0600 Subject: [PATCH 13/13] remove log in teardown --- lightbug_http/socket.mojo | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lightbug_http/socket.mojo b/lightbug_http/socket.mojo index 732a1217..b794a165 100644 --- a/lightbug_http/socket.mojo +++ b/lightbug_http/socket.mojo @@ -168,11 +168,7 @@ struct Socket[AddrType: Addr, address_family: Int = AF_INET](Representable, Stri logger.debug("Socket.teardown: Failed to shutdown socket: " + str(e)) if not self._closed: - try: - self.close() - except e: - logger.error("Socket.teardown: Failed to close socket.") - raise e + self.close() fn __enter__(owned self) -> Self: return self^