diff --git a/README.md b/README.md index 06ca5a71..fa1d558d 100644 --- a/README.md +++ b/README.md @@ -127,33 +127,55 @@ Once you have Mojo set up locally,

(back to top)

-### Using the client +### Serving static files -Create a file, e.g `client.mojo` with the following code: +The default welcome screen shows an example of how to serve files like images or HTML using Lightbug. Mojo has built-in `open`, `read` and `read_bytes` methods that you can use to read files from e.g. a `static` directory and serve them on a route: ```mojo -from lightbug_http.http import HTTPRequest -from lightbug_http.uri import URI -from lightbug_http.sys.client import MojoClient +@value +struct Welcome(HTTPService): + fn func(self, req: HTTPRequest) raises -> HTTPResponse: + var uri = req.uri() + + if uri.path() == "/": + var html: Bytes + with open("static/lightbug_welcome.html", "r") as f: + html = f.read_bytes() + return OK(html, "text/html; charset=utf-8") + + if uri.path() == "/logo.png": + var image: Bytes + with open("static/logo.png", "r") as f: + image = f.read_bytes() + return OK(image, "image/png") + + return NotFound(uri.path()) +``` + +### Using the client +Create a file, e.g `client.mojo` with the following code. Run `mojo client.mojo` to execute the request to a given URL. + +```mojo fn test_request(inout client: MojoClient) raises -> None: - var uri = URI("http://httpbin.org/") + var uri = URI("http://httpbin.org/status/404") var request = HTTPRequest(uri) var response = client.do(request) # print status code print("Response:", response.header.status_code()) - # print various parsed headers - print("Header", response.header.content_length()) + # print raw headers + # print("Headers:", response.header.headers()) + + # print parsed headers (only some are parsed for now) + print("Content-Type:", String(response.header.content_type())) + print("Content-Length", response.header.content_length()) + print("Connection:", response.header.connection_close()) + print("Server:", String(response.header.server())) # print body print(String(response.get_body())) - - -fn main() raises -> None: - var client = MojoClient() - test_request(client) ``` Pure Mojo-based client is available by default. This client is also used internally for testing the server. diff --git a/client.mojo b/client.mojo new file mode 100644 index 00000000..e84e7bd8 --- /dev/null +++ b/client.mojo @@ -0,0 +1,29 @@ +from lightbug_http.http import HTTPRequest, encode +from lightbug_http.header import RequestHeader +from lightbug_http.uri import URI +from lightbug_http.sys.client import MojoClient + +fn test_request(inout client: MojoClient) raises -> None: + var uri = URI("http://httpbin.org/status/404") + var request = HTTPRequest(uri) + var response = client.do(request) + + # print status code + print("Response:", response.header.status_code()) + + # print raw headers + # print("Headers:", response.header.headers()) + + # print parsed headers (only some are parsed for now) + print("Content-Type:", String(response.header.content_type())) + print("Content-Length", response.header.content_length()) + print("Connection:", response.header.connection_close()) + print("Server:", String(response.header.server())) + + # print body + print(String(response.get_body())) + + +fn main() raises -> None: + var client = MojoClient() + test_request(client) \ No newline at end of file diff --git a/external/libc.mojo b/external/libc.mojo index f1975af5..1e2eceb2 100644 --- a/external/libc.mojo +++ b/external/libc.mojo @@ -1,3 +1,5 @@ +from lightbug_http.io.bytes import Bytes + alias IPPROTO_IPV6 = 41 alias IPV6_V6ONLY = 26 alias EPROTONOSUPPORT = 93 @@ -83,6 +85,12 @@ fn to_char_ptr(s: String) -> Pointer[c_char]: return ptr +fn to_char_ptr(s: Bytes) -> Pointer[c_char]: + var ptr = Pointer[c_char]().alloc(len(s)) + for i in range(len(s)): + ptr.store(i, int(s[i])) + return ptr + fn c_charptr_to_string(s: Pointer[c_char]) -> String: return String(s.bitcast[Int8](), strlen(s)) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index 9d322d1d..ad27aacc 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -1,5 +1,4 @@ from lightbug_http import * -from sys import is_defined fn main() raises: var server = SysServer() diff --git a/lightbug_http/error.mojo b/lightbug_http/error.mojo index 9b8995fb..e19e87d0 100644 --- a/lightbug_http/error.mojo +++ b/lightbug_http/error.mojo @@ -6,9 +6,4 @@ from lightbug_http.header import ResponseHeader @value struct ErrorHandler: fn Error(self) -> HTTPResponse: - return HTTPResponse(ResponseHeader(), String("TODO").as_bytes()) - - -alias errNeedMore = Error("need more data: cannot find trailing lf") -alias errInvalidName = Error("invalid header name") -alias errSmallBuffer = Error("small read buffer. Increase ReadBufferSize") + return HTTPResponse(ResponseHeader(), String("TODO")._buffer) \ No newline at end of file diff --git a/lightbug_http/header.mojo b/lightbug_http/header.mojo index a4e037a6..bfc3fff8 100644 --- a/lightbug_http/header.mojo +++ b/lightbug_http/header.mojo @@ -8,7 +8,6 @@ from lightbug_http.strings import ( nChar, ) from lightbug_http.io.bytes import Bytes, bytes_equal -from lightbug_http.error import errNeedMore, errInvalidName alias statusOK = 200 @@ -105,7 +104,7 @@ struct RequestHeader: self.__trailer = trailer fn set_content_type(inout self, content_type: String) -> Self: - self.__content_type = content_type.as_bytes() + self.__content_type = content_type._buffer return self fn set_content_type_bytes(inout self, content_type: Bytes) -> Self: @@ -116,7 +115,7 @@ struct RequestHeader: return self.__content_type fn set_host(inout self, host: String) -> Self: - self.__host = host.as_bytes() + self.__host = host._buffer return self fn set_host_bytes(inout self, host: Bytes) -> Self: @@ -127,7 +126,7 @@ struct RequestHeader: return self.__host fn set_user_agent(inout self, user_agent: String) -> Self: - self.__user_agent = user_agent.as_bytes() + self.__user_agent = user_agent._buffer return self fn set_user_agent_bytes(inout self, user_agent: Bytes) -> Self: @@ -138,7 +137,7 @@ struct RequestHeader: return self.__user_agent fn set_method(inout self, method: String) -> Self: - self.__method = method.as_bytes() + self.__method = method._buffer return self fn set_method_bytes(inout self, method: Bytes) -> Self: @@ -151,8 +150,8 @@ struct RequestHeader: return self.__method fn set_protocol(inout self, method: String) -> Self: - self.no_http_1_1 = bytes_equal(method.as_bytes(), strHttp11) - self.proto = method.as_bytes() + self.no_http_1_1 = bytes_equal(method._buffer, strHttp11) + self.proto = method._buffer return self fn set_protocol_bytes(inout self, method: Bytes) -> Self: @@ -190,7 +189,7 @@ struct RequestHeader: return self.__request_uri fn set_trailer(inout self, trailer: String) -> Self: - self.__trailer = trailer.as_bytes() + self.__trailer = trailer._buffer return self fn set_trailer_bytes(inout self, trailer: Bytes) -> Self: @@ -276,7 +275,7 @@ struct RequestHeader: if self.content_length() != -1: var content_length = s.value _ = self.set_content_length(atol(content_length)) - _ = self.set_content_length_bytes(content_length.as_bytes()) + _ = self.set_content_length_bytes(content_length._buffer) continue if s.key.lower() == "connection": if s.value == "close": @@ -369,6 +368,27 @@ struct ResponseHeader: self.__server = Bytes() self.__trailer = Bytes() self.raw_headers = Bytes() + + fn __init__( + inout self, + status_code: Int, + status_message: Bytes, + content_type: Bytes, + content_encoding: Bytes, + ) -> None: + self.disable_normalization = False + self.no_http_1_1 = False + self.__connection_close = False + self.__status_code = status_code + self.__status_message = status_message + self.__protocol = Bytes() + self.__content_length = 0 + self.__content_length_bytes = Bytes() + self.__content_type = content_type + self.__content_encoding = content_encoding + self.__server = Bytes() + self.__trailer = Bytes() + self.raw_headers = Bytes() fn __init__( inout self, @@ -440,7 +460,7 @@ struct ResponseHeader: return self.__content_type fn set_content_type(inout self, content_type: String) -> Self: - self.__content_type = content_type.as_bytes() + self.__content_type = content_type._buffer return self fn set_content_type_bytes(inout self, content_type: Bytes) -> Self: @@ -451,7 +471,7 @@ struct ResponseHeader: return self.__content_encoding fn set_content_encoding(inout self, content_encoding: String) -> Self: - self.__content_encoding = content_encoding.as_bytes() + self.__content_encoding = content_encoding._buffer return self fn set_content_encoding_bytes(inout self, content_encoding: Bytes) -> Self: @@ -473,7 +493,7 @@ struct ResponseHeader: return self.__server fn set_server(inout self, server: String) -> Self: - self.__server = server.as_bytes() + self.__server = server._buffer return self fn set_server_bytes(inout self, server: Bytes) -> Self: @@ -490,7 +510,7 @@ struct ResponseHeader: return self.__protocol fn set_trailer(inout self, trailer: String) -> Self: - self.__trailer = trailer.as_bytes() + self.__trailer = trailer._buffer return self fn set_trailer_bytes(inout self, trailer: Bytes) -> Self: @@ -511,34 +531,27 @@ struct ResponseHeader: fn connection_close(self) -> Bool: return self.__connection_close - fn parse(inout self, request_line: String) raises -> None: - var headers = self.raw_headers - - var n = request_line.find(" ") - if n <= 0: - raise Error("Cannot find HTTP request method in the request") + fn headers(self) -> String: + return String(self.raw_headers) - var method = request_line[:n] - var rest_of_request_line = request_line[n + 1 :] + fn parse(inout self, first_line: String) raises -> None: + var headers = self.raw_headers # Defaults to HTTP/1.1 var proto_str = String(strHttp11) - # Parse requestURI - n = rest_of_request_line.rfind(" ") - if n < 0: - n = len(rest_of_request_line) - proto_str = strHttp10 - elif n == 0: - raise Error("Request URI cannot be empty") - else: - var proto = rest_of_request_line[n + 1 :] - if proto != strHttp11: - proto_str = proto + var n = first_line.find(" ") + var proto = first_line[:n] + if proto != strHttp11: + proto_str = proto - var request_uri = rest_of_request_line[:n] + var rest_of_response_line = first_line[n + 1 :] + var status_code = atol(rest_of_response_line[:3]) + var message = rest_of_response_line[4:] _ = self.set_protocol(proto_str._buffer) + _ = self.set_status_code(status_code) + _ = self.set_status_message(message._buffer) _ = self.set_content_length(-2) var s = headerScanner() @@ -547,8 +560,7 @@ struct ResponseHeader: while s.next(): if len(s.key) > 0: - # Spaces between the header key and colon are not allowed. - # See RFC 7230, Section 3.2.4. + # Spaces between header key and colon not allowed (RFC 7230, 3.2.4) if s.key.find(" ") != -1 or s.key.find("\t") != -1: raise Error("Invalid header key") elif s.key[0] == "c" or s.key[0] == "C": @@ -562,25 +574,22 @@ struct ResponseHeader: if self.content_length() != -1: var content_length = s.value _ = self.set_content_length(atol(content_length)) - _ = self.set_content_length_bytes(content_length.as_bytes()) + _ = self.set_content_length_bytes(content_length._buffer) continue if s.key.lower() == "connection": if s.value == "close": _ = self.set_connection_close() else: _ = self.reset_connection_close() - # _ = self.appendargbytes(s.key, s.value) continue elif s.key[0] == "s" or s.key[0] == "S": if s.key.lower() == "server": _ = self.set_server(s.value) continue - # TODO: set cookie elif s.key[0] == "t" or s.key[0] == "T": if s.key.lower() == "transfer-encoding": if s.value != "identity": _ = self.set_content_length(-1) - # _ = self.setargbytes(s.key, strChunked) continue if s.key.lower() == "trailer": _ = self.set_trailer(s.value) @@ -612,20 +621,17 @@ struct headerScanner: if not self.initialized: self.initialized = True - if self.b.startswith('\n\n'): + if self.b.startswith('\r\n\r\n'): self.b = self.b[2:] - print("Error: Double newline") return False - if self.b.startswith('\n'): + if self.b.startswith('\r\n'): self.b = self.b[1:] - print("Error: Newline at start") return False var n = self.b.find(':') - var x = self.b.find('\n') + var x = self.b.find('\r\n') if x != -1 and x < n: - print("Error: Newline before colon") return False if n == -1: @@ -635,17 +641,15 @@ struct headerScanner: self.key = self.b[:n].strip() self.b = self.b[n+1:].strip() - x = self.b.find('\n') + x = self.b.find('\r\n') if x == -1: - # If we don't find a newline, assume we have reached the end if len(self.b) == 0: - print("Error: No ending newline and no data after colon") return False self.value = self.b.strip() self.b = '' else: self.value = self.b[:x].strip() self.b = self.b[x+1:] - + return True diff --git a/lightbug_http/http.mojo b/lightbug_http/http.mojo index 6b481a6c..4734806a 100644 --- a/lightbug_http/http.mojo +++ b/lightbug_http/http.mojo @@ -2,7 +2,7 @@ from time import now from external.morrow import Morrow from external.gojo.strings import StringBuilder from lightbug_http.uri import URI -from lightbug_http.io.bytes import Bytes +from lightbug_http.io.bytes import Bytes, bytes from lightbug_http.header import RequestHeader, ResponseHeader from lightbug_http.io.sync import Duration from lightbug_http.net import Addr, TCPAddr @@ -173,11 +173,10 @@ struct HTTPResponse(Response): var laddr: TCPAddr fn __init__(inout self, body_bytes: Bytes): - # TODO: infer content type from the body self.header = ResponseHeader( 200, - String("OK").as_bytes(), - String("Content-Type: application/octet-stream\r\n").as_bytes(), + bytes("OK"), + bytes("Content-Type: application/octet-stream\r\n"), ) self.stream_immediate_header_flush = False self.stream_body = False @@ -212,25 +211,45 @@ struct HTTPResponse(Response): fn connection_close(self) -> Bool: return self.header.connection_close() +fn OK(body: StringLiteral) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(200, bytes("OK"), bytes("Content-Type: text/plain")), bytes(body), + ) -fn OK(body: Bytes) -> HTTPResponse: +fn OK(body: StringLiteral, content_type: String) -> HTTPResponse: return HTTPResponse( - ResponseHeader( - True, - 200, - String("OK").as_bytes(), - String("Content-Type: text/plain").as_bytes(), - ), - body, + ResponseHeader(200, bytes("OK"), bytes(content_type)), bytes(body), + ) + +fn OK(body: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(200, bytes("OK"), bytes("Content-Type: text/plain")), bytes(body), + ) + +fn OK(body: String, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(200, bytes("OK"), bytes(content_type)), bytes(body), ) +fn OK(body: Bytes) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(200, bytes("OK"), bytes("Content-Type: text/plain")), body, + ) fn OK(body: Bytes, content_type: String) -> HTTPResponse: return HTTPResponse( - ResponseHeader(True, 200, String("OK").as_bytes(), content_type.as_bytes()), - body, + ResponseHeader(200, bytes("OK"), bytes(content_type)), body, + ) + +fn OK(body: Bytes, content_type: String, content_encoding: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(200, bytes("OK"), bytes(content_type), bytes(content_encoding)), body, ) +fn NotFound(path: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(404, bytes("Not Found"), bytes("text/plain")), bytes("path " + path + " not found"), + ) fn encode(req: HTTPRequest, uri: URI) raises -> Bytes: var res_str = String() @@ -249,7 +268,7 @@ fn encode(req: HTTPRequest, uri: URI) raises -> Bytes: _ = builder.write(protocol) _ = builder.write_string(String("\r\n")) - _ = builder.write_string(String("Host: " + req.host())) + _ = builder.write_string(String("Host: " + String(uri.host()))) _ = builder.write_string(String("\r\n")) if len(req.body_raw) > 0: @@ -266,15 +285,15 @@ fn encode(req: HTTPRequest, uri: URI) raises -> Bytes: _ = builder.write_string(String("close")) else: _ = builder.write_string(String("keep-alive")) + _ = builder.write_string(String("\r\n")) - _ = builder.write_string(String("\r\n")) + if len(req.body_raw) > 0: _ = builder.write_string(String("\r\n")) _ = builder.write(req.body_raw) - # Currently the server is expecting a null terminated string for conn.send(). - return builder.get_null_terminated_bytes() + return builder.get_bytes() fn encode(res: HTTPResponse) raises -> Bytes: @@ -303,8 +322,10 @@ fn encode(res: HTTPResponse) raises -> Bytes: _ = builder.write(res.header.content_type()) _ = builder.write_string(String("\r\n")) - # TODO: propagate charset - # res_str += String("; charset=utf-8") + if len(res.header.content_encoding()) > 0: + _ = builder.write_string(String("Content-Encoding: ")) + _ = builder.write(res.header.content_encoding()) + _ = builder.write_string(String("\r\n")) if len(res.body_raw) > 0: _ = builder.write_string(String("Content-Length: ")) @@ -326,5 +347,4 @@ fn encode(res: HTTPResponse) raises -> Bytes: _ = builder.write_string(String("\r\n")) _ = builder.write(res.body_raw) - # Currently the server is expecting a null terminated string for conn.send(). - return builder.get_null_terminated_bytes() + return builder.get_bytes() diff --git a/lightbug_http/io/bytes.mojo b/lightbug_http/io/bytes.mojo index bf28a896..bdee734d 100644 --- a/lightbug_http/io/bytes.mojo +++ b/lightbug_http/io/bytes.mojo @@ -2,6 +2,17 @@ from python import PythonObject alias Bytes = List[Int8] +fn bytes(s: StringLiteral) -> Bytes: + # This is currently null-terminated, which we don't want in HTTP responses + var buf = String(s)._buffer + _ = buf.pop() + return buf + +fn bytes(s: String) -> Bytes: + # This is currently null-terminated, which we don't want in HTTP responses + var buf = s._buffer + _ = buf.pop() + return buf @value @register_passable("trivial") diff --git a/lightbug_http/python/server.mojo b/lightbug_http/python/server.mojo index 79458fcb..db9088cd 100644 --- a/lightbug_http/python/server.mojo +++ b/lightbug_http/python/server.mojo @@ -61,9 +61,6 @@ struct PythonServer: fn serve[ T: HTTPService ](inout self, ln: PythonTCPListener, handler: T) raises -> None: - # var max_worker_count = self.get_concurrency() - # TODO: logic for non-blocking read and write here, see for example https://github.com/valyala/fasthttp/blob/9ba16466dfd5d83e2e6a005576ee0d8e127457e2/server.go#L1789 - self.ln = ln while True: diff --git a/lightbug_http/service.mojo b/lightbug_http/service.mojo index 4ceae107..908feeab 100644 --- a/lightbug_http/service.mojo +++ b/lightbug_http/service.mojo @@ -1,5 +1,5 @@ -from lightbug_http.http import HTTPRequest, HTTPResponse, OK - +from lightbug_http.http import HTTPRequest, HTTPResponse, OK, NotFound +from lightbug_http.io.bytes import Bytes, bytes trait HTTPService: fn func(self, req: HTTPRequest) raises -> HTTPResponse: @@ -18,11 +18,21 @@ struct Printer(HTTPService): @value struct Welcome(HTTPService): fn func(self, req: HTTPRequest) raises -> HTTPResponse: - var html: String - with open("static/lightbug_welcome.html", "r") as f: - html = f.read() + var uri = req.uri() - return OK(html.as_bytes(), "text/html") + if uri.path() == "/": + var html: Bytes + with open("static/lightbug_welcome.html", "r") as f: + html = f.read_bytes() + return OK(html, "text/html; charset=utf-8") + + if uri.path() == "/logo.png": + var image: Bytes + with open("static/logo.png", "r") as f: + image = f.read_bytes() + return OK(image, "image/png") + + return NotFound(uri.path()) @value @@ -50,10 +60,8 @@ struct TechEmpowerRouter(HTTPService): var uri = req.uri() if uri.path() == "/plaintext": - return OK(String("Hello, World!").as_bytes(), "text/plain") + return OK("Hello, World!", "text/plain") elif uri.path() == "/json": - return OK( - String('{"message": "Hello, World!"}').as_bytes(), "application/json" - ) + return OK('{"message": "Hello, World!"}', "application/json") - return OK(String("Hello world!").as_bytes(), "text/plain") + return OK("Hello world!") # text/plain is the default diff --git a/lightbug_http/strings.mojo b/lightbug_http/strings.mojo index 693ab5b2..f74cdde9 100644 --- a/lightbug_http/strings.mojo +++ b/lightbug_http/strings.mojo @@ -14,7 +14,7 @@ alias rChar = String("\r").as_bytes() alias nChar = String("\n").as_bytes() -# TODO: tuples don't work with strings in Mojo currently, to be replaced with a tuple +# This is temporary due to no string support in tuples in Mojo, to be removed @value struct TwoLines: var first_line: String diff --git a/lightbug_http/sys/client.mojo b/lightbug_http/sys/client.mojo index 94b96f75..de93d1d8 100644 --- a/lightbug_http/sys/client.mojo +++ b/lightbug_http/sys/client.mojo @@ -104,17 +104,23 @@ struct MojoClient(Client): if bytes_recv == 0: conn.close() - var response = next_line(new_buf) - var headers_and_body = next_line(new_buf, "\n\n") - var headers_full = next_line(headers_and_body.first_line, "\n\n") - var headers_with_first_line = next_line(headers_full.first_line) - var first_line = headers_with_first_line.first_line - var response_headers = headers_with_first_line.rest - var response_body = headers_and_body.rest + var response_first_line_headers_and_body = next_line(new_buf, "\r\n\r\n") + var response_first_line_headers = response_first_line_headers_and_body.first_line + var response_body = response_first_line_headers_and_body.rest + + var response_first_line_and_headers = next_line(response_first_line_headers, "\r\n") + var response_first_line = response_first_line_and_headers.first_line + var response_headers = response_first_line_and_headers.rest + + # Ugly hack for now in case the default buffer is too large and we read additional responses from the server + var newline_in_body = response_body.find("\r\n") + if newline_in_body != -1: + response_body = response_body[:newline_in_body] + var header = ResponseHeader(response_headers._buffer) - + try: - header.parse(first_line) + header.parse(response_first_line) except e: conn.close() raise Error("Failed to parse response header: " + e.__str__()) diff --git a/lightbug_http/sys/net.mojo b/lightbug_http/sys/net.mojo index 622c24f4..2a0b7409 100644 --- a/lightbug_http/sys/net.mojo +++ b/lightbug_http/sys/net.mojo @@ -219,7 +219,6 @@ struct SysConnection(Connection): var new_buf = Pointer[UInt8]().alloc(default_buffer_size) var bytes_recv = recv(self.fd, new_buf, default_buffer_size, 0) if bytes_recv == -1: - print("Failed to receive message") return 0 if bytes_recv == 0: return 0 @@ -227,10 +226,14 @@ struct SysConnection(Connection): buf = bytes_str._buffer return bytes_recv - fn write(self, buf: Bytes) raises -> Int: - var msg = String(buf) + fn write(self, msg: String) raises -> Int: if send(self.fd, to_char_ptr(msg).bitcast[c_void](), len(msg), 0) == -1: print("Failed to send response") + return len(msg) + + fn write(self, buf: Bytes) raises -> Int: + if send(self.fd, to_char_ptr(buf).bitcast[c_void](), len(buf), 0) == -1: + print("Failed to send response") return len(buf) fn close(self) raises: diff --git a/lightbug_http/sys/server.mojo b/lightbug_http/sys/server.mojo index b43da7dc..c407f60f 100644 --- a/lightbug_http/sys/server.mojo +++ b/lightbug_http/sys/server.mojo @@ -109,48 +109,55 @@ struct SysServer: while True: var conn = self.ln.accept() var buf = Bytes() + var read_len = conn.read(buf) - while True: - var read_len = conn.read(buf) - if read_len == 0: - conn.close() - break - var request = next_line(buf) - var headers_and_body = next_line(request.rest, "\n\n") - var request_headers = headers_and_body.first_line - var request_body = headers_and_body.rest - var header = RequestHeader(request_headers._buffer) - try: - header.parse(request.first_line) - except e: - conn.close() - raise Error("Failed to parse request header: " + e.__str__()) + if read_len == 0: + conn.close() + break - var uri = URI(self.address() + String(header.request_uri())) - try: - uri.parse() - except e: - conn.close() - raise Error("Failed to parse request line:" + e.__str__()) - - if header.content_length() != 0 and header.content_length() != (len(request_body) + 1): - var remaining_body = Bytes() - var remaining_len = header.content_length() - len(request_body + 1) - while remaining_len > 0: - var read_len = conn.read(remaining_body) - buf.extend(remaining_body) - remaining_len -= read_len - - var res = handler.func( - HTTPRequest( - uri, - buf, - header, - ) - ) - var res_encoded = encode(res) - _ = conn.write(res_encoded) + var request_first_line_headers_and_body = next_line(buf, "\r\n\r\n") + var request_first_line_headers = request_first_line_headers_and_body.first_line + var request_body = request_first_line_headers_and_body.rest + + var request_first_line_headers_split = next_line(request_first_line_headers, "\r\n") + var request_first_line = request_first_line_headers_split.first_line + var request_headers = request_first_line_headers_split.rest + + var header = RequestHeader(request_headers._buffer) + + try: + header.parse(request_first_line) + except e: + conn.close() + raise Error("Failed to parse request header: " + e.__str__()) - if header.connection_close(): - conn.close() - break + var uri = URI(self.address() + String(header.request_uri())) + try: + uri.parse() + except e: + conn.close() + raise Error("Failed to parse request line:" + e.__str__()) + + if header.content_length() != 0 and header.content_length() != (len(request_body) + 1): + var remaining_body = Bytes() + var remaining_len = header.content_length() - len(request_body + 1) + while remaining_len > 0: + var read_len = conn.read(remaining_body) + buf.extend(remaining_body) + remaining_len -= read_len + + var res = handler.func( + HTTPRequest( + uri, + buf, + header, + ) + ) + + # Always close the connection as long as we don't support concurrency + _ = res.set_connection_close(True) + + var res_encoded = encode(res) + _ = conn.write(res_encoded) + + conn.close() diff --git a/lightbug_http/uri.mojo b/lightbug_http/uri.mojo index 5430f46d..221012d7 100644 --- a/lightbug_http/uri.mojo +++ b/lightbug_http/uri.mojo @@ -10,7 +10,6 @@ from lightbug_http.strings import ( ) -# TODO: this really needs refactoring @value struct URI: var __path_original: Bytes diff --git a/static/lightbug_welcome.html b/static/lightbug_welcome.html index 82106599..7f2d71e8 100644 --- a/static/lightbug_welcome.html +++ b/static/lightbug_welcome.html @@ -56,10 +56,8 @@
Welcome to Lightbug!
A Mojo HTTP framework with wings
-
To get started, edit lightbug.iOS fire emoji
- Lightbug Image +
To get started, edit lightbug.🔥
+ Lightbug Image \ No newline at end of file