diff options
author | Caleb Maclennan <caleb@alerque.com> | 2023-11-10 09:12:04 +0300 |
---|---|---|
committer | Caleb Maclennan <caleb@alerque.com> | 2023-11-10 09:12:04 +0300 |
commit | 5c4fc93d5f4137bf4c22ddf1a048c907a4a26727 (patch) | |
tree | a9a68e1f6a9c3bfe2b64fa1c3a4098865b7d3b5d /src/http.lua | |
parent | ccef3bc4e2aa6ee5b997a80aabb58f4ff0b0e98f (diff) | |
parent | 43a97b7f0053313b43906371dbdc226271e6c8ab (diff) | |
download | luasocket-hjelmeland-patch-1.tar.gz luasocket-hjelmeland-patch-1.tar.bz2 luasocket-hjelmeland-patch-1.zip |
Merge branch 'master' into hjelmeland-patch-1hjelmeland-patch-1
Diffstat (limited to 'src/http.lua')
-rw-r--r-- | src/http.lua | 131 |
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 | ||
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 | |||
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 | ||
109 | function _M.open(host, port, create) | 122 | function _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 |
120 | end | 133 | end |
@@ -144,10 +157,15 @@ function metat.__index:sendbody(headers, source, step) | |||
144 | end | 157 | end |
145 | 158 | ||
146 | function metat.__index:receivestatusline() | 159 | function 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 | ||
210 | local function adjustheaders(reqt) | 228 | local 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 |
241 | local default = { | 263 | local default = { |
242 | host = "", | 264 | path ="/" |
243 | port = _M.PORT, | 265 | , scheme = "http" |
244 | path ="/", | ||
245 | scheme = "http" | ||
246 | } | 266 | } |
247 | 267 | ||
248 | local function adjustrequest(reqt) | 268 | local 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 |
263 | end | 296 | end |
264 | 297 | ||
265 | local function shouldredirect(reqt, code, headers) | 298 | local 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))) | ||
272 | end | 315 | end |
273 | 316 | ||
274 | local function shouldreceivebody(reqt, code) | 317 | local function shouldreceivebody(reqt, code) |
@@ -282,17 +325,23 @@ end | |||
282 | local trequest, tredirect | 325 | local 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 |
339 | end | 390 | end |
340 | 391 | ||
341 | local function srequest(u, b) | 392 | -- turns an url and a body into a generic request |
393 | local 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 | 409 | end |
410 | |||
411 | _M.genericform = genericform | ||
412 | |||
413 | local 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 | ||
357 | end | 417 | end |
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 |
362 | end) | 422 | end) |
363 | 423 | ||
424 | _M.schemes = SCHEMES | ||
364 | return _M | 425 | return _M |