0
|
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( "tcp://127.0.0.1:9009", PULL )
|
|
430 |
result.response_sock = zmq.connect( "tcp://127.0.0.1:9008", 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 |
|