moonbridge
changeset 159:bd7225b30391
Further work on new HTTP layer (not finished)
author | jbe |
---|---|
date | Fri Jun 05 19:53:41 2015 +0200 (2015-06-05) |
parents | 99a70d18e47c |
children | 573995950b0b |
files | moonbridge_http.lua |
line diff
1.1 --- a/moonbridge_http.lua Wed May 27 01:51:04 2015 +0200 1.2 +++ b/moonbridge_http.lua Fri Jun 05 19:53:41 2015 +0200 1.3 @@ -108,6 +108,98 @@ 1.4 return newtbl 1.5 end 1.6 1.7 +local headers_mt_self = setmetatable({}, {__mode="k"}) 1.8 + 1.9 +local headers_mts = { 1.10 + headers_mt = { 1.11 + __index = function(tbl, key) 1.12 + local self = headers_mt_self[tbl] 1.13 + local lowerkey = string.lower(key) 1.14 + local result = self._headers[lowerkey] 1.15 + if result == nil then 1.16 + result = {} 1.17 + end 1.18 + tbl[lowerkey] = result 1.19 + tbl[key] = result 1.20 + return result 1.21 + end 1.22 + }, 1.23 + -- table mapping header field names to value-lists 1.24 + -- (for headers with comma separated values): 1.25 + headers_csv_table = { 1.26 + __index = function(tbl, key) 1.27 + local self = headers_mt_self[tbl] 1.28 + local result = {} 1.29 + for i, line in ipairs(self.headers[key]) do 1.30 + for entry in string.gmatch(line, "[^,]+") do 1.31 + local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") 1.32 + if value then 1.33 + result[#result+1] = value 1.34 + end 1.35 + end 1.36 + end 1.37 + tbl[key] = result 1.38 + return result 1.39 + end 1.40 + }, 1.41 + -- table mapping header field names to a comma separated string 1.42 + -- (for headers with comma separated values): 1.43 + headers_csv_string = { 1.44 + __index = function(tbl, key) 1.45 + local self = headers_mt_self[tbl] 1.46 + local result = {} 1.47 + for i, line in ipairs(self.headers[key]) do 1.48 + result[#result+1] = line 1.49 + end 1.50 + result = string.concat(result, ", ") 1.51 + tbl[key] = result 1.52 + return result 1.53 + end 1.54 + }, 1.55 + -- table mapping header field names to a single string value 1.56 + -- (or false if header has been sent multiple times): 1.57 + headers_value = { 1.58 + __index = function(tbl, key) 1.59 + local self = headers_mt_self[tbl] 1.60 + if self._headers_value_nil[key] then 1.61 + return nil 1.62 + end 1.63 + local result = nil 1.64 + local values = self.headers_csv_table[key] 1.65 + if #values == 0 then 1.66 + self._headers_value_nil[key] = true 1.67 + elseif #values == 1 then 1.68 + result = values[1] 1.69 + else 1.70 + result = false 1.71 + end 1.72 + tbl[key] = result 1.73 + return result 1.74 + end 1.75 + }, 1.76 + -- table mapping header field names to a flag table, 1.77 + -- indicating if the comma separated value contains certain entries: 1.78 + headers_flags = { 1.79 + __index = function(tbl, key) 1.80 + local self = headers_mt_self[tbl] 1.81 + local result = setmetatable({}, { 1.82 + __index = function(tbl, key) 1.83 + local lowerkey = string.lower(key) 1.84 + local result = rawget(tbl, lowerkey) or false 1.85 + tbl[lowerkey] = result 1.86 + tbl[key] = result 1.87 + return result 1.88 + end 1.89 + }) 1.90 + for i, value in ipairs(self.headers_csv_table[key]) do 1.91 + result[string.lower(value)] = true 1.92 + end 1.93 + tbl[key] = result 1.94 + return result 1.95 + end 1.96 + } 1.97 +} 1.98 + 1.99 request_pt = {} 1.100 request_mt = { __index = request_pt } 1.101 1.102 @@ -165,111 +257,22 @@ 1.103 end 1.104 end 1.105 1.106 -function request_pt:_create_header_metatables() 1.107 - -- table mapping header field names to value-lists: 1.108 - self._headers_mt = { 1.109 - __index = function(tbl, key) 1.110 - local lowerkey = string.lower(key) 1.111 - local result = self._headers[lowerkey] 1.112 - if result == nil then 1.113 - result = {} 1.114 - end 1.115 - tbl[lowerkey] = result 1.116 - tbl[key] = result 1.117 - return result 1.118 - end 1.119 - } 1.120 - -- table mapping header field names to value-lists 1.121 - -- (for headers with comma separated values): 1.122 - self._headers_csv_table_mt = { 1.123 - __index = function(tbl, key) 1.124 - local result = {} 1.125 - for i, line in ipairs(self.headers[key]) do 1.126 - for entry in string.gmatch(line, "[^,]+") do 1.127 - local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") 1.128 - if value then 1.129 - result[#result+1] = value 1.130 - end 1.131 - end 1.132 - end 1.133 - tbl[key] = result 1.134 - return result 1.135 - end 1.136 - } 1.137 - -- table mapping header field names to a comma separated string 1.138 - -- (for headers with comma separated values): 1.139 - self._headers_csv_string_mt = { 1.140 - __index = function(tbl, key) 1.141 - local result = {} 1.142 - for i, line in ipairs(self.headers[key]) do 1.143 - result[#result+1] = line 1.144 - end 1.145 - result = string.concat(result, ", ") 1.146 - tbl[key] = result 1.147 - return result 1.148 - end 1.149 - } 1.150 - -- table mapping header field names to a single string value 1.151 - -- (or false if header has been sent multiple times): 1.152 - self._headers_value_mt = { 1.153 - __index = function(tbl, key) 1.154 - if self._headers_value_nil[key] then 1.155 - return nil 1.156 - end 1.157 - local result = nil 1.158 - local values = self.headers_csv_table[key] 1.159 - if #values == 0 then 1.160 - self._headers_value_nil[key] = true 1.161 - elseif #values == 1 then 1.162 - result = values[1] 1.163 - else 1.164 - result = false 1.165 - end 1.166 - tbl[key] = result 1.167 - return result 1.168 - end 1.169 - } 1.170 - -- table mapping header field names to a flag table, 1.171 - -- indicating if the comma separated value contains certain entries: 1.172 - self._headers_flags_mt = { 1.173 - __index = function(tbl, key) 1.174 - local result = setmetatable({}, { 1.175 - __index = function(tbl, key) 1.176 - local lowerkey = string.lower(key) 1.177 - local result = rawget(tbl, lowerkey) or false 1.178 - tbl[lowerkey] = result 1.179 - tbl[key] = result 1.180 - return result 1.181 - end 1.182 - }) 1.183 - for i, value in ipairs(self.headers_csv_table[key]) do 1.184 - result[string.lower(value)] = true 1.185 - end 1.186 - tbl[key] = result 1.187 - return result 1.188 - end 1.189 - } 1.190 -end 1.191 - 1.192 -function request_pt:_create_magictable(name) 1.193 - self[name] = setmetatable({}, self["_"..name.."_mt"]) 1.194 -end 1.195 - 1.196 function request_pt:_handler(socket) 1.197 self._socket = socket 1.198 self._survive = true 1.199 self._socket_set = {[socket] = true} 1.200 self._faulty = false 1.201 + self._state = "config" 1.202 self._connection_close_requested = false 1.203 self._connection_close_responded = false 1.204 - self:_create_magictable("headers") 1.205 - self:_create_magictable("headers_csv_table") 1.206 - self:_create_magictable("headers_csv_string") 1.207 - self:_create_magictable("headers_value") 1.208 - self:_create_magictable("headers_flags") 1.209 + for name, mt in pairs(headers_mts) do 1.210 + local tbl = setmetatable({}, mt) 1.211 + headers_mt_self[tbl] = self 1.212 + self[name] = tbl 1.213 + end 1.214 repeat 1.215 -- wait for input: 1.216 - if not moonbridge_io.poll(self._socket_set, nil, self._request_idle_timeout) then 1.217 + if not self._poll(self._socket_set, nil, self._request_idle_timeout) then 1.218 self:_error("408 Request Timeout", "Idle connection timed out") 1.219 return self._survive 1.220 end 1.221 @@ -299,7 +302,7 @@ 1.222 end 1.223 end 1.224 -- prepare reading of body: 1.225 - self._read_body_coro = coroutine.wrap(self._read_body) 1.226 + self._read_body_coro = coroutine.wrap(self._read_body) --TODO? 1.227 -- set timeout for application handler: 1.228 timeout(self._response_timeout or 0) 1.229 -- call application handler: 1.230 @@ -314,6 +317,177 @@ 1.231 return self._survive 1.232 end 1.233 1.234 +function request_pt:_prepare_body() 1.235 + self:_assert_not_faulty() 1.236 + if self._state == "prepare" then 1.237 + error("Unexpected state in HTTP module") 1.238 + elseif self._state ~= "config" then 1.239 + return 1.240 + end 1.241 + self._state = "prepare" 1.242 + local content_type = self.headers_value["Content-Type"] 1.243 + if content_type then 1.244 + if 1.245 + content_type == "application/x-www-form-urlencoded" or 1.246 + string.match(content_type, "^application/x%-www%-form%-urlencoded *;") 1.247 + then 1.248 + self._consume_all_input() 1.249 + self.post_params_list = read_urlencoded_form(self.body) 1.250 + else 1.251 + local boundary = string.match( 1.252 + content_type, 1.253 + '^multipart/form%-data[ \t]*[;,][ \t]*boundary="([^"]+)"$' 1.254 + ) or string.match( 1.255 + content_type, 1.256 + '^multipart/form%-data[ \t]*[;,][ \t]*boundary=([^"; \t]+)$' 1.257 + ) 1.258 + if boundary then 1.259 + self.post_metadata_list = {} 1.260 + boundary = "--" .. boundary 1.261 + local headerdata = "" 1.262 + local streamer 1.263 + local field_name 1.264 + local metadata = {} 1.265 + local value_parts 1.266 + local function default_streamer(chunk) 1.267 + value_parts[#value_parts+1] = chunk 1.268 + end 1.269 + local function stream_part_finish() 1.270 + if streamer == default_streamer then 1.271 + local value = table.concat(value_parts) 1.272 + value_parts = nil 1.273 + if field_name then 1.274 + local values = self.post_params_list[field_name] 1.275 + values[#values+1] = value 1.276 + local metadata_entries = post_metadata_list[field_name] 1.277 + metadata_entries[#metadata_entries+1] = metadata 1.278 + end 1.279 + else 1.280 + streamer() 1.281 + end 1.282 + headerdata = "" 1.283 + streamer = nil 1.284 + field_name = nil 1.285 + metadata = {} 1.286 + end 1.287 + local function stream_part_chunk(chunk) 1.288 + if streamer then 1.289 + streamer(chunk) 1.290 + else 1.291 + headerdata = headerdata .. chunk 1.292 + while true do 1.293 + local line, remaining = string.match(headerdata, "^(.-)\r?\n(.*)$") 1.294 + if not line then 1.295 + break 1.296 + end 1.297 + if line == "" then 1.298 + streamer = streamed_post_params[field_name] 1.299 + if not streamer then 1.300 + for i, rule in ipairs(streamed_post_param_patterns) do 1.301 + if string.match(field_name, rule[1]) then 1.302 + streamer = rule[2] 1.303 + break 1.304 + end 1.305 + end 1.306 + end 1.307 + if not streamer then 1.308 + value_parts = {} 1.309 + streamer = default_streamer 1.310 + end 1.311 + streamer(remaining, field_name, metadata) 1.312 + return 1.313 + end 1.314 + headerdata = remaining 1.315 + local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$") 1.316 + if not header_key then 1.317 + request_error(true, "400 Bad Request", "Invalid header in multipart/form-data part") 1.318 + end 1.319 + header_key = string.lower(header_key) 1.320 + if header_key == "content-disposition" then 1.321 + local escaped_header_value = string.gsub(header_value, '"[^"]*"', function(str) 1.322 + return string.gsub(str, "=", "==") 1.323 + end) 1.324 + field_name = string.match(escaped_header_value, ';[ \t]*name="([^"]*)"') 1.325 + if field_name then 1.326 + field_name = string.gsub(field_name, "==", "=") 1.327 + else 1.328 + field_name = string.match(header_value, ';[ \t]*name=([^"; \t]+)') 1.329 + end 1.330 + metadata.file_name = string.match(escaped_header_value, ';[ \t]*filename="([^"]*)"') 1.331 + if metadata.file_name then 1.332 + metadata.file_name = string.gsub(metadata.file_name, "==", "=") 1.333 + else 1.334 + string.match(header_value, ';[ \t]*filename=([^"; \t]+)') 1.335 + end 1.336 + elseif header_key == "content-type" then 1.337 + metadata.content_type = header_value 1.338 + elseif header_key == "content-transfer-encoding" then 1.339 + request_error(true, "400 Bad Request", "Content-transfer-encoding not supported by multipart/form-data parser") 1.340 + end 1.341 + end 1.342 + end 1.343 + end 1.344 + local skippart = true -- ignore data until first boundary 1.345 + local afterbound = false -- interpret 2 bytes after boundary ("\r\n" or "--") 1.346 + local terminated = false -- final boundary read 1.347 + local bigchunk = "" 1.348 + request:stream_request_body(function(chunk) 1.349 + if terminated then 1.350 + return 1.351 + end 1.352 + bigchunk = bigchunk .. chunk 1.353 + while true do 1.354 + if afterbound then 1.355 + if #bigchunk <= 2 then 1.356 + return 1.357 + end 1.358 + local terminator = string.sub(bigchunk, 1, 2) 1.359 + if terminator == "\r\n" then 1.360 + afterbound = false 1.361 + bigchunk = string.sub(bigchunk, 3) 1.362 + elseif terminator == "--" then 1.363 + terminated = true 1.364 + bigchunk = nil 1.365 + return 1.366 + else 1.367 + request_error(true, "400 Bad Request", "Error while parsing multipart body (expected CRLF or double minus)") 1.368 + end 1.369 + end 1.370 + local pos1, pos2 = string.find(bigchunk, boundary, 1, true) 1.371 + if not pos1 then 1.372 + if not skippart then 1.373 + local safe = #bigchunk-#boundary 1.374 + if safe > 0 then 1.375 + stream_part_chunk(string.sub(bigchunk, 1, safe)) 1.376 + bigchunk = string.sub(bigchunk, safe+1) 1.377 + end 1.378 + end 1.379 + return 1.380 + end 1.381 + if not skippart then 1.382 + stream_part_chunk(string.sub(bigchunk, 1, pos1 - 1)) 1.383 + stream_part_finish() 1.384 + else 1.385 + boundary = "\r\n" .. boundary 1.386 + skippart = false 1.387 + end 1.388 + bigchunk = string.sub(bigchunk, pos2 + 1) 1.389 + afterbound = true 1.390 + end 1.391 + end) 1.392 + if not terminated then 1.393 + request_error(true, "400 Bad Request", "Premature end of multipart/form-data request body") 1.394 + end 1.395 + request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata 1.396 + else 1.397 + request_error(true, "415 Unsupported Media Type", "Unknown Content-Type of request body") 1.398 + end 1.399 + end 1.400 + end 1.401 + self.post_params = get_first_values(self.post_params_list) 1.402 + self._state = "no_status_sent" 1.403 +end 1.404 + 1.405 function request_pt:_drain_input() 1.406 self._read_body_coro = "drain" 1.407 end