moonbridge
view moonbridge_http.lua @ 167:625ab06babe9
Use old system for GET/POST param tables in new HTTP module implementation
| author | jbe | 
|---|---|
| date | Mon Jun 15 00:07:08 2015 +0200 (2015-06-15) | 
| parents | 41a44ae5c293 | 
| children | d4f5d6a7d401 | 
 line source
     1 #!/usr/bin/env lua
     3 -- module preamble
     4 local _G, _M = _ENV, {}
     5 _ENV = setmetatable({}, {
     6   __index = function(self, key)
     7     local value = _M[key]; if value ~= nil then return value end
     8     return _G[key]
     9   end,
    10   __newindex = _M
    11 })
    13 -- function that encodes certain HTML entities:
    14 -- (not used by the library itself)
    15 function encode_html(text)
    16   return (
    17     string.gsub(
    18       text, '[<>&"]',
    19       function(char)
    20         if char == '<' then
    21           return "<"
    22         elseif char == '>' then
    23           return ">"
    24         elseif char == '&' then
    25           return "&"
    26         elseif char == '"' then
    27           return """
    28         end
    29       end
    30     )
    31   )
    33 end
    35 -- function that encodes special characters for URIs:
    36 -- (not used by the library itself)
    37 function encode_uri(text)
    38   return (
    39     string.gsub(text, "[^0-9A-Za-z_%.~-]",
    40       function (char)
    41         return string.format("%%%02x", string.byte(char))
    42       end
    43     )
    44   )
    45 end
    47 -- function undoing URL encoding:
    48 do
    49   local b0 = string.byte("0")
    50   local b9 = string.byte("9")
    51   local bA = string.byte("A")
    52   local bF = string.byte("F")
    53   local ba = string.byte("a")
    54   local bf = string.byte("f")
    55   function decode_uri(str)
    56     return (
    57       string.gsub(
    58         string.gsub(str, "%+", " "),
    59         "%%([0-9A-Fa-f][0-9A-Fa-f])",
    60         function(hex)
    61           local n1, n2 = string.byte(hex, 1, 2)
    62           if n1 >= b0 and n1 <= b9 then n1 = n1 - b0
    63           elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10
    64           elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10
    65           else error("Assertion failed") end
    66           if n2 >= b0 and n2 <= b9 then n2 = n2 - b0
    67           elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10
    68           elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10
    69           else error("Assertion failed") end
    70           return string.char(n1 * 16 + n2)
    71         end
    72       )
    73     )
    74   end
    75 end
    77 -- status codes that carry no response body (in addition to 1xx):
    78 -- (set to "zero_content_length" if Content-Length header is required)
    79 status_without_response_body = {
    80   ["101"] = true,  -- list 101 to allow protocol switch
    81   ["204"] = true,
    82   ["205"] = "zero_content_length",
    83   ["304"] = true
    84 }
    86 -- handling of GET/POST param tables:
    87 local new_params_list  -- defined later
    88 do
    89   local params_list_mapping = setmetatable({}, {__mode="k"})
    90   local function nextnonempty(tbl, key)
    91     while true do
    92       key = next(tbl, key)
    93       if key == nil then
    94         return nil
    95       end
    96       local value = tbl[key]
    97       if #value > 0 then
    98         return key, value
    99       end
   100     end
   101   end
   102   local function nextvalue(tbl, key)
   103     key = next(tbl, key)
   104     if key == nil then
   105       return nil
   106     end
   107     return key, tbl[key][1]
   108   end
   109   local params_list_metatable = {
   110     __index = function(self, key)
   111       local tbl = {}
   112       self[key] = tbl
   113       return tbl
   114     end,
   115     __pairs = function(self)
   116       return nextnonempty, self, nil
   117     end
   118   }
   119   local params_metatable = {
   120     __index = function(self, key)
   121       return params_list_mapping[self][key][1]
   122     end,
   123     __newindex = function(self, key, value)
   124       params_list_mapping[self][key] = {value}
   125     end,
   126     __pairs = function(self)
   127       return nextvalue, params_list_mapping[self], nil
   128     end
   129   }
   130   -- returns a table to store key value-list pairs (i.e. multiple values),
   131   -- and a second table automatically mapping keys to the first value
   132   -- using the key value-list pairs in the first table:
   133   new_params_list = function()
   134     local params_list = setmetatable({}, params_list_metatable)
   135     local params = setmetatable({}, params_metatable)
   136     params_list_mapping[params] = params_list
   137     return params_list, params
   138   end
   139 end
   141 -- parses URL encoded form data and stores it in
   142 -- a key value-list pairs structure that has to be
   143 -- previously obtained by calling by new_params_list():
   144 local function read_urlencoded_form(tbl, data)
   145   for rawkey, rawvalue in string.gmatch(data, "([^?=&]*)=([^?=&]*)") do
   146     local subtbl = tbl[decode_uri(rawkey)]
   147     subtbl[#subtbl+1] = decode_uri(rawvalue)
   148   end
   149 end
   151 -- extracts first value from each subtable:
   152 local function get_first_values(tbl)
   153   local newtbl = {}
   154   for key, subtbl in pairs(tbl) do
   155     newtbl[key] = subtbl[1]
   156   end
   157   return newtbl
   158 end
   160 function generate_handler(handler, options)
   161   -- swap arguments if necessary (for convenience):
   162   if type(handler) ~= "function" and type(options) == "function" then
   163     handler, options = options, handler
   164   end
   165   -- helper function to process options:
   166   local function default(name, default_value)
   167     local value = options[name]
   168     if value == nil then
   169       return default_value
   170     else
   171       return value or nil
   172     end
   173   end
   174   -- process options:
   175   options = options or {}
   176   local preamble = ""  -- preamble sent with every(!) HTTP response
   177   do
   178     -- named arg "static_headers" is used to create the preamble:
   179     local s = options.static_headers
   180     local t = {}
   181     if s then
   182       if type(s) == "string" then
   183         for line in string.gmatch(s, "[^\r\n]+") do
   184           t[#t+1] = line
   185         end
   186       else
   187         for i, kv in ipairs(options.static_headers) do
   188           if type(kv) == "string" then
   189             t[#t+1] = kv
   190           else
   191             t[#t+1] = kv[1] .. ": " .. kv[2]
   192           end
   193         end
   194       end
   195     end
   196     t[#t+1] = ""
   197     preamble = table.concat(t, "\r\n")
   198   end
   199   local input_chunk_size  = options.maximum_input_chunk_size  or options.chunk_size or 16384
   200   local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024
   201   local header_size_limit = options.header_size_limit or 1024*1024
   202   local body_size_limit   = options.body_size_limit   or 64*1024*1024
   203   local request_idle_timeout     = default("request_idle_timeout", 330)
   204   local request_header_timeout   = default("request_header_timeout", 30)
   205   local request_body_timeout     = default("request_body_timeout", 60)
   206   local request_response_timeout = default("request_response_timeout", 1800)
   207   local poll = options.poll_function or moonbridge_io.poll
   208   -- return socket handler:
   209   return function(socket)
   210     local socket_set = {[socket] = true}  -- used for poll function
   211     local survive = true  -- set to false if process shall be terminated later
   212     local consume  -- function that reads some input if possible
   213     -- function that drains some input if possible:
   214     local function drain()
   215       local bytes, status = socket:drain_nb(input_chunk_size)
   216       if not bytes or status == "eof" then
   217         consume = nil
   218       end
   219     end
   220     -- function trying to unblock socket by reading:
   221     local function unblock()
   222       if consume then
   223         poll(socket_set, socket_set)
   224         consume()
   225       else
   226         poll(nil, socket_set)
   227       end
   228     end
   229     -- function that enforces consumption of all input:
   230     local function consume_all()
   231       while consume do
   232         poll(socket_set, nil)
   233         consume()
   234       end
   235     end
   236     -- handle requests in a loop:
   237     repeat
   238       -- table for caching nil values:
   239       local headers_value_nil = {}
   240       -- create a new request object:
   241       local request  -- allow references to local variable
   242       request = {
   243         -- allow access to underlying socket:
   244         socket = socket,
   245         -- cookies are simply stored in a table:
   246         cookies = {},
   247         -- table mapping header field names to value-lists
   248         -- (raw access, but case-insensitive):
   249         headers = setmetatable({}, {
   250           __index = function(self, key)
   251             local lowerkey = string.lower(key)
   252             if lowerkey == key then
   253               return
   254             end
   255             local result = rawget(self, lowerkey)
   256             if result == nil then
   257               result = {}
   258             end
   259             self[lowerkey] = result
   260             self[key] = result
   261             return result
   262           end
   263         }),
   264         -- table mapping header field names to value-lists
   265         -- (for headers with comma separated values):
   266         headers_csv_table = setmetatable({}, {
   267           __index = function(self, key)
   268             local result = {}
   269             for i, line in ipairs(request.headers[key]) do
   270               for entry in string.gmatch(line, "[^,]+") do
   271                 local value = string.match(entry, "^[ \t]*(..-)[ \t]*$")
   272                 if value then
   273                   result[#result+1] = value
   274                 end
   275               end
   276             end
   277             self[key] = result
   278             return result
   279           end
   280         }),
   281         -- table mapping header field names to a comma separated string
   282         -- (for headers with comma separated values):
   283         headers_csv_string = setmetatable({}, {
   284           __index = function(self, key)
   285             local result = {}
   286             for i, line in ipairs(request.headers[key]) do
   287               result[#result+1] = line
   288             end
   289             result = string.concat(result, ", ")
   290             self[key] = result
   291             return result
   292           end
   293         }),
   294         -- table mapping header field names to a single string value
   295         -- (or false if header has been sent multiple times):
   296         headers_value = setmetatable({}, {
   297           __index = function(self, key)
   298             if headers_value_nil[key] then
   299               return nil
   300             end
   301             local result = nil
   302             local values = request.headers_csv_table[key]
   303             if #values == 0 then
   304               headers_value_nil[key] = true
   305             elseif #values == 1 then
   306               result = values[1]
   307             else
   308               result = false
   309             end
   310             self[key] = result
   311             return result
   312           end
   313         }),
   314         -- table mapping header field names to a flag table,
   315         -- indicating if the comma separated value contains certain entries:
   316         headers_flags = setmetatable({}, {
   317           __index = function(self, key)
   318             local result = setmetatable({}, {
   319               __index = function(self, key)
   320                 local lowerkey = string.lower(key)
   321                 local result = rawget(self, lowerkey) or false
   322                 self[lowerkey] = result
   323                 self[key] = result
   324                 return result
   325               end
   326             })
   327             for i, value in ipairs(request.headers_csv_table[key]) do
   328               result[string.lower(value)] = true
   329             end
   330             self[key] = result
   331             return result
   332           end
   333         })
   334       }
   335       -- local variables to track the state:
   336       local state = "init"        -- one of:
   337       --  "init"                  (initial state)
   338       --  "prepare"               (configureation in preparation)
   339       --  "no_status_sent"        (configuration complete)
   340       --  "info_status_sent"      (1xx status code has been sent)
   341       --  "bodyless_status_sent"  (204/304 status code has been sent)
   342       --  "status_sent"           (regular status code has been sent)
   343       --  "headers_sent"          (headers have been terminated)
   344       --  "finished"              (request has been answered completely)
   345       --  "faulty"                (I/O or protocaol error)
   346       local close_requested = false  -- "Connection: close" requested
   347       local close_responded = false  -- "Connection: close" sent
   348       local content_length = nil  -- value of Content-Length header sent
   349       local chunk_parts = {}  -- list of chunks to send
   350       local chunk_bytes = 0   -- sum of lengths of chunks to send
   351       -- functions to assert proper output/closing:
   352       local function assert_output(...)
   353         local retval, errmsg = ...
   354         if retval then return ...  end
   355         state = "faulty"
   356         socket:reset()
   357         error("Could not send data to client: " .. errmsg)
   358       end
   359       local function assert_close(...)
   360         local retval, errmsg = ...
   361         if retval then return ...  end
   362         state = "faulty"
   363         error("Could not finish sending data to client: " .. errmsg)
   364       end
   365       -- function to assert non-faulty handle:
   366       local function assert_not_faulty()
   367         assert(state ~= "faulty", "Tried to use faulty request handle")
   368       end
   369       -- functions to send data to the browser:
   370       local function send(...)
   371         assert_output(socket:write_call(unblock, ...))
   372       end
   373       local function send_flush(...)
   374         assert_output(socket:flush_call(unblock, ...))
   375       end
   376       -- function to finish request:
   377       local function finish()
   378         if close_responded then
   379           -- discard any input:
   380           consume = drain
   381           -- close output stream:
   382           send_flush()
   383           assert_close(socket:finish())
   384           -- wait for EOF of peer to avoid immediate TCP RST condition:
   385           consume_all()
   386           -- fully close socket:
   387           assert_close(socket:close())
   388         else
   389           send_flush()
   390           consume_all()
   391         end
   392       end
   393       -- function that writes out buffered chunks (without flushing the socket):
   394       function send_chunk()
   395         if chunk_bytes > 0 then
   396           assert_output(socket:write(string.format("%x\r\n", chunk_bytes)))
   397           for i = 1, #chunk_parts do  -- TODO: evaluated only once?
   398             send(chunk_parts[i])
   399             chunk_parts[i] = nil
   400           end
   401           chunk_bytes = 0
   402           send("\r\n")
   403         end
   404       end
   405       -- function to prepare (or skip) body processing:
   406       local function prepare()
   407         assert_not_faulty()
   408         if state == "prepare" then
   409           error("Unexpected internal status in HTTP engine (recursive prepare)")
   410         elseif state ~= "init" then
   411           return
   412         end
   413         state = "prepare"
   414         -- TODO
   415         state = "no_status_sent"
   416       end
   417       -- method to ignore input and close connection after response:
   418       function request:monologue()
   419         assert_not_faulty()
   420         if
   421           state == "headers_sent" or
   422           state == "finished"
   423         then
   424           error("All HTTP headers have already been sent")
   425         end
   426         local old_state = state
   427         state = "faulty"
   428         consume = drain
   429         close_requested = true
   430         if old_state == "init" or old_state == "prepare" then  -- TODO: ok?
   431           state = "no_status_sent"
   432         else
   433           state = old_state
   434         end
   435       end
   436       --
   437       -- method to send a HTTP response status (e.g. "200 OK"):
   438       function request:send_status(status)
   439         prepare()
   440         local old_state = state
   441         state = "faulty"
   442         if old_state == "info_status_sent" then
   443           send_flush("\r\n")
   444         elseif old_state ~= "no_status_sent" then
   445           error("HTTP status has already been sent")
   446         end
   447         local status1 = string.sub(status, 1, 1)
   448         local status3 = string.sub(status, 1, 3)
   449         send("HTTP/1.1 ", status, "\r\n", preamble)
   450         local wrb = status_without_response_body[status3]
   451         if wrb then
   452           state = "bodyless_status_sent"
   453           if wrb == "zero_content_length" then
   454             request:send_header("Content-Length", 0)
   455           end
   456         elseif status1 == "1" then
   457           state = "info_status_sent"
   458         else
   459           state = "status_sent"
   460         end
   461       end
   462       -- method to send a HTTP response header:
   463       -- (key and value must be provided as separate args)
   464       function request:send_header(key, value)
   465         assert_not_faulty()
   466         if
   467           state == "init" or
   468           state == "prepare" or
   469           state == "no_status_sent"
   470         then
   471           error("HTTP status has not been sent yet")
   472         elseif
   473           state == "headers_sent" or
   474           state == "finished"
   475         then
   476           error("All HTTP headers have already been sent")
   477         end
   478         local key_lower = string.lower(key)
   479         if key_lower == "content-length" then
   480           if state == "info_status_sent" then
   481             error("Cannot set Content-Length for informational status response")
   482           end
   483           local cl = assert(tonumber(value), "Invalid content-length")
   484           if content_length == nil then
   485             content_length = cl
   486           elseif content_length == cl then
   487             return
   488           else
   489             error("Content-Length has been set multiple times with different values")
   490           end
   491         elseif key_lower == "connection" then
   492           for entry in string.gmatch(string.lower(value), "[^,]+") do
   493             if string.match(entry, "^[ \t]*close[ \t]*$") then
   494               if state == "info_status_sent" then
   495                 error("Cannot set \"Connection: close\" for informational status response")
   496               end
   497               close_responded = true
   498               break
   499             end
   500           end
   501         end
   502         assert_output(socket:write(key, ": ", value, "\r\n"))
   503       end
   504       -- function to terminate header section in response, optionally flushing:
   505       -- (may be called multiple times unless response is finished)
   506       local function finish_headers(with_flush)
   507         if state == "finished" then
   508           error("Response has already been finished")
   509         elseif state == "info_status_sent" then
   510           send_flush("\r\n")
   511           state = "no_status_sent"
   512         elseif state == "bodyless_status_sent" then
   513           if close_requested and not close_responded then
   514             request:send_header("Connection", "close")
   515           end
   516           send("\r\n")
   517           finish()
   518           state = "finished"
   519         elseif state == "status_sent" then
   520           if not content_length then
   521             request:send_header("Transfer-Encoding", "chunked")
   522           end
   523           if close_requested and not close_responded then
   524             request:send_header("Connection", "close")
   525           end
   526           send("\r\n")
   527           if request.method == "HEAD" then
   528             finish()
   529           elseif with_flush then
   530             send_flush()
   531           end
   532           state = "headers_sent"
   533         elseif state ~= "headers_sent" then
   534           error("HTTP status has not been sent yet")
   535         end
   536       end
   537       -- method to finish and flush headers:
   538       function request:finish_headers()
   539         assert_not_faulty()
   540         finish_headers(true)
   541       end
   542       -- method to send body data:
   543       function request:send_data(...)
   544         assert_not_faulty()
   545         if output_state == "info_status_sent" then
   546           error("No (non-informational) HTTP status has been sent yet")
   547         elseif output_state == "bodyless_status_sent" then
   548           error("Cannot send response data for body-less status message")
   549         end
   550         finish_headers(false)
   551         if output_state ~= "headers_sent" then
   552           error("Unexpected internal status in HTTP engine")
   553         end
   554         if request.method == "HEAD" then
   555           return
   556         end
   557         for i = 1, select("#", ...) do
   558           local str = tostring(select(i, ...))
   559           if #str > 0 then
   560             if content_length then
   561               local bytes_to_send = #str
   562               if bytes_sent + bytes_to_send > content_length then
   563                 error("Content length exceeded")
   564               else
   565                 send(str)
   566                 bytes_sent = bytes_sent + bytes_to_send
   567               end
   568             else
   569               chunk_bytes = chunk_bytes + #str
   570               chunk_parts[#chunk_parts+1] = str
   571             end
   572           end
   573         end
   574         if chunk_bytes >= output_chunk_size then
   575           send_chunk()
   576         end
   577       end
   578       -- method to flush output buffer:
   579       function request:flush()
   580         assert_not_faulty()
   581         send_chunk()
   582         send_flush()
   583       end
   584       -- method to finish response:
   585       function request:finish()
   586         assert_not_faulty()
   587         if state == "finished" then
   588           return
   589         elseif state == "info_status_sent" then
   590           error("Informational HTTP response can be finished with :finish_headers() method")
   591         end
   592         finish_headers(false)
   593         if state == "headers_sent" then
   594           if request.method ~= "HEAD" then
   595             state = "faulty"
   596             if content_length then
   597               if bytes_sent ~= content_length then
   598                 error("Content length not used")
   599               end
   600             else
   601               send_chunk()
   602               send("0\r\n\r\n")
   603             end
   604             finish()
   605           end
   606           state = "finished"
   607         elseif state ~= "finished" then
   608           error("Unexpected internal status in HTTP engine")
   609         end
   610       end
   611       -- function to report an error:
   612       local function request_error(throw_error, status, text)
   613         local errmsg = "Error while reading request from client. Error response: " .. status
   614         if text then
   615           errmsg = errmsg .. " (" .. text .. ")"
   616         end
   617         if
   618           state == "init" or
   619           state == "prepare" or
   620           state == "no_status_sent" or
   621           state == "info_status_sent"
   622         then
   623           local error_response_status, errmsg2 = pcall(function()
   624             request:monologue()
   625             request:send_status(status)
   626             request:send_header("Content-Type", "text/plain")
   627             request:send_data(status, "\n")
   628             if text then
   629               request:send_data("\n", text, "\n")
   630             end
   631             request:finish()
   632           end)
   633           if not error_response_status then
   634             error("Unexpected error while sending error response: " .. errmsg2)
   635           end
   636         elseif state ~= "faulty" then
   637           assert_close(socket:reset())
   638         end
   639         if throw_error then
   640           error(errmsg)
   641         else
   642           return survive
   643         end
   644       end
   645       -- wait for input:
   646       if not poll(socket_set, nil, request_idle_timeout) then
   647         return request_error(false, "408 Request Timeout", "Idle connection timed out")
   648       end
   649     until close_responded
   650     return survive
   651   end
   652 end
   654 return _M
