libevent: rename `bind_to` to the more precise `bind_tcp`
[nit.git] / lib / nitcorn / reactor.nit
index 1cbb54a..def3699 100644 (file)
@@ -2,6 +2,7 @@
 #
 # Copyright 2013 Jean-Philippe Caissy <jpcaissy@piji.ca>
 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+# Copyright 2014 Alexandre Terrasa <alexandre@moz-code.org>
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 module reactor
 
 import more_collections
-import libevent
 
-import server_config
+import vararg_routes
 import http_request
+import http_request_buffer
 import http_response
 
 # A server handling a single connection
 class HttpServer
-       super Connection
+       super HTTPConnection
 
        # The associated `HttpFactory`
        var factory: HttpFactory
 
-       init(buf_ev: NativeBufferEvent, factory: HttpFactory) do self.factory = factory
-
        private var parser = new HttpRequestParser is lazy
 
-       redef fun read_callback(str)
-       do
-               # TODO support bigger inputs (such as big forms and file upload)
+       # Human readable address of the remote client
+       var remote_address: String
 
+       redef fun read_http_request(str)
+       do
                var request_object = parser.parse_http_request(str.to_s)
-
                if request_object != null then delegate_answer request_object
        end
 
@@ -69,31 +68,56 @@ class HttpServer
                if virtual_host != null then
                        var route = virtual_host.routes[request.uri]
                        if route != null then
+                               # include uri parameters in request
+                               request.uri_params = route.parse_params(request.uri)
+
                                var handler = route.handler
-                               var root = route.path
+                               var root = route.resolve_path(request)
                                var turi
                                if root != null then
                                        turi = ("/" + request.uri.substring_from(root.length)).simplify_path
                                else turi = request.uri
-                               response = handler.answer(request, turi)
-                       else response = new HttpResponse(405)
-               else response = new HttpResponse(405)
 
-               # Send back a response
-               write response.to_s
+                               # Delegate the responsibility to respond to the `Action`
+                               handler.prepare_respond_and_close(request, turi, self)
+                               return
+                       else response = new HttpResponse(404)
+               else response = new HttpResponse(404)
+
+               respond response
                close
        end
+
+       # Send back `response` to the client
+       fun respond(response: HttpResponse)
+       do
+               response.render.write_to(self)
+               for path in response.files do write_file path
+       end
 end
 
 redef abstract class Action
-       # Handle a request with the relative URI `truncated_uri`
+       # Prepare a `HttpResponse` destined to the client in response to the `request`
        #
-       # `request` is fully formed request object and has a reference to the session
-       # if one preexists.
+       # `request` is fully formed request object with a reference to the session
+       # if one already exists.
        #
        # `truncated_uri` is the ending of the full request URI, truncated from the route
        # leading to this `Action`.
        fun answer(request: HttpRequest, truncated_uri: String): HttpResponse is abstract
+
+       # Full  to a `request` with sending the response and closing of the `http_server`
+       #
+       # Must users only need to implement `answer`, this method is for advanced use only.
+       # It can be used to delay an answer until an event is raised or work is done on a different thread.
+       #
+       # By default this method calls `answer`, relays the response to `http_server.respond` and closes `http_server`.
+       protected fun prepare_respond_and_close(request: HttpRequest, truncated_uri: String, http_server: HttpServer)
+       do
+               var response = answer(request, truncated_uri)
+               http_server.respond response
+               http_server.close
+       end
 end
 
 # Factory to create `HttpServer` instances, and hold the libevent base handler
@@ -105,24 +129,32 @@ class HttpFactory
        # It should be populated after this object has instanciated
        var config = new ServerConfig.with_factory(self)
 
-       # Instanciate a server and libvent
+       # Instantiate a server and libvent
        #
        # You can use this to create the first `HttpFactory`, which is the most common.
        init and_libevent do init(new NativeEventBase)
 
-       redef fun spawn_connection(buf_ev) do return new HttpServer(buf_ev, self)
+       redef fun spawn_connection(buf_ev, address) do return new HttpServer(buf_ev, self, address)
 
-       # Launch the main loop of this server
+       # Execute the main listening loop to accept connections
+       #
+       # After the loop ends, the underlying resources are freed.
+       #
+       # When the environment variable `NIT_TESTING` is set to `true`,
+       # the loop is not executed but the resources are still freed.
        fun run
        do
-               event_base.dispatch
-               event_base.destroy
+               if "NIT_TESTING".environ != "true" then
+                       event_base.dispatch
+               end
+
+               event_base.free
        end
 end
 
 redef class ServerConfig
        # Handle to retreive the `HttpFactory` on config change
-       private var factory: HttpFactory
+       private var factory: HttpFactory is noinit
 
        private init with_factory(factory: HttpFactory) do self.factory = factory
 end
@@ -144,13 +176,14 @@ redef class Sys
 
                var listener = listeners[name, port]
                if listener == null then
-                       listener = factory.bind_to(name, port)
+                       listener = factory.bind_tcp(name, port)
                        if listener != null then
                                sys.listeners[name, port] = listener
                                listeners_count[name, port] = 1
                        end
                else
-                       listeners_count[name, port] += 1
+                       var value = listeners_count[name, port].as(not null)
+                       listeners_count[name, port] = value + 1
                end
 
                interfac.registered = true
@@ -168,7 +201,15 @@ redef class Interfaces
        redef fun add(e)
        do
                super
-               if vh.server_config != null then sys.listen_on(e, vh.server_config.factory)
+               var config = virtual_host.server_config
+               if config != null then register_and_listen(e, config)
+       end
+
+       # Indirection to `listen_on` and check if this targets all addresses
+       private fun register_and_listen(e: Interface, config: ServerConfig)
+       do
+               listen_on(e, config.factory)
+               if e.name == "0.0.0.0" or e.name == "::0" then config.default_virtual_host = virtual_host
        end
 
        # TODO remove
@@ -178,7 +219,7 @@ redef class VirtualHosts
        redef fun add(e)
        do
                super
-               for i in e.interfaces do sys.listen_on(i, config.factory)
+               for i in e.interfaces do e.interfaces.register_and_listen(i, config)
        end
 
        # TODO remove