net/httpserver.lua

1
-- Prosody IM
2
-- Copyright (C) 2008-2010 Matthew Wild
3
-- Copyright (C) 2008-2010 Waqas Hussain
4
-- 
5
-- This project is MIT/X11 licensed. Please see the
6
-- COPYING file in the source package for more information.
7
--
8
 
9
 
10
local socket = require "socket"
11
local server = require "net.server"
12
local url_parse = require "socket.url".parse;
13
local httpstream_new = require "util.httpstream".new;
14
 
15
local connlisteners_start = require "net.connlisteners".start;
16
local connlisteners_get = require "net.connlisteners".get;
17
local listener;
18
 
19
local t_insert, t_concat = table.insert, table.concat;
20
local s_match, s_gmatch = string.match, string.gmatch;
21
local tonumber, tostring, pairs, ipairs, type = tonumber, tostring, pairs, ipairs, type;
22
 
23
local urlencode = function (s) return s and (s:gsub("%W", function (c) return string.format("%%%02x", c:byte()); end)); end
24
 
25
local log = require "util.logger".init("httpserver");
26
 
27
local http_servers = {};
28
 
29
module "httpserver"
30
 
31
local default_handler;
32
 
33
local function expectbody(reqt)
34
	return reqt.method == "POST";
35
end
36
 
37
local function send_response(request, response)
38
	-- Write status line
39
	local resp;
40
	if response.body or response.headers then
41
		local body = response.body and tostring(response.body);
42
		log("debug", "Sending response to %s", request.id);
43
		resp = { "HTTP/1.0 "..(response.status or "200 OK").."\r\n" };
44
		local h = response.headers;
45
		if h then
46
			for k, v in pairs(h) do
47
				t_insert(resp, k..": "..v.."\r\n");
48
			end
49
		end
50
		if body and not (h and h["Content-Length"]) then
51
			t_insert(resp, "Content-Length: "..#body.."\r\n");
52
		end
53
		t_insert(resp, "\r\n");
54
		
55
		if body and request.method ~= "HEAD" then
56
			t_insert(resp, body);
57
		end
58
		request.write(t_concat(resp));
59
	else
60
		-- Response we have is just a string (the body)
61
		log("debug", "Sending 200 response to %s", request.id or "<none>");
62
		
63
		local resp = "HTTP/1.0 200 OK\r\n"
64
			.. "Connection: close\r\n"
65
			.. "Content-Type: text/html\r\n"
66
			.. "Content-Length: "..#response.."\r\n"
67
			.. "\r\n"
68
			.. response;
69
		
70
		request.write(resp);
71
	end
72
	if not request.stayopen then
73
		request:destroy();
74
	end
75
end
76
 
77
local function call_callback(request, err)
78
	if request.handled then return; end
79
	request.handled = true;
80
	local callback = request.callback;
81
	if not callback and request.path then
82
		local path = request.url.path;
83
		local base = path:match("^/([^/?]+)");
84
		if not base then
85
			base = path:match("^http://[^/?]+/([^/?]+)");
86
		end
87
		
88
		callback = (request.server and request.server.handlers[base]) or default_handler;
89
	end
90
	if callback then
91
		if err then
92
			log("debug", "Request error: "..err);
93
			if not callback(nil, err, request) then
94
				destroy_request(request);
95
			end
96
			return;
97
		end
98
		
99
		local response = callback(request.method, request.body and t_concat(request.body), request);
100
		if response then
101
			if response == true and not request.destroyed then
102
				-- Keep connection open, we will reply later
103
				log("debug", "Request %s left open, on_destroy is %s", request.id, tostring(request.on_destroy));
104
			elseif response ~= true then
105
				-- Assume response
106
				send_response(request, response);
107
				destroy_request(request);
108
			end
109
		else
110
			log("debug", "Request handler provided no response, destroying request...");
111
			-- No response, close connection
112
			destroy_request(request);
113
		end
114
	end
115
end
116
 
117
local function request_reader(request, data, startpos)
118
	if not request.parser then
119
		local function success_cb(r)
120
			for k,v in pairs(r) do request[k] = v; end
121
			request.url = url_parse(request.path);
122
			request.body = { request.body };
123
			call_callback(request);
124
		end
125
		local function error_cb(r)
126
			call_callback(request, r or "connection-closed");
127
			destroy_request(request);
128
		end
129
		request.parser = httpstream_new(success_cb, error_cb);
130
	end
131
	request.parser:feed(data);
132
end
133
 
134
-- The default handler for requests
135
default_handler = function (method, body, request)
136
	log("debug", method.." request for "..tostring(request.path) .. " on port "..request.handler:serverport());
137
	return { status = "404 Not Found",
138
			headers = { ["Content-Type"] = "text/html" },
139
			body = "<html><head><title>Page Not Found</title></head><body>Not here :(</body></html>" };
140
end
141
 
142
 
143
function new_request(handler)
144
	return { handler = handler, conn = handler,
145
			write = function (...) return handler:write(...); end, state = "request",
146
			server = http_servers[handler:serverport()],
147
			send = send_response,
148
			destroy = destroy_request,
149
			id = tostring{}:match("%x+$")
150
			 };
151
end
152
 
153
function destroy_request(request)
154
	log("debug", "Destroying request %s", request.id);
155
	listener = listener or connlisteners_get("httpserver");
156
	if not request.destroyed then
157
		request.destroyed = true;
158
		if request.on_destroy then
159
			log("debug", "Request has destroy callback");
160
			request.on_destroy(request);
161
		else
162
			log("debug", "Request has no destroy callback");
163
		end
164
		request.handler:close()
165
		if request.conn then
166
			listener.ondisconnect(request.conn, "closed");
167
		end
168
	end
169
end
170
 
171
function new(params)
172
	local http_server = http_servers[params.port];
173
	if not http_server then
174
		http_server = { handlers = {} };
175
		http_servers[params.port] = http_server;
176
		-- We weren't already listening on this port, so start now
177
		connlisteners_start("httpserver", params);
178
	end
179
	if params.base then
180
		http_server.handlers[params.base] = params.handler;
181
	end
182
end
183
 
184
function set_default_handler(handler)
185
	default_handler = handler;
186
end
187
 
188
function new_from_config(ports, handle_request, default_options)
189
	if type(handle_request) == "string" then -- COMPAT with old plugins
190
		log("warn", "Old syntax of httpserver.new_from_config being used to register %s", handle_request);
191
		handle_request, default_options = default_options, { base = handle_request };
192
	end
193
	ports = ports or {5280};
194
	for _, options in ipairs(ports) do
195
		local port = default_options.port or 5280;
196
		local base = default_options.base;
197
		local ssl = default_options.ssl or false;
198
		local interface = default_options.interface;
199
		if type(options) == "number" then
200
			port = options;
201
		elseif type(options) == "table" then
202
			port = options.port or port;
203
			base = options.path or base;
204
			ssl = options.ssl or ssl;
205
			interface = options.interface or interface;
206
		elseif type(options) == "string" then
207
			base = options;
208
		end
209
		
210
		if ssl then
211
			ssl.mode = "server";
212
			ssl.protocol = "sslv23";
213
			ssl.options = "no_sslv2";
214
		end
215
		
216
		new{ port = port, interface = interface,
217
			base = base, handler = handle_request,
218
			ssl = ssl, type = (ssl and "ssl") or "tcp" };
219
	end
220
end
221
 
222
_M.request_reader = request_reader;
223
_M.send_response = send_response;
224
_M.urlencode = urlencode;
225
 
226
return _M;