Mercurial > luasocket
comparison src/http.lua @ 0:4b915342e2a8
LuaSocket 2.0.2 + CMake build description.
author | Eric Wing <ewing . public |-at-| gmail . com> |
---|---|
date | Tue, 26 Aug 2008 18:40:01 -0700 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4b915342e2a8 |
---|---|
1 ----------------------------------------------------------------------------- | |
2 -- HTTP/1.1 client support for the Lua language. | |
3 -- LuaSocket toolkit. | |
4 -- Author: Diego Nehab | |
5 -- RCS ID: $Id: http.lua,v 1.71 2007/10/13 23:55:20 diego Exp $ | |
6 ----------------------------------------------------------------------------- | |
7 | |
8 ----------------------------------------------------------------------------- | |
9 -- Declare module and import dependencies | |
10 ------------------------------------------------------------------------------- | |
11 local socket = require("socket") | |
12 local url = require("socket.url") | |
13 local ltn12 = require("ltn12") | |
14 local mime = require("mime") | |
15 local string = require("string") | |
16 local base = _G | |
17 local table = require("table") | |
18 module("socket.http") | |
19 | |
20 ----------------------------------------------------------------------------- | |
21 -- Program constants | |
22 ----------------------------------------------------------------------------- | |
23 -- connection timeout in seconds | |
24 TIMEOUT = 60 | |
25 -- default port for document retrieval | |
26 PORT = 80 | |
27 -- user agent field sent in request | |
28 USERAGENT = socket._VERSION | |
29 | |
30 ----------------------------------------------------------------------------- | |
31 -- Reads MIME headers from a connection, unfolding where needed | |
32 ----------------------------------------------------------------------------- | |
33 local function receiveheaders(sock, headers) | |
34 local line, name, value, err | |
35 headers = headers or {} | |
36 -- get first line | |
37 line, err = sock:receive() | |
38 if err then return nil, err end | |
39 -- headers go until a blank line is found | |
40 while line ~= "" do | |
41 -- get field-name and value | |
42 name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) | |
43 if not (name and value) then return nil, "malformed reponse headers" end | |
44 name = string.lower(name) | |
45 -- get next line (value might be folded) | |
46 line, err = sock:receive() | |
47 if err then return nil, err end | |
48 -- unfold any folded values | |
49 while string.find(line, "^%s") do | |
50 value = value .. line | |
51 line = sock:receive() | |
52 if err then return nil, err end | |
53 end | |
54 -- save pair in table | |
55 if headers[name] then headers[name] = headers[name] .. ", " .. value | |
56 else headers[name] = value end | |
57 end | |
58 return headers | |
59 end | |
60 | |
61 ----------------------------------------------------------------------------- | |
62 -- Extra sources and sinks | |
63 ----------------------------------------------------------------------------- | |
64 socket.sourcet["http-chunked"] = function(sock, headers) | |
65 return base.setmetatable({ | |
66 getfd = function() return sock:getfd() end, | |
67 dirty = function() return sock:dirty() end | |
68 }, { | |
69 __call = function() | |
70 -- get chunk size, skip extention | |
71 local line, err = sock:receive() | |
72 if err then return nil, err end | |
73 local size = base.tonumber(string.gsub(line, ";.*", ""), 16) | |
74 if not size then return nil, "invalid chunk size" end | |
75 -- was it the last chunk? | |
76 if size > 0 then | |
77 -- if not, get chunk and skip terminating CRLF | |
78 local chunk, err, part = sock:receive(size) | |
79 if chunk then sock:receive() end | |
80 return chunk, err | |
81 else | |
82 -- if it was, read trailers into headers table | |
83 headers, err = receiveheaders(sock, headers) | |
84 if not headers then return nil, err end | |
85 end | |
86 end | |
87 }) | |
88 end | |
89 | |
90 socket.sinkt["http-chunked"] = function(sock) | |
91 return base.setmetatable({ | |
92 getfd = function() return sock:getfd() end, | |
93 dirty = function() return sock:dirty() end | |
94 }, { | |
95 __call = function(self, chunk, err) | |
96 if not chunk then return sock:send("0\r\n\r\n") end | |
97 local size = string.format("%X\r\n", string.len(chunk)) | |
98 return sock:send(size .. chunk .. "\r\n") | |
99 end | |
100 }) | |
101 end | |
102 | |
103 ----------------------------------------------------------------------------- | |
104 -- Low level HTTP API | |
105 ----------------------------------------------------------------------------- | |
106 local metat = { __index = {} } | |
107 | |
108 function open(host, port, create) | |
109 -- create socket with user connect function, or with default | |
110 local c = socket.try((create or socket.tcp)()) | |
111 local h = base.setmetatable({ c = c }, metat) | |
112 -- create finalized try | |
113 h.try = socket.newtry(function() h:close() end) | |
114 -- set timeout before connecting | |
115 h.try(c:settimeout(TIMEOUT)) | |
116 h.try(c:connect(host, port or PORT)) | |
117 -- here everything worked | |
118 return h | |
119 end | |
120 | |
121 function metat.__index:sendrequestline(method, uri) | |
122 local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) | |
123 return self.try(self.c:send(reqline)) | |
124 end | |
125 | |
126 function metat.__index:sendheaders(headers) | |
127 local h = "\r\n" | |
128 for i, v in base.pairs(headers) do | |
129 h = i .. ": " .. v .. "\r\n" .. h | |
130 end | |
131 self.try(self.c:send(h)) | |
132 return 1 | |
133 end | |
134 | |
135 function metat.__index:sendbody(headers, source, step) | |
136 source = source or ltn12.source.empty() | |
137 step = step or ltn12.pump.step | |
138 -- if we don't know the size in advance, send chunked and hope for the best | |
139 local mode = "http-chunked" | |
140 if headers["content-length"] then mode = "keep-open" end | |
141 return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) | |
142 end | |
143 | |
144 function metat.__index:receivestatusline() | |
145 local status = self.try(self.c:receive(5)) | |
146 -- identify HTTP/0.9 responses, which do not contain a status line | |
147 -- this is just a heuristic, but is what the RFC recommends | |
148 if status ~= "HTTP/" then return nil, status end | |
149 -- otherwise proceed reading a status line | |
150 status = self.try(self.c:receive("*l", status)) | |
151 local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) | |
152 return self.try(base.tonumber(code), status) | |
153 end | |
154 | |
155 function metat.__index:receiveheaders() | |
156 return self.try(receiveheaders(self.c)) | |
157 end | |
158 | |
159 function metat.__index:receivebody(headers, sink, step) | |
160 sink = sink or ltn12.sink.null() | |
161 step = step or ltn12.pump.step | |
162 local length = base.tonumber(headers["content-length"]) | |
163 local t = headers["transfer-encoding"] -- shortcut | |
164 local mode = "default" -- connection close | |
165 if t and t ~= "identity" then mode = "http-chunked" | |
166 elseif base.tonumber(headers["content-length"]) then mode = "by-length" end | |
167 return self.try(ltn12.pump.all(socket.source(mode, self.c, length), | |
168 sink, step)) | |
169 end | |
170 | |
171 function metat.__index:receive09body(status, sink, step) | |
172 local source = ltn12.source.rewind(socket.source("until-closed", self.c)) | |
173 source(status) | |
174 return self.try(ltn12.pump.all(source, sink, step)) | |
175 end | |
176 | |
177 function metat.__index:close() | |
178 return self.c:close() | |
179 end | |
180 | |
181 ----------------------------------------------------------------------------- | |
182 -- High level HTTP API | |
183 ----------------------------------------------------------------------------- | |
184 local function adjusturi(reqt) | |
185 local u = reqt | |
186 -- if there is a proxy, we need the full url. otherwise, just a part. | |
187 if not reqt.proxy and not PROXY then | |
188 u = { | |
189 path = socket.try(reqt.path, "invalid path 'nil'"), | |
190 params = reqt.params, | |
191 query = reqt.query, | |
192 fragment = reqt.fragment | |
193 } | |
194 end | |
195 return url.build(u) | |
196 end | |
197 | |
198 local function adjustproxy(reqt) | |
199 local proxy = reqt.proxy or PROXY | |
200 if proxy then | |
201 proxy = url.parse(proxy) | |
202 return proxy.host, proxy.port or 3128 | |
203 else | |
204 return reqt.host, reqt.port | |
205 end | |
206 end | |
207 | |
208 local function adjustheaders(reqt) | |
209 -- default headers | |
210 local lower = { | |
211 ["user-agent"] = USERAGENT, | |
212 ["host"] = reqt.host, | |
213 ["connection"] = "close, TE", | |
214 ["te"] = "trailers" | |
215 } | |
216 -- if we have authentication information, pass it along | |
217 if reqt.user and reqt.password then | |
218 lower["authorization"] = | |
219 "Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password)) | |
220 end | |
221 -- override with user headers | |
222 for i,v in base.pairs(reqt.headers or lower) do | |
223 lower[string.lower(i)] = v | |
224 end | |
225 return lower | |
226 end | |
227 | |
228 -- default url parts | |
229 local default = { | |
230 host = "", | |
231 port = PORT, | |
232 path ="/", | |
233 scheme = "http" | |
234 } | |
235 | |
236 local function adjustrequest(reqt) | |
237 -- parse url if provided | |
238 local nreqt = reqt.url and url.parse(reqt.url, default) or {} | |
239 -- explicit components override url | |
240 for i,v in base.pairs(reqt) do nreqt[i] = v end | |
241 if nreqt.port == "" then nreqt.port = 80 end | |
242 socket.try(nreqt.host and nreqt.host ~= "", | |
243 "invalid host '" .. base.tostring(nreqt.host) .. "'") | |
244 -- compute uri if user hasn't overriden | |
245 nreqt.uri = reqt.uri or adjusturi(nreqt) | |
246 -- ajust host and port if there is a proxy | |
247 nreqt.host, nreqt.port = adjustproxy(nreqt) | |
248 -- adjust headers in request | |
249 nreqt.headers = adjustheaders(nreqt) | |
250 return nreqt | |
251 end | |
252 | |
253 local function shouldredirect(reqt, code, headers) | |
254 return headers.location and | |
255 string.gsub(headers.location, "%s", "") ~= "" and | |
256 (reqt.redirect ~= false) and | |
257 (code == 301 or code == 302) and | |
258 (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") | |
259 and (not reqt.nredirects or reqt.nredirects < 5) | |
260 end | |
261 | |
262 local function shouldreceivebody(reqt, code) | |
263 if reqt.method == "HEAD" then return nil end | |
264 if code == 204 or code == 304 then return nil end | |
265 if code >= 100 and code < 200 then return nil end | |
266 return 1 | |
267 end | |
268 | |
269 -- forward declarations | |
270 local trequest, tredirect | |
271 | |
272 function tredirect(reqt, location) | |
273 local result, code, headers, status = trequest { | |
274 -- the RFC says the redirect URL has to be absolute, but some | |
275 -- servers do not respect that | |
276 url = url.absolute(reqt.url, location), | |
277 source = reqt.source, | |
278 sink = reqt.sink, | |
279 headers = reqt.headers, | |
280 proxy = reqt.proxy, | |
281 nredirects = (reqt.nredirects or 0) + 1, | |
282 create = reqt.create | |
283 } | |
284 -- pass location header back as a hint we redirected | |
285 headers = headers or {} | |
286 headers.location = headers.location or location | |
287 return result, code, headers, status | |
288 end | |
289 | |
290 function trequest(reqt) | |
291 -- we loop until we get what we want, or | |
292 -- until we are sure there is no way to get it | |
293 local nreqt = adjustrequest(reqt) | |
294 local h = open(nreqt.host, nreqt.port, nreqt.create) | |
295 -- send request line and headers | |
296 h:sendrequestline(nreqt.method, nreqt.uri) | |
297 h:sendheaders(nreqt.headers) | |
298 -- if there is a body, send it | |
299 if nreqt.source then | |
300 h:sendbody(nreqt.headers, nreqt.source, nreqt.step) | |
301 end | |
302 local code, status = h:receivestatusline() | |
303 -- if it is an HTTP/0.9 server, simply get the body and we are done | |
304 if not code then | |
305 h:receive09body(status, nreqt.sink, nreqt.step) | |
306 return 1, 200 | |
307 end | |
308 local headers | |
309 -- ignore any 100-continue messages | |
310 while code == 100 do | |
311 headers = h:receiveheaders() | |
312 code, status = h:receivestatusline() | |
313 end | |
314 headers = h:receiveheaders() | |
315 -- at this point we should have a honest reply from the server | |
316 -- we can't redirect if we already used the source, so we report the error | |
317 if shouldredirect(nreqt, code, headers) and not nreqt.source then | |
318 h:close() | |
319 return tredirect(reqt, headers.location) | |
320 end | |
321 -- here we are finally done | |
322 if shouldreceivebody(nreqt, code) then | |
323 h:receivebody(headers, nreqt.sink, nreqt.step) | |
324 end | |
325 h:close() | |
326 return 1, code, headers, status | |
327 end | |
328 | |
329 local function srequest(u, b) | |
330 local t = {} | |
331 local reqt = { | |
332 url = u, | |
333 sink = ltn12.sink.table(t) | |
334 } | |
335 if b then | |
336 reqt.source = ltn12.source.string(b) | |
337 reqt.headers = { | |
338 ["content-length"] = string.len(b), | |
339 ["content-type"] = "application/x-www-form-urlencoded" | |
340 } | |
341 reqt.method = "POST" | |
342 end | |
343 local code, headers, status = socket.skip(1, trequest(reqt)) | |
344 return table.concat(t), code, headers, status | |
345 end | |
346 | |
347 request = socket.protect(function(reqt, body) | |
348 if base.type(reqt) == "string" then return srequest(reqt, body) | |
349 else return trequest(reqt) end | |
350 end) |