# HG changeset patch # User Mahlon E. Smith # Date 1539027087 25200 # Node ID ffb8b9920057cad663e0f506fbb1a761a9af48a1 # Parent ecde1a3326921173fe569687940c5a9671cb8420 Re-arrange for use with nimble, updates for nim 0.19. diff -r ecde1a332692 -r ffb8b9920057 .hgignore --- a/.hgignore Tue Sep 15 11:28:40 2015 -0700 +++ b/.hgignore Mon Oct 08 12:31:27 2018 -0700 @@ -1,4 +1,6 @@ syntax: glob .cache -tnetstring +mongrel2 +handler +src/mongrel2 *.html diff -r ecde1a332692 -r ffb8b9920057 Makefile --- a/Makefile Tue Sep 15 11:28:40 2015 -0700 +++ b/Makefile Mon Oct 08 12:31:27 2018 -0700 @@ -1,20 +1,24 @@ -FILES = mongrel2.nim +FILES = src/mongrel2.nim default: development debug: ${FILES} nim --assertions:on --nimcache:.cache c ${FILES} + @mv src/mongrel2 . development: ${FILES} # can use gdb with this... nim -r --debugInfo --linedir:on --define:testing --nimcache:.cache c ${FILES} + @mv src/mongrel2 . debugger: ${FILES} nim --debugger:on --nimcache:.cache c ${FILES} + @mv src/mongrel2 . release: ${FILES} nim -d:release --opt:speed --nimcache:.cache c ${FILES} + @mv src/mongrel2 . docs: nim doc ${FILES} diff -r ecde1a332692 -r ffb8b9920057 handler.nim --- a/handler.nim Tue Sep 15 11:28:40 2015 -0700 +++ b/handler.nim Mon Oct 08 12:31:27 2018 -0700 @@ -1,67 +1,16 @@ -import - mongrel2, - json, - re - -let handler = newM2Handler( "app", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ) -var data = %*[] ## the JSON data to "remember" +import src/mongrel2 +let handler = newM2Handler( "mlist", "tcp://127.0.0.1:9019", "tcp://127.0.0.1:9018" ) -proc demo( request: M2Request ): M2Response = - ## This is a demonstraction handler action. - ## - ## It accepts and stores a JSON data structure - ## on POST, and returns it on GET. - - # Create a response object for the current request. +proc woo( request: M2Request ): M2Response = var response = request.response - case request.meth - - # For GET requests, display the current JSON structure. - # - of "GET": - if request[ "Accept" ].match( re(".*text/(html|plain).*") ): - response[ "Content-Type" ] = "text/plain" - response.body = "Hi there. POST some JSON to me and I'll remember it.\n\n" & $( data ) - response.status = HTTP_OK - - elif request[ "Accept" ].match( re("application/json") ): - response[ "Content-Type" ] = "application/json" - response.body = $( data ) - response.status = HTTP_OK - - else: - response.status = HTTP_BAD_REQUEST - - # Overwrite the current JSON structure. - # - of "POST": - if request[ "Content-Type" ].match( re(".*application/json.*") ): - try: - data = request.body.parse_json - response.status = HTTP_OK - response[ "Content-Type" ] = "application/json" - response.body = $( data ) - except: - response.status = HTTP_BAD_REQUEST - response[ "Content-Type" ] = "text/plain" - response.body = request.body - - else: - response.body = "I only accept valid JSON strings." - response.status = HTTP_BAD_REQUEST - - else: - response.status = HTTP_NOT_ACCEPTABLE - + response[ "Content-Type" ] = "text/plain" + response.body = "Hi there." + response.status = HTTP_OK return response - -# Attach the proc reference to the handler action. -handler.action = demo - -# Start 'er up! +# handler.action = woo handler.run diff -r ecde1a332692 -r ffb8b9920057 mongrel2.nim --- a/mongrel2.nim Tue Sep 15 11:28:40 2015 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,735 +0,0 @@ -# -# Copyright (c) 2015, Mahlon E. Smith -# All rights reserved. -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of Mahlon E. Smith nor the names of his -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -## This module is a simple interface to the Mongrel2 webserver -## environment (http://mongrel2.org/). After a Mongrel2 server has been -## properly configured, you can use this library to easily service client -## requests. -## -## Bare minimal, do-nothing example: -## -## .. code-block:: nim -## -## newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ).run -## -## Yep, that's it. Assuming your Mongrel2 server is configured with a -## matching application identifier and request/response communication -## ports, that's enough to see the default output from the handler when -## loaded in browser. -## -## It's likely that you'll want to do something of more substance. This -## is performed via an `action` hook, which is just a proc() reference. -## It is passed the parsed `M2Request` client request, and it needs to -## return a matching `M2Response` object. What happens in between is -## entirely up to you. -## -## Here's a "hello world": -## -## .. code-block:: nim -## -## let handler = newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ) -## -## proc hello_world( request: M2Request ): M2Response = -## result = request.response -## result[ "Content-Type" ] = "text/plain" -## result.body = "Hello there, world!" -## result.status = HTTP_OK -## -## handler.action = hello_world -## handler.run -## -## And finally, a slightly more interesting example: -## -## .. code-block:: nim -## -## import -## mongrel2, -## json, -## re -## -## let handler = newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ) -## var data = %*[] ## the JSON data to "remember" -## -## -## proc demo( request: M2Request ): M2Response = -## ## This is a demonstration handler action. -## ## -## ## It accepts and stores a JSON data structure -## ## on POST, and returns it on GET. -## -## # Create a response object for the current request. -## var response = request.response -## -## case request.meth -## -## # For GET requests, display the current JSON structure. -## # -## of "GET": -## if request[ "Accept" ].match( re(".*text/(html|plain).*") ): -## response[ "Content-Type" ] = "text/plain" -## response.body = "Hi there. POST some JSON to me and I'll remember it.\n\n" & $( data ) -## response.status = HTTP_OK -## -## elif request[ "Accept" ].match( re("application/json") ): -## response[ "Content-Type" ] = "application/json" -## response.body = $( data ) -## response.status = HTTP_OK -## -## else: -## response.status = HTTP_BAD_REQUEST -## -## # POST requests overwrite the current JSON structure. -## # -## of "POST": -## if request[ "Content-Type" ].match( re(".*application/json.*") ): -## try: -## data = request.body.parse_json -## response.status = HTTP_OK -## response[ "Content-Type" ] = "application/json" -## response.body = $( data ) -## except: -## response.status = HTTP_BAD_REQUEST -## response[ "Content-Type" ] = "text/plain" -## response.body = request.body -## -## else: -## response.body = "I only accept valid JSON strings." -## response.status = HTTP_BAD_REQUEST -## -## else: -## response.status = HTTP_NOT_ACCEPTABLE -## -## return response -## -## -## # Attach the proc reference to the handler action. -## handler.action = demo -## -## # Start 'er up! -## handler.run -## - - -import - json, - strutils, - tables, - times, - tnetstring, - zmq - -type - M2Handler* = ref object of RootObj - handler_id: string - request_sock: TConnection - response_sock: TConnection - action*: proc ( request: M2Request ): M2Response - disconnect_action*: proc ( request: M2Request ) - - M2Request* = ref object of RootObj - sender_id*: string ## Mongrel2 app-id - conn_id*: string - path*: string - meth*: string - version*: string - uri*: string - pattern*: string - scheme*: string - remote_addr*: string - query*: string - headers*: seq[ tuple[name: string, value: string] ] - body*: string - - M2Response* = ref object of RootObj - sender_id*: string - conn_id*: string - status*: int - headers*: seq[ tuple[name: string, value: string] ] - body*: string - extended: string - ex_data: seq[ string ] - -const - MAX_CLIENT_BROADCAST = 128 ## The maximum number of client to broadcast a response to - CRLF = "\r\n" ## Network line ending - DEFAULT_CONTENT = """ - - - - It works! - - - - -
-

