net/http/server.lua
author Matthew Wild <mwild1@gmail.com>
Tue, 24 Apr 2012 19:07:12 +0100
changeset 4683 c1374e083c97
parent 4682 9d90c70b6358
child 4688 8d275c86a04f
permissions -rw-r--r--
net.http.server: Handle results returned by handlers, and send as a response. Also removes explicit firing of '*', which can now be done via wildcard events.


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 debug = debug;
local tostring = tostring;
local codes = require "net.http.codes";
local _G = _G;
local legacy_httpserver = require "net.httpserver";

local _M = {};

local sessions = {};
local handlers = {};

local listener = {};

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 event_map = events._event_map;
setmetatable(events._handlers, {
	__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[event]
		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"))), priority };
					table.insert(handlers_array, handler);
				end
			end
		end
		if #handlers_array == 0 then return; end
		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 ~= b then -- If equal, compare next score value
					return a_score[i] < b_score[i];
				end
			end
			return false;
		end);
		handlers[curr_event] = handlers_array;
		return handlers_array;
	end;
	__newindex = function (handlers, curr_event, handlers_array)
		if handlers_array == nil
		and is_wildcard_event(curr_event) then
			-- Invalidate all matching
			for event in pairs(handlers) do
				if is_wildcard_match(curr_event, event) then
					handlers[event] = nil;
				end
			end
		end
	end;
});

local handle_request;
local _1, _2, _3;
local function _handle_request() return handle_request(_1, _2, _3); end
local function _traceback_handler(err) log("error", "Traceback[http]: %s: %s", tostring(err), debug.traceback()); end

function listener.onconnect(conn)
	local secure = conn:ssl() and true or nil;
	local pending = {};
	local waiting = false;
	local function process_next(last_response)
		--if waiting then log("debug", "can't process_next, waiting"); return; end
		if sessions[conn] and #pending > 0 then
			local request = t_remove(pending);
			--log("debug", "process_next: %s", request.path);
			waiting = true;
			--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 503 Internal Server Error\r\n\r\nAn error occured during the processing of this request.");
				conn:close();
			end
		else
			--log("debug", "ready for more");
			waiting = false;
		end
	end
	local function success_cb(request)
		--log("debug", "success_cb: %s", request.path);
		request.secure = secure;
		t_insert(pending, request);
		if not waiting then
			process_next();
		end
	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)
	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;
	local keep_alive = conn_header == "Keep-Alive" or (request.httpversion == "1.1" and conn_header ~= "close");

	local response = {
		request = request;
		status_code = 200;
		headers = { date = date_header, connection = (keep_alive and "Keep-Alive" or "close") };
		conn = conn;
		send = _M.send_response;
		finish_cb = finish_cb;
	};

	if not request.headers.host then
		response.status_code = 400;
		response.headers.content_type = "text/html";
		response:send("<html><head>400 Bad Request</head><body>400 Bad Request: No Host header.</body></html>");
	else
		-- TODO call handler
		--response.headers.content_type = "text/plain";
		--response:send("host="..(request.headers.host or "").."\npath="..request.path.."\n"..(request.body or ""));
		local host = request.headers.host;
		if host then
			host = host:match("[^:]*"):lower();
			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 code, body = 200, "";
					local result_type = type(result);
					if result_type == "number" then
						response.status_code = result;
					elseif result_type == "string" then
						body = result;
					elseif result_type == "table" then
						body = result.body;
						result.body = nil;
						for k, v in pairs(result) do
							response[k] = v;
						end
					end
					response:send(body);
				end
				return;
			end
		end

		-- if handler not called, fallback to legacy httpserver handlers
		_M.legacy_handler(request, response);
	end
end
function _M.send_response(response, body)
	local status_line = "HTTP/"..response.request.httpversion.." "..(response.status or codes[response.status_code]);
	local headers = response.headers;
	body = 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 headers.connection == "Keep-Alive" then
		response:finish_cb();
	else
		response.conn:close();
	end
end
function _M.legacy_handler(request, response)
	log("debug", "Invoking legacy handler");
	local base = request.path:match("^/([^/?]+)");
	local legacy_server = legacy_httpserver and legacy_httpserver.new.http_servers[5280];
	local handler = legacy_server and legacy_server.handlers[base];
	if not handler then handler = legacy_httpserver and legacy_httpserver.set_default_handler.default_handler; end
	if handler then
		-- add legacy properties to request object
		request.url = { path = request.path };
		request.handler = response.conn;
		request.id = tostring{}:match("%x+$");
		local headers = {};
		for k,v in pairs(request.headers) do
			headers[k:gsub("_", "-")] = v;
		end
		request.headers = headers;
		function request:send(resp)
			if self.destroyed then return; end
			if resp.body or resp.headers then
				if resp.headers then
					for k,v in pairs(resp.headers) do response.headers[k] = v; end
				end
				response:send(resp.body)
			else
				response:send(resp)
			end
			self.sent = true;
			self:destroy();
		end
		function request:destroy()
			if self.destroyed then return; end
			if not self.sent then return self:send(""); end
			self.destroyed = true;
			if self.on_destroy then
				log("debug", "Request has destroy callback");
				self:on_destroy();
			else
				log("debug", "Request has no destroy callback");
			end
		end
		local r = handler(request.method, request.body, request);
		if r ~= true then
			request:send(r);
		end
	else
		log("debug", "No handler found");
		response.status_code = 404;
		response.headers.content_type = "text/html";
		response:send("<html><head><title>404 Not Found</title></head><body>404 Not Found: No such page.</body></html>");
	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

_M.listener = listener;
_M.codes = codes;
return _M;