aboutsummaryrefslogtreecommitdiff
path: root/src/http.lua
diff options
context:
space:
mode:
authorDiego Nehab <diego@tecgraf.puc-rio.br>2004-06-15 06:24:00 +0000
committerDiego Nehab <diego@tecgraf.puc-rio.br>2004-06-15 06:24:00 +0000
commit58096449c6044b7aade5cd41cfd71c6bec1d273d (patch)
tree1814ffebe89c4c2556d84f97f66db37a7e8b4554 /src/http.lua
parent9ed7f955e5fc69af9bf1794fa2c8cd227981ba24 (diff)
downloadluasocket-58096449c6044b7aade5cd41cfd71c6bec1d273d.tar.gz
luasocket-58096449c6044b7aade5cd41cfd71c6bec1d273d.tar.bz2
luasocket-58096449c6044b7aade5cd41cfd71c6bec1d273d.zip
Manual is almost done. HTTP is missing.
Implemented new distribution scheme. Select is now purely C. HTTP reimplemented seems faster dunno why. LTN12 functions that coroutines fail gracefully.
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