3 HTTP protocol implementation for LuCI
4 (c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>
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
10 http://www.apache.org/licenses/LICENSE-2.0
16 module("luci.http.protocol", package.seeall)
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
26 -- Decode an urlencoded string.
27 -- Returns the decoded value.
28 function urldecode( str )
30 local function __chrdec( hex )
31 return string.char( tonumber( hex, 16 ) )
34 if type(str) == "string" then
35 str = str:gsub( "+", " " ):gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
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 )
49 url = url:gsub( "^.+%?([^?]+)", "%1" )
52 for i, pair in ipairs(luci.util.split( url, "[&;]+", nil, true )) do
55 local key = urldecode( pair:match("^([^=]+)") )
56 local val = urldecode( pair:match("^[^=]+=(.+)$") )
59 if type(key) == "string" and key:len() > 0 then
60 if type(val) ~= "string" then val = "" end
62 if not params[key] then
64 elseif type(params[key]) ~= "table" then
65 params[key] = { params[key], val }
67 table.insert( params[key], val )
76 -- Encode given string in urlencoded format.
77 -- Returns the encoded string.
78 function urlencode( str )
80 local function __chrenc( chr )
82 "%%%02x", string.byte( chr )
86 if type(str) == "string" then
88 "([^a-zA-Z0-9$_%-%.+!*'(),])",
97 -- Encode given table to urlencoded string.
98 -- Returns the encoded string.
99 function urlencode_params( tbl )
102 for k, v in pairs(tbl) do
103 enc = enc .. ( enc and "&" or "" ) ..
104 urlencode(k) .. "=" ..
112 -- Decode MIME encoded data.
113 -- Returns a table with decoded values.
114 function mimedecode( data, boundary, filecb )
118 -- create a line reader
119 local reader = _linereader( data )
122 local in_part = false
123 local in_file = false
124 local in_fbeg = false
133 -- try to read all mime parts
134 for line in reader do
136 -- update content length
137 clen = clen + line:len()
139 if clen >= HTTP_MAX_CONTENT then
143 -- when no boundary is given, try to find it
145 boundary = line:match("^%-%-([^\r\n]+)\r?\n$")
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
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"
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$","")
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
173 params[field] = buffer
183 -- Abort here if we reached maximum allowed size
184 if not in_size then break end
186 -- Do we got the last boundary?
187 if line:len() > #boundary + 4 and
188 line:sub( #boundary + 2, #boundary + 4 ) == "--"
190 -- No more processing
193 -- It's a middle boundary
197 local hlen, headers = extract_headers( reader )
199 -- Check for valid headers
200 if headers['Content-Disposition'] then
202 -- Got no content type header, assume content-type "text/plain"
203 if not headers['Content-Type'] then
204 headers['Content-Type'] = 'text/plain'
208 local hdrvals = luci.util.split(
209 headers['Content-Disposition'], '; '
212 -- Valid form data part?
213 if hdrvals[1] == "form-data" and hdrvals[2]:match("^name=") then
215 -- Store field identifier
216 field = hdrvals[2]:match('^name="(.+)"$')
218 -- Do we got a file upload field?
219 if #hdrvals == 3 and hdrvals[3]:match("^filename=") then
222 filename = hdrvals[3]:match('^filename="(.+)"$')
225 -- Entering next part processing
231 -- Processing content
234 -- XXX: Would be really good to switch from line based to
235 -- buffered reading here.
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
242 -- If we're not processing the first chunk, then call
244 filecb( field, filename, buffer, false )
247 -- Clear in_fbeg flag after first run
253 -- Append date to buffer
254 buffer = buffer .. line
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 )
269 for line in reader do
271 local method, uri = line:match("^([A-Z]+) ([^ ]+) HTTP/[01]%.[019]\r?\n$")
275 return method:lower(), uri, nil
279 local code, message = line:match("^HTTP/[01]%.[019] ([0-9]+) ([^\r\n]+)\r?\n$")
283 return "response", code + 0, message
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 )
298 local headers = tbl or { }
301 -- Iterate line by line
302 for line in reader do
304 -- Look for a valid header format
305 local hdr, val = line:match( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r?\n$" )
307 if type(hdr) == "string" and hdr:len() > 0 and
308 type(val) == "string" and val:len() > 0
310 count = count + line:len()
313 elseif line:match("^\r?\n$") then
315 return count + line:len(), headers
318 -- junk data, don't add length
319 return count, headers
323 return count, headers
327 -- Parse a http message
328 function parse_message( data, filecb )
330 local reader = _linereader( data )
331 local message = parse_message_header( reader )
334 parse_message_body( reader, message, filecb )
341 -- Parse a http message header
342 function parse_message_header( data )
344 -- Create a line reader
345 local reader = _linereader( data )
348 -- Try to extract magic
349 local method, arg1, arg2 = extract_magic( reader )
351 -- Does it looks like a valid message?
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
359 if method == "response" then
360 message.type = "response"
362 message.type = "request"
366 local hlen, hdrs = extract_headers( reader )
369 if hlen > 2 and type(hdrs) == "table" then
371 message.headers = hdrs
373 -- Process get parameters
374 if ( method == "get" or method == "post" ) and
375 message.request_uri:match("?")
377 message.params = urldecode_params( message.request_uri )
382 -- Populate common environment variables
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
392 -- Populate HTTP_* environment variables
393 for i, hdr in ipairs( {
404 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
405 local val = hdrs[hdr]
407 message.env[var] = val
417 -- Parse a http message body
418 function parse_message_body( reader, message, filecb )
420 if type(message) == "table" then
421 local env = message.env
423 local clen = ( env.CONTENT_LENGTH or HTTP_MAX_CONTENT ) + 0
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(
431 env.CONTENT_TYPE:match("boundary=(.+)"),
434 message.params[k] = v
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
445 -- XXX: unreliable (undefined line length)
446 if clen + chunk:len() >= HTTP_MAX_CONTENT then
450 clen = clen + chunk:len()
453 -- Unhandled encoding
454 -- If a file callback is given then feed it line by line, else
455 -- store whole buffer in message.content
457 for chunk in reader do
459 -- We have a callback, feed it.
460 if type(filecb) == "function" then
462 filecb( "_post", nil, chunk, false )
464 -- Append to .content buffer.
467 type(message.content) == "string"
468 and message.content .. chunk
473 if clen + chunk:len() >= HTTP_MAX_CONTENT then
477 clen = clen + chunk:len()
480 -- Send eof to callback
481 if type(filecb) == "function" then
482 filecb( "_post", nil, "", true )
490 function _linereader( obj )
493 if type(obj) == "string" then
495 return obj:gmatch( "[^\r\n]*\r?\n" )
497 -- object is a function
498 elseif type(obj) == "function" then
502 -- object is a table and implements a readline() function
503 elseif type(obj) == "table" and type(obj.readline) == "function" then
507 -- object is a table and has a lines property
508 elseif type(obj) == "table" and obj.lines then
510 -- decide wheather to use "lines" as function or table
511 local _lns = ( type(obj.lines) == "function" ) and obj.lines() or obj.lines
515 if _pos <= #_lns then
521 -- no usable data type