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