aboutsummaryrefslogtreecommitdiff
path: root/vendor/luasocket/src/http.lua
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/luasocket/src/http.lua')
-rw-r--r--vendor/luasocket/src/http.lua424
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-------------------------------------------------------------------------------
10local socket = require("socket")
11local url = require("socket.url")
12local ltn12 = require("ltn12")
13local mime = require("mime")
14local string = require("string")
15local headers = require("socket.headers")
16local base = _G
17local table = require("table")
18socket.http = {}
19local _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
30local 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-----------------------------------------------------------------------------
47local 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
73end
74
75-----------------------------------------------------------------------------
76-- Extra sources and sinks
77-----------------------------------------------------------------------------
78socket.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 })
102end
103
104socket.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 })
115end
116
117-----------------------------------------------------------------------------
118-- Low level HTTP API
119-----------------------------------------------------------------------------
120local metat = { __index = {} }
121
122function _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
133end
134
135function 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))
138end
139
140function 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
148end
149
150function 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))
157end
158
159function 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)
173end
174
175function metat.__index:receiveheaders()
176 return self.try(receiveheaders(self.c))
177end
178
179function 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))
189end
190
191function 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))
195end
196
197function metat.__index:close()
198 return self.c:close()
199end
200
201-----------------------------------------------------------------------------
202-- High level HTTP API
203-----------------------------------------------------------------------------
204local 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)
216end
217
218local 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
226end
227
228local 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
260end
261
262-- default url parts
263local default = {
264 path ="/"
265 , scheme = "http"
266}
267
268local 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
296end
297
298local 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)))
313end
314
315local 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
320end
321
322-- forward declarations
323local 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
348end
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
389end
390
391-- turns an url and a body into a generic request
392local 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
408end
409
410_M.genericform = genericform
411
412local 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
416end
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
421end)
422
423_M.schemes = SCHEMES
424return _M