moonbridge

diff moonbridge_http.lua @ 4:583e2ad140dc

Renamed "http" module to "moonbridge_http"
author jbe
date Fri Jan 09 22:51:25 2015 +0100 (2015-01-09)
parents http.lua@f6d3b3f70dab
children 4b6d7ca25381
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/moonbridge_http.lua	Fri Jan 09 22:51:25 2015 +0100
     1.3 @@ -0,0 +1,883 @@
     1.4 +#!/usr/bin/env lua
     1.5 +
     1.6 +-- module preamble
     1.7 +local _G, _M = _ENV, {}
     1.8 +_ENV = setmetatable({}, {
     1.9 +  __index = function(self, key)
    1.10 +    local value = _M[key]; if value ~= nil then return value end
    1.11 +    return _G[key]
    1.12 +  end,
    1.13 +  __newindex = function(self, key, value) _M[key] = value end
    1.14 +})
    1.15 +
    1.16 +-- function that encodes certain HTML entities:
    1.17 +-- (not used by the library itself)
    1.18 +function encode_html(text)
    1.19 +  return (
    1.20 +    string.gsub(
    1.21 +      text, '[<>&"]',
    1.22 +      function(char)
    1.23 +        if char == '<' then
    1.24 +          return "&lt;"
    1.25 +        elseif char == '>' then
    1.26 +          return "&gt;"
    1.27 +        elseif char == '&' then
    1.28 +          return "&amp;"
    1.29 +        elseif char == '"' then
    1.30 +          return "&quot;"
    1.31 +        end
    1.32 +      end
    1.33 +    )
    1.34 +  )
    1.35 +
    1.36 +end
    1.37 +
    1.38 +-- function that encodes special characters for URIs:
    1.39 +-- (not used by the library itself)
    1.40 +function encode_uri(text)
    1.41 +  return (
    1.42 +    string.gsub(text, "[^0-9A-Za-z_%.~-]",
    1.43 +      function (char)
    1.44 +        return string.format("%%%02x", string.byte(char))
    1.45 +      end
    1.46 +    )
    1.47 +  )
    1.48 +end
    1.49 +
    1.50 +-- function undoing URL encoding:
    1.51 +do
    1.52 +  local b0 = string.byte("0")
    1.53 +  local b9 = string.byte("9")
    1.54 +  local bA = string.byte("A")
    1.55 +  local bF = string.byte("F")
    1.56 +  local ba = string.byte("a")
    1.57 +  local bf = string.byte("f")
    1.58 +  function decode_uri(str)
    1.59 +    return (
    1.60 +      string.gsub(
    1.61 +        string.gsub(str, "%+", " "),
    1.62 +        "%%([0-9A-Fa-f][0-9A-Fa-f])",
    1.63 +        function(hex)
    1.64 +          local n1, n2 = string.byte(hex, 1, 2)
    1.65 +          if n1 >= b0 and n1 <= b9 then n1 = n1 - b0
    1.66 +          elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10
    1.67 +          elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10
    1.68 +          else error("Assertion failed") end
    1.69 +          if n2 >= b0 and n2 <= b9 then n2 = n2 - b0
    1.70 +          elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10
    1.71 +          elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10
    1.72 +          else error("Assertion failed") end
    1.73 +          return string.char(n1 * 16 + n2)
    1.74 +        end
    1.75 +      )
    1.76 +    )
    1.77 +  end
    1.78 +end
    1.79 +
    1.80 +-- status codes that carry no response body (in addition to 1xx):
    1.81 +-- (set to "zero_content_length" if Content-Length header is required)
    1.82 +status_without_response_body = {
    1.83 +  ["101"] = true,
    1.84 +  ["204"] = true,
    1.85 +  ["205"] = "zero_content_length",
    1.86 +  ["304"] = true
    1.87 +}
    1.88 +
    1.89 +-- handling of GET/POST param tables:
    1.90 +local new_params_list  -- defined later
    1.91 +do
    1.92 +  local params_list_mapping = setmetatable({}, {__mode="k"})
    1.93 +  local params_list_metatable = {
    1.94 +    __index = function(self, key)
    1.95 +      local tbl = {}
    1.96 +      self[key] = tbl
    1.97 +      return tbl
    1.98 +    end
    1.99 +  }
   1.100 +  local params_metatable = {
   1.101 +    __index = function(self, key)
   1.102 +      local value = params_list_mapping[self][key][1]
   1.103 +      self[key] = value
   1.104 +      return value
   1.105 +    end
   1.106 +  }
   1.107 +  -- returns a table to store key value-list pairs (i.e. multiple values),
   1.108 +  -- and a second table automatically mapping keys to the first value
   1.109 +  -- using the key value-list pairs in the first table:
   1.110 +  new_params_list = function()
   1.111 +    local params_list = setmetatable({}, params_list_metatable)
   1.112 +    local params = setmetatable({}, params_metatable)
   1.113 +    params_list_mapping[params] = params_list
   1.114 +    return params_list, params
   1.115 +  end
   1.116 +end
   1.117 +-- parses URL encoded form data and stores it in
   1.118 +-- a key value-list pairs structure that has to be
   1.119 +-- previously obtained by calling by new_params_list():
   1.120 +local function read_urlencoded_form(tbl, data)
   1.121 +  for rawkey, rawvalue in string.gmatch(data, "([^=&]*)=([^=&]*)") do
   1.122 +    local subtbl = tbl[decode_uri(rawkey)]
   1.123 +    subtbl[#subtbl+1] = decode_uri(rawvalue)
   1.124 +  end
   1.125 +end
   1.126 +
   1.127 +-- function creating a HTTP handler:
   1.128 +function generate_handler(handler, options)
   1.129 +  -- swap arguments if necessary (for convenience):
   1.130 +  if type(handler) ~= "function" and type(options) == "function" then
   1.131 +    handler, options = options, handler
   1.132 +  end
   1.133 +  -- process options:
   1.134 +  options = options or {}
   1.135 +  local preamble = ""  -- preamble sent with every(!) HTTP response
   1.136 +  do
   1.137 +    -- named arg "static_headers" is used to create the preamble:
   1.138 +    local s = options.static_headers
   1.139 +    local t = {}
   1.140 +    if s then
   1.141 +      if type(s) == "string" then
   1.142 +        for line in string.gmatch(s, "[^\r\n]+") do
   1.143 +          t[#t+1] = line
   1.144 +        end
   1.145 +      else
   1.146 +        for i, kv in ipairs(options.static_headers) do
   1.147 +          if type(kv) == "string" then
   1.148 +            t[#t+1] = kv
   1.149 +          else
   1.150 +            t[#t+1] = kv[1] .. ": " .. kv[2]
   1.151 +          end
   1.152 +        end
   1.153 +      end
   1.154 +    end
   1.155 +    t[#t+1] = ""
   1.156 +    preamble = table.concat(t, "\r\n")
   1.157 +  end
   1.158 +  -- return connect handler:
   1.159 +  return function(socket)
   1.160 +    local survive = true  -- set to false if process shall be terminated later
   1.161 +    while true do
   1.162 +      -- desired chunk sizes:
   1.163 +      local input_chunk_size = options.maximum_input_chunk_size or options.chunk_size or 16384
   1.164 +      local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024
   1.165 +      -- process named arguments "request_header_size_limit" and "request_body_size_limit":
   1.166 +      local remaining_header_size_limit = options.request_header_size_limit or 1024*1024
   1.167 +      local remaining_body_size_limit = options.request_body_size_limit or 64*1024*1024
   1.168 +      -- state variables for request handling:
   1.169 +      local output_state = "no_status_sent"  -- one of:
   1.170 +      --  "no_status_sent"        (initial state)
   1.171 +      --  "info_status_sent"      (1xx status code has been sent)
   1.172 +      --  "bodyless_status_sent"  (204/304 status code has been sent)
   1.173 +      --  "status_sent"           (regular status code has been sent)
   1.174 +      --  "headers_sent"          (headers have been terminated)
   1.175 +      --  "finished"              (request has been answered completely)
   1.176 +      local content_length    -- value of Content-Length header sent
   1.177 +      local bytes_sent = 0    -- number of bytes sent if Content-Length is set
   1.178 +      local chunk_parts = {}  -- list of chunks to send
   1.179 +      local chunk_bytes = 0   -- sum of lengths of chunks to send
   1.180 +      local connection_close_requested = false
   1.181 +      local connection_close_responded = false
   1.182 +      local headers_value_nil = {}          -- header values that are nil
   1.183 +      local request_body_content_length     -- Content-Length of request body
   1.184 +      local input_state = "pending"  -- one of:
   1.185 +      -- "pending"   (request body has not been processed yet)
   1.186 +      -- "deferred"  (request body processing is deferred)
   1.187 +      -- "reading"   (request body is currently being read)
   1.188 +      -- "finished"  (request body has been read)
   1.189 +      local streamed_post_params         = {}  -- mapping from POST field name to stream function
   1.190 +      local streamed_post_param_patterns = {}  -- list of POST field pattern and stream function pairs
   1.191 +      -- object passed to handler (with methods, GET/POST params, etc.):
   1.192 +      local request
   1.193 +      -- reads a number of bytes from the socket,
   1.194 +      -- optionally feeding these bytes chunk-wise
   1.195 +      -- into a callback function:
   1.196 +      local function read_body_bytes(remaining, callback)
   1.197 +        while remaining > 0 do
   1.198 +          local limit
   1.199 +          if remaining > input_chunk_size then
   1.200 +            limit = input_chunk_size
   1.201 +          else
   1.202 +            limit = remaining
   1.203 +          end
   1.204 +          local chunk = socket:read(limit)
   1.205 +          if not chunk or #chunk ~= limit then
   1.206 +            error("Unexpected EOF while reading chunk of request body")
   1.207 +          end
   1.208 +          remaining = remaining - limit
   1.209 +          if callback then
   1.210 +            callback(chunk)
   1.211 +          end
   1.212 +        end
   1.213 +      end
   1.214 +      -- flushes or closes the socket (depending on
   1.215 +      -- whether "Connection: close" header was sent):
   1.216 +      local function finish_response()
   1.217 +        if connection_close_responded then
   1.218 +          -- close output stream:
   1.219 +          socket.output:close()
   1.220 +          -- wait for EOF of peer to avoid immediate TCP RST condition:
   1.221 +          timeout(2, function()
   1.222 +            while socket.input:read(input_chunk_size) do end
   1.223 +          end)
   1.224 +          -- fully close socket:
   1.225 +          socket:close()
   1.226 +        else
   1.227 +          socket:flush()
   1.228 +          request:stream_request_body()
   1.229 +        end
   1.230 +      end
   1.231 +      -- writes out buffered chunks (without flushing the socket):
   1.232 +      local function send_chunk()
   1.233 +        if chunk_bytes > 0 then
   1.234 +          socket:write(string.format("%x\r\n", chunk_bytes))
   1.235 +          for i = 1, #chunk_parts do
   1.236 +            socket:write(chunk_parts[i])
   1.237 +          end
   1.238 +          chunk_parts = {}
   1.239 +          chunk_bytes = 0
   1.240 +          socket:write("\r\n")
   1.241 +        end
   1.242 +      end
   1.243 +      -- terminate header section in response, optionally flushing:
   1.244 +      -- (may be called multiple times unless response is finished)
   1.245 +      local function finish_headers(flush)
   1.246 +        if output_state == "no_status_sent" then
   1.247 +          error("HTTP status has not been sent yet")
   1.248 +        elseif output_state == "finished" then
   1.249 +          error("Response has already been finished")
   1.250 +        elseif output_state == "info_status_sent" then
   1.251 +          socket:write("\r\n")
   1.252 +          socket:flush()
   1.253 +          output_state = "no_status_sent"
   1.254 +        elseif output_state == "bodyless_status_sent" then
   1.255 +          if connection_close_requested and not connection_close_responded then
   1.256 +            request:send_header("Connection", "close")
   1.257 +          end
   1.258 +          socket:write("\r\n")
   1.259 +          finish_response()
   1.260 +          output_state = "finished"
   1.261 +        elseif output_state == "status_sent" then
   1.262 +          if not content_length then
   1.263 +            socket:write("Transfer-Encoding: chunked\r\n")
   1.264 +          end
   1.265 +          if connection_close_requested and not connection_close_responded then
   1.266 +            request:send_header("Connection", "close")
   1.267 +          end
   1.268 +          socket:write("\r\n")
   1.269 +          if request.method == "HEAD" then
   1.270 +            finish_response()
   1.271 +          elseif flush then
   1.272 +            socket:flush()
   1.273 +          end
   1.274 +          output_state = "headers_sent"
   1.275 +        elseif output_state ~= "headers_sent" then
   1.276 +          error("Unexpected internal status in HTTP engine")
   1.277 +        end
   1.278 +      end
   1.279 +      -- create request object and set several functions and values:
   1.280 +      request = {
   1.281 +        -- allow raw socket access:
   1.282 +        socket = socket,
   1.283 +        -- parsed cookies:
   1.284 +        cookies = {},
   1.285 +        -- send a HTTP response status (e.g. "200 OK"):
   1.286 +        send_status = function(self, value)
   1.287 +          if input_state == "pending" then
   1.288 +            request:process_request_body()
   1.289 +          end
   1.290 +          if output_state == "info_status_sent" then
   1.291 +            socket:write("\r\n")
   1.292 +            socket:flush()
   1.293 +          elseif output_state ~= "no_status_sent" then
   1.294 +            error("HTTP status has already been sent")
   1.295 +          end
   1.296 +          local status1 = string.sub(value, 1, 1)
   1.297 +          local status3 = string.sub(value, 1, 3)
   1.298 +          socket:write("HTTP/1.1 ", value, "\r\n", preamble)
   1.299 +          local without_response_body = status_without_response_body[status3]
   1.300 +          if without_response_body then
   1.301 +            output_state = "bodyless_status_sent"
   1.302 +            if without_response_body == "zero_content_length" then
   1.303 +              request:send_header("Content-Length", 0)
   1.304 +            end
   1.305 +          elseif status1 == "1" then
   1.306 +            output_state = "info_status_sent"
   1.307 +          else
   1.308 +            output_state = "status_sent"
   1.309 +          end
   1.310 +        end,
   1.311 +        -- send a HTTP response header
   1.312 +        -- (key and value as separate args):
   1.313 +        send_header = function(self, key, value)
   1.314 +          if output_state == "no_status_sent" then
   1.315 +            error("HTTP status has not been sent yet")
   1.316 +          elseif
   1.317 +            output_state ~= "info_status_sent" and
   1.318 +            output_state ~= "bodyless_status_sent" and
   1.319 +            output_state ~= "status_sent"
   1.320 +          then
   1.321 +            error("All HTTP headers have already been sent")
   1.322 +          end
   1.323 +          local key_lower = string.lower(key)
   1.324 +          if key_lower == "content-length" then
   1.325 +            if output_state == "info_status_sent" then
   1.326 +              error("Cannot set Content-Length for informational status response")
   1.327 +            end
   1.328 +            local new_content_length = assert(tonumber(value), "Invalid content-length")
   1.329 +            if content_length == nil then
   1.330 +              content_length = new_content_length
   1.331 +            elseif content_length == new_content_length then
   1.332 +              return
   1.333 +            else
   1.334 +              error("Content-Length has been set multiple times with different values")
   1.335 +            end
   1.336 +          elseif key_lower == "connection" then
   1.337 +            for entry in string.gmatch(string.lower(value), "[^,]+") do
   1.338 +              if string.match(entry, "^[ \t]*close[ \t]*$") then
   1.339 +                if output_state == "info_status_sent" then
   1.340 +                  error("Cannot set \"Connection: close\" for informational status response")
   1.341 +                end
   1.342 +                if connection_close_responded then
   1.343 +                  return
   1.344 +                else
   1.345 +                  connection_close_responded = true
   1.346 +                  break
   1.347 +                end
   1.348 +              end
   1.349 +            end
   1.350 +          end
   1.351 +          socket:write(key, ": ", value, "\r\n")
   1.352 +        end,
   1.353 +        -- method to finish and flush headers:
   1.354 +        finish_headers = function()
   1.355 +          finish_headers(true)
   1.356 +        end,
   1.357 +        -- send data for response body:
   1.358 +        send_data = function(self, ...)
   1.359 +          if output_state == "info_status_sent" then
   1.360 +            error("No (non-informational) HTTP status has been sent yet")
   1.361 +          elseif output_state == "bodyless_status_sent" then
   1.362 +            error("Cannot send response data for body-less status message")
   1.363 +          end
   1.364 +          finish_headers(false)
   1.365 +          if output_state ~= "headers_sent" then
   1.366 +            error("Unexpected internal status in HTTP engine")
   1.367 +          end
   1.368 +          if request.method == "HEAD" then
   1.369 +            return
   1.370 +          end
   1.371 +          for i = 1, select("#", ...) do
   1.372 +            local str = tostring(select(i, ...))
   1.373 +            if #str > 0 then
   1.374 +              if content_length then
   1.375 +                local bytes_to_send = #str
   1.376 +                if bytes_sent + bytes_to_send > content_length then
   1.377 +                  socket:write(string.sub(str, 1, content_length - bytes_sent))
   1.378 +                  bytes_sent = content_length
   1.379 +                  error("Content length exceeded")
   1.380 +                else
   1.381 +                  socket:write(str)
   1.382 +                  bytes_sent = bytes_sent + bytes_to_send
   1.383 +                end
   1.384 +              else
   1.385 +                chunk_bytes = chunk_bytes + #str
   1.386 +                chunk_parts[#chunk_parts+1] = str
   1.387 +              end
   1.388 +            end
   1.389 +          end
   1.390 +          if chunk_bytes >= output_chunk_size then
   1.391 +            send_chunk()
   1.392 +          end
   1.393 +        end,
   1.394 +        -- flush output buffer:
   1.395 +        flush = function(self)
   1.396 +          send_chunk()
   1.397 +          socket:flush()
   1.398 +        end,
   1.399 +        -- finish response:
   1.400 +        finish = function(self)
   1.401 +          if output_state == "finished" then
   1.402 +            return
   1.403 +          elseif output_state == "info_status_sent" then
   1.404 +            error("Informational HTTP response can be finished with :finish_headers() method")
   1.405 +          end
   1.406 +          finish_headers(false)
   1.407 +          if output_state == "headers_sent" then
   1.408 +            if request.method ~= "HEAD" then
   1.409 +              if content_length then
   1.410 +                if bytes_sent ~= content_length then
   1.411 +                  error("Content length not used")
   1.412 +                end
   1.413 +              else
   1.414 +                send_chunk()
   1.415 +                socket:write("0\r\n\r\n")
   1.416 +              end
   1.417 +              finish_response()
   1.418 +            end
   1.419 +            output_state = "finished"
   1.420 +          elseif output_state ~= finished then
   1.421 +            error("Unexpected internal status in HTTP engine")
   1.422 +          end
   1.423 +        end,
   1.424 +        -- table mapping header field names to value-lists
   1.425 +        -- (raw access):
   1.426 +        headers = setmetatable({}, {
   1.427 +          __index = function(self, key)
   1.428 +            local lowerkey = string.lower(key)
   1.429 +            if lowerkey == key then
   1.430 +              return
   1.431 +            end
   1.432 +            local result = rawget(self, lowerkey)
   1.433 +            if result == nil then
   1.434 +              result = {}
   1.435 +            end
   1.436 +            self[lowerkey] = result
   1.437 +            self[key] = result
   1.438 +            return result
   1.439 +          end
   1.440 +        }),
   1.441 +        -- table mapping header field names to value-lists
   1.442 +        -- (for headers with comma separated values):
   1.443 +        headers_csv_table = setmetatable({}, {
   1.444 +          __index = function(self, key)
   1.445 +            local result = {}
   1.446 +            for i, line in ipairs(request.headers[key]) do
   1.447 +              for entry in string.gmatch(line, "[^,]+") do
   1.448 +                local value = string.match(entry, "^[ \t]*(..-)[ \t]*$")
   1.449 +                if value then
   1.450 +                  result[#result+1] = value
   1.451 +                end
   1.452 +              end
   1.453 +            end
   1.454 +            self[key] = result
   1.455 +            return result
   1.456 +          end
   1.457 +        }),
   1.458 +        -- table mapping header field names to a comma separated string
   1.459 +        -- (for headers with comma separated values):
   1.460 +        headers_csv_string = setmetatable({}, {
   1.461 +          __index = function(self, key)
   1.462 +            local result = {}
   1.463 +            for i, line in ipairs(request.headers[key]) do
   1.464 +              result[#result+1] = line
   1.465 +            end
   1.466 +            result = string.concat(result, ", ")
   1.467 +            self[key] = result
   1.468 +            return result
   1.469 +          end
   1.470 +        }),
   1.471 +        -- table mapping header field names to a single string value
   1.472 +        -- (or false if header has been sent multiple times):
   1.473 +        headers_value = setmetatable({}, {
   1.474 +          __index = function(self, key)
   1.475 +            if headers_value_nil[key] then
   1.476 +              return nil
   1.477 +            end
   1.478 +            local result = nil
   1.479 +            local values = request.headers_csv_table[key]
   1.480 +            if #values == 0 then
   1.481 +              headers_value_nil[key] = true
   1.482 +            elseif #values == 1 then
   1.483 +              result = values[1]
   1.484 +            else
   1.485 +              result = false
   1.486 +            end
   1.487 +            self[key] = result
   1.488 +            return result
   1.489 +          end
   1.490 +        }),
   1.491 +        -- table mapping header field names to a flag table,
   1.492 +        -- indicating if the comma separated value contains certain entries:
   1.493 +        headers_flags = setmetatable({}, {
   1.494 +          __index = function(self, key)
   1.495 +            local result = setmetatable({}, {
   1.496 +              __index = function(self, key)
   1.497 +                local lowerkey = string.lower(key)
   1.498 +                local result = rawget(self, lowerkey) or false
   1.499 +                self[lowerkey] = result
   1.500 +                self[key] = result
   1.501 +                return result
   1.502 +              end
   1.503 +            })
   1.504 +            for i, value in ipairs(request.headers_csv_table[key]) do
   1.505 +              result[string.lower(value)] = true
   1.506 +            end
   1.507 +            self[key] = result
   1.508 +            return result
   1.509 +          end
   1.510 +        }),
   1.511 +        -- register POST param stream handler for a single field name:
   1.512 +        stream_post_param = function(self, field_name, callback)
   1.513 +          if input_state == "inprogress" or input_state == "finished" then
   1.514 +            error("Cannot register POST param streaming function if request body is already processed")
   1.515 +          end
   1.516 +          streamed_post_params[field_name] = callback
   1.517 +        end,
   1.518 +        -- register POST param stream handler for a field name pattern:
   1.519 +        stream_post_params = function(self, pattern, callback)
   1.520 +          if input_state == "inprogress" or input_state == "finished" then
   1.521 +            error("Cannot register POST param streaming function if request body is already processed")
   1.522 +          end
   1.523 +          streamed_post_param_patterns[#streamed_post_param_patterns+1] = {pattern, callback}
   1.524 +        end,
   1.525 +        -- disables automatic request body processing on write
   1.526 +        -- (use with caution):
   1.527 +        defer_reading = function(self)
   1.528 +          if input_state == "pending" then
   1.529 +            input_state = "deferred"
   1.530 +          end
   1.531 +        end,
   1.532 +        -- processes the request body and sets the request.post_params,
   1.533 +        -- request.post_params_list, request.meta_post_params, and
   1.534 +        -- request.meta_post_params_list values (can be called manually or
   1.535 +        -- automatically if post_params are accessed or data is written out)
   1.536 +        process_request_body = function(self)
   1.537 +          if input_state == "finished" then
   1.538 +            return
   1.539 +          end
   1.540 +          local post_params_list, post_params = new_params_list()
   1.541 +          local content_type = request.headers_value["Content-Type"]
   1.542 +          if content_type then
   1.543 +            if
   1.544 +              content_type == "application/x-www-form-urlencoded" or
   1.545 +              string.match(content_type, "^application/x%-www%-form%-urlencoded *;")
   1.546 +            then
   1.547 +              read_urlencoded_form(post_params_list, request.body)
   1.548 +            else
   1.549 +              local boundary = string.match(
   1.550 +                content_type,
   1.551 +                '^multipart/form%-data[ \t]*[;,][ \t]*boundary="([^"]+)"$'
   1.552 +              ) or string.match(
   1.553 +                content_type,
   1.554 +                '^multipart/form%-data[ \t]*[;,][ \t]*boundary=([^"; \t]+)$'
   1.555 +              )
   1.556 +              if boundary then
   1.557 +                local post_metadata_list, post_metadata = new_params_list()
   1.558 +                boundary = "--" .. boundary
   1.559 +                local headerdata = ""
   1.560 +                local streamer
   1.561 +                local field_name
   1.562 +                local metadata = {}
   1.563 +                local value_parts
   1.564 +                local function default_streamer(chunk)
   1.565 +                  value_parts[#value_parts+1] = chunk
   1.566 +                end
   1.567 +                local function stream_part_finish()
   1.568 +                  if streamer == default_streamer then
   1.569 +                    local value = table.concat(value_parts)
   1.570 +                    value_parts = nil
   1.571 +                    if field_name then
   1.572 +                      local values = post_params_list[field_name]
   1.573 +                      values[#values+1] = value
   1.574 +                      local metadata_entries = post_metadata_list[field_name]
   1.575 +                      metadata_entries[#metadata_entries+1] = metadata
   1.576 +                    end
   1.577 +                  else
   1.578 +                    streamer()
   1.579 +                  end
   1.580 +                  headerdata   = ""
   1.581 +                  streamer     = nil
   1.582 +                  field_name   = nil
   1.583 +                  metadata     = {}
   1.584 +                end
   1.585 +                local function stream_part_chunk(chunk)
   1.586 +                  if streamer then
   1.587 +                    streamer(chunk)
   1.588 +                  else
   1.589 +                    headerdata = headerdata .. chunk
   1.590 +                    while true do
   1.591 +                      local line, remaining = string.match(headerdata, "^(.-)\r?\n(.*)$")
   1.592 +                      if not line then
   1.593 +                        break
   1.594 +                      end
   1.595 +                      if line == "" then
   1.596 +                        streamer = streamed_post_params[field_name]
   1.597 +                        if not streamer then
   1.598 +                          for i, rule in ipairs(streamed_post_param_patterns) do
   1.599 +                            if string.match(field_name, rule[1]) then
   1.600 +                              streamer = rule[2]
   1.601 +                              break
   1.602 +                            end
   1.603 +                          end
   1.604 +                        end
   1.605 +                        if not streamer then
   1.606 +                          value_parts = {}
   1.607 +                          streamer = default_streamer
   1.608 +                        end
   1.609 +                        streamer(remaining, field_name, metadata)
   1.610 +                        return
   1.611 +                      end
   1.612 +                      headerdata = remaining
   1.613 +                      local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$")
   1.614 +                      if not header_key then
   1.615 +                        error("Invalid header in multipart/form-data part")
   1.616 +                      end
   1.617 +                      header_key = string.lower(header_key)
   1.618 +                      if header_key == "content-disposition" then
   1.619 +                        local escaped_header_value = string.gsub(header_value, '"[^"]*"', function(str)
   1.620 +                          return string.gsub(str, "=", "==")
   1.621 +                        end)
   1.622 +                        field_name = string.match(escaped_header_value, ';[ \t]*name="([^"]*)"')
   1.623 +                        if field_name then
   1.624 +                          field_name = string.gsub(field_name, "==", "=")
   1.625 +                        else
   1.626 +                          field_name = string.match(header_value, ';[ \t]*name=([^"; \t]+)')
   1.627 +                        end
   1.628 +                        metadata.file_name = string.match(escaped_header_value, ';[ \t]*filename="([^"]*)"')
   1.629 +                        if metadata.file_name then
   1.630 +                          metadata.file_name = string.gsub(metadata.file_name, "==", "=")
   1.631 +                        else
   1.632 +                          string.match(header_value, ';[ \t]*filename=([^"; \t]+)')
   1.633 +                        end
   1.634 +                      elseif header_key == "content-type" then
   1.635 +                        metadata.content_type = header_value
   1.636 +                      elseif header_key == "content-transfer-encoding" then
   1.637 +                        error("Content-transfer-encoding not supported by multipart/form-data parser")
   1.638 +                      end
   1.639 +                    end
   1.640 +                  end
   1.641 +                end
   1.642 +                local skippart   = true   -- ignore data until first boundary
   1.643 +                local afterbound = false  -- interpret 2 bytes after boundary ("\r\n" or "--")
   1.644 +                local terminated = false  -- final boundary read
   1.645 +                local bigchunk = ""
   1.646 +                request:stream_request_body(function(chunk)
   1.647 +                  if terminated then
   1.648 +                    return
   1.649 +                  end
   1.650 +                  bigchunk = bigchunk .. chunk
   1.651 +                  while true do
   1.652 +                    if afterbound then
   1.653 +                      if #bigchunk <= 2 then
   1.654 +                        return
   1.655 +                      end
   1.656 +                      local terminator = string.sub(bigchunk, 1, 2)
   1.657 +                      if terminator == "\r\n" then
   1.658 +                        afterbound = false
   1.659 +                        bigchunk = string.sub(bigchunk, 3)
   1.660 +                      elseif terminator == "--" then
   1.661 +                        terminated = true
   1.662 +                        bigchunk = nil
   1.663 +                        return
   1.664 +                      else
   1.665 +                        error("Error while parsing multipart body (expected CRLF or double minus)")
   1.666 +                      end
   1.667 +                    end
   1.668 +                    local pos1, pos2 = string.find(bigchunk, boundary, 1, true)
   1.669 +                    if not pos1 then
   1.670 +                      if not skippart then
   1.671 +                        local safe = #bigchunk-#boundary
   1.672 +                        if safe > 0 then
   1.673 +                          stream_part_chunk(string.sub(bigchunk, 1, safe))
   1.674 +                          bigchunk = string.sub(bigchunk, safe+1)
   1.675 +                        end
   1.676 +                      end
   1.677 +                      return
   1.678 +                    end
   1.679 +                    if not skippart then
   1.680 +                      stream_part_chunk(string.sub(bigchunk, 1, pos1 - 1))
   1.681 +                      stream_part_finish()
   1.682 +                    else
   1.683 +                      boundary = "\r\n" .. boundary
   1.684 +                      skippart = false
   1.685 +                    end
   1.686 +                    bigchunk = string.sub(bigchunk, pos2 + 1)
   1.687 +                    afterbound = true
   1.688 +                  end
   1.689 +                end)
   1.690 +                if not terminated then
   1.691 +                  error("Premature end of multipart/form-data request body")
   1.692 +                end
   1.693 +                request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata
   1.694 +              else
   1.695 +                error("Unknown Content-Type of request body")
   1.696 +              end
   1.697 +            end
   1.698 +          end
   1.699 +          request.post_params_list, request.post_params = post_params_list, post_params
   1.700 +        end,
   1.701 +        -- stream request body to an (optional) callback function
   1.702 +        -- without processing it otherwise:
   1.703 +        stream_request_body = function(self, callback)
   1.704 +          if input_state ~= "pending" and input_state ~= "deferred" then
   1.705 +            if callback then
   1.706 +              if input_state == "inprogress" then
   1.707 +                error("Request body is already being processed")
   1.708 +              else
   1.709 +                error("Request body has already been processed")
   1.710 +              end
   1.711 +            end
   1.712 +            return
   1.713 +          end
   1.714 +          input_state = "inprogress"
   1.715 +          if request.headers_flags["Expect"]["100-continue"] then
   1.716 +            request:send_status("100 Continue")
   1.717 +            request:finish_headers()
   1.718 +          end
   1.719 +          if request.headers_flags["Transfer-Encoding"]["chunked"] then
   1.720 +            while true do
   1.721 +              local line = socket:readuntil("\n", 32 + remaining_body_size_limit)
   1.722 +              if not line then
   1.723 +                error("Unexpected EOF while reading next chunk of request body")
   1.724 +              end
   1.725 +              local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$")
   1.726 +              local chunkext
   1.727 +              if lenstr then
   1.728 +                chunkext = ""
   1.729 +              else
   1.730 +                zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$")
   1.731 +              end
   1.732 +              if not lenstr or #lenstr > 13 then
   1.733 +                error("Encoding error or unexpected EOF while reading chunk of request body")
   1.734 +              end
   1.735 +              local len = tonumber("0x" .. lenstr)
   1.736 +              remaining_body_size_limit = remaining_body_size_limit - (#zeros + #chunkext + len)
   1.737 +              if remaining_body_size_limit < 0 then
   1.738 +                error("Request body size limit exceeded")
   1.739 +              end
   1.740 +              if len == 0 then break end
   1.741 +              read_body_bytes(len, callback)
   1.742 +              local term = socket:readuntil("\n", 2)
   1.743 +              if term ~= "\r\n" and term ~= "\n" then
   1.744 +                error("Encoding error while reading chunk of request body")
   1.745 +              end
   1.746 +            end
   1.747 +            while true do
   1.748 +              local line = socket:readuntil("\n", 2 + remaining_body_size_limit)
   1.749 +              if line == "\r\n" or line == "\n" then break end
   1.750 +              remaining_body_size_limit = remaining_body_size_limit - #line
   1.751 +              if remaining_body_size_limit < 0 then
   1.752 +                error("Request body size limit exceeded while reading trailer section of chunked request body")
   1.753 +              end
   1.754 +            end
   1.755 +          elseif request_body_content_length then
   1.756 +            read_body_bytes(request_body_content_length, callback)
   1.757 +          end
   1.758 +          input_state = "finished"
   1.759 +        end
   1.760 +      }
   1.761 +      -- initialize tables for GET params in request object:
   1.762 +      request.get_params_list, request.get_params = new_params_list()
   1.763 +      -- add meta table to request object to allow access to "body" and POST params:
   1.764 +      setmetatable(request, {
   1.765 +        __index = function(self, key)
   1.766 +          if key == "body" then
   1.767 +            local chunks = {}
   1.768 +            request:stream_request_body(function(chunk)
   1.769 +              chunks[#chunks+1] = chunk
   1.770 +            end)
   1.771 +            self.body = table.concat(chunks)
   1.772 +            return self.body
   1.773 +          elseif
   1.774 +            key == "post_params_list" or key == "post_params" or
   1.775 +            key == "post_metadata_list" or key == "post_metadata"
   1.776 +          then
   1.777 +            request:process_request_body()
   1.778 +            return request[key]
   1.779 +          end
   1.780 +        end
   1.781 +      })
   1.782 +      -- low level HTTP error response (for malformed requests, etc.):
   1.783 +      local function error_response(status, text)
   1.784 +        request:send_status(status)
   1.785 +        request:send_header("Content-Type", "text/plain")
   1.786 +        if not connection_close_responded then
   1.787 +          request:send_header("Connection", "close")
   1.788 +        end
   1.789 +        request:send_data(status, "\n")
   1.790 +        if text then
   1.791 +          request:send_data("\n", text, "\n")
   1.792 +        end
   1.793 +        request:finish()
   1.794 +        return survive
   1.795 +      end
   1.796 +      -- read and parse request line:
   1.797 +      local line = socket:readuntil("\n", remaining_header_size_limit)
   1.798 +      if not line then return survive end
   1.799 +      remaining_header_size_limit = remaining_header_size_limit - #line
   1.800 +      if remaining_header_size_limit == 0 then
   1.801 +        return error_response("413 Request Entity Too Large", "Request line too long")
   1.802 +      end
   1.803 +      local proto
   1.804 +      request.method, request.url, proto =
   1.805 +        line:match("^([^ \t\r]+)[ \t]+([^ \t\r]+)[ \t]*([^ \t\r]*)[ \t]*\r?\n$")
   1.806 +      if not request.method then
   1.807 +        return error_response("400 Bad Request")
   1.808 +      elseif proto ~= "HTTP/1.1" then
   1.809 +        return error_response("505 HTTP Version Not Supported")
   1.810 +      else
   1.811 +        -- read and parse headers:
   1.812 +        while true do
   1.813 +          local line = socket:readuntil("\n", remaining_header_size_limit);
   1.814 +          remaining_header_size_limit = remaining_header_size_limit - #line
   1.815 +          if not line then
   1.816 +            return error_response("400 Bad Request")
   1.817 +          end
   1.818 +          if line == "\r\n" or line == "\n" then
   1.819 +            break
   1.820 +          end
   1.821 +          if remaining_header_size_limit == 0 then
   1.822 +            return error_response("413 Request Entity Too Large", "Headers too long")
   1.823 +          end
   1.824 +          local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$")
   1.825 +          if not key then
   1.826 +            return error_response("400 Bad Request")
   1.827 +          end
   1.828 +          local values = request.headers[key]
   1.829 +          values[#values+1] = value
   1.830 +        end
   1.831 +        -- process "Connection: close" header if existent:
   1.832 +        connection_close_requested = request.headers_flags["Connection"]["close"]
   1.833 +        -- process "Content-Length" header if existent:
   1.834 +        do
   1.835 +          local values = request.headers_csv_table["Content-Length"]
   1.836 +          if #values > 0 then
   1.837 +            request_body_content_length = tonumber(values[1])
   1.838 +            local proper_value = tostring(request_body_content_length)
   1.839 +            for i, value in ipairs(values) do
   1.840 +              value = string.match(value, "^0*(.*)")
   1.841 +              if value ~= proper_value then
   1.842 +                return error_response("400 Bad Request", "Content-Length header(s) invalid")
   1.843 +              end
   1.844 +            end
   1.845 +            if request_body_content_length > remaining_body_size_limit then
   1.846 +              return error_response("413 Request Entity Too Large", "Request body too big")
   1.847 +            end
   1.848 +          end
   1.849 +        end
   1.850 +        -- process "Transfer-Encoding" header if existent:
   1.851 +        do
   1.852 +          local flag = request.headers_flags["Transfer-Encoding"]["chunked"]
   1.853 +          local list = request.headers_csv_table["Transfer-Encoding"]
   1.854 +          if (flag and #list ~= 1) or (not flag and #list ~= 0) then
   1.855 +            return error_response("400 Bad Request", "Unexpected Transfer-Encoding")
   1.856 +          end
   1.857 +        end
   1.858 +        -- process "Expect" header if existent:
   1.859 +        for i, value in ipairs(request.headers_csv_table["Expect"]) do
   1.860 +          if string.lower(value) ~= "100-continue" then
   1.861 +            return error_response("417 Expectation Failed", "Unexpected Expect header")
   1.862 +          end
   1.863 +        end
   1.864 +        -- parse GET params:
   1.865 +        request.path, request.query = string.match(request.url, "^([^?]*)%??(.*)$")
   1.866 +        read_urlencoded_form(request.get_params_list, request.query)
   1.867 +        -- parse cookies:
   1.868 +        for i, line in ipairs(request.headers["Cookie"]) do
   1.869 +          for rawkey, rawvalue in
   1.870 +            string.gmatch(line, "([^=; ]*)=([^=; ]*)")
   1.871 +          do
   1.872 +            request.cookies[decode_uri(rawkey)] = decode_uri(rawvalue)
   1.873 +          end
   1.874 +        end
   1.875 +        -- call underlying handler and remember boolean result:
   1.876 +        if handler(request) ~= true then survive = false end
   1.877 +        -- finish request (unless already done by underlying handler):
   1.878 +        request:finish()
   1.879 +      end
   1.880 +    end
   1.881 +    return survive
   1.882 +  end
   1.883 +end
   1.884 +
   1.885 +return _M
   1.886 +

Impressum / About Us