Billy Barrow il y a 5 jours
Parent
commit
803ec1985b
10 fichiers modifiés avec 130 ajouts et 77 suppressions
  1. 7 7
      examples/SimpleApi.vala
  2. 1 0
      meson.build
  3. 37 20
      src/Context.vala
  4. 14 0
      src/Headers.vala
  5. 23 0
      src/HttpValues.vala
  6. 8 0
      src/RequestHandler.vala
  7. 6 11
      src/Router.vala
  8. 21 37
      src/Server.vala
  9. 5 2
      src/meson.build
  10. 8 0
      vapi/libmicrohttpd.vapi

+ 7 - 7
examples/SimpleApi.vala

@@ -1,17 +1,17 @@
 using Astralis;
 
 void main() {
-    var server = new Server(8080);
+    var router = new Router();
+    var server = new Server(8080, router);
     
-    server.map_get("/hello", (context) => {
+    router.map_get("/hello", (context) => {
         print("Handling /hello\n");
-        context.response.body = "Hello from Astralis!";
-        context.response.content_type = "text/plain";
+        context.respond_string(StatusCode.OK, "Hello from Astralis!");
     });
 
-    server.map_get("/json", (context) => {
-        context.response.body = "{ \"message\": \"Hello JSON\" }";
-        context.response.content_type = "application/json";
+    router.map_get("/json", (context) => {
+        print("Handling /json\n");
+        context.respond_string(StatusCode.OK, "{ \"message\": \"Hello JSON\" }", "application/json");
     });
 
     server.run();

+ 1 - 0
meson.build

@@ -5,6 +5,7 @@ project('astralis', ['c', 'vala'],
 # Dependencies
 glib_dep = dependency('glib-2.0')
 gobject_dep = dependency('gobject-2.0')
+gio_dep = dependency('gio-2.0')
 mhd_dep = dependency('libmicrohttpd')
 invercargill_dep = dependency('invercargill-1')
 

+ 37 - 20
src/Context.vala

@@ -1,40 +1,57 @@
 using MHD;
+using Invercargill;
 
 namespace Astralis {
 
-    public class HttpRequest : Object {
+    public class Request : Object {
         public string url { get; private set; }
         public string method { get; private set; }
         public string version { get; private set; }
-        
-        // In a real implementation we would have headers, query params etc map
-        // but for now we keep it simple to just wrappers.
-        private Connection connection;
-
-        public HttpRequest(Connection connection, string url, string method, string version) {
-            this.connection = connection;
+    
+        public Request(string url, string method, string version) {
             this.url = url;
             this.method = method;
             this.version = version;
         }
     }
 
-    public class HttpResponse : Object {
-        public uint status_code { get; set; default = 200; }
-        public string content_type { get; set; default = "text/plain"; }
-        public string body { get; set; default = ""; }
+    public class HttpContext : Object {
+        public Request request { get; private set; }
+        private Connection connection;
 
-        public HttpResponse() {
+        internal HttpContext(Connection connection, Request request) {
+            this.request = request;
+            this.connection = connection;
         }
-    }
 
-    public class HttpContext : Object {
-        public HttpRequest request { get; private set; }
-        public HttpResponse response { get; private set; }
+        public void respond(StatusCode status, Enumerable<HttpHeader> headers, uint8[] buffer) {
+            var mhd_response = new Response.from_buffer(
+                buffer.length, 
+                buffer, 
+                ResponseMemoryMode.RESPMEM_MUST_COPY
+            );
+            add_response_headers(mhd_response, headers);
+            queue_response(status, mhd_response);            
+        }
 
-        public HttpContext(HttpRequest request, HttpResponse response) {
-            this.request = request;
-            this.response = response;
+        public void respond_string(StatusCode status, string response, string content_type = "text/plain") {
+            var headers = Iterate.these<HttpHeader>(new HttpHeader("Content-Type", content_type));
+            respond(status, headers, response.data);     
+        }
+
+
+        internal static void add_response_headers(Response response, Enumerable<HttpHeader> headers) {
+            foreach (var header in headers) {
+                response.add_header(header.header, header.value);
+            }
+        }
+
+        internal void queue_response(StatusCode status, Response response) {
+            var res = MHD.queue_response(connection, status, response);
+            if(res != MHD.Result.YES) {
+                printerr("Astralis Internal Error: Unable to queue response\n");
+            }
+            MHD.resume_connection(connection);
         }
     }
 }

+ 14 - 0
src/Headers.vala

@@ -0,0 +1,14 @@
+using Invercargill;
+namespace Astralis {
+
+    public class HttpHeader {
+        public string header { get; private set; }
+        public string value { get; private set; }
+        
+        public HttpHeader(string header, string value) {
+            this.header = header;
+            this.value = value;
+        }
+    }
+
+}

+ 23 - 0
src/HttpValues.vala

@@ -0,0 +1,23 @@
+
+namespace Astralis {
+
+    public enum Method {
+        GET,
+        HEAD,
+        OPTIONS,
+        TRACE,
+        PUT,
+        DELETE,
+        POST,
+        PATCH,
+        CONNECT
+    }
+
+    public enum StatusCode {
+        OK = 200,
+        BAD_REQUEST = 400,
+        NOT_FOUND = 404,
+        INTERNAL_SERVER_ERROR = 500
+        // TODO More
+    }
+}

+ 8 - 0
src/RequestHandler.vala

@@ -0,0 +1,8 @@
+
+namespace Astralis {
+
+    public interface HttpHandler : Object {
+        public abstract async void handle(HttpContext context) throws Error;
+    }
+
+}

+ 6 - 11
src/Router.vala

@@ -12,7 +12,7 @@ namespace Astralis {
         }
     }
 
-    public class Router : Object {
+    public class Router : HttpHandler, Object {
         private Dictionary<string, RouteEntry> routes = new Dictionary<string, RouteEntry>();
 
         public void map_get(string path, owned RouteHandler handler) {
@@ -21,17 +21,12 @@ namespace Astralis {
             routes.set(path, new RouteEntry((owned) handler));
         }
 
-        public bool handle(HttpContext context) {
-            if (routes.has(context.request.url)) {
-                try {
-                    routes.get(context.request.url).handler(context);
-                    return true;
-                } catch (Invercargill.IndexError e) {
-                    // This should not happen since we checked has() first
-                    return false;
-                }
+        public async void handle(HttpContext context) {
+            try {
+                routes.get(context.request.url).handler(context);
+            } catch (Invercargill.IndexError e) {
+                context.respond_string(StatusCode.NOT_FOUND, "Not Found");
             }
-            return false;
         }
     }
 }

+ 21 - 37
src/Server.vala

@@ -4,16 +4,12 @@ namespace Astralis {
 
     public class Server : Object {
         private Daemon daemon;
-        private Router router;
+        private HttpHandler handler;
         private int port;
 
-        public Server(int port) {
+        public Server(int port, HttpHandler handler) {
             this.port = port;
-            this.router = new Router();
-        }
-
-        public void map_get(string path, owned RouteHandler handler) {
-            router.map_get(path, (owned) handler);
+            this.handler = handler;
         }
 
         private int access_handler (Connection connection, string? url, string? method, string? version, string? upload_data, size_t* upload_data_size, void** con_cls) {
@@ -32,42 +28,25 @@ namespace Astralis {
                 return Result.YES;
             }
 
-            var request = new HttpRequest(connection, url ?? "", method ?? "", version ?? "");
-            var response = new HttpResponse();
-            var context = new HttpContext(request, response);
+            var request = new Request(url ?? "", method ?? "", version ?? "");
+            var context = new HttpContext(connection, request);
+            MHD.suspend_connection(connection);
 
-            bool handled = router.handle(context);
-            
-            if (!handled) {
-                response.status_code = 404;
-                response.body = "Not Found";
-            }
+            handler.handle.begin(context, (obj, res) => {
+                try {
+                    handler.handle.end(res);
+                }
+                catch(Error e) {
+                    handle_error(e, context);
+                }
+            });
 
-            // Create MHD response
-            var mhd_response = new Response.from_buffer(
-                response.body.length, 
-                response.body.data, 
-                ResponseMemoryMode.RESPMEM_MUST_COPY
-            );
-            
-            mhd_response.add_header("Content-Type", response.content_type);
-            
-            int ret = MHD.queue_response(connection, response.status_code, mhd_response);
-            
-            // In C we'd destroy response here, but Vala handles memory management via the binding's free_function hopefully, 
-            // OR we'd need to be careful. The VAPI says free_function = MHD_destroy_response, so Vala will call it when the object goes out of scope.
-            // However, MHD_queue_response usually increments refcount or copies? 
-            // Actually MHD_create_response.. returns a response with refcount 1.
-            // MHD_queue_response increments it.
-            // So if our wrapper object dies, it calls destroy, which decrements.
-            // That should be correct.
-            
-            return ret;
+            return Result.YES;
         }
 
         public void run() {
             daemon = Daemon.start(
-                MHD.USE_SELECT_INTERNALLY | MHD.USE_DEBUG, 
+                MHD.USE_SELECT_INTERNALLY | MHD.ALLOW_SUSPEND_RESUME | MHD.USE_DEBUG, 
                 (uint16) this.port, 
                 null, 
                 (connection, url, method, version, upload_data, upload_data_size, con_cls) => {
@@ -84,5 +63,10 @@ namespace Astralis {
             print(@"Server running on port $port\nPress Ctrl+C to stop...\n");
             new MainLoop().run();
         }
+
+        private void handle_error(Error error, HttpContext context) {
+            printerr(@"Astralis Internal Server Error: Unhandled Error: $(error.message)\n");
+            context.respond_string(StatusCode.INTERNAL_SERVER_ERROR, "Internal Server Error");
+        }
     }
 }

+ 5 - 2
src/meson.build

@@ -1,12 +1,15 @@
 sources = files(
     'Context.vala',
     'Router.vala',
-    'Server.vala'
+    'Server.vala',
+    'HttpValues.vala',
+    'RequestHandler.vala',
+    'Headers.vala',
 )
 
 libastralis = shared_library('astralis',
     sources,
-    dependencies: [glib_dep, gobject_dep, mhd_dep, invercargill_dep],
+    dependencies: [glib_dep, gobject_dep, mhd_dep, gio_dep, invercargill_dep],
     install: true
 )
 

+ 8 - 0
vapi/libmicrohttpd.vapi

@@ -33,6 +33,8 @@ namespace MHD {
     public const uint USE_THREAD_PER_CONNECTION;
     [CCode (cname = "MHD_USE_DEBUG")]
     public const uint USE_DEBUG;
+    [CCode (cname = "MHD_ALLOW_SUSPEND_RESUME")]
+    public const uint ALLOW_SUSPEND_RESUME;
 
     [CCode (cname = "MHD_OPTION_END")]
     public const int OPTION_END;
@@ -79,6 +81,12 @@ namespace MHD {
     [CCode (cname = "MHD_queue_response")]
     public int queue_response (Connection connection, uint status_code, Response response);
 
+    [CCode (cname = "MHD_suspend_connection")]
+    public int suspend_connection (Connection connection);
+
+    [CCode (cname = "MHD_resume_connection")]
+    public int resume_connection (Connection connection);
+
     [CCode (cname = "MHD_lookup_connection_value")]
     public unowned string? lookup_connection_value (Connection connection, ValueKind kind, string key);
 }