mongrel2.nim
changeset 0 f480e159f575
child 3 ecde1a332692
equal deleted inserted replaced
-1:000000000000 0:f480e159f575
       
     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