moonbridge

changeset 172:fb54c76e1484

Completed new HTTP module implementation (untested yet)
author jbe
date Tue Jun 16 21:44:32 2015 +0200 (2015-06-16)
parents 32d7423a6753
children 6e80bcf89bd5
files moonbridge_http.lua
line diff
     1.1 --- a/moonbridge_http.lua	Tue Jun 16 02:49:34 2015 +0200
     1.2 +++ b/moonbridge_http.lua	Tue Jun 16 21:44:32 2015 +0200
     1.3 @@ -240,7 +240,7 @@
     1.4        local remaining_body_size_limit = body_size_limit
     1.5        -- table for caching nil values:
     1.6        local headers_value_nil = {}
     1.7 -      -- create a new request object:
     1.8 +      -- create a new request object with metatable:
     1.9        local request  -- allow references to local variable
    1.10        request = {
    1.11          -- allow access to underlying socket:
    1.12 @@ -289,7 +289,7 @@
    1.13              for i, line in ipairs(request.headers[key]) do
    1.14                result[#result+1] = line
    1.15              end
    1.16 -            result = string.concat(result, ", ")
    1.17 +            result = table.concat(result, ", ")
    1.18              self[key] = result
    1.19              return result
    1.20            end
    1.21 @@ -335,6 +335,11 @@
    1.22            end
    1.23          })
    1.24        }
    1.25 +      -- create metatable for request object:
    1.26 +      local request_mt = {}
    1.27 +      setmetatable(request, request_mt)
    1.28 +      -- callback for request body streaming:
    1.29 +      local process_body_chunk
    1.30        -- local variables to track the state:
    1.31        local state = "init"        -- one of:
    1.32        --  "init"                  (initial state)
    1.33 @@ -350,6 +355,8 @@
    1.34        local content_length = nil  -- value of Content-Length header sent
    1.35        local chunk_parts = {}  -- list of chunks to send
    1.36        local chunk_bytes = 0   -- sum of lengths of chunks to send
    1.37 +      local streamed_post_params         = {}  -- mapping from POST field name to stream function
    1.38 +      local streamed_post_param_patterns = {}  -- list of POST field pattern and stream function pairs
    1.39        -- functions to assert proper output/closing:
    1.40        local function assert_output(...)
    1.41          local retval, errmsg = ...
    1.42 @@ -389,6 +396,7 @@
    1.43            assert_close(socket:close())
    1.44          else
    1.45            send_flush()
    1.46 +          process_body_chunk = nil
    1.47            consume_all()
    1.48          end
    1.49        end
    1.50 @@ -449,8 +457,6 @@
    1.51          end
    1.52          return data
    1.53        end
    1.54 -      -- callback for request body streaming:
    1.55 -      local process_body_chunk
    1.56        -- reads a number of bytes from the socket,
    1.57        -- optionally feeding these bytes chunk-wise
    1.58        -- into a callback function:
    1.59 @@ -508,9 +514,177 @@
    1.60            read_body_bytes(request_body_content_length)
    1.61          end
    1.62        end
    1.63 -      -- function to prepare (or skip) body processing:
    1.64 +      -- function to setup default request body handling:
    1.65 +      local function default_request_body_handling()
    1.66 +        local post_params_list, post_params = new_params_list()
    1.67 +        local content_type = request.headers_value["Content-Type"]
    1.68 +        if content_type then
    1.69 +          if
    1.70 +            content_type == "application/x-www-form-urlencoded" or
    1.71 +            string.match(content_type, "^application/x%-www%-form%-urlencoded *;")
    1.72 +          then
    1.73 +            read_urlencoded_form(post_params_list, request.body)
    1.74 +            request.post_params_list, request.post_params = post_params_list, post_params
    1.75 +          else
    1.76 +            local boundary = string.match(
    1.77 +              content_type,
    1.78 +              '^multipart/form%-data[ \t]*[;,][ \t]*boundary="([^"]+)"$'
    1.79 +            ) or string.match(
    1.80 +              content_type,
    1.81 +              '^multipart/form%-data[ \t]*[;,][ \t]*boundary=([^"; \t]+)$'
    1.82 +            )
    1.83 +            if boundary then
    1.84 +              local post_metadata_list, post_metadata = new_params_list()
    1.85 +              boundary = "--" .. boundary
    1.86 +              local headerdata = ""
    1.87 +              local streamer
    1.88 +              local field_name
    1.89 +              local metadata = {}
    1.90 +              local value_parts
    1.91 +              local function default_streamer(chunk)
    1.92 +                value_parts[#value_parts+1] = chunk
    1.93 +              end
    1.94 +              local function stream_part_finish()
    1.95 +                if streamer == default_streamer then
    1.96 +                  local value = table.concat(value_parts)
    1.97 +                  value_parts = nil
    1.98 +                  if field_name then
    1.99 +                    local values = post_params_list[field_name]
   1.100 +                    values[#values+1] = value
   1.101 +                    local metadata_entries = post_metadata_list[field_name]
   1.102 +                    metadata_entries[#metadata_entries+1] = metadata
   1.103 +                  end
   1.104 +                else
   1.105 +                  streamer()
   1.106 +                end
   1.107 +                headerdata   = ""
   1.108 +                streamer     = nil
   1.109 +                field_name   = nil
   1.110 +                metadata     = {}
   1.111 +              end
   1.112 +              local function stream_part_chunk(chunk)
   1.113 +                if streamer then
   1.114 +                  streamer(chunk)
   1.115 +                else
   1.116 +                  headerdata = headerdata .. chunk
   1.117 +                  while true do
   1.118 +                    local line, remaining = string.match(headerdata, "^(.-)\r?\n(.*)$")
   1.119 +                    if not line then
   1.120 +                      break
   1.121 +                    end
   1.122 +                    if line == "" then
   1.123 +                      streamer = streamed_post_params[field_name]
   1.124 +                      if not streamer then
   1.125 +                        for i, rule in ipairs(streamed_post_param_patterns) do
   1.126 +                          if string.match(field_name, rule[1]) then
   1.127 +                            streamer = rule[2]
   1.128 +                            break
   1.129 +                          end
   1.130 +                        end
   1.131 +                      end
   1.132 +                      if not streamer then
   1.133 +                        value_parts = {}
   1.134 +                        streamer = default_streamer
   1.135 +                      end
   1.136 +                      streamer(remaining, field_name, metadata)
   1.137 +                      return
   1.138 +                    end
   1.139 +                    headerdata = remaining
   1.140 +                    local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$")
   1.141 +                    if not header_key then
   1.142 +                      request_error(true, "400 Bad Request", "Invalid header in multipart/form-data part")
   1.143 +                    end
   1.144 +                    header_key = string.lower(header_key)
   1.145 +                    if header_key == "content-disposition" then
   1.146 +                      local escaped_header_value = string.gsub(header_value, '"[^"]*"', function(str)
   1.147 +                        return string.gsub(str, "=", "==")
   1.148 +                      end)
   1.149 +                      field_name = string.match(escaped_header_value, ';[ \t]*name="([^"]*)"')
   1.150 +                      if field_name then
   1.151 +                        field_name = string.gsub(field_name, "==", "=")
   1.152 +                      else
   1.153 +                        field_name = string.match(header_value, ';[ \t]*name=([^"; \t]+)')
   1.154 +                      end
   1.155 +                      metadata.file_name = string.match(escaped_header_value, ';[ \t]*filename="([^"]*)"')
   1.156 +                      if metadata.file_name then
   1.157 +                        metadata.file_name = string.gsub(metadata.file_name, "==", "=")
   1.158 +                      else
   1.159 +                        string.match(header_value, ';[ \t]*filename=([^"; \t]+)')
   1.160 +                      end
   1.161 +                    elseif header_key == "content-type" then
   1.162 +                      metadata.content_type = header_value
   1.163 +                    elseif header_key == "content-transfer-encoding" then
   1.164 +                      request_error(true, "400 Bad Request", "Content-transfer-encoding not supported by multipart/form-data parser")
   1.165 +                    end
   1.166 +                  end
   1.167 +                end
   1.168 +              end
   1.169 +              local skippart   = true   -- ignore data until first boundary
   1.170 +              local afterbound = false  -- interpret 2 bytes after boundary ("\r\n" or "--")
   1.171 +              local terminated = false  -- final boundary read
   1.172 +              local bigchunk = ""
   1.173 +              request:set_request_body_streamer(function(chunk)
   1.174 +                if chunk == nil then
   1.175 +                  if not terminated then
   1.176 +                    request_error(true, "400 Bad Request", "Premature end of multipart/form-data request body")
   1.177 +                  end
   1.178 +                  request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata
   1.179 +                end
   1.180 +                if terminated then
   1.181 +                  return
   1.182 +                end
   1.183 +                bigchunk = bigchunk .. chunk
   1.184 +                while true do
   1.185 +                  if afterbound then
   1.186 +                    if #bigchunk <= 2 then
   1.187 +                      return
   1.188 +                    end
   1.189 +                    local terminator = string.sub(bigchunk, 1, 2)
   1.190 +                    if terminator == "\r\n" then
   1.191 +                      afterbound = false
   1.192 +                      bigchunk = string.sub(bigchunk, 3)
   1.193 +                    elseif terminator == "--" then
   1.194 +                      terminated = true
   1.195 +                      bigchunk = nil
   1.196 +                      return
   1.197 +                    else
   1.198 +                      request_error(true, "400 Bad Request", "Error while parsing multipart body (expected CRLF or double minus)")
   1.199 +                    end
   1.200 +                  end
   1.201 +                  local pos1, pos2 = string.find(bigchunk, boundary, 1, true)
   1.202 +                  if not pos1 then
   1.203 +                    if not skippart then
   1.204 +                      local safe = #bigchunk-#boundary
   1.205 +                      if safe > 0 then
   1.206 +                        stream_part_chunk(string.sub(bigchunk, 1, safe))
   1.207 +                        bigchunk = string.sub(bigchunk, safe+1)
   1.208 +                      end
   1.209 +                    end
   1.210 +                    return
   1.211 +                  end
   1.212 +                  if not skippart then
   1.213 +                    stream_part_chunk(string.sub(bigchunk, 1, pos1 - 1))
   1.214 +                    stream_part_finish()
   1.215 +                  else
   1.216 +                    boundary = "\r\n" .. boundary
   1.217 +                    skippart = false
   1.218 +                  end
   1.219 +                  bigchunk = string.sub(bigchunk, pos2 + 1)
   1.220 +                  afterbound = true
   1.221 +                end
   1.222 +              end)
   1.223 +            else
   1.224 +              request_error(true, "415 Unsupported Media Type", "Unknown Content-Type of request body")
   1.225 +            end
   1.226 +          end
   1.227 +        end
   1.228 +      end
   1.229 +      -- function to prepare body processing:
   1.230        local function prepare()
   1.231          assert_not_faulty()
   1.232 +        if process_body_chunk == nil then
   1.233 +          default_request_body_handling()
   1.234 +        end
   1.235          if state ~= "init" then
   1.236            return
   1.237          end
   1.238 @@ -711,6 +885,74 @@
   1.239            error("Unexpected internal status in HTTP engine")
   1.240          end
   1.241        end
   1.242 +      -- method to register POST param stream handler for a single field name:
   1.243 +      function request:stream_post_param(field_name, callback)
   1.244 +        if state ~= "init" then
   1.245 +          error("Cannot setup request body streamer at this stage")
   1.246 +        end
   1.247 +        streamed_post_params[field_name] = callback
   1.248 +      end
   1.249 +      -- method to register POST param stream handler for a field name pattern:
   1.250 +      function request:stream_post_params(pattern, callback)
   1.251 +        if state ~= "init" then
   1.252 +          error("Cannot setup request body streamer at this stage")
   1.253 +        end
   1.254 +        streamed_post_param_patterns[#streamed_post_param_patterns+1] = {pattern, callback}
   1.255 +      end
   1.256 +      -- method to register request body stream handler
   1.257 +      function request:set_request_body_streamer(callback)
   1.258 +        if state ~= "init" then
   1.259 +          error("Cannot setup request body streamer at this stage")
   1.260 +        end
   1.261 +        local inprogress = false
   1.262 +        local buffer = {}
   1.263 +        process_body_chunk = function(chunk)
   1.264 +          if inprogress then
   1.265 +            buffer[#buffer+1] = chunk
   1.266 +          else
   1.267 +            inprogress = true
   1.268 +            callback(chunk)
   1.269 +            while #buffer > 0 do
   1.270 +              chunk = table.concat(buffer)
   1.271 +              buffer = {}
   1.272 +              callback(chunk)
   1.273 +            end
   1.274 +            inprogress = false
   1.275 +          end
   1.276 +        end
   1.277 +      end
   1.278 +      -- method to start reading request body
   1.279 +      function request:consume_input()
   1.280 +        prepare()
   1.281 +        consume_all()
   1.282 +      end
   1.283 +      -- method to stream request body
   1.284 +      function request:stream_request_body(callback)
   1.285 +        request:set_request_body_streamer(function(chunk)
   1.286 +          if chunk ~= nil then
   1.287 +            callback(chunk)
   1.288 +          end
   1.289 +        end)
   1.290 +        request:consume_input()
   1.291 +      end
   1.292 +      -- metamethod to read special attibutes of request object:
   1.293 +      function request_mt:__index(key, value)
   1.294 +        if key == "body" then
   1.295 +          local chunks = {}
   1.296 +          request:stream_request_body(function(chunk)
   1.297 +            chunks[#chunks+1] = chunk
   1.298 +          end)
   1.299 +          self.body = table.concat(chunks)
   1.300 +          return self.body
   1.301 +        elseif
   1.302 +          key == "post_params_list" or key == "post_params" or
   1.303 +          key == "post_metadata_list" or key == "post_metadata"
   1.304 +        then
   1.305 +          prepare()
   1.306 +          consume_all()
   1.307 +          return self[key]
   1.308 +        end
   1.309 +      end
   1.310        -- wait for input:
   1.311        if not poll(socket_set, nil, request_idle_timeout) then
   1.312          return request_error(false, "408 Request Timeout", "Idle connection timed out")

Impressum / About Us