aboutsummaryrefslogtreecommitdiffstats
path: root/net/http
diff options
context:
space:
mode:
Diffstat (limited to 'net/http')
-rw-r--r--net/http/codes.lua67
-rw-r--r--net/http/parser.lua160
-rw-r--r--net/http/server.lua303
3 files changed, 530 insertions, 0 deletions
diff --git a/net/http/codes.lua b/net/http/codes.lua
new file mode 100644
index 00000000..0cadd079
--- /dev/null
+++ b/net/http/codes.lua
@@ -0,0 +1,67 @@
+
+local response_codes = {
+ -- Source: http://www.iana.org/assignments/http-status-codes
+ -- s/^\(\d*\)\s*\(.*\S\)\s*\[RFC.*\]\s*$/^I["\1"] = "\2";
+ [100] = "Continue";
+ [101] = "Switching Protocols";
+ [102] = "Processing";
+
+ [200] = "OK";
+ [201] = "Created";
+ [202] = "Accepted";
+ [203] = "Non-Authoritative Information";
+ [204] = "No Content";
+ [205] = "Reset Content";
+ [206] = "Partial Content";
+ [207] = "Multi-Status";
+ [208] = "Already Reported";
+ [226] = "IM Used";
+
+ [300] = "Multiple Choices";
+ [301] = "Moved Permanently";
+ [302] = "Found";
+ [303] = "See Other";
+ [304] = "Not Modified";
+ [305] = "Use Proxy";
+ -- The 306 status code was used in a previous version of [RFC2616], is no longer used, and the code is reserved.
+ [307] = "Temporary Redirect";
+
+ [400] = "Bad Request";
+ [401] = "Unauthorized";
+ [402] = "Payment Required";
+ [403] = "Forbidden";
+ [404] = "Not Found";
+ [405] = "Method Not Allowed";
+ [406] = "Not Acceptable";
+ [407] = "Proxy Authentication Required";
+ [408] = "Request Timeout";
+ [409] = "Conflict";
+ [410] = "Gone";
+ [411] = "Length Required";
+ [412] = "Precondition Failed";
+ [413] = "Request Entity Too Large";
+ [414] = "Request-URI Too Long";
+ [415] = "Unsupported Media Type";
+ [416] = "Requested Range Not Satisfiable";
+ [417] = "Expectation Failed";
+ [418] = "I'm a teapot";
+ [422] = "Unprocessable Entity";
+ [423] = "Locked";
+ [424] = "Failed Dependency";
+ -- The 425 status code is reserved for the WebDAV advanced collections expired proposal [RFC2817]
+ [426] = "Upgrade Required";
+
+ [500] = "Internal Server Error";
+ [501] = "Not Implemented";
+ [502] = "Bad Gateway";
+ [503] = "Service Unavailable";
+ [504] = "Gateway Timeout";
+ [505] = "HTTP Version Not Supported";
+ [506] = "Variant Also Negotiates"; -- Experimental
+ [507] = "Insufficient Storage";
+ [508] = "Loop Detected";
+ [510] = "Not Extended";
+};
+
+for k,v in pairs(response_codes) do response_codes[k] = k.." "..v; end
+return setmetatable(response_codes, { __index = function(t, k) return k.." Unassigned"; end })
diff --git a/net/http/parser.lua b/net/http/parser.lua
new file mode 100644
index 00000000..f9e6cea0
--- /dev/null
+++ b/net/http/parser.lua
@@ -0,0 +1,160 @@
+local tonumber = tonumber;
+local assert = assert;
+local url_parse = require "socket.url".parse;
+local urldecode = require "util.http".urldecode;
+
+local function preprocess_path(path)
+ path = urldecode((path:gsub("//+", "/")));
+ if path:sub(1,1) ~= "/" then
+ path = "/"..path;
+ end
+ local level = 0;
+ for component in path:gmatch("([^/]+)/") do
+ if component == ".." then
+ level = level - 1;
+ elseif component ~= "." then
+ level = level + 1;
+ end
+ if level < 0 then
+ return nil;
+ end
+ end
+ return path;
+end
+
+local httpstream = {};
+
+function httpstream.new(success_cb, error_cb, parser_type, options_cb)
+ local client = true;
+ if not parser_type or parser_type == "server" then client = false; else assert(parser_type == "client", "Invalid parser type"); end
+ local buf = "";
+ local chunked, chunk_size, chunk_start;
+ local state = nil;
+ local packet;
+ local len;
+ local have_body;
+ local error;
+ return {
+ feed = function(self, data)
+ if error then return nil, "parse has failed"; end
+ if not data then -- EOF
+ if state and client and not len then -- reading client body until EOF
+ packet.body = buf;
+ success_cb(packet);
+ elseif buf ~= "" then -- unexpected EOF
+ error = true; return error_cb();
+ end
+ return;
+ end
+ buf = buf..data;
+ while #buf > 0 do
+ if state == nil then -- read request
+ local index = buf:find("\r\n\r\n", nil, true);
+ if not index then return; end -- not enough data
+ local method, path, httpversion, status_code, reason_phrase;
+ local first_line;
+ local headers = {};
+ for line in buf:sub(1,index+1):gmatch("([^\r\n]+)\r\n") do -- parse request
+ if first_line then
+ local key, val = line:match("^([^%s:]+): *(.*)$");
+ if not key then error = true; return error_cb("invalid-header-line"); end -- TODO handle multi-line and invalid headers
+ key = key:lower();
+ headers[key] = headers[key] and headers[key]..","..val or val;
+ else
+ first_line = line;
+ if client then
+ httpversion, status_code, reason_phrase = line:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$");
+ status_code = tonumber(status_code);
+ if not status_code then error = true; return error_cb("invalid-status-line"); end
+ have_body = not
+ ( (options_cb and options_cb().method == "HEAD")
+ or (status_code == 204 or status_code == 304 or status_code == 301)
+ or (status_code >= 100 and status_code < 200) );
+ else
+ method, path, httpversion = line:match("^(%w+) (%S+) HTTP/(1%.[01])$");
+ if not method then error = true; return error_cb("invalid-status-line"); end
+ end
+ end
+ end
+ if not first_line then error = true; return error_cb("invalid-status-line"); end
+ chunked = have_body and headers["transfer-encoding"] == "chunked";
+ len = tonumber(headers["content-length"]); -- TODO check for invalid len
+ if client then
+ -- FIXME handle '100 Continue' response (by skipping it)
+ if not have_body then len = 0; end
+ packet = {
+ code = status_code;
+ httpversion = httpversion;
+ headers = headers;
+ body = have_body and "" or nil;
+ -- COMPAT the properties below are deprecated
+ responseversion = httpversion;
+ responseheaders = headers;
+ };
+ else
+ local parsed_url;
+ if path:byte() == 47 then -- starts with /
+ local _path, _query = path:match("([^?]*).?(.*)");
+ if _query == "" then _query = nil; end
+ parsed_url = { path = _path, query = _query };
+ else
+ parsed_url = url_parse(path);
+ if not(parsed_url and parsed_url.path) then error = true; return error_cb("invalid-url"); end
+ end
+ path = preprocess_path(parsed_url.path);
+ headers.host = parsed_url.host or headers.host;
+
+ len = len or 0;
+ packet = {
+ method = method;
+ url = parsed_url;
+ path = path;
+ httpversion = httpversion;
+ headers = headers;
+ body = nil;
+ };
+ end
+ buf = buf:sub(index + 4);
+ state = true;
+ end
+ if state then -- read body
+ if client then
+ if chunked then
+ if not buf:find("\r\n", nil, true) then
+ return;
+ end -- not enough data
+ if not chunk_size then
+ chunk_size, chunk_start = buf:match("^(%x+)[^\r\n]*\r\n()");
+ chunk_size = chunk_size and tonumber(chunk_size, 16);
+ if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end
+ end
+ if chunk_size == 0 and buf:find("\r\n\r\n", chunk_start-2, true) then
+ state, chunk_size = nil, nil;
+ buf = buf:gsub("^.-\r\n\r\n", ""); -- This ensure extensions and trailers are stripped
+ success_cb(packet);
+ elseif #buf - chunk_start + 2 >= chunk_size then -- we have a chunk
+ packet.body = packet.body..buf:sub(chunk_start, chunk_start + (chunk_size-1));
+ buf = buf:sub(chunk_start + chunk_size + 2);
+ chunk_size, chunk_start = nil, nil;
+ else -- Partial chunk remaining
+ break;
+ end
+ elseif len and #buf >= len then
+ packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
+ state = nil; success_cb(packet);
+ else
+ break;
+ end
+ elseif #buf >= len then
+ packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
+ state = nil; success_cb(packet);
+ else
+ break;
+ end
+ end
+ end
+ end;
+ };
+end
+
+return httpstream;
diff --git a/net/http/server.lua b/net/http/server.lua
new file mode 100644
index 00000000..5961169f
--- /dev/null
+++ b/net/http/server.lua
@@ -0,0 +1,303 @@
+
+local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
+local parser_new = require "net.http.parser".new;
+local events = require "util.events".new();
+local addserver = require "net.server".addserver;
+local log = require "util.logger".init("http.server");
+local os_date = os.date;
+local pairs = pairs;
+local s_upper = string.upper;
+local setmetatable = setmetatable;
+local xpcall = xpcall;
+local traceback = debug.traceback;
+local tostring = tostring;
+local codes = require "net.http.codes";
+
+local _M = {};
+
+local sessions = {};
+local listener = {};
+local hosts = {};
+local default_host;
+
+local function is_wildcard_event(event)
+ return event:sub(-2, -1) == "/*";
+end
+local function is_wildcard_match(wildcard_event, event)
+ return wildcard_event:sub(1, -2) == event:sub(1, #wildcard_event-1);
+end
+
+local recent_wildcard_events, max_cached_wildcard_events = {}, 10000;
+
+local event_map = events._event_map;
+setmetatable(events._handlers, {
+ -- Called when firing an event that doesn't exist (but may match a wildcard handler)
+ __index = function (handlers, curr_event)
+ if is_wildcard_event(curr_event) then return; end -- Wildcard events cannot be fired
+ -- Find all handlers that could match this event, sort them
+ -- and then put the array into handlers[curr_event] (and return it)
+ local matching_handlers_set = {};
+ local handlers_array = {};
+ for event, handlers_set in pairs(event_map) do
+ if event == curr_event or
+ is_wildcard_event(event) and is_wildcard_match(event, curr_event) then
+ for handler, priority in pairs(handlers_set) do
+ matching_handlers_set[handler] = { (select(2, event:gsub("/", "%1"))), is_wildcard_event(event) and 0 or 1, priority };
+ table.insert(handlers_array, handler);
+ end
+ end
+ end
+ if #handlers_array > 0 then
+ table.sort(handlers_array, function(b, a)
+ local a_score, b_score = matching_handlers_set[a], matching_handlers_set[b];
+ for i = 1, #a_score do
+ if a_score[i] ~= b_score[i] then -- If equal, compare next score value
+ return a_score[i] < b_score[i];
+ end
+ end
+ return false;
+ end);
+ else
+ handlers_array = false;
+ end
+ rawset(handlers, curr_event, handlers_array);
+ if not event_map[curr_event] then -- Only wildcard handlers match, if any
+ table.insert(recent_wildcard_events, curr_event);
+ if #recent_wildcard_events > max_cached_wildcard_events then
+ rawset(handlers, table.remove(recent_wildcard_events, 1), nil);
+ end
+ end
+ return handlers_array;
+ end;
+ __newindex = function (handlers, curr_event, handlers_array)
+ if handlers_array == nil
+ and is_wildcard_event(curr_event) then
+ -- Invalidate the indexes of all matching events
+ for event in pairs(handlers) do
+ if is_wildcard_match(curr_event, event) then
+ handlers[event] = nil;
+ end
+ end
+ end
+ rawset(handlers, curr_event, handlers_array);
+ end;
+});
+
+local handle_request;
+local _1, _2, _3;
+local function _handle_request() return handle_request(_1, _2, _3); end
+
+local last_err;
+local function _traceback_handler(err) last_err = err; log("error", "Traceback[httpserver]: %s", traceback(tostring(err), 2)); end
+events.add_handler("http-error", function (error)
+ return "Error processing request: "..codes[error.code]..". Check your error log for more information.";
+end, -1);
+
+function listener.onconnect(conn)
+ local secure = conn:ssl() and true or nil;
+ local pending = {};
+ local waiting = false;
+ local function process_next()
+ if waiting then log("debug", "can't process_next, waiting"); return; end
+ waiting = true;
+ while sessions[conn] and #pending > 0 do
+ local request = t_remove(pending);
+ --log("debug", "process_next: %s", request.path);
+ --handle_request(conn, request, process_next);
+ _1, _2, _3 = conn, request, process_next;
+ if not xpcall(_handle_request, _traceback_handler) then
+ conn:write("HTTP/1.0 500 Internal Server Error\r\n\r\n"..events.fire_event("http-error", { code = 500, private_message = last_err }));
+ conn:close();
+ end
+ end
+ --log("debug", "ready for more");
+ waiting = false;
+ end
+ local function success_cb(request)
+ --log("debug", "success_cb: %s", request.path);
+ if waiting then
+ log("error", "http connection handler is not reentrant: %s", request.path);
+ assert(false, "http connection handler is not reentrant");
+ end
+ request.secure = secure;
+ t_insert(pending, request);
+ process_next();
+ end
+ local function error_cb(err)
+ log("debug", "error_cb: %s", err or "<nil>");
+ -- FIXME don't close immediately, wait until we process current stuff
+ -- FIXME if err, send off a bad-request response
+ sessions[conn] = nil;
+ conn:close();
+ end
+ sessions[conn] = parser_new(success_cb, error_cb);
+end
+
+function listener.ondisconnect(conn)
+ local open_response = conn._http_open_response;
+ if open_response and open_response.on_destroy then
+ open_response.finished = true;
+ open_response:on_destroy();
+ end
+ sessions[conn] = nil;
+end
+
+function listener.onincoming(conn, data)
+ sessions[conn]:feed(data);
+end
+
+local headerfix = setmetatable({}, {
+ __index = function(t, k)
+ local v = "\r\n"..k:gsub("_", "-"):gsub("%f[%w].", s_upper)..": ";
+ t[k] = v;
+ return v;
+ end
+});
+
+function _M.hijack_response(response, listener)
+ error("TODO");
+end
+function handle_request(conn, request, finish_cb)
+ --log("debug", "handler: %s", request.path);
+ local headers = {};
+ for k,v in pairs(request.headers) do headers[k:gsub("-", "_")] = v; end
+ request.headers = headers;
+ request.conn = conn;
+
+ local date_header = os_date('!%a, %d %b %Y %H:%M:%S GMT'); -- FIXME use
+ local conn_header = request.headers.connection;
+ conn_header = conn_header and ","..conn_header:gsub("[ \t]", ""):lower().."," or ""
+ local httpversion = request.httpversion
+ local persistent = conn_header:find(",keep-alive,", 1, true)
+ or (httpversion == "1.1" and not conn_header:find(",close,", 1, true));
+
+ local response_conn_header;
+ if persistent then
+ response_conn_header = "Keep-Alive";
+ else
+ response_conn_header = httpversion == "1.1" and "close" or nil
+ end
+
+ local response = {
+ request = request;
+ status_code = 200;
+ headers = { date = date_header, connection = response_conn_header };
+ persistent = persistent;
+ conn = conn;
+ send = _M.send_response;
+ finish_cb = finish_cb;
+ };
+ conn._http_open_response = response;
+
+ local host = (request.headers.host or ""):match("[^:]+");
+
+ -- Some sanity checking
+ local err_code, err;
+ if not request.path then
+ err_code, err = 400, "Invalid path";
+ elseif not hosts[host] then
+ if hosts[default_host] then
+ host = default_host;
+ elseif host then
+ err_code, err = 404, "Unknown host: "..host;
+ else
+ err_code, err = 400, "Missing or invalid 'Host' header";
+ end
+ end
+
+ if err then
+ response.status_code = err_code;
+ response:send(events.fire_event("http-error", { code = err_code, message = err }));
+ return;
+ end
+
+ local event = request.method.." "..host..request.path:match("[^?]*");
+ local payload = { request = request, response = response };
+ --log("debug", "Firing event: %s", event);
+ local result = events.fire_event(event, payload);
+ if result ~= nil then
+ if result ~= true then
+ local body;
+ local result_type = type(result);
+ if result_type == "number" then
+ response.status_code = result;
+ if result >= 400 then
+ body = events.fire_event("http-error", { code = result });
+ end
+ elseif result_type == "string" then
+ body = result;
+ elseif result_type == "table" then
+ for k, v in pairs(result) do
+ if k ~= "headers" then
+ response[k] = v;
+ else
+ for header_name, header_value in pairs(v) do
+ response.headers[header_name] = header_value;
+ end
+ end
+ end
+ end
+ response:send(body);
+ end
+ return;
+ end
+
+ -- if handler not called, return 404
+ response.status_code = 404;
+ response:send(events.fire_event("http-error", { code = 404 }));
+end
+function _M.send_response(response, body)
+ if response.finished then return; end
+ response.finished = true;
+ response.conn._http_open_response = nil;
+
+ local status_line = "HTTP/"..response.request.httpversion.." "..(response.status or codes[response.status_code]);
+ local headers = response.headers;
+ body = body or response.body or "";
+ headers.content_length = #body;
+
+ local output = { status_line };
+ for k,v in pairs(headers) do
+ t_insert(output, headerfix[k]..v);
+ end
+ t_insert(output, "\r\n\r\n");
+ t_insert(output, body);
+
+ response.conn:write(t_concat(output));
+ if response.on_destroy then
+ response:on_destroy();
+ response.on_destroy = nil;
+ end
+ if response.persistent then
+ response:finish_cb();
+ else
+ response.conn:close();
+ end
+end
+function _M.add_handler(event, handler, priority)
+ events.add_handler(event, handler, priority);
+end
+function _M.remove_handler(event, handler)
+ events.remove_handler(event, handler);
+end
+
+function _M.listen_on(port, interface, ssl)
+ addserver(interface or "*", port, listener, "*a", ssl);
+end
+function _M.add_host(host)
+ hosts[host] = true;
+end
+function _M.remove_host(host)
+ hosts[host] = nil;
+end
+function _M.set_default_host(host)
+ default_host = host;
+end
+function _M.fire_event(event, ...)
+ return events.fire_event(event, ...);
+end
+
+_M.listener = listener;
+_M.codes = codes;
+_M._events = events;
+return _M;