diff options
Diffstat (limited to 'src/http.lua')
-rw-r--r-- | src/http.lua | 312 |
1 files changed, 312 insertions, 0 deletions
diff --git a/src/http.lua b/src/http.lua new file mode 100644 index 0000000..8f08725 --- /dev/null +++ b/src/http.lua | |||
@@ -0,0 +1,312 @@ | |||
1 | ----------------------------------------------------------------------------- | ||
2 | -- Simple HTTP/1.1 support for the Lua language using the LuaSocket toolkit. | ||
3 | -- Author: Diego Nehab | ||
4 | -- Date: 26/12/2000 | ||
5 | -- Conforming to: RFC 2068 | ||
6 | ----------------------------------------------------------------------------- | ||
7 | |||
8 | ----------------------------------------------------------------------------- | ||
9 | -- Program constants | ||
10 | ----------------------------------------------------------------------------- | ||
11 | -- connection timeout in seconds | ||
12 | local TIMEOUT = 60 | ||
13 | -- default port for document retrieval | ||
14 | local PORT = 80 | ||
15 | -- user agent field sent in request | ||
16 | local USERAGENT = "LuaSocket/HTTP 1.0" | ||
17 | |||
18 | ----------------------------------------------------------------------------- | ||
19 | -- Tries to get a line from the server or close socket if error | ||
20 | -- sock: socket connected to the server | ||
21 | -- Returns | ||
22 | -- line: line received or nil in case of error | ||
23 | -- err: error message if any | ||
24 | ----------------------------------------------------------------------------- | ||
25 | local try_getline = function(sock) | ||
26 | line, err = sock:receive() | ||
27 | if err then | ||
28 | sock:close() | ||
29 | return nil, err | ||
30 | end | ||
31 | return line | ||
32 | end | ||
33 | |||
34 | ----------------------------------------------------------------------------- | ||
35 | -- Tries to send a line to the server or close socket if error | ||
36 | -- sock: socket connected to the server | ||
37 | -- line: line to send | ||
38 | -- Returns | ||
39 | -- err: error message if any | ||
40 | ----------------------------------------------------------------------------- | ||
41 | local try_sendline = function(sock, line) | ||
42 | err = sock:send(line) | ||
43 | if err then sock:close() end | ||
44 | return err | ||
45 | end | ||
46 | |||
47 | ----------------------------------------------------------------------------- | ||
48 | -- Retrieves status from http reply | ||
49 | -- Input | ||
50 | -- reply: http reply string | ||
51 | -- Returns | ||
52 | -- status: integer with status code | ||
53 | ----------------------------------------------------------------------------- | ||
54 | local get_status = function(reply) | ||
55 | local _,_, status = strfind(reply, " (%d%d%d) ") | ||
56 | return tonumber(status) | ||
57 | end | ||
58 | |||
59 | ----------------------------------------------------------------------------- | ||
60 | -- Receive server reply messages | ||
61 | -- Input | ||
62 | -- sock: server socket | ||
63 | -- Returns | ||
64 | -- status: server reply status code or nil if error | ||
65 | -- reply: full server reply | ||
66 | -- err: error message if any | ||
67 | ----------------------------------------------------------------------------- | ||
68 | local get_reply = function(sock) | ||
69 | local reply, err | ||
70 | reply, err = %try_getline(sock) | ||
71 | if not err then return %get_status(reply), reply | ||
72 | else return nil, nil, err end | ||
73 | end | ||
74 | |||
75 | ----------------------------------------------------------------------------- | ||
76 | -- Receive and parse mime headers | ||
77 | -- Input | ||
78 | -- sock: server socket | ||
79 | -- mime: a table that might already contain mime headers | ||
80 | -- Returns | ||
81 | -- mime: a table with all mime headers in the form | ||
82 | -- {name_1 = "value_1", name_2 = "value_2" ... name_n = "value_n"} | ||
83 | -- all name_i are lowercase | ||
84 | -- nil and error message in case of error | ||
85 | ----------------------------------------------------------------------------- | ||
86 | local get_mime = function(sock, mime) | ||
87 | local line, err | ||
88 | local name, value | ||
89 | -- get first line | ||
90 | line, err = %try_getline(sock) | ||
91 | if err then return nil, err end | ||
92 | -- headers go until a blank line is found | ||
93 | while line ~= "" do | ||
94 | -- get field-name and value | ||
95 | _,_, name, value = strfind(line, "(.-):%s*(.*)") | ||
96 | name = strlower(name) | ||
97 | -- get next line (value might be folded) | ||
98 | line, err = %try_getline(sock) | ||
99 | if err then return nil, err end | ||
100 | -- unfold any folded values | ||
101 | while not err and line ~= "" and (strsub(line, 1, 1) == " ") do | ||
102 | value = value .. line | ||
103 | line, err = %try_getline(sock) | ||
104 | if err then return nil, err end | ||
105 | end | ||
106 | -- save pair in table | ||
107 | if mime[name] then | ||
108 | -- join any multiple field | ||
109 | mime[name] = mime[name] .. ", " .. value | ||
110 | else | ||
111 | -- new field | ||
112 | mime[name] = value | ||
113 | end | ||
114 | end | ||
115 | return mime | ||
116 | end | ||
117 | |||
118 | ----------------------------------------------------------------------------- | ||
119 | -- Receives http body | ||
120 | -- Input | ||
121 | -- sock: server socket | ||
122 | -- mime: initial mime headers | ||
123 | -- Returns | ||
124 | -- body: a string containing the body of the document | ||
125 | -- nil and error message in case of error | ||
126 | -- Obs: | ||
127 | -- mime: headers might be modified by chunked transfer | ||
128 | ----------------------------------------------------------------------------- | ||
129 | local get_body = function(sock, mime) | ||
130 | local body, err | ||
131 | if mime["transfer-encoding"] == "chunked" then | ||
132 | local chunk_size, line | ||
133 | body = "" | ||
134 | repeat | ||
135 | -- get chunk size, skip extention | ||
136 | line, err = %try_getline(sock) | ||
137 | if err then return nil, err end | ||
138 | chunk_size = tonumber(gsub(line, ";.*", ""), 16) | ||
139 | if not chunk_size then | ||
140 | sock:close() | ||
141 | return nil, "invalid chunk size" | ||
142 | end | ||
143 | -- get chunk | ||
144 | line, err = sock:receive(chunk_size) | ||
145 | if err then | ||
146 | sock:close() | ||
147 | return nil, err | ||
148 | end | ||
149 | -- concatenate new chunk | ||
150 | body = body .. line | ||
151 | -- skip blank line | ||
152 | _, err = %try_getline(sock) | ||
153 | if err then return nil, err end | ||
154 | until chunk_size <= 0 | ||
155 | -- store extra mime headers | ||
156 | --_, err = %get_mime(sock, mime) | ||
157 | --if err then return nil, err end | ||
158 | elseif mime["content-length"] then | ||
159 | body, err = sock:receive(tonumber(mime["content-length"])) | ||
160 | if err then | ||
161 | sock:close() | ||
162 | return nil, err | ||
163 | end | ||
164 | else | ||
165 | -- get it all until connection closes! | ||
166 | body, err = sock:receive("*a") | ||
167 | if err then | ||
168 | sock:close() | ||
169 | return nil, err | ||
170 | end | ||
171 | end | ||
172 | -- return whole body | ||
173 | return body | ||
174 | end | ||
175 | |||
176 | ----------------------------------------------------------------------------- | ||
177 | -- Parses a url and returns its scheme, user, password, host, port | ||
178 | -- and path components, according to RFC 1738, Uniform Resource Locators (URL), | ||
179 | -- of December 1994 | ||
180 | -- Input | ||
181 | -- url: unique resource locator desired | ||
182 | -- default: table containing default values to be returned | ||
183 | -- Returns | ||
184 | -- table with the following fields: | ||
185 | -- host: host to connect | ||
186 | -- path: url path | ||
187 | -- port: host port to connect | ||
188 | -- user: user name | ||
189 | -- pass: password | ||
190 | -- scheme: protocol | ||
191 | ----------------------------------------------------------------------------- | ||
192 | local split_url = function(url, default) | ||
193 | -- initialize default parameters | ||
194 | local parsed = default or {} | ||
195 | -- get scheme | ||
196 | url = gsub(url, "^(.+)://", function (s) %parsed.scheme = s end) | ||
197 | -- get user name and password. both can be empty! | ||
198 | -- moreover, password can be ommited | ||
199 | url = gsub(url, "^([^@:/]*)(:?)([^:@/]-)@", function (u, c, p) | ||
200 | %parsed.user = u | ||
201 | -- there can be an empty password, but the ':' has to be there | ||
202 | -- or else there is no password | ||
203 | %parsed.pass = nil -- kill default password | ||
204 | if c == ":" then %parsed.pass = p end | ||
205 | end) | ||
206 | -- get host | ||
207 | url = gsub(url, "^([%w%.%-]+)", function (h) %parsed.host = h end) | ||
208 | -- get port if any | ||
209 | url = gsub(url, "^:(%d+)", function (p) %parsed.port = p end) | ||
210 | -- whatever is left is the path | ||
211 | if url ~= "" then parsed.path = url end | ||
212 | return parsed | ||
213 | end | ||
214 | |||
215 | ----------------------------------------------------------------------------- | ||
216 | -- Sends a GET message through socket | ||
217 | -- Input | ||
218 | -- socket: http connection socket | ||
219 | -- path: path requested | ||
220 | -- mime: mime headers to send in request | ||
221 | -- Returns | ||
222 | -- err: nil in case of success, error message otherwise | ||
223 | ----------------------------------------------------------------------------- | ||
224 | local send_get = function(sock, path, mime) | ||
225 | local err = %try_sendline(sock, "GET " .. path .. " HTTP/1.1\r\n") | ||
226 | if err then return err end | ||
227 | for i, v in mime do | ||
228 | err = %try_sendline(sock, i .. ": " .. v .. "\r\n") | ||
229 | if err then return err end | ||
230 | end | ||
231 | err = %try_sendline(sock, "\r\n") | ||
232 | return err | ||
233 | end | ||
234 | |||
235 | ----------------------------------------------------------------------------- | ||
236 | -- Converts field names to lowercase | ||
237 | -- Input | ||
238 | -- headers: user header fields | ||
239 | -- parsed: parsed url components | ||
240 | -- Returns | ||
241 | -- mime: a table with the same headers, but with lowercase field names | ||
242 | ----------------------------------------------------------------------------- | ||
243 | local fill_headers = function(headers, parsed) | ||
244 | local mime = {} | ||
245 | headers = headers or {} | ||
246 | for i,v in headers do | ||
247 | mime[strlower(i)] = v | ||
248 | end | ||
249 | mime["connection"] = "close" | ||
250 | mime["host"] = parsed.host | ||
251 | mime["user-agent"] = %USERAGENT | ||
252 | if parsed.user and parsed.pass then -- Basic Authentication | ||
253 | mime["authorization"] = "Basic ".. | ||
254 | base64(parsed.user .. ":" .. parsed.pass) | ||
255 | end | ||
256 | return mime | ||
257 | end | ||
258 | |||
259 | ----------------------------------------------------------------------------- | ||
260 | -- We need base64 convertion routines for Basic Authentication Scheme | ||
261 | ----------------------------------------------------------------------------- | ||
262 | dofile("base64.lua") | ||
263 | |||
264 | ----------------------------------------------------------------------------- | ||
265 | -- Downloads and receives a http url, with its mime headers | ||
266 | -- Input | ||
267 | -- url: unique resource locator desired | ||
268 | -- headers: headers to send with request | ||
269 | -- tried: is this an authentication retry? | ||
270 | -- Returns | ||
271 | -- body: document body, if successfull | ||
272 | -- mime: headers received with document, if sucessfull | ||
273 | -- reply: server reply, if successfull | ||
274 | -- err: error message, if any | ||
275 | ----------------------------------------------------------------------------- | ||
276 | function http_get(url, headers) | ||
277 | local sock, err, mime, body, status, reply | ||
278 | -- get url components | ||
279 | local parsed = %split_url(url, {port = %PORT, path ="/"}) | ||
280 | -- fill default headers | ||
281 | headers = %fill_headers(headers, parsed) | ||
282 | -- try connection | ||
283 | sock, err = connect(parsed.host, parsed.port) | ||
284 | if not sock then return nil, nil, nil, err end | ||
285 | -- set connection timeout | ||
286 | sock:timeout(%TIMEOUT) | ||
287 | -- send request | ||
288 | err = %send_get(sock, parsed.path, headers) | ||
289 | if err then return nil, nil, nil, err end | ||
290 | -- get server message | ||
291 | status, reply, err = %get_reply(sock) | ||
292 | if err then return nil, nil, nil, err end | ||
293 | -- get url accordingly | ||
294 | if status == 200 then -- ok, go on and get it | ||
295 | mime, err = %get_mime(sock, {}) | ||
296 | if err then return nil, nil, reply, err end | ||
297 | body, err = %get_body(sock, mime) | ||
298 | if err then return nil, mime, reply, err end | ||
299 | sock:close() | ||
300 | return body, mime, reply | ||
301 | elseif status == 301 then -- moved permanently, try again | ||
302 | mime = %get_mime(sock, {}) | ||
303 | sock:close() | ||
304 | if mime["location"] then return http_get(mime["location"], headers) | ||
305 | else return nil, mime, reply end | ||
306 | elseif status == 401 then | ||
307 | mime, err = %get_mime(sock, {}) | ||
308 | if err then return nil, nil, reply, err end | ||
309 | return nil, mime, reply | ||
310 | end | ||
311 | return nil, nil, reply | ||
312 | end | ||