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
|
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 =
tonumber, tostring, pairs, xpcall, select, debug.traceback;
local log = require "util.logger".init("http");
local print = function () end
local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end });
local urlencode = function (s) return s and (s:gsub("%W", function (c) return string.format("%%%02x", c:byte()); end)); end
module "http"
local function expectbody(reqt, code)
if reqt.method == "HEAD" then return nil end
if code == 204 or code == 304 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);
end
destroy_request(request);
request.body = nil;
request.state = "completed";
return;
end
if request.state == "body" and request.state ~= "completed" then
print("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
print("", "Have "..request.havebodylength.." bytes out of "..request.bodylength);
end
end
elseif request.state == "headers" then
print("Reading headers...")
local pos = startpos;
local headers = request.responseheaders or {};
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;
print("Header: "..k:lower().." = "..v);
elseif #line == 0 then
request.responseheaders = headers;
break;
else
print("Unhandled header line: "..line);
end
end
-- Reached the end of the headers
request.state = "body";
if #data > startpos then
return request_reader(request, data, startpos);
end
elseif request.state == "status" then
print("Reading status...")
local http, code, text, linelen = data:match("^HTTP/(%S+) (%d+) (.-)\r\n()", startpos);
code = tonumber(code);
if not code then
return request.callback("invalid-status-line", 0, request);
end
request.code, request.responseversion = code, http;
if request.onlystatus or not expectbody(request, code) 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);
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.custom_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.wraptcpclient(listener, socket.tcp(), req.host, req.port or 80, 0, "*a");
req.write = req.handler.write;
req.conn:settimeout(0);
local ok, err = req.conn:connect(req.host, req.port or 80);
if not ok and err ~= "timeout" then
return nil, err;
end
req.write((req.method or "GET ")..req.path.." HTTP/1.0\r\n");
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, code %s content: %s", code or "---", content 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.handler.close()
listener.disconnect(request.conn, "closed");
end
end
_M.urlencode = urlencode;
return _M;
|