diff options
Diffstat (limited to 'vendor/luasocket/src/http.lua')
-rw-r--r-- | vendor/luasocket/src/http.lua | 424 |
1 files changed, 424 insertions, 0 deletions
diff --git a/vendor/luasocket/src/http.lua b/vendor/luasocket/src/http.lua new file mode 100644 index 00000000..1330355f --- /dev/null +++ b/vendor/luasocket/src/http.lua | |||
@@ -0,0 +1,424 @@ | |||
1 | ----------------------------------------------------------------------------- | ||
2 | -- HTTP/1.1 client support for the Lua language. | ||
3 | -- LuaSocket toolkit. | ||
4 | -- Author: Diego Nehab | ||
5 | ----------------------------------------------------------------------------- | ||
6 | |||
7 | ----------------------------------------------------------------------------- | ||
8 | -- Declare module and import dependencies | ||
9 | ------------------------------------------------------------------------------- | ||
10 | local socket = require("socket") | ||
11 | local url = require("socket.url") | ||
12 | local ltn12 = require("ltn12") | ||
13 | local mime = require("mime") | ||
14 | local string = require("string") | ||
15 | local headers = require("socket.headers") | ||
16 | local base = _G | ||
17 | local table = require("table") | ||
18 | socket.http = {} | ||
19 | local _M = socket.http | ||
20 | |||
21 | ----------------------------------------------------------------------------- | ||
22 | -- Program constants | ||
23 | ----------------------------------------------------------------------------- | ||
24 | -- connection timeout in seconds | ||
25 | _M.TIMEOUT = 60 | ||
26 | -- user agent field sent in request | ||
27 | _M.USERAGENT = socket._VERSION | ||
28 | |||
29 | -- supported schemes and their particulars | ||
30 | local SCHEMES = { | ||
31 | http = { | ||
32 | port = 80 | ||
33 | , create = function(t) | ||
34 | return socket.tcp end } | ||
35 | , https = { | ||
36 | port = 443 | ||
37 | , create = function(t) | ||
38 | local https = assert( | ||
39 | require("ssl.https"), 'LuaSocket: LuaSec not found') | ||
40 | local tcp = assert( | ||
41 | https.tcp, 'LuaSocket: Function tcp() not available from LuaSec') | ||
42 | return tcp(t) end }} | ||
43 | |||
44 | ----------------------------------------------------------------------------- | ||
45 | -- Reads MIME headers from a connection, unfolding where needed | ||
46 | ----------------------------------------------------------------------------- | ||
47 | local function receiveheaders(sock, headers) | ||
48 | local line, name, value, err | ||
49 | headers = headers or {} | ||
50 | -- get first line | ||
51 | line, err = sock:receive() | ||
52 | if err then return nil, err end | ||
53 | -- headers go until a blank line is found | ||
54 | while line ~= "" do | ||
55 | -- get field-name and value | ||
56 | name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) | ||
57 | if not (name and value) then return nil, "malformed reponse headers" end | ||
58 | name = string.lower(name) | ||
59 | -- get next line (value might be folded) | ||
60 | line, err = sock:receive() | ||
61 | if err then return nil, err end | ||
62 | -- unfold any folded values | ||
63 | while string.find(line, "^%s") do | ||
64 | value = value .. line | ||
65 | line = sock:receive() | ||
66 | if err then return nil, err end | ||
67 | end | ||
68 | -- save pair in table | ||
69 | if headers[name] then headers[name] = headers[name] .. ", " .. value | ||
70 | else headers[name] = value end | ||
71 | end | ||
72 | return headers | ||
73 | end | ||
74 | |||
75 | ----------------------------------------------------------------------------- | ||
76 | -- Extra sources and sinks | ||
77 | ----------------------------------------------------------------------------- | ||
78 | socket.sourcet["http-chunked"] = function(sock, headers) | ||
79 | return base.setmetatable({ | ||
80 | getfd = function() return sock:getfd() end, | ||
81 | dirty = function() return sock:dirty() end | ||
82 | }, { | ||
83 | __call = function() | ||
84 | -- get chunk size, skip extention | ||
85 | local line, err = sock:receive() | ||
86 | if err then return nil, err end | ||
87 | local size = base.tonumber(string.gsub(line, ";.*", ""), 16) | ||
88 | if not size then return nil, "invalid chunk size" end | ||
89 | -- was it the last chunk? | ||
90 | if size > 0 then | ||
91 | -- if not, get chunk and skip terminating CRLF | ||
92 | local chunk, err, _ = sock:receive(size) | ||
93 | if chunk then sock:receive() end | ||
94 | return chunk, err | ||
95 | else | ||
96 | -- if it was, read trailers into headers table | ||
97 | headers, err = receiveheaders(sock, headers) | ||
98 | if not headers then return nil, err end | ||
99 | end | ||
100 | end | ||
101 | }) | ||
102 | end | ||
103 | |||
104 | socket.sinkt["http-chunked"] = function(sock) | ||
105 | return base.setmetatable({ | ||
106 | getfd = function() return sock:getfd() end, | ||
107 | dirty = function() return sock:dirty() end | ||
108 | }, { | ||
109 | __call = function(self, chunk, err) | ||
110 | if not chunk then return sock:send("0\r\n\r\n") end | ||
111 | local size = string.format("%X\r\n", string.len(chunk)) | ||
112 | return sock:send(size .. chunk .. "\r\n") | ||
113 | end | ||
114 | }) | ||
115 | end | ||
116 | |||
117 | ----------------------------------------------------------------------------- | ||
118 | -- Low level HTTP API | ||
119 | ----------------------------------------------------------------------------- | ||
120 | local metat = { __index = {} } | ||
121 | |||
122 | function _M.open(host, port, create) | ||
123 | -- create socket with user connect function, or with default | ||
124 | local c = socket.try(create()) | ||
125 | local h = base.setmetatable({ c = c }, metat) | ||
126 | -- create finalized try | ||
127 | h.try = socket.newtry(function() h:close() end) | ||
128 | -- set timeout before connecting | ||
129 | h.try(c:settimeout(_M.TIMEOUT)) | ||
130 | h.try(c:connect(host, port)) | ||
131 | -- here everything worked | ||
132 | return h | ||
133 | end | ||
134 | |||
135 | function metat.__index:sendrequestline(method, uri) | ||
136 | local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) | ||
137 | return self.try(self.c:send(reqline)) | ||
138 | end | ||
139 | |||
140 | function metat.__index:sendheaders(tosend) | ||
141 | local canonic = headers.canonic | ||
142 | local h = "\r\n" | ||
143 | for f, v in base.pairs(tosend) do | ||
144 | h = (canonic[f] or f) .. ": " .. v .. "\r\n" .. h | ||
145 | end | ||
146 | self.try(self.c:send(h)) | ||
147 | return 1 | ||
148 | end | ||
149 | |||
150 | function metat.__index:sendbody(headers, source, step) | ||
151 | source = source or ltn12.source.empty() | ||
152 | step = step or ltn12.pump.step | ||
153 | -- if we don't know the size in advance, send chunked and hope for the best | ||
154 | local mode = "http-chunked" | ||
155 | if headers["content-length"] then mode = "keep-open" end | ||
156 | return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) | ||
157 | end | ||
158 | |||
159 | function metat.__index:receivestatusline() | ||
160 | local status,ec = self.try(self.c:receive(5)) | ||
161 | -- identify HTTP/0.9 responses, which do not contain a status line | ||
162 | -- this is just a heuristic, but is what the RFC recommends | ||
163 | if status ~= "HTTP/" then | ||
164 | if ec == "timeout" then | ||
165 | return 408 | ||
166 | end | ||
167 | return nil, status | ||
168 | end | ||
169 | -- otherwise proceed reading a status line | ||
170 | status = self.try(self.c:receive("*l", status)) | ||
171 | local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) | ||
172 | return self.try(base.tonumber(code), status) | ||
173 | end | ||
174 | |||
175 | function metat.__index:receiveheaders() | ||
176 | return self.try(receiveheaders(self.c)) | ||
177 | end | ||
178 | |||
179 | function metat.__index:receivebody(headers, sink, step) | ||
180 | sink = sink or ltn12.sink.null() | ||
181 | step = step or ltn12.pump.step | ||
182 | local length = base.tonumber(headers["content-length"]) | ||
183 | local t = headers["transfer-encoding"] -- shortcut | ||
184 | local mode = "default" -- connection close | ||
185 | if t and t ~= "identity" then mode = "http-chunked" | ||
186 | elseif base.tonumber(headers["content-length"]) then mode = "by-length" end | ||
187 | return self.try(ltn12.pump.all(socket.source(mode, self.c, length), | ||
188 | sink, step)) | ||
189 | end | ||
190 | |||
191 | function metat.__index:receive09body(status, sink, step) | ||
192 | local source = ltn12.source.rewind(socket.source("until-closed", self.c)) | ||
193 | source(status) | ||
194 | return self.try(ltn12.pump.all(source, sink, step)) | ||
195 | end | ||
196 | |||
197 | function metat.__index:close() | ||
198 | return self.c:close() | ||
199 | end | ||
200 | |||
201 | ----------------------------------------------------------------------------- | ||
202 | -- High level HTTP API | ||
203 | ----------------------------------------------------------------------------- | ||
204 | local function adjusturi(reqt) | ||
205 | local u = reqt | ||
206 | -- if there is a proxy, we need the full url. otherwise, just a part. | ||
207 | if not reqt.proxy and not _M.PROXY then | ||
208 | u = { | ||
209 | path = socket.try(reqt.path, "invalid path 'nil'"), | ||
210 | params = reqt.params, | ||
211 | query = reqt.query, | ||
212 | fragment = reqt.fragment | ||
213 | } | ||
214 | end | ||
215 | return url.build(u) | ||
216 | end | ||
217 | |||
218 | local function adjustproxy(reqt) | ||
219 | local proxy = reqt.proxy or _M.PROXY | ||
220 | if proxy then | ||
221 | proxy = url.parse(proxy) | ||
222 | return proxy.host, proxy.port or 3128 | ||
223 | else | ||
224 | return reqt.host, reqt.port | ||
225 | end | ||
226 | end | ||
227 | |||
228 | local function adjustheaders(reqt) | ||
229 | -- default headers | ||
230 | local host = reqt.host | ||
231 | local port = tostring(reqt.port) | ||
232 | if port ~= tostring(SCHEMES[reqt.scheme].port) then | ||
233 | host = host .. ':' .. port end | ||
234 | local lower = { | ||
235 | ["user-agent"] = _M.USERAGENT, | ||
236 | ["host"] = host, | ||
237 | ["connection"] = "close, TE", | ||
238 | ["te"] = "trailers" | ||
239 | } | ||
240 | -- if we have authentication information, pass it along | ||
241 | if reqt.user and reqt.password then | ||
242 | lower["authorization"] = | ||
243 | "Basic " .. (mime.b64(reqt.user .. ":" .. | ||
244 | url.unescape(reqt.password))) | ||
245 | end | ||
246 | -- if we have proxy authentication information, pass it along | ||
247 | local proxy = reqt.proxy or _M.PROXY | ||
248 | if proxy then | ||
249 | proxy = url.parse(proxy) | ||
250 | if proxy.user and proxy.password then | ||
251 | lower["proxy-authorization"] = | ||
252 | "Basic " .. (mime.b64(proxy.user .. ":" .. proxy.password)) | ||
253 | end | ||
254 | end | ||
255 | -- override with user headers | ||
256 | for i,v in base.pairs(reqt.headers or lower) do | ||
257 | lower[string.lower(i)] = v | ||
258 | end | ||
259 | return lower | ||
260 | end | ||
261 | |||
262 | -- default url parts | ||
263 | local default = { | ||
264 | path ="/" | ||
265 | , scheme = "http" | ||
266 | } | ||
267 | |||
268 | local function adjustrequest(reqt) | ||
269 | -- parse url if provided | ||
270 | local nreqt = reqt.url and url.parse(reqt.url, default) or {} | ||
271 | -- explicit components override url | ||
272 | for i,v in base.pairs(reqt) do nreqt[i] = v end | ||
273 | -- default to scheme particulars | ||
274 | local schemedefs, host, port, method | ||
275 | = SCHEMES[nreqt.scheme], nreqt.host, nreqt.port, nreqt.method | ||
276 | if not nreqt.create then nreqt.create = schemedefs.create(nreqt) end | ||
277 | if not (port and port ~= '') then nreqt.port = schemedefs.port end | ||
278 | if not (method and method ~= '') then nreqt.method = 'GET' end | ||
279 | if not (host and host ~= "") then | ||
280 | socket.try(nil, "invalid host '" .. base.tostring(nreqt.host) .. "'") | ||
281 | end | ||
282 | -- compute uri if user hasn't overriden | ||
283 | nreqt.uri = reqt.uri or adjusturi(nreqt) | ||
284 | -- adjust headers in request | ||
285 | nreqt.headers = adjustheaders(nreqt) | ||
286 | if nreqt.source | ||
287 | and not nreqt.headers["content-length"] | ||
288 | and not nreqt.headers["transfer-encoding"] | ||
289 | then | ||
290 | nreqt.headers["transfer-encoding"] = "chunked" | ||
291 | end | ||
292 | |||
293 | -- ajust host and port if there is a proxy | ||
294 | nreqt.host, nreqt.port = adjustproxy(nreqt) | ||
295 | return nreqt | ||
296 | end | ||
297 | |||
298 | local function shouldredirect(reqt, code, headers) | ||
299 | local location = headers.location | ||
300 | if not location then return false end | ||
301 | location = string.gsub(location, "%s", "") | ||
302 | if location == "" then return false end | ||
303 | local scheme = url.parse(location).scheme | ||
304 | if scheme and (not SCHEMES[scheme]) then return false end | ||
305 | -- avoid https downgrades | ||
306 | if ('https' == reqt.scheme) and ('https' ~= scheme) then return false end | ||
307 | return (reqt.redirect ~= false) and | ||
308 | (code == 301 or code == 302 or code == 303 or code == 307) and | ||
309 | (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") | ||
310 | and ((false == reqt.maxredirects) | ||
311 | or ((reqt.nredirects or 0) | ||
312 | < (reqt.maxredirects or 5))) | ||
313 | end | ||
314 | |||
315 | local function shouldreceivebody(reqt, code) | ||
316 | if reqt.method == "HEAD" then return nil end | ||
317 | if code == 204 or code == 304 then return nil end | ||
318 | if code >= 100 and code < 200 then return nil end | ||
319 | return 1 | ||
320 | end | ||
321 | |||
322 | -- forward declarations | ||
323 | local trequest, tredirect | ||
324 | |||
325 | --[[local]] function tredirect(reqt, location) | ||
326 | -- the RFC says the redirect URL has to be absolute, but some | ||
327 | -- servers do not respect that | ||
328 | local newurl = url.absolute(reqt.url, location) | ||
329 | -- if switching schemes, reset port and create function | ||
330 | if url.parse(newurl).scheme ~= reqt.scheme then | ||
331 | reqt.port = nil | ||
332 | reqt.create = nil end | ||
333 | -- make new request | ||
334 | local result, code, headers, status = trequest { | ||
335 | url = newurl, | ||
336 | source = reqt.source, | ||
337 | sink = reqt.sink, | ||
338 | headers = reqt.headers, | ||
339 | proxy = reqt.proxy, | ||
340 | maxredirects = reqt.maxredirects, | ||
341 | nredirects = (reqt.nredirects or 0) + 1, | ||
342 | create = reqt.create | ||
343 | } | ||
344 | -- pass location header back as a hint we redirected | ||
345 | headers = headers or {} | ||
346 | headers.location = headers.location or location | ||
347 | return result, code, headers, status | ||
348 | end | ||
349 | |||
350 | --[[local]] function trequest(reqt) | ||
351 | -- we loop until we get what we want, or | ||
352 | -- until we are sure there is no way to get it | ||
353 | local nreqt = adjustrequest(reqt) | ||
354 | local h = _M.open(nreqt.host, nreqt.port, nreqt.create) | ||
355 | -- send request line and headers | ||
356 | h:sendrequestline(nreqt.method, nreqt.uri) | ||
357 | h:sendheaders(nreqt.headers) | ||
358 | -- if there is a body, send it | ||
359 | if nreqt.source then | ||
360 | h:sendbody(nreqt.headers, nreqt.source, nreqt.step) | ||
361 | end | ||
362 | local code, status = h:receivestatusline() | ||
363 | -- if it is an HTTP/0.9 server, simply get the body and we are done | ||
364 | if not code then | ||
365 | h:receive09body(status, nreqt.sink, nreqt.step) | ||
366 | return 1, 200 | ||
367 | elseif code == 408 then | ||
368 | return 1, code | ||
369 | end | ||
370 | local headers | ||
371 | -- ignore any 100-continue messages | ||
372 | while code == 100 do | ||
373 | h:receiveheaders() | ||
374 | code, status = h:receivestatusline() | ||
375 | end | ||
376 | headers = h:receiveheaders() | ||
377 | -- at this point we should have a honest reply from the server | ||
378 | -- we can't redirect if we already used the source, so we report the error | ||
379 | if shouldredirect(nreqt, code, headers) and not nreqt.source then | ||
380 | h:close() | ||
381 | return tredirect(reqt, headers.location) | ||
382 | end | ||
383 | -- here we are finally done | ||
384 | if shouldreceivebody(nreqt, code) then | ||
385 | h:receivebody(headers, nreqt.sink, nreqt.step) | ||
386 | end | ||
387 | h:close() | ||
388 | return 1, code, headers, status | ||
389 | end | ||
390 | |||
391 | -- turns an url and a body into a generic request | ||
392 | local function genericform(u, b) | ||
393 | local t = {} | ||
394 | local reqt = { | ||
395 | url = u, | ||
396 | sink = ltn12.sink.table(t), | ||
397 | target = t | ||
398 | } | ||
399 | if b then | ||
400 | reqt.source = ltn12.source.string(b) | ||
401 | reqt.headers = { | ||
402 | ["content-length"] = string.len(b), | ||
403 | ["content-type"] = "application/x-www-form-urlencoded" | ||
404 | } | ||
405 | reqt.method = "POST" | ||
406 | end | ||
407 | return reqt | ||
408 | end | ||
409 | |||
410 | _M.genericform = genericform | ||
411 | |||
412 | local function srequest(u, b) | ||
413 | local reqt = genericform(u, b) | ||
414 | local _, code, headers, status = trequest(reqt) | ||
415 | return table.concat(reqt.target), code, headers, status | ||
416 | end | ||
417 | |||
418 | _M.request = socket.protect(function(reqt, body) | ||
419 | if base.type(reqt) == "string" then return srequest(reqt, body) | ||
420 | else return trequest(reqt) end | ||
421 | end) | ||
422 | |||
423 | _M.schemes = SCHEMES | ||
424 | return _M | ||