lib/nitcorn: skip the listening loop when running continuous integration tests
[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 redef fun read_http_request(str)
39 do
40 var request_object = parser.parse_http_request(str.to_s)
41 if request_object != null then delegate_answer request_object
42 end
43
44 # Answer to a request
45 fun delegate_answer(request: HttpRequest)
46 do
47 # Find target virtual host
48 var virtual_host = null
49 if request.header.keys.has("Host") then
50 var host = request.header["Host"]
51 if host.index_of(':') == -1 then host += ":80"
52 for vh in factory.config.virtual_hosts do
53 for i in vh.interfaces do if i.to_s == host then
54 virtual_host = vh
55 break label
56 end
57 end label
58 end
59
60 # Use default virtual host if none already responded
61 if virtual_host == null then virtual_host = factory.config.default_virtual_host
62
63 # Get a response from the virtual host
64 var response
65 if virtual_host != null then
66 var route = virtual_host.routes[request.uri]
67 if route != null then
68 # include uri parameters in request
69 request.uri_params = route.parse_params(request.uri)
70
71 var handler = route.handler
72 var root = route.path
73 var turi
74 if root != null then
75 turi = ("/" + request.uri.substring_from(root.length)).simplify_path
76 else turi = request.uri
77
78 # Delegate the responsibility to respond to the `Action`
79 handler.prepare_respond_and_close(request, turi, self)
80 return
81 else response = new HttpResponse(405)
82 else response = new HttpResponse(405)
83
84 respond response
85 close
86 end
87
88 # Send back `response` to the client
89 fun respond(response: HttpResponse)
90 do
91 write response.to_s
92 for path in response.files do write_file path
93 end
94 end
95
96 redef abstract class Action
97 # Prepare a `HttpResponse` destined to the client in response to the `request`
98 #
99 # `request` is fully formed request object with a reference to the session
100 # if one already exists.
101 #
102 # `truncated_uri` is the ending of the full request URI, truncated from the route
103 # leading to this `Action`.
104 fun answer(request: HttpRequest, truncated_uri: String): HttpResponse is abstract
105
106 # Full to a `request` with sending the response and closing of the `http_server`
107 #
108 # Must users only need to implement `answer`, this method is for advanced use only.
109 # It can be used to delay an answer until an event is raised or work is done on a different thread.
110 #
111 # By default this method calls `answer`, relays the response to `http_server.respond` and closes `http_server`.
112 protected fun prepare_respond_and_close(request: HttpRequest, truncated_uri: String, http_server: HttpServer)
113 do
114 var response = answer(request, truncated_uri)
115 http_server.respond response
116 http_server.close
117 end
118 end
119
120 # Factory to create `HttpServer` instances, and hold the libevent base handler
121 class HttpFactory
122 super ConnectionFactory
123
124 # Configuration of this server
125 #
126 # It should be populated after this object has instanciated
127 var config = new ServerConfig.with_factory(self)
128
129 # Instantiate a server and libvent
130 #
131 # You can use this to create the first `HttpFactory`, which is the most common.
132 init and_libevent do init(new NativeEventBase)
133
134 redef fun spawn_connection(buf_ev) do return new HttpServer(buf_ev, self)
135
136 # Execute the main listening loop to accept connections
137 #
138 # After the loop ends, the underlying resources are freed.
139 #
140 # When the environment variable `NIT_TESTING` is set to `true`,
141 # the loop is not executed but the resources are still freed.
142 fun run
143 do
144 if "NIT_TESTING".environ != "true" then
145 event_base.dispatch
146 end
147
148 event_base.destroy
149 end
150 end
151
152 redef class ServerConfig
153 # Handle to retreive the `HttpFactory` on config change
154 private var factory: HttpFactory
155
156 private init with_factory(factory: HttpFactory) do self.factory = factory
157 end
158
159 redef class Sys
160 # Active listeners
161 private var listeners = new HashMap2[String, Int, ConnectionListener]
162
163 # Hosts needong each listener
164 private var listeners_count = new HashMap2[String, Int, Int]
165
166 # Activate a listener on `interfac` if there's not already one
167 private fun listen_on(interfac: Interface, factory: HttpFactory)
168 do
169 if interfac.registered then return
170
171 var name = interfac.name
172 var port = interfac.port
173
174 var listener = listeners[name, port]
175 if listener == null then
176 listener = factory.bind_to(name, port)
177 if listener != null then
178 sys.listeners[name, port] = listener
179 listeners_count[name, port] = 1
180 end
181 else
182 listeners_count[name, port] += 1
183 end
184
185 interfac.registered = true
186 end
187
188 # TODO close listener
189 end
190
191 redef class Interface
192 # Has `self` been registered by `listen_on`?
193 private var registered = false
194 end
195
196 redef class Interfaces
197 redef fun add(e)
198 do
199 super
200 if vh.server_config != null then sys.listen_on(e, vh.server_config.factory)
201 end
202
203 # TODO remove
204 end
205
206 redef class VirtualHosts
207 redef fun add(e)
208 do
209 super
210 for i in e.interfaces do sys.listen_on(i, config.factory)
211 end
212
213 # TODO remove
214 end