aboutsummaryrefslogtreecommitdiff
path: root/src/http.lua
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/http.lua131
1 files changed, 96 insertions, 35 deletions
diff --git a/src/http.lua b/src/http.lua
index 45ffa15..fbd5ff6 100644
--- a/src/http.lua
+++ b/src/http.lua
@@ -23,11 +23,24 @@ local _M = socket.http
23----------------------------------------------------------------------------- 23-----------------------------------------------------------------------------
24-- connection timeout in seconds 24-- connection timeout in seconds
25_M.TIMEOUT = 60 25_M.TIMEOUT = 60
26-- default port for document retrieval
27_M.PORT = 80
28-- user agent field sent in request 26-- user agent field sent in request
29_M.USERAGENT = socket._VERSION 27_M.USERAGENT = socket._VERSION
30 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
31----------------------------------------------------------------------------- 44-----------------------------------------------------------------------------
32-- Reads MIME headers from a connection, unfolding where needed 45-- Reads MIME headers from a connection, unfolding where needed
33----------------------------------------------------------------------------- 46-----------------------------------------------------------------------------
@@ -76,7 +89,7 @@ socket.sourcet["http-chunked"] = function(sock, headers)
76 -- was it the last chunk? 89 -- was it the last chunk?
77 if size > 0 then 90 if size > 0 then
78 -- if not, get chunk and skip terminating CRLF 91 -- if not, get chunk and skip terminating CRLF
79 local chunk, err, part = sock:receive(size) 92 local chunk, err, _ = sock:receive(size)
80 if chunk then sock:receive() end 93 if chunk then sock:receive() end
81 return chunk, err 94 return chunk, err
82 else 95 else
@@ -108,13 +121,13 @@ local metat = { __index = {} }
108 121
109function _M.open(host, port, create) 122function _M.open(host, port, create)
110 -- create socket with user connect function, or with default 123 -- create socket with user connect function, or with default
111 local c = socket.try((create or socket.tcp)()) 124 local c = socket.try(create())
112 local h = base.setmetatable({ c = c }, metat) 125 local h = base.setmetatable({ c = c }, metat)
113 -- create finalized try 126 -- create finalized try
114 h.try = socket.newtry(function() h:close() end) 127 h.try = socket.newtry(function() h:close() end)
115 -- set timeout before connecting 128 -- set timeout before connecting
116 h.try(c:settimeout(_M.TIMEOUT)) 129 h.try(c:settimeout(_M.TIMEOUT))
117 h.try(c:connect(host, port or _M.PORT)) 130 h.try(c:connect(host, port))
118 -- here everything worked 131 -- here everything worked
119 return h 132 return h
120end 133end
@@ -144,10 +157,15 @@ function metat.__index:sendbody(headers, source, step)
144end 157end
145 158
146function metat.__index:receivestatusline() 159function metat.__index:receivestatusline()
147 local status = self.try(self.c:receive(5)) 160 local status,ec = self.try(self.c:receive(5))
148 -- identify HTTP/0.9 responses, which do not contain a status line 161 -- identify HTTP/0.9 responses, which do not contain a status line
149 -- this is just a heuristic, but is what the RFC recommends 162 -- this is just a heuristic, but is what the RFC recommends
150 if status ~= "HTTP/" then return nil, status end 163 if status ~= "HTTP/" then
164 if ec == "timeout" then
165 return 408
166 end
167 return nil, status
168 end
151 -- otherwise proceed reading a status line 169 -- otherwise proceed reading a status line
152 status = self.try(self.c:receive("*l", status)) 170 status = self.try(self.c:receive("*l", status))
153 local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) 171 local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)"))
@@ -209,7 +227,10 @@ end
209 227
210local function adjustheaders(reqt) 228local function adjustheaders(reqt)
211 -- default headers 229 -- default headers
212 local host = string.gsub(reqt.authority, "^.-@", "") 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
213 local lower = { 234 local lower = {
214 ["user-agent"] = _M.USERAGENT, 235 ["user-agent"] = _M.USERAGENT,
215 ["host"] = host, 236 ["host"] = host,
@@ -218,15 +239,16 @@ local function adjustheaders(reqt)
218 } 239 }
219 -- if we have authentication information, pass it along 240 -- if we have authentication information, pass it along
220 if reqt.user and reqt.password then 241 if reqt.user and reqt.password then
221 lower["authorization"] = 242 lower["authorization"] =
222 "Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password)) 243 "Basic " .. (mime.b64(reqt.user .. ":" ..
244 url.unescape(reqt.password)))
223 end 245 end
224 -- if we have proxy authentication information, pass it along 246 -- if we have proxy authentication information, pass it along
225 local proxy = reqt.proxy or _M.PROXY 247 local proxy = reqt.proxy or _M.PROXY
226 if proxy then 248 if proxy then
227 proxy = url.parse(proxy) 249 proxy = url.parse(proxy)
228 if proxy.user and proxy.password then 250 if proxy.user and proxy.password then
229 lower["proxy-authorization"] = 251 lower["proxy-authorization"] =
230 "Basic " .. (mime.b64(proxy.user .. ":" .. proxy.password)) 252 "Basic " .. (mime.b64(proxy.user .. ":" .. proxy.password))
231 end 253 end
232 end 254 end
@@ -239,10 +261,8 @@ end
239 261
240-- default url parts 262-- default url parts
241local default = { 263local default = {
242 host = "", 264 path ="/"
243 port = _M.PORT, 265 , scheme = "http"
244 path ="/",
245 scheme = "http"
246} 266}
247 267
248local function adjustrequest(reqt) 268local function adjustrequest(reqt)
@@ -250,25 +270,48 @@ local function adjustrequest(reqt)
250 local nreqt = reqt.url and url.parse(reqt.url, default) or {} 270 local nreqt = reqt.url and url.parse(reqt.url, default) or {}
251 -- explicit components override url 271 -- explicit components override url
252 for i,v in base.pairs(reqt) do nreqt[i] = v end 272 for i,v in base.pairs(reqt) do nreqt[i] = v end
253 if nreqt.port == "" then nreqt.port = 80 end 273 -- default to scheme particulars
254 socket.try(nreqt.host and nreqt.host ~= "", 274 local schemedefs, host, port, method
255 "invalid host '" .. base.tostring(nreqt.host) .. "'") 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
256 -- compute uri if user hasn't overriden 282 -- compute uri if user hasn't overriden
257 nreqt.uri = reqt.uri or adjusturi(nreqt) 283 nreqt.uri = reqt.uri or adjusturi(nreqt)
258 -- adjust headers in request 284 -- adjust headers in request
259 nreqt.headers = adjustheaders(nreqt) 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
260 -- ajust host and port if there is a proxy 293 -- ajust host and port if there is a proxy
261 nreqt.host, nreqt.port = adjustproxy(nreqt) 294 nreqt.host, nreqt.port = adjustproxy(nreqt)
262 return nreqt 295 return nreqt
263end 296end
264 297
265local function shouldredirect(reqt, code, headers) 298local function shouldredirect(reqt, code, headers)
266 return headers.location and 299 local location = headers.location
267 string.gsub(headers.location, "%s", "") ~= "" and 300 if not location then return false end
268 (reqt.redirect ~= false) and 301 location = string.gsub(location, "%s", "")
302 if location == "" then return false end
303 -- the RFC says the redirect URL may be relative
304 location = url.absolute(reqt.url, location)
305 local scheme = url.parse(location).scheme
306 if scheme and (not SCHEMES[scheme]) then return false end
307 -- avoid https downgrades
308 if ('https' == reqt.scheme) and ('https' ~= scheme) then return false end
309 return (reqt.redirect ~= false) and
269 (code == 301 or code == 302 or code == 303 or code == 307) and 310 (code == 301 or code == 302 or code == 303 or code == 307) and
270 (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") 311 (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD")
271 and (not reqt.nredirects or reqt.nredirects < 5) 312 and ((false == reqt.maxredirects)
313 or ((reqt.nredirects or 0)
314 < (reqt.maxredirects or 5)))
272end 315end
273 316
274local function shouldreceivebody(reqt, code) 317local function shouldreceivebody(reqt, code)
@@ -282,17 +325,23 @@ end
282local trequest, tredirect 325local trequest, tredirect
283 326
284--[[local]] function tredirect(reqt, location) 327--[[local]] function tredirect(reqt, location)
328 -- the RFC says the redirect URL may be relative
329 local newurl = url.absolute(reqt.url, location)
330 -- if switching schemes, reset port and create function
331 if url.parse(newurl).scheme ~= reqt.scheme then
332 reqt.port = nil
333 reqt.create = nil end
334 -- make new request
285 local result, code, headers, status = trequest { 335 local result, code, headers, status = trequest {
286 -- the RFC says the redirect URL has to be absolute, but some 336 url = newurl,
287 -- servers do not respect that
288 url = url.absolute(reqt.url, location),
289 source = reqt.source, 337 source = reqt.source,
290 sink = reqt.sink, 338 sink = reqt.sink,
291 headers = reqt.headers, 339 headers = reqt.headers,
292 proxy = reqt.proxy, 340 proxy = reqt.proxy,
341 maxredirects = reqt.maxredirects,
293 nredirects = (reqt.nredirects or 0) + 1, 342 nredirects = (reqt.nredirects or 0) + 1,
294 create = reqt.create 343 create = reqt.create
295 } 344 }
296 -- pass location header back as a hint we redirected 345 -- pass location header back as a hint we redirected
297 headers = headers or {} 346 headers = headers or {}
298 headers.location = headers.location or location 347 headers.location = headers.location or location
@@ -309,23 +358,25 @@ end
309 h:sendheaders(nreqt.headers) 358 h:sendheaders(nreqt.headers)
310 -- if there is a body, send it 359 -- if there is a body, send it
311 if nreqt.source then 360 if nreqt.source then
312 h:sendbody(nreqt.headers, nreqt.source, nreqt.step) 361 h:sendbody(nreqt.headers, nreqt.source, nreqt.step)
313 end 362 end
314 local code, status = h:receivestatusline() 363 local code, status = h:receivestatusline()
315 -- if it is an HTTP/0.9 server, simply get the body and we are done 364 -- if it is an HTTP/0.9 server, simply get the body and we are done
316 if not code then 365 if not code then
317 h:receive09body(status, nreqt.sink, nreqt.step) 366 h:receive09body(status, nreqt.sink, nreqt.step)
318 return 1, 200 367 return 1, 200
368 elseif code == 408 then
369 return 1, code
319 end 370 end
320 local headers 371 local headers
321 -- ignore any 100-continue messages 372 -- ignore any 100-continue messages
322 while code == 100 do 373 while code == 100 do
323 headers = h:receiveheaders() 374 h:receiveheaders()
324 code, status = h:receivestatusline() 375 code, status = h:receivestatusline()
325 end 376 end
326 headers = h:receiveheaders() 377 headers = h:receiveheaders()
327 -- at this point we should have a honest reply from the server 378 -- at this point we should have a honest reply from the server
328 -- we can't redirect if we already used the source, so we report the error 379 -- we can't redirect if we already used the source, so we report the error
329 if shouldredirect(nreqt, code, headers) and not nreqt.source then 380 if shouldredirect(nreqt, code, headers) and not nreqt.source then
330 h:close() 381 h:close()
331 return tredirect(reqt, headers.location) 382 return tredirect(reqt, headers.location)
@@ -338,11 +389,13 @@ end
338 return 1, code, headers, status 389 return 1, code, headers, status
339end 390end
340 391
341local function srequest(u, b) 392-- turns an url and a body into a generic request
393local function genericform(u, b)
342 local t = {} 394 local t = {}
343 local reqt = { 395 local reqt = {
344 url = u, 396 url = u,
345 sink = ltn12.sink.table(t) 397 sink = ltn12.sink.table(t),
398 target = t
346 } 399 }
347 if b then 400 if b then
348 reqt.source = ltn12.source.string(b) 401 reqt.source = ltn12.source.string(b)
@@ -352,8 +405,15 @@ local function srequest(u, b)
352 } 405 }
353 reqt.method = "POST" 406 reqt.method = "POST"
354 end 407 end
355 local code, headers, status = socket.skip(1, trequest(reqt)) 408 return reqt
356 return table.concat(t), code, headers, status 409end
410
411_M.genericform = genericform
412
413local function srequest(u, b)
414 local reqt = genericform(u, b)
415 local _, code, headers, status = trequest(reqt)
416 return table.concat(reqt.target), code, headers, status
357end 417end
358 418
359_M.request = socket.protect(function(reqt, body) 419_M.request = socket.protect(function(reqt, body)
@@ -361,4 +421,5 @@ _M.request = socket.protect(function(reqt, body)
361 else return trequest(reqt) end 421 else return trequest(reqt) end
362end) 422end)
363 423
424_M.schemes = SCHEMES
364return _M 425return _M