Almost there...

-

- This is the default handler output. While this is - useful to demonstrate that your Mongrel2 - server is indeed speaking to your nim - handler, you're probably going to want to do something else for production use. -

-

- Here's an example handler: -

- -
-
-import
-    mongrel2,
-    json,
-    re
-
-let handler = newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" )
-var data    = %*[] ## the JSON data to "remember"
-
-
-proc demo( request: M2Request ): M2Response =
-    ## This is a demonstration handler action.
-    ##
-    ## It accepts and stores a JSON data structure
-    ## on POST, and returns it on GET.
-
-    # Create a response object for the current request.
-    var response = request.response
-
-    case request.meth
-
-    # For GET requests, display the current JSON structure.
-    #
-    of "GET":
-        if request[ "Accept" ].match( re(".*text/(html|plain).*") ):
-            response[ "Content-Type" ] = "text/plain"
-            response.body   = "Hi there.  POST some JSON to me and I'll remember it.\n\n" & $( data )
-            response.status = HTTP_OK
-
-        elif request[ "Accept" ].match( re("application/json") ):
-            response[ "Content-Type" ] = "application/json"
-            response.body   = $( data )
-            response.status = HTTP_OK
-
-        else:
-            response.status = HTTP_BAD_REQUEST
-
-    # Overwrite the current JSON structure.
-    #
-    of "POST":
-        if request[ "Content-Type" ].match( re(".*application/json.*") ):
-            try:
-                data = request.body.parse_json
-                response.status = HTTP_OK
-                response[ "Content-Type" ] = "application/json"
-                response.body = $( data )
-            except:
-                response.status = HTTP_BAD_REQUEST
-                response[ "Content-Type" ] = "text/plain"
-                response.body = request.body
-
-        else:
-            response.body   = "I only accept valid JSON strings."
-            response.status = HTTP_BAD_REQUEST
-
-    else:
-        response.status = HTTP_NOT_ACCEPTABLE
-
-    return response
-
-
-# Attach the proc reference to the handler action.
-handler.action = demo
-
-# Start 'er up!
-handler.run
-            
-
-
- - - """ - - HTTP_CONTINUE* = 100 - HTTP_SWITCHING_PROTOCOLS* = 101 - HTTP_PROCESSING* = 102 - HTTP_OK* = 200 - HTTP_CREATED* = 201 - HTTP_ACCEPTED* = 202 - HTTP_NON_AUTHORITATIVE* = 203 - HTTP_NO_CONTENT* = 204 - HTTP_RESET_CONTENT* = 205 - HTTP_PARTIAL_CONTENT* = 206 - HTTP_MULTI_STATUS* = 207 - HTTP_MULTIPLE_CHOICES* = 300 - HTTP_MOVED_PERMANENTLY* = 301 - HTTP_MOVED* = 301 - HTTP_MOVED_TEMPORARILY* = 302 - HTTP_REDIRECT* = 302 - HTTP_SEE_OTHER* = 303 - HTTP_NOT_MODIFIED* = 304 - HTTP_USE_PROXY* = 305 - HTTP_TEMPORARY_REDIRECT* = 307 - HTTP_BAD_REQUEST* = 400 - HTTP_AUTH_REQUIRED* = 401 - HTTP_UNAUTHORIZED* = 401 - HTTP_PAYMENT_REQUIRED* = 402 - HTTP_FORBIDDEN* = 403 - HTTP_NOT_FOUND* = 404 - HTTP_METHOD_NOT_ALLOWED* = 405 - HTTP_NOT_ACCEPTABLE* = 406 - HTTP_PROXY_AUTHENTICATION_REQUIRED* = 407 - HTTP_REQUEST_TIME_OUT* = 408 - HTTP_CONFLICT* = 409 - HTTP_GONE* = 410 - HTTP_LENGTH_REQUIRED* = 411 - HTTP_PRECONDITION_FAILED* = 412 - HTTP_REQUEST_ENTITY_TOO_LARGE* = 413 - HTTP_REQUEST_URI_TOO_LARGE* = 414 - HTTP_UNSUPPORTED_MEDIA_TYPE* = 415 - HTTP_RANGE_NOT_SATISFIABLE* = 416 - HTTP_EXPECTATION_FAILED* = 417 - HTTP_UNPROCESSABLE_ENTITY* = 422 - HTTP_LOCKED* = 423 - HTTP_FAILED_DEPENDENCY* = 424 - HTTP_UPGRADE_REQUIRED* = 426 - HTTP_RECONDITION_REQUIRED* = 428 - HTTP_TOO_MANY_REQUESTS* = 429 - HTTP_REQUEST_HEADERS_TOO_LARGE* = 431 - HTTP_SERVER_ERROR* = 500 - HTTP_NOT_IMPLEMENTED* = 501 - HTTP_BAD_GATEWAY* = 502 - HTTP_SERVICE_UNAVAILABLE* = 503 - HTTP_GATEWAY_TIME_OUT* = 504 - HTTP_VERSION_NOT_SUPPORTED* = 505 - HTTP_VARIANT_ALSO_VARIES* = 506 - HTTP_INSUFFICIENT_STORAGE* = 507 - HTTP_NOT_EXTENDED* = 510 - - HTTPCODE = { - HTTP_CONTINUE: ( desc: "Continue", label: "CONTINUE" ), - HTTP_SWITCHING_PROTOCOLS: ( desc: "Switching Protocols", label: "SWITCHING_PROTOCOLS" ), - HTTP_PROCESSING: ( desc: "Processing", label: "PROCESSING" ), - HTTP_OK: ( desc: "OK", label: "OK" ), - HTTP_CREATED: ( desc: "Created", label: "CREATED" ), - HTTP_ACCEPTED: ( desc: "Accepted", label: "ACCEPTED" ), - HTTP_NON_AUTHORITATIVE: ( desc: "Non-Authoritative Information", label: "NON_AUTHORITATIVE" ), - HTTP_NO_CONTENT: ( desc: "No Content", label: "NO_CONTENT" ), - HTTP_RESET_CONTENT: ( desc: "Reset Content", label: "RESET_CONTENT" ), - HTTP_PARTIAL_CONTENT: ( desc: "Partial Content", label: "PARTIAL_CONTENT" ), - HTTP_MULTI_STATUS: ( desc: "Multi-Status", label: "MULTI_STATUS" ), - HTTP_MULTIPLE_CHOICES: ( desc: "Multiple Choices", label: "MULTIPLE_CHOICES" ), - HTTP_MOVED_PERMANENTLY: ( desc: "Moved Permanently", label: "MOVED_PERMANENTLY" ), - HTTP_MOVED: ( desc: "Moved Permanently", label: "MOVED" ), - HTTP_MOVED_TEMPORARILY: ( desc: "Found", label: "MOVED_TEMPORARILY" ), - HTTP_REDIRECT: ( desc: "Found", label: "REDIRECT" ), - HTTP_SEE_OTHER: ( desc: "See Other", label: "SEE_OTHER" ), - HTTP_NOT_MODIFIED: ( desc: "Not Modified", label: "NOT_MODIFIED" ), - HTTP_USE_PROXY: ( desc: "Use Proxy", label: "USE_PROXY" ), - HTTP_TEMPORARY_REDIRECT: ( desc: "Temporary Redirect", label: "TEMPORARY_REDIRECT" ), - HTTP_BAD_REQUEST: ( desc: "Bad Request", label: "BAD_REQUEST" ), - HTTP_AUTH_REQUIRED: ( desc: "Authorization Required", label: "AUTH_REQUIRED" ), - HTTP_UNAUTHORIZED: ( desc: "Authorization Required", label: "UNAUTHORIZED" ), - HTTP_PAYMENT_REQUIRED: ( desc: "Payment Required", label: "PAYMENT_REQUIRED" ), - HTTP_FORBIDDEN: ( desc: "Forbidden", label: "FORBIDDEN" ), - HTTP_NOT_FOUND: ( desc: "Not Found", label: "NOT_FOUND" ), - HTTP_METHOD_NOT_ALLOWED: ( desc: "Method Not Allowed", label: "METHOD_NOT_ALLOWED" ), - HTTP_NOT_ACCEPTABLE: ( desc: "Not Acceptable", label: "NOT_ACCEPTABLE" ), - HTTP_PROXY_AUTHENTICATION_REQUIRED: ( desc: "Proxy Authentication Required", label: "PROXY_AUTHENTICATION_REQUIRED" ), - HTTP_REQUEST_TIME_OUT: ( desc: "Request Time-out", label: "REQUEST_TIME_OUT" ), - HTTP_CONFLICT: ( desc: "Conflict", label: "CONFLICT" ), - HTTP_GONE: ( desc: "Gone", label: "GONE" ), - HTTP_LENGTH_REQUIRED: ( desc: "Length Required", label: "LENGTH_REQUIRED" ), - HTTP_PRECONDITION_FAILED: ( desc: "Precondition Failed", label: "PRECONDITION_FAILED" ), - HTTP_REQUEST_ENTITY_TOO_LARGE: ( desc: "Request Entity Too Large", label: "REQUEST_ENTITY_TOO_LARGE" ), - HTTP_REQUEST_URI_TOO_LARGE: ( desc: "Request-URI Too Large", label: "REQUEST_URI_TOO_LARGE" ), - HTTP_UNSUPPORTED_MEDIA_TYPE: ( desc: "Unsupported Media Type", label: "UNSUPPORTED_MEDIA_TYPE" ), - HTTP_RANGE_NOT_SATISFIABLE: ( desc: "Requested Range Not Satisfiable", label: "RANGE_NOT_SATISFIABLE" ), - HTTP_EXPECTATION_FAILED: ( desc: "Expectation Failed", label: "EXPECTATION_FAILED" ), - HTTP_UNPROCESSABLE_ENTITY: ( desc: "Unprocessable Entity", label: "UNPROCESSABLE_ENTITY" ), - HTTP_LOCKED: ( desc: "Locked", label: "LOCKED" ), - HTTP_FAILED_DEPENDENCY: ( desc: "Failed Dependency", label: "FAILED_DEPENDENCY" ), - HTTP_UPGRADE_REQUIRED: ( desc: "Upgrade Required", label: "UPGRADE_REQUIRED" ), - HTTP_RECONDITION_REQUIRED: ( desc: "Precondition Required", label: "RECONDITION_REQUIRED" ), - HTTP_TOO_MANY_REQUESTS: ( desc: "Too Many Requests", label: "TOO_MANY_REQUESTS" ), - HTTP_REQUEST_HEADERS_TOO_LARGE: ( desc: "Request Headers too Large", label: "REQUEST_HEADERS_TOO_LARGE" ), - HTTP_SERVER_ERROR: ( desc: "Internal Server Error", label: "SERVER_ERROR" ), - HTTP_NOT_IMPLEMENTED: ( desc: "Method Not Implemented", label: "NOT_IMPLEMENTED" ), - HTTP_BAD_GATEWAY: ( desc: "Bad Gateway", label: "BAD_GATEWAY" ), - HTTP_SERVICE_UNAVAILABLE: ( desc: "Service Temporarily Unavailable", label: "SERVICE_UNAVAILABLE" ), - HTTP_GATEWAY_TIME_OUT: ( desc: "Gateway Time-out", label: "GATEWAY_TIME_OUT" ), - HTTP_VERSION_NOT_SUPPORTED: ( desc: "HTTP Version Not Supported", label: "VERSION_NOT_SUPPORTED" ), - HTTP_VARIANT_ALSO_VARIES: ( desc: "Variant Also Negotiates", label: "VARIANT_ALSO_VARIES" ), - HTTP_INSUFFICIENT_STORAGE: ( desc: "Insufficient Storage", label: "INSUFFICIENT_STORAGE" ), - HTTP_NOT_EXTENDED: ( desc: "Not Extended", label: "NOT_EXTENDED" ) - }.toTable - - - -proc newM2Handler*( id: string, req_sock: string, res_sock: string ): M2Handler = - ## Instantiate a new `M2Handler` object. - ## The `id` should match the app ID in the Mongrel2 config, - ## `req_sock` is the ZMQ::PULL socket Mongrel2 sends client request - ## data on, and `res_sock` is the ZMQ::PUB socket Mongrel2 subscribes - ## to. Nothing is put into action until the run() method is invoked. - new( result ) - result.handler_id = id - result.request_sock = zmq.connect( req_sock, PULL ) - result.response_sock = zmq.connect( res_sock, PUB ) - - -proc parse_request( request: string ): M2Request = - ## Parse a message `request` string received from Mongrel2, - ## return it as a M2Request object. - var - reqstr = request.split( ' ' ) - rest = "" - req_tnstr: TNetstringNode - headers: JsonNode - - new( result ) - result.sender_id = reqstr[ 0 ] - result.conn_id = reqstr[ 1 ] - result.path = reqstr[ 2 ] - - # There must be a better way to join() a seq... - # - for i in 3 .. reqstr.high: - rest = rest & reqstr[ i ] - if i < reqstr.high: rest = rest & ' ' - reqtnstr = rest.parse_tnetstring - result.body = reqtnstr.extra.parse_tnetstring.getStr("") - - # Pull Mongrel2 control headers into the request object. - # - headers = reqtnstr.getStr.parse_json - - if headers.has_key( "METHOD" ): - result.meth = headers[ "METHOD" ].getStr - headers.delete( "METHOD" ) - - if headers.has_key( "PATTERN" ): - result.pattern = headers[ "PATTERN" ].getStr - headers.delete( "PATTERN" ) - - if headers.has_key( "REMOTE_ADDR" ): - result.remote_addr = headers[ "REMOTE_ADDR" ].getStr - headers.delete( "REMOTE_ADDR" ) - - if headers.has_key( "URI" ): - result.uri = headers[ "URI" ].getStr - headers.delete( "URI" ) - - if headers.has_key( "URL_SCHEME" ): - result.scheme = headers[ "URL_SCHEME" ].getStr - headers.delete( "URL_SCHEME" ) - - if headers.has_key( "VERSION" ): - result.version = headers[ "VERSION" ].getStr - headers.delete( "VERSION" ) - - if headers.has_key( "QUERY" ): - result.query = headers[ "QUERY" ].getStr - headers.delete( "QUERY" ) - - # Remaining headers are client supplied. - # - result.headers = @[] - for key, val in headers: - result.headers.add( ( key, val.getStr ) ) - - -proc response*( request: M2Request ): M2Response = - ## Instantiate a new `M2Response`, paired from an `M2Request`. - new( result ) - result.sender_id = request.sender_id - result.conn_id = request.conn_id - result.headers = @[] - - -proc `[]`*( request: M2Request, label: string ): string = - ## Defer to the underlying headers tuple array. Lookup is case-insensitive. - for header in request.headers: - if cmpIgnoreCase( label, header.name ) == 0: - return header.value - return nil - - -proc is_disconnect*( request: M2Request ): bool = - ## Returns true if this is a Mongrel2 disconnect request. - if ( request.path == "@*" and request.meth == "JSON") : - var body = request.body.parseJson - if ( body.has_key( "type" ) and body[ "type" ].getStr == "disconnect" ): - return true - return false - - -proc `[]`*( response: M2Response, label: string ): string = - ## Defer to the underlying headers tuple array. Lookup is case-insensitive. - for header in response.headers: - if cmpIgnoreCase( label, header.name ) == 0: - return header.value - return nil - - -proc `[]=`*( response: M2Response, name: string, value: string ) = - ## Set a header on the response. Duplicates are replaced. - var new_headers: seq[ tuple[name: string, value: string] ] = @[] - for header in response.headers: - if cmpIgnoreCase( name, header.name ) != 0: - new_headers.add( header ) - response.headers = new_headers - response.headers.add( (name, value) ) - - -proc add_header*( response: M2Response, name: string, value: string ) = - ## Adds a header to the response. Duplicates are ignored. - response.headers.add( (name, value) ) - - -proc extend*( response: M2Response, filter: string ) = - ## Set a response as extended. This means different things depending on - ## the Mongrel2 filter in use. - response.extended = filter - - -proc add_extended_data*( response: M2Response, data: varargs[string, `$`] ) = - ## Attach filter arguments to the extended response. Arguments should - ## be coercible into strings. - if isNil( response.ex_data ): response.ex_data = @[] - for arg in data: - response.ex_data.add( arg ) - - -proc is_extended*( response: M2Response ): bool = - ## Predicate method to determine if a response is extended. - return not isNil( response.extended ) - - -proc broadcast*[T]( response: M2Response, ids: openarray[T] ) = - ## Send the response to multiple backend client IDs. - assert( ids.len <= MAX_CLIENT_BROADCAST, "Exceeded client broadcast maximum" ) - - response.conn_id = $( ids[0] ) - for i in 1 .. ids.high: - if i <= ids.high: response.conn_id = response.conn_id & ' ' - response.conn_id = response.conn_id & $( ids[i] ) - - -proc format( response: M2Response ): string = - ## Format an `M2Response` object for Mongrel2. - var conn_id: string - - # Mongrel2 extended response. - # - if response.is_extended: - conn_id = newTNetstringString( "X " & response.conn_id ).dump_tnetstring - result = response.sender_id & ' ' & conn_id - - # 1st argument is the filter name. - # - var tnet_array = newTNetstringArray() - tnet_array.add( newTNetstringString(response.extended) ) - - # rest are the filter arguments, if any. - # - if not isNil( response.ex_data ): - for data in response.ex_data: - tnet_array.add( newTNetstringString(data) ) - - result = result & ' ' & tnet_array.dump_tnetstring - - - else: - # Regular HTTP request/response cycle. - # - if isNil( response.body ): - response.body = HTTPCODE[ response.status ].desc - response[ "Content-Length" ] = $( response.body.len ) - else: - response[ "Content-Length" ] = $( response.body.len ) - - let code = "$1 $2" % [ $(response.status), HTTPCODE[response.status].label ] - conn_id = newTNetstringString( response.conn_id ).dump_tnetstring - result = response.sender_id & ' ' & conn_id - result = result & " HTTP/1.1 " & code & CRLF - - for header in response.headers: - result = result & header.name & ": " & header.value & CRLF - - result = result & CRLF & response.body - - -proc handle_default( request: M2Request ): M2Response = - ## This is the default handler, if the caller didn't install one. - result = request.response - result[ "Content-Type" ] = "text/html" - result.body = DEFAULT_CONTENT - - -proc run*( handler: M2Handler ) {. noreturn .} = - ## Enter the request loop conversation with Mongrel2. - ## If an action() proc is attached, run that to generate - ## a response. Otherwise, run the default. - while true: - var - request: M2Request - response: M2Response - info: string - - request = parse_request( handler.request_sock.receive ) # block, waiting for next request - - # Ignore disconnects unless there's a separate - # disconnect_action. - # - if request.is_disconnect: - if not isNil( handler.disconnect_action ): - discard handler.disconnect_action - continue - - # Defer regular response content to the handler action. - # - if isNil( handler.action ): - handler.action = handle_default - response = handler.action( request ) - - if response.status == 0: response.status = HTTP_OK - - if defined( testing ): - echo "REQUEST:\n", repr(request) - echo "RESPONSE:\n", repr(response) - - info = "$1 $2 $3" % [ - request.remote_addr, - request.meth, - request.uri - ] - - echo "$1: $2 --> $3 $4" % [ - $(get_localtime(getTime())), - info, - $( response.status ), - HTTPCODE[ response.status ].label - ] - - handler.response_sock.send( response.format ) - - - -# -# Tests! -# -when isMainModule: - - 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:,""" - - var req = parse_request( reqstr ) - var dreq = parse_request( """host 1234 @* 17:{"METHOD":"JSON"},21:{"type":"disconnect"},""" ) - - # Request parsing. - # - doAssert( req.sender_id == "host" ) - doAssert( req.conn_id == "33" ) - doAssert( req.path == "/hosts" ) - doAssert( req.remote_addr == "10.3.0.75" ) - doAssert( req.scheme == "http" ) - doAssert( req.meth == "GET" ) - doAssert( req["DNT"] == "1" ) - doAssert( req["X-Forwarded-For"] == "10.3.0.75" ) - - doAssert( req.is_disconnect == false ) - - var res = req.response - res.status = HTTP_OK - - doAssert( res.sender_id == req.sender_id ) - doAssert( res.conn_id == req.conn_id ) - - # Response headers - # - res.add_header( "alright", "yep" ) - res[ "something" ] = "nope" - res[ "something" ] = "yep" - doAssert( res["alright"] == "yep" ) - doAssert( res["something"] == "yep" ) - - # Client broadcasts - # - res.broadcast([ 1, 2, 3, 4 ]) - doAssert( res.conn_id == "1 2 3 4" ) - 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" ) - - # Extended replies - # - doAssert( res.is_extended == false ) - res.extend( "sendfile" ) - doAssert( res.is_extended == true ) - doAssert( res.format == "host 9:X 1 2 3 4, 11:8:sendfile,]" ) - res.add_extended_data( "arg1", "arg2" ) - res.add_extended_data( "arg3" ) - doAssert( res.format == "host 9:X 1 2 3 4, 32:8:sendfile,4:arg1,4:arg2,4:arg3,]" ) - - doAssert( dreq.is_disconnect == true ) - - # Automatic body if none is specified - # - res.extended = nil - res.body = nil - res.status = HTTP_CREATED - discard res.format - doAssert( res.body == "Created" ) - - echo "* Tests passed!" - diff -r ecde1a332692 -r ffb8b9920057 mongrel2.nimble --- a/mongrel2.nimble Tue Sep 15 11:28:40 2015 -0700 +++ b/mongrel2.nimble Mon Oct 08 12:31:27 2018 -0700 @@ -1,10 +1,17 @@ -[Package] -name = "mongrel2" -version = "0.1.0" + +# Package + +version = "0.1.1" author = "Mahlon E. Smith " description = "Simplistic handler framework for the Mongrel2 webserver." license = "MIT" +installExt = @["mongrel2"] +srcDir = "src" -[Deps] -Requires: "nim >= 0.11.0, tnetstring >= 0.1.1, zmq >= 0.2.1" + +# Dependencies +requires "nim >= 0.19.0" +requires "tnetstring >= 0.1.1" +requires "zmq >= 0.2.1" + diff -r ecde1a332692 -r ffb8b9920057 src/mongrel2.nim --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/mongrel2.nim Mon Oct 08 12:31:27 2018 -0700 @@ -0,0 +1,736 @@ +# +# Copyright (c) 2015-2018, Mahlon E. Smith +# All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of Mahlon E. Smith nor the names of his +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## This module is a simple interface to the Mongrel2 webserver +## environment (http://mongrel2.org/). After a Mongrel2 server has been +## properly configured, you can use this library to easily service client +## requests. +## +## Bare minimal, do-nothing example: +## +## .. code-block:: nim +## +## newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ).run +## +## Yep, that's it. Assuming your Mongrel2 server is configured with a +## matching application identifier and request/response communication +## ports, that's enough to see the default output from the handler when +## loaded in browser. +## +## It's likely that you'll want to do something of more substance. This +## is performed via an `action` hook, which is just a proc() reference. +## It is passed the parsed `M2Request` client request, and it needs to +## return a matching `M2Response` object. What happens in between is +## entirely up to you. +## +## Here's a "hello world": +## +## .. code-block:: nim +## +## let handler = newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ) +## +## proc hello_world( request: M2Request ): M2Response = +## result = request.response +## result[ "Content-Type" ] = "text/plain" +## result.body = "Hello there, world!" +## result.status = HTTP_OK +## +## handler.action = hello_world +## handler.run +## +## And finally, a slightly more interesting example: +## +## .. code-block:: nim +## +## import +## mongrel2, +## json, +## re +## +## let handler = newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" ) +## var data = %*[] ## the JSON data to "remember" +## +## +## proc demo( request: M2Request ): M2Response = +## ## This is a demonstration handler action. +## ## +## ## It accepts and stores a JSON data structure +## ## on POST, and returns it on GET. +## +## # Create a response object for the current request. +## var response = request.response +## +## case request.meth +## +## # For GET requests, display the current JSON structure. +## # +## of "GET": +## if request[ "Accept" ].match( re(".*text/(html|plain).*") ): +## response[ "Content-Type" ] = "text/plain" +## response.body = "Hi there. POST some JSON to me and I'll remember it.\n\n" & $( data ) +## response.status = HTTP_OK +## +## elif request[ "Accept" ].match( re("application/json") ): +## response[ "Content-Type" ] = "application/json" +## response.body = $( data ) +## response.status = HTTP_OK +## +## else: +## response.status = HTTP_BAD_REQUEST +## +## # POST requests overwrite the current JSON structure. +## # +## of "POST": +## if request[ "Content-Type" ].match( re(".*application/json.*") ): +## try: +## data = request.body.parse_json +## response.status = HTTP_OK +## response[ "Content-Type" ] = "application/json" +## response.body = $( data ) +## except: +## response.status = HTTP_BAD_REQUEST +## response[ "Content-Type" ] = "text/plain" +## response.body = request.body +## +## else: +## response.body = "I only accept valid JSON strings." +## response.status = HTTP_BAD_REQUEST +## +## else: +## response.status = HTTP_NOT_ACCEPTABLE +## +## return response +## +## +## # Attach the proc reference to the handler action. +## handler.action = demo +## +## # Start 'er up! +## handler.run +## + + +import + json, + strutils, + tables, + times, + tnetstring, + zmq + +type + M2Handler* = ref object of RootObj + handler_id: string + request_sock: TConnection + response_sock: TConnection + action*: proc ( request: M2Request ): M2Response + disconnect_action*: proc ( request: M2Request ) + + M2Request* = ref object of RootObj + sender_id*: string ## Mongrel2 app-id + conn_id*: string + path*: string + meth*: string + version*: string + uri*: string + pattern*: string + scheme*: string + remote_addr*: string + query*: string + headers*: seq[ tuple[name: string, value: string] ] + body*: string + + M2Response* = ref object of RootObj + sender_id*: string + conn_id*: string + status*: int + headers*: seq[ tuple[name: string, value: string] ] + body*: string + extended: string + ex_data: seq[ string ] + +const + MAX_CLIENT_BROADCAST = 128 ## The maximum number of client to broadcast a response to + CRLF = "\r\n" ## Network line ending + DEFAULT_CONTENT = """ + + + + It works! + + + + +
+

Almost there...

+

+ This is the default handler output. While this is + useful to demonstrate that your Mongrel2 + server is indeed speaking to your nim + handler, you're probably going to want to do something else for production use. +

+

+ Here's an example handler: +

+ +
+
+import
+    mongrel2,
+    json,
+    re
+
+let handler = newM2Handler( "app-id", "tcp://127.0.0.1:9009", "tcp://127.0.0.1:9008" )
+var data    = %*[] ## the JSON data to "remember"
+
+
+proc demo( request: M2Request ): M2Response =
+    ## This is a demonstration handler action.
+    ##
+    ## It accepts and stores a JSON data structure
+    ## on POST, and returns it on GET.
+
+    # Create a response object for the current request.
+    var response = request.response
+
+    case request.meth
+
+    # For GET requests, display the current JSON structure.
+    #
+    of "GET":
+        if request[ "Accept" ].match( re(".*text/(html|plain).*") ):
+            response[ "Content-Type" ] = "text/plain"
+            response.body   = "Hi there.  POST some JSON to me and I'll remember it.\n\n" & $( data )
+            response.status = HTTP_OK
+
+        elif request[ "Accept" ].match( re("application/json") ):
+            response[ "Content-Type" ] = "application/json"
+            response.body   = $( data )
+            response.status = HTTP_OK
+
+        else:
+            response.status = HTTP_BAD_REQUEST
+
+    # Overwrite the current JSON structure.
+    #
+    of "POST":
+        if request[ "Content-Type" ].match( re(".*application/json.*") ):
+            try:
+                data = request.body.parse_json
+                response.status = HTTP_OK
+                response[ "Content-Type" ] = "application/json"
+                response.body = $( data )
+            except:
+                response.status = HTTP_BAD_REQUEST
+                response[ "Content-Type" ] = "text/plain"
+                response.body = request.body
+
+        else:
+            response.body   = "I only accept valid JSON strings."
+            response.status = HTTP_BAD_REQUEST
+
+    else:
+        response.status = HTTP_NOT_ACCEPTABLE
+
+    return response
+
+
+# Attach the proc reference to the handler action.
+handler.action = demo
+
+# Start 'er up!
+handler.run
+            
+
+
+ + + """ + + HTTP_CONTINUE* = 100 + HTTP_SWITCHING_PROTOCOLS* = 101 + HTTP_PROCESSING* = 102 + HTTP_OK* = 200 + HTTP_CREATED* = 201 + HTTP_ACCEPTED* = 202 + HTTP_NON_AUTHORITATIVE* = 203 + HTTP_NO_CONTENT* = 204 + HTTP_RESET_CONTENT* = 205 + HTTP_PARTIAL_CONTENT* = 206 + HTTP_MULTI_STATUS* = 207 + HTTP_MULTIPLE_CHOICES* = 300 + HTTP_MOVED_PERMANENTLY* = 301 + HTTP_MOVED* = 301 + HTTP_MOVED_TEMPORARILY* = 302 + HTTP_REDIRECT* = 302 + HTTP_SEE_OTHER* = 303 + HTTP_NOT_MODIFIED* = 304 + HTTP_USE_PROXY* = 305 + HTTP_TEMPORARY_REDIRECT* = 307 + HTTP_BAD_REQUEST* = 400 + HTTP_AUTH_REQUIRED* = 401 + HTTP_UNAUTHORIZED* = 401 + HTTP_PAYMENT_REQUIRED* = 402 + HTTP_FORBIDDEN* = 403 + HTTP_NOT_FOUND* = 404 + HTTP_METHOD_NOT_ALLOWED* = 405 + HTTP_NOT_ACCEPTABLE* = 406 + HTTP_PROXY_AUTHENTICATION_REQUIRED* = 407 + HTTP_REQUEST_TIME_OUT* = 408 + HTTP_CONFLICT* = 409 + HTTP_GONE* = 410 + HTTP_LENGTH_REQUIRED* = 411 + HTTP_PRECONDITION_FAILED* = 412 + HTTP_REQUEST_ENTITY_TOO_LARGE* = 413 + HTTP_REQUEST_URI_TOO_LARGE* = 414 + HTTP_UNSUPPORTED_MEDIA_TYPE* = 415 + HTTP_RANGE_NOT_SATISFIABLE* = 416 + HTTP_EXPECTATION_FAILED* = 417 + HTTP_UNPROCESSABLE_ENTITY* = 422 + HTTP_LOCKED* = 423 + HTTP_FAILED_DEPENDENCY* = 424 + HTTP_UPGRADE_REQUIRED* = 426 + HTTP_RECONDITION_REQUIRED* = 428 + HTTP_TOO_MANY_REQUESTS* = 429 + HTTP_REQUEST_HEADERS_TOO_LARGE* = 431 + HTTP_SERVER_ERROR* = 500 + HTTP_NOT_IMPLEMENTED* = 501 + HTTP_BAD_GATEWAY* = 502 + HTTP_SERVICE_UNAVAILABLE* = 503 + HTTP_GATEWAY_TIME_OUT* = 504 + HTTP_VERSION_NOT_SUPPORTED* = 505 + HTTP_VARIANT_ALSO_VARIES* = 506 + HTTP_INSUFFICIENT_STORAGE* = 507 + HTTP_NOT_EXTENDED* = 510 + + HTTPCODE = { + HTTP_CONTINUE: ( desc: "Continue", label: "CONTINUE" ), + HTTP_SWITCHING_PROTOCOLS: ( desc: "Switching Protocols", label: "SWITCHING_PROTOCOLS" ), + HTTP_PROCESSING: ( desc: "Processing", label: "PROCESSING" ), + HTTP_OK: ( desc: "OK", label: "OK" ), + HTTP_CREATED: ( desc: "Created", label: "CREATED" ), + HTTP_ACCEPTED: ( desc: "Accepted", label: "ACCEPTED" ), + HTTP_NON_AUTHORITATIVE: ( desc: "Non-Authoritative Information", label: "NON_AUTHORITATIVE" ), + HTTP_NO_CONTENT: ( desc: "No Content", label: "NO_CONTENT" ), + HTTP_RESET_CONTENT: ( desc: "Reset Content", label: "RESET_CONTENT" ), + HTTP_PARTIAL_CONTENT: ( desc: "Partial Content", label: "PARTIAL_CONTENT" ), + HTTP_MULTI_STATUS: ( desc: "Multi-Status", label: "MULTI_STATUS" ), + HTTP_MULTIPLE_CHOICES: ( desc: "Multiple Choices", label: "MULTIPLE_CHOICES" ), + HTTP_MOVED_PERMANENTLY: ( desc: "Moved Permanently", label: "MOVED_PERMANENTLY" ), + HTTP_MOVED: ( desc: "Moved Permanently", label: "MOVED" ), + HTTP_MOVED_TEMPORARILY: ( desc: "Found", label: "MOVED_TEMPORARILY" ), + HTTP_REDIRECT: ( desc: "Found", label: "REDIRECT" ), + HTTP_SEE_OTHER: ( desc: "See Other", label: "SEE_OTHER" ), + HTTP_NOT_MODIFIED: ( desc: "Not Modified", label: "NOT_MODIFIED" ), + HTTP_USE_PROXY: ( desc: "Use Proxy", label: "USE_PROXY" ), + HTTP_TEMPORARY_REDIRECT: ( desc: "Temporary Redirect", label: "TEMPORARY_REDIRECT" ), + HTTP_BAD_REQUEST: ( desc: "Bad Request", label: "BAD_REQUEST" ), + HTTP_AUTH_REQUIRED: ( desc: "Authorization Required", label: "AUTH_REQUIRED" ), + HTTP_UNAUTHORIZED: ( desc: "Authorization Required", label: "UNAUTHORIZED" ), + HTTP_PAYMENT_REQUIRED: ( desc: "Payment Required", label: "PAYMENT_REQUIRED" ), + HTTP_FORBIDDEN: ( desc: "Forbidden", label: "FORBIDDEN" ), + HTTP_NOT_FOUND: ( desc: "Not Found", label: "NOT_FOUND" ), + HTTP_METHOD_NOT_ALLOWED: ( desc: "Method Not Allowed", label: "METHOD_NOT_ALLOWED" ), + HTTP_NOT_ACCEPTABLE: ( desc: "Not Acceptable", label: "NOT_ACCEPTABLE" ), + HTTP_PROXY_AUTHENTICATION_REQUIRED: ( desc: "Proxy Authentication Required", label: "PROXY_AUTHENTICATION_REQUIRED" ), + HTTP_REQUEST_TIME_OUT: ( desc: "Request Time-out", label: "REQUEST_TIME_OUT" ), + HTTP_CONFLICT: ( desc: "Conflict", label: "CONFLICT" ), + HTTP_GONE: ( desc: "Gone", label: "GONE" ), + HTTP_LENGTH_REQUIRED: ( desc: "Length Required", label: "LENGTH_REQUIRED" ), + HTTP_PRECONDITION_FAILED: ( desc: "Precondition Failed", label: "PRECONDITION_FAILED" ), + HTTP_REQUEST_ENTITY_TOO_LARGE: ( desc: "Request Entity Too Large", label: "REQUEST_ENTITY_TOO_LARGE" ), + HTTP_REQUEST_URI_TOO_LARGE: ( desc: "Request-URI Too Large", label: "REQUEST_URI_TOO_LARGE" ), + HTTP_UNSUPPORTED_MEDIA_TYPE: ( desc: "Unsupported Media Type", label: "UNSUPPORTED_MEDIA_TYPE" ), + HTTP_RANGE_NOT_SATISFIABLE: ( desc: "Requested Range Not Satisfiable", label: "RANGE_NOT_SATISFIABLE" ), + HTTP_EXPECTATION_FAILED: ( desc: "Expectation Failed", label: "EXPECTATION_FAILED" ), + HTTP_UNPROCESSABLE_ENTITY: ( desc: "Unprocessable Entity", label: "UNPROCESSABLE_ENTITY" ), + HTTP_LOCKED: ( desc: "Locked", label: "LOCKED" ), + HTTP_FAILED_DEPENDENCY: ( desc: "Failed Dependency", label: "FAILED_DEPENDENCY" ), + HTTP_UPGRADE_REQUIRED: ( desc: "Upgrade Required", label: "UPGRADE_REQUIRED" ), + HTTP_RECONDITION_REQUIRED: ( desc: "Precondition Required", label: "RECONDITION_REQUIRED" ), + HTTP_TOO_MANY_REQUESTS: ( desc: "Too Many Requests", label: "TOO_MANY_REQUESTS" ), + HTTP_REQUEST_HEADERS_TOO_LARGE: ( desc: "Request Headers too Large", label: "REQUEST_HEADERS_TOO_LARGE" ), + HTTP_SERVER_ERROR: ( desc: "Internal Server Error", label: "SERVER_ERROR" ), + HTTP_NOT_IMPLEMENTED: ( desc: "Method Not Implemented", label: "NOT_IMPLEMENTED" ), + HTTP_BAD_GATEWAY: ( desc: "Bad Gateway", label: "BAD_GATEWAY" ), + HTTP_SERVICE_UNAVAILABLE: ( desc: "Service Temporarily Unavailable", label: "SERVICE_UNAVAILABLE" ), + HTTP_GATEWAY_TIME_OUT: ( desc: "Gateway Time-out", label: "GATEWAY_TIME_OUT" ), + HTTP_VERSION_NOT_SUPPORTED: ( desc: "HTTP Version Not Supported", label: "VERSION_NOT_SUPPORTED" ), + HTTP_VARIANT_ALSO_VARIES: ( desc: "Variant Also Negotiates", label: "VARIANT_ALSO_VARIES" ), + HTTP_INSUFFICIENT_STORAGE: ( desc: "Insufficient Storage", label: "INSUFFICIENT_STORAGE" ), + HTTP_NOT_EXTENDED: ( desc: "Not Extended", label: "NOT_EXTENDED" ) + }.toTable + + + +proc newM2Handler*( id: string, req_sock: string, res_sock: string ): M2Handler = + ## Instantiate a new `M2Handler` object. + ## The `id` should match the app ID in the Mongrel2 config, + ## `req_sock` is the ZMQ::PULL socket Mongrel2 sends client request + ## data on, and `res_sock` is the ZMQ::PUB socket Mongrel2 subscribes + ## to. Nothing is put into action until the run() method is invoked. + new( result ) + result.handler_id = id + result.request_sock = zmq.connect( req_sock, PULL ) + result.response_sock = zmq.connect( res_sock, PUB ) + + +proc parse_request( request: string ): M2Request = + ## Parse a message `request` string received from Mongrel2, + ## return it as a M2Request object. + var + reqstr = request.split( ' ' ) + rest = "" + req_tnstr: TNetstringNode + headers: JsonNode + + new( result ) + result.sender_id = reqstr[ 0 ] + result.conn_id = reqstr[ 1 ] + result.path = reqstr[ 2 ] + + # There must be a better way to join() a seq... + # + for i in 3 .. reqstr.high: + rest = rest & reqstr[ i ] + if i < reqstr.high: rest = rest & ' ' + reqtnstr = rest.parse_tnetstring + result.body = reqtnstr.extra.parse_tnetstring.getStr("") + + # Pull Mongrel2 control headers into the request object. + # + headers = reqtnstr.getStr.parse_json + + if headers.has_key( "METHOD" ): + result.meth = headers[ "METHOD" ].getStr + headers.delete( "METHOD" ) + + if headers.has_key( "PATTERN" ): + result.pattern = headers[ "PATTERN" ].getStr + headers.delete( "PATTERN" ) + + if headers.has_key( "REMOTE_ADDR" ): + result.remote_addr = headers[ "REMOTE_ADDR" ].getStr + headers.delete( "REMOTE_ADDR" ) + + if headers.has_key( "URI" ): + result.uri = headers[ "URI" ].getStr + headers.delete( "URI" ) + + if headers.has_key( "URL_SCHEME" ): + result.scheme = headers[ "URL_SCHEME" ].getStr + headers.delete( "URL_SCHEME" ) + + if headers.has_key( "VERSION" ): + result.version = headers[ "VERSION" ].getStr + headers.delete( "VERSION" ) + + if headers.has_key( "QUERY" ): + result.query = headers[ "QUERY" ].getStr + headers.delete( "QUERY" ) + + # Remaining headers are client supplied. + # + result.headers = @[] + for key, val in headers: + result.headers.add( ( key, val.getStr ) ) + + +proc response*( request: M2Request ): M2Response = + ## Instantiate a new `M2Response`, paired from an `M2Request`. + new( result ) + result.sender_id = request.sender_id + result.conn_id = request.conn_id + result.headers = @[] + + +proc `[]`*( request: M2Request, label: string ): string = + ## Defer to the underlying headers tuple array. Lookup is case-insensitive. + for header in request.headers: + if cmpIgnoreCase( label, header.name ) == 0: + return header.value + return "" + + +proc is_disconnect*( request: M2Request ): bool = + ## Returns true if this is a Mongrel2 disconnect request. + if ( request.path == "@*" and request.meth == "JSON") : + var body = request.body.parseJson + if ( body.has_key( "type" ) and body[ "type" ].getStr == "disconnect" ): + return true + return false + + +proc `[]`*( response: M2Response, label: string ): string = + ## Defer to the underlying headers tuple array. Lookup is case-insensitive. + for header in response.headers: + if cmpIgnoreCase( label, header.name ) == 0: + return header.value + return "" + + +proc `[]=`*( response: M2Response, name: string, value: string ) = + ## Set a header on the response. Duplicates are replaced. + var new_headers: seq[ tuple[name: string, value: string] ] = @[] + for header in response.headers: + if cmpIgnoreCase( name, header.name ) != 0: + new_headers.add( header ) + response.headers = new_headers + response.headers.add( (name, value) ) + + +proc add_header*( response: M2Response, name: string, value: string ) = + ## Adds a header to the response. Duplicates are ignored. + response.headers.add( (name, value) ) + + +proc extend*( response: M2Response, filter: string ) = + ## Set a response as extended. This means different things depending on + ## the Mongrel2 filter in use. + response.extended = filter + + +proc add_extended_data*( response: M2Response, data: varargs[string, `$`] ) = + ## Attach filter arguments to the extended response. Arguments should + ## be coercible into strings. + # if isNil( response.ex_data ): response.ex_data = @[] + # response.ex_data = @[] + for arg in data: + response.ex_data.add( arg ) + + +proc is_extended*( response: M2Response ): bool = + ## Predicate method to determine if a response is extended. + return not ( response.extended == "" ) + + +proc broadcast*[T]( response: M2Response, ids: openarray[T] ) = + ## Send the response to multiple backend client IDs. + assert( ids.len <= MAX_CLIENT_BROADCAST, "Exceeded client broadcast maximum" ) + + response.conn_id = $( ids[0] ) + for i in 1 .. ids.high: + if i <= ids.high: response.conn_id = response.conn_id & ' ' + response.conn_id = response.conn_id & $( ids[i] ) + + +proc format( response: M2Response ): string = + ## Format an `M2Response` object for Mongrel2. + var conn_id: string + + # Mongrel2 extended response. + # + if response.is_extended: + conn_id = newTNetstringString( "X " & response.conn_id ).dump_tnetstring + result = response.sender_id & ' ' & conn_id + + # 1st argument is the filter name. + # + var tnet_array = newTNetstringArray() + tnet_array.add( newTNetstringString(response.extended) ) + + # rest are the filter arguments, if any. + # + if response.is_extended: + for data in response.ex_data: + tnet_array.add( newTNetstringString(data) ) + + result = result & ' ' & tnet_array.dump_tnetstring + + + else: + # Regular HTTP request/response cycle. + # + if response.body == "": + response.body = HTTPCODE[ response.status ].desc + response[ "Content-Length" ] = $( response.body.len ) + else: + response[ "Content-Length" ] = $( response.body.len ) + + let code = "$1 $2" % [ $(response.status), HTTPCODE[response.status].label ] + conn_id = newTNetstringString( response.conn_id ).dump_tnetstring + result = response.sender_id & ' ' & conn_id + result = result & " HTTP/1.1 " & code & CRLF + + for header in response.headers: + result = result & header.name & ": " & header.value & CRLF + + result = result & CRLF & response.body + + +proc handle_default( request: M2Request ): M2Response = + ## This is the default handler, if the caller didn't install one. + result = request.response + result[ "Content-Type" ] = "text/html" + result.body = DEFAULT_CONTENT + + +proc run*( handler: M2Handler ) {. noreturn .} = + ## Enter the request loop conversation with Mongrel2. + ## If an action() proc is attached, run that to generate + ## a response. Otherwise, run the default. + while true: + var + request: M2Request + response: M2Response + info: string + + request = parse_request( handler.request_sock.receive ) # block, waiting for next request + + # Ignore disconnects unless there's a separate + # disconnect_action. + # + if request.is_disconnect: + if not isNil( handler.disconnect_action ): + discard handler.disconnect_action + continue + + # Defer regular response content to the handler action. + # + if isNil( handler.action ): + handler.action = handle_default + response = handler.action( request ) + + if response.status == 0: response.status = HTTP_OK + + if defined( testing ): + echo "REQUEST:\n", repr(request) + echo "RESPONSE:\n", repr(response) + + info = "$1 $2 $3" % [ + request.remote_addr, + request.meth, + request.uri + ] + + echo "$1: $2 --> $3 $4" % [ + $(get_localtime(getTime())), + info, + $( response.status ), + HTTPCODE[ response.status ].label + ] + + handler.response_sock.send( response.format ) + + + +# +# Tests! +# +when isMainModule: + + 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:,""" + + var req = parse_request( reqstr ) + var dreq = parse_request( """host 1234 @* 17:{"METHOD":"JSON"},21:{"type":"disconnect"},""" ) + + # Request parsing. + # + doAssert( req.sender_id == "host" ) + doAssert( req.conn_id == "33" ) + doAssert( req.path == "/hosts" ) + doAssert( req.remote_addr == "10.3.0.75" ) + doAssert( req.scheme == "http" ) + doAssert( req.meth == "GET" ) + doAssert( req["DNT"] == "1" ) + doAssert( req["X-Forwarded-For"] == "10.3.0.75" ) + + doAssert( req.is_disconnect == false ) + + var res = req.response + res.status = HTTP_OK + + doAssert( res.sender_id == req.sender_id ) + doAssert( res.conn_id == req.conn_id ) + + # Response headers + # + res.add_header( "alright", "yep" ) + res[ "something" ] = "nope" + res[ "something" ] = "yep" + doAssert( res["alright"] == "yep" ) + doAssert( res["something"] == "yep" ) + + # Client broadcasts + # + res.broadcast([ 1, 2, 3, 4 ]) + doAssert( res.conn_id == "1 2 3 4" ) + 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" ) + + # Extended replies + # + doAssert( res.is_extended == false ) + res.extend( "sendfile" ) + doAssert( res.is_extended == true ) + doAssert( res.format == "host 9:X 1 2 3 4, 11:8:sendfile,]" ) + res.add_extended_data( "arg1", "arg2" ) + res.add_extended_data( "arg3" ) + doAssert( res.format == "host 9:X 1 2 3 4, 32:8:sendfile,4:arg1,4:arg2,4:arg3,]" ) + + doAssert( dreq.is_disconnect == true ) + + # Automatic body if none is specified + # + res.extended = "" + res.body = "" + res.status = HTTP_CREATED + discard res.format + doAssert( res.body == "Created" ) + + echo "* Tests passed!" +