85a2068dfb442ff80947bf66369f55e17f7451a0
[nit.git] / lib / nitcorn / reactor.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2013 Jean-Philippe Caissy <jpcaissy@piji.ca>
4 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
5 # Copyright 2014 Alexandre Terrasa <alexandre@moz-code.org>
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
10 #
11 # http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18
19 # Core of the `nitcorn` project, provides `HttpFactory` and `Action`
20 module reactor
21
22 import more_collections
23
24 import vararg_routes
25 import http_request
26 import http_request_buffer
27 import http_response
28
29 # A server handling a single connection
30 class HttpServer
31 super HTTPConnection
32
33 # The associated `HttpFactory`
34 var factory: HttpFactory
35
36 private var parser = new HttpRequestParser is lazy
37
38 # Human readable address of the remote client
39 var remote_address: String
40
41 redef fun read_http_request(str)
42 do
43 var request_object = parser.parse_http_request(str.to_s)
44 if request_object != null then delegate_answer request_object
45 end
46
47 # Answer to a request
48 fun delegate_answer(request: HttpRequest)
49 do
50 # Find target virtual host
51 var virtual_host = null
52 if request.header.keys.has("Host") then
53 var host = request.header["Host"]
54 if host.index_of(':') == -1 then host += ":80"
55 for vh in factory.config.virtual_hosts do
56 for i in vh.interfaces do if i.to_s == host then
57 virtual_host = vh
58 break label
59 end
60 end label
61 end
62
63 # Use default virtual host if none already responded
64 if virtual_host == null then virtual_host = factory.config.default_virtual_host
65
66 # Get a response from the virtual host
67 var response
68 if virtual_host != null then
69 var route = virtual_host.routes[request.uri]
70 if route != null then
71 # include uri parameters in request
72 request.uri_params = route.parse_params(request.uri)
73
74 var handler = route.handler
75 var root = route.resolve_path(request)
76 var turi
77 if root != null then
78 turi = ("/" + request.uri.substring_from(root.length)).simplify_path
79 else turi = request.uri
80
81 # Delegate the responsibility to respond to the `Action`
82 handler.prepare_respond_and_close(request, turi, self)
83 return
84 else response = new HttpResponse(404)
85 else response = new HttpResponse(404)
86
87 respond response
88 close
89 end
90
91 # Send back `response` to the client
92 fun respond(response: HttpResponse)
93 do
94 response.render.write_to(self)
95 for path in response.files do write_file path
96 end
97 end
98
99 redef abstract class Action
100 # Prepare a `HttpResponse` destined to the client in response to the `request`
101 #
102 # `request` is fully formed request object with a reference to the session
103 # if one already exists.
104 #
105 # `truncated_uri` is the ending of the full request URI, truncated from the route
106 # leading to this `Action`.
107 fun answer(request: HttpRequest, truncated_uri: String): HttpResponse is abstract
108
109 # Full to a `request` with sending the response and closing of the `http_server`
110 #
111 # Must users only need to implement `answer`, this method is for advanced use only.
112 # It can be used to delay an answer until an event is raised or work is done on a different thread.
113 #
114 # By default this method calls `answer`, relays the response to `http_server.respond` and closes `http_server`.
115 protected fun prepare_respond_and_close(request: HttpRequest, truncated_uri: String, http_server: HttpServer)
116 do
117 var response = answer(request, truncated_uri)
118 http_server.respond response
119 http_server.close
120 end
121 end
122
123 # Factory to create `HttpServer` instances, and hold the libevent base handler
124 class HttpFactory
125 super ConnectionFactory
126
127 # Configuration of this server
128 #
129 # It should be populated after this object has instanciated
130 var config = new ServerConfig.with_factory(self)
131
132 # Instantiate a server and libvent
133 #
134 # You can use this to create the first `HttpFactory`, which is the most common.
135 init and_libevent do init(new NativeEventBase)
136
137 redef fun spawn_connection(buf_ev, address) do return new HttpServer(buf_ev, self, address)
138
139 # Execute the main listening loop to accept connections
140 #
141 # After the loop ends, the underlying resources are freed.
142 #
143 # When the environment variable `NIT_TESTING` is set to `true`,
144 # the loop is not executed but the resources are still freed.
145 fun run
146 do
147 if "NIT_TESTING".environ != "true" then
148 event_base.dispatch
149 end
150
151 event_base.free
152 end
153 end
154
155 redef class ServerConfig
156 # Handle to retreive the `HttpFactory` on config change
157 private var factory: HttpFactory is noinit
158
159 private init with_factory(factory: HttpFactory) do self.factory = factory
160 end
161
162 redef class Sys
163 # Active listeners
164 private var listeners = new HashMap2[String, Int, ConnectionListener]
165
166 # Hosts needong each listener
167 private var listeners_count = new HashMap2[String, Int, Int]
168
169 # Activate a listener on `interfac` if there's not already one
170 private fun listen_on(interfac: Interface, factory: HttpFactory)
171 do
172 if interfac.registered then return
173
174 var name = interfac.name
175 var port = interfac.port
176
177 var listener = listeners[name, port]
178 if listener == null then
179 listener = factory.bind_to(name, port)
180 if listener != null then
181 sys.listeners[name, port] = listener
182 listeners_count[name, port] = 1
183 end
184 else
185 var value = listeners_count[name, port].as(not null)
186 listeners_count[name, port] = value + 1
187 end
188
189 interfac.registered = true
190 end
191
192 # TODO close listener
193 end
194
195 redef class Interface
196 # Has `self` been registered by `listen_on`?
197 private var registered = false
198 end
199
200 redef class Interfaces
201 redef fun add(e)
202 do
203 super
204 var config = virtual_host.server_config
205 if config != null then register_and_listen(e, config)
206 end
207
208 # Indirection to `listen_on` and check if this targets all addresses
209 private fun register_and_listen(e: Interface, config: ServerConfig)
210 do
211 listen_on(e, config.factory)
212 if e.name == "0.0.0.0" or e.name == "::0" then config.default_virtual_host = virtual_host
213 end
214
215 # TODO remove
216 end
217
218 redef class VirtualHosts
219 redef fun add(e)
220 do
221 super
222 for i in e.interfaces do e.interfaces.register_and_listen(i, config)
223 end
224
225 # TODO remove
226 end