* luci/libs: protocol.lua: store status code and status description in http message...
[project/luci.git] / libs / web / luasrc / http / protocol.lua
1 --[[
2
3 HTTP protocol implementation for LuCI
4 (c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>
5
6 Licensed under the Apache License, Version 2.0 (the "License");
7 you may not use this file except in compliance with the License.
8 You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 $Id$
13
14 ]]--
15
16 module("luci.http.protocol", package.seeall)
17
18 require("luci.util")
19 require("luci.bits")
20
21
22 HTTP_MAX_CONTENT = 1048576 -- 1 MB
23 HTTP_DEFAULT_CTYPE = "text/html" -- default content type
24 HTTP_DEFAULT_VERSION = "1.0" -- HTTP default version
25
26
27 -- Decode an urlencoded string.
28 -- Returns the decoded value.
29 function urldecode( str )
30
31 local function __chrdec( hex )
32 return string.char( luci.bits.Hex2Dec( hex ) )
33 end
34
35 if type(str) == "string" then
36 str = str:gsub( "+", " " ):gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
37 end
38
39 return str
40 end
41
42
43 -- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url.
44 -- Returns a table value with urldecoded values.
45 function urldecode_params( url )
46
47 local params = { }
48
49 if url:find("?") then
50 url = url:gsub( "^.+%?([^?]+)", "%1" )
51 end
52
53 for i, pair in ipairs(luci.util.split( url, "[&;]+", nil, true )) do
54
55 -- find key and value
56 local key = urldecode( pair:match("^([^=]+)") )
57 local val = urldecode( pair:match("^[^=]+=(.+)$") )
58
59 -- store
60 if type(key) == "string" and key:len() > 0 then
61 if type(val) ~= "string" then val = "" end
62
63 if not params[key] then
64 params[key] = val
65 elseif type(params[key]) ~= "table" then
66 params[key] = { params[key], val }
67 else
68 table.insert( params[key], val )
69 end
70 end
71 end
72
73 return params
74 end
75
76
77 -- Encode given string in urlencoded format.
78 -- Returns the encoded string.
79 function urlencode( str )
80
81 local function __chrenc( chr )
82 return string.format(
83 "%%%02x", string.byte( chr )
84 )
85 end
86
87 if type(str) == "string" then
88 str = str:gsub(
89 "([^a-zA-Z0-9$_%-%.+!*'(),])",
90 __chrenc
91 )
92 end
93
94 return str
95 end
96
97
98 -- Encode given table to urlencoded string.
99 -- Returns the encoded string.
100 function urlencode_params( tbl )
101 local enc = ""
102
103 for k, v in pairs(tbl) do
104 enc = enc .. ( enc and "&" or "" ) ..
105 urlencode(k) .. "=" ..
106 urlencode(v)
107 end
108
109 return enc
110 end
111
112
113 -- Decode MIME encoded data.
114 -- Returns a table with decoded values.
115 function mimedecode( data, boundary, filecb )
116
117 local params = { }
118
119 -- create a line reader
120 local reader = _linereader( data )
121
122 -- state variables
123 local in_part = false
124 local in_file = false
125 local in_fbeg = false
126 local in_size = true
127
128 local filename
129 local buffer
130 local field
131 local clen = 0
132
133
134 -- try to read all mime parts
135 for line in reader do
136
137 -- update content length
138 clen = clen + line:len()
139
140 if clen >= HTTP_MAX_CONTENT then
141 in_size = false
142 end
143
144 -- when no boundary is given, try to find it
145 if not boundary then
146 boundary = line:match("^%-%-([^\r\n]+)\r?\n$")
147 end
148
149 -- Got a valid boundary line or reached max allowed size.
150 if ( boundary and line:sub(1,2) == "--" and line:len() > #boundary + 2 and
151 line:sub( 3, 2 + #boundary ) == boundary ) or not in_size
152 then
153 -- Flush the data of the previous mime part.
154 -- When field and/or buffer are set to nil we should discard
155 -- the previous section entirely due to format violations.
156 if type(field) == "string" and field:len() > 0 and
157 type(buffer) == "string"
158 then
159 -- According to the rfc the \r\n preceeding a boundary
160 -- is assumed to be part of the boundary itself.
161 -- Since we are reading line by line here, this crlf
162 -- is part of the last line of our section content,
163 -- so strip it before storing the buffer.
164 buffer = buffer:gsub("\r?\n$","")
165
166 -- If we're in a file part and a file callback has been provided
167 -- then do a final call and send eof.
168 if in_file and type(filecb) == "function" then
169 filecb( field, filename, buffer, true )
170 params[field] = filename
171
172 -- Store buffer.
173 else
174 params[field] = buffer
175 end
176 end
177
178 -- Reset vars
179 buffer = ""
180 filename = nil
181 field = nil
182 in_file = false
183
184 -- Abort here if we reached maximum allowed size
185 if not in_size then break end
186
187 -- Do we got the last boundary?
188 if line:len() > #boundary + 4 and
189 line:sub( #boundary + 2, #boundary + 4 ) == "--"
190 then
191 -- No more processing
192 in_part = false
193
194 -- It's a middle boundary
195 else
196
197 -- Read headers
198 local hlen, headers = extract_headers( reader )
199
200 -- Check for valid headers
201 if headers['Content-Disposition'] then
202
203 -- Got no content type header, assume content-type "text/plain"
204 if not headers['Content-Type'] then
205 headers['Content-Type'] = 'text/plain'
206 end
207
208 -- Find field name
209 local hdrvals = luci.util.split(
210 headers['Content-Disposition'], '; '
211 )
212
213 -- Valid form data part?
214 if hdrvals[1] == "form-data" and hdrvals[2]:match("^name=") then
215
216 -- Store field identifier
217 field = hdrvals[2]:match('^name="(.+)"$')
218
219 -- Do we got a file upload field?
220 if #hdrvals == 3 and hdrvals[3]:match("^filename=") then
221 in_file = true
222 if_fbeg = true
223 filename = hdrvals[3]:match('^filename="(.+)"$')
224 end
225
226 -- Entering next part processing
227 in_part = true
228 end
229 end
230 end
231
232 -- Processing content
233 elseif in_part then
234
235 -- XXX: Would be really good to switch from line based to
236 -- buffered reading here.
237
238
239 -- If we're in a file part and a file callback has been provided
240 -- then call the callback and reset the buffer.
241 if in_file and type(filecb) == "function" then
242
243 -- If we're not processing the first chunk, then call
244 if not in_fbeg then
245 filecb( field, filename, buffer, false )
246 buffer = ""
247
248 -- Clear in_fbeg flag after first run
249 else
250 in_fbeg = false
251 end
252 end
253
254 -- Append date to buffer
255 buffer = buffer .. line
256 end
257 end
258
259 return params
260 end
261
262
263 -- Extract "magic", the first line of a http message.
264 -- Returns the message type ("get", "post" or "response"), the requested uri
265 -- if it is a valid http request or the status code if the line descripes a
266 -- http response. For requests the third parameter is nil, for responses it
267 -- contains the human readable status description.
268 function extract_magic( reader )
269
270 for line in reader do
271 -- Is it a request?
272 local method, uri = line:match("^([A-Z]+) ([^ ]+) HTTP/[01]%.[019]\r?\n$")
273
274 -- Yup, it is
275 if method then
276 return method:lower(), uri, nil
277
278 -- Is it a response?
279 else
280 local code, message = line:match("^HTTP/[01]%.[019] ([0-9]+) ([^\r\n]+)\r?\n$")
281
282 -- Is a response
283 if code then
284 return "response", code + 0, message
285
286 -- Can't handle it
287 else
288 return nil
289 end
290 end
291 end
292 end
293
294
295 -- Extract headers from given string.
296 -- Returns a table of extracted headers and the remainder of the parsed data.
297 function extract_headers( reader, tbl )
298
299 local headers = tbl or { }
300 local count = 0
301
302 -- Iterate line by line
303 for line in reader do
304
305 -- Look for a valid header format
306 local hdr, val = line:match( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r?\n$" )
307
308 if type(hdr) == "string" and hdr:len() > 0 and
309 type(val) == "string" and val:len() > 0
310 then
311 count = count + line:len()
312 headers[hdr] = val
313
314 elseif line:match("^\r?\n$") then
315
316 return count + line:len(), headers
317
318 else
319 -- junk data, don't add length
320 return count, headers
321 end
322 end
323
324 return count, headers
325 end
326
327
328 -- Parse a http message
329 function parse_message( data, filecb )
330
331 -- Create a line reader
332 local reader = _linereader( data )
333 local message = { }
334
335 -- Try to extract magic
336 local method, arg1, arg2 = extract_magic( reader )
337
338 -- Does it looks like a valid message?
339 if method then
340
341 message.request_method = method
342 message.status_code = arg2 and arg1 or 200
343 message.status_message = arg2 or nil
344 message.request_uri = arg2 and nil or arg1
345
346 if method == "response" then
347 message.type = "response"
348 else
349 message.type = "request"
350 end
351
352 -- Parse headers?
353 local hlen, hdrs = extract_headers( reader )
354
355 -- Valid headers?
356 if hlen > 2 and type(hdrs) == "table" then
357
358 message.headers = hdrs
359
360 -- Get content
361 local clen = ( hdrs['Content-Length'] or HTTP_MAX_CONTENT ) + 0
362
363 -- Process get parameters
364 if ( method == "get" or method == "post" ) and
365 message.request_uri:match("?")
366 then
367 message.params = urldecode_params( message.request_uri )
368 else
369 message.params = { }
370 end
371
372 -- Process post method
373 if method == "post" and hdrs['Content-Type'] then
374
375 -- Is it multipart/form-data ?
376 if hdrs['Content-Type']:match("^multipart/form%-data") then
377 for k, v in pairs( mimedecode(
378 reader,
379 hdrs['Content-Type']:match("boundary=(.+)"),
380 filecb
381 ) ) do
382 message.params[k] = v
383 end
384
385 -- Is it x-www-urlencoded?
386 elseif hdrs['Content-Type'] == 'application/x-www-urlencoded' then
387
388 -- XXX: readline isn't the best solution here
389 for chunk in reader do
390 for k, v in pairs( urldecode_params( chunk ) ) do
391 message.params[k] = v
392 end
393
394 -- XXX: unreliable (undefined line length)
395 if clen + chunk:len() >= HTTP_MAX_CONTENT then
396 break
397 end
398
399 clen = clen + chunk:len()
400 end
401
402 -- Unhandled encoding
403 -- If a file callback is given then feed it line by line, else
404 -- store whole buffer in message.content
405 else
406
407 for chunk in reader do
408
409 -- We have a callback, feed it.
410 if type(filecb) == "function" then
411
412 filecb( "_post", nil, chunk, false )
413
414 -- Append to .content buffer.
415 else
416 message.content =
417 type(message.content) == "string"
418 and message.content .. chunk
419 or chunk
420 end
421
422 -- XXX: unreliable
423 if clen + chunk:len() >= HTTP_MAX_CONTENT then
424 break
425 end
426
427 clen = clen + chunk:len()
428 end
429
430 -- Send eof to callback
431 if type(filecb) == "function" then
432 filecb( "_post", nil, "", true )
433 end
434 end
435 end
436
437 -- Populate common environment variables
438 message.env = {
439 CONTENT_LENGTH = hdrs['Content-Length'];
440 CONTENT_TYPE = hdrs['Content-Type'];
441 REQUEST_METHOD = message.request_method;
442 REQUEST_URI = message.request_uri;
443 SCRIPT_NAME = message.request_uri:gsub("?.+$","");
444 SCRIPT_FILENAME = "" -- XXX implement me
445 }
446
447 -- Populate HTTP_* environment variables
448 for i, hdr in ipairs( {
449 'Accept',
450 'Accept-Charset',
451 'Accept-Encoding',
452 'Accept-Language',
453 'Connection',
454 'Cookie',
455 'Host',
456 'Referer',
457 'User-Agent',
458 } ) do
459 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
460 local val = hdrs[hdr]
461
462 message.env[var] = val
463 end
464
465
466 return message
467 end
468 end
469 end
470
471 function _linereader( obj )
472
473 -- object is string
474 if type(obj) == "string" then
475
476 return obj:gmatch( "[^\r\n]*\r?\n" )
477
478 -- object is a function
479 elseif type(obj) == "function" then
480
481 return obj
482
483 -- object is a table and implements a readline() function
484 elseif type(obj) == "table" and type(obj.readline) == "function" then
485
486 return obj.readline
487
488 -- object is a table and has a lines property
489 elseif type(obj) == "table" and obj.lines then
490
491 -- decide wheather to use "lines" as function or table
492 local _lns = ( type(obj.lines) == "function" ) and obj.lines() or obj.lines
493 local _pos = 1
494
495 return function()
496 if _pos <= #_lns then
497 _pos = _pos + 1
498 return _lns[_pos]
499 end
500 end
501
502 -- no usable data type
503 else
504
505 -- dummy iterator
506 return function()
507 return nil
508 end
509 end
510 end