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")