1 # |
|
2 # Copyright (c) 2015, Mahlon E. Smith <mahlon@martini.nu> |
|
3 # All rights reserved. |
|
4 # Redistribution and use in source and binary forms, with or without |
|
5 # modification, are permitted provided that the following conditions are met: |
|
6 # |
|
7 # * Redistributions of source code must retain the above copyright |
|
8 # notice, this list of conditions and the following disclaimer. |
|
9 # |
|
10 # * Redistributions in binary form must reproduce the above copyright |
|
11 # notice, this list of conditions and the following disclaimer in the |
|
12 # documentation and/or other materials provided with the distribution. |
|
13 # |
|
14 # * Neither the name of Mahlon E. Smith nor the names of his |
|
15 # contributors may be used to endorse or promote products derived |
|
16 # from this software without specific prior written permission. |
|
17 # |
|
18 # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY |
|
19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
21 # DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY |
|
22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
|
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
|
25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
27 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
28 |
|
29 ## This module is a simple interface to the Mongrel2 webserver |
|
30 ## environment (http://mongrel2.org/). After a Mongrel2 server has been |
|
31 ## properly configured, you can use this library to easily service client |
|
32 ## requests. |
|
33 ## |
|
34 ## Bare minimal, do-nothing example: |
|
35 ## |
|
36 ## .. code-block:: nim |
|
37 ## |
|
38 ## newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ).run |
|
39 ## |
|
40 ## Yep, that's it. Assuming your Mongrel2 server is configured with a |
|
41 ## matching application identifier and request/response communication |
|
42 ## ports, that's enough to see the default output from the handler when |
|
43 ## loaded in browser. |
|
44 ## |
|
45 ## It's likely that you'll want to do something of more substance. This |
|
46 ## is performed via an `action` hook, which is just a proc() reference. |
|
47 ## It is passed the parsed `M2Request` client request, and it needs to |
|
48 ## return a matching `M2Response` object. What happens in between is |
|
49 ## entirely up to you. |
|
50 ## |
|
51 ## Here's a "hello world": |
|
52 ## |
|
53 ## .. code-block:: nim |
|
54 ## |
|
55 ## let handler = newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ) |
|
56 ## |
|
57 ## proc hello_world( request: M2Request ): M2Response = |
|
58 ## result = request.response |
|
59 ## result[ "Content-Type" ] = "text/plain" |
|
60 ## result.body = "Hello there, world!" |
|
61 ## result.status = HTTP_OK |
|
62 ## |
|
63 ## handler.action = hello_world |
|
64 ## handler.run |
|
65 ## |
|
66 ## And finally, a slightly more interesting example: |
|
67 ## |
|
68 ## .. code-block:: nim |
|
69 ## |
|
70 ## import |
|
71 ## mongrel2, |
|
72 ## json, |
|
73 ## re |
|
74 ## |
|
75 ## let handler = newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ) |
|
76 ## var data = %*[] ## the JSON data to "remember" |
|
77 ## |
|
78 ## |
|
79 ## proc demo( request: M2Request ): M2Response = |
|
80 ## ## This is a demonstration handler action. |
|
81 ## ## |
|
82 ## ## It accepts and stores a JSON data structure |
|
83 ## ## on POST, and returns it on GET. |
|
84 ## |
|
85 ## # Create a response object for the current request. |
|
86 ## var response = request.response |
|
87 ## |
|
88 ## case request.meth |
|
89 ## |
|
90 ## # For GET requests, display the current JSON structure. |
|
91 ## # |
|
92 ## of "GET": |
|
93 ## if request[ "Accept" ].match( re(".*text/(html|plain).*") ): |
|
94 ## response[ "Content-Type" ] = "text/plain" |
|
95 ## response.body = "Hi there. POST some JSON to me and I'll remember it.\n\n" & $( data ) |
|
96 ## response.status = HTTP_OK |
|
97 ## |
|
98 ## elif request[ "Accept" ].match( re("application/json") ): |
|
99 ## response[ "Content-Type" ] = "application/json" |
|
100 ## response.body = $( data ) |
|
101 ## response.status = HTTP_OK |
|
102 ## |
|
103 ## else: |
|
104 ## response.status = HTTP_BAD_REQUEST |
|
105 ## |
|
106 ## # POST requests overwrite the current JSON structure. |
|
107 ## # |
|
108 ## of "POST": |
|
109 ## if request[ "Content-Type" ].match( re(".*application/json.*") ): |
|
110 ## try: |
|
111 ## data = request.body.parse_json |
|
112 ## response.status = HTTP_OK |
|
113 ## response[ "Content-Type" ] = "application/json" |
|
114 ## response.body = $( data ) |
|
115 ## except: |
|
116 ## response.status = HTTP_BAD_REQUEST |
|
117 ## response[ "Content-Type" ] = "text/plain" |
|
118 ## response.body = request.body |
|
119 ## |
|
120 ## else: |
|
121 ## response.body = "I only accept valid JSON strings." |
|
122 ## response.status = HTTP_BAD_REQUEST |
|
123 ## |
|
124 ## else: |
|
125 ## response.status = HTTP_NOT_ACCEPTABLE |
|
126 ## |
|
127 ## return response |
|
128 ## |
|
129 ## |
|
130 ## # Attach the proc reference to the handler action. |
|
131 ## handler.action = demo |
|
132 ## |
|
133 ## # Start 'er up! |
|
134 ## handler.run |
|
135 ## |
|
136 |
|
137 |
|
138 import |
|
139 json, |
|
140 strutils, |
|
141 tables, |
|
142 times, |
|
143 tnetstring, |
|
144 zmq |
|
145 |
|
146 type |
|
147 M2Handler* = ref object of RootObj |
|
148 handler_id: string |
|
149 request_sock: TConnection |
|
150 response_sock: TConnection |
|
151 action*: proc ( request: M2Request ): M2Response |
|
152 disconnect_action*: proc ( request: M2Request ) |
|
153 |
|
154 M2Request* = ref object of RootObj |
|
155 sender_id*: string ## Mongrel2 app-id |
|
156 conn_id*: string |
|
157 path*: string |
|
158 meth*: string |
|
159 version*: string |
|
160 uri*: string |
|
161 pattern*: string |
|
162 scheme*: string |
|
163 remote_addr*: string |
|
164 query*: string |
|
165 headers*: seq[ tuple[name: string, value: string] ] |
|
166 body*: string |
|
167 |
|
168 M2Response* = ref object of RootObj |
|
169 sender_id*: string |
|
170 conn_id*: string |
|
171 status*: int |
|
172 headers*: seq[ tuple[name: string, value: string] ] |
|
173 body*: string |
|
174 extended: string |
|
175 ex_data: seq[ string ] |
|
176 |
|
177 const |
|
178 MAX_CLIENT_BROADCAST = 128 ## The maximum number of client to broadcast a response to |
|
179 CRLF = "\r\n" ## Network line ending |
|
180 DEFAULT_CONTENT = """ |
|
181 <!DOCTYPE html> |
|
182 <html lang="en"> |
|
183 <head> |
|
184 <title>It works!</title> |
|
185 <link href='http://fonts.googleapis.com/css?family=Play' rel='stylesheet' type='text/css'> |
|
186 <style> |
|
187 body { |
|
188 margin: 50px; |
|
189 height: 100%; |
|
190 background-color: #ddd; |
|
191 font-family: Play, Arial, serif; |
|
192 font-weight: 400; |
|
193 background: linear-gradient( to bottom, #7db9e8 25%,#fff 100% ); |
|
194 } |
|
195 a, a:hover, a:visited { |
|
196 text-decoration: none; |
|
197 color: rgb( 66,131,251 ); |
|
198 } |
|
199 .content { |
|
200 font-size: 1.2em; |
|
201 border-radius: 10px; |
|
202 margin: 0 auto 0 auto; |
|
203 width: 50%; |
|
204 padding: 5px 20px 20px 20px; |
|
205 box-shadow: 1px 2px 6px #000; |
|
206 background-color: #fff; |
|
207 border: 1px; |
|
208 } |
|
209 .handler { |
|
210 font-size: 0.9em; |
|
211 padding: 2px 0 0 10px; |
|
212 color: rgb( 192,220,255 ); |
|
213 background-color: rgb( 6,20,85 ); |
|
214 border: 2px solid rgb( 66,131,251 ); |
|
215 } |
|
216 </style> |
|
217 </head> |
|
218 <body> |
|
219 <div class="content"> |
|
220 <h1>Almost there...</h1> |
|
221 <p> |
|
222 This is the default handler output. While this is |
|
223 useful to demonstrate that your <a href="http://mongrel2.org">Mongrel2</a> |
|
224 server is indeed speaking to your <a href="http://nim-lang.org">nim</a> |
|
225 handler, you're probably going to want to do something else for production use. |
|
226 </p> |
|
227 <p> |
|
228 Here's an example handler: |
|
229 </p> |
|
230 |
|
231 <div class="handler"> |
|
232 <pre> |
|
233 import |
|
234 mongrel2, |
|
235 json, |
|
236 re |
|
237 |
|
238 let handler = newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ) |
|
239 var data = %*[] ## the JSON data to "remember" |
|
240 |
|
241 |
|
242 proc demo( request: M2Request ): M2Response = |
|
243 ## This is a demonstration handler action. |
|
244 ## |
|
245 ## It accepts and stores a JSON data structure |
|
246 ## on POST, and returns it on GET. |
|
247 |
|
248 # Create a response object for the current request. |
|
249 var response = request.response |
|
250 |
|
251 case request.meth |
|
252 |
|
253 # For GET requests, display the current JSON structure. |
|
254 # |
|
255 of "GET": |
|
256 if request[ "Accept" ].match( re(".*text/(html|plain).*") ): |
|
257 response[ "Content-Type" ] = "text/plain" |
|
258 response.body = "Hi there. POST some JSON to me and I'll remember it.\n\n" & $( data ) |
|
259 response.status = HTTP_OK |
|
260 |
|
261 elif request[ "Accept" ].match( re("application/json") ): |
|
262 response[ "Content-Type" ] = "application/json" |
|
263 response.body = $( data ) |
|
264 response.status = HTTP_OK |
|
265 |
|
266 else: |
|
267 response.status = HTTP_BAD_REQUEST |
|
268 |
|
269 # Overwrite the current JSON structure. |
|
270 # |
|
271 of "POST": |
|
272 if request[ "Content-Type" ].match( re(".*application/json.*") ): |
|
273 try: |
|
274 data = request.body.parse_json |
|
275 response.status = HTTP_OK |
|
276 response[ "Content-Type" ] = "application/json" |
|
277 response.body = $( data ) |
|
278 except: |
|
279 response.status = HTTP_BAD_REQUEST |
|
280 response[ "Content-Type" ] = "text/plain" |
|
281 response.body = request.body |
|
282 |
|
283 else: |
|
284 response.body = "I only accept valid JSON strings." |
|
285 response.status = HTTP_BAD_REQUEST |
|
286 |
|
287 else: |
|
288 response.status = HTTP_NOT_ACCEPTABLE |
|
289 |
|
290 return response |
|
291 |
|
292 |
|
293 # Attach the proc reference to the handler action. |
|
294 handler.action = demo |
|
295 |
|
296 # Start 'er up! |
|
297 handler.run |
|
298 </pre> |
|
299 </div> |
|
300 </div> |
|
301 </body> |
|
302 </html> |
|
303 """ |
|
304 |
|
305 HTTP_CONTINUE* = 100 |
|
306 HTTP_SWITCHING_PROTOCOLS* = 101 |
|
307 HTTP_PROCESSING* = 102 |
|
308 HTTP_OK* = 200 |
|
309 HTTP_CREATED* = 201 |
|
310 HTTP_ACCEPTED* = 202 |
|
311 HTTP_NON_AUTHORITATIVE* = 203 |
|
312 HTTP_NO_CONTENT* = 204 |
|
313 HTTP_RESET_CONTENT* = 205 |
|
314 HTTP_PARTIAL_CONTENT* = 206 |
|
315 HTTP_MULTI_STATUS* = 207 |
|
316 HTTP_MULTIPLE_CHOICES* = 300 |
|
317 HTTP_MOVED_PERMANENTLY* = 301 |
|
318 HTTP_MOVED* = 301 |
|
319 HTTP_MOVED_TEMPORARILY* = 302 |
|
320 HTTP_REDIRECT* = 302 |
|
321 HTTP_SEE_OTHER* = 303 |
|
322 HTTP_NOT_MODIFIED* = 304 |
|
323 HTTP_USE_PROXY* = 305 |
|
324 HTTP_TEMPORARY_REDIRECT* = 307 |
|
325 HTTP_BAD_REQUEST* = 400 |
|
326 HTTP_AUTH_REQUIRED* = 401 |
|
327 HTTP_UNAUTHORIZED* = 401 |
|
328 HTTP_PAYMENT_REQUIRED* = 402 |
|
329 HTTP_FORBIDDEN* = 403 |
|
330 HTTP_NOT_FOUND* = 404 |
|
331 HTTP_METHOD_NOT_ALLOWED* = 405 |
|
332 HTTP_NOT_ACCEPTABLE* = 406 |
|
333 HTTP_PROXY_AUTHENTICATION_REQUIRED* = 407 |
|
334 HTTP_REQUEST_TIME_OUT* = 408 |
|
335 HTTP_CONFLICT* = 409 |
|
336 HTTP_GONE* = 410 |
|
337 HTTP_LENGTH_REQUIRED* = 411 |
|
338 HTTP_PRECONDITION_FAILED* = 412 |
|
339 HTTP_REQUEST_ENTITY_TOO_LARGE* = 413 |
|
340 HTTP_REQUEST_URI_TOO_LARGE* = 414 |
|
341 HTTP_UNSUPPORTED_MEDIA_TYPE* = 415 |
|
342 HTTP_RANGE_NOT_SATISFIABLE* = 416 |
|
343 HTTP_EXPECTATION_FAILED* = 417 |
|
344 HTTP_UNPROCESSABLE_ENTITY* = 422 |
|
345 HTTP_LOCKED* = 423 |
|
346 HTTP_FAILED_DEPENDENCY* = 424 |
|
347 HTTP_UPGRADE_REQUIRED* = 426 |
|
348 HTTP_RECONDITION_REQUIRED* = 428 |
|
349 HTTP_TOO_MANY_REQUESTS* = 429 |
|
350 HTTP_REQUEST_HEADERS_TOO_LARGE* = 431 |
|
351 HTTP_SERVER_ERROR* = 500 |
|
352 HTTP_NOT_IMPLEMENTED* = 501 |
|
353 HTTP_BAD_GATEWAY* = 502 |
|
354 HTTP_SERVICE_UNAVAILABLE* = 503 |
|
355 HTTP_GATEWAY_TIME_OUT* = 504 |
|
356 HTTP_VERSION_NOT_SUPPORTED* = 505 |
|
357 HTTP_VARIANT_ALSO_VARIES* = 506 |
|
358 HTTP_INSUFFICIENT_STORAGE* = 507 |
|
359 HTTP_NOT_EXTENDED* = 510 |
|
360 |
|
361 HTTPCODE = { |
|
362 HTTP_CONTINUE: ( desc: "Continue", label: "CONTINUE" ), |
|
363 HTTP_SWITCHING_PROTOCOLS: ( desc: "Switching Protocols", label: "SWITCHING_PROTOCOLS" ), |
|
364 HTTP_PROCESSING: ( desc: "Processing", label: "PROCESSING" ), |
|
365 HTTP_OK: ( desc: "OK", label: "OK" ), |
|
366 HTTP_CREATED: ( desc: "Created", label: "CREATED" ), |
|
367 HTTP_ACCEPTED: ( desc: "Accepted", label: "ACCEPTED" ), |
|
368 HTTP_NON_AUTHORITATIVE: ( desc: "Non-Authoritative Information", label: "NON_AUTHORITATIVE" ), |
|
369 HTTP_NO_CONTENT: ( desc: "No Content", label: "NO_CONTENT" ), |
|
370 HTTP_RESET_CONTENT: ( desc: "Reset Content", label: "RESET_CONTENT" ), |
|
371 HTTP_PARTIAL_CONTENT: ( desc: "Partial Content", label: "PARTIAL_CONTENT" ), |
|
372 HTTP_MULTI_STATUS: ( desc: "Multi-Status", label: "MULTI_STATUS" ), |
|
373 HTTP_MULTIPLE_CHOICES: ( desc: "Multiple Choices", label: "MULTIPLE_CHOICES" ), |
|
374 HTTP_MOVED_PERMANENTLY: ( desc: "Moved Permanently", label: "MOVED_PERMANENTLY" ), |
|
375 HTTP_MOVED: ( desc: "Moved Permanently", label: "MOVED" ), |
|
376 HTTP_MOVED_TEMPORARILY: ( desc: "Found", label: "MOVED_TEMPORARILY" ), |
|
377 HTTP_REDIRECT: ( desc: "Found", label: "REDIRECT" ), |
|
378 HTTP_SEE_OTHER: ( desc: "See Other", label: "SEE_OTHER" ), |
|
379 HTTP_NOT_MODIFIED: ( desc: "Not Modified", label: "NOT_MODIFIED" ), |
|
380 HTTP_USE_PROXY: ( desc: "Use Proxy", label: "USE_PROXY" ), |
|
381 HTTP_TEMPORARY_REDIRECT: ( desc: "Temporary Redirect", label: "TEMPORARY_REDIRECT" ), |
|
382 HTTP_BAD_REQUEST: ( desc: "Bad Request", label: "BAD_REQUEST" ), |
|
383 HTTP_AUTH_REQUIRED: ( desc: "Authorization Required", label: "AUTH_REQUIRED" ), |
|
384 HTTP_UNAUTHORIZED: ( desc: "Authorization Required", label: "UNAUTHORIZED" ), |
|
385 HTTP_PAYMENT_REQUIRED: ( desc: "Payment Required", label: "PAYMENT_REQUIRED" ), |
|
386 HTTP_FORBIDDEN: ( desc: "Forbidden", label: "FORBIDDEN" ), |
|
387 HTTP_NOT_FOUND: ( desc: "Not Found", label: "NOT_FOUND" ), |
|
388 HTTP_METHOD_NOT_ALLOWED: ( desc: "Method Not Allowed", label: "METHOD_NOT_ALLOWED" ), |
|
389 HTTP_NOT_ACCEPTABLE: ( desc: "Not Acceptable", label: "NOT_ACCEPTABLE" ), |
|
390 HTTP_PROXY_AUTHENTICATION_REQUIRED: ( desc: "Proxy Authentication Required", label: "PROXY_AUTHENTICATION_REQUIRED" ), |
|
391 HTTP_REQUEST_TIME_OUT: ( desc: "Request Time-out", label: "REQUEST_TIME_OUT" ), |
|
392 HTTP_CONFLICT: ( desc: "Conflict", label: "CONFLICT" ), |
|
393 HTTP_GONE: ( desc: "Gone", label: "GONE" ), |
|
394 HTTP_LENGTH_REQUIRED: ( desc: "Length Required", label: "LENGTH_REQUIRED" ), |
|
395 HTTP_PRECONDITION_FAILED: ( desc: "Precondition Failed", label: "PRECONDITION_FAILED" ), |
|
396 HTTP_REQUEST_ENTITY_TOO_LARGE: ( desc: "Request Entity Too Large", label: "REQUEST_ENTITY_TOO_LARGE" ), |
|
397 HTTP_REQUEST_URI_TOO_LARGE: ( desc: "Request-URI Too Large", label: "REQUEST_URI_TOO_LARGE" ), |
|
398 HTTP_UNSUPPORTED_MEDIA_TYPE: ( desc: "Unsupported Media Type", label: "UNSUPPORTED_MEDIA_TYPE" ), |
|
399 HTTP_RANGE_NOT_SATISFIABLE: ( desc: "Requested Range Not Satisfiable", label: "RANGE_NOT_SATISFIABLE" ), |
|
400 HTTP_EXPECTATION_FAILED: ( desc: "Expectation Failed", label: "EXPECTATION_FAILED" ), |
|
401 HTTP_UNPROCESSABLE_ENTITY: ( desc: "Unprocessable Entity", label: "UNPROCESSABLE_ENTITY" ), |
|
402 HTTP_LOCKED: ( desc: "Locked", label: "LOCKED" ), |
|
403 HTTP_FAILED_DEPENDENCY: ( desc: "Failed Dependency", label: "FAILED_DEPENDENCY" ), |
|
404 HTTP_UPGRADE_REQUIRED: ( desc: "Upgrade Required", label: "UPGRADE_REQUIRED" ), |
|
405 HTTP_RECONDITION_REQUIRED: ( desc: "Precondition Required", label: "RECONDITION_REQUIRED" ), |
|
406 HTTP_TOO_MANY_REQUESTS: ( desc: "Too Many Requests", label: "TOO_MANY_REQUESTS" ), |
|
407 HTTP_REQUEST_HEADERS_TOO_LARGE: ( desc: "Request Headers too Large", label: "REQUEST_HEADERS_TOO_LARGE" ), |
|
408 HTTP_SERVER_ERROR: ( desc: "Internal Server Error", label: "SERVER_ERROR" ), |
|
409 HTTP_NOT_IMPLEMENTED: ( desc: "Method Not Implemented", label: "NOT_IMPLEMENTED" ), |
|
410 HTTP_BAD_GATEWAY: ( desc: "Bad Gateway", label: "BAD_GATEWAY" ), |
|
411 HTTP_SERVICE_UNAVAILABLE: ( desc: "Service Temporarily Unavailable", label: "SERVICE_UNAVAILABLE" ), |
|
412 HTTP_GATEWAY_TIME_OUT: ( desc: "Gateway Time-out", label: "GATEWAY_TIME_OUT" ), |
|
413 HTTP_VERSION_NOT_SUPPORTED: ( desc: "HTTP Version Not Supported", label: "VERSION_NOT_SUPPORTED" ), |
|
414 HTTP_VARIANT_ALSO_VARIES: ( desc: "Variant Also Negotiates", label: "VARIANT_ALSO_VARIES" ), |
|
415 HTTP_INSUFFICIENT_STORAGE: ( desc: "Insufficient Storage", label: "INSUFFICIENT_STORAGE" ), |
|
416 HTTP_NOT_EXTENDED: ( desc: "Not Extended", label: "NOT_EXTENDED" ) |
|
417 }.toTable |
|
418 |
|
419 |
|
420 |
|
421 proc newM2Handler*( id: string, req_sock: string, res_sock: string ): M2Handler = |
|
422 ## Instantiate a new `M2Handler` object. |
|
423 ## The `id` should match the app ID in the Mongrel2 config, |
|
424 ## `req_sock` is the ZMQ::PULL socket Mongrel2 sends client request |
|
425 ## data on, and `res_sock` is the ZMQ::PUB socket Mongrel2 subscribes |
|
426 ## to. Nothing is put into action until the run() method is invoked. |
|
427 new( result ) |
|
428 result.handler_id = id |
|
429 result.request_sock = zmq.connect( req_sock, PULL ) |
|
430 result.response_sock = zmq.connect( res_sock, PUB ) |
|
431 |
|
432 |
|
433 proc parse_request( request: string ): M2Request = |
|
434 ## Parse a message `request` string received from Mongrel2, |
|
435 ## return it as a M2Request object. |
|
436 var |
|
437 reqstr = request.split( ' ' ) |
|
438 rest = "" |
|
439 req_tnstr: TNetstringNode |
|
440 headers: JsonNode |
|
441 |
|
442 new( result ) |
|
443 result.sender_id = reqstr[ 0 ] |
|
444 result.conn_id = reqstr[ 1 ] |
|
445 result.path = reqstr[ 2 ] |
|
446 |
|
447 # There must be a better way to join() a seq... |
|
448 # |
|
449 for i in 3 .. reqstr.high: |
|
450 rest = rest & reqstr[ i ] |
|
451 if i < reqstr.high: rest = rest & ' ' |
|
452 reqtnstr = rest.parse_tnetstring |
|
453 result.body = reqtnstr.extra.parse_tnetstring.getStr("") |
|
454 |
|
455 # Pull Mongrel2 control headers into the request object. |
|
456 # |
|
457 headers = reqtnstr.getStr.parse_json |
|
458 |
|
459 if headers.has_key( "METHOD" ): |
|
460 result.meth = headers[ "METHOD" ].getStr |
|
461 headers.delete( "METHOD" ) |
|
462 |
|
463 if headers.has_key( "PATTERN" ): |
|
464 result.pattern = headers[ "PATTERN" ].getStr |
|
465 headers.delete( "PATTERN" ) |
|
466 |
|
467 if headers.has_key( "REMOTE_ADDR" ): |
|
468 result.remote_addr = headers[ "REMOTE_ADDR" ].getStr |
|
469 headers.delete( "REMOTE_ADDR" ) |
|
470 |
|
471 if headers.has_key( "URI" ): |
|
472 result.uri = headers[ "URI" ].getStr |
|
473 headers.delete( "URI" ) |
|
474 |
|
475 if headers.has_key( "URL_SCHEME" ): |
|
476 result.scheme = headers[ "URL_SCHEME" ].getStr |
|
477 headers.delete( "URL_SCHEME" ) |
|
478 |
|
479 if headers.has_key( "VERSION" ): |
|
480 result.version = headers[ "VERSION" ].getStr |
|
481 headers.delete( "VERSION" ) |
|
482 |
|
483 if headers.has_key( "QUERY" ): |
|
484 result.query = headers[ "QUERY" ].getStr |
|
485 headers.delete( "QUERY" ) |
|
486 |
|
487 # Remaining headers are client supplied. |
|
488 # |
|
489 result.headers = @[] |
|
490 for key, val in headers: |
|
491 result.headers.add( ( key, val.getStr ) ) |
|
492 |
|
493 |
|
494 proc response*( request: M2Request ): M2Response = |
|
495 ## Instantiate a new `M2Response`, paired from an `M2Request`. |
|
496 new( result ) |
|
497 result.sender_id = request.sender_id |
|
498 result.conn_id = request.conn_id |
|
499 result.headers = @[] |
|
500 |
|
501 |
|
502 proc `[]`*( request: M2Request, label: string ): string = |
|
503 ## Defer to the underlying headers tuple array. Lookup is case-insensitive. |
|
504 for header in request.headers: |
|
505 if cmpIgnoreCase( label, header.name ) == 0: |
|
506 return header.value |
|
507 return nil |
|
508 |
|
509 |
|
510 proc is_disconnect*( request: M2Request ): bool = |
|
511 ## Returns true if this is a Mongrel2 disconnect request. |
|
512 if ( request.path == "@*" and request.meth == "JSON") : |
|
513 var body = request.body.parseJson |
|
514 if ( body.has_key( "type" ) and body[ "type" ].getStr == "disconnect" ): |
|
515 return true |
|
516 return false |
|
517 |
|
518 |
|
519 proc `[]`*( response: M2Response, label: string ): string = |
|
520 ## Defer to the underlying headers tuple array. Lookup is case-insensitive. |
|
521 for header in response.headers: |
|
522 if cmpIgnoreCase( label, header.name ) == 0: |
|
523 return header.value |
|
524 return nil |
|
525 |
|
526 |
|
527 proc `[]=`*( response: M2Response, name: string, value: string ) = |
|
528 ## Set a header on the response. Duplicates are replaced. |
|
529 var new_headers: seq[ tuple[name: string, value: string] ] = @[] |
|
530 for header in response.headers: |
|
531 if cmpIgnoreCase( name, header.name ) != 0: |
|
532 new_headers.add( header ) |
|
533 response.headers = new_headers |
|
534 response.headers.add( (name, value) ) |
|
535 |
|
536 |
|
537 proc add_header*( response: M2Response, name: string, value: string ) = |
|
538 ## Adds a header to the response. Duplicates are ignored. |
|
539 response.headers.add( (name, value) ) |
|
540 |
|
541 |
|
542 proc extend*( response: M2Response, filter: string ) = |
|
543 ## Set a response as extended. This means different things depending on |
|
544 ## the Mongrel2 filter in use. |
|
545 response.extended = filter |
|
546 |
|
547 |
|
548 proc add_extended_data*( response: M2Response, data: varargs[string, `$`] ) = |
|
549 ## Attach filter arguments to the extended response. Arguments should |
|
550 ## be coercible into strings. |
|
551 if isNil( response.ex_data ): response.ex_data = @[] |
|
552 for arg in data: |
|
553 response.ex_data.add( arg ) |
|
554 |
|
555 |
|
556 proc is_extended*( response: M2Response ): bool = |
|
557 ## Predicate method to determine if a response is extended. |
|
558 return not isNil( response.extended ) |
|
559 |
|
560 |
|
561 proc broadcast*[T]( response: M2Response, ids: openarray[T] ) = |
|
562 ## Send the response to multiple backend client IDs. |
|
563 assert( ids.len <= MAX_CLIENT_BROADCAST, "Exceeded client broadcast maximum" ) |
|
564 |
|
565 response.conn_id = $( ids[0] ) |
|
566 for i in 1 .. ids.high: |
|
567 if i <= ids.high: response.conn_id = response.conn_id & ' ' |
|
568 response.conn_id = response.conn_id & $( ids[i] ) |
|
569 |
|
570 |
|
571 proc format( response: M2Response ): string = |
|
572 ## Format an `M2Response` object for Mongrel2. |
|
573 var conn_id: string |
|
574 |
|
575 # Mongrel2 extended response. |
|
576 # |
|
577 if response.is_extended: |
|
578 conn_id = newTNetstringString( "X " & response.conn_id ).dump_tnetstring |
|
579 result = response.sender_id & ' ' & conn_id |
|
580 |
|
581 # 1st argument is the filter name. |
|
582 # |
|
583 var tnet_array = newTNetstringArray() |
|
584 tnet_array.add( newTNetstringString(response.extended) ) |
|
585 |
|
586 # rest are the filter arguments, if any. |
|
587 # |
|
588 if not isNil( response.ex_data ): |
|
589 for data in response.ex_data: |
|
590 tnet_array.add( newTNetstringString(data) ) |
|
591 |
|
592 result = result & ' ' & tnet_array.dump_tnetstring |
|
593 |
|
594 |
|
595 else: |
|
596 # Regular HTTP request/response cycle. |
|
597 # |
|
598 if isNil( response.body ): |
|
599 response.body = HTTPCODE[ response.status ].desc |
|
600 response[ "Content-Length" ] = $( response.body.len ) |
|
601 else: |
|
602 response[ "Content-Length" ] = $( response.body.len ) |
|
603 |
|
604 let code = "$1 $2" % [ $(response.status), HTTPCODE[response.status].label ] |
|
605 conn_id = newTNetstringString( response.conn_id ).dump_tnetstring |
|
606 result = response.sender_id & ' ' & conn_id |
|
607 result = result & " HTTP/1.1 " & code & CRLF |
|
608 |
|
609 for header in response.headers: |
|
610 result = result & header.name & ": " & header.value & CRLF |
|
611 |
|
612 result = result & CRLF & response.body |
|
613 |
|
614 |
|
615 proc handle_default( request: M2Request ): M2Response = |
|
616 ## This is the default handler, if the caller didn't install one. |
|
617 result = request.response |
|
618 result[ "Content-Type" ] = "text/html" |
|
619 result.body = DEFAULT_CONTENT |
|
620 |
|
621 |
|
622 proc run*( handler: M2Handler ) {. noreturn .} = |
|
623 ## Enter the request loop conversation with Mongrel2. |
|
624 ## If an action() proc is attached, run that to generate |
|
625 ## a response. Otherwise, run the default. |
|
626 while true: |
|
627 var |
|
628 request: M2Request |
|
629 response: M2Response |
|
630 info: string |
|
631 |
|
632 request = parse_request( handler.request_sock.receive ) # block, waiting for next request |
|
633 |
|
634 # Ignore disconnects unless there's a separate |
|
635 # disconnect_action. |
|
636 # |
|
637 if request.is_disconnect: |
|
638 if not isNil( handler.disconnect_action ): |
|
639 discard handler.disconnect_action |
|
640 continue |
|
641 |
|
642 # Defer regular response content to the handler action. |
|
643 # |
|
644 if isNil( handler.action ): |
|
645 handler.action = handle_default |
|
646 response = handler.action( request ) |
|
647 |
|
648 if response.status == 0: response.status = HTTP_OK |
|
649 |
|
650 if defined( testing ): |
|
651 echo "REQUEST:\n", repr(request) |
|
652 echo "RESPONSE:\n", repr(response) |
|
653 |
|
654 info = "$1 $2 $3" % [ |
|
655 request.remote_addr, |
|
656 request.meth, |
|
657 request.uri |
|
658 ] |
|
659 |
|
660 echo "$1: $2 --> $3 $4" % [ |
|
661 $(get_localtime(getTime())), |
|
662 info, |
|
663 $( response.status ), |
|
664 HTTPCODE[ response.status ].label |
|
665 ] |
|
666 |
|
667 handler.response_sock.send( response.format ) |
|
668 |
|
669 |
|
670 |
|
671 # |
|
672 # Tests! |
|
673 # |
|
674 when isMainModule: |
|
675 |
|
676 var reqstr = """host 33 /hosts 502:{"PATH":"/hosts","x-forwarded-for":"10.3.0.75","cache-control":"max-age=0","accept-language":"en-US,en;q=0.5","connection":"keep-alive","accept-encoding":"gzip, deflate","dnt":"1","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","user-agent":"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0","host":"hotsoup.sunset.laika.com:8080","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/hosts","PATTERN":"/hosts","URL_SCHEME":"http","REMOTE_ADDR":"10.3.0.75"},0:,""" |
|
677 |
|
678 var req = parse_request( reqstr ) |
|
679 var dreq = parse_request( """host 1234 @* 17:{"METHOD":"JSON"},21:{"type":"disconnect"},""" ) |
|
680 |
|
681 # Request parsing. |
|
682 # |
|
683 doAssert( req.sender_id == "host" ) |
|
684 doAssert( req.conn_id == "33" ) |
|
685 doAssert( req.path == "/hosts" ) |
|
686 doAssert( req.remote_addr == "10.3.0.75" ) |
|
687 doAssert( req.scheme == "http" ) |
|
688 doAssert( req.meth == "GET" ) |
|
689 doAssert( req["DNT"] == "1" ) |
|
690 doAssert( req["X-Forwarded-For"] == "10.3.0.75" ) |
|
691 |
|
692 doAssert( req.is_disconnect == false ) |
|
693 |
|
694 var res = req.response |
|
695 res.status = HTTP_OK |
|
696 |
|
697 doAssert( res.sender_id == req.sender_id ) |
|
698 doAssert( res.conn_id == req.conn_id ) |
|
699 |
|
700 # Response headers |
|
701 # |
|
702 res.add_header( "alright", "yep" ) |
|
703 res[ "something" ] = "nope" |
|
704 res[ "something" ] = "yep" |
|
705 doAssert( res["alright"] == "yep" ) |
|
706 doAssert( res["something"] == "yep" ) |
|
707 |
|
708 # Client broadcasts |
|
709 # |
|
710 res.broadcast([ 1, 2, 3, 4 ]) |
|
711 doAssert( res.conn_id == "1 2 3 4" ) |
|
712 doAssert( res.format == "host 7:1 2 3 4, HTTP/1.1 200 OK\r\nalright: yep\r\nsomething: yep\r\nContent-Length: 2\r\n\r\nOK" ) |
|
713 |
|
714 # Extended replies |
|
715 # |
|
716 doAssert( res.is_extended == false ) |
|
717 res.extend( "sendfile" ) |
|
718 doAssert( res.is_extended == true ) |
|
719 doAssert( res.format == "host 9:X 1 2 3 4, 11:8:sendfile,]" ) |
|
720 res.add_extended_data( "arg1", "arg2" ) |
|
721 res.add_extended_data( "arg3" ) |
|
722 doAssert( res.format == "host 9:X 1 2 3 4, 32:8:sendfile,4:arg1,4:arg2,4:arg3,]" ) |
|
723 |
|
724 doAssert( dreq.is_disconnect == true ) |
|
725 |
|
726 # Automatic body if none is specified |
|
727 # |
|
728 res.extended = nil |
|
729 res.body = nil |
|
730 res.status = HTTP_CREATED |
|
731 discard res.format |
|
732 doAssert( res.body == "Created" ) |
|
733 |
|
734 echo "* Tests passed!" |
|
735 |
|