2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
10 local socket = require "socket"
11 local mime = require "mime"
12 local url = require "socket.url"
14 local server = require "net.server"
16 local connlisteners_get = require "net.connlisteners".get;
17 local listener = connlisteners_get("httpclient") or error("No httpclient listener!");
19 local t_insert, t_concat = table.insert, table.concat;
20 local pairs, ipairs = pairs, ipairs;
21 local tonumber, tostring, xpcall, select, debug_traceback, char, format =
22 tonumber, tostring, xpcall, select, debug.traceback, string.char, string.format;
24 local log = require "util.logger".init("http");
28 function urlencode(s) return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); end
29 function urldecode(s) return s and (s:gsub("%%(%x%x)", function (c) return char(tonumber(c,16)); end)); end
31 local function _formencodepart(s)
32 return s and (s:gsub("%W", function (c)
34 return format("%%%02x", c:byte());
40 function formencode(form)
42 for _, field in ipairs(form) do
43 t_insert(result, _formencodepart(field.name).."=".._formencodepart(field.value));
45 return t_concat(result, "&");
48 local function expectbody(reqt, code)
49 if reqt.method == "HEAD" then return nil end
50 if code == 204 or code == 304 or code == 301 then return nil end
51 if code >= 100 and code < 200 then return nil end
55 local function request_reader(request, data, startpos)
58 log("debug", "Connection closed, but we have data, calling callback...");
59 request.callback(t_concat(request.body), request.code, request);
60 elseif request.state ~= "completed" then
61 -- Error.. connection was closed prematurely
62 request.callback("connection-closed", 0, request);
65 destroy_request(request);
67 request.state = "completed";
70 if request.state == "body" and request.state ~= "completed" then
71 log("debug", "Reading body...")
72 if not request.body then request.body = {}; request.havebodylength, request.bodylength = 0, tonumber(request.responseheaders["content-length"]); end
74 data = data:sub(startpos, -1)
76 t_insert(request.body, data);
77 if request.bodylength then
78 request.havebodylength = request.havebodylength + #data;
79 if request.havebodylength >= request.bodylength then
81 log("debug", "Have full body, calling callback");
82 if request.callback then
83 request.callback(t_concat(request.body), request.code, request);
86 request.state = "completed";
88 log("debug", "Have "..request.havebodylength.." bytes out of "..request.bodylength);
91 elseif request.state == "headers" then
92 log("debug", "Reading headers...")
94 local headers, headers_complete = request.responseheaders;
97 request.responseheaders = headers;
99 for line in data:sub(startpos, -1):gmatch("(.-)\r\n") do
100 startpos = startpos + #line + 2;
101 local k, v = line:match("(%S+): (.+)");
103 headers[k:lower()] = v;
104 --log("debug", "Header: "..k:lower().." = "..v);
105 elseif #line == 0 then
106 headers_complete = true;
109 log("warn", "Unhandled header line: "..line);
112 if not headers_complete then return; end
113 -- Reached the end of the headers
114 if not expectbody(request, request.code) then
115 request.callback(nil, request.code, request);
118 request.state = "body";
119 if #data > startpos then
120 return request_reader(request, data, startpos);
122 elseif request.state == "status" then
123 log("debug", "Reading status...")
124 local http, code, text, linelen = data:match("^HTTP/(%S+) (%d+) (.-)\r\n()", startpos);
125 code = tonumber(code);
127 log("warn", "Invalid HTTP status line, telling callback then closing");
128 local ret = request.callback("invalid-status-line", 0, request);
129 destroy_request(request);
133 request.code, request.responseversion = code, http;
135 if request.onlystatus then
136 if request.callback then
137 request.callback(nil, code, request);
139 destroy_request(request);
143 request.state = "headers";
145 if #data > linelen then
146 return request_reader(request, data, linelen);
151 local function handleerr(err) log("error", "Traceback[http]: %s: %s", tostring(err), debug_traceback()); end
152 function request(u, ex, callback)
153 local req = url.parse(u);
155 if not (req and req.host) then
156 callback(nil, 0, req);
157 return nil, "invalid-url";
164 local custom_headers, body;
165 local default_headers = { ["Host"] = req.host, ["User-Agent"] = "Prosody XMPP Server" }
169 default_headers["Authorization"] = "Basic "..mime.b64(req.userinfo);
173 custom_headers = ex.headers;
174 req.onlystatus = ex.onlystatus;
177 req.method = "POST ";
178 default_headers["Content-Length"] = tostring(#body);
179 default_headers["Content-Type"] = "application/x-www-form-urlencoded";
181 if ex.method then req.method = ex.method; end
184 req.handler, req.conn = server.wrapclient(socket.tcp(), req.host, req.port or 80, listener, "*a");
185 req.write = function (...) return req.handler:write(...); end
186 req.conn:settimeout(0);
187 local ok, err = req.conn:connect(req.host, req.port or 80);
188 if not ok and err ~= "timeout" then
189 callback(nil, 0, req);
193 local request_line = { req.method or "GET", " ", req.path, " HTTP/1.1\r\n" };
196 t_insert(request_line, 4, "?");
197 t_insert(request_line, 5, req.query);
200 req.write(t_concat(request_line));
201 local t = { [2] = ": ", [4] = "\r\n" };
202 if custom_headers then
203 for k, v in pairs(custom_headers) do
205 req.write(t_concat(t));
206 default_headers[k] = nil;
210 for k, v in pairs(default_headers) do
212 req.write(t_concat(t));
213 default_headers[k] = nil;
221 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
222 req.reader = request_reader;
223 req.state = "status";
225 listener.register_request(req.handler, req);
230 function destroy_request(request)
233 request.handler:close()
234 listener.ondisconnect(request.handler, "closed");
238 _M.urlencode = urlencode;