Automated merge with http://waqas.ath.cx:8000/
[prosody.git] / net / http.lua
1
2 local socket = require "socket"
3 local mime = require "mime"
4 local url = require "socket.url"
5
6 local server = require "net.server"
7
8 local connlisteners_get = require "net.connlisteners".get;
9 local listener = connlisteners_get("httpclient") or error("No httpclient listener!");
10
11 local t_insert, t_concat = table.insert, table.concat;
12 local tonumber, tostring, pairs, xpcall, select, debug_traceback, char = 
13         tonumber, tostring, pairs, xpcall, select, debug.traceback, string.char;
14
15 local log = require "util.logger".init("http");
16 local print = function () end
17
18 local urlencode = function (s) return s and (s:gsub("%W", function (c) return string.format("%%%02x", c:byte()); end)); end
19
20 module "http"
21
22 local function expectbody(reqt, code)
23     if reqt.method == "HEAD" then return nil end
24     if code == 204 or code == 304 then return nil end
25     if code >= 100 and code < 200 then return nil end
26     return 1
27 end
28
29 local function request_reader(request, data, startpos)
30         if not data then
31                 if request.body then
32                         log("debug", "Connection closed, but we have data, calling callback...");
33                         request.callback(t_concat(request.body), request.code, request);
34                 elseif request.state ~= "completed" then
35                         -- Error.. connection was closed prematurely
36                         request.callback("connection-closed", 0, request);
37                 end
38                 destroy_request(request);
39                 request.body = nil;
40                 request.state = "completed";
41                 return;
42         end
43         if request.state == "body" and request.state ~= "completed" then
44                 print("Reading body...")
45                 if not request.body then request.body = {}; request.havebodylength, request.bodylength = 0, tonumber(request.responseheaders["content-length"]); end
46                 if startpos then
47                         data = data:sub(startpos, -1)
48                 end
49                 t_insert(request.body, data);
50                 if request.bodylength then
51                         request.havebodylength = request.havebodylength + #data;
52                         if request.havebodylength >= request.bodylength then
53                                 -- We have the body
54                                 log("debug", "Have full body, calling callback");
55                                 if request.callback then
56                                         request.callback(t_concat(request.body), request.code, request);
57                                 end
58                                 request.body = nil;
59                                 request.state = "completed";
60                         else
61                                 print("", "Have "..request.havebodylength.." bytes out of "..request.bodylength);
62                         end
63                 end
64         elseif request.state == "headers" then
65                 print("Reading headers...")
66                 local pos = startpos;
67                 local headers = request.responseheaders or {};
68                 for line in data:sub(startpos, -1):gmatch("(.-)\r\n") do
69                         startpos = startpos + #line + 2;
70                         local k, v = line:match("(%S+): (.+)");
71                         if k and v then
72                                 headers[k:lower()] = v;
73                                 print("Header: "..k:lower().." = "..v);
74                         elseif #line == 0 then
75                                 request.responseheaders = headers;
76                                 break;
77                         else
78                                 print("Unhandled header line: "..line);
79                         end
80                 end
81                 -- Reached the end of the headers
82                 request.state = "body";
83                 if #data > startpos then
84                         return request_reader(request, data, startpos);
85                 end
86         elseif request.state == "status" then
87                 print("Reading status...")
88                 local http, code, text, linelen = data:match("^HTTP/(%S+) (%d+) (.-)\r\n()", startpos);
89                 code = tonumber(code);
90                 if not code then
91                         return request.callback("invalid-status-line", 0, request);
92                 end
93                 
94                 request.code, request.responseversion = code, http;
95                 
96                 if request.onlystatus or not expectbody(request, code) then
97                         if request.callback then
98                                 request.callback(nil, code, request);
99                         end
100                         destroy_request(request);
101                         return;
102                 end
103                 
104                 request.state = "headers";
105                 
106                 if #data > linelen then
107                         return request_reader(request, data, linelen);
108                 end
109         end
110 end
111
112 local function handleerr(err) log("error", "Traceback[http]: %s: %s", tostring(err), debug_traceback()); end
113 function request(u, ex, callback)
114         local req = url.parse(u);
115         
116         if not (req and req.host) then
117                 callback(nil, 0, req);
118                 return nil, "invalid-url";
119         end
120         
121         if not req.path then
122                 req.path = "/";
123         end
124         
125         local custom_headers, body;
126         local default_headers = { ["Host"] = req.host, ["User-Agent"] = "Prosody XMPP Server" }
127         
128         
129         if req.userinfo then
130                 default_headers["Authorization"] = "Basic "..mime.b64(req.userinfo);
131         end
132         
133         if ex then
134                 custom_headers = ex.headers;
135                 req.onlystatus = ex.onlystatus;
136                 body = ex.body;
137                 if body then
138                         req.method = "POST ";
139                         default_headers["Content-Length"] = tostring(#body);
140                         default_headers["Content-Type"] = "application/x-www-form-urlencoded";
141                 end
142                 if ex.method then req.method = ex.method; end
143         end
144         
145         req.handler, req.conn = server.wrapclient(socket.tcp(), req.host, req.port or 80, listener, "*a");
146         req.write = req.handler.write;
147         req.conn:settimeout(0);
148         local ok, err = req.conn:connect(req.host, req.port or 80);
149         if not ok and err ~= "timeout" then
150                 callback(nil, 0, req);
151                 return nil, err;
152         end
153         
154         local request_line = { req.method or "GET", " ", req.path, " HTTP/1.1\r\n" };
155         
156         if req.query then
157                 t_insert(request_line, 4, "?");
158                 t_insert(request_line, 5, req.query);
159         end
160         
161         req.write(t_concat(request_line));
162         local t = { [2] = ": ", [4] = "\r\n" };
163         if custom_headers then
164                 for k, v in pairs(custom_headers) do
165                         t[1], t[3] = k, v;
166                         req.write(t_concat(t));
167                         default_headers[k] = nil;
168                 end
169         end
170         
171         for k, v in pairs(default_headers) do
172                 t[1], t[3] = k, v;
173                 req.write(t_concat(t));
174                 default_headers[k] = nil;
175         end
176         req.write("\r\n");
177         
178         if body then
179                 req.write(body);
180         end
181         
182         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
183         req.reader = request_reader;
184         req.state = "status";
185         
186         listener.register_request(req.handler, req);
187
188         return req;
189 end
190
191 function destroy_request(request)
192         if request.conn then
193                 request.handler.close()
194                 listener.disconnect(request.conn, "closed");
195         end
196 end
197
198 _M.urlencode = urlencode;
199
200 return _M;