moonbridge
changeset 4:583e2ad140dc
Renamed "http" module to "moonbridge_http"
author | jbe |
---|---|
date | Fri Jan 09 22:51:25 2015 +0100 (2015-01-09) |
parents | 3b3f0ecc4ac4 |
children | 4b6d7ca25381 |
files | example_application.lua http.lua moonbridge_http.lua |
line diff
1.1 --- a/example_application.lua Thu Jan 08 20:55:56 2015 +0100 1.2 +++ b/example_application.lua Fri Jan 09 22:51:25 2015 +0100 1.3 @@ -1,7 +1,7 @@ 1.4 -- Moonbridge example application 1.5 -- invoke with ./moonbridge example_application.lua 1.6 1.7 -local http = require "http" 1.8 +local http = require "moonbridge_http" 1.9 1.10 local documents = {"example_webpage.html", "example_webpage.css"} 1.11
2.1 --- a/http.lua Thu Jan 08 20:55:56 2015 +0100 2.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 2.3 @@ -1,883 +0,0 @@ 2.4 -#!/usr/bin/env lua 2.5 - 2.6 --- module preamble 2.7 -local _G, _M = _ENV, {} 2.8 -_ENV = setmetatable({}, { 2.9 - __index = function(self, key) 2.10 - local value = _M[key]; if value ~= nil then return value end 2.11 - return _G[key] 2.12 - end, 2.13 - __newindex = function(self, key, value) _M[key] = value end 2.14 -}) 2.15 - 2.16 --- function that encodes certain HTML entities: 2.17 --- (not used by the library itself) 2.18 -function encode_html(text) 2.19 - return ( 2.20 - string.gsub( 2.21 - text, '[<>&"]', 2.22 - function(char) 2.23 - if char == '<' then 2.24 - return "<" 2.25 - elseif char == '>' then 2.26 - return ">" 2.27 - elseif char == '&' then 2.28 - return "&" 2.29 - elseif char == '"' then 2.30 - return """ 2.31 - end 2.32 - end 2.33 - ) 2.34 - ) 2.35 - 2.36 -end 2.37 - 2.38 --- function that encodes special characters for URIs: 2.39 --- (not used by the library itself) 2.40 -function encode_uri(text) 2.41 - return ( 2.42 - string.gsub(text, "[^0-9A-Za-z_%.~-]", 2.43 - function (char) 2.44 - return string.format("%%%02x", string.byte(char)) 2.45 - end 2.46 - ) 2.47 - ) 2.48 -end 2.49 - 2.50 --- function undoing URL encoding: 2.51 -do 2.52 - local b0 = string.byte("0") 2.53 - local b9 = string.byte("9") 2.54 - local bA = string.byte("A") 2.55 - local bF = string.byte("F") 2.56 - local ba = string.byte("a") 2.57 - local bf = string.byte("f") 2.58 - function decode_uri(str) 2.59 - return ( 2.60 - string.gsub( 2.61 - string.gsub(str, "%+", " "), 2.62 - "%%([0-9A-Fa-f][0-9A-Fa-f])", 2.63 - function(hex) 2.64 - local n1, n2 = string.byte(hex, 1, 2) 2.65 - if n1 >= b0 and n1 <= b9 then n1 = n1 - b0 2.66 - elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10 2.67 - elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10 2.68 - else error("Assertion failed") end 2.69 - if n2 >= b0 and n2 <= b9 then n2 = n2 - b0 2.70 - elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10 2.71 - elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10 2.72 - else error("Assertion failed") end 2.73 - return string.char(n1 * 16 + n2) 2.74 - end 2.75 - ) 2.76 - ) 2.77 - end 2.78 -end 2.79 - 2.80 --- status codes that carry no response body (in addition to 1xx): 2.81 --- (set to "zero_content_length" if Content-Length header is required) 2.82 -status_without_response_body = { 2.83 - ["101"] = true, 2.84 - ["204"] = true, 2.85 - ["205"] = "zero_content_length", 2.86 - ["304"] = true 2.87 -} 2.88 - 2.89 --- handling of GET/POST param tables: 2.90 -local new_params_list -- defined later 2.91 -do 2.92 - local params_list_mapping = setmetatable({}, {__mode="k"}) 2.93 - local params_list_metatable = { 2.94 - __index = function(self, key) 2.95 - local tbl = {} 2.96 - self[key] = tbl 2.97 - return tbl 2.98 - end 2.99 - } 2.100 - local params_metatable = { 2.101 - __index = function(self, key) 2.102 - local value = params_list_mapping[self][key][1] 2.103 - self[key] = value 2.104 - return value 2.105 - end 2.106 - } 2.107 - -- returns a table to store key value-list pairs (i.e. multiple values), 2.108 - -- and a second table automatically mapping keys to the first value 2.109 - -- using the key value-list pairs in the first table: 2.110 - new_params_list = function() 2.111 - local params_list = setmetatable({}, params_list_metatable) 2.112 - local params = setmetatable({}, params_metatable) 2.113 - params_list_mapping[params] = params_list 2.114 - return params_list, params 2.115 - end 2.116 -end 2.117 --- parses URL encoded form data and stores it in 2.118 --- a key value-list pairs structure that has to be 2.119 --- previously obtained by calling by new_params_list(): 2.120 -local function read_urlencoded_form(tbl, data) 2.121 - for rawkey, rawvalue in string.gmatch(data, "([^=&]*)=([^=&]*)") do 2.122 - local subtbl = tbl[decode_uri(rawkey)] 2.123 - subtbl[#subtbl+1] = decode_uri(rawvalue) 2.124 - end 2.125 -end 2.126 - 2.127 --- function creating a HTTP handler: 2.128 -function generate_handler(handler, options) 2.129 - -- swap arguments if necessary (for convenience): 2.130 - if type(handler) ~= "function" and type(options) == "function" then 2.131 - handler, options = options, handler 2.132 - end 2.133 - -- process options: 2.134 - options = options or {} 2.135 - local preamble = "" -- preamble sent with every(!) HTTP response 2.136 - do 2.137 - -- named arg "static_headers" is used to create the preamble: 2.138 - local s = options.static_headers 2.139 - local t = {} 2.140 - if s then 2.141 - if type(s) == "string" then 2.142 - for line in string.gmatch(s, "[^\r\n]+") do 2.143 - t[#t+1] = line 2.144 - end 2.145 - else 2.146 - for i, kv in ipairs(options.static_headers) do 2.147 - if type(kv) == "string" then 2.148 - t[#t+1] = kv 2.149 - else 2.150 - t[#t+1] = kv[1] .. ": " .. kv[2] 2.151 - end 2.152 - end 2.153 - end 2.154 - end 2.155 - t[#t+1] = "" 2.156 - preamble = table.concat(t, "\r\n") 2.157 - end 2.158 - -- return connect handler: 2.159 - return function(socket) 2.160 - local survive = true -- set to false if process shall be terminated later 2.161 - while true do 2.162 - -- desired chunk sizes: 2.163 - local input_chunk_size = options.maximum_input_chunk_size or options.chunk_size or 16384 2.164 - local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024 2.165 - -- process named arguments "request_header_size_limit" and "request_body_size_limit": 2.166 - local remaining_header_size_limit = options.request_header_size_limit or 1024*1024 2.167 - local remaining_body_size_limit = options.request_body_size_limit or 64*1024*1024 2.168 - -- state variables for request handling: 2.169 - local output_state = "no_status_sent" -- one of: 2.170 - -- "no_status_sent" (initial state) 2.171 - -- "info_status_sent" (1xx status code has been sent) 2.172 - -- "bodyless_status_sent" (204/304 status code has been sent) 2.173 - -- "status_sent" (regular status code has been sent) 2.174 - -- "headers_sent" (headers have been terminated) 2.175 - -- "finished" (request has been answered completely) 2.176 - local content_length -- value of Content-Length header sent 2.177 - local bytes_sent = 0 -- number of bytes sent if Content-Length is set 2.178 - local chunk_parts = {} -- list of chunks to send 2.179 - local chunk_bytes = 0 -- sum of lengths of chunks to send 2.180 - local connection_close_requested = false 2.181 - local connection_close_responded = false 2.182 - local headers_value_nil = {} -- header values that are nil 2.183 - local request_body_content_length -- Content-Length of request body 2.184 - local input_state = "pending" -- one of: 2.185 - -- "pending" (request body has not been processed yet) 2.186 - -- "deferred" (request body processing is deferred) 2.187 - -- "reading" (request body is currently being read) 2.188 - -- "finished" (request body has been read) 2.189 - local streamed_post_params = {} -- mapping from POST field name to stream function 2.190 - local streamed_post_param_patterns = {} -- list of POST field pattern and stream function pairs 2.191 - -- object passed to handler (with methods, GET/POST params, etc.): 2.192 - local request 2.193 - -- reads a number of bytes from the socket, 2.194 - -- optionally feeding these bytes chunk-wise 2.195 - -- into a callback function: 2.196 - local function read_body_bytes(remaining, callback) 2.197 - while remaining > 0 do 2.198 - local limit 2.199 - if remaining > input_chunk_size then 2.200 - limit = input_chunk_size 2.201 - else 2.202 - limit = remaining 2.203 - end 2.204 - local chunk = socket:read(limit) 2.205 - if not chunk or #chunk ~= limit then 2.206 - error("Unexpected EOF while reading chunk of request body") 2.207 - end 2.208 - remaining = remaining - limit 2.209 - if callback then 2.210 - callback(chunk) 2.211 - end 2.212 - end 2.213 - end 2.214 - -- flushes or closes the socket (depending on 2.215 - -- whether "Connection: close" header was sent): 2.216 - local function finish_response() 2.217 - if connection_close_responded then 2.218 - -- close output stream: 2.219 - socket.output:close() 2.220 - -- wait for EOF of peer to avoid immediate TCP RST condition: 2.221 - timeout(2, function() 2.222 - while socket.input:read(input_chunk_size) do end 2.223 - end) 2.224 - -- fully close socket: 2.225 - socket:close() 2.226 - else 2.227 - socket:flush() 2.228 - request:stream_request_body() 2.229 - end 2.230 - end 2.231 - -- writes out buffered chunks (without flushing the socket): 2.232 - local function send_chunk() 2.233 - if chunk_bytes > 0 then 2.234 - socket:write(string.format("%x\r\n", chunk_bytes)) 2.235 - for i = 1, #chunk_parts do 2.236 - socket:write(chunk_parts[i]) 2.237 - end 2.238 - chunk_parts = {} 2.239 - chunk_bytes = 0 2.240 - socket:write("\r\n") 2.241 - end 2.242 - end 2.243 - -- terminate header section in response, optionally flushing: 2.244 - -- (may be called multiple times unless response is finished) 2.245 - local function finish_headers(flush) 2.246 - if output_state == "no_status_sent" then 2.247 - error("HTTP status has not been sent yet") 2.248 - elseif output_state == "finished" then 2.249 - error("Response has already been finished") 2.250 - elseif output_state == "info_status_sent" then 2.251 - socket:write("\r\n") 2.252 - socket:flush() 2.253 - output_state = "no_status_sent" 2.254 - elseif output_state == "bodyless_status_sent" then 2.255 - if connection_close_requested and not connection_close_responded then 2.256 - request:send_header("Connection", "close") 2.257 - end 2.258 - socket:write("\r\n") 2.259 - finish_response() 2.260 - output_state = "finished" 2.261 - elseif output_state == "status_sent" then 2.262 - if not content_length then 2.263 - socket:write("Transfer-Encoding: chunked\r\n") 2.264 - end 2.265 - if connection_close_requested and not connection_close_responded then 2.266 - request:send_header("Connection", "close") 2.267 - end 2.268 - socket:write("\r\n") 2.269 - if request.method == "HEAD" then 2.270 - finish_response() 2.271 - elseif flush then 2.272 - socket:flush() 2.273 - end 2.274 - output_state = "headers_sent" 2.275 - elseif output_state ~= "headers_sent" then 2.276 - error("Unexpected internal status in HTTP engine") 2.277 - end 2.278 - end 2.279 - -- create request object and set several functions and values: 2.280 - request = { 2.281 - -- allow raw socket access: 2.282 - socket = socket, 2.283 - -- parsed cookies: 2.284 - cookies = {}, 2.285 - -- send a HTTP response status (e.g. "200 OK"): 2.286 - send_status = function(self, value) 2.287 - if input_state == "pending" then 2.288 - request:process_request_body() 2.289 - end 2.290 - if output_state == "info_status_sent" then 2.291 - socket:write("\r\n") 2.292 - socket:flush() 2.293 - elseif output_state ~= "no_status_sent" then 2.294 - error("HTTP status has already been sent") 2.295 - end 2.296 - local status1 = string.sub(value, 1, 1) 2.297 - local status3 = string.sub(value, 1, 3) 2.298 - socket:write("HTTP/1.1 ", value, "\r\n", preamble) 2.299 - local without_response_body = status_without_response_body[status3] 2.300 - if without_response_body then 2.301 - output_state = "bodyless_status_sent" 2.302 - if without_response_body == "zero_content_length" then 2.303 - request:send_header("Content-Length", 0) 2.304 - end 2.305 - elseif status1 == "1" then 2.306 - output_state = "info_status_sent" 2.307 - else 2.308 - output_state = "status_sent" 2.309 - end 2.310 - end, 2.311 - -- send a HTTP response header 2.312 - -- (key and value as separate args): 2.313 - send_header = function(self, key, value) 2.314 - if output_state == "no_status_sent" then 2.315 - error("HTTP status has not been sent yet") 2.316 - elseif 2.317 - output_state ~= "info_status_sent" and 2.318 - output_state ~= "bodyless_status_sent" and 2.319 - output_state ~= "status_sent" 2.320 - then 2.321 - error("All HTTP headers have already been sent") 2.322 - end 2.323 - local key_lower = string.lower(key) 2.324 - if key_lower == "content-length" then 2.325 - if output_state == "info_status_sent" then 2.326 - error("Cannot set Content-Length for informational status response") 2.327 - end 2.328 - local new_content_length = assert(tonumber(value), "Invalid content-length") 2.329 - if content_length == nil then 2.330 - content_length = new_content_length 2.331 - elseif content_length == new_content_length then 2.332 - return 2.333 - else 2.334 - error("Content-Length has been set multiple times with different values") 2.335 - end 2.336 - elseif key_lower == "connection" then 2.337 - for entry in string.gmatch(string.lower(value), "[^,]+") do 2.338 - if string.match(entry, "^[ \t]*close[ \t]*$") then 2.339 - if output_state == "info_status_sent" then 2.340 - error("Cannot set \"Connection: close\" for informational status response") 2.341 - end 2.342 - if connection_close_responded then 2.343 - return 2.344 - else 2.345 - connection_close_responded = true 2.346 - break 2.347 - end 2.348 - end 2.349 - end 2.350 - end 2.351 - socket:write(key, ": ", value, "\r\n") 2.352 - end, 2.353 - -- method to finish and flush headers: 2.354 - finish_headers = function() 2.355 - finish_headers(true) 2.356 - end, 2.357 - -- send data for response body: 2.358 - send_data = function(self, ...) 2.359 - if output_state == "info_status_sent" then 2.360 - error("No (non-informational) HTTP status has been sent yet") 2.361 - elseif output_state == "bodyless_status_sent" then 2.362 - error("Cannot send response data for body-less status message") 2.363 - end 2.364 - finish_headers(false) 2.365 - if output_state ~= "headers_sent" then 2.366 - error("Unexpected internal status in HTTP engine") 2.367 - end 2.368 - if request.method == "HEAD" then 2.369 - return 2.370 - end 2.371 - for i = 1, select("#", ...) do 2.372 - local str = tostring(select(i, ...)) 2.373 - if #str > 0 then 2.374 - if content_length then 2.375 - local bytes_to_send = #str 2.376 - if bytes_sent + bytes_to_send > content_length then 2.377 - socket:write(string.sub(str, 1, content_length - bytes_sent)) 2.378 - bytes_sent = content_length 2.379 - error("Content length exceeded") 2.380 - else 2.381 - socket:write(str) 2.382 - bytes_sent = bytes_sent + bytes_to_send 2.383 - end 2.384 - else 2.385 - chunk_bytes = chunk_bytes + #str 2.386 - chunk_parts[#chunk_parts+1] = str 2.387 - end 2.388 - end 2.389 - end 2.390 - if chunk_bytes >= output_chunk_size then 2.391 - send_chunk() 2.392 - end 2.393 - end, 2.394 - -- flush output buffer: 2.395 - flush = function(self) 2.396 - send_chunk() 2.397 - socket:flush() 2.398 - end, 2.399 - -- finish response: 2.400 - finish = function(self) 2.401 - if output_state == "finished" then 2.402 - return 2.403 - elseif output_state == "info_status_sent" then 2.404 - error("Informational HTTP response can be finished with :finish_headers() method") 2.405 - end 2.406 - finish_headers(false) 2.407 - if output_state == "headers_sent" then 2.408 - if request.method ~= "HEAD" then 2.409 - if content_length then 2.410 - if bytes_sent ~= content_length then 2.411 - error("Content length not used") 2.412 - end 2.413 - else 2.414 - send_chunk() 2.415 - socket:write("0\r\n\r\n") 2.416 - end 2.417 - finish_response() 2.418 - end 2.419 - output_state = "finished" 2.420 - elseif output_state ~= finished then 2.421 - error("Unexpected internal status in HTTP engine") 2.422 - end 2.423 - end, 2.424 - -- table mapping header field names to value-lists 2.425 - -- (raw access): 2.426 - headers = setmetatable({}, { 2.427 - __index = function(self, key) 2.428 - local lowerkey = string.lower(key) 2.429 - if lowerkey == key then 2.430 - return 2.431 - end 2.432 - local result = rawget(self, lowerkey) 2.433 - if result == nil then 2.434 - result = {} 2.435 - end 2.436 - self[lowerkey] = result 2.437 - self[key] = result 2.438 - return result 2.439 - end 2.440 - }), 2.441 - -- table mapping header field names to value-lists 2.442 - -- (for headers with comma separated values): 2.443 - headers_csv_table = setmetatable({}, { 2.444 - __index = function(self, key) 2.445 - local result = {} 2.446 - for i, line in ipairs(request.headers[key]) do 2.447 - for entry in string.gmatch(line, "[^,]+") do 2.448 - local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") 2.449 - if value then 2.450 - result[#result+1] = value 2.451 - end 2.452 - end 2.453 - end 2.454 - self[key] = result 2.455 - return result 2.456 - end 2.457 - }), 2.458 - -- table mapping header field names to a comma separated string 2.459 - -- (for headers with comma separated values): 2.460 - headers_csv_string = setmetatable({}, { 2.461 - __index = function(self, key) 2.462 - local result = {} 2.463 - for i, line in ipairs(request.headers[key]) do 2.464 - result[#result+1] = line 2.465 - end 2.466 - result = string.concat(result, ", ") 2.467 - self[key] = result 2.468 - return result 2.469 - end 2.470 - }), 2.471 - -- table mapping header field names to a single string value 2.472 - -- (or false if header has been sent multiple times): 2.473 - headers_value = setmetatable({}, { 2.474 - __index = function(self, key) 2.475 - if headers_value_nil[key] then 2.476 - return nil 2.477 - end 2.478 - local result = nil 2.479 - local values = request.headers_csv_table[key] 2.480 - if #values == 0 then 2.481 - headers_value_nil[key] = true 2.482 - elseif #values == 1 then 2.483 - result = values[1] 2.484 - else 2.485 - result = false 2.486 - end 2.487 - self[key] = result 2.488 - return result 2.489 - end 2.490 - }), 2.491 - -- table mapping header field names to a flag table, 2.492 - -- indicating if the comma separated value contains certain entries: 2.493 - headers_flags = setmetatable({}, { 2.494 - __index = function(self, key) 2.495 - local result = setmetatable({}, { 2.496 - __index = function(self, key) 2.497 - local lowerkey = string.lower(key) 2.498 - local result = rawget(self, lowerkey) or false 2.499 - self[lowerkey] = result 2.500 - self[key] = result 2.501 - return result 2.502 - end 2.503 - }) 2.504 - for i, value in ipairs(request.headers_csv_table[key]) do 2.505 - result[string.lower(value)] = true 2.506 - end 2.507 - self[key] = result 2.508 - return result 2.509 - end 2.510 - }), 2.511 - -- register POST param stream handler for a single field name: 2.512 - stream_post_param = function(self, field_name, callback) 2.513 - if input_state == "inprogress" or input_state == "finished" then 2.514 - error("Cannot register POST param streaming function if request body is already processed") 2.515 - end 2.516 - streamed_post_params[field_name] = callback 2.517 - end, 2.518 - -- register POST param stream handler for a field name pattern: 2.519 - stream_post_params = function(self, pattern, callback) 2.520 - if input_state == "inprogress" or input_state == "finished" then 2.521 - error("Cannot register POST param streaming function if request body is already processed") 2.522 - end 2.523 - streamed_post_param_patterns[#streamed_post_param_patterns+1] = {pattern, callback} 2.524 - end, 2.525 - -- disables automatic request body processing on write 2.526 - -- (use with caution): 2.527 - defer_reading = function(self) 2.528 - if input_state == "pending" then 2.529 - input_state = "deferred" 2.530 - end 2.531 - end, 2.532 - -- processes the request body and sets the request.post_params, 2.533 - -- request.post_params_list, request.meta_post_params, and 2.534 - -- request.meta_post_params_list values (can be called manually or 2.535 - -- automatically if post_params are accessed or data is written out) 2.536 - process_request_body = function(self) 2.537 - if input_state == "finished" then 2.538 - return 2.539 - end 2.540 - local post_params_list, post_params = new_params_list() 2.541 - local content_type = request.headers_value["Content-Type"] 2.542 - if content_type then 2.543 - if 2.544 - content_type == "application/x-www-form-urlencoded" or 2.545 - string.match(content_type, "^application/x%-www%-form%-urlencoded *;") 2.546 - then 2.547 - read_urlencoded_form(post_params_list, request.body) 2.548 - else 2.549 - local boundary = string.match( 2.550 - content_type, 2.551 - '^multipart/form%-data[ \t]*[;,][ \t]*boundary="([^"]+)"$' 2.552 - ) or string.match( 2.553 - content_type, 2.554 - '^multipart/form%-data[ \t]*[;,][ \t]*boundary=([^"; \t]+)$' 2.555 - ) 2.556 - if boundary then 2.557 - local post_metadata_list, post_metadata = new_params_list() 2.558 - boundary = "--" .. boundary 2.559 - local headerdata = "" 2.560 - local streamer 2.561 - local field_name 2.562 - local metadata = {} 2.563 - local value_parts 2.564 - local function default_streamer(chunk) 2.565 - value_parts[#value_parts+1] = chunk 2.566 - end 2.567 - local function stream_part_finish() 2.568 - if streamer == default_streamer then 2.569 - local value = table.concat(value_parts) 2.570 - value_parts = nil 2.571 - if field_name then 2.572 - local values = post_params_list[field_name] 2.573 - values[#values+1] = value 2.574 - local metadata_entries = post_metadata_list[field_name] 2.575 - metadata_entries[#metadata_entries+1] = metadata 2.576 - end 2.577 - else 2.578 - streamer() 2.579 - end 2.580 - headerdata = "" 2.581 - streamer = nil 2.582 - field_name = nil 2.583 - metadata = {} 2.584 - end 2.585 - local function stream_part_chunk(chunk) 2.586 - if streamer then 2.587 - streamer(chunk) 2.588 - else 2.589 - headerdata = headerdata .. chunk 2.590 - while true do 2.591 - local line, remaining = string.match(headerdata, "^(.-)\r?\n(.*)$") 2.592 - if not line then 2.593 - break 2.594 - end 2.595 - if line == "" then 2.596 - streamer = streamed_post_params[field_name] 2.597 - if not streamer then 2.598 - for i, rule in ipairs(streamed_post_param_patterns) do 2.599 - if string.match(field_name, rule[1]) then 2.600 - streamer = rule[2] 2.601 - break 2.602 - end 2.603 - end 2.604 - end 2.605 - if not streamer then 2.606 - value_parts = {} 2.607 - streamer = default_streamer 2.608 - end 2.609 - streamer(remaining, field_name, metadata) 2.610 - return 2.611 - end 2.612 - headerdata = remaining 2.613 - local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$") 2.614 - if not header_key then 2.615 - error("Invalid header in multipart/form-data part") 2.616 - end 2.617 - header_key = string.lower(header_key) 2.618 - if header_key == "content-disposition" then 2.619 - local escaped_header_value = string.gsub(header_value, '"[^"]*"', function(str) 2.620 - return string.gsub(str, "=", "==") 2.621 - end) 2.622 - field_name = string.match(escaped_header_value, ';[ \t]*name="([^"]*)"') 2.623 - if field_name then 2.624 - field_name = string.gsub(field_name, "==", "=") 2.625 - else 2.626 - field_name = string.match(header_value, ';[ \t]*name=([^"; \t]+)') 2.627 - end 2.628 - metadata.file_name = string.match(escaped_header_value, ';[ \t]*filename="([^"]*)"') 2.629 - if metadata.file_name then 2.630 - metadata.file_name = string.gsub(metadata.file_name, "==", "=") 2.631 - else 2.632 - string.match(header_value, ';[ \t]*filename=([^"; \t]+)') 2.633 - end 2.634 - elseif header_key == "content-type" then 2.635 - metadata.content_type = header_value 2.636 - elseif header_key == "content-transfer-encoding" then 2.637 - error("Content-transfer-encoding not supported by multipart/form-data parser") 2.638 - end 2.639 - end 2.640 - end 2.641 - end 2.642 - local skippart = true -- ignore data until first boundary 2.643 - local afterbound = false -- interpret 2 bytes after boundary ("\r\n" or "--") 2.644 - local terminated = false -- final boundary read 2.645 - local bigchunk = "" 2.646 - request:stream_request_body(function(chunk) 2.647 - if terminated then 2.648 - return 2.649 - end 2.650 - bigchunk = bigchunk .. chunk 2.651 - while true do 2.652 - if afterbound then 2.653 - if #bigchunk <= 2 then 2.654 - return 2.655 - end 2.656 - local terminator = string.sub(bigchunk, 1, 2) 2.657 - if terminator == "\r\n" then 2.658 - afterbound = false 2.659 - bigchunk = string.sub(bigchunk, 3) 2.660 - elseif terminator == "--" then 2.661 - terminated = true 2.662 - bigchunk = nil 2.663 - return 2.664 - else 2.665 - error("Error while parsing multipart body (expected CRLF or double minus)") 2.666 - end 2.667 - end 2.668 - local pos1, pos2 = string.find(bigchunk, boundary, 1, true) 2.669 - if not pos1 then 2.670 - if not skippart then 2.671 - local safe = #bigchunk-#boundary 2.672 - if safe > 0 then 2.673 - stream_part_chunk(string.sub(bigchunk, 1, safe)) 2.674 - bigchunk = string.sub(bigchunk, safe+1) 2.675 - end 2.676 - end 2.677 - return 2.678 - end 2.679 - if not skippart then 2.680 - stream_part_chunk(string.sub(bigchunk, 1, pos1 - 1)) 2.681 - stream_part_finish() 2.682 - else 2.683 - boundary = "\r\n" .. boundary 2.684 - skippart = false 2.685 - end 2.686 - bigchunk = string.sub(bigchunk, pos2 + 1) 2.687 - afterbound = true 2.688 - end 2.689 - end) 2.690 - if not terminated then 2.691 - error("Premature end of multipart/form-data request body") 2.692 - end 2.693 - request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata 2.694 - else 2.695 - error("Unknown Content-Type of request body") 2.696 - end 2.697 - end 2.698 - end 2.699 - request.post_params_list, request.post_params = post_params_list, post_params 2.700 - end, 2.701 - -- stream request body to an (optional) callback function 2.702 - -- without processing it otherwise: 2.703 - stream_request_body = function(self, callback) 2.704 - if input_state ~= "pending" and input_state ~= "deferred" then 2.705 - if callback then 2.706 - if input_state == "inprogress" then 2.707 - error("Request body is already being processed") 2.708 - else 2.709 - error("Request body has already been processed") 2.710 - end 2.711 - end 2.712 - return 2.713 - end 2.714 - input_state = "inprogress" 2.715 - if request.headers_flags["Expect"]["100-continue"] then 2.716 - request:send_status("100 Continue") 2.717 - request:finish_headers() 2.718 - end 2.719 - if request.headers_flags["Transfer-Encoding"]["chunked"] then 2.720 - while true do 2.721 - local line = socket:readuntil("\n", 32 + remaining_body_size_limit) 2.722 - if not line then 2.723 - error("Unexpected EOF while reading next chunk of request body") 2.724 - end 2.725 - local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$") 2.726 - local chunkext 2.727 - if lenstr then 2.728 - chunkext = "" 2.729 - else 2.730 - zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$") 2.731 - end 2.732 - if not lenstr or #lenstr > 13 then 2.733 - error("Encoding error or unexpected EOF while reading chunk of request body") 2.734 - end 2.735 - local len = tonumber("0x" .. lenstr) 2.736 - remaining_body_size_limit = remaining_body_size_limit - (#zeros + #chunkext + len) 2.737 - if remaining_body_size_limit < 0 then 2.738 - error("Request body size limit exceeded") 2.739 - end 2.740 - if len == 0 then break end 2.741 - read_body_bytes(len, callback) 2.742 - local term = socket:readuntil("\n", 2) 2.743 - if term ~= "\r\n" and term ~= "\n" then 2.744 - error("Encoding error while reading chunk of request body") 2.745 - end 2.746 - end 2.747 - while true do 2.748 - local line = socket:readuntil("\n", 2 + remaining_body_size_limit) 2.749 - if line == "\r\n" or line == "\n" then break end 2.750 - remaining_body_size_limit = remaining_body_size_limit - #line 2.751 - if remaining_body_size_limit < 0 then 2.752 - error("Request body size limit exceeded while reading trailer section of chunked request body") 2.753 - end 2.754 - end 2.755 - elseif request_body_content_length then 2.756 - read_body_bytes(request_body_content_length, callback) 2.757 - end 2.758 - input_state = "finished" 2.759 - end 2.760 - } 2.761 - -- initialize tables for GET params in request object: 2.762 - request.get_params_list, request.get_params = new_params_list() 2.763 - -- add meta table to request object to allow access to "body" and POST params: 2.764 - setmetatable(request, { 2.765 - __index = function(self, key) 2.766 - if key == "body" then 2.767 - local chunks = {} 2.768 - request:stream_request_body(function(chunk) 2.769 - chunks[#chunks+1] = chunk 2.770 - end) 2.771 - self.body = table.concat(chunks) 2.772 - return self.body 2.773 - elseif 2.774 - key == "post_params_list" or key == "post_params" or 2.775 - key == "post_metadata_list" or key == "post_metadata" 2.776 - then 2.777 - request:process_request_body() 2.778 - return request[key] 2.779 - end 2.780 - end 2.781 - }) 2.782 - -- low level HTTP error response (for malformed requests, etc.): 2.783 - local function error_response(status, text) 2.784 - request:send_status(status) 2.785 - request:send_header("Content-Type", "text/plain") 2.786 - if not connection_close_responded then 2.787 - request:send_header("Connection", "close") 2.788 - end 2.789 - request:send_data(status, "\n") 2.790 - if text then 2.791 - request:send_data("\n", text, "\n") 2.792 - end 2.793 - request:finish() 2.794 - return survive 2.795 - end 2.796 - -- read and parse request line: 2.797 - local line = socket:readuntil("\n", remaining_header_size_limit) 2.798 - if not line then return survive end 2.799 - remaining_header_size_limit = remaining_header_size_limit - #line 2.800 - if remaining_header_size_limit == 0 then 2.801 - return error_response("413 Request Entity Too Large", "Request line too long") 2.802 - end 2.803 - local proto 2.804 - request.method, request.url, proto = 2.805 - line:match("^([^ \t\r]+)[ \t]+([^ \t\r]+)[ \t]*([^ \t\r]*)[ \t]*\r?\n$") 2.806 - if not request.method then 2.807 - return error_response("400 Bad Request") 2.808 - elseif proto ~= "HTTP/1.1" then 2.809 - return error_response("505 HTTP Version Not Supported") 2.810 - else 2.811 - -- read and parse headers: 2.812 - while true do 2.813 - local line = socket:readuntil("\n", remaining_header_size_limit); 2.814 - remaining_header_size_limit = remaining_header_size_limit - #line 2.815 - if not line then 2.816 - return error_response("400 Bad Request") 2.817 - end 2.818 - if line == "\r\n" or line == "\n" then 2.819 - break 2.820 - end 2.821 - if remaining_header_size_limit == 0 then 2.822 - return error_response("413 Request Entity Too Large", "Headers too long") 2.823 - end 2.824 - local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$") 2.825 - if not key then 2.826 - return error_response("400 Bad Request") 2.827 - end 2.828 - local values = request.headers[key] 2.829 - values[#values+1] = value 2.830 - end 2.831 - -- process "Connection: close" header if existent: 2.832 - connection_close_requested = request.headers_flags["Connection"]["close"] 2.833 - -- process "Content-Length" header if existent: 2.834 - do 2.835 - local values = request.headers_csv_table["Content-Length"] 2.836 - if #values > 0 then 2.837 - request_body_content_length = tonumber(values[1]) 2.838 - local proper_value = tostring(request_body_content_length) 2.839 - for i, value in ipairs(values) do 2.840 - value = string.match(value, "^0*(.*)") 2.841 - if value ~= proper_value then 2.842 - return error_response("400 Bad Request", "Content-Length header(s) invalid") 2.843 - end 2.844 - end 2.845 - if request_body_content_length > remaining_body_size_limit then 2.846 - return error_response("413 Request Entity Too Large", "Request body too big") 2.847 - end 2.848 - end 2.849 - end 2.850 - -- process "Transfer-Encoding" header if existent: 2.851 - do 2.852 - local flag = request.headers_flags["Transfer-Encoding"]["chunked"] 2.853 - local list = request.headers_csv_table["Transfer-Encoding"] 2.854 - if (flag and #list ~= 1) or (not flag and #list ~= 0) then 2.855 - return error_response("400 Bad Request", "Unexpected Transfer-Encoding") 2.856 - end 2.857 - end 2.858 - -- process "Expect" header if existent: 2.859 - for i, value in ipairs(request.headers_csv_table["Expect"]) do 2.860 - if string.lower(value) ~= "100-continue" then 2.861 - return error_response("417 Expectation Failed", "Unexpected Expect header") 2.862 - end 2.863 - end 2.864 - -- parse GET params: 2.865 - request.path, request.query = string.match(request.url, "^([^?]*)%??(.*)$") 2.866 - read_urlencoded_form(request.get_params_list, request.query) 2.867 - -- parse cookies: 2.868 - for i, line in ipairs(request.headers["Cookie"]) do 2.869 - for rawkey, rawvalue in 2.870 - string.gmatch(line, "([^=; ]*)=([^=; ]*)") 2.871 - do 2.872 - request.cookies[decode_uri(rawkey)] = decode_uri(rawvalue) 2.873 - end 2.874 - end 2.875 - -- call underlying handler and remember boolean result: 2.876 - if handler(request) ~= true then survive = false end 2.877 - -- finish request (unless already done by underlying handler): 2.878 - request:finish() 2.879 - end 2.880 - end 2.881 - return survive 2.882 - end 2.883 -end 2.884 - 2.885 -return _M 2.886 -
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/moonbridge_http.lua Fri Jan 09 22:51:25 2015 +0100 3.3 @@ -0,0 +1,883 @@ 3.4 +#!/usr/bin/env lua 3.5 + 3.6 +-- module preamble 3.7 +local _G, _M = _ENV, {} 3.8 +_ENV = setmetatable({}, { 3.9 + __index = function(self, key) 3.10 + local value = _M[key]; if value ~= nil then return value end 3.11 + return _G[key] 3.12 + end, 3.13 + __newindex = function(self, key, value) _M[key] = value end 3.14 +}) 3.15 + 3.16 +-- function that encodes certain HTML entities: 3.17 +-- (not used by the library itself) 3.18 +function encode_html(text) 3.19 + return ( 3.20 + string.gsub( 3.21 + text, '[<>&"]', 3.22 + function(char) 3.23 + if char == '<' then 3.24 + return "<" 3.25 + elseif char == '>' then 3.26 + return ">" 3.27 + elseif char == '&' then 3.28 + return "&" 3.29 + elseif char == '"' then 3.30 + return """ 3.31 + end 3.32 + end 3.33 + ) 3.34 + ) 3.35 + 3.36 +end 3.37 + 3.38 +-- function that encodes special characters for URIs: 3.39 +-- (not used by the library itself) 3.40 +function encode_uri(text) 3.41 + return ( 3.42 + string.gsub(text, "[^0-9A-Za-z_%.~-]", 3.43 + function (char) 3.44 + return string.format("%%%02x", string.byte(char)) 3.45 + end 3.46 + ) 3.47 + ) 3.48 +end 3.49 + 3.50 +-- function undoing URL encoding: 3.51 +do 3.52 + local b0 = string.byte("0") 3.53 + local b9 = string.byte("9") 3.54 + local bA = string.byte("A") 3.55 + local bF = string.byte("F") 3.56 + local ba = string.byte("a") 3.57 + local bf = string.byte("f") 3.58 + function decode_uri(str) 3.59 + return ( 3.60 + string.gsub( 3.61 + string.gsub(str, "%+", " "), 3.62 + "%%([0-9A-Fa-f][0-9A-Fa-f])", 3.63 + function(hex) 3.64 + local n1, n2 = string.byte(hex, 1, 2) 3.65 + if n1 >= b0 and n1 <= b9 then n1 = n1 - b0 3.66 + elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10 3.67 + elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10 3.68 + else error("Assertion failed") end 3.69 + if n2 >= b0 and n2 <= b9 then n2 = n2 - b0 3.70 + elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10 3.71 + elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10 3.72 + else error("Assertion failed") end 3.73 + return string.char(n1 * 16 + n2) 3.74 + end 3.75 + ) 3.76 + ) 3.77 + end 3.78 +end 3.79 + 3.80 +-- status codes that carry no response body (in addition to 1xx): 3.81 +-- (set to "zero_content_length" if Content-Length header is required) 3.82 +status_without_response_body = { 3.83 + ["101"] = true, 3.84 + ["204"] = true, 3.85 + ["205"] = "zero_content_length", 3.86 + ["304"] = true 3.87 +} 3.88 + 3.89 +-- handling of GET/POST param tables: 3.90 +local new_params_list -- defined later 3.91 +do 3.92 + local params_list_mapping = setmetatable({}, {__mode="k"}) 3.93 + local params_list_metatable = { 3.94 + __index = function(self, key) 3.95 + local tbl = {} 3.96 + self[key] = tbl 3.97 + return tbl 3.98 + end 3.99 + } 3.100 + local params_metatable = { 3.101 + __index = function(self, key) 3.102 + local value = params_list_mapping[self][key][1] 3.103 + self[key] = value 3.104 + return value 3.105 + end 3.106 + } 3.107 + -- returns a table to store key value-list pairs (i.e. multiple values), 3.108 + -- and a second table automatically mapping keys to the first value 3.109 + -- using the key value-list pairs in the first table: 3.110 + new_params_list = function() 3.111 + local params_list = setmetatable({}, params_list_metatable) 3.112 + local params = setmetatable({}, params_metatable) 3.113 + params_list_mapping[params] = params_list 3.114 + return params_list, params 3.115 + end 3.116 +end 3.117 +-- parses URL encoded form data and stores it in 3.118 +-- a key value-list pairs structure that has to be 3.119 +-- previously obtained by calling by new_params_list(): 3.120 +local function read_urlencoded_form(tbl, data) 3.121 + for rawkey, rawvalue in string.gmatch(data, "([^=&]*)=([^=&]*)") do 3.122 + local subtbl = tbl[decode_uri(rawkey)] 3.123 + subtbl[#subtbl+1] = decode_uri(rawvalue) 3.124 + end 3.125 +end 3.126 + 3.127 +-- function creating a HTTP handler: 3.128 +function generate_handler(handler, options) 3.129 + -- swap arguments if necessary (for convenience): 3.130 + if type(handler) ~= "function" and type(options) == "function" then 3.131 + handler, options = options, handler 3.132 + end 3.133 + -- process options: 3.134 + options = options or {} 3.135 + local preamble = "" -- preamble sent with every(!) HTTP response 3.136 + do 3.137 + -- named arg "static_headers" is used to create the preamble: 3.138 + local s = options.static_headers 3.139 + local t = {} 3.140 + if s then 3.141 + if type(s) == "string" then 3.142 + for line in string.gmatch(s, "[^\r\n]+") do 3.143 + t[#t+1] = line 3.144 + end 3.145 + else 3.146 + for i, kv in ipairs(options.static_headers) do 3.147 + if type(kv) == "string" then 3.148 + t[#t+1] = kv 3.149 + else 3.150 + t[#t+1] = kv[1] .. ": " .. kv[2] 3.151 + end 3.152 + end 3.153 + end 3.154 + end 3.155 + t[#t+1] = "" 3.156 + preamble = table.concat(t, "\r\n") 3.157 + end 3.158 + -- return connect handler: 3.159 + return function(socket) 3.160 + local survive = true -- set to false if process shall be terminated later 3.161 + while true do 3.162 + -- desired chunk sizes: 3.163 + local input_chunk_size = options.maximum_input_chunk_size or options.chunk_size or 16384 3.164 + local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024 3.165 + -- process named arguments "request_header_size_limit" and "request_body_size_limit": 3.166 + local remaining_header_size_limit = options.request_header_size_limit or 1024*1024 3.167 + local remaining_body_size_limit = options.request_body_size_limit or 64*1024*1024 3.168 + -- state variables for request handling: 3.169 + local output_state = "no_status_sent" -- one of: 3.170 + -- "no_status_sent" (initial state) 3.171 + -- "info_status_sent" (1xx status code has been sent) 3.172 + -- "bodyless_status_sent" (204/304 status code has been sent) 3.173 + -- "status_sent" (regular status code has been sent) 3.174 + -- "headers_sent" (headers have been terminated) 3.175 + -- "finished" (request has been answered completely) 3.176 + local content_length -- value of Content-Length header sent 3.177 + local bytes_sent = 0 -- number of bytes sent if Content-Length is set 3.178 + local chunk_parts = {} -- list of chunks to send 3.179 + local chunk_bytes = 0 -- sum of lengths of chunks to send 3.180 + local connection_close_requested = false 3.181 + local connection_close_responded = false 3.182 + local headers_value_nil = {} -- header values that are nil 3.183 + local request_body_content_length -- Content-Length of request body 3.184 + local input_state = "pending" -- one of: 3.185 + -- "pending" (request body has not been processed yet) 3.186 + -- "deferred" (request body processing is deferred) 3.187 + -- "reading" (request body is currently being read) 3.188 + -- "finished" (request body has been read) 3.189 + local streamed_post_params = {} -- mapping from POST field name to stream function 3.190 + local streamed_post_param_patterns = {} -- list of POST field pattern and stream function pairs 3.191 + -- object passed to handler (with methods, GET/POST params, etc.): 3.192 + local request 3.193 + -- reads a number of bytes from the socket, 3.194 + -- optionally feeding these bytes chunk-wise 3.195 + -- into a callback function: 3.196 + local function read_body_bytes(remaining, callback) 3.197 + while remaining > 0 do 3.198 + local limit 3.199 + if remaining > input_chunk_size then 3.200 + limit = input_chunk_size 3.201 + else 3.202 + limit = remaining 3.203 + end 3.204 + local chunk = socket:read(limit) 3.205 + if not chunk or #chunk ~= limit then 3.206 + error("Unexpected EOF while reading chunk of request body") 3.207 + end 3.208 + remaining = remaining - limit 3.209 + if callback then 3.210 + callback(chunk) 3.211 + end 3.212 + end 3.213 + end 3.214 + -- flushes or closes the socket (depending on 3.215 + -- whether "Connection: close" header was sent): 3.216 + local function finish_response() 3.217 + if connection_close_responded then 3.218 + -- close output stream: 3.219 + socket.output:close() 3.220 + -- wait for EOF of peer to avoid immediate TCP RST condition: 3.221 + timeout(2, function() 3.222 + while socket.input:read(input_chunk_size) do end 3.223 + end) 3.224 + -- fully close socket: 3.225 + socket:close() 3.226 + else 3.227 + socket:flush() 3.228 + request:stream_request_body() 3.229 + end 3.230 + end 3.231 + -- writes out buffered chunks (without flushing the socket): 3.232 + local function send_chunk() 3.233 + if chunk_bytes > 0 then 3.234 + socket:write(string.format("%x\r\n", chunk_bytes)) 3.235 + for i = 1, #chunk_parts do 3.236 + socket:write(chunk_parts[i]) 3.237 + end 3.238 + chunk_parts = {} 3.239 + chunk_bytes = 0 3.240 + socket:write("\r\n") 3.241 + end 3.242 + end 3.243 + -- terminate header section in response, optionally flushing: 3.244 + -- (may be called multiple times unless response is finished) 3.245 + local function finish_headers(flush) 3.246 + if output_state == "no_status_sent" then 3.247 + error("HTTP status has not been sent yet") 3.248 + elseif output_state == "finished" then 3.249 + error("Response has already been finished") 3.250 + elseif output_state == "info_status_sent" then 3.251 + socket:write("\r\n") 3.252 + socket:flush() 3.253 + output_state = "no_status_sent" 3.254 + elseif output_state == "bodyless_status_sent" then 3.255 + if connection_close_requested and not connection_close_responded then 3.256 + request:send_header("Connection", "close") 3.257 + end 3.258 + socket:write("\r\n") 3.259 + finish_response() 3.260 + output_state = "finished" 3.261 + elseif output_state == "status_sent" then 3.262 + if not content_length then 3.263 + socket:write("Transfer-Encoding: chunked\r\n") 3.264 + end 3.265 + if connection_close_requested and not connection_close_responded then 3.266 + request:send_header("Connection", "close") 3.267 + end 3.268 + socket:write("\r\n") 3.269 + if request.method == "HEAD" then 3.270 + finish_response() 3.271 + elseif flush then 3.272 + socket:flush() 3.273 + end 3.274 + output_state = "headers_sent" 3.275 + elseif output_state ~= "headers_sent" then 3.276 + error("Unexpected internal status in HTTP engine") 3.277 + end 3.278 + end 3.279 + -- create request object and set several functions and values: 3.280 + request = { 3.281 + -- allow raw socket access: 3.282 + socket = socket, 3.283 + -- parsed cookies: 3.284 + cookies = {}, 3.285 + -- send a HTTP response status (e.g. "200 OK"): 3.286 + send_status = function(self, value) 3.287 + if input_state == "pending" then 3.288 + request:process_request_body() 3.289 + end 3.290 + if output_state == "info_status_sent" then 3.291 + socket:write("\r\n") 3.292 + socket:flush() 3.293 + elseif output_state ~= "no_status_sent" then 3.294 + error("HTTP status has already been sent") 3.295 + end 3.296 + local status1 = string.sub(value, 1, 1) 3.297 + local status3 = string.sub(value, 1, 3) 3.298 + socket:write("HTTP/1.1 ", value, "\r\n", preamble) 3.299 + local without_response_body = status_without_response_body[status3] 3.300 + if without_response_body then 3.301 + output_state = "bodyless_status_sent" 3.302 + if without_response_body == "zero_content_length" then 3.303 + request:send_header("Content-Length", 0) 3.304 + end 3.305 + elseif status1 == "1" then 3.306 + output_state = "info_status_sent" 3.307 + else 3.308 + output_state = "status_sent" 3.309 + end 3.310 + end, 3.311 + -- send a HTTP response header 3.312 + -- (key and value as separate args): 3.313 + send_header = function(self, key, value) 3.314 + if output_state == "no_status_sent" then 3.315 + error("HTTP status has not been sent yet") 3.316 + elseif 3.317 + output_state ~= "info_status_sent" and 3.318 + output_state ~= "bodyless_status_sent" and 3.319 + output_state ~= "status_sent" 3.320 + then 3.321 + error("All HTTP headers have already been sent") 3.322 + end 3.323 + local key_lower = string.lower(key) 3.324 + if key_lower == "content-length" then 3.325 + if output_state == "info_status_sent" then 3.326 + error("Cannot set Content-Length for informational status response") 3.327 + end 3.328 + local new_content_length = assert(tonumber(value), "Invalid content-length") 3.329 + if content_length == nil then 3.330 + content_length = new_content_length 3.331 + elseif content_length == new_content_length then 3.332 + return 3.333 + else 3.334 + error("Content-Length has been set multiple times with different values") 3.335 + end 3.336 + elseif key_lower == "connection" then 3.337 + for entry in string.gmatch(string.lower(value), "[^,]+") do 3.338 + if string.match(entry, "^[ \t]*close[ \t]*$") then 3.339 + if output_state == "info_status_sent" then 3.340 + error("Cannot set \"Connection: close\" for informational status response") 3.341 + end 3.342 + if connection_close_responded then 3.343 + return 3.344 + else 3.345 + connection_close_responded = true 3.346 + break 3.347 + end 3.348 + end 3.349 + end 3.350 + end 3.351 + socket:write(key, ": ", value, "\r\n") 3.352 + end, 3.353 + -- method to finish and flush headers: 3.354 + finish_headers = function() 3.355 + finish_headers(true) 3.356 + end, 3.357 + -- send data for response body: 3.358 + send_data = function(self, ...) 3.359 + if output_state == "info_status_sent" then 3.360 + error("No (non-informational) HTTP status has been sent yet") 3.361 + elseif output_state == "bodyless_status_sent" then 3.362 + error("Cannot send response data for body-less status message") 3.363 + end 3.364 + finish_headers(false) 3.365 + if output_state ~= "headers_sent" then 3.366 + error("Unexpected internal status in HTTP engine") 3.367 + end 3.368 + if request.method == "HEAD" then 3.369 + return 3.370 + end 3.371 + for i = 1, select("#", ...) do 3.372 + local str = tostring(select(i, ...)) 3.373 + if #str > 0 then 3.374 + if content_length then 3.375 + local bytes_to_send = #str 3.376 + if bytes_sent + bytes_to_send > content_length then 3.377 + socket:write(string.sub(str, 1, content_length - bytes_sent)) 3.378 + bytes_sent = content_length 3.379 + error("Content length exceeded") 3.380 + else 3.381 + socket:write(str) 3.382 + bytes_sent = bytes_sent + bytes_to_send 3.383 + end 3.384 + else 3.385 + chunk_bytes = chunk_bytes + #str 3.386 + chunk_parts[#chunk_parts+1] = str 3.387 + end 3.388 + end 3.389 + end 3.390 + if chunk_bytes >= output_chunk_size then 3.391 + send_chunk() 3.392 + end 3.393 + end, 3.394 + -- flush output buffer: 3.395 + flush = function(self) 3.396 + send_chunk() 3.397 + socket:flush() 3.398 + end, 3.399 + -- finish response: 3.400 + finish = function(self) 3.401 + if output_state == "finished" then 3.402 + return 3.403 + elseif output_state == "info_status_sent" then 3.404 + error("Informational HTTP response can be finished with :finish_headers() method") 3.405 + end 3.406 + finish_headers(false) 3.407 + if output_state == "headers_sent" then 3.408 + if request.method ~= "HEAD" then 3.409 + if content_length then 3.410 + if bytes_sent ~= content_length then 3.411 + error("Content length not used") 3.412 + end 3.413 + else 3.414 + send_chunk() 3.415 + socket:write("0\r\n\r\n") 3.416 + end 3.417 + finish_response() 3.418 + end 3.419 + output_state = "finished" 3.420 + elseif output_state ~= finished then 3.421 + error("Unexpected internal status in HTTP engine") 3.422 + end 3.423 + end, 3.424 + -- table mapping header field names to value-lists 3.425 + -- (raw access): 3.426 + headers = setmetatable({}, { 3.427 + __index = function(self, key) 3.428 + local lowerkey = string.lower(key) 3.429 + if lowerkey == key then 3.430 + return 3.431 + end 3.432 + local result = rawget(self, lowerkey) 3.433 + if result == nil then 3.434 + result = {} 3.435 + end 3.436 + self[lowerkey] = result 3.437 + self[key] = result 3.438 + return result 3.439 + end 3.440 + }), 3.441 + -- table mapping header field names to value-lists 3.442 + -- (for headers with comma separated values): 3.443 + headers_csv_table = setmetatable({}, { 3.444 + __index = function(self, key) 3.445 + local result = {} 3.446 + for i, line in ipairs(request.headers[key]) do 3.447 + for entry in string.gmatch(line, "[^,]+") do 3.448 + local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") 3.449 + if value then 3.450 + result[#result+1] = value 3.451 + end 3.452 + end 3.453 + end 3.454 + self[key] = result 3.455 + return result 3.456 + end 3.457 + }), 3.458 + -- table mapping header field names to a comma separated string 3.459 + -- (for headers with comma separated values): 3.460 + headers_csv_string = setmetatable({}, { 3.461 + __index = function(self, key) 3.462 + local result = {} 3.463 + for i, line in ipairs(request.headers[key]) do 3.464 + result[#result+1] = line 3.465 + end 3.466 + result = string.concat(result, ", ") 3.467 + self[key] = result 3.468 + return result 3.469 + end 3.470 + }), 3.471 + -- table mapping header field names to a single string value 3.472 + -- (or false if header has been sent multiple times): 3.473 + headers_value = setmetatable({}, { 3.474 + __index = function(self, key) 3.475 + if headers_value_nil[key] then 3.476 + return nil 3.477 + end 3.478 + local result = nil 3.479 + local values = request.headers_csv_table[key] 3.480 + if #values == 0 then 3.481 + headers_value_nil[key] = true 3.482 + elseif #values == 1 then 3.483 + result = values[1] 3.484 + else 3.485 + result = false 3.486 + end 3.487 + self[key] = result 3.488 + return result 3.489 + end 3.490 + }), 3.491 + -- table mapping header field names to a flag table, 3.492 + -- indicating if the comma separated value contains certain entries: 3.493 + headers_flags = setmetatable({}, { 3.494 + __index = function(self, key) 3.495 + local result = setmetatable({}, { 3.496 + __index = function(self, key) 3.497 + local lowerkey = string.lower(key) 3.498 + local result = rawget(self, lowerkey) or false 3.499 + self[lowerkey] = result 3.500 + self[key] = result 3.501 + return result 3.502 + end 3.503 + }) 3.504 + for i, value in ipairs(request.headers_csv_table[key]) do 3.505 + result[string.lower(value)] = true 3.506 + end 3.507 + self[key] = result 3.508 + return result 3.509 + end 3.510 + }), 3.511 + -- register POST param stream handler for a single field name: 3.512 + stream_post_param = function(self, field_name, callback) 3.513 + if input_state == "inprogress" or input_state == "finished" then 3.514 + error("Cannot register POST param streaming function if request body is already processed") 3.515 + end 3.516 + streamed_post_params[field_name] = callback 3.517 + end, 3.518 + -- register POST param stream handler for a field name pattern: 3.519 + stream_post_params = function(self, pattern, callback) 3.520 + if input_state == "inprogress" or input_state == "finished" then 3.521 + error("Cannot register POST param streaming function if request body is already processed") 3.522 + end 3.523 + streamed_post_param_patterns[#streamed_post_param_patterns+1] = {pattern, callback} 3.524 + end, 3.525 + -- disables automatic request body processing on write 3.526 + -- (use with caution): 3.527 + defer_reading = function(self) 3.528 + if input_state == "pending" then 3.529 + input_state = "deferred" 3.530 + end 3.531 + end, 3.532 + -- processes the request body and sets the request.post_params, 3.533 + -- request.post_params_list, request.meta_post_params, and 3.534 + -- request.meta_post_params_list values (can be called manually or 3.535 + -- automatically if post_params are accessed or data is written out) 3.536 + process_request_body = function(self) 3.537 + if input_state == "finished" then 3.538 + return 3.539 + end 3.540 + local post_params_list, post_params = new_params_list() 3.541 + local content_type = request.headers_value["Content-Type"] 3.542 + if content_type then 3.543 + if 3.544 + content_type == "application/x-www-form-urlencoded" or 3.545 + string.match(content_type, "^application/x%-www%-form%-urlencoded *;") 3.546 + then 3.547 + read_urlencoded_form(post_params_list, request.body) 3.548 + else 3.549 + local boundary = string.match( 3.550 + content_type, 3.551 + '^multipart/form%-data[ \t]*[;,][ \t]*boundary="([^"]+)"$' 3.552 + ) or string.match( 3.553 + content_type, 3.554 + '^multipart/form%-data[ \t]*[;,][ \t]*boundary=([^"; \t]+)$' 3.555 + ) 3.556 + if boundary then 3.557 + local post_metadata_list, post_metadata = new_params_list() 3.558 + boundary = "--" .. boundary 3.559 + local headerdata = "" 3.560 + local streamer 3.561 + local field_name 3.562 + local metadata = {} 3.563 + local value_parts 3.564 + local function default_streamer(chunk) 3.565 + value_parts[#value_parts+1] = chunk 3.566 + end 3.567 + local function stream_part_finish() 3.568 + if streamer == default_streamer then 3.569 + local value = table.concat(value_parts) 3.570 + value_parts = nil 3.571 + if field_name then 3.572 + local values = post_params_list[field_name] 3.573 + values[#values+1] = value 3.574 + local metadata_entries = post_metadata_list[field_name] 3.575 + metadata_entries[#metadata_entries+1] = metadata 3.576 + end 3.577 + else 3.578 + streamer() 3.579 + end 3.580 + headerdata = "" 3.581 + streamer = nil 3.582 + field_name = nil 3.583 + metadata = {} 3.584 + end 3.585 + local function stream_part_chunk(chunk) 3.586 + if streamer then 3.587 + streamer(chunk) 3.588 + else 3.589 + headerdata = headerdata .. chunk 3.590 + while true do 3.591 + local line, remaining = string.match(headerdata, "^(.-)\r?\n(.*)$") 3.592 + if not line then 3.593 + break 3.594 + end 3.595 + if line == "" then 3.596 + streamer = streamed_post_params[field_name] 3.597 + if not streamer then 3.598 + for i, rule in ipairs(streamed_post_param_patterns) do 3.599 + if string.match(field_name, rule[1]) then 3.600 + streamer = rule[2] 3.601 + break 3.602 + end 3.603 + end 3.604 + end 3.605 + if not streamer then 3.606 + value_parts = {} 3.607 + streamer = default_streamer 3.608 + end 3.609 + streamer(remaining, field_name, metadata) 3.610 + return 3.611 + end 3.612 + headerdata = remaining 3.613 + local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$") 3.614 + if not header_key then 3.615 + error("Invalid header in multipart/form-data part") 3.616 + end 3.617 + header_key = string.lower(header_key) 3.618 + if header_key == "content-disposition" then 3.619 + local escaped_header_value = string.gsub(header_value, '"[^"]*"', function(str) 3.620 + return string.gsub(str, "=", "==") 3.621 + end) 3.622 + field_name = string.match(escaped_header_value, ';[ \t]*name="([^"]*)"') 3.623 + if field_name then 3.624 + field_name = string.gsub(field_name, "==", "=") 3.625 + else 3.626 + field_name = string.match(header_value, ';[ \t]*name=([^"; \t]+)') 3.627 + end 3.628 + metadata.file_name = string.match(escaped_header_value, ';[ \t]*filename="([^"]*)"') 3.629 + if metadata.file_name then 3.630 + metadata.file_name = string.gsub(metadata.file_name, "==", "=") 3.631 + else 3.632 + string.match(header_value, ';[ \t]*filename=([^"; \t]+)') 3.633 + end 3.634 + elseif header_key == "content-type" then 3.635 + metadata.content_type = header_value 3.636 + elseif header_key == "content-transfer-encoding" then 3.637 + error("Content-transfer-encoding not supported by multipart/form-data parser") 3.638 + end 3.639 + end 3.640 + end 3.641 + end 3.642 + local skippart = true -- ignore data until first boundary 3.643 + local afterbound = false -- interpret 2 bytes after boundary ("\r\n" or "--") 3.644 + local terminated = false -- final boundary read 3.645 + local bigchunk = "" 3.646 + request:stream_request_body(function(chunk) 3.647 + if terminated then 3.648 + return 3.649 + end 3.650 + bigchunk = bigchunk .. chunk 3.651 + while true do 3.652 + if afterbound then 3.653 + if #bigchunk <= 2 then 3.654 + return 3.655 + end 3.656 + local terminator = string.sub(bigchunk, 1, 2) 3.657 + if terminator == "\r\n" then 3.658 + afterbound = false 3.659 + bigchunk = string.sub(bigchunk, 3) 3.660 + elseif terminator == "--" then 3.661 + terminated = true 3.662 + bigchunk = nil 3.663 + return 3.664 + else 3.665 + error("Error while parsing multipart body (expected CRLF or double minus)") 3.666 + end 3.667 + end 3.668 + local pos1, pos2 = string.find(bigchunk, boundary, 1, true) 3.669 + if not pos1 then 3.670 + if not skippart then 3.671 + local safe = #bigchunk-#boundary 3.672 + if safe > 0 then 3.673 + stream_part_chunk(string.sub(bigchunk, 1, safe)) 3.674 + bigchunk = string.sub(bigchunk, safe+1) 3.675 + end 3.676 + end 3.677 + return 3.678 + end 3.679 + if not skippart then 3.680 + stream_part_chunk(string.sub(bigchunk, 1, pos1 - 1)) 3.681 + stream_part_finish() 3.682 + else 3.683 + boundary = "\r\n" .. boundary 3.684 + skippart = false 3.685 + end 3.686 + bigchunk = string.sub(bigchunk, pos2 + 1) 3.687 + afterbound = true 3.688 + end 3.689 + end) 3.690 + if not terminated then 3.691 + error("Premature end of multipart/form-data request body") 3.692 + end 3.693 + request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata 3.694 + else 3.695 + error("Unknown Content-Type of request body") 3.696 + end 3.697 + end 3.698 + end 3.699 + request.post_params_list, request.post_params = post_params_list, post_params 3.700 + end, 3.701 + -- stream request body to an (optional) callback function 3.702 + -- without processing it otherwise: 3.703 + stream_request_body = function(self, callback) 3.704 + if input_state ~= "pending" and input_state ~= "deferred" then 3.705 + if callback then 3.706 + if input_state == "inprogress" then 3.707 + error("Request body is already being processed") 3.708 + else 3.709 + error("Request body has already been processed") 3.710 + end 3.711 + end 3.712 + return 3.713 + end 3.714 + input_state = "inprogress" 3.715 + if request.headers_flags["Expect"]["100-continue"] then 3.716 + request:send_status("100 Continue") 3.717 + request:finish_headers() 3.718 + end 3.719 + if request.headers_flags["Transfer-Encoding"]["chunked"] then 3.720 + while true do 3.721 + local line = socket:readuntil("\n", 32 + remaining_body_size_limit) 3.722 + if not line then 3.723 + error("Unexpected EOF while reading next chunk of request body") 3.724 + end 3.725 + local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$") 3.726 + local chunkext 3.727 + if lenstr then 3.728 + chunkext = "" 3.729 + else 3.730 + zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$") 3.731 + end 3.732 + if not lenstr or #lenstr > 13 then 3.733 + error("Encoding error or unexpected EOF while reading chunk of request body") 3.734 + end 3.735 + local len = tonumber("0x" .. lenstr) 3.736 + remaining_body_size_limit = remaining_body_size_limit - (#zeros + #chunkext + len) 3.737 + if remaining_body_size_limit < 0 then 3.738 + error("Request body size limit exceeded") 3.739 + end 3.740 + if len == 0 then break end 3.741 + read_body_bytes(len, callback) 3.742 + local term = socket:readuntil("\n", 2) 3.743 + if term ~= "\r\n" and term ~= "\n" then 3.744 + error("Encoding error while reading chunk of request body") 3.745 + end 3.746 + end 3.747 + while true do 3.748 + local line = socket:readuntil("\n", 2 + remaining_body_size_limit) 3.749 + if line == "\r\n" or line == "\n" then break end 3.750 + remaining_body_size_limit = remaining_body_size_limit - #line 3.751 + if remaining_body_size_limit < 0 then 3.752 + error("Request body size limit exceeded while reading trailer section of chunked request body") 3.753 + end 3.754 + end 3.755 + elseif request_body_content_length then 3.756 + read_body_bytes(request_body_content_length, callback) 3.757 + end 3.758 + input_state = "finished" 3.759 + end 3.760 + } 3.761 + -- initialize tables for GET params in request object: 3.762 + request.get_params_list, request.get_params = new_params_list() 3.763 + -- add meta table to request object to allow access to "body" and POST params: 3.764 + setmetatable(request, { 3.765 + __index = function(self, key) 3.766 + if key == "body" then 3.767 + local chunks = {} 3.768 + request:stream_request_body(function(chunk) 3.769 + chunks[#chunks+1] = chunk 3.770 + end) 3.771 + self.body = table.concat(chunks) 3.772 + return self.body 3.773 + elseif 3.774 + key == "post_params_list" or key == "post_params" or 3.775 + key == "post_metadata_list" or key == "post_metadata" 3.776 + then 3.777 + request:process_request_body() 3.778 + return request[key] 3.779 + end 3.780 + end 3.781 + }) 3.782 + -- low level HTTP error response (for malformed requests, etc.): 3.783 + local function error_response(status, text) 3.784 + request:send_status(status) 3.785 + request:send_header("Content-Type", "text/plain") 3.786 + if not connection_close_responded then 3.787 + request:send_header("Connection", "close") 3.788 + end 3.789 + request:send_data(status, "\n") 3.790 + if text then 3.791 + request:send_data("\n", text, "\n") 3.792 + end 3.793 + request:finish() 3.794 + return survive 3.795 + end 3.796 + -- read and parse request line: 3.797 + local line = socket:readuntil("\n", remaining_header_size_limit) 3.798 + if not line then return survive end 3.799 + remaining_header_size_limit = remaining_header_size_limit - #line 3.800 + if remaining_header_size_limit == 0 then 3.801 + return error_response("413 Request Entity Too Large", "Request line too long") 3.802 + end 3.803 + local proto 3.804 + request.method, request.url, proto = 3.805 + line:match("^([^ \t\r]+)[ \t]+([^ \t\r]+)[ \t]*([^ \t\r]*)[ \t]*\r?\n$") 3.806 + if not request.method then 3.807 + return error_response("400 Bad Request") 3.808 + elseif proto ~= "HTTP/1.1" then 3.809 + return error_response("505 HTTP Version Not Supported") 3.810 + else 3.811 + -- read and parse headers: 3.812 + while true do 3.813 + local line = socket:readuntil("\n", remaining_header_size_limit); 3.814 + remaining_header_size_limit = remaining_header_size_limit - #line 3.815 + if not line then 3.816 + return error_response("400 Bad Request") 3.817 + end 3.818 + if line == "\r\n" or line == "\n" then 3.819 + break 3.820 + end 3.821 + if remaining_header_size_limit == 0 then 3.822 + return error_response("413 Request Entity Too Large", "Headers too long") 3.823 + end 3.824 + local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$") 3.825 + if not key then 3.826 + return error_response("400 Bad Request") 3.827 + end 3.828 + local values = request.headers[key] 3.829 + values[#values+1] = value 3.830 + end 3.831 + -- process "Connection: close" header if existent: 3.832 + connection_close_requested = request.headers_flags["Connection"]["close"] 3.833 + -- process "Content-Length" header if existent: 3.834 + do 3.835 + local values = request.headers_csv_table["Content-Length"] 3.836 + if #values > 0 then 3.837 + request_body_content_length = tonumber(values[1]) 3.838 + local proper_value = tostring(request_body_content_length) 3.839 + for i, value in ipairs(values) do 3.840 + value = string.match(value, "^0*(.*)") 3.841 + if value ~= proper_value then 3.842 + return error_response("400 Bad Request", "Content-Length header(s) invalid") 3.843 + end 3.844 + end 3.845 + if request_body_content_length > remaining_body_size_limit then 3.846 + return error_response("413 Request Entity Too Large", "Request body too big") 3.847 + end 3.848 + end 3.849 + end 3.850 + -- process "Transfer-Encoding" header if existent: 3.851 + do 3.852 + local flag = request.headers_flags["Transfer-Encoding"]["chunked"] 3.853 + local list = request.headers_csv_table["Transfer-Encoding"] 3.854 + if (flag and #list ~= 1) or (not flag and #list ~= 0) then 3.855 + return error_response("400 Bad Request", "Unexpected Transfer-Encoding") 3.856 + end 3.857 + end 3.858 + -- process "Expect" header if existent: 3.859 + for i, value in ipairs(request.headers_csv_table["Expect"]) do 3.860 + if string.lower(value) ~= "100-continue" then 3.861 + return error_response("417 Expectation Failed", "Unexpected Expect header") 3.862 + end 3.863 + end 3.864 + -- parse GET params: 3.865 + request.path, request.query = string.match(request.url, "^([^?]*)%??(.*)$") 3.866 + read_urlencoded_form(request.get_params_list, request.query) 3.867 + -- parse cookies: 3.868 + for i, line in ipairs(request.headers["Cookie"]) do 3.869 + for rawkey, rawvalue in 3.870 + string.gmatch(line, "([^=; ]*)=([^=; ]*)") 3.871 + do 3.872 + request.cookies[decode_uri(rawkey)] = decode_uri(rawvalue) 3.873 + end 3.874 + end 3.875 + -- call underlying handler and remember boolean result: 3.876 + if handler(request) ~= true then survive = false end 3.877 + -- finish request (unless already done by underlying handler): 3.878 + request:finish() 3.879 + end 3.880 + end 3.881 + return survive 3.882 + end 3.883 +end 3.884 + 3.885 +return _M 3.886 +