aboutsummaryrefslogtreecommitdiff
path: root/src/http.lua
diff options
context:
space:
mode:
Diffstat (limited to 'src/http.lua')
-rw-r--r--src/http.lua351
1 files changed, 147 insertions, 204 deletions
diff --git a/src/http.lua b/src/http.lua
index ebe6b54..129b562 100644
--- a/src/http.lua
+++ b/src/http.lua
@@ -7,7 +7,7 @@
7----------------------------------------------------------------------------- 7-----------------------------------------------------------------------------
8 8
9----------------------------------------------------------------------------- 9-----------------------------------------------------------------------------
10-- Load other required modules 10-- Load required modules
11------------------------------------------------------------------------------- 11-------------------------------------------------------------------------------
12local socket = require("socket") 12local socket = require("socket")
13local ltn12 = require("ltn12") 13local ltn12 = require("ltn12")
@@ -17,42 +17,68 @@ local url = require("url")
17----------------------------------------------------------------------------- 17-----------------------------------------------------------------------------
18-- Setup namespace 18-- Setup namespace
19------------------------------------------------------------------------------- 19-------------------------------------------------------------------------------
20http = {} 20_LOADED["http"] = getfenv(1)
21-- make all module globals fall into namespace
22setmetatable(http, { __index = _G })
23setfenv(1, http)
24 21
25----------------------------------------------------------------------------- 22-----------------------------------------------------------------------------
26-- Program constants 23-- Program constants
27----------------------------------------------------------------------------- 24-----------------------------------------------------------------------------
28-- connection timeout in seconds 25-- connection timeout in seconds
29TIMEOUT = 60 26TIMEOUT = 4
30-- default port for document retrieval 27-- default port for document retrieval
31PORT = 80 28PORT = 80
32-- user agent field sent in request 29-- user agent field sent in request
33USERAGENT = socket.version 30USERAGENT = socket.VERSION
34-- block size used in transfers 31-- block size used in transfers
35BLOCKSIZE = 2048 32BLOCKSIZE = 2048
36 33
37----------------------------------------------------------------------------- 34-----------------------------------------------------------------------------
38-- Function return value selectors 35-- Low level HTTP API
39----------------------------------------------------------------------------- 36-----------------------------------------------------------------------------
40local function second(a, b) 37local metat = { __index = {} }
41 return b 38
39function open(host, port)
40 local con = socket.try(socket.tcp())
41 socket.try(con:settimeout(TIMEOUT))
42 port = port or PORT
43 socket.try(con:connect(host, port))
44 return setmetatable({ con = con }, metat)
45end
46
47function metat.__index:sendrequestline(method, uri)
48 local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri)
49 return socket.try(self.con:send(reqline))
42end 50end
43 51
44local function third(a, b, c) 52function metat.__index:sendheaders(headers)
45 return c 53 for i, v in pairs(headers) do
54 socket.try(self.con:send(i .. ": " .. v .. "\r\n"))
55 end
56 -- mark end of request headers
57 socket.try(self.con:send("\r\n"))
58 return 1
46end 59end
47 60
48local function receive_headers(reqt, respt, tmp) 61function metat.__index:sendbody(headers, source, step)
49 local sock = tmp.sock 62 source = source or ltn12.source.empty()
63 step = step or ltn12.pump.step
64 -- if we don't know the size in advance, send chunked and hope for the best
65 local mode
66 if headers["content-length"] then mode = "keep-open"
67 else mode = "http-chunked" end
68 return socket.try(ltn12.pump.all(source, socket.sink(mode, self.con), step))
69end
70
71function metat.__index:receivestatusline()
72 local status = socket.try(self.con:receive())
73 local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)"))
74 return socket.try(tonumber(code), status)
75end
76
77function metat.__index:receiveheaders()
50 local line, name, value 78 local line, name, value
51 local headers = {} 79 local headers = {}
52 -- store results
53 respt.headers = headers
54 -- get first line 80 -- get first line
55 line = socket.try(sock:receive()) 81 line = socket.try(self.con:receive())
56 -- headers go until a blank line is found 82 -- headers go until a blank line is found
57 while line ~= "" do 83 while line ~= "" do
58 -- get field-name and value 84 -- get field-name and value
@@ -60,189 +86,137 @@ local function receive_headers(reqt, respt, tmp)
60 socket.try(name and value, "malformed reponse headers") 86 socket.try(name and value, "malformed reponse headers")
61 name = string.lower(name) 87 name = string.lower(name)
62 -- get next line (value might be folded) 88 -- get next line (value might be folded)
63 line = socket.try(sock:receive()) 89 line = socket.try(self.con:receive())
64 -- unfold any folded values 90 -- unfold any folded values
65 while string.find(line, "^%s") do 91 while string.find(line, "^%s") do
66 value = value .. line 92 value = value .. line
67 line = socket.try(sock:receive()) 93 line = socket.try(self.con:receive())
68 end 94 end
69 -- save pair in table 95 -- save pair in table
70 if headers[name] then headers[name] = headers[name] .. ", " .. value 96 if headers[name] then headers[name] = headers[name] .. ", " .. value
71 else headers[name] = value end 97 else headers[name] = value end
72 end 98 end
99 return headers
73end 100end
74 101
75local function receive_body(reqt, respt, tmp) 102function metat.__index:receivebody(headers, sink, step)
76 local sink = reqt.sink or ltn12.sink.null() 103 sink = sink or ltn12.sink.null()
77 local step = reqt.step or ltn12.pump.step 104 step = step or ltn12.pump.step
78 local source 105 local length = tonumber(headers["content-length"])
79 local te = respt.headers["transfer-encoding"] 106 local TE = headers["transfer-encoding"]
80 if te and te ~= "identity" then 107 local mode
81 -- get by chunked transfer-coding of message body 108 if TE and TE ~= "identity" then mode = "http-chunked"
82 source = socket.source("http-chunked", tmp.sock) 109 elseif tonumber(headers["content-length"]) then mode = "by-length"
83 elseif tonumber(respt.headers["content-length"]) then 110 else mode = "default" end
84 -- get by content-length 111 return socket.try(ltn12.pump.all(socket.source(mode, self.con, length),
85 local length = tonumber(respt.headers["content-length"]) 112 sink, step))
86 source = socket.source("by-length", tmp.sock, length)
87 else
88 -- get it all until connection closes
89 source = socket.source(tmp.sock)
90 end
91 socket.try(ltn12.pump.all(source, sink, step))
92end 113end
93 114
94local function send_headers(sock, headers) 115function metat.__index:close()
95 -- send request headers 116 return self.con:close()
96 for i, v in pairs(headers) do
97 socket.try(sock:send(i .. ": " .. v .. "\r\n"))
98 end
99 -- mark end of request headers
100 socket.try(sock:send("\r\n"))
101end 117end
102 118
103local function should_receive_body(reqt, respt, tmp) 119-----------------------------------------------------------------------------
104 if reqt.method == "HEAD" then return nil end 120-- High level HTTP API
105 if respt.code == 204 or respt.code == 304 then return nil end 121-----------------------------------------------------------------------------
106 if respt.code >= 100 and respt.code < 200 then return nil end 122local function uri(reqt)
107 return 1 123 local u = reqt
108end 124 if not reqt.proxy and not PROXY then
109 125 u = {
110local function receive_status(reqt, respt, tmp) 126 path = reqt.path,
111 local status = socket.try(tmp.sock:receive()) 127 params = reqt.params,
112 local code = third(string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) 128 query = reqt.query,
113 -- store results 129 fragment = reqt.fragment
114 respt.code, respt.status = tonumber(code), status
115end
116
117local function request_uri(reqt, respt, tmp)
118 local u = tmp.parsed
119 if not reqt.proxy then
120 local parsed = tmp.parsed
121 u = {
122 path = parsed.path,
123 params = parsed.params,
124 query = parsed.query,
125 fragment = parsed.fragment
126 } 130 }
127 end 131 end
128 return url.build(u) 132 return url.build(u)
129end 133end
130 134
131local function send_request(reqt, respt, tmp) 135local function adjustheaders(headers, host)
132 local uri = request_uri(reqt, respt, tmp)
133 local headers = tmp.headers
134 local step = reqt.step or ltn12.pump.step
135 -- send request line
136 socket.try(tmp.sock:send((reqt.method or "GET")
137 .. " " .. uri .. " HTTP/1.1\r\n"))
138 if reqt.source and not headers["content-length"] then
139 headers["transfer-encoding"] = "chunked"
140 end
141 send_headers(tmp.sock, headers)
142 -- send request message body, if any
143 if not reqt.source then return end
144 if headers["content-length"] then
145 socket.try(ltn12.pump.all(reqt.source,
146 socket.sink(tmp.sock), step))
147 else
148 socket.try(ltn12.pump.all(reqt.source,
149 socket.sink("http-chunked", tmp.sock), step))
150 end
151end
152
153local function open(reqt, respt, tmp)
154 local proxy = reqt.proxy or PROXY
155 local host, port
156 if proxy then
157 local pproxy = url.parse(proxy)
158 socket.try(pproxy.port and pproxy.host, "invalid proxy")
159 host, port = pproxy.host, pproxy.port
160 else
161 host, port = tmp.parsed.host, tmp.parsed.port
162 end
163 -- store results
164 tmp.sock = socket.try(socket.tcp())
165 socket.try(tmp.sock:settimeout(reqt.timeout or TIMEOUT))
166 socket.try(tmp.sock:connect(host, port))
167end
168
169local function adjust_headers(reqt, respt, tmp)
170 local lower = {} 136 local lower = {}
171 -- override with user values 137 -- override with user values
172 for i,v in (reqt.headers or lower) do 138 for i,v in (headers or lower) do
173 lower[string.lower(i)] = v 139 lower[string.lower(i)] = v
174 end 140 end
175 lower["user-agent"] = lower["user-agent"] or USERAGENT 141 lower["user-agent"] = lower["user-agent"] or USERAGENT
176 -- these cannot be overriden 142 -- these cannot be overriden
177 lower["host"] = tmp.parsed.host 143 lower["host"] = host
178 lower["connection"] = "close" 144 return lower
179 -- store results
180 tmp.headers = lower
181end 145end
182 146
183local function parse_url(reqt, respt, tmp) 147local function adjustrequest(reqt)
184 -- parse url with default fields 148 -- parse url with default fields
185 local parsed = url.parse(reqt.url, { 149 local parsed = url.parse(reqt.url, {
186 host = "", 150 host = "",
187 port = PORT, 151 port = PORT,
188 path ="/", 152 path ="/",
189 scheme = "http" 153 scheme = "http"
190 }) 154 })
191 -- scheme has to be http 155 -- explicit info in reqt overrides that given by the URL
192 socket.try(parsed.scheme == "http", 156 for i,v in reqt do parsed[i] = v end
193 string.format("unknown scheme '%s'", parsed.scheme)) 157 -- compute uri if user hasn't overriden
194 -- explicit authentication info overrides that given by the URL 158 parsed.uri = parsed.uri or uri(parsed)
195 parsed.user = reqt.user or parsed.user 159 -- adjust headers in request
196 parsed.password = reqt.password or parsed.password 160 parsed.headers = adjustheaders(parsed.headers, parsed.host)
197 -- store results 161 return parsed
198 tmp.parsed = parsed
199end 162end
200 163
201-- forward declaration 164local function shouldredirect(reqt, respt)
202local request_p 165 return (reqt.redirect ~= false) and
166 (respt.code == 301 or respt.code == 302) and
167 (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD")
168 and (not reqt.nredirects or reqt.nredirects < 5)
169end
203 170
204local function should_authorize(reqt, respt, tmp) 171local function shouldauthorize(reqt, respt)
205 -- if there has been an authorization attempt, it must have failed 172 -- if there has been an authorization attempt, it must have failed
206 if reqt.headers and reqt.headers["authorization"] then return nil end 173 if reqt.headers and reqt.headers["authorization"] then return nil end
207 -- if last attempt didn't fail due to lack of authentication, 174 -- if last attempt didn't fail due to lack of authentication,
208 -- or we don't have authorization information, we can't retry 175 -- or we don't have authorization information, we can't retry
209 return respt.code == 401 and tmp.parsed.user and tmp.parsed.password 176 return respt.code == 401 and reqt.user and reqt.password
210end 177end
211 178
212local function clone(headers) 179local function shouldreceivebody(reqt, respt)
213 if not headers then return nil end 180 if reqt.method == "HEAD" then return nil end
214 local copy = {} 181 local code = respt.code
215 for i,v in pairs(headers) do 182 if code == 204 or code == 304 then return nil end
216 copy[i] = v 183 if code >= 100 and code < 200 then return nil end
217 end 184 return 1
218 return copy
219end 185end
220 186
221local function authorize(reqt, respt, tmp) 187local requestp, authorizep, redirectp
222 local headers = clone(reqt.headers) or {} 188
223 headers["authorization"] = "Basic " .. 189function requestp(reqt)
224 (mime.b64(tmp.parsed.user .. ":" .. tmp.parsed.password)) 190 local reqt = adjustrequest(reqt)
225 local autht = { 191 local respt = {}
226 method = reqt.method, 192 local con = open(reqt.host, reqt.port)
227 url = reqt.url, 193 con:sendrequestline(reqt.method, reqt.uri)
228 source = reqt.source, 194 con:sendheaders(reqt.headers)
229 sink = reqt.sink, 195 con:sendbody(reqt.headers, reqt.source, reqt.step)
230 headers = headers, 196 respt.code, respt.status = con:receivestatusline()
231 timeout = reqt.timeout, 197 respt.headers = con:receiveheaders()
232 proxy = reqt.proxy, 198 if shouldredirect(reqt, respt) then
233 } 199 con:close()
234 request_p(autht, respt, tmp) 200 return redirectp(reqt, respt)
201 elseif shouldauthorize(reqt, respt) then
202 con:close()
203 return authorizep(reqt, respt)
204 elseif shouldreceivebody(reqt, respt) then
205 con:receivebody(respt.headers, reqt.sink, reqt.step)
206 end
207 con:close()
208 return respt
235end 209end
236 210
237local function should_redirect(reqt, respt, tmp) 211function authorizep(reqt, respt)
238 return (reqt.redirect ~= false) and 212 local auth = "Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password))
239 (respt.code == 301 or respt.code == 302) and 213 reqt.headers["authorization"] = auth
240 (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") 214 return requestp(reqt)
241 and (not tmp.nredirects or tmp.nredirects < 5)
242end 215end
243 216
244local function redirect(reqt, respt, tmp) 217function redirectp(reqt, respt)
245 tmp.nredirects = (tmp.nredirects or 0) + 1 218 -- we create a new table to get rid of anything we don't
219 -- absolutely need, including authentication info
246 local redirt = { 220 local redirt = {
247 method = reqt.method, 221 method = reqt.method,
248 -- the RFC says the redirect URL has to be absolute, but some 222 -- the RFC says the redirect URL has to be absolute, but some
@@ -251,69 +225,38 @@ local function redirect(reqt, respt, tmp)
251 source = reqt.source, 225 source = reqt.source,
252 sink = reqt.sink, 226 sink = reqt.sink,
253 headers = reqt.headers, 227 headers = reqt.headers,
254 timeout = reqt.timeout, 228 proxy = reqt.proxy,
255 proxy = reqt.proxy 229 nredirects = (reqt.nredirects or 0) + 1
256 } 230 }
257 request_p(redirt, respt, tmp) 231 respt = requestp(redirt)
258 -- we pass the location header as a clue we redirected 232 -- we pass the location header as a clue we redirected
259 if respt.headers then respt.headers.location = redirt.url end 233 if respt.headers then respt.headers.location = redirt.url end
260end
261
262local function skip_continue(reqt, respt, tmp)
263 if respt.code == 100 then
264 receive_status(reqt, respt, tmp)
265 end
266end
267
268-- execute a request of through an exception
269function request_p(reqt, respt, tmp)
270 parse_url(reqt, respt, tmp)
271 adjust_headers(reqt, respt, tmp)
272 open(reqt, respt, tmp)
273 send_request(reqt, respt, tmp)
274 receive_status(reqt, respt, tmp)
275 skip_continue(reqt, respt, tmp)
276 receive_headers(reqt, respt, tmp)
277 if should_redirect(reqt, respt, tmp) then
278 tmp.sock:close()
279 redirect(reqt, respt, tmp)
280 elseif should_authorize(reqt, respt, tmp) then
281 tmp.sock:close()
282 authorize(reqt, respt, tmp)
283 elseif should_receive_body(reqt, respt, tmp) then
284 receive_body(reqt, respt, tmp)
285 end
286end
287
288function request(reqt)
289 local respt, tmp = {}, {}
290 local s, e = pcall(request_p, reqt, respt, tmp)
291 if not s then respt.error = e end
292 if tmp.sock then tmp.sock:close() end
293 return respt 234 return respt
294end 235end
295 236
296function get(u) 237request = socket.protect(requestp)
238
239get = socket.protect(function(u)
297 local t = {} 240 local t = {}
298 respt = request { 241 local respt = requestp {
299 url = u, 242 url = u,
300 sink = ltn12.sink.table(t) 243 sink = ltn12.sink.table(t)
301 } 244 }
302 return (table.getn(t) > 0 or nil) and table.concat(t), respt.headers, 245 return (table.getn(t) > 0 or nil) and table.concat(t), respt.headers,
303 respt.code, respt.error 246 respt.code
304end 247end)
305 248
306function post(u, body) 249post = socket.protect(function(u, body)
307 local t = {} 250 local t = {}
308 respt = request { 251 local respt = requestp {
309 url = u, 252 url = u,
310 method = "POST", 253 method = "POST",
311 source = ltn12.source.string(body), 254 source = ltn12.source.string(body),
312 sink = ltn12.sink.table(t), 255 sink = ltn12.sink.table(t),
313 headers = { ["content-length"] = string.len(body) } 256 headers = { ["content-length"] = string.len(body) }
314 } 257 }
315 return (table.getn(t) > 0 or nil) and table.concat(t), 258 return (table.getn(t) > 0 or nil) and table.concat(t),
316 respt.headers, respt.code, respt.error 259 respt.headers, respt.code
317end 260end)
318 261
319return http 262return http