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 "<" 1.25 + elseif char == '>' then 1.26 + return ">" 1.27 + elseif char == '&' then 1.28 + return "&" 1.29 + elseif char == '"' then 1.30 + return """ 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 +