From d7cdfd4e4490b590765a249131eaa9f701800f05 Mon Sep 17 00:00:00 2001 From: Brian Grenier Date: Thu, 26 Sep 2024 20:52:49 -0600 Subject: [PATCH 1/4] Split http module into multiple files Signed-off-by: Brian Grenier --- lightbug_http/header.mojo | 1 + lightbug_http/http.mojo | 369 ------------------------ lightbug_http/http/__init__.mojo | 13 + lightbug_http/http/common_response.mojo | 45 +++ lightbug_http/http/http_version.mojo | 25 ++ lightbug_http/http/request.mojo | 147 ++++++++++ lightbug_http/http/response.mojo | 175 +++++++++++ tests/lightbug_http/test_http.mojo | 6 +- 8 files changed, 411 insertions(+), 370 deletions(-) delete mode 100644 lightbug_http/http.mojo create mode 100644 lightbug_http/http/__init__.mojo create mode 100644 lightbug_http/http/common_response.mojo create mode 100644 lightbug_http/http/http_version.mojo create mode 100644 lightbug_http/http/request.mojo create mode 100644 lightbug_http/http/response.mojo diff --git a/lightbug_http/header.mojo b/lightbug_http/header.mojo index 3e9db7a3..c223be11 100644 --- a/lightbug_http/header.mojo +++ b/lightbug_http/header.mojo @@ -11,6 +11,7 @@ struct HeaderKey: alias CONTENT_TYPE = "content-type" alias CONTENT_LENGTH = "content-length" alias CONTENT_ENCODING = "content-encoding" + alias TRANSFER_ENCODING = "transfer-encoding" alias DATE = "date" alias LOCATION = "location" alias HOST = "host" diff --git a/lightbug_http/http.mojo b/lightbug_http/http.mojo deleted file mode 100644 index 6d8804a3..00000000 --- a/lightbug_http/http.mojo +++ /dev/null @@ -1,369 +0,0 @@ -from utils.string_slice import StringSlice -from utils import Span -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 -from lightbug_http.header import Headers, HeaderKey, Header, write_header -from lightbug_http.io.sync import Duration -from lightbug_http.net import Addr, TCPAddr -from lightbug_http.strings import ( - strHttp11, - strHttp, - strSlash, - whitespace, - rChar, - nChar, - lineBreak, - to_string, -) - - -alias OK_MESSAGE = String("OK").as_bytes() -alias NOT_FOUND_MESSAGE = String("Not Found").as_bytes() -alias TEXT_PLAIN_CONTENT_TYPE = String("text/plain").as_bytes() -alias OCTET_STREAM_CONTENT_TYPE = String("application/octet-stream").as_bytes() - - -@always_inline -fn encode(owned req: HTTPRequest) -> Bytes: - return req._encoded() - - -@always_inline -fn encode(owned res: HTTPResponse) -> Bytes: - return res._encoded() - - -struct StatusCode: - alias OK = 200 - alias MOVED_PERMANENTLY = 301 - alias FOUND = 302 - alias TEMPORARY_REDIRECT = 307 - alias PERMANENT_REDIRECT = 308 - alias NOT_FOUND = 404 - - -@value -struct HTTPRequest(Formattable, Stringable): - var headers: Headers - var uri: URI - var body_raw: Bytes - - var method: String - var protocol: String - - var server_is_tls: Bool - var timeout: Duration - - @staticmethod - fn from_bytes(addr: String, max_body_size: Int, owned b: Bytes) raises -> HTTPRequest: - var reader = ByteReader(b^) - var headers = Headers() - var method: String - var protocol: String - var uri_str: String - try: - method, uri_str, protocol = headers.parse_raw(reader) - except e: - raise Error("Failed to parse request headers: " + e.__str__()) - - var uri = URI.parse_raises(addr + uri_str) - - var content_length = headers.content_length() - - if content_length > 0 and max_body_size > 0 and content_length > max_body_size: - raise Error("Request body too large") - - var request = HTTPRequest(uri, headers=headers, method=method, protocol=protocol) - - try: - request.read_body(reader, content_length, max_body_size) - except e: - raise Error("Failed to read request body: " + e.__str__()) - - return request - - fn __init__( - inout self, - uri: URI, - headers: Headers = Headers(), - method: String = "GET", - protocol: String = strHttp11, - body: Bytes = Bytes(), - server_is_tls: Bool = False, - timeout: Duration = Duration(), - ): - self.headers = headers - self.method = method - self.protocol = protocol - self.uri = uri - self.body_raw = body - self.server_is_tls = server_is_tls - self.timeout = timeout - self.set_content_length(len(body)) - if HeaderKey.CONNECTION not in self.headers: - self.set_connection_close() - if HeaderKey.HOST not in self.headers: - self.headers[HeaderKey.HOST] = uri.host - - fn set_connection_close(inout self): - self.headers[HeaderKey.CONNECTION] = "close" - - fn set_content_length(inout self, l: Int): - self.headers[HeaderKey.CONTENT_LENGTH] = str(l) - - fn connection_close(self) -> Bool: - return self.headers[HeaderKey.CONNECTION] == "close" - - @always_inline - fn read_body(inout self, inout r: ByteReader, content_length: Int, max_body_size: Int) raises -> None: - if content_length > max_body_size: - raise Error("Request body too large") - - r.consume(self.body_raw, content_length) - self.set_content_length(content_length) - - fn format_to(self, inout writer: Formatter): - writer.write(self.method, whitespace) - path = self.uri.path if len(self.uri.path) > 1 else strSlash - if len(self.uri.query_string) > 0: - path += "?" + self.uri.query_string - - writer.write(path) - - writer.write( - whitespace, - self.protocol, - lineBreak, - ) - - self.headers.format_to(writer) - writer.write(lineBreak) - writer.write(to_string(self.body_raw)) - - fn _encoded(inout self) -> Bytes: - """Encodes request as bytes. - - This method consumes the data in this request and it should - no longer be considered valid. - """ - var writer = ByteWriter() - writer.write(self.method) - writer.write(whitespace) - var path = self.uri.path if len(self.uri.path) > 1 else strSlash - if len(self.uri.query_string) > 0: - path += "?" + self.uri.query_string - writer.write(path) - writer.write(whitespace) - writer.write(self.protocol) - writer.write(lineBreak) - - self.headers.encode_to(writer) - writer.write(lineBreak) - - writer.write(self.body_raw) - - return writer.consume() - - fn __str__(self) -> String: - return to_string(self) - - -@value -struct HTTPResponse(Formattable, Stringable): - var headers: Headers - var body_raw: Bytes - - var status_code: Int - var status_text: String - var protocol: String - - @staticmethod - fn from_bytes(owned b: Bytes) raises -> HTTPResponse: - var reader = ByteReader(b^) - - var headers = Headers() - var protocol: String - var status_code: String - var status_text: String - - try: - protocol, status_code, status_text = headers.parse_raw(reader) - except e: - raise Error("Failed to parse response headers: " + e.__str__()) - - var response = HTTPResponse( - Bytes(), - headers=headers, - protocol=protocol, - status_code=int(status_code), - status_text=status_text, - ) - - try: - response.read_body(reader) - return response - except e: - raise Error("Failed to read request body: " + e.__str__()) - - fn __init__( - inout self, - body_bytes: Bytes, - headers: Headers = Headers(), - status_code: Int = 200, - status_text: String = "OK", - protocol: String = strHttp11, - ): - self.headers = headers - if HeaderKey.CONTENT_TYPE not in self.headers: - self.headers[HeaderKey.CONTENT_TYPE] = "application/octet-stream" - self.status_code = status_code - self.status_text = status_text - self.protocol = protocol - self.body_raw = body_bytes - if HeaderKey.CONNECTION not in self.headers: - self.set_connection_keep_alive() - if HeaderKey.CONTENT_LENGTH not in self.headers: - self.set_content_length(len(body_bytes)) - if HeaderKey.DATE not in self.headers: - try: - var current_time = now(utc=True).__str__() - self.headers[HeaderKey.DATE] = current_time - except: - pass - - fn get_body_bytes(self) -> Bytes: - return self.body_raw - - @always_inline - fn set_connection_close(inout self): - self.headers[HeaderKey.CONNECTION] = "close" - - @always_inline - fn set_connection_keep_alive(inout self): - self.headers[HeaderKey.CONNECTION] = "keep-alive" - - fn connection_close(self) -> Bool: - return self.headers[HeaderKey.CONNECTION] == "close" - - @always_inline - fn set_content_length(inout self, l: Int): - self.headers[HeaderKey.CONTENT_LENGTH] = str(l) - - @always_inline - fn content_length(inout self) -> Int: - try: - return int(self.headers[HeaderKey.CONTENT_LENGTH]) - except: - return 0 - - fn is_redirect(self) -> Bool: - return ( - self.status_code == StatusCode.MOVED_PERMANENTLY - or self.status_code == StatusCode.FOUND - or self.status_code == StatusCode.TEMPORARY_REDIRECT - or self.status_code == StatusCode.PERMANENT_REDIRECT - ) - - @always_inline - fn read_body(inout self, inout r: ByteReader) raises -> None: - r.consume(self.body_raw, self.content_length()) - self.set_content_length(len(self.body_raw)) - - fn format_to(self, inout writer: Formatter): - writer.write( - self.protocol, - whitespace, - self.status_code, - whitespace, - self.status_text, - lineBreak, - "server: lightbug_http", - lineBreak, - ) - - self.headers.format_to(writer) - - writer.write(lineBreak) - writer.write(to_string(self.body_raw)) - - fn _encoded(inout self) -> Bytes: - """Encodes response as bytes. - - This method consumes the data in this request and it should - no longer be considered valid. - """ - var writer = ByteWriter() - writer.write(self.protocol) - writer.write(whitespace) - writer.write(bytes(str(self.status_code))) - writer.write(whitespace) - writer.write(self.status_text) - writer.write(lineBreak) - writer.write("server: lightbug_http") - writer.write(lineBreak) - - if HeaderKey.DATE not in self.headers: - try: - var current_time = now(utc=True).__str__() - write_header(writer, HeaderKey.DATE, current_time) - except: - pass - - self.headers.encode_to(writer) - - writer.write(lineBreak) - writer.write(self.body_raw) - - return writer.consume() - - fn __str__(self) -> String: - return to_string(self) - - -fn OK(body: String) -> HTTPResponse: - return HTTPResponse( - headers=Headers(Header(HeaderKey.CONTENT_TYPE, "text/plain")), - body_bytes=bytes(body), - ) - - -fn OK(body: String, content_type: String) -> HTTPResponse: - return HTTPResponse( - headers=Headers(Header(HeaderKey.CONTENT_TYPE, content_type)), - body_bytes=bytes(body), - ) - - -fn OK(body: Bytes) -> HTTPResponse: - return HTTPResponse( - headers=Headers(Header(HeaderKey.CONTENT_TYPE, "text/plain")), - body_bytes=body, - ) - - -fn OK(body: Bytes, content_type: String) -> HTTPResponse: - return HTTPResponse( - headers=Headers(Header(HeaderKey.CONTENT_TYPE, content_type)), - body_bytes=body, - ) - - -fn OK(body: Bytes, content_type: String, content_encoding: String) -> HTTPResponse: - return HTTPResponse( - headers=Headers( - Header(HeaderKey.CONTENT_TYPE, content_type), - Header(HeaderKey.CONTENT_ENCODING, content_encoding), - ), - body_bytes=body, - ) - - -fn NotFound(path: String) -> HTTPResponse: - return HTTPResponse( - status_code=404, - status_text="Not Found", - headers=Headers(Header(HeaderKey.CONTENT_TYPE, "text/plain")), - body_bytes=bytes("path " + path + " not found"), - ) diff --git a/lightbug_http/http/__init__.mojo b/lightbug_http/http/__init__.mojo new file mode 100644 index 00000000..927b1a6d --- /dev/null +++ b/lightbug_http/http/__init__.mojo @@ -0,0 +1,13 @@ +from .common_response import * +from .response import * +from .request import * + + +@always_inline +fn encode(owned req: HTTPRequest) -> Bytes: + return req._encoded() + + +@always_inline +fn encode(owned res: HTTPResponse) -> Bytes: + return res._encoded() diff --git a/lightbug_http/http/common_response.mojo b/lightbug_http/http/common_response.mojo new file mode 100644 index 00000000..65b09888 --- /dev/null +++ b/lightbug_http/http/common_response.mojo @@ -0,0 +1,45 @@ +fn OK(body: String) -> HTTPResponse: + return HTTPResponse( + headers=Headers(Header(HeaderKey.CONTENT_TYPE, "text/plain")), + body_bytes=bytes(body), + ) + + +fn OK(body: String, content_type: String) -> HTTPResponse: + return HTTPResponse( + headers=Headers(Header(HeaderKey.CONTENT_TYPE, content_type)), + body_bytes=bytes(body), + ) + + +fn OK(body: Bytes) -> HTTPResponse: + return HTTPResponse( + headers=Headers(Header(HeaderKey.CONTENT_TYPE, "text/plain")), + body_bytes=body, + ) + + +fn OK(body: Bytes, content_type: String) -> HTTPResponse: + return HTTPResponse( + headers=Headers(Header(HeaderKey.CONTENT_TYPE, content_type)), + body_bytes=body, + ) + + +fn OK(body: Bytes, content_type: String, content_encoding: String) -> HTTPResponse: + return HTTPResponse( + headers=Headers( + Header(HeaderKey.CONTENT_TYPE, content_type), + Header(HeaderKey.CONTENT_ENCODING, content_encoding), + ), + body_bytes=body, + ) + + +fn NotFound(path: String) -> HTTPResponse: + return HTTPResponse( + status_code=404, + status_text="Not Found", + headers=Headers(Header(HeaderKey.CONTENT_TYPE, "text/plain")), + body_bytes=bytes("path " + path + " not found"), + ) diff --git a/lightbug_http/http/http_version.mojo b/lightbug_http/http/http_version.mojo new file mode 100644 index 00000000..c63809f0 --- /dev/null +++ b/lightbug_http/http/http_version.mojo @@ -0,0 +1,25 @@ +# TODO: Can't be used yet. +# This doesn't work right because of float point +# Shenaningans and round() doesn't give me what I want +@value +@register_passable("trivial") +struct HttpVersion(EqualityComparable, Stringable): + var _v: Float64 + + fn __init__(inout self, version: String) raises: + self._v = atof(version[version.find("/") + 1 :]) + + fn __eq__(self, other: Self) -> Bool: + return self._v == other._v + + fn __ne__(self, other: Self) -> Bool: + return self._v != other._v + + fn __eq__(self, other: Float64) -> Bool: + return self._v == other + + fn __ne__(self, other: Float64) -> Bool: + return self._v != other + + fn __str__(self) -> String: + return "HTTP/" + str(self._v) diff --git a/lightbug_http/http/request.mojo b/lightbug_http/http/request.mojo new file mode 100644 index 00000000..318dcaef --- /dev/null +++ b/lightbug_http/http/request.mojo @@ -0,0 +1,147 @@ +from lightbug_http.io.bytes import Bytes, bytes, Byte +from lightbug_http.header import Headers, HeaderKey, Header, write_header +from lightbug_http.uri import URI +from lightbug_http.utils import ByteReader, ByteWriter +from lightbug_http.io.bytes import Bytes, bytes, Byte +from lightbug_http.io.sync import Duration +from lightbug_http.strings import ( + strHttp11, + strHttp, + strSlash, + whitespace, + rChar, + nChar, + lineBreak, + to_string, +) + + +@always_inline +fn encode(owned req: HTTPRequest) -> Bytes: + return req._encoded() + + +@value +struct HTTPRequest(Formattable, Stringable): + var headers: Headers + var uri: URI + var body_raw: Bytes + + var method: String + var protocol: String + + var server_is_tls: Bool + var timeout: Duration + + @staticmethod + fn from_bytes(addr: String, max_body_size: Int, owned b: Bytes) raises -> HTTPRequest: + var reader = ByteReader(b^) + var headers = Headers() + var method: String + var protocol: String + var uri_str: String + try: + method, uri_str, protocol = headers.parse_raw(reader) + except e: + raise Error("Failed to parse request headers: " + e.__str__()) + + var uri = URI.parse_raises(addr + uri_str) + + var content_length = headers.content_length() + + if content_length > 0 and max_body_size > 0 and content_length > max_body_size: + raise Error("Request body too large") + + var request = HTTPRequest(uri, headers=headers, method=method, protocol=protocol) + + try: + request.read_body(reader, content_length, max_body_size) + except e: + raise Error("Failed to read request body: " + e.__str__()) + + return request + + fn __init__( + inout self, + uri: URI, + headers: Headers = Headers(), + method: String = "GET", + protocol: String = strHttp11, + body: Bytes = Bytes(), + server_is_tls: Bool = False, + timeout: Duration = Duration(), + ): + self.headers = headers + self.method = method + self.protocol = protocol + self.uri = uri + self.body_raw = body + self.server_is_tls = server_is_tls + self.timeout = timeout + self.set_content_length(len(body)) + if HeaderKey.CONNECTION not in self.headers: + self.set_connection_close() + if HeaderKey.HOST not in self.headers: + self.headers[HeaderKey.HOST] = uri.host + + fn set_connection_close(inout self): + self.headers[HeaderKey.CONNECTION] = "close" + + fn set_content_length(inout self, l: Int): + self.headers[HeaderKey.CONTENT_LENGTH] = str(l) + + fn connection_close(self) -> Bool: + return self.headers[HeaderKey.CONNECTION] == "close" + + @always_inline + fn read_body(inout self, inout r: ByteReader, content_length: Int, max_body_size: Int) raises -> None: + if content_length > max_body_size: + raise Error("Request body too large") + + r.consume(self.body_raw, content_length) + self.set_content_length(content_length) + + fn format_to(self, inout writer: Formatter): + writer.write(self.method, whitespace) + path = self.uri.path if len(self.uri.path) > 1 else strSlash + if len(self.uri.query_string) > 0: + path += "?" + self.uri.query_string + + writer.write(path) + + writer.write( + whitespace, + self.protocol, + lineBreak, + ) + + self.headers.format_to(writer) + writer.write(lineBreak) + writer.write(to_string(self.body_raw)) + + fn _encoded(inout self) -> Bytes: + """Encodes request as bytes. + + This method consumes the data in this request and it should + no longer be considered valid. + """ + var writer = ByteWriter() + writer.write(self.method) + writer.write(whitespace) + var path = self.uri.path if len(self.uri.path) > 1 else strSlash + if len(self.uri.query_string) > 0: + path += "?" + self.uri.query_string + writer.write(path) + writer.write(whitespace) + writer.write(self.protocol) + writer.write(lineBreak) + + self.headers.encode_to(writer) + writer.write(lineBreak) + + writer.write(self.body_raw) + + return writer.consume() + + fn __str__(self) -> String: + return to_string(self) diff --git a/lightbug_http/http/response.mojo b/lightbug_http/http/response.mojo new file mode 100644 index 00000000..6634fd18 --- /dev/null +++ b/lightbug_http/http/response.mojo @@ -0,0 +1,175 @@ +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 +from lightbug_http.strings import ( + strHttp11, + strHttp, + strSlash, + whitespace, + rChar, + nChar, + lineBreak, + to_string, +) + + +struct StatusCode: + alias OK = 200 + alias MOVED_PERMANENTLY = 301 + alias FOUND = 302 + alias TEMPORARY_REDIRECT = 307 + alias PERMANENT_REDIRECT = 308 + alias NOT_FOUND = 404 + + +@value +struct HTTPResponse(Formattable, Stringable): + var headers: Headers + var body_raw: Bytes + + var status_code: Int + var status_text: String + var protocol: String + + @staticmethod + fn from_bytes(owned b: Bytes) raises -> HTTPResponse: + var reader = ByteReader(b^) + + var headers = Headers() + var protocol: String + var status_code: String + var status_text: String + + try: + protocol, status_code, status_text = headers.parse_raw(reader) + except e: + raise Error("Failed to parse response headers: " + e.__str__()) + + var response = HTTPResponse( + Bytes(), + headers=headers, + protocol=protocol, + status_code=int(status_code), + status_text=status_text, + ) + + try: + response.read_body(reader) + return response + except e: + raise Error("Failed to read request body: " + e.__str__()) + + fn __init__( + inout self, + body_bytes: Bytes, + headers: Headers = Headers(), + status_code: Int = 200, + status_text: String = "OK", + protocol: String = strHttp11, + ): + self.headers = headers + if HeaderKey.CONTENT_TYPE not in self.headers: + self.headers[HeaderKey.CONTENT_TYPE] = "application/octet-stream" + self.status_code = status_code + self.status_text = status_text + self.protocol = protocol + self.body_raw = body_bytes + if HeaderKey.CONNECTION not in self.headers: + self.set_connection_keep_alive() + if HeaderKey.CONTENT_LENGTH not in self.headers: + self.set_content_length(len(body_bytes)) + if HeaderKey.DATE not in self.headers: + try: + var current_time = now(utc=True).__str__() + self.headers[HeaderKey.DATE] = current_time + except: + pass + + fn get_body_bytes(self) -> Bytes: + return self.body_raw + + @always_inline + fn set_connection_close(inout self): + self.headers[HeaderKey.CONNECTION] = "close" + + @always_inline + fn set_connection_keep_alive(inout self): + self.headers[HeaderKey.CONNECTION] = "keep-alive" + + fn connection_close(self) -> Bool: + return self.headers[HeaderKey.CONNECTION] == "close" + + @always_inline + fn set_content_length(inout self, l: Int): + self.headers[HeaderKey.CONTENT_LENGTH] = str(l) + + @always_inline + fn content_length(inout self) -> Int: + try: + return int(self.headers[HeaderKey.CONTENT_LENGTH]) + except: + return 0 + + fn is_redirect(self) -> Bool: + return ( + self.status_code == StatusCode.MOVED_PERMANENTLY + or self.status_code == StatusCode.FOUND + or self.status_code == StatusCode.TEMPORARY_REDIRECT + or self.status_code == StatusCode.PERMANENT_REDIRECT + ) + + @always_inline + fn read_body(inout self, inout r: ByteReader) raises -> None: + r.consume(self.body_raw, self.content_length()) + self.set_content_length(len(self.body_raw)) + + fn format_to(self, inout writer: Formatter): + writer.write( + self.protocol, + whitespace, + self.status_code, + whitespace, + self.status_text, + lineBreak, + "server: lightbug_http", + lineBreak, + ) + + self.headers.format_to(writer) + + writer.write(lineBreak) + writer.write(to_string(self.body_raw)) + + fn _encoded(inout self) -> Bytes: + """Encodes response as bytes. + + This method consumes the data in this request and it should + no longer be considered valid. + """ + var writer = ByteWriter() + writer.write(self.protocol) + writer.write(whitespace) + writer.write(bytes(str(self.status_code))) + writer.write(whitespace) + writer.write(self.status_text) + writer.write(lineBreak) + writer.write("server: lightbug_http") + writer.write(lineBreak) + + if HeaderKey.DATE not in self.headers: + try: + var current_time = now(utc=True).__str__() + write_header(writer, HeaderKey.DATE, current_time) + except: + pass + + self.headers.encode_to(writer) + + writer.write(lineBreak) + writer.write(self.body_raw) + + return writer.consume() + + fn __str__(self) -> String: + return to_string(self) diff --git a/tests/lightbug_http/test_http.mojo b/tests/lightbug_http/test_http.mojo index 2a9a1cb3..a8acc6ac 100644 --- a/tests/lightbug_http/test_http.mojo +++ b/tests/lightbug_http/test_http.mojo @@ -9,7 +9,6 @@ from lightbug_http.strings import to_string alias default_server_conn_string = "http://localhost:8080" - def test_encode_http_request(): var uri = URI.parse_raises(default_server_conn_string + "/foobar?baz") var req = HTTPRequest( @@ -43,3 +42,8 @@ def test_encode_http_response(): testing.assert_equal(res_encoded, expected_full) testing.assert_equal(res_encoded, as_str) + + +# def test_http_version_parse(): +# var v1 = HttpVersion("HTTP/1.1") +# testing.assert_equal(v1, 1.1) \ No newline at end of file From 5052a855d36df190f31ee7c5dc74e62804a31497 Mon Sep 17 00:00:00 2001 From: Val Date: Fri, 27 Sep 2024 22:11:10 +0200 Subject: [PATCH 2/4] remove sys package --- README.md | 12 +- bench_server.mojo | 4 +- client.mojo | 10 +- "lightbug.\360\237\224\245" | 4 +- lightbug_http/__init__.mojo | 2 +- lightbug_http/client.mojo | 119 ++++- lightbug_http/header.mojo | 2 +- lightbug_http/net.mojo | 463 ++++++++++++++++-- lightbug_http/server.mojo | 201 +++++++- lightbug_http/sys/__init__.mojo | 0 lightbug_http/sys/client.mojo | 123 ----- lightbug_http/sys/net.mojo | 442 ----------------- lightbug_http/sys/server.mojo | 195 -------- .../lightbug_http/{sys => }/test_client.mojo | 14 +- 14 files changed, 746 insertions(+), 845 deletions(-) delete mode 100644 lightbug_http/sys/__init__.mojo delete mode 100644 lightbug_http/sys/client.mojo delete mode 100644 lightbug_http/sys/net.mojo delete mode 100644 lightbug_http/sys/server.mojo rename tests/lightbug_http/{sys => }/test_client.mojo (91%) diff --git a/README.md b/README.md index 304bfa39..7ba91188 100644 --- a/README.md +++ b/README.md @@ -106,10 +106,10 @@ Once you have a Mojo project set up locally, ``` 6. Start a server listening on a port with your service like so. ```mojo - from lightbug_http import Welcome, SysServer + from lightbug_http import Welcome, Server fn main() raises: - var server = SysServer() + var server = Server() var handler = Welcome() server.listen_and_serve("0.0.0.0:8080", handler) ``` @@ -180,9 +180,9 @@ Create a file, e.g `client.mojo` with the following code. Run `magic run mojo cl ```mojo from lightbug_http import * -from lightbug_http.sys.client import MojoClient +from lightbug_http.client import Client -fn test_request(inout client: MojoClient) raises -> None: +fn test_request(inout client: Client) raises -> None: var uri = URI.parse_raises("http://httpbin.org/status/404") var headers = Header("Host", "httpbin.org") @@ -207,7 +207,7 @@ fn test_request(inout client: MojoClient) raises -> None: fn main() -> None: try: - var client = MojoClient() + var client = Client() test_request(client) except e: print(e) @@ -216,7 +216,7 @@ fn main() -> None: Pure Mojo-based client is available by default. This client is also used internally for testing the server. ## Switching between pure Mojo and Python implementations -By default, Lightbug uses the pure Mojo implementation for networking. To use Python's `socket` library instead, just import the `PythonServer` instead of the `SysServer` with the following line: +By default, Lightbug uses the pure Mojo implementation for networking. To use Python's `socket` library instead, just import the `PythonServer` instead of the `Server` with the following line: ```mojo from lightbug_http.python.server import PythonServer ``` diff --git a/bench_server.mojo b/bench_server.mojo index eefdba40..40804494 100644 --- a/bench_server.mojo +++ b/bench_server.mojo @@ -1,10 +1,10 @@ -from lightbug_http.sys.server import SysServer +from lightbug_http.server import Server from lightbug_http.service import TechEmpowerRouter def main(): try: - var server = SysServer(tcp_keep_alive=True) + var server = Server(tcp_keep_alive=True) var handler = TechEmpowerRouter() server.listen_and_serve("0.0.0.0:8080", handler) except e: diff --git a/client.mojo b/client.mojo index 3f91c506..e37f9b21 100644 --- a/client.mojo +++ b/client.mojo @@ -1,10 +1,10 @@ from lightbug_http import * -from lightbug_http.sys.client import MojoClient +from lightbug_http.client import Client -fn test_request(inout client: MojoClient) raises -> None: - var uri = URI.parse_raises("http://httpbin.org/status/404") - var headers = Header("Host", "httpbin.org") +fn test_request(inout client: Client) raises -> None: + var uri = URI.parse_raises("http://google.com") + var headers = Headers(Header("Host", "google.com"), Header("User-Agent", "curl/8.1.2"), Header("Accept", "*/*")) var request = HTTPRequest(uri, headers) var response = client.do(request^) @@ -27,7 +27,7 @@ fn test_request(inout client: MojoClient) raises -> None: fn main() -> None: try: - var client = MojoClient() + var client = Client() test_request(client) except e: print(e) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index 28afc7dc..4cc40e23 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -1,6 +1,6 @@ -from lightbug_http import Welcome, SysServer +from lightbug_http import Welcome, Server fn main() raises: - var server = SysServer() + var server = Server() var handler = Welcome() server.listen_and_serve("0.0.0.0:8080", handler) diff --git a/lightbug_http/__init__.mojo b/lightbug_http/__init__.mojo index a859cf08..7ff9d46d 100644 --- a/lightbug_http/__init__.mojo +++ b/lightbug_http/__init__.mojo @@ -2,7 +2,7 @@ from lightbug_http.http import HTTPRequest, HTTPResponse, OK, NotFound from lightbug_http.uri import URI from lightbug_http.header import Header, Headers, HeaderKey from lightbug_http.service import HTTPService, Welcome -from lightbug_http.sys.server import SysServer +from lightbug_http.server import Server from lightbug_http.strings import to_string diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index c0e3127a..35eaadaf 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -1,12 +1,121 @@ -from lightbug_http.http import HTTPRequest, HTTPResponse +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.io.bytes import Bytes +from lightbug_http.utils import ByteReader +from lightbug_http.net import create_connection, default_buffer_size +from lightbug_http.http import HTTPRequest, HTTPResponse, encode +from lightbug_http.header import Headers, HeaderKey -trait Client: +struct Client: + var host: StringLiteral + var port: Int + var name: String + fn __init__(inout self) raises: - ... + self.host = "127.0.0.1" + self.port = 8888 + self.name = "lightbug_http_client" fn __init__(inout self, host: StringLiteral, port: Int) raises: - ... + self.host = host + self.port = port + self.name = "lightbug_http_client" fn do(self, owned req: 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: + 1. Creates a connection to the server specified in the request. + 2. Sends the request body using the connection. + 3. Receives the response from the server. + 4. Closes the connection. + 5. Returns the received response as an `HTTPResponse` object. + + Note: The code assumes that the `HTTPRequest` object passed as an argument has a valid URI with a host and port specified. + + Parameters + ---------- + req : HTTPRequest : + An `HTTPRequest` object representing the request to be sent. + + Returns + ------- + HTTPResponse : + The received response. + + Raises + ------ + Error : + If there is a failure in sending or receiving the message. + """ + var uri = req.uri + var host = uri.host + + if host == "": + raise Error("URI is nil") + var is_tls = False + + if uri.is_https(): + is_tls = True + + var host_str: String + var port: Int + + if ":" in host: + var host_port = host.split(":") + host_str = host_port[0] + port = atol(host_port[1]) + else: + host_str = host + if is_tls: + port = 443 + else: + port = 80 + + # TODO: Actually handle persistent connections + var conn = create_connection(socket(AF_INET, SOCK_STREAM, 0), host_str, port) + var bytes_sent = conn.write(encode(req)) + if bytes_sent == -1: + raise Error("Failed to send message") + + var new_buf = Bytes(capacity=default_buffer_size) + var bytes_recv = conn.read(new_buf) + print("new_buf:", String(new_buf)) + if bytes_recv == 0: + conn.close() + try: + var res = HTTPResponse.from_bytes(new_buf^) + if res.is_redirect(): + conn.close() + return self._handle_redirect(req^, res^) + return res + except e: + conn.close() + raise e + + return HTTPResponse(Bytes()) + + fn _handle_redirect( + self, owned original_req: HTTPRequest, owned original_response: HTTPResponse + ) raises -> HTTPResponse: + var new_uri: URI + var new_location = original_response.headers[HeaderKey.LOCATION] + if new_location.startswith("http"): + new_uri = URI.parse_raises(new_location) + original_req.headers[HeaderKey.HOST] = new_uri.host + else: + new_uri = original_req.uri + new_uri.path = new_location + original_req.uri = new_uri + return self.do(original_req^) diff --git a/lightbug_http/header.mojo b/lightbug_http/header.mojo index c223be11..31d06acf 100644 --- a/lightbug_http/header.mojo +++ b/lightbug_http/header.mojo @@ -1,6 +1,6 @@ +from collections import Dict from lightbug_http.io.bytes import Bytes, Byte from lightbug_http.strings import BytesConstant -from collections import Dict from lightbug_http.utils import ByteReader, ByteWriter, is_newline, is_space from lightbug_http.strings import rChar, nChar, lineBreak, to_string diff --git a/lightbug_http/net.mojo b/lightbug_http/net.mojo index 1d757ff4..0350f6ba 100644 --- a/lightbug_http/net.mojo +++ b/lightbug_http/net.mojo @@ -1,99 +1,455 @@ -from sys.info import sizeof +from utils import StaticTuple +from time import sleep +from sys.info import sizeof, os_is_macos +from sys.ffi import external_call from lightbug_http.strings import NetworkType -from lightbug_http.io.bytes import Bytes +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.sys.net import SysConnection from .libc import ( c_void, - AF_INET, + c_int, + c_uint, + c_char, + c_ssize_t, + in_addr, sockaddr, sockaddr_in, socklen_t, - getsockname, - getpeername, + AI_PASSIVE, + AF_INET, + AF_INET6, + SOCK_STREAM, + SOL_SOCKET, + SO_REUSEADDR, + SHUT_RDWR, + htons, ntohs, + inet_pton, inet_ntop, + to_char_ptr, + socket, + connect, + setsockopt, + listen, + accept, + send, + recv, + bind, + shutdown, + close, + getsockname, + getpeername, ) + alias default_buffer_size = 4096 alias default_tcp_keep_alive = Duration(15 * 1000 * 1000 * 1000) # 15 seconds -trait Net(DefaultConstructible): - fn __init__(inout self) raises: +trait Connection(Movable): + fn __init__(inout self, laddr: String, raddr: String) raises: ... - fn __init__(inout self, keep_alive: Duration) raises: + fn __init__(inout self, laddr: TCPAddr, raddr: TCPAddr) raises: ... - # A listen method should be implemented on structs that implement Net. - # Signature is not enforced for now. - # fn listen(inout self, network: String, addr: String) raises -> Listener: - # ... + fn read(self, inout buf: Bytes) raises -> Int: + ... + fn write(self, buf: Bytes) raises -> Int: + ... -trait ListenConfig: - fn __init__(inout self, keep_alive: Duration) raises: + fn close(self) raises: + ... + + fn local_addr(inout self) raises -> TCPAddr: ... - # A listen method should be implemented on structs that implement ListenConfig. - # Signature is not enforced for now. - # fn listen(inout self, network: String, address: String) raises -> Listener: - # ... + fn remote_addr(self) raises -> TCPAddr: + ... -trait Listener(Movable): - fn __init__(inout self) raises: +trait Addr(CollectionElement): + fn __init__(inout self): ... - fn __init__(inout self, addr: TCPAddr) raises: + fn __init__(inout self, ip: String, port: Int): ... - fn accept(borrowed self) raises -> SysConnection: + fn network(self) -> String: ... - fn close(self) raises: + fn string(self) -> String: ... - fn addr(self) -> TCPAddr: + +trait AnAddrInfo: + fn get_ip_address(self, host: String) raises -> in_addr: + """ + TODO: Once default functions can be implemented in traits, this function should use the functions currently + implemented in the `addrinfo_macos` and `addrinfo_unix` structs. + """ ... -trait Connection(Movable): +@value +struct NoTLSListener: + """ + A TCP listener that listens for incoming connections and can accept them. + """ + + var fd: c_int + var __addr: TCPAddr + + fn __init__(inout self) raises: + self.__addr = TCPAddr("localhost", 8080) + self.fd = socket(AF_INET, SOCK_STREAM, 0) + + fn __init__(inout self, addr: TCPAddr) raises: + self.__addr = addr + self.fd = socket(AF_INET, SOCK_STREAM, 0) + + fn __init__(inout self, addr: TCPAddr, fd: c_int) raises: + self.__addr = addr + self.fd = fd + + fn accept(self) raises -> SysConnection: + var their_addr = sockaddr(0, StaticTuple[c_char, 14]()) + var their_addr_ptr = Reference[sockaddr](their_addr) + var sin_size = socklen_t(sizeof[socklen_t]()) + var sin_size_ptr = Reference[socklen_t](sin_size) + var new_sockfd = external_call["accept", c_int](self.fd, their_addr_ptr, sin_size_ptr) + + # var new_sockfd = accept( + # self.fd, their_addr_ptr, UnsafePointer[socklen_t].address_of(sin_size) + # ) + if new_sockfd == -1: + print("Failed to accept connection, system accept() returned an error.") + var peer = get_peer_name(new_sockfd) + + return SysConnection(self.__addr, TCPAddr(peer.host, atol(peer.port)), new_sockfd) + + fn close(self) raises: + _ = shutdown(self.fd, SHUT_RDWR) + var close_status = close(self.fd) + if close_status == -1: + print("Failed to close new_sockfd") + + fn addr(self) -> TCPAddr: + return self.__addr + + +struct ListenConfig: + var __keep_alive: Duration + + fn __init__(inout self) raises: + self.__keep_alive = default_tcp_keep_alive + + fn __init__(inout self, keep_alive: Duration) raises: + self.__keep_alive = keep_alive + + fn listen(inout self, network: String, address: String) raises -> NoTLSListener: + var addr = resolve_internet_addr(network, address) + var address_family = AF_INET + var ip_buf_size = 4 + if address_family == AF_INET6: + ip_buf_size = 16 + + var sockfd = socket(address_family, SOCK_STREAM, 0) + if sockfd == -1: + print("Socket creation error") + + var yes: Int = 1 + var setsockopt_result = setsockopt( + sockfd, + SOL_SOCKET, + SO_REUSEADDR, + UnsafePointer[Int].address_of(yes).bitcast[c_void](), + sizeof[Int](), + ) + + var bind_success = False + var bind_fail_logged = False + + var ip_buf = UnsafePointer[c_void].alloc(ip_buf_size) + var conv_status = inet_pton(address_family, to_char_ptr(addr.ip), ip_buf) + var raw_ip = ip_buf.bitcast[c_uint]()[] + var bin_port = htons(UInt16(addr.port)) + + var ai = sockaddr_in(address_family, bin_port, raw_ip, StaticTuple[c_char, 8]()) + var ai_ptr = Reference[sockaddr_in](ai) + + while not bind_success: + # var bind = bind(sockfd, ai_ptr, sizeof[sockaddr_in]()) + var bind = external_call["bind", c_int](sockfd, ai_ptr, sizeof[sockaddr_in]()) + if bind == 0: + bind_success = True + else: + if not bind_fail_logged: + print("Bind attempt failed. The address might be in use or the socket might not be available.") + print("Retrying. Might take 10-15 seconds.") + bind_fail_logged = True + print(".", end="", flush=True) + _ = shutdown(sockfd, SHUT_RDWR) + sleep(1) + + if listen(sockfd, c_int(128)) == -1: + print("Listen failed.\n on sockfd " + sockfd.__str__()) + + var listener = NoTLSListener(addr, sockfd) + + print("\nšŸ”„šŸ Lightbug is listening on " + "http://" + addr.ip + ":" + addr.port.__str__()) + print("Ready to accept connections...") + + return listener + + +@value +struct SysConnection(Connection): + var fd: c_int + var raddr: TCPAddr + var laddr: TCPAddr + fn __init__(inout self, laddr: String, raddr: String) raises: - ... + self.raddr = resolve_internet_addr(NetworkType.tcp4.value, raddr) + self.laddr = resolve_internet_addr(NetworkType.tcp4.value, laddr) + self.fd = socket(AF_INET, SOCK_STREAM, 0) fn __init__(inout self, laddr: TCPAddr, raddr: TCPAddr) raises: - ... + self.raddr = raddr + self.laddr = laddr + self.fd = socket(AF_INET, SOCK_STREAM, 0) + + fn __init__(inout self, laddr: TCPAddr, raddr: TCPAddr, fd: c_int) raises: + self.raddr = raddr + self.laddr = laddr + self.fd = fd fn read(self, inout buf: Bytes) raises -> Int: - ... + var bytes_recv = recv( + self.fd, + buf.unsafe_ptr().offset(buf.size), + buf.capacity - buf.size, + 0, + ) + if bytes_recv == -1: + return 0 + buf.size += bytes_recv + if bytes_recv == 0: + return 0 + if bytes_recv < buf.capacity: + return bytes_recv + return bytes_recv + + fn write(self, owned msg: String) raises -> Int: + var bytes_sent = send(self.fd, msg.unsafe_ptr(), len(msg), 0) + if bytes_sent == -1: + print("Failed to send response") + return bytes_sent fn write(self, buf: Bytes) raises -> Int: - ... + var content = to_string(buf) + print("Content: " + content) + var bytes_sent = send(self.fd, content.unsafe_ptr(), len(content), 0) + if bytes_sent == -1: + print("Failed to send response") + _ = content + return bytes_sent fn close(self) raises: - ... + _ = shutdown(self.fd, SHUT_RDWR) + var close_status = close(self.fd) + if close_status == -1: + print("Failed to close new_sockfd") fn local_addr(inout self) raises -> TCPAddr: - ... + return self.laddr fn remote_addr(self) raises -> TCPAddr: - ... + return self.raddr -trait Addr(CollectionElement): +struct SysNet: + var __lc: ListenConfig + + fn __init__(inout self) raises: + self.__lc = ListenConfig(default_tcp_keep_alive) + + fn __init__(inout self, keep_alive: Duration) raises: + self.__lc = ListenConfig(keep_alive) + + fn listen(inout self, network: String, addr: String) raises -> NoTLSListener: + return self.__lc.listen(network, addr) + + +@value +@register_passable("trivial") +struct addrinfo_macos(AnAddrInfo): + """ + For MacOS, I had to swap the order of ai_canonname and ai_addr. + https://stackoverflow.com/questions/53575101/calling-getaddrinfo-directly-from-python-ai-addr-is-null-pointer. + """ + + var ai_flags: c_int + var ai_family: c_int + var ai_socktype: c_int + var ai_protocol: c_int + var ai_addrlen: socklen_t + var ai_canonname: UnsafePointer[c_char] + var ai_addr: UnsafePointer[sockaddr] + var ai_next: UnsafePointer[c_void] + fn __init__(inout self): - ... + self.ai_flags = 0 + self.ai_family = 0 + self.ai_socktype = 0 + self.ai_protocol = 0 + self.ai_addrlen = 0 + self.ai_canonname = UnsafePointer[c_char]() + self.ai_addr = UnsafePointer[sockaddr]() + self.ai_next = UnsafePointer[c_void]() - fn __init__(inout self, ip: String, port: Int): - ... + fn get_ip_address(self, host: String) raises -> in_addr: + """ + Returns an IP address based on the host. + This is a MacOS-specific implementation. - fn network(self) -> String: - ... + Args: + host: String - The host to get the IP from. - fn string(self) -> String: - ... + Returns: + in_addr - The IP address. + """ + var host_ptr = to_char_ptr(host) + var servinfo = Reference(Self()) + var servname = UnsafePointer[Int8]() + + var hints = Self() + hints.ai_family = AF_INET + hints.ai_socktype = SOCK_STREAM + hints.ai_flags = AI_PASSIVE + + var error = external_call[ + "getaddrinfo", + Int32, + ](host_ptr, servname, Reference(hints), Reference(servinfo)) + + if error != 0: + print("getaddrinfo failed with error code: " + error.__str__()) + raise Error("Failed to get IP address. getaddrinfo failed.") + + var addrinfo = servinfo[] + + var ai_addr = addrinfo.ai_addr + if not ai_addr: + print("ai_addr is null") + raise Error("Failed to get IP address. getaddrinfo was called successfully, but ai_addr is null.") + + var addr_in = ai_addr.bitcast[sockaddr_in]()[] + + return addr_in.sin_addr + + +@value +@register_passable("trivial") +struct addrinfo_unix(AnAddrInfo): + """ + Standard addrinfo struct for Unix systems. Overwrites the existing libc `getaddrinfo` function to adhere to the AnAddrInfo trait. + """ + + var ai_flags: c_int + var ai_family: c_int + var ai_socktype: c_int + var ai_protocol: c_int + var ai_addrlen: socklen_t + var ai_addr: UnsafePointer[sockaddr] + var ai_canonname: UnsafePointer[c_char] + var ai_next: UnsafePointer[c_void] + + fn __init__(inout self): + self.ai_flags = 0 + self.ai_family = 0 + self.ai_socktype = 0 + self.ai_protocol = 0 + self.ai_addrlen = 0 + self.ai_addr = UnsafePointer[sockaddr]() + self.ai_canonname = UnsafePointer[c_char]() + self.ai_next = UnsafePointer[c_void]() + + fn get_ip_address(self, host: String) raises -> in_addr: + """ + Returns an IP address based on the host. + This is a Unix-specific implementation. + + Args: + host: String - The host to get IP from. + + Returns: + UInt32 - The IP address. + """ + var host_ptr = to_char_ptr(host) + var servinfo = UnsafePointer[Self]().alloc(1) + servinfo.init_pointee_move(Self()) + + var hints = Self() + hints.ai_family = AF_INET + hints.ai_socktype = SOCK_STREAM + hints.ai_flags = AI_PASSIVE + + var error = getaddrinfo[Self]( + host_ptr, + UnsafePointer[UInt8](), + UnsafePointer.address_of(hints), + UnsafePointer.address_of(servinfo), + ) + if error != 0: + print("getaddrinfo failed") + raise Error("Failed to get IP address. getaddrinfo failed.") + + var addrinfo = servinfo[] + + var ai_addr = addrinfo.ai_addr + if not ai_addr: + print("ai_addr is null") + raise Error("Failed to get IP address. getaddrinfo was called successfully, but ai_addr is null.") + + var addr_in = ai_addr.bitcast[sockaddr_in]()[] + + return addr_in.sin_addr + + +fn create_connection(sock: c_int, host: String, port: UInt16) raises -> SysConnection: + """ + Connect to a server using a socket. + + Args: + sock: Int32 - The socket file descriptor. + host: String - The host to connect to. + port: UInt16 - The port to connect to. + + Returns: + Int32 - The socket file descriptor. + """ + var ip: in_addr + if os_is_macos(): + ip = addrinfo_macos().get_ip_address(host) + else: + ip = addrinfo_unix().get_ip_address(host) + + # Convert ip address to network byte order. + var addr: sockaddr_in = sockaddr_in(AF_INET, htons(port), ip, StaticTuple[c_char, 8](0, 0, 0, 0, 0, 0, 0, 0)) + var addr_ptr = Reference[sockaddr_in](addr) + + if external_call["connect", c_int](sock, addr_ptr, sizeof[sockaddr_in]()) == -1: + _ = shutdown(sock, SHUT_RDWR) + raise Error("Failed to connect to server") + + var laddr = TCPAddr() + var raddr = TCPAddr(host, int(port)) + var conn = SysConnection(sock, laddr, raddr) + + return conn alias TCPAddrList = List[TCPAddr] @@ -281,3 +637,28 @@ fn get_peer_name(fd: Int32) raises -> HostPort: host=convert_binary_ip_to_string(addr_in.sin_addr.s_addr, AF_INET, 16), port=convert_binary_port_to_int(addr_in.sin_port).__str__(), ) + + +fn getaddrinfo[ + T: AnAddrInfo +]( + nodename: UnsafePointer[c_char], + servname: UnsafePointer[c_char], + hints: UnsafePointer[T], + res: UnsafePointer[UnsafePointer[T]], +) -> c_int: + """ + Overwrites the existing libc `getaddrinfo` function to use the AnAddrInfo trait. + + Libc POSIX `getaddrinfo` function + Reference: https://man7.org/linux/man-pages/man3/getaddrinfo.3p.html + Fn signature: int getaddrinfo(const char *restrict nodename, const char *restrict servname, const struct addrinfo *restrict hints, struct addrinfo **restrict res). + """ + return external_call[ + "getaddrinfo", + c_int, # FnName, RetType + UnsafePointer[c_char], + UnsafePointer[c_char], + UnsafePointer[T], # Args + UnsafePointer[UnsafePointer[T]], # Args + ](nodename, servname, hints, res) diff --git a/lightbug_http/server.mojo b/lightbug_http/server.mojo index 2d17ca57..3ae00b8e 100644 --- a/lightbug_http/server.mojo +++ b/lightbug_http/server.mojo @@ -1,24 +1,195 @@ -from lightbug_http.error import ErrorHandler +from lightbug_http.io.sync import Duration +from lightbug_http.io.bytes import Bytes, bytes +from lightbug_http.strings import NetworkType +from lightbug_http.utils import ByteReader +from lightbug_http.net import NoTLSListener, default_buffer_size, NoTLSListener, SysConnection, SysNet +from lightbug_http.http import HTTPRequest, encode +from lightbug_http.uri import URI +from lightbug_http.header import Headers from lightbug_http.service import HTTPService -from lightbug_http.net import Listener +from lightbug_http.error import ErrorHandler + alias DefaultConcurrency: Int = 256 * 1024 +alias default_max_request_body_size = 4 * 1024 * 1024 # 4MB + + +@value +struct Server: + """ + A Mojo-based server that accept incoming requests and delivers HTTP services. + """ + + var error_handler: ErrorHandler + + var name: String + var __address: String + var max_concurrent_connections: Int + var max_requests_per_connection: Int + + var __max_request_body_size: Int + var tcp_keep_alive: Bool + + var ln: NoTLSListener + + fn __init__(inout self) raises: + self.error_handler = ErrorHandler() + self.name = "lightbug_http" + self.__address = "127.0.0.1" + self.max_concurrent_connections = 1000 + self.max_requests_per_connection = 0 + self.__max_request_body_size = default_max_request_body_size + self.tcp_keep_alive = False + self.ln = NoTLSListener() + + fn __init__(inout self, tcp_keep_alive: Bool) raises: + self.error_handler = ErrorHandler() + self.name = "lightbug_http" + self.__address = "127.0.0.1" + self.max_concurrent_connections = 1000 + self.max_requests_per_connection = 0 + self.__max_request_body_size = default_max_request_body_size + self.tcp_keep_alive = tcp_keep_alive + self.ln = NoTLSListener() + + fn __init__(inout self, own_address: String) raises: + self.error_handler = ErrorHandler() + self.name = "lightbug_http" + self.__address = own_address + self.max_concurrent_connections = 1000 + self.max_requests_per_connection = 0 + self.__max_request_body_size = default_max_request_body_size + self.tcp_keep_alive = False + self.ln = NoTLSListener() + + fn __init__(inout self, error_handler: ErrorHandler) raises: + self.error_handler = error_handler + self.name = "lightbug_http" + self.__address = "127.0.0.1" + self.max_concurrent_connections = 1000 + self.max_requests_per_connection = 0 + self.__max_request_body_size = default_max_request_body_size + self.tcp_keep_alive = False + self.ln = NoTLSListener() + + fn __init__(inout self, max_request_body_size: Int) raises: + self.error_handler = ErrorHandler() + self.name = "lightbug_http" + self.__address = "127.0.0.1" + self.max_concurrent_connections = 1000 + self.max_requests_per_connection = 0 + self.__max_request_body_size = max_request_body_size + self.tcp_keep_alive = False + self.ln = NoTLSListener() + + fn __init__(inout self, max_request_body_size: Int, tcp_keep_alive: Bool) raises: + self.error_handler = ErrorHandler() + self.name = "lightbug_http" + self.__address = "127.0.0.1" + self.max_concurrent_connections = 1000 + self.max_requests_per_connection = 0 + self.__max_request_body_size = max_request_body_size + self.tcp_keep_alive = tcp_keep_alive + self.ln = NoTLSListener() + + fn address(self) -> String: + return self.__address + + fn set_address(inout self, own_address: String) -> Self: + self.__address = own_address + return self + fn max_request_body_size(self) -> Int: + return self.__max_request_body_size -trait ServerTrait: - fn __init__( - inout self, - addr: String, - service: HTTPService, - error_handler: ErrorHandler, - ): - ... + fn set_max_request_body_size(inout self, size: Int) -> Self: + self.__max_request_body_size = size + return self fn get_concurrency(self) -> Int: - ... + """ + Retrieve the concurrency level which is either + the configured max_concurrent_connections or the DefaultConcurrency. + + Returns: + Int: concurrency level for the server. + """ + var concurrency = self.max_concurrent_connections + if concurrency <= 0: + concurrency = DefaultConcurrency + return concurrency + + fn listen_and_serve[T: HTTPService](inout self, address: String, handler: T) raises -> None: + """ + Listen for incoming connections and serve HTTP requests. + + Args: + address : String - The address (host:port) to listen on. + handler : HTTPService - An object that handles incoming HTTP requests. + """ + var __net = SysNet() + var listener = __net.listen(NetworkType.tcp4.value, address) + _ = self.set_address(address) + self.serve(listener, handler) + + fn serve[T: HTTPService](inout self, ln: NoTLSListener, handler: T) raises -> None: + """ + Serve HTTP requests. + + Args: + ln : NoTLSListener - TCP server that listens for incoming connections. + handler : HTTPService - An object that handles incoming HTTP requests. + + Raises: + If there is an error while serving requests. + """ + self.ln = ln + + while True: + var conn = self.ln.accept() + self.serve_connection(conn, handler) + + fn serve_connection[T: HTTPService](inout self, conn: SysConnection, handler: T) raises -> None: + """ + Serve a single connection. + + Args: + conn : SysConnection - A connection object that represents a client connection. + handler : HTTPService - An object that handles incoming HTTP requests. + + Raises: + If there is an error while serving the connection. + """ + # var b = Bytes(capacity=default_buffer_size) + # var bytes_recv = conn.read(b) + # if bytes_recv == 0: + # conn.close() + # return + + var max_request_body_size = self.max_request_body_size() + if max_request_body_size <= 0: + max_request_body_size = default_max_request_body_size + + var req_number = 0 + + while True: + req_number += 1 + + b = Bytes(capacity=default_buffer_size) + bytes_recv = conn.read(b) + if bytes_recv == 0: + conn.close() + break + + var request = HTTPRequest.from_bytes(self.address(), max_request_body_size, b^) + + var res = handler.func(request) + + if not self.tcp_keep_alive: + _ = res.set_connection_close() - fn listen_and_serve(self, address: String, handler: HTTPService) raises -> None: - ... + _ = conn.write(encode(res^)) - fn serve(self, ln: Listener, handler: HTTPService) raises -> None: - ... + if not self.tcp_keep_alive: + conn.close() + return diff --git a/lightbug_http/sys/__init__.mojo b/lightbug_http/sys/__init__.mojo deleted file mode 100644 index e69de29b..00000000 diff --git a/lightbug_http/sys/client.mojo b/lightbug_http/sys/client.mojo deleted file mode 100644 index f45dd0eb..00000000 --- a/lightbug_http/sys/client.mojo +++ /dev/null @@ -1,123 +0,0 @@ -from ..libc import ( - c_int, - AF_INET, - SOCK_STREAM, - socket, - connect, - send, - recv, - close, -) -from lightbug_http.strings import to_string -from lightbug_http.client import Client -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.sys.net import create_connection -from lightbug_http.io.bytes import Bytes -from lightbug_http.utils import ByteReader - - -struct MojoClient(Client): - var host: StringLiteral - var port: Int - var name: String - - fn __init__(inout self) raises: - self.host = "127.0.0.1" - self.port = 8888 - self.name = "lightbug_http_client" - - fn __init__(inout self, host: StringLiteral, port: Int) raises: - self.host = host - self.port = port - self.name = "lightbug_http_client" - - fn do(self, owned req: 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: - 1. Creates a connection to the server specified in the request. - 2. Sends the request body using the connection. - 3. Receives the response from the server. - 4. Closes the connection. - 5. Returns the received response as an `HTTPResponse` object. - - Note: The code assumes that the `HTTPRequest` object passed as an argument has a valid URI with a host and port specified. - - Parameters - ---------- - req : HTTPRequest : - An `HTTPRequest` object representing the request to be sent. - - Returns - ------- - HTTPResponse : - The received response. - - Raises - ------ - Error : - If there is a failure in sending or receiving the message. - """ - var uri = req.uri - var host = uri.host - - if host == "": - raise Error("URI is nil") - var is_tls = False - - if uri.is_https(): - is_tls = True - - var host_str: String - var port: Int - - if ":" in host: - var host_port = host.split(":") - host_str = host_port[0] - port = atol(host_port[1]) - else: - host_str = host - if is_tls: - port = 443 - else: - port = 80 - - # TODO: Actually handle persistent connections - var conn = create_connection(socket(AF_INET, SOCK_STREAM, 0), host_str, port) - var bytes_sent = conn.write(encode(req)) - if bytes_sent == -1: - raise Error("Failed to send message") - - var new_buf = Bytes(capacity=default_buffer_size) - var bytes_recv = conn.read(new_buf) - - if bytes_recv == 0: - conn.close() - try: - var res = HTTPResponse.from_bytes(new_buf^) - if res.is_redirect(): - conn.close() - return self._handle_redirect(req^, res^) - return res - except e: - conn.close() - raise e - - return HTTPResponse(Bytes()) - - fn _handle_redirect( - self, owned original_req: HTTPRequest, owned original_response: HTTPResponse - ) raises -> HTTPResponse: - var new_uri: URI - var new_location = original_response.headers[HeaderKey.LOCATION] - if new_location.startswith("http"): - new_uri = URI.parse_raises(new_location) - original_req.headers[HeaderKey.HOST] = new_uri.host - else: - new_uri = original_req.uri - new_uri.path = new_location - original_req.uri = new_uri - return self.do(original_req^) diff --git a/lightbug_http/sys/net.mojo b/lightbug_http/sys/net.mojo deleted file mode 100644 index 54102766..00000000 --- a/lightbug_http/sys/net.mojo +++ /dev/null @@ -1,442 +0,0 @@ -from utils import StaticTuple -from sys.info import sizeof -from sys.ffi import external_call -from lightbug_http.net import ( - Listener, - ListenConfig, - Connection, - TCPAddr, - Net, - resolve_internet_addr, - default_buffer_size, - default_tcp_keep_alive, - get_peer_name, -) -from lightbug_http.strings import NetworkType, to_string -from lightbug_http.io.bytes import Bytes, bytes -from lightbug_http.io.sync import Duration -from ..libc import ( - c_void, - c_int, - c_uint, - c_char, - c_ssize_t, - in_addr, - sockaddr, - sockaddr_in, - socklen_t, - AI_PASSIVE, - AF_INET, - AF_INET6, - SOCK_STREAM, - SOL_SOCKET, - SO_REUSEADDR, - SHUT_RDWR, - htons, - inet_pton, - to_char_ptr, - socket, - connect, - setsockopt, - listen, - accept, - send, - recv, - bind, - shutdown, - close, -) -from sys.info import os_is_macos -from time import sleep - - -trait AnAddrInfo: - fn get_ip_address(self, host: String) raises -> in_addr: - """ - TODO: Once default functions can be implemented in traits, this function should use the functions currently - implemented in the `addrinfo_macos` and `addrinfo_unix` structs. - """ - ... - - -fn getaddrinfo[ - T: AnAddrInfo -]( - nodename: UnsafePointer[c_char], - servname: UnsafePointer[c_char], - hints: UnsafePointer[T], - res: UnsafePointer[UnsafePointer[T]], -) -> c_int: - """ - Overwrites the existing libc `getaddrinfo` function to use the AnAddrInfo trait. - - Libc POSIX `getaddrinfo` function - Reference: https://man7.org/linux/man-pages/man3/getaddrinfo.3p.html - Fn signature: int getaddrinfo(const char *restrict nodename, const char *restrict servname, const struct addrinfo *restrict hints, struct addrinfo **restrict res). - """ - return external_call[ - "getaddrinfo", - c_int, # FnName, RetType - UnsafePointer[c_char], - UnsafePointer[c_char], - UnsafePointer[T], # Args - UnsafePointer[UnsafePointer[T]], # Args - ](nodename, servname, hints, res) - - -@value -struct SysListener: - """ - A TCP listener that listens for incoming connections and can accept them. - """ - - var fd: c_int - var __addr: TCPAddr - - fn __init__(inout self) raises: - self.__addr = TCPAddr("localhost", 8080) - self.fd = socket(AF_INET, SOCK_STREAM, 0) - - fn __init__(inout self, addr: TCPAddr) raises: - self.__addr = addr - self.fd = socket(AF_INET, SOCK_STREAM, 0) - - fn __init__(inout self, addr: TCPAddr, fd: c_int) raises: - self.__addr = addr - self.fd = fd - - fn accept(self) raises -> SysConnection: - var their_addr = sockaddr(0, StaticTuple[c_char, 14]()) - var their_addr_ptr = Reference[sockaddr](their_addr) - var sin_size = socklen_t(sizeof[socklen_t]()) - var sin_size_ptr = Reference[socklen_t](sin_size) - var new_sockfd = external_call["accept", c_int](self.fd, their_addr_ptr, sin_size_ptr) - - # var new_sockfd = accept( - # self.fd, their_addr_ptr, UnsafePointer[socklen_t].address_of(sin_size) - # ) - if new_sockfd == -1: - print("Failed to accept connection, system accept() returned an error.") - var peer = get_peer_name(new_sockfd) - - return SysConnection(self.__addr, TCPAddr(peer.host, atol(peer.port)), new_sockfd) - - fn close(self) raises: - _ = shutdown(self.fd, SHUT_RDWR) - var close_status = close(self.fd) - if close_status == -1: - print("Failed to close new_sockfd") - - fn addr(self) -> TCPAddr: - return self.__addr - - -struct SysListenConfig(ListenConfig): - var __keep_alive: Duration - - fn __init__(inout self) raises: - self.__keep_alive = default_tcp_keep_alive - - fn __init__(inout self, keep_alive: Duration) raises: - self.__keep_alive = keep_alive - - fn listen(inout self, network: String, address: String) raises -> SysListener: - var addr = resolve_internet_addr(network, address) - var address_family = AF_INET - var ip_buf_size = 4 - if address_family == AF_INET6: - ip_buf_size = 16 - - var sockfd = socket(address_family, SOCK_STREAM, 0) - if sockfd == -1: - print("Socket creation error") - - var yes: Int = 1 - var setsockopt_result = setsockopt( - sockfd, - SOL_SOCKET, - SO_REUSEADDR, - UnsafePointer[Int].address_of(yes).bitcast[c_void](), - sizeof[Int](), - ) - - var bind_success = False - var bind_fail_logged = False - - var ip_buf = UnsafePointer[c_void].alloc(ip_buf_size) - var conv_status = inet_pton(address_family, to_char_ptr(addr.ip), ip_buf) - var raw_ip = ip_buf.bitcast[c_uint]()[] - var bin_port = htons(UInt16(addr.port)) - - var ai = sockaddr_in(address_family, bin_port, raw_ip, StaticTuple[c_char, 8]()) - var ai_ptr = Reference[sockaddr_in](ai) - - while not bind_success: - # var bind = bind(sockfd, ai_ptr, sizeof[sockaddr_in]()) - var bind = external_call["bind", c_int](sockfd, ai_ptr, sizeof[sockaddr_in]()) - if bind == 0: - bind_success = True - else: - if not bind_fail_logged: - print("Bind attempt failed. The address might be in use or the socket might not be available.") - print("Retrying. Might take 10-15 seconds.") - bind_fail_logged = True - print(".", end="", flush=True) - _ = shutdown(sockfd, SHUT_RDWR) - sleep(1) - - if listen(sockfd, c_int(128)) == -1: - print("Listen failed.\n on sockfd " + sockfd.__str__()) - - var listener = SysListener(addr, sockfd) - - print("\nšŸ”„šŸ Lightbug is listening on " + "http://" + addr.ip + ":" + addr.port.__str__()) - print("Ready to accept connections...") - - return listener - - -@value -struct SysConnection(Connection): - var fd: c_int - var raddr: TCPAddr - var laddr: TCPAddr - - fn __init__(inout self, laddr: String, raddr: String) raises: - self.raddr = resolve_internet_addr(NetworkType.tcp4.value, raddr) - self.laddr = resolve_internet_addr(NetworkType.tcp4.value, laddr) - self.fd = socket(AF_INET, SOCK_STREAM, 0) - - fn __init__(inout self, laddr: TCPAddr, raddr: TCPAddr) raises: - self.raddr = raddr - self.laddr = laddr - self.fd = socket(AF_INET, SOCK_STREAM, 0) - - fn __init__(inout self, laddr: TCPAddr, raddr: TCPAddr, fd: c_int) raises: - self.raddr = raddr - self.laddr = laddr - self.fd = fd - - fn read(self, inout buf: Bytes) raises -> Int: - var bytes_recv = recv( - self.fd, - buf.unsafe_ptr().offset(buf.size), - buf.capacity - buf.size, - 0, - ) - if bytes_recv == -1: - return 0 - buf.size += bytes_recv - if bytes_recv == 0: - return 0 - if bytes_recv < buf.capacity: - return bytes_recv - return bytes_recv - - fn write(self, owned msg: String) raises -> Int: - var bytes_sent = send(self.fd, msg.unsafe_ptr(), len(msg), 0) - if bytes_sent == -1: - print("Failed to send response") - return bytes_sent - - fn write(self, buf: Bytes) raises -> Int: - var content = to_string(buf) - var bytes_sent = send(self.fd, content.unsafe_ptr(), len(content), 0) - if bytes_sent == -1: - print("Failed to send response") - _ = content - return bytes_sent - - fn close(self) raises: - _ = shutdown(self.fd, SHUT_RDWR) - var close_status = close(self.fd) - if close_status == -1: - print("Failed to close new_sockfd") - - fn local_addr(inout self) raises -> TCPAddr: - return self.laddr - - fn remote_addr(self) raises -> TCPAddr: - return self.raddr - - -struct SysNet: - var __lc: SysListenConfig - - fn __init__(inout self) raises: - self.__lc = SysListenConfig(default_tcp_keep_alive) - - fn __init__(inout self, keep_alive: Duration) raises: - self.__lc = SysListenConfig(keep_alive) - - fn listen(inout self, network: String, addr: String) raises -> SysListener: - return self.__lc.listen(network, addr) - - -@value -@register_passable("trivial") -struct addrinfo_macos(AnAddrInfo): - """ - For MacOS, I had to swap the order of ai_canonname and ai_addr. - https://stackoverflow.com/questions/53575101/calling-getaddrinfo-directly-from-python-ai-addr-is-null-pointer. - """ - - var ai_flags: c_int - var ai_family: c_int - var ai_socktype: c_int - var ai_protocol: c_int - var ai_addrlen: socklen_t - var ai_canonname: UnsafePointer[c_char] - var ai_addr: UnsafePointer[sockaddr] - var ai_next: UnsafePointer[c_void] - - fn __init__(inout self): - self.ai_flags = 0 - self.ai_family = 0 - self.ai_socktype = 0 - self.ai_protocol = 0 - self.ai_addrlen = 0 - self.ai_canonname = UnsafePointer[c_char]() - self.ai_addr = UnsafePointer[sockaddr]() - self.ai_next = UnsafePointer[c_void]() - - fn get_ip_address(self, host: String) raises -> in_addr: - """ - Returns an IP address based on the host. - This is a MacOS-specific implementation. - - Args: - host: String - The host to get the IP from. - - Returns: - in_addr - The IP address. - """ - var host_ptr = to_char_ptr(host) - var servinfo = Reference(Self()) - var servname = UnsafePointer[Int8]() - - var hints = Self() - hints.ai_family = AF_INET - hints.ai_socktype = SOCK_STREAM - hints.ai_flags = AI_PASSIVE - - var error = external_call[ - "getaddrinfo", - Int32, - ](host_ptr, servname, Reference(hints), Reference(servinfo)) - - if error != 0: - print("getaddrinfo failed with error code: " + error.__str__()) - raise Error("Failed to get IP address. getaddrinfo failed.") - - var addrinfo = servinfo[] - - var ai_addr = addrinfo.ai_addr - if not ai_addr: - print("ai_addr is null") - raise Error("Failed to get IP address. getaddrinfo was called successfully, but ai_addr is null.") - - var addr_in = ai_addr.bitcast[sockaddr_in]()[] - - return addr_in.sin_addr - - -@value -@register_passable("trivial") -struct addrinfo_unix(AnAddrInfo): - """ - Standard addrinfo struct for Unix systems. Overwrites the existing libc `getaddrinfo` function to adhere to the AnAddrInfo trait. - """ - - var ai_flags: c_int - var ai_family: c_int - var ai_socktype: c_int - var ai_protocol: c_int - var ai_addrlen: socklen_t - var ai_addr: UnsafePointer[sockaddr] - var ai_canonname: UnsafePointer[c_char] - var ai_next: UnsafePointer[c_void] - - fn __init__(inout self): - self.ai_flags = 0 - self.ai_family = 0 - self.ai_socktype = 0 - self.ai_protocol = 0 - self.ai_addrlen = 0 - self.ai_addr = UnsafePointer[sockaddr]() - self.ai_canonname = UnsafePointer[c_char]() - self.ai_next = UnsafePointer[c_void]() - - fn get_ip_address(self, host: String) raises -> in_addr: - """ - Returns an IP address based on the host. - This is a Unix-specific implementation. - - Args: - host: String - The host to get IP from. - - Returns: - UInt32 - The IP address. - """ - var host_ptr = to_char_ptr(host) - var servinfo = UnsafePointer[Self]().alloc(1) - servinfo.init_pointee_move(Self()) - - var hints = Self() - hints.ai_family = AF_INET - hints.ai_socktype = SOCK_STREAM - hints.ai_flags = AI_PASSIVE - - var error = getaddrinfo[Self]( - host_ptr, - UnsafePointer[UInt8](), - UnsafePointer.address_of(hints), - UnsafePointer.address_of(servinfo), - ) - if error != 0: - print("getaddrinfo failed") - raise Error("Failed to get IP address. getaddrinfo failed.") - - var addrinfo = servinfo[] - - var ai_addr = addrinfo.ai_addr - if not ai_addr: - print("ai_addr is null") - raise Error("Failed to get IP address. getaddrinfo was called successfully, but ai_addr is null.") - - var addr_in = ai_addr.bitcast[sockaddr_in]()[] - - return addr_in.sin_addr - - -fn create_connection(sock: c_int, host: String, port: UInt16) raises -> SysConnection: - """ - Connect to a server using a socket. - - Args: - sock: Int32 - The socket file descriptor. - host: String - The host to connect to. - port: UInt16 - The port to connect to. - - Returns: - Int32 - The socket file descriptor. - """ - var ip: in_addr - if os_is_macos(): - ip = addrinfo_macos().get_ip_address(host) - else: - ip = addrinfo_unix().get_ip_address(host) - - # Convert ip address to network byte order. - var addr: sockaddr_in = sockaddr_in(AF_INET, htons(port), ip, StaticTuple[c_char, 8](0, 0, 0, 0, 0, 0, 0, 0)) - var addr_ptr = Reference[sockaddr_in](addr) - - if external_call["connect", c_int](sock, addr_ptr, sizeof[sockaddr_in]()) == -1: - _ = shutdown(sock, SHUT_RDWR) - raise Error("Failed to connect to server") - - var laddr = TCPAddr() - var raddr = TCPAddr(host, int(port)) - var conn = SysConnection(sock, laddr, raddr) - - return conn diff --git a/lightbug_http/sys/server.mojo b/lightbug_http/sys/server.mojo deleted file mode 100644 index d550fd5f..00000000 --- a/lightbug_http/sys/server.mojo +++ /dev/null @@ -1,195 +0,0 @@ -from lightbug_http.server import DefaultConcurrency -from lightbug_http.net import Listener, default_buffer_size -from lightbug_http.http import HTTPRequest, encode -from lightbug_http.uri import URI -from lightbug_http.header import Headers -from lightbug_http.sys.net import SysListener, SysConnection, SysNet -from lightbug_http.service import HTTPService -from lightbug_http.io.sync import Duration -from lightbug_http.io.bytes import Bytes, bytes -from lightbug_http.error import ErrorHandler -from lightbug_http.strings import NetworkType -from lightbug_http.utils import ByteReader - -alias default_max_request_body_size = 4 * 1024 * 1024 # 4MB - - -@value -struct SysServer: - """ - A Mojo-based server that accept incoming requests and delivers HTTP services. - """ - - var error_handler: ErrorHandler - - var name: String - var __address: String - var max_concurrent_connections: Int - var max_requests_per_connection: Int - - var __max_request_body_size: Int - var tcp_keep_alive: Bool - - var ln: SysListener - - fn __init__(inout self) raises: - self.error_handler = ErrorHandler() - self.name = "lightbug_http" - self.__address = "127.0.0.1" - self.max_concurrent_connections = 1000 - self.max_requests_per_connection = 0 - self.__max_request_body_size = default_max_request_body_size - self.tcp_keep_alive = False - self.ln = SysListener() - - fn __init__(inout self, tcp_keep_alive: Bool) raises: - self.error_handler = ErrorHandler() - self.name = "lightbug_http" - self.__address = "127.0.0.1" - self.max_concurrent_connections = 1000 - self.max_requests_per_connection = 0 - self.__max_request_body_size = default_max_request_body_size - self.tcp_keep_alive = tcp_keep_alive - self.ln = SysListener() - - fn __init__(inout self, own_address: String) raises: - self.error_handler = ErrorHandler() - self.name = "lightbug_http" - self.__address = own_address - self.max_concurrent_connections = 1000 - self.max_requests_per_connection = 0 - self.__max_request_body_size = default_max_request_body_size - self.tcp_keep_alive = False - self.ln = SysListener() - - fn __init__(inout self, error_handler: ErrorHandler) raises: - self.error_handler = error_handler - self.name = "lightbug_http" - self.__address = "127.0.0.1" - self.max_concurrent_connections = 1000 - self.max_requests_per_connection = 0 - self.__max_request_body_size = default_max_request_body_size - self.tcp_keep_alive = False - self.ln = SysListener() - - fn __init__(inout self, max_request_body_size: Int) raises: - self.error_handler = ErrorHandler() - self.name = "lightbug_http" - self.__address = "127.0.0.1" - self.max_concurrent_connections = 1000 - self.max_requests_per_connection = 0 - self.__max_request_body_size = max_request_body_size - self.tcp_keep_alive = False - self.ln = SysListener() - - fn __init__(inout self, max_request_body_size: Int, tcp_keep_alive: Bool) raises: - self.error_handler = ErrorHandler() - self.name = "lightbug_http" - self.__address = "127.0.0.1" - self.max_concurrent_connections = 1000 - self.max_requests_per_connection = 0 - self.__max_request_body_size = max_request_body_size - self.tcp_keep_alive = tcp_keep_alive - self.ln = SysListener() - - fn address(self) -> String: - return self.__address - - fn set_address(inout self, own_address: String) -> Self: - self.__address = own_address - return self - - fn max_request_body_size(self) -> Int: - return self.__max_request_body_size - - fn set_max_request_body_size(inout self, size: Int) -> Self: - self.__max_request_body_size = size - return self - - fn get_concurrency(self) -> Int: - """ - Retrieve the concurrency level which is either - the configured max_concurrent_connections or the DefaultConcurrency. - - Returns: - Int: concurrency level for the server. - """ - var concurrency = self.max_concurrent_connections - if concurrency <= 0: - concurrency = DefaultConcurrency - return concurrency - - fn listen_and_serve[T: HTTPService](inout self, address: String, handler: T) raises -> None: - """ - Listen for incoming connections and serve HTTP requests. - - Args: - address : String - The address (host:port) to listen on. - handler : HTTPService - An object that handles incoming HTTP requests. - """ - var __net = SysNet() - var listener = __net.listen(NetworkType.tcp4.value, address) - _ = self.set_address(address) - self.serve(listener, handler) - - fn serve[T: HTTPService](inout self, ln: SysListener, handler: T) raises -> None: - """ - Serve HTTP requests. - - Args: - ln : SysListener - TCP server that listens for incoming connections. - handler : HTTPService - An object that handles incoming HTTP requests. - - Raises: - If there is an error while serving requests. - """ - self.ln = ln - - while True: - var conn = self.ln.accept() - self.serve_connection(conn, handler) - - fn serve_connection[T: HTTPService](inout self, conn: SysConnection, handler: T) raises -> None: - """ - Serve a single connection. - - Args: - conn : SysConnection - A connection object that represents a client connection. - handler : HTTPService - An object that handles incoming HTTP requests. - - Raises: - If there is an error while serving the connection. - """ - # var b = Bytes(capacity=default_buffer_size) - # var bytes_recv = conn.read(b) - # if bytes_recv == 0: - # conn.close() - # return - - var max_request_body_size = self.max_request_body_size() - if max_request_body_size <= 0: - max_request_body_size = default_max_request_body_size - - var req_number = 0 - - while True: - req_number += 1 - - b = Bytes(capacity=default_buffer_size) - bytes_recv = conn.read(b) - if bytes_recv == 0: - conn.close() - break - - var request = HTTPRequest.from_bytes(self.address(), max_request_body_size, b^) - - var res = handler.func(request) - - if not self.tcp_keep_alive: - _ = res.set_connection_close() - - _ = conn.write(encode(res^)) - - if not self.tcp_keep_alive: - conn.close() - return diff --git a/tests/lightbug_http/sys/test_client.mojo b/tests/lightbug_http/test_client.mojo similarity index 91% rename from tests/lightbug_http/sys/test_client.mojo rename to tests/lightbug_http/test_client.mojo index 2631af61..5b29dd2f 100644 --- a/tests/lightbug_http/sys/test_client.mojo +++ b/tests/lightbug_http/test_client.mojo @@ -1,5 +1,5 @@ import testing -from lightbug_http.sys.client import MojoClient +from lightbug_http.client import Client from lightbug_http.http import HTTPRequest, encode from lightbug_http.uri import URI from lightbug_http.header import Header, Headers @@ -7,7 +7,7 @@ from lightbug_http.io.bytes import bytes fn test_mojo_client_redirect_external_req_google() raises: - var client = MojoClient() + var client = Client() var req = HTTPRequest( uri=URI.parse_raises("http://google.com"), headers=Headers( @@ -21,7 +21,7 @@ fn test_mojo_client_redirect_external_req_google() raises: print(e) fn test_mojo_client_redirect_external_req_302() raises: - var client = MojoClient() + var client = Client() var req = HTTPRequest( uri=URI.parse_raises("http://httpbin.org/status/302"), headers=Headers( @@ -35,7 +35,7 @@ fn test_mojo_client_redirect_external_req_302() raises: print(e) fn test_mojo_client_redirect_external_req_308() raises: - var client = MojoClient() + var client = Client() var req = HTTPRequest( uri=URI.parse_raises("http://httpbin.org/status/308"), headers=Headers( @@ -49,7 +49,7 @@ fn test_mojo_client_redirect_external_req_308() raises: print(e) fn test_mojo_client_redirect_external_req_307() raises: - var client = MojoClient() + var client = Client() var req = HTTPRequest( uri=URI.parse_raises("http://httpbin.org/status/307"), headers=Headers( @@ -63,7 +63,7 @@ fn test_mojo_client_redirect_external_req_307() raises: print(e) fn test_mojo_client_redirect_external_req_301() raises: - var client = MojoClient() + var client = Client() var req = HTTPRequest( uri=URI.parse_raises("http://httpbin.org/status/301"), headers=Headers( @@ -78,7 +78,7 @@ fn test_mojo_client_redirect_external_req_301() raises: print(e) fn test_mojo_client_lightbug_external_req_200() raises: - var client = MojoClient() + var client = Client() var req = HTTPRequest( uri=URI.parse_raises("http://httpbin.org/status/200"), headers=Headers( From b6b25553d5bef54e16c1e162948a8d691a50168b Mon Sep 17 00:00:00 2001 From: Brian Grenier Date: Tue, 8 Oct 2024 15:21:29 -0600 Subject: [PATCH 3/4] ignore 1.x version other than 1.1 Signed-off-by: Brian Grenier --- lightbug_http/http/__init__.mojo | 1 + lightbug_http/http/http_version.mojo | 16 ++++++++-------- lightbug_http/http/request.mojo | 6 ------ tests/lightbug_http/test_http.mojo | 10 ++++++---- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/lightbug_http/http/__init__.mojo b/lightbug_http/http/__init__.mojo index 927b1a6d..118a394c 100644 --- a/lightbug_http/http/__init__.mojo +++ b/lightbug_http/http/__init__.mojo @@ -1,6 +1,7 @@ from .common_response import * from .response import * from .request import * +from .http_version import HttpVersion @always_inline diff --git a/lightbug_http/http/http_version.mojo b/lightbug_http/http/http_version.mojo index c63809f0..cec1d419 100644 --- a/lightbug_http/http/http_version.mojo +++ b/lightbug_http/http/http_version.mojo @@ -1,13 +1,11 @@ -# TODO: Can't be used yet. -# This doesn't work right because of float point -# Shenaningans and round() doesn't give me what I want +# TODO: Apply this to request/response structs @value @register_passable("trivial") struct HttpVersion(EqualityComparable, Stringable): - var _v: Float64 + var _v: Int fn __init__(inout self, version: String) raises: - self._v = atof(version[version.find("/") + 1 :]) + self._v = int(version[version.find("/") + 1]) fn __eq__(self, other: Self) -> Bool: return self._v == other._v @@ -15,11 +13,13 @@ struct HttpVersion(EqualityComparable, Stringable): fn __ne__(self, other: Self) -> Bool: return self._v != other._v - fn __eq__(self, other: Float64) -> Bool: + fn __eq__(self, other: Int) -> Bool: return self._v == other - fn __ne__(self, other: Float64) -> Bool: + fn __ne__(self, other: Int) -> Bool: return self._v != other fn __str__(self) -> String: - return "HTTP/" + str(self._v) + # Only support version 1.1 so don't need to account for 1.0 + v = "1.1" if self._v == 1 else str(self._v) + return "HTTP/" + v diff --git a/lightbug_http/http/request.mojo b/lightbug_http/http/request.mojo index 318dcaef..7f9ecb97 100644 --- a/lightbug_http/http/request.mojo +++ b/lightbug_http/http/request.mojo @@ -15,12 +15,6 @@ from lightbug_http.strings import ( to_string, ) - -@always_inline -fn encode(owned req: HTTPRequest) -> Bytes: - return req._encoded() - - @value struct HTTPRequest(Formattable, Stringable): var headers: Headers diff --git a/tests/lightbug_http/test_http.mojo b/tests/lightbug_http/test_http.mojo index a8acc6ac..05613563 100644 --- a/tests/lightbug_http/test_http.mojo +++ b/tests/lightbug_http/test_http.mojo @@ -1,7 +1,7 @@ import testing from collections import Dict, List from lightbug_http.io.bytes import Bytes, bytes -from lightbug_http.http import HTTPRequest, HTTPResponse, encode +from lightbug_http.http import HTTPRequest, HTTPResponse, encode, HttpVersion from lightbug_http.header import Header, Headers, HeaderKey from lightbug_http.uri import URI from lightbug_http.strings import to_string @@ -44,6 +44,8 @@ def test_encode_http_response(): testing.assert_equal(res_encoded, as_str) -# def test_http_version_parse(): -# var v1 = HttpVersion("HTTP/1.1") -# testing.assert_equal(v1, 1.1) \ No newline at end of file +def test_http_version_parse(): + var v1 = HttpVersion("HTTP/1.1") + testing.assert_equal(v1, 1) + var v2 = HttpVersion("HTTP/2") + testing.assert_equal(v2, 2) \ No newline at end of file From ff57ab6e2b3177dac8a86db31b22048f27f1c74e Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 23 Oct 2024 19:14:52 +0200 Subject: [PATCH 4/4] remove print statements --- lightbug_http/client.mojo | 4 ++-- lightbug_http/net.mojo | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index 35eaadaf..dc5c2533 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -88,10 +88,10 @@ struct Client: var bytes_sent = conn.write(encode(req)) if bytes_sent == -1: raise Error("Failed to send message") - + var new_buf = Bytes(capacity=default_buffer_size) var bytes_recv = conn.read(new_buf) - print("new_buf:", String(new_buf)) + if bytes_recv == 0: conn.close() try: diff --git a/lightbug_http/net.mojo b/lightbug_http/net.mojo index 0350f6ba..eabaea09 100644 --- a/lightbug_http/net.mojo +++ b/lightbug_http/net.mojo @@ -250,7 +250,6 @@ struct SysConnection(Connection): fn write(self, buf: Bytes) raises -> Int: var content = to_string(buf) - print("Content: " + content) var bytes_sent = send(self.fd, content.unsafe_ptr(), len(content), 0) if bytes_sent == -1: print("Failed to send response")