From 99150cc0b9dcd3ceca0271487bf6f04c1002dd7b Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sat, 4 May 2024 14:30:33 -0700 Subject: [PATCH 01/17] feat: support middleware --- lightbug_http/middleware.mojo | 153 ++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 lightbug_http/middleware.mojo diff --git a/lightbug_http/middleware.mojo b/lightbug_http/middleware.mojo new file mode 100644 index 00000000..a096f645 --- /dev/null +++ b/lightbug_http/middleware.mojo @@ -0,0 +1,153 @@ +struct Context: + var request: Request + var params: Dict[String, AnyType] + + func __init__(request: Request): + self.request = request + self.params = Dict[String, AnyType]() + +trait Middleware: + var next: Middleware + + func call(context: Context) -> Response: + ... + +struct ErrorMiddleware(Middleware): + func call(context: Context) -> Response: + do: + return next.call(context: context) + catch e: Exception: + return InternalServerError() + +struct LoggerMiddleware(Middleware): + func call(context: Context) -> Response: + print("Request: \(context.request)") + return next.call(context: context) + +struct RouterMiddleware(Middleware): + var routes: Dict[String, Middleware] + + func __init__(): + self.routes = Dict[String, Middleware]() + + func add(route: String, middleware: Middleware): + routes[route] = middleware + + func call(context: Context) -> Response: + # TODO: create a more advanced router + + var route = context.request.path + if middleware = routes[route]: + return middleware.call(context: context) + else: + return NotFound() + +struct StaticMiddleware(Middleware): + var path: String + + funct __init__(path: String): + self.path = path + + func call(context: Context) -> Response: + var file = File(path: path + context.request.path) + if file.exists: + return FileResponse(file: file) + else: + return next.call(context: context) + +struct CorsMiddleware(Middleware): + func call(context: Context) -> Response: + var response = next.call(context: context) + response.headers["Access-Control-Allow-Origin"] = "*" + return response + +struct CompressionMiddleware(Middleware): + func call(context: Context) -> Response: + var response = next.call(context: context) + response.body = compress(response.body) + return response + +struct SessionMiddleware(Middleware): + var session: Session + + func call(context: Context) -> Response: + var request = context.request + var response = context.response + var session = session.load(request) + context.params["session"] = session + response = next.call(context: context) + session.save(response) + return response + +struct BasicAuthMiddleware(Middleware): + var username: String + var password: String + + func __init__(username: String, password: String): + self.username = username + self.password = password + + func call(context: Context) -> Response: + var request = context.request + var auth = request.headers["Authorization"] + if auth == "Basic \(username):\(password)": + return next.call(context: context) + else: + return Unauthorized() + +struct MiddlewareChain: + var middlewares: Array[Middleware] + + func __init__(): + self.middlewares = Array[Middleware]() + + func add(middleware: Middleware): + if middlewares.count == 0: + middlewares.append(middleware) + else: + var last = middlewares[middlewares.count - 1] + last.next = middleware + middlewares.append(middleware) + + func execute(request: Request) -> Response: + var context = Context(request: request, response: response) + if middlewares.count > 0: + return middlewares[0].call(context: context) + else: + return NotFound() + +fn OK(body: Bytes) -> HTTPResponse: + return OK(body, String("text/plain")) + +fn OK(body: Bytes, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 200, String("OK").as_bytes(), content_type.as_bytes()), + body, + ) + +fn NotFound(body: Bytes) -> HTTPResponse: + return NotFoundResponse(body, String("text/plain")) + +fn NotFound(body: Bytes, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 404, String("Not Found").as_bytes(), content_type.as_bytes()), + body, + ) + +fn InternalServerError(body: Bytes) -> HTTPResponse: + return InternalServerErrorResponse(body, String("text/plain")) + +fn InternalServerError(body: Bytes, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 500, String("Internal Server Error").as_bytes(), content_type.as_bytes()), + body, + ) + +fn Unauthorized(body: Bytes) -> HTTPResponse: + return UnauthorizedResponse(body, String("text/plain")) + +fn Unauthorized(body: Bytes, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 401, String("Unauthorized").as_bytes(), content_type.as_bytes()), + body, + ) From c8f6f44a890361771647ba63b8e348f051a094cf Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sat, 4 May 2024 15:09:26 -0700 Subject: [PATCH 02/17] feat: support middleware --- "lightbug.\360\237\224\245" | 16 ++- lightbug_http/__init__.mojo | 1 + lightbug_http/middleware.mojo | 123 +++++++++++-------- lightbug_http/service.mojo | 3 + static/{lightbug_welcome.html => index.html} | 0 5 files changed, 89 insertions(+), 54 deletions(-) rename static/{lightbug_welcome.html => index.html} (100%) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index ad27aacc..6372ed84 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -2,5 +2,17 @@ from lightbug_http import * fn main() raises: var server = SysServer() - var handler = Welcome() - server.listen_and_serve("0.0.0.0:8080", handler) + var middleware = MiddlewareChain() + middleware.add(CompressionMiddleware()) + middleware.add(ErrorMiddleware()) + middleware.add(LoggerMiddleware()) + middleware.add(CorsMiddleware(allows_origin = "*")) + middleware.add(BasicAuthMiddleware("admin", "password")) + + var router = RouterMiddleware() + router.add("GET", "/static", StaticMiddleware("static")) + + middleware.add(router) + + server.listen_and_serve("0.0.0.0:8080", middleware) + diff --git a/lightbug_http/__init__.mojo b/lightbug_http/__init__.mojo index 7259a87c..e5c8ed44 100644 --- a/lightbug_http/__init__.mojo +++ b/lightbug_http/__init__.mojo @@ -2,6 +2,7 @@ from lightbug_http.http import HTTPRequest, HTTPResponse, OK from lightbug_http.service import HTTPService, Welcome from lightbug_http.sys.server import SysServer from lightbug_http.tests.run import run_tests +from lightbug_http.middleware import * trait DefaultConstructible: fn __init__(inout self) raises: diff --git a/lightbug_http/middleware.mojo b/lightbug_http/middleware.mojo index a096f645..fd10576f 100644 --- a/lightbug_http/middleware.mojo +++ b/lightbug_http/middleware.mojo @@ -1,107 +1,128 @@ +from lightbug_http.http import HTTPRequest, HTTPResponse + struct Context: var request: Request var params: Dict[String, AnyType] - func __init__(request: Request): + fn __init__(self, request: Request): self.request = request self.params = Dict[String, AnyType]() trait Middleware: var next: Middleware - func call(context: Context) -> Response: + fn call(self, context: Context) -> Response: ... struct ErrorMiddleware(Middleware): - func call(context: Context) -> Response: - do: + fn call(self, context: Context) -> Response: + try: return next.call(context: context) catch e: Exception: return InternalServerError() struct LoggerMiddleware(Middleware): - func call(context: Context) -> Response: + fn call(self, context: Context) -> Response: print("Request: \(context.request)") return next.call(context: context) -struct RouterMiddleware(Middleware): - var routes: Dict[String, Middleware] - - func __init__(): - self.routes = Dict[String, Middleware]() - - func add(route: String, middleware: Middleware): - routes[route] = middleware - - func call(context: Context) -> Response: - # TODO: create a more advanced router - - var route = context.request.path - if middleware = routes[route]: - return middleware.call(context: context) - else: - return NotFound() - struct StaticMiddleware(Middleware): var path: String - funct __init__(path: String): + fnt __init__(self, path: String): self.path = path - func call(context: Context) -> Response: - var file = File(path: path + context.request.path) + fn call(self, context: Context) -> Response: + if context.request.path == "/": + var file = File(path: path + "index.html") + else: + var file = File(path: path + context.request.path) + if file.exists: - return FileResponse(file: file) + var html: String + with open(file, "r") as f: + html = f.read() + return OK(html.as_bytes(), "text/html") else: return next.call(context: context) struct CorsMiddleware(Middleware): - func call(context: Context) -> Response: - var response = next.call(context: context) - response.headers["Access-Control-Allow-Origin"] = "*" - return response + var allow_origin: String + + fn __init__(self, allow_origin: String): + self.allow_origin = allow_origin + + fn call(self, context: Context) -> Response: + if context.request.method == "OPTIONS": + var response = next.call(context: context) + response.headers["Access-Control-Allow-Origin"] = allow_origin + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" + return response + + if context.request.origin == allow_origin: + return next.call(context: context) + else: + return Unauthorized() struct CompressionMiddleware(Middleware): - func call(context: Context) -> Response: + fn call(self, context: Context) -> Response: var response = next.call(context: context) response.body = compress(response.body) return response -struct SessionMiddleware(Middleware): - var session: Session + fn compress(self, body: Bytes) -> Bytes: + #TODO: implement compression + return body - func call(context: Context) -> Response: - var request = context.request - var response = context.response - var session = session.load(request) - context.params["session"] = session - response = next.call(context: context) - session.save(response) - return response + +struct RouterMiddleware(Middleware): + var routes: Dict[String, Middleware] + + fn __init__(self): + self.routes = Dict[String, Middleware]() + + fn add(self, method: String, route: String, middleware: Middleware): + routes[method + ":" + route] = middleware + + fn call(self, context: Context) -> Response: + # TODO: create a more advanced router + var method = context.request.method + var route = context.request.path + if middleware = routes[method + ":" + route]: + return middleware.call(context: context) + else: + return next.call(context: context) struct BasicAuthMiddleware(Middleware): var username: String var password: String - func __init__(username: String, password: String): + fn __init__(self, username: String, password: String): self.username = username self.password = password - func call(context: Context) -> Response: + fn call(self, context: Context) -> Response: var request = context.request var auth = request.headers["Authorization"] if auth == "Basic \(username):\(password)": + context.params["username"] = username return next.call(context: context) else: return Unauthorized() -struct MiddlewareChain: +# always add at the end of the middleware chain +struct NotFoundMiddleware(Middleware): + fn call(self, context: Context) -> Response: + return NotFound() + +struct MiddlewareChain(HttpService): var middlewares: Array[Middleware] - func __init__(): + fn __init__(self): self.middlewares = Array[Middleware]() - func add(middleware: Middleware): + fn add(self, middleware: Middleware): if middlewares.count == 0: middlewares.append(middleware) else: @@ -109,12 +130,10 @@ struct MiddlewareChain: last.next = middleware middlewares.append(middleware) - func execute(request: Request) -> Response: + fn func(self, request: Request) -> Response: + self.add(NotFoundMiddleware()) var context = Context(request: request, response: response) - if middlewares.count > 0: - return middlewares[0].call(context: context) - else: - return NotFound() + return middlewares[0].call(context: context) fn OK(body: Bytes) -> HTTPResponse: return OK(body, String("text/plain")) diff --git a/lightbug_http/service.mojo b/lightbug_http/service.mojo index 908feeab..0731ecdb 100644 --- a/lightbug_http/service.mojo +++ b/lightbug_http/service.mojo @@ -19,6 +19,9 @@ struct Printer(HTTPService): struct Welcome(HTTPService): fn func(self, req: HTTPRequest) raises -> HTTPResponse: var uri = req.uri() + var html: String + with open("static/index.html", "r") as f: + html = f.read() if uri.path() == "/": var html: Bytes diff --git a/static/lightbug_welcome.html b/static/index.html similarity index 100% rename from static/lightbug_welcome.html rename to static/index.html From af9f37c06c3e21bc832dd63814c4a7be799086b5 Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sat, 4 May 2024 16:27:15 -0700 Subject: [PATCH 03/17] fix: more bug fixes --- "lightbug.\360\237\224\245" | 11 ++-- lightbug_http/middleware.mojo | 115 +++++++++++++++++++--------------- 2 files changed, 69 insertions(+), 57 deletions(-) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index 6372ed84..c89a27a7 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -2,17 +2,18 @@ from lightbug_http import * fn main() raises: var server = SysServer() + + var router = RouterMiddleware() + router.add("GET", "/static", StaticMiddleware("static")) + var middleware = MiddlewareChain() middleware.add(CompressionMiddleware()) middleware.add(ErrorMiddleware()) middleware.add(LoggerMiddleware()) middleware.add(CorsMiddleware(allows_origin = "*")) middleware.add(BasicAuthMiddleware("admin", "password")) - - var router = RouterMiddleware() - router.add("GET", "/static", StaticMiddleware("static")) - middleware.add(router) - + middleware.add(NotFoundMiddleware()) + server.listen_and_serve("0.0.0.0:8080", middleware) diff --git a/lightbug_http/middleware.mojo b/lightbug_http/middleware.mojo index fd10576f..d0a723cb 100644 --- a/lightbug_http/middleware.mojo +++ b/lightbug_http/middleware.mojo @@ -1,42 +1,47 @@ -from lightbug_http.http import HTTPRequest, HTTPResponse +from lightbug_http.http import * +from lightbug_http.service import HTTPService +@value struct Context: - var request: Request - var params: Dict[String, AnyType] + var request: HTTPRequest + var params: Dict[String, String] - fn __init__(self, request: Request): + fn __init__(inout self, request: HTTPRequest): self.request = request - self.params = Dict[String, AnyType]() + self.params = Dict[String, String]() trait Middleware: - var next: Middleware - - fn call(self, context: Context) -> Response: + fn call(self, context: Context) -> HTTPResponse: ... struct ErrorMiddleware(Middleware): - fn call(self, context: Context) -> Response: + var next: Middleware + + fn call(inout self, context: Context) -> HTTPResponse: try: - return next.call(context: context) + return next.call(context) catch e: Exception: return InternalServerError() struct LoggerMiddleware(Middleware): - fn call(self, context: Context) -> Response: - print("Request: \(context.request)") - return next.call(context: context) + var next: Middleware + + fn call(self, context: Context) -> HTTPResponse: + print(f"Request: {context.request}") + return next.call(context) struct StaticMiddleware(Middleware): + var next: Middleware var path: String - fnt __init__(self, path: String): + fn __init__(self, path: String): self.path = path - fn call(self, context: Context) -> Response: - if context.request.path == "/": + fn call(self, context: Context) -> HTTPResponse: + if context.request.uri().path() == "/": var file = File(path: path + "index.html") else: - var file = File(path: path + context.request.path) + var file = File(path: path + context.request.uri().path()) if file.exists: var html: String @@ -44,30 +49,32 @@ struct StaticMiddleware(Middleware): html = f.read() return OK(html.as_bytes(), "text/html") else: - return next.call(context: context) + return next.call(context) struct CorsMiddleware(Middleware): + var next: Middleware var allow_origin: String fn __init__(self, allow_origin: String): self.allow_origin = allow_origin - fn call(self, context: Context) -> Response: - if context.request.method == "OPTIONS": - var response = next.call(context: context) + fn call(self, context: Context) -> HTTPResponse: + if context.request.header.method() == "OPTIONS": + var response = next.call(context) response.headers["Access-Control-Allow-Origin"] = allow_origin response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" return response if context.request.origin == allow_origin: - return next.call(context: context) + return next.call(context) else: return Unauthorized() struct CompressionMiddleware(Middleware): - fn call(self, context: Context) -> Response: - var response = next.call(context: context) + var next: Middleware + fn call(self, context: Context) -> HTTPResponse: + var response = next.call(context) response.body = compress(response.body) return response @@ -75,26 +82,28 @@ struct CompressionMiddleware(Middleware): #TODO: implement compression return body - struct RouterMiddleware(Middleware): + var next: Middleware var routes: Dict[String, Middleware] - fn __init__(self): + fn __init__(inout self): self.routes = Dict[String, Middleware]() fn add(self, method: String, route: String, middleware: Middleware): - routes[method + ":" + route] = middleware + self.routes[method + ":" + route] = middleware - fn call(self, context: Context) -> Response: + fn call(self, context: Context) -> HTTPResponse: # TODO: create a more advanced router - var method = context.request.method - var route = context.request.path - if middleware = routes[method + ":" + route]: - return middleware.call(context: context) + var method = context.request.header.method() + var route = context.request.uri().path() + var middleware = self.routes.find(method + ":" + route) + if middleware: + return middleware.value().call(context) else: - return next.call(context: context) + return next.call(context) struct BasicAuthMiddleware(Middleware): + var next: Middleware var username: String var password: String @@ -102,38 +111,37 @@ struct BasicAuthMiddleware(Middleware): self.username = username self.password = password - fn call(self, context: Context) -> Response: + fn call(self, context: Context) -> HTTPResponse: var request = context.request var auth = request.headers["Authorization"] - if auth == "Basic \(username):\(password)": + if auth == f"Basic {username}:{password}": context.params["username"] = username - return next.call(context: context) + return next.call(context) else: - return Unauthorized() + return Unauthorized("Requires Basic Authentication") # always add at the end of the middleware chain struct NotFoundMiddleware(Middleware): - fn call(self, context: Context) -> Response: - return NotFound() + fn call(self, context: Context) -> HTTPResponse: + return NotFound(String("Not Found").as_bytes()) -struct MiddlewareChain(HttpService): - var middlewares: Array[Middleware] +struct MiddlewareChain(HTTPService): + var middlewares: List[Middleware] - fn __init__(self): + fn __init__(inout self): self.middlewares = Array[Middleware]() fn add(self, middleware: Middleware): - if middlewares.count == 0: - middlewares.append(middleware) + if self.middlewares.count == 0: + self.middlewares.append(middleware) else: - var last = middlewares[middlewares.count - 1] + var last = self.middlewares[middlewares.count - 1] last.next = middleware - middlewares.append(middleware) + self.middlewares.append(middleware) - fn func(self, request: Request) -> Response: - self.add(NotFoundMiddleware()) - var context = Context(request: request, response: response) - return middlewares[0].call(context: context) + fn func(self, req: HTTPRequest) raises -> HTTPResponse: + var context = Context(request) + return self.middlewares[0].call(context) fn OK(body: Bytes) -> HTTPResponse: return OK(body, String("text/plain")) @@ -145,7 +153,7 @@ fn OK(body: Bytes, content_type: String) -> HTTPResponse: ) fn NotFound(body: Bytes) -> HTTPResponse: - return NotFoundResponse(body, String("text/plain")) + return NotFound(body, String("text/plain")) fn NotFound(body: Bytes, content_type: String) -> HTTPResponse: return HTTPResponse( @@ -166,7 +174,10 @@ fn Unauthorized(body: Bytes) -> HTTPResponse: return UnauthorizedResponse(body, String("text/plain")) fn Unauthorized(body: Bytes, content_type: String) -> HTTPResponse: + var header = ResponseHeader(True, 401, String("Unauthorized").as_bytes(), content_type.as_bytes()) + header.headers["WWW-Authenticate"] = "Basic realm=\"Login Required\"" + return HTTPResponse( - ResponseHeader(True, 401, String("Unauthorized").as_bytes(), content_type.as_bytes()), + header, body, ) From 76bb36dce241dc08f981a19a12444c7ce0eb00e0 Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sun, 5 May 2024 04:34:45 -0700 Subject: [PATCH 04/17] fix: fixed most issues. --- "lightbug.\360\237\224\245" | 10 +- lightbug_http/middleware.mojo | 206 +++++++++++++++++++++++----------- lightbug_http/service.mojo | 4 + 3 files changed, 149 insertions(+), 71 deletions(-) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index c89a27a7..ac203ed3 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -1,10 +1,12 @@ from lightbug_http import * -fn main() raises: - var server = SysServer() +struct HelloWorld(HTTPHandler): + fn handle(self, context: Context) -> HTTPResponse: + return OK(String("Hello, World!").as_bytes()) +fn main() raises: var router = RouterMiddleware() - router.add("GET", "/static", StaticMiddleware("static")) + router.add("GET", "/hello", HelloWorld()) var middleware = MiddlewareChain() middleware.add(CompressionMiddleware()) @@ -12,8 +14,10 @@ fn main() raises: middleware.add(LoggerMiddleware()) middleware.add(CorsMiddleware(allows_origin = "*")) middleware.add(BasicAuthMiddleware("admin", "password")) + middleware.add(StaticMiddleware("static")) middleware.add(router) middleware.add(NotFoundMiddleware()) + var server = SysServer() server.listen_and_serve("0.0.0.0:8080", middleware) diff --git a/lightbug_http/middleware.mojo b/lightbug_http/middleware.mojo index d0a723cb..fc2115dd 100644 --- a/lightbug_http/middleware.mojo +++ b/lightbug_http/middleware.mojo @@ -1,6 +1,10 @@ from lightbug_http.http import * from lightbug_http.service import HTTPService + +## Context is a container for the request and response data. +## It is passed to each middleware in the chain. +## It also contains a dictionary of parameters that can be shared between middleware. @value struct Context: var request: HTTPRequest @@ -10,51 +14,93 @@ struct Context: self.request = request self.params = Dict[String, String]() + +## Middleware is an interface for processing HTTP requests. +## Each middleware in the chain can modify the request and response. trait Middleware: + fn set_next(self, next: Middleware): + ... fn call(self, context: Context) -> HTTPResponse: ... +## MiddlewareChain is a chain of middleware that processes the request. +@value +struct MiddlewareChain(HTTPService): + var root: Middleware + + fn add(self, middleware: Middleware): + if self.root == nil: + self.root = middleware + else: + var current = self.root + while current.next != nil: + current = current.next + current.set_next(middleware) + + fn func(self, request: HTTPRequest) raises -> HTTPResponse: + var context = Context(request) + return self.root.call(context) + + +# Middleware implementations + +## Error handler will catch any exceptions thrown by the other +## middleware and return a 500 response. +## It should be the first middleware in the chain. +@value struct ErrorMiddleware(Middleware): var next: Middleware + fn set_next(self, next: Middleware): + self.next = next + fn call(inout self, context: Context) -> HTTPResponse: try: - return next.call(context) - catch e: Exception: - return InternalServerError() + return self.next.call(context) + except e: + return InternalServerError(e.as_bytes()) -struct LoggerMiddleware(Middleware): + +## Compression middleware compresses the response body. +@value +struct CompressionMiddleware(Middleware): var next: Middleware + fn set_next(self, next: Middleware): + self.next = next + fn call(self, context: Context) -> HTTPResponse: - print(f"Request: {context.request}") - return next.call(context) + var response = self.next.call(context) + response.body = self.compress(response.body) + return response -struct StaticMiddleware(Middleware): + fn compress(self, body: Bytes) -> Bytes: + #TODO: implement compression + return body + + +## Logger middleware logs the request to the console. +@value +struct LoggerMiddleware(Middleware): var next: Middleware - var path: String - fn __init__(self, path: String): - self.path = path + fn set_next(self, next: Middleware): + self.next = next fn call(self, context: Context) -> HTTPResponse: - if context.request.uri().path() == "/": - var file = File(path: path + "index.html") - else: - var file = File(path: path + context.request.uri().path()) + print(f"Request: {context.request}") + return self.next.call(context) - if file.exists: - var html: String - with open(file, "r") as f: - html = f.read() - return OK(html.as_bytes(), "text/html") - else: - return next.call(context) +## CORS middleware adds the necessary headers to allow cross-origin requests. +@value struct CorsMiddleware(Middleware): var next: Middleware var allow_origin: String + fn set_next(self, next: Middleware): + self.next = next + fn __init__(self, allow_origin: String): self.allow_origin = allow_origin @@ -67,46 +113,21 @@ struct CorsMiddleware(Middleware): return response if context.request.origin == allow_origin: - return next.call(context) + return self.next.call(context) else: - return Unauthorized() - -struct CompressionMiddleware(Middleware): - var next: Middleware - fn call(self, context: Context) -> HTTPResponse: - var response = next.call(context) - response.body = compress(response.body) - return response - - fn compress(self, body: Bytes) -> Bytes: - #TODO: implement compression - return body - -struct RouterMiddleware(Middleware): - var next: Middleware - var routes: Dict[String, Middleware] - - fn __init__(inout self): - self.routes = Dict[String, Middleware]() - - fn add(self, method: String, route: String, middleware: Middleware): - self.routes[method + ":" + route] = middleware + return Unauthorized("CORS not allowed") - fn call(self, context: Context) -> HTTPResponse: - # TODO: create a more advanced router - var method = context.request.header.method() - var route = context.request.uri().path() - var middleware = self.routes.find(method + ":" + route) - if middleware: - return middleware.value().call(context) - else: - return next.call(context) +## BasicAuth middleware requires basic authentication to access the route. +@value struct BasicAuthMiddleware(Middleware): var next: Middleware var username: String var password: String + fn set_next(self, next: Middleware): + self.next = next + fn __init__(self, username: String, password: String): self.username = username self.password = password @@ -120,29 +141,78 @@ struct BasicAuthMiddleware(Middleware): else: return Unauthorized("Requires Basic Authentication") -# always add at the end of the middleware chain -struct NotFoundMiddleware(Middleware): + +## Static middleware serves static files from a directory. +@value +struct StaticMiddleware(Middleware): + var next: Middleware + var path: String + + fn set_next(self, next: Middleware): + self.next = next + + fn __init__(self, path: String): + self.path = path + fn call(self, context: Context) -> HTTPResponse: - return NotFound(String("Not Found").as_bytes()) + var path = context.request.uri().path() + if path.endswith("/"): + path = path + "index.html" -struct MiddlewareChain(HTTPService): - var middlewares: List[Middleware] + try: + var html: String + with open(file, "r") as f: + html = f.read() + + return OK(html.as_bytes(), "text/html") + except e: + return self.next.call(context) + + + + +## HTTPHandler is an interface for handling HTTP requests in the RouterMiddleware. +## It is a leaf node in the middleware chain. +trait HTTPHandler: + fn handle(self, context: Context) -> HTTPResponse: + ... + + +## Router middleware routes requests to different middleware based on the path. +@value +struct RouterMiddleware(Middleware): + var next: Middleware + var routes: Dict[String, HTTPHandler] fn __init__(inout self): - self.middlewares = Array[Middleware]() + self.routes = Dict[String, HTTPHandler]() - fn add(self, middleware: Middleware): - if self.middlewares.count == 0: - self.middlewares.append(middleware) + fn set_next(self, next: Middleware): + self.next = next + + fn add(self, method: String, route: String, handler: HTTPHandler): + self.routes[method + ":" + route] = handler + + fn call(self, context: Context) -> HTTPResponse: + # TODO: create a more advanced router + var method = context.request.header.method() + var route = context.request.uri().path() + var handler = self.routes.find(method + ":" + route) + if handler: + return handler.value().handle(context) else: - var last = self.middlewares[middlewares.count - 1] - last.next = middleware - self.middlewares.append(middleware) + return next.call(context) + + +## NotFound middleware returns a 404 response if no other middleware handles the request. It is a leaf node and always add at the end of the middleware chain +@value +struct NotFoundMiddleware(Middleware): + fn call(self, context: Context) -> HTTPResponse: + return NotFound(String("Not Found").as_bytes()) + - fn func(self, req: HTTPRequest) raises -> HTTPResponse: - var context = Context(request) - return self.middlewares[0].call(context) +### Helper functions to create HTTP responses fn OK(body: Bytes) -> HTTPResponse: return OK(body, String("text/plain")) diff --git a/lightbug_http/service.mojo b/lightbug_http/service.mojo index 0731ecdb..5134752d 100644 --- a/lightbug_http/service.mojo +++ b/lightbug_http/service.mojo @@ -5,6 +5,10 @@ trait HTTPService: fn func(self, req: HTTPRequest) raises -> HTTPResponse: ... +@value +struct HelloWorld(HTTPService): + fn func(self, req: HTTPRequest) raises -> HTTPResponse: + return OK("Hello, World!".as_bytes(), "text/plain") @value struct Printer(HTTPService): From 6efed03e019d5fab35aa513fdb71b94ad6263f43 Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sun, 5 May 2024 04:46:08 -0700 Subject: [PATCH 05/17] fix: change response helpers to String --- "lightbug.\360\237\224\245" | 2 +- lightbug_http/middleware.mojo | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index ac203ed3..7967f511 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -2,7 +2,7 @@ from lightbug_http import * struct HelloWorld(HTTPHandler): fn handle(self, context: Context) -> HTTPResponse: - return OK(String("Hello, World!").as_bytes()) + return Success("Hello, World!") fn main() raises: var router = RouterMiddleware() diff --git a/lightbug_http/middleware.mojo b/lightbug_http/middleware.mojo index fc2115dd..8e184ced 100644 --- a/lightbug_http/middleware.mojo +++ b/lightbug_http/middleware.mojo @@ -58,7 +58,7 @@ struct ErrorMiddleware(Middleware): try: return self.next.call(context) except e: - return InternalServerError(e.as_bytes()) + return InternalServerError(e) ## Compression middleware compresses the response body. @@ -164,7 +164,7 @@ struct StaticMiddleware(Middleware): with open(file, "r") as f: html = f.read() - return OK(html.as_bytes(), "text/html") + return Success(html, "text/html") except e: return self.next.call(context) @@ -208,46 +208,46 @@ struct RouterMiddleware(Middleware): @value struct NotFoundMiddleware(Middleware): fn call(self, context: Context) -> HTTPResponse: - return NotFound(String("Not Found").as_bytes()) + return NotFound("Not Found") ### Helper functions to create HTTP responses -fn OK(body: Bytes) -> HTTPResponse: - return OK(body, String("text/plain")) +fn Success(body: String) -> HTTPResponse: + return Success(body, String("text/plain")) -fn OK(body: Bytes, content_type: String) -> HTTPResponse: +fn Success(body: String, content_type: String) -> HTTPResponse: return HTTPResponse( - ResponseHeader(True, 200, String("OK").as_bytes(), content_type.as_bytes()), - body, + ResponseHeader(True, 200, String("Success").as_bytes(), content_type.as_bytes()), + body.as_bytes(), ) -fn NotFound(body: Bytes) -> HTTPResponse: +fn NotFound(body: String) -> HTTPResponse: return NotFound(body, String("text/plain")) -fn NotFound(body: Bytes, content_type: String) -> HTTPResponse: +fn NotFound(body: String, content_type: String) -> HTTPResponse: return HTTPResponse( ResponseHeader(True, 404, String("Not Found").as_bytes(), content_type.as_bytes()), - body, + body.as_bytes(), ) -fn InternalServerError(body: Bytes) -> HTTPResponse: +fn InternalServerError(body: String) -> HTTPResponse: return InternalServerErrorResponse(body, String("text/plain")) -fn InternalServerError(body: Bytes, content_type: String) -> HTTPResponse: +fn InternalServerError(body: String, content_type: String) -> HTTPResponse: return HTTPResponse( ResponseHeader(True, 500, String("Internal Server Error").as_bytes(), content_type.as_bytes()), - body, + body.as_bytes(), ) -fn Unauthorized(body: Bytes) -> HTTPResponse: +fn Unauthorized(body: String) -> HTTPResponse: return UnauthorizedResponse(body, String("text/plain")) -fn Unauthorized(body: Bytes, content_type: String) -> HTTPResponse: +fn Unauthorized(body: String, content_type: String) -> HTTPResponse: var header = ResponseHeader(True, 401, String("Unauthorized").as_bytes(), content_type.as_bytes()) header.headers["WWW-Authenticate"] = "Basic realm=\"Login Required\"" return HTTPResponse( header, - body, + body.as_bytes(), ) From 6c5008210ad0d96fb18ab3bdd624017432176e08 Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sun, 5 May 2024 05:02:00 -0700 Subject: [PATCH 06/17] fix: demonstrate params in context --- "lightbug.\360\237\224\245" | 3 ++- lightbug_http/middleware.mojo | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index 7967f511..c74e4370 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -2,7 +2,8 @@ from lightbug_http import * struct HelloWorld(HTTPHandler): fn handle(self, context: Context) -> HTTPResponse: - return Success("Hello, World!") + var name = context.params.get("username", "world") + return Success("Hello, " + name + "!") fn main() raises: var router = RouterMiddleware() diff --git a/lightbug_http/middleware.mojo b/lightbug_http/middleware.mojo index 8e184ced..fa78e12d 100644 --- a/lightbug_http/middleware.mojo +++ b/lightbug_http/middleware.mojo @@ -8,11 +8,11 @@ from lightbug_http.service import HTTPService @value struct Context: var request: HTTPRequest - var params: Dict[String, String] + var params: Dict[String, AnyType] fn __init__(inout self, request: HTTPRequest): self.request = request - self.params = Dict[String, String]() + self.params = Dict[String, AnyType]() ## Middleware is an interface for processing HTTP requests. @@ -24,6 +24,7 @@ trait Middleware: ... ## MiddlewareChain is a chain of middleware that processes the request. +## The chain is a linked list of middleware objects. @value struct MiddlewareChain(HTTPService): var root: Middleware From 19bbf1222820d584acdb57d306a399114fca8bae Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sun, 5 May 2024 08:50:19 -0700 Subject: [PATCH 07/17] refactor: more cleanup and moving stuff around --- "lightbug.\360\237\224\245" | 1 + lightbug_http/middleware.mojo | 254 ---------------------- lightbug_http/middleware/__init__.mojo | 17 ++ lightbug_http/middleware/basicauth.mojo | 24 ++ lightbug_http/middleware/compression.mojo | 17 ++ lightbug_http/middleware/cors.mojo | 26 +++ lightbug_http/middleware/error.mojo | 18 ++ lightbug_http/middleware/helpers.mojo | 39 ++++ lightbug_http/middleware/logger.mojo | 12 + lightbug_http/middleware/middleware.mojo | 43 ++++ lightbug_http/middleware/notfound.mojo | 7 + lightbug_http/middleware/router.mojo | 31 +++ lightbug_http/middleware/static.mojo | 27 +++ 13 files changed, 262 insertions(+), 254 deletions(-) delete mode 100644 lightbug_http/middleware.mojo create mode 100644 lightbug_http/middleware/__init__.mojo create mode 100644 lightbug_http/middleware/basicauth.mojo create mode 100644 lightbug_http/middleware/compression.mojo create mode 100644 lightbug_http/middleware/cors.mojo create mode 100644 lightbug_http/middleware/error.mojo create mode 100644 lightbug_http/middleware/helpers.mojo create mode 100644 lightbug_http/middleware/logger.mojo create mode 100644 lightbug_http/middleware/middleware.mojo create mode 100644 lightbug_http/middleware/notfound.mojo create mode 100644 lightbug_http/middleware/router.mojo create mode 100644 lightbug_http/middleware/static.mojo diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index c74e4370..a69472a5 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -1,4 +1,5 @@ from lightbug_http import * +from lightbug_http.middleware.helpers import Success struct HelloWorld(HTTPHandler): fn handle(self, context: Context) -> HTTPResponse: diff --git a/lightbug_http/middleware.mojo b/lightbug_http/middleware.mojo deleted file mode 100644 index fa78e12d..00000000 --- a/lightbug_http/middleware.mojo +++ /dev/null @@ -1,254 +0,0 @@ -from lightbug_http.http import * -from lightbug_http.service import HTTPService - - -## Context is a container for the request and response data. -## It is passed to each middleware in the chain. -## It also contains a dictionary of parameters that can be shared between middleware. -@value -struct Context: - var request: HTTPRequest - var params: Dict[String, AnyType] - - fn __init__(inout self, request: HTTPRequest): - self.request = request - self.params = Dict[String, AnyType]() - - -## Middleware is an interface for processing HTTP requests. -## Each middleware in the chain can modify the request and response. -trait Middleware: - fn set_next(self, next: Middleware): - ... - fn call(self, context: Context) -> HTTPResponse: - ... - -## MiddlewareChain is a chain of middleware that processes the request. -## The chain is a linked list of middleware objects. -@value -struct MiddlewareChain(HTTPService): - var root: Middleware - - fn add(self, middleware: Middleware): - if self.root == nil: - self.root = middleware - else: - var current = self.root - while current.next != nil: - current = current.next - current.set_next(middleware) - - fn func(self, request: HTTPRequest) raises -> HTTPResponse: - var context = Context(request) - return self.root.call(context) - - -# Middleware implementations - -## Error handler will catch any exceptions thrown by the other -## middleware and return a 500 response. -## It should be the first middleware in the chain. -@value -struct ErrorMiddleware(Middleware): - var next: Middleware - - fn set_next(self, next: Middleware): - self.next = next - - fn call(inout self, context: Context) -> HTTPResponse: - try: - return self.next.call(context) - except e: - return InternalServerError(e) - - -## Compression middleware compresses the response body. -@value -struct CompressionMiddleware(Middleware): - var next: Middleware - - fn set_next(self, next: Middleware): - self.next = next - - fn call(self, context: Context) -> HTTPResponse: - var response = self.next.call(context) - response.body = self.compress(response.body) - return response - - fn compress(self, body: Bytes) -> Bytes: - #TODO: implement compression - return body - - -## Logger middleware logs the request to the console. -@value -struct LoggerMiddleware(Middleware): - var next: Middleware - - fn set_next(self, next: Middleware): - self.next = next - - fn call(self, context: Context) -> HTTPResponse: - print(f"Request: {context.request}") - return self.next.call(context) - - -## CORS middleware adds the necessary headers to allow cross-origin requests. -@value -struct CorsMiddleware(Middleware): - var next: Middleware - var allow_origin: String - - fn set_next(self, next: Middleware): - self.next = next - - fn __init__(self, allow_origin: String): - self.allow_origin = allow_origin - - fn call(self, context: Context) -> HTTPResponse: - if context.request.header.method() == "OPTIONS": - var response = next.call(context) - response.headers["Access-Control-Allow-Origin"] = allow_origin - response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" - response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" - return response - - if context.request.origin == allow_origin: - return self.next.call(context) - else: - return Unauthorized("CORS not allowed") - - -## BasicAuth middleware requires basic authentication to access the route. -@value -struct BasicAuthMiddleware(Middleware): - var next: Middleware - var username: String - var password: String - - fn set_next(self, next: Middleware): - self.next = next - - fn __init__(self, username: String, password: String): - self.username = username - self.password = password - - fn call(self, context: Context) -> HTTPResponse: - var request = context.request - var auth = request.headers["Authorization"] - if auth == f"Basic {username}:{password}": - context.params["username"] = username - return next.call(context) - else: - return Unauthorized("Requires Basic Authentication") - - -## Static middleware serves static files from a directory. -@value -struct StaticMiddleware(Middleware): - var next: Middleware - var path: String - - fn set_next(self, next: Middleware): - self.next = next - - fn __init__(self, path: String): - self.path = path - - fn call(self, context: Context) -> HTTPResponse: - var path = context.request.uri().path() - if path.endswith("/"): - path = path + "index.html" - - try: - var html: String - with open(file, "r") as f: - html = f.read() - - return Success(html, "text/html") - except e: - return self.next.call(context) - - - - -## HTTPHandler is an interface for handling HTTP requests in the RouterMiddleware. -## It is a leaf node in the middleware chain. -trait HTTPHandler: - fn handle(self, context: Context) -> HTTPResponse: - ... - - -## Router middleware routes requests to different middleware based on the path. -@value -struct RouterMiddleware(Middleware): - var next: Middleware - var routes: Dict[String, HTTPHandler] - - fn __init__(inout self): - self.routes = Dict[String, HTTPHandler]() - - fn set_next(self, next: Middleware): - self.next = next - - fn add(self, method: String, route: String, handler: HTTPHandler): - self.routes[method + ":" + route] = handler - - fn call(self, context: Context) -> HTTPResponse: - # TODO: create a more advanced router - var method = context.request.header.method() - var route = context.request.uri().path() - var handler = self.routes.find(method + ":" + route) - if handler: - return handler.value().handle(context) - else: - return next.call(context) - - -## NotFound middleware returns a 404 response if no other middleware handles the request. It is a leaf node and always add at the end of the middleware chain -@value -struct NotFoundMiddleware(Middleware): - fn call(self, context: Context) -> HTTPResponse: - return NotFound("Not Found") - - - -### Helper functions to create HTTP responses -fn Success(body: String) -> HTTPResponse: - return Success(body, String("text/plain")) - -fn Success(body: String, content_type: String) -> HTTPResponse: - return HTTPResponse( - ResponseHeader(True, 200, String("Success").as_bytes(), content_type.as_bytes()), - body.as_bytes(), - ) - -fn NotFound(body: String) -> HTTPResponse: - return NotFound(body, String("text/plain")) - -fn NotFound(body: String, content_type: String) -> HTTPResponse: - return HTTPResponse( - ResponseHeader(True, 404, String("Not Found").as_bytes(), content_type.as_bytes()), - body.as_bytes(), - ) - -fn InternalServerError(body: String) -> HTTPResponse: - return InternalServerErrorResponse(body, String("text/plain")) - -fn InternalServerError(body: String, content_type: String) -> HTTPResponse: - return HTTPResponse( - ResponseHeader(True, 500, String("Internal Server Error").as_bytes(), content_type.as_bytes()), - body.as_bytes(), - ) - -fn Unauthorized(body: String) -> HTTPResponse: - return UnauthorizedResponse(body, String("text/plain")) - -fn Unauthorized(body: String, content_type: String) -> HTTPResponse: - var header = ResponseHeader(True, 401, String("Unauthorized").as_bytes(), content_type.as_bytes()) - header.headers["WWW-Authenticate"] = "Basic realm=\"Login Required\"" - - return HTTPResponse( - header, - body.as_bytes(), - ) diff --git a/lightbug_http/middleware/__init__.mojo b/lightbug_http/middleware/__init__.mojo new file mode 100644 index 00000000..b76f8671 --- /dev/null +++ b/lightbug_http/middleware/__init__.mojo @@ -0,0 +1,17 @@ +from lightbug_http.middleware.middleware import Context, Middleware, MiddlewareChain + +from lightbug_http.middleware.basicauth import BasicAuthMiddleware +from lightbug_http.middleware.compression import CompressionMiddleware +from lightbug_http.middleware.cors import CorsMiddleware +from lightbug_http.middleware.error import ErrorMiddleware +from lightbug_http.middleware.logger import LoggerMiddleware +from lightbug_http.middleware.notfound import NotFoundMiddleware +from lightbug_http.middleware.router import RouterMiddleware, HTTPHandler +from lightbug_http.middleware.static import StaticMiddleware + +# from lightbug_http.middleware.csrf import CsrfMiddleware +# from lightbug_http.middleware.session import SessionMiddleware +# from lightbug_http.middleware.websocket import WebSocketMiddleware +# from lightbug_http.middleware.cache import CacheMiddleware +# from lightbug_http.middleware.cookies import CookiesMiddleware +# from lightbug_http.middleware.session import SessionMiddleware diff --git a/lightbug_http/middleware/basicauth.mojo b/lightbug_http/middleware/basicauth.mojo new file mode 100644 index 00000000..8617c598 --- /dev/null +++ b/lightbug_http/middleware/basicauth.mojo @@ -0,0 +1,24 @@ +from lightbug_http.middleware.helpers import Unauthorized + +## BasicAuth middleware requires basic authentication to access the route. +@value +struct BasicAuthMiddleware(Middleware): + var next: Middleware + var username: String + var password: String + + fn set_next(self, next: Middleware): + self.next = next + + fn __init__(self, username: String, password: String): + self.username = username + self.password = password + + fn call(self, context: Context) -> HTTPResponse: + var request = context.request + var auth = request.headers["Authorization"] + if auth == f"Basic {username}:{password}": + context.params["username"] = username + return next.call(context) + else: + return Unauthorized("Requires Basic Authentication") diff --git a/lightbug_http/middleware/compression.mojo b/lightbug_http/middleware/compression.mojo new file mode 100644 index 00000000..677b10da --- /dev/null +++ b/lightbug_http/middleware/compression.mojo @@ -0,0 +1,17 @@ +## Compression middleware compresses the response body. +@value +struct CompressionMiddleware(Middleware): + var next: Middleware + + fn set_next(self, next: Middleware): + self.next = next + + fn call(self, context: Context) -> HTTPResponse: + var response = self.next.call(context) + response.body = self.compress(response.body) + return response + + fn compress(self, body: Bytes) -> Bytes: + #TODO: implement compression + return body + diff --git a/lightbug_http/middleware/cors.mojo b/lightbug_http/middleware/cors.mojo new file mode 100644 index 00000000..cc4d60b9 --- /dev/null +++ b/lightbug_http/middleware/cors.mojo @@ -0,0 +1,26 @@ +from lightbug_http.middleware.helpers import Unauthorized + +## CORS middleware adds the necessary headers to allow cross-origin requests. +@value +struct CorsMiddleware(Middleware): + var next: Middleware + var allow_origin: String + + fn set_next(self, next: Middleware): + self.next = next + + fn __init__(self, allow_origin: String): + self.allow_origin = allow_origin + + fn call(self, context: Context) -> HTTPResponse: + if context.request.header.method() == "OPTIONS": + var response = next.call(context) + response.headers["Access-Control-Allow-Origin"] = allow_origin + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" + return response + + if context.request.origin == allow_origin: + return self.next.call(context) + else: + return Unauthorized("CORS not allowed") diff --git a/lightbug_http/middleware/error.mojo b/lightbug_http/middleware/error.mojo new file mode 100644 index 00000000..adb28c38 --- /dev/null +++ b/lightbug_http/middleware/error.mojo @@ -0,0 +1,18 @@ +from lightbug_http.middleware.helpers import InternalServerError + +## Error handler will catch any exceptions thrown by the other +## middleware and return a 500 response. +## It should be the first middleware in the chain. +@value +struct ErrorMiddleware(Middleware): + var next: Middleware + + fn set_next(self, next: Middleware): + self.next = next + + fn call(inout self, context: Context) -> HTTPResponse: + try: + return self.next.call(context) + except e: + return InternalServerError(e) + diff --git a/lightbug_http/middleware/helpers.mojo b/lightbug_http/middleware/helpers.mojo new file mode 100644 index 00000000..0e174727 --- /dev/null +++ b/lightbug_http/middleware/helpers.mojo @@ -0,0 +1,39 @@ +### Helper functions to create HTTP responses +fn Success(body: String) -> HTTPResponse: + return Success(body, String("text/plain")) + +fn Success(body: String, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 200, String("Success").as_bytes(), content_type.as_bytes()), + body.as_bytes(), + ) + +fn NotFound(body: String) -> HTTPResponse: + return NotFound(body, String("text/plain")) + +fn NotFound(body: String, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 404, String("Not Found").as_bytes(), content_type.as_bytes()), + body.as_bytes(), + ) + +fn InternalServerError(body: String) -> HTTPResponse: + return InternalServerErrorResponse(body, String("text/plain")) + +fn InternalServerError(body: String, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 500, String("Internal Server Error").as_bytes(), content_type.as_bytes()), + body.as_bytes(), + ) + +fn Unauthorized(body: String) -> HTTPResponse: + return UnauthorizedResponse(body, String("text/plain")) + +fn Unauthorized(body: String, content_type: String) -> HTTPResponse: + var header = ResponseHeader(True, 401, String("Unauthorized").as_bytes(), content_type.as_bytes()) + header.headers["WWW-Authenticate"] = "Basic realm=\"Login Required\"" + + return HTTPResponse( + header, + body.as_bytes(), + ) diff --git a/lightbug_http/middleware/logger.mojo b/lightbug_http/middleware/logger.mojo new file mode 100644 index 00000000..4b689b41 --- /dev/null +++ b/lightbug_http/middleware/logger.mojo @@ -0,0 +1,12 @@ +## Logger middleware logs the request to the console. +@value +struct LoggerMiddleware(Middleware): + var next: Middleware + + fn set_next(self, next: Middleware): + self.next = next + + fn call(self, context: Context) -> HTTPResponse: + print(f"Request: {context.request}") + return self.next.call(context) + print(f"Response: {context.response}") diff --git a/lightbug_http/middleware/middleware.mojo b/lightbug_http/middleware/middleware.mojo new file mode 100644 index 00000000..fa8130f9 --- /dev/null +++ b/lightbug_http/middleware/middleware.mojo @@ -0,0 +1,43 @@ +from lightbug_http.service import HTTPService +from lightbug_http.http import HTTPRequest, HTTPResponse +from lightbug_http.middleware import * + +## Context is a container for the request and response data. +## It is passed to each middleware in the chain. +## It also contains a dictionary of parameters that can be shared between middleware. +@value +struct Context: + var request: HTTPRequest + var params: Dict[String, AnyType] + + fn __init__(inout self, request: HTTPRequest): + self.request = request + self.params = Dict[String, AnyType]() + + +## Middleware is an interface for processing HTTP requests. +## Each middleware in the chain can modify the request and response. +trait Middleware: + fn set_next(self, next: Middleware): + ... + fn call(self, context: Context) -> HTTPResponse: + ... + +## MiddlewareChain is a chain of middleware that processes the request. +## The chain is a linked list of middleware objects. +@value +struct MiddlewareChain(HTTPService): + var root: Middleware + + fn add(self, middleware: Middleware): + if self.root == nil: + self.root = middleware + else: + var current = self.root + while current.next != nil: + current = current.next + current.set_next(middleware) + + fn func(self, request: HTTPRequest) raises -> HTTPResponse: + var context = Context(request) + return self.root.call(context) diff --git a/lightbug_http/middleware/notfound.mojo b/lightbug_http/middleware/notfound.mojo new file mode 100644 index 00000000..43719c48 --- /dev/null +++ b/lightbug_http/middleware/notfound.mojo @@ -0,0 +1,7 @@ +from lightbug_http.middleware.helpers import NotFound + +## NotFound middleware returns a 404 response if no other middleware handles the request. It is a leaf node and always add at the end of the middleware chain +@value +struct NotFoundMiddleware(Middleware): + fn call(self, context: Context) -> HTTPResponse: + return NotFound("Not Found") diff --git a/lightbug_http/middleware/router.mojo b/lightbug_http/middleware/router.mojo new file mode 100644 index 00000000..fa19ce9c --- /dev/null +++ b/lightbug_http/middleware/router.mojo @@ -0,0 +1,31 @@ +## HTTPHandler is an interface for handling HTTP requests in the RouterMiddleware. +## It is a leaf node in the middleware chain. +trait HTTPHandler: + fn handle(self, context: Context) -> HTTPResponse: + ... + + +## Router middleware routes requests to different middleware based on the path. +@value +struct RouterMiddleware(Middleware): + var next: Middleware + var routes: Dict[String, HTTPHandler] + + fn __init__(inout self): + self.routes = Dict[String, HTTPHandler]() + + fn set_next(self, next: Middleware): + self.next = next + + fn add(self, method: String, route: String, handler: HTTPHandler): + self.routes[method + ":" + route] = handler + + fn call(self, context: Context) -> HTTPResponse: + # TODO: create a more advanced router + var method = context.request.header.method() + var route = context.request.uri().path() + var handler = self.routes.find(method + ":" + route) + if handler: + return handler.value().handle(context) + else: + return next.call(context) diff --git a/lightbug_http/middleware/static.mojo b/lightbug_http/middleware/static.mojo new file mode 100644 index 00000000..3634e388 --- /dev/null +++ b/lightbug_http/middleware/static.mojo @@ -0,0 +1,27 @@ +from lightbug_http.middleware.helpers import Success + +## Static middleware serves static files from a directory. +@value +struct StaticMiddleware(Middleware): + var next: Middleware + var path: String + + fn set_next(self, next: Middleware): + self.next = next + + fn __init__(self, path: String): + self.path = path + + fn call(self, context: Context) -> HTTPResponse: + var path = context.request.uri().path() + if path.endswith("/"): + path = path + "index.html" + + try: + var html: String + with open(file, "r") as f: + html = f.read() + + return Success(html, "text/html") + except e: + return self.next.call(context) From cc61676bfaa200bbeda2a22960b829573e010328 Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sun, 5 May 2024 08:55:00 -0700 Subject: [PATCH 08/17] fix: remove unused code --- lightbug_http/service.mojo | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lightbug_http/service.mojo b/lightbug_http/service.mojo index 5134752d..016dbe53 100644 --- a/lightbug_http/service.mojo +++ b/lightbug_http/service.mojo @@ -5,11 +5,6 @@ trait HTTPService: fn func(self, req: HTTPRequest) raises -> HTTPResponse: ... -@value -struct HelloWorld(HTTPService): - fn func(self, req: HTTPRequest) raises -> HTTPResponse: - return OK("Hello, World!".as_bytes(), "text/plain") - @value struct Printer(HTTPService): fn func(self, req: HTTPRequest) raises -> HTTPResponse: From 7b280079534c47948d6a7f3cb56a4474b1486f27 Mon Sep 17 00:00:00 2001 From: Val Date: Thu, 9 May 2024 20:16:11 +0200 Subject: [PATCH 09/17] resolve merge conflicts --- "lightbug.\360\237\224\245" | 36 ++++++++++++++++---------- lightbug_http/middleware/__init__.mojo | 1 + 2 files changed, 23 insertions(+), 14 deletions(-) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index a69472a5..92d6a82f 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -1,5 +1,7 @@ from lightbug_http import * from lightbug_http.middleware.helpers import Success +from sys import is_defined +from lightbug_http import * struct HelloWorld(HTTPHandler): fn handle(self, context: Context) -> HTTPResponse: @@ -7,19 +9,25 @@ struct HelloWorld(HTTPHandler): return Success("Hello, " + name + "!") fn main() raises: - var router = RouterMiddleware() - router.add("GET", "/hello", HelloWorld()) - - var middleware = MiddlewareChain() - middleware.add(CompressionMiddleware()) - middleware.add(ErrorMiddleware()) - middleware.add(LoggerMiddleware()) - middleware.add(CorsMiddleware(allows_origin = "*")) - middleware.add(BasicAuthMiddleware("admin", "password")) - middleware.add(StaticMiddleware("static")) - middleware.add(router) - middleware.add(NotFoundMiddleware()) + if not is_defined["TEST"](): + var router = RouterMiddleware() + router.add("GET", "/hello", HelloWorld()) - var server = SysServer() - server.listen_and_serve("0.0.0.0:8080", middleware) + var middleware = MiddlewareChain() + middleware.add(CompressionMiddleware()) + middleware.add(ErrorMiddleware()) + middleware.add(LoggerMiddleware()) + middleware.add(CorsMiddleware(allows_origin = "*")) + middleware.add(BasicAuthMiddleware("admin", "password")) + middleware.add(StaticMiddleware("static")) + middleware.add(router) + middleware.add(NotFoundMiddleware()) + var server = SysServer() + server.listen_and_serve("0.0.0.0:8080", middleware) + else: + try: + run_tests() + print("Test suite passed") + except e: + print("Test suite failed: " + e.__str__()) diff --git a/lightbug_http/middleware/__init__.mojo b/lightbug_http/middleware/__init__.mojo index b76f8671..a225d1e2 100644 --- a/lightbug_http/middleware/__init__.mojo +++ b/lightbug_http/middleware/__init__.mojo @@ -1,3 +1,4 @@ +from lightbug_http.middleware.helpers import Success from lightbug_http.middleware.middleware import Context, Middleware, MiddlewareChain from lightbug_http.middleware.basicauth import BasicAuthMiddleware From 2b21143c32cf977e7f94946c73d05e13337ad71b Mon Sep 17 00:00:00 2001 From: Val Date: Sat, 11 May 2024 22:03:04 +0200 Subject: [PATCH 10/17] fix trait parameter --- "lightbug.\360\237\224\245" | 1 + lightbug_http/middleware/helpers.mojo | 2 ++ lightbug_http/middleware/middleware.mojo | 6 +++--- lightbug_http/middleware/router.mojo | 8 ++++---- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index 92d6a82f..9d345a9b 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -3,6 +3,7 @@ from lightbug_http.middleware.helpers import Success from sys import is_defined from lightbug_http import * +@value struct HelloWorld(HTTPHandler): fn handle(self, context: Context) -> HTTPResponse: var name = context.params.get("username", "world") diff --git a/lightbug_http/middleware/helpers.mojo b/lightbug_http/middleware/helpers.mojo index 0e174727..c3332dfa 100644 --- a/lightbug_http/middleware/helpers.mojo +++ b/lightbug_http/middleware/helpers.mojo @@ -1,3 +1,5 @@ +from lightbug_http.http import HTTPRequest, HTTPResponse, ResponseHeader + ### Helper functions to create HTTP responses fn Success(body: String) -> HTTPResponse: return Success(body, String("text/plain")) diff --git a/lightbug_http/middleware/middleware.mojo b/lightbug_http/middleware/middleware.mojo index fa8130f9..41cac663 100644 --- a/lightbug_http/middleware/middleware.mojo +++ b/lightbug_http/middleware/middleware.mojo @@ -6,13 +6,13 @@ from lightbug_http.middleware import * ## It is passed to each middleware in the chain. ## It also contains a dictionary of parameters that can be shared between middleware. @value -struct Context: +struct Context[ParamType: CollectionElement]: var request: HTTPRequest - var params: Dict[String, AnyType] + var params: Dict[String, ParamType] fn __init__(inout self, request: HTTPRequest): self.request = request - self.params = Dict[String, AnyType]() + self.params = Dict[String, ParamType]() ## Middleware is an interface for processing HTTP requests. diff --git a/lightbug_http/middleware/router.mojo b/lightbug_http/middleware/router.mojo index fa19ce9c..8307c7b8 100644 --- a/lightbug_http/middleware/router.mojo +++ b/lightbug_http/middleware/router.mojo @@ -1,18 +1,18 @@ ## HTTPHandler is an interface for handling HTTP requests in the RouterMiddleware. ## It is a leaf node in the middleware chain. -trait HTTPHandler: +trait HTTPHandler(CollectionElement): fn handle(self, context: Context) -> HTTPResponse: ... ## Router middleware routes requests to different middleware based on the path. @value -struct RouterMiddleware(Middleware): +struct RouterMiddleware[HTTPHandlerType: HTTPHandler](Middleware): var next: Middleware - var routes: Dict[String, HTTPHandler] + var routes: Dict[String, HTTPHandlerType] fn __init__(inout self): - self.routes = Dict[String, HTTPHandler]() + self.routes = Dict[String, HTTPHandlerType]() fn set_next(self, next: Middleware): self.next = next From f3774950da855e44266e65f075f18110b3e25d3b Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sat, 4 May 2024 15:09:26 -0700 Subject: [PATCH 11/17] feat: support middleware --- lightbug_http/__init__.mojo | 1 + lightbug_http/middleware.mojo | 172 ++++++++++++++++++++++++++++++++++ static/index.html | 7 ++ 3 files changed, 180 insertions(+) create mode 100644 lightbug_http/middleware.mojo diff --git a/lightbug_http/__init__.mojo b/lightbug_http/__init__.mojo index e5c8ed44..a0253e3b 100644 --- a/lightbug_http/__init__.mojo +++ b/lightbug_http/__init__.mojo @@ -3,6 +3,7 @@ from lightbug_http.service import HTTPService, Welcome from lightbug_http.sys.server import SysServer from lightbug_http.tests.run import run_tests from lightbug_http.middleware import * +from lightbug_http.middleware import * trait DefaultConstructible: fn __init__(inout self) raises: diff --git a/lightbug_http/middleware.mojo b/lightbug_http/middleware.mojo new file mode 100644 index 00000000..fd10576f --- /dev/null +++ b/lightbug_http/middleware.mojo @@ -0,0 +1,172 @@ +from lightbug_http.http import HTTPRequest, HTTPResponse + +struct Context: + var request: Request + var params: Dict[String, AnyType] + + fn __init__(self, request: Request): + self.request = request + self.params = Dict[String, AnyType]() + +trait Middleware: + var next: Middleware + + fn call(self, context: Context) -> Response: + ... + +struct ErrorMiddleware(Middleware): + fn call(self, context: Context) -> Response: + try: + return next.call(context: context) + catch e: Exception: + return InternalServerError() + +struct LoggerMiddleware(Middleware): + fn call(self, context: Context) -> Response: + print("Request: \(context.request)") + return next.call(context: context) + +struct StaticMiddleware(Middleware): + var path: String + + fnt __init__(self, path: String): + self.path = path + + fn call(self, context: Context) -> Response: + if context.request.path == "/": + var file = File(path: path + "index.html") + else: + var file = File(path: path + context.request.path) + + if file.exists: + var html: String + with open(file, "r") as f: + html = f.read() + return OK(html.as_bytes(), "text/html") + else: + return next.call(context: context) + +struct CorsMiddleware(Middleware): + var allow_origin: String + + fn __init__(self, allow_origin: String): + self.allow_origin = allow_origin + + fn call(self, context: Context) -> Response: + if context.request.method == "OPTIONS": + var response = next.call(context: context) + response.headers["Access-Control-Allow-Origin"] = allow_origin + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" + return response + + if context.request.origin == allow_origin: + return next.call(context: context) + else: + return Unauthorized() + +struct CompressionMiddleware(Middleware): + fn call(self, context: Context) -> Response: + var response = next.call(context: context) + response.body = compress(response.body) + return response + + fn compress(self, body: Bytes) -> Bytes: + #TODO: implement compression + return body + + +struct RouterMiddleware(Middleware): + var routes: Dict[String, Middleware] + + fn __init__(self): + self.routes = Dict[String, Middleware]() + + fn add(self, method: String, route: String, middleware: Middleware): + routes[method + ":" + route] = middleware + + fn call(self, context: Context) -> Response: + # TODO: create a more advanced router + var method = context.request.method + var route = context.request.path + if middleware = routes[method + ":" + route]: + return middleware.call(context: context) + else: + return next.call(context: context) + +struct BasicAuthMiddleware(Middleware): + var username: String + var password: String + + fn __init__(self, username: String, password: String): + self.username = username + self.password = password + + fn call(self, context: Context) -> Response: + var request = context.request + var auth = request.headers["Authorization"] + if auth == "Basic \(username):\(password)": + context.params["username"] = username + return next.call(context: context) + else: + return Unauthorized() + +# always add at the end of the middleware chain +struct NotFoundMiddleware(Middleware): + fn call(self, context: Context) -> Response: + return NotFound() + +struct MiddlewareChain(HttpService): + var middlewares: Array[Middleware] + + fn __init__(self): + self.middlewares = Array[Middleware]() + + fn add(self, middleware: Middleware): + if middlewares.count == 0: + middlewares.append(middleware) + else: + var last = middlewares[middlewares.count - 1] + last.next = middleware + middlewares.append(middleware) + + fn func(self, request: Request) -> Response: + self.add(NotFoundMiddleware()) + var context = Context(request: request, response: response) + return middlewares[0].call(context: context) + +fn OK(body: Bytes) -> HTTPResponse: + return OK(body, String("text/plain")) + +fn OK(body: Bytes, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 200, String("OK").as_bytes(), content_type.as_bytes()), + body, + ) + +fn NotFound(body: Bytes) -> HTTPResponse: + return NotFoundResponse(body, String("text/plain")) + +fn NotFound(body: Bytes, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 404, String("Not Found").as_bytes(), content_type.as_bytes()), + body, + ) + +fn InternalServerError(body: Bytes) -> HTTPResponse: + return InternalServerErrorResponse(body, String("text/plain")) + +fn InternalServerError(body: Bytes, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 500, String("Internal Server Error").as_bytes(), content_type.as_bytes()), + body, + ) + +fn Unauthorized(body: Bytes) -> HTTPResponse: + return UnauthorizedResponse(body, String("text/plain")) + +fn Unauthorized(body: Bytes, content_type: String) -> HTTPResponse: + return HTTPResponse( + ResponseHeader(True, 401, String("Unauthorized").as_bytes(), content_type.as_bytes()), + body, + ) diff --git a/static/index.html b/static/index.html index 7f2d71e8..89ebf9af 100644 --- a/static/index.html +++ b/static/index.html @@ -56,8 +56,15 @@
Welcome to Lightbug!
A Mojo HTTP framework with wings
+<<<<<<< HEAD
To get started, edit lightbug.🔥
Lightbug Image +======= +
To get started, edit lightbug.iOS fire emoji
+ Lightbug Image +>>>>>>> 7ae8912 (feat: support middleware) \ No newline at end of file From 7c4365da92e335147862b43e7be75293dedee9a6 Mon Sep 17 00:00:00 2001 From: Val Date: Thu, 9 May 2024 20:16:11 +0200 Subject: [PATCH 12/17] resolve merge conflicts --- lightbug_http/__init__.mojo | 1 - 1 file changed, 1 deletion(-) diff --git a/lightbug_http/__init__.mojo b/lightbug_http/__init__.mojo index a0253e3b..e5c8ed44 100644 --- a/lightbug_http/__init__.mojo +++ b/lightbug_http/__init__.mojo @@ -3,7 +3,6 @@ from lightbug_http.service import HTTPService, Welcome from lightbug_http.sys.server import SysServer from lightbug_http.tests.run import run_tests from lightbug_http.middleware import * -from lightbug_http.middleware import * trait DefaultConstructible: fn __init__(inout self) raises: From 393bdfe40d2a4cd9892a580557b510b7d2e965e3 Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sun, 2 Jun 2024 10:32:32 -0700 Subject: [PATCH 13/17] fix: minor bugs --- lightbug_http/middleware/basicauth.mojo | 2 +- lightbug_http/middleware/cors.mojo | 2 +- lightbug_http/middleware/notfound.mojo | 5 +++++ lightbug_http/middleware/router.mojo | 2 +- lightbug_http/middleware/static.mojo | 4 ++-- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lightbug_http/middleware/basicauth.mojo b/lightbug_http/middleware/basicauth.mojo index 8617c598..287046eb 100644 --- a/lightbug_http/middleware/basicauth.mojo +++ b/lightbug_http/middleware/basicauth.mojo @@ -10,7 +10,7 @@ struct BasicAuthMiddleware(Middleware): fn set_next(self, next: Middleware): self.next = next - fn __init__(self, username: String, password: String): + fn __init__(inout self, username: String, password: String): self.username = username self.password = password diff --git a/lightbug_http/middleware/cors.mojo b/lightbug_http/middleware/cors.mojo index cc4d60b9..67021e79 100644 --- a/lightbug_http/middleware/cors.mojo +++ b/lightbug_http/middleware/cors.mojo @@ -9,7 +9,7 @@ struct CorsMiddleware(Middleware): fn set_next(self, next: Middleware): self.next = next - fn __init__(self, allow_origin: String): + fn __init__(inout self, allow_origin: String): self.allow_origin = allow_origin fn call(self, context: Context) -> HTTPResponse: diff --git a/lightbug_http/middleware/notfound.mojo b/lightbug_http/middleware/notfound.mojo index 43719c48..1f6bf9b4 100644 --- a/lightbug_http/middleware/notfound.mojo +++ b/lightbug_http/middleware/notfound.mojo @@ -3,5 +3,10 @@ from lightbug_http.middleware.helpers import NotFound ## NotFound middleware returns a 404 response if no other middleware handles the request. It is a leaf node and always add at the end of the middleware chain @value struct NotFoundMiddleware(Middleware): + var next: Middleware + + fn set_next(self, next: Middleware): + self.next = next + fn call(self, context: Context) -> HTTPResponse: return NotFound("Not Found") diff --git a/lightbug_http/middleware/router.mojo b/lightbug_http/middleware/router.mojo index 8307c7b8..5aa7566c 100644 --- a/lightbug_http/middleware/router.mojo +++ b/lightbug_http/middleware/router.mojo @@ -17,7 +17,7 @@ struct RouterMiddleware[HTTPHandlerType: HTTPHandler](Middleware): fn set_next(self, next: Middleware): self.next = next - fn add(self, method: String, route: String, handler: HTTPHandler): + fn add(self, method: String, route: String, handler: HTTPHandlerType): self.routes[method + ":" + route] = handler fn call(self, context: Context) -> HTTPResponse: diff --git a/lightbug_http/middleware/static.mojo b/lightbug_http/middleware/static.mojo index 3634e388..d738e579 100644 --- a/lightbug_http/middleware/static.mojo +++ b/lightbug_http/middleware/static.mojo @@ -9,7 +9,7 @@ struct StaticMiddleware(Middleware): fn set_next(self, next: Middleware): self.next = next - fn __init__(self, path: String): + fn __init__(inout self, path: String): self.path = path fn call(self, context: Context) -> HTTPResponse: @@ -19,7 +19,7 @@ struct StaticMiddleware(Middleware): try: var html: String - with open(file, "r") as f: + with open(path, "r") as f: html = f.read() return Success(html, "text/html") From 76aea0a4454cba42768b9757a3ad6dcba2237ad7 Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Fri, 7 Jun 2024 20:10:50 -0700 Subject: [PATCH 14/17] fix: other bugs --- "lightbug.\360\237\224\245" | 7 +++++-- lightbug_http/middleware/middleware.mojo | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index 9d345a9b..582e699a 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -6,8 +6,11 @@ from lightbug_http import * @value struct HelloWorld(HTTPHandler): fn handle(self, context: Context) -> HTTPResponse: - var name = context.params.get("username", "world") - return Success("Hello, " + name + "!") + var name = context.params.find("username") + if name: + return Success("Hello!") + else: + return Success("Hello, World!") fn main() raises: if not is_defined["TEST"](): diff --git a/lightbug_http/middleware/middleware.mojo b/lightbug_http/middleware/middleware.mojo index 41cac663..802787dc 100644 --- a/lightbug_http/middleware/middleware.mojo +++ b/lightbug_http/middleware/middleware.mojo @@ -8,11 +8,11 @@ from lightbug_http.middleware import * @value struct Context[ParamType: CollectionElement]: var request: HTTPRequest - var params: Dict[String, ParamType] + var params: Dict[String, String] fn __init__(inout self, request: HTTPRequest): self.request = request - self.params = Dict[String, ParamType]() + self.params = Dict[String, String]() ## Middleware is an interface for processing HTTP requests. From 476bcbc6527d5f6fb365582b3a17ea9503533f3c Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Fri, 7 Jun 2024 20:37:19 -0700 Subject: [PATCH 15/17] fix: bunch of TODO's added --- lightbug_http/middleware/compression.mojo | 4 +++- lightbug_http/middleware/cors.mojo | 18 +++++++++++------- lightbug_http/middleware/helpers.mojo | 7 ++++--- lightbug_http/middleware/logger.mojo | 9 ++++++--- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lightbug_http/middleware/compression.mojo b/lightbug_http/middleware/compression.mojo index 677b10da..acc800b0 100644 --- a/lightbug_http/middleware/compression.mojo +++ b/lightbug_http/middleware/compression.mojo @@ -11,7 +11,9 @@ struct CompressionMiddleware(Middleware): response.body = self.compress(response.body) return response - fn compress(self, body: Bytes) -> Bytes: + # TODO: implement compression + # Should return Bytes instead of String but we don't have Bytes type yet + fn compress(self, body: String) -> String: #TODO: implement compression return body diff --git a/lightbug_http/middleware/cors.mojo b/lightbug_http/middleware/cors.mojo index 67021e79..e933e4bc 100644 --- a/lightbug_http/middleware/cors.mojo +++ b/lightbug_http/middleware/cors.mojo @@ -1,4 +1,5 @@ from lightbug_http.middleware.helpers import Unauthorized +from lightbug_http.io.bytes import bytes, bytes_equal ## CORS middleware adds the necessary headers to allow cross-origin requests. @value @@ -13,14 +14,17 @@ struct CorsMiddleware(Middleware): self.allow_origin = allow_origin fn call(self, context: Context) -> HTTPResponse: - if context.request.header.method() == "OPTIONS": - var response = next.call(context) - response.headers["Access-Control-Allow-Origin"] = allow_origin + if bytes_equal(context.request.header.method(), bytes("OPTIONS")): + var response = self.next.call(context) + response.headers["Access-Control-Allow-Origin"] = self.allow_origin response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" return response - if context.request.origin == allow_origin: - return self.next.call(context) - else: - return Unauthorized("CORS not allowed") + # TODO: implement headers + # if context.request.headers["origin"] == self.allow_origin: + # return self.next.call(context) + # else: + # return Unauthorized("CORS not allowed") + + return self.next.call(context) diff --git a/lightbug_http/middleware/helpers.mojo b/lightbug_http/middleware/helpers.mojo index c3332dfa..6cba3da2 100644 --- a/lightbug_http/middleware/helpers.mojo +++ b/lightbug_http/middleware/helpers.mojo @@ -20,7 +20,7 @@ fn NotFound(body: String, content_type: String) -> HTTPResponse: ) fn InternalServerError(body: String) -> HTTPResponse: - return InternalServerErrorResponse(body, String("text/plain")) + return InternalServerError(body, String("text/plain")) fn InternalServerError(body: String, content_type: String) -> HTTPResponse: return HTTPResponse( @@ -29,11 +29,12 @@ fn InternalServerError(body: String, content_type: String) -> HTTPResponse: ) fn Unauthorized(body: String) -> HTTPResponse: - return UnauthorizedResponse(body, String("text/plain")) + return Unauthorized(body, String("text/plain")) fn Unauthorized(body: String, content_type: String) -> HTTPResponse: var header = ResponseHeader(True, 401, String("Unauthorized").as_bytes(), content_type.as_bytes()) - header.headers["WWW-Authenticate"] = "Basic realm=\"Login Required\"" + # TODO: currently no way to set headers or cookies + # header.headers["WWW-Authenticate"] = "Basic realm=\"Login Required\"" return HTTPResponse( header, diff --git a/lightbug_http/middleware/logger.mojo b/lightbug_http/middleware/logger.mojo index 4b689b41..5d8acd51 100644 --- a/lightbug_http/middleware/logger.mojo +++ b/lightbug_http/middleware/logger.mojo @@ -7,6 +7,9 @@ struct LoggerMiddleware(Middleware): self.next = next fn call(self, context: Context) -> HTTPResponse: - print(f"Request: {context.request}") - return self.next.call(context) - print(f"Response: {context.response}") + var request = context.request + #TODO: request is not printable + # print("Request: ", request) + var response = self.next.call(context) + print("Response:", response) + return response From 4748d88431d608d906eab71bee2f63764a5e78fd Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Fri, 7 Jun 2024 21:02:03 -0700 Subject: [PATCH 16/17] fix: simplify by removing set_next --- lightbug_http/middleware/basicauth.mojo | 9 ++++----- lightbug_http/middleware/compression.mojo | 13 ++++++------- lightbug_http/middleware/cors.mojo | 3 --- lightbug_http/middleware/error.mojo | 3 --- lightbug_http/middleware/logger.mojo | 3 --- lightbug_http/middleware/middleware.mojo | 4 +--- lightbug_http/middleware/notfound.mojo | 3 --- lightbug_http/middleware/router.mojo | 3 --- lightbug_http/middleware/static.mojo | 3 --- 9 files changed, 11 insertions(+), 33 deletions(-) diff --git a/lightbug_http/middleware/basicauth.mojo b/lightbug_http/middleware/basicauth.mojo index 287046eb..0cb2ec1e 100644 --- a/lightbug_http/middleware/basicauth.mojo +++ b/lightbug_http/middleware/basicauth.mojo @@ -7,17 +7,16 @@ struct BasicAuthMiddleware(Middleware): var username: String var password: String - fn set_next(self, next: Middleware): - self.next = next - fn __init__(inout self, username: String, password: String): self.username = username self.password = password fn call(self, context: Context) -> HTTPResponse: var request = context.request - var auth = request.headers["Authorization"] - if auth == f"Basic {username}:{password}": + #TODO: request object should have a way to get headers + # var auth = request.headers["Authorization"] + var auth = "Basic " + self.username + ":" + self.password + if auth == "Basic " + self.username + ":" + self.password: context.params["username"] = username return next.call(context) else: diff --git a/lightbug_http/middleware/compression.mojo b/lightbug_http/middleware/compression.mojo index acc800b0..40db0d67 100644 --- a/lightbug_http/middleware/compression.mojo +++ b/lightbug_http/middleware/compression.mojo @@ -1,19 +1,18 @@ -## Compression middleware compresses the response body. +from lightbug_http.io.bytes import bytes + +alias Bytes = List[Int8] + @value struct CompressionMiddleware(Middleware): var next: Middleware - fn set_next(self, next: Middleware): - self.next = next - fn call(self, context: Context) -> HTTPResponse: var response = self.next.call(context) response.body = self.compress(response.body) return response # TODO: implement compression - # Should return Bytes instead of String but we don't have Bytes type yet - fn compress(self, body: String) -> String: + fn compress(self, body: String) -> Bytes: #TODO: implement compression - return body + return bytes(body) diff --git a/lightbug_http/middleware/cors.mojo b/lightbug_http/middleware/cors.mojo index e933e4bc..da3c576b 100644 --- a/lightbug_http/middleware/cors.mojo +++ b/lightbug_http/middleware/cors.mojo @@ -7,9 +7,6 @@ struct CorsMiddleware(Middleware): var next: Middleware var allow_origin: String - fn set_next(self, next: Middleware): - self.next = next - fn __init__(inout self, allow_origin: String): self.allow_origin = allow_origin diff --git a/lightbug_http/middleware/error.mojo b/lightbug_http/middleware/error.mojo index adb28c38..4a9dede2 100644 --- a/lightbug_http/middleware/error.mojo +++ b/lightbug_http/middleware/error.mojo @@ -7,9 +7,6 @@ from lightbug_http.middleware.helpers import InternalServerError struct ErrorMiddleware(Middleware): var next: Middleware - fn set_next(self, next: Middleware): - self.next = next - fn call(inout self, context: Context) -> HTTPResponse: try: return self.next.call(context) diff --git a/lightbug_http/middleware/logger.mojo b/lightbug_http/middleware/logger.mojo index 5d8acd51..e5afc6c7 100644 --- a/lightbug_http/middleware/logger.mojo +++ b/lightbug_http/middleware/logger.mojo @@ -3,9 +3,6 @@ struct LoggerMiddleware(Middleware): var next: Middleware - fn set_next(self, next: Middleware): - self.next = next - fn call(self, context: Context) -> HTTPResponse: var request = context.request #TODO: request is not printable diff --git a/lightbug_http/middleware/middleware.mojo b/lightbug_http/middleware/middleware.mojo index 802787dc..11f67539 100644 --- a/lightbug_http/middleware/middleware.mojo +++ b/lightbug_http/middleware/middleware.mojo @@ -18,8 +18,6 @@ struct Context[ParamType: CollectionElement]: ## Middleware is an interface for processing HTTP requests. ## Each middleware in the chain can modify the request and response. trait Middleware: - fn set_next(self, next: Middleware): - ... fn call(self, context: Context) -> HTTPResponse: ... @@ -36,7 +34,7 @@ struct MiddlewareChain(HTTPService): var current = self.root while current.next != nil: current = current.next - current.set_next(middleware) + current.next = middleware fn func(self, request: HTTPRequest) raises -> HTTPResponse: var context = Context(request) diff --git a/lightbug_http/middleware/notfound.mojo b/lightbug_http/middleware/notfound.mojo index 1f6bf9b4..48e8c538 100644 --- a/lightbug_http/middleware/notfound.mojo +++ b/lightbug_http/middleware/notfound.mojo @@ -5,8 +5,5 @@ from lightbug_http.middleware.helpers import NotFound struct NotFoundMiddleware(Middleware): var next: Middleware - fn set_next(self, next: Middleware): - self.next = next - fn call(self, context: Context) -> HTTPResponse: return NotFound("Not Found") diff --git a/lightbug_http/middleware/router.mojo b/lightbug_http/middleware/router.mojo index 5aa7566c..53fc92ac 100644 --- a/lightbug_http/middleware/router.mojo +++ b/lightbug_http/middleware/router.mojo @@ -14,9 +14,6 @@ struct RouterMiddleware[HTTPHandlerType: HTTPHandler](Middleware): fn __init__(inout self): self.routes = Dict[String, HTTPHandlerType]() - fn set_next(self, next: Middleware): - self.next = next - fn add(self, method: String, route: String, handler: HTTPHandlerType): self.routes[method + ":" + route] = handler diff --git a/lightbug_http/middleware/static.mojo b/lightbug_http/middleware/static.mojo index d738e579..4554223e 100644 --- a/lightbug_http/middleware/static.mojo +++ b/lightbug_http/middleware/static.mojo @@ -6,9 +6,6 @@ struct StaticMiddleware(Middleware): var next: Middleware var path: String - fn set_next(self, next: Middleware): - self.next = next - fn __init__(inout self, path: String): self.path = path From 5f6dc978797c65753ae2f1effc22c33cd0559a55 Mon Sep 17 00:00:00 2001 From: Dru Jensen Date: Sat, 22 Jun 2024 13:13:12 -0700 Subject: [PATCH 17/17] fix: TODO for headers --- lightbug_http/middleware/compression.mojo | 2 +- lightbug_http/middleware/cors.mojo | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lightbug_http/middleware/compression.mojo b/lightbug_http/middleware/compression.mojo index 40db0d67..ddbc016d 100644 --- a/lightbug_http/middleware/compression.mojo +++ b/lightbug_http/middleware/compression.mojo @@ -8,7 +8,7 @@ struct CompressionMiddleware(Middleware): fn call(self, context: Context) -> HTTPResponse: var response = self.next.call(context) - response.body = self.compress(response.body) + response.body_raw = self.compress(response.body_raw) return response # TODO: implement compression diff --git a/lightbug_http/middleware/cors.mojo b/lightbug_http/middleware/cors.mojo index da3c576b..43e2fd03 100644 --- a/lightbug_http/middleware/cors.mojo +++ b/lightbug_http/middleware/cors.mojo @@ -13,9 +13,10 @@ struct CorsMiddleware(Middleware): fn call(self, context: Context) -> HTTPResponse: if bytes_equal(context.request.header.method(), bytes("OPTIONS")): var response = self.next.call(context) - response.headers["Access-Control-Allow-Origin"] = self.allow_origin - response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" - response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" + # TODO: implement headers + # response.headers["Access-Control-Allow-Origin"] = self.allow_origin + # response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" + # response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" return response # TODO: implement headers