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
|
-- 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 tonumber, tostring, pairs, xpcall, select, debug_traceback, char, format =
tonumber, tostring, pairs, 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 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;
|