Changeset

4631:fc5d3b053454

net.http.{server|codes|parser}: Initial commit.
author Waqas Hussain <waqas20@gmail.com>
date Sun, 08 Apr 2012 04:09:33 +0500
parents 4630:9502c0224caf
children 4632:52b6901cabb0
files net/http/codes.lua net/http/parser.lua net/http/server.lua
diffstat 3 files changed, 405 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/http/codes.lua	Sun Apr 08 04:09:33 2012 +0500
@@ -0,0 +1,66 @@
+
+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";
+	[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 })
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/http/parser.lua	Sun Apr 08 04:09:33 2012 +0500
@@ -0,0 +1,116 @@
+
+local tonumber = tonumber;
+local assert = assert;
+
+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;
+	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) (.*)$");
+								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) );
+								chunked = have_body and headers["transfer-encoding"] == "chunked";
+							else
+								method, path, httpversion = line:match("^(%w+) (%S+) HTTP/(1%.[01])$");
+								if not method then error = true; return error_cb("invalid-status-line"); end
+								path = path:gsub("^//+", "/"); -- TODO parse url more
+							end
+						end
+					end
+					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
+						len = len or 0;
+						packet = {
+							method = method;
+							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
+							local index = buf:find("\r\n", nil, true);
+							if not index then return; end -- not enough data
+							local chunk_size = buf:match("^%x+");
+							if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end
+							chunk_size = tonumber(chunk_size, 16);
+							index = index + 2;
+							if chunk_size == 0 then
+								state = nil; success_cb(packet);
+							elseif #buf - index + 1 >= chunk_size then -- we have a chunk
+								packet.body = packet.body..buf:sub(index, index + chunk_size - 1);
+								buf = buf:sub(index + chunk_size);
+							end
+							error("trailers"); -- FIXME MUST read trailers
+						elseif len and #buf >= len then
+							packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
+							state = nil; success_cb(packet);
+						end
+					elseif #buf >= len then
+						packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
+						state = nil; success_cb(packet);
+					end
+				end
+			end
+		end;
+	};
+end
+
+return httpstream;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/http/server.lua	Sun Apr 08 04:09:33 2012 +0500
@@ -0,0 +1,223 @@
+
+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 _M = {};
+
+local sessions = {};
+local handlers = {};
+
+local listener = {};
+
+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 };
+			--[[repeat
+				if events.fire_event(event, payload) ~= nil then return; end
+				event = (event:sub(-1) == "/") and event:sub(1, -1) or event:gsub("[^/]*$", "");
+				if event:sub(-1) == "/" then
+					event = event:sub(1, -1);
+				else
+					event = event:gsub("[^/]*$", "");
+				end
+			until not event:find("/", 1, true);]]
+			--log("debug", "Event: %s", event);
+			if events.fire_event(event, payload) ~= nil then return; end
+			-- TODO try adding/stripping / at the end, but this needs to work via an HTTP redirect
+		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 = _G.httpserver and _G.httpserver.new.http_servers[5280];
+	local handler = legacy_server and legacy_server.handlers[base];
+	if not handler then handler = _G.httpserver and _G.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>404 Not Found</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;