aboutsummaryrefslogtreecommitdiffstats
path: root/net/http.lua
blob: 63ae31adae01a0c1b7a017ddec49a35964767bac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
-- 
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--


local socket = require "socket"
local mime = require "mime"
local url = require "socket.url"

local server = require "net.server"

local connlisteners_get = require "net.connlisteners".get;
local listener = connlisteners_get("httpclient") or error("No httpclient listener!");

local t_insert, t_concat = table.insert, table.concat;
local pairs, ipairs = pairs, ipairs;
local tonumber, tostring, xpcall, select, debug_traceback, char, format =
      tonumber, tostring, xpcall, select, debug.traceback, string.char, string.format;

local log = require "util.logger".init("http");

module "http"

function urlencode(s) return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); end
function urldecode(s) return s and (s:gsub("%%(%x%x)", function (c) return char(tonumber(c,16)); end)); end

local function _formencodepart(s)
	return s and (s:gsub("%W", function (c)
		if c ~= " " then
			return format("%%%02x", c:byte());
		else
			return "+";
		end
	end));
end
function formencode(form)
	local result = {};
	for _, field in ipairs(form) do
		t_insert(result, _formencodepart(field.name).."=".._formencodepart(field.value));
	end
	return t_concat(result, "&");
end

local function expectbody(reqt, code)
	if reqt.method == "HEAD" then return nil end
	if code == 204 or code == 304 or code == 301 then return nil end
	if code >= 100 and code < 200 then return nil end
	return 1
end

local function request_reader(request, data, startpos)
	if not data then
		if request.body then
			log("debug", "Connection closed, but we have data, calling callback...");
			request.callback(t_concat(request.body), request.code, request);
		elseif request.state ~= "completed" then
			-- Error.. connection was closed prematurely
			request.callback("connection-closed", 0, request);
			return;
		end
		destroy_request(request);
		request.body = nil;
		request.state = "completed";
		return;
	end
	if request.state == "body" and request.state ~= "completed" then
		log("debug", "Reading body...")
		if not request.body then request.body = {}; request.havebodylength, request.bodylength = 0, tonumber(request.responseheaders["content-length"]); end
		if startpos then
			data = data:sub(startpos, -1)
		end
		t_insert(request.body, data);
		if request.bodylength then
			request.havebodylength = request.havebodylength + #data;
			if request.havebodylength >= request.bodylength then
				-- We have the body
				log("debug", "Have full body, calling callback");
				if request.callback then
					request.callback(t_concat(request.body), request.code, request);
				end
				request.body = nil;
				request.state = "completed";
			else
				log("debug", "Have "..request.havebodylength.." bytes out of "..request.bodylength);
			end
		end
	elseif request.state == "headers" then
		log("debug", "Reading headers...")
		local pos = startpos;
		local headers, headers_complete = request.responseheaders;
		if not headers then
			headers = {};
			request.responseheaders = headers;
		end
		for line in data:sub(startpos, -1):gmatch("(.-)\r\n") do
			startpos = startpos + #line + 2;
			local k, v = line:match("(%S+): (.+)");
			if k and v then
				headers[k:lower()] = v;
				--log("debug", "Header: "..k:lower().." = "..v);
			elseif #line == 0 then
				headers_complete = true;
				break;
			else
				log("warn", "Unhandled header line: "..line);
			end
		end
		if not headers_complete then return; end
		-- Reached the end of the headers
		if not expectbody(request, request.code) then
			request.callback(nil, request.code, request);
			return;
		end
			request.state = "body";
		if #data > startpos then
			return request_reader(request, data, startpos);
		end
	elseif request.state == "status" then
		log("debug", "Reading status...")
		local http, code, text, linelen = data:match("^HTTP/(%S+) (%d+) (.-)\r\n()", startpos);
		code = tonumber(code);
		if not code then
			log("warn", "Invalid HTTP status line, telling callback then closing");
			local ret = request.callback("invalid-status-line", 0, request);
			destroy_request(request);
			return ret;
		end
		
		request.code, request.responseversion = code, http;
		
		if request.onlystatus then
			if request.callback then
				request.callback(nil, code, request);
			end
			destroy_request(request);
			return;
		end
		
		request.state = "headers";
		
		if #data > linelen then
			return request_reader(request, data, linelen);
		end
	end
end

local function handleerr(err) log("error", "Traceback[http]: %s: %s", tostring(err), debug_traceback()); end
function request(u, ex, callback)
	local req = url.parse(u);
	
	if not (req and req.host) then
		callback(nil, 0, req);
		return nil, "invalid-url";
	end
	
	if not req.path then
		req.path = "/";
	end
	
	local custom_headers, body;
	local default_headers = { ["Host"] = req.host, ["User-Agent"] = "Prosody XMPP Server" }
	
	
	if req.userinfo then
		default_headers["Authorization"] = "Basic "..mime.b64(req.userinfo);
	end
	
	if ex then
		custom_headers = ex.headers;
		req.onlystatus = ex.onlystatus;
		body = ex.body;
		if body then
			req.method = "POST ";
			default_headers["Content-Length"] = tostring(#body);
			default_headers["Content-Type"] = "application/x-www-form-urlencoded";
		end
		if ex.method then req.method = ex.method; end
	end
	
	req.handler, req.conn = server.wrapclient(socket.tcp(), req.host, req.port or 80, listener, "*a");
	req.write = function (...) return req.handler:write(...); end
	req.conn:settimeout(0);
	local ok, err = req.conn:connect(req.host, req.port or 80);
	if not ok and err ~= "timeout" then
		callback(nil, 0, req);
		return nil, err;
	end
	
	local request_line = { req.method or "GET", " ", req.path, " HTTP/1.1\r\n" };
	
	if req.query then
		t_insert(request_line, 4, "?");
		t_insert(request_line, 5, req.query);
	end
	
	req.write(t_concat(request_line));
	local t = { [2] = ": ", [4] = "\r\n" };
	if custom_headers then
		for k, v in pairs(custom_headers) do
			t[1], t[3] = k, v;
			req.write(t_concat(t));
			default_headers[k] = nil;
		end
	end
	
	for k, v in pairs(default_headers) do
		t[1], t[3] = k, v;
		req.write(t_concat(t));
		default_headers[k] = nil;
	end
	req.write("\r\n");
	
	if body then
		req.write(body);
	end
	
	req.callback = function (content, code, request) log("debug", "Calling callback, status %s", code or "---"); return select(2, xpcall(function () return callback(content, code, request) end, handleerr)); end
	req.reader = request_reader;
	req.state = "status";
	
	listener.register_request(req.handler, req);

	return req;
end

function destroy_request(request)
	if request.conn then
		request.conn = nil;
		request.handler:close()
		listener.ondisconnect(request.handler, "closed");
	end
end

_M.urlencode = urlencode;

return _M;