--[[ HTTP protocol implementation for LuCI (c) 2008 Freifunk Leipzig / Jo-Philipp Wich Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 $Id$ ]]-- module("luci.http.protocol", package.seeall) require("luci.util") require("luci.bits") HTTP_MAX_CONTENT = 1048576 -- 1 MB HTTP_DEFAULT_CTYPE = "text/html" -- default content type HTTP_DEFAULT_VERSION = "1.0" -- HTTP default version -- Decode an urlencoded string. -- Returns the decoded value. function urldecode( str ) local function __chrdec( hex ) return string.char( luci.bits.Hex2Dec( hex ) ) end if type(str) == "string" then str = str:gsub( "+", " " ):gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec ) end return str end -- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url. -- Returns a table value with urldecoded values. function urldecode_params( url ) local params = { } if url:find("?") then url = url:gsub( "^.+%?([^?]+)", "%1" ) end for i, pair in ipairs(luci.util.split( url, "[&;]+", nil, true )) do -- find key and value local key = urldecode( pair:match("^([^=]+)") ) local val = urldecode( pair:match("^[^=]+=(.+)$") ) -- store if type(key) == "string" and key:len() > 0 then if type(val) ~= "string" then val = "" end if not params[key] then params[key] = val elseif type(params[key]) ~= "table" then params[key] = { params[key], val } else table.insert( params[key], val ) end end end return params end -- Encode given string in urlencoded format. -- Returns the encoded string. function urlencode( str ) local function __chrenc( chr ) return string.format( "%%%02x", string.byte( chr ) ) end if type(str) == "string" then str = str:gsub( "([^a-zA-Z0-9$_%-%.+!*'(),])", __chrenc ) end return str end -- Encode given table to urlencoded string. -- Returns the encoded string. function urlencode_params( tbl ) local enc = "" for k, v in pairs(tbl) do enc = enc .. ( enc and "&" or "" ) .. urlencode(k) .. "=" .. urlencode(v) end return enc end -- Decode MIME encoded data. -- Returns a table with decoded values. function mimedecode( data, boundary, filecb ) local params = { } -- create a line reader local reader = _linereader( data ) -- state variables local in_part = false local in_file = false local in_fbeg = false local in_size = true local filename local buffer local field local clen = 0 -- try to read all mime parts for line in reader do -- update content length clen = clen + line:len() if clen >= HTTP_MAX_CONTENT then in_size = false end -- when no boundary is given, try to find it if not boundary then boundary = line:match("^%-%-([^\r\n]+)\r?\n$") end -- Got a valid boundary line or reached max allowed size. if ( boundary and line:sub(1,2) == "--" and line:len() > #boundary + 2 and line:sub( 3, 2 + #boundary ) == boundary ) or not in_size then -- Flush the data of the previous mime part. -- When field and/or buffer are set to nil we should discard -- the previous section entirely due to format violations. if type(field) == "string" and field:len() > 0 and type(buffer) == "string" then -- According to the rfc the \r\n preceeding a boundary -- is assumed to be part of the boundary itself. -- Since we are reading line by line here, this crlf -- is part of the last line of our section content, -- so strip it before storing the buffer. buffer = buffer:gsub("\r?\n$","") -- If we're in a file part and a file callback has been provided -- then do a final call and send eof. if in_file and type(filecb) == "function" then filecb( field, filename, buffer, true ) params[field] = filename -- Store buffer. else params[field] = buffer end end -- Reset vars buffer = "" filename = nil field = nil in_file = false -- Abort here if we reached maximum allowed size if not in_size then break end -- Do we got the last boundary? if line:len() > #boundary + 4 and line:sub( #boundary + 2, #boundary + 4 ) == "--" then -- No more processing in_part = false -- It's a middle boundary else -- Read headers local hlen, headers = extract_headers( reader ) -- Check for valid headers if headers['Content-Disposition'] then -- Got no content type header, assume content-type "text/plain" if not headers['Content-Type'] then headers['Content-Type'] = 'text/plain' end -- Find field name local hdrvals = luci.util.split( headers['Content-Disposition'], '; ' ) -- Valid form data part? if hdrvals[1] == "form-data" and hdrvals[2]:match("^name=") then -- Store field identifier field = hdrvals[2]:match('^name="(.+)"$') -- Do we got a file upload field? if #hdrvals == 3 and hdrvals[3]:match("^filename=") then in_file = true if_fbeg = true filename = hdrvals[3]:match('^filename="(.+)"$') end -- Entering next part processing in_part = true end end end -- Processing content elseif in_part then -- XXX: Would be really good to switch from line based to -- buffered reading here. -- If we're in a file part and a file callback has been provided -- then call the callback and reset the buffer. if in_file and type(filecb) == "function" then -- If we're not processing the first chunk, then call if not in_fbeg then filecb( field, filename, buffer, false ) buffer = "" -- Clear in_fbeg flag after first run else in_fbeg = false end end -- Append date to buffer buffer = buffer .. line end end return params end -- Extract "magic", the first line of a http message. -- Returns the message type ("get", "post" or "response"), the requested uri -- if it is a valid http request or the status code if the line descripes a -- http response. For requests the third parameter is nil, for responses it -- contains the human readable status description. function extract_magic( reader ) for line in reader do -- Is it a request? local method, uri = line:match("^([A-Z]+) ([^ ]+) HTTP/[01]%.[019]\r?\n$") -- Yup, it is if method then return method:lower(), uri, nil -- Is it a response? else local code, message = line:match("^HTTP/[01]%.[019] ([0-9]+) ([^\r\n]+)\r?\n$") -- Is a response if code then return "response", code + 0, message -- Can't handle it else return nil end end end end -- Extract headers from given string. -- Returns a table of extracted headers and the remainder of the parsed data. function extract_headers( reader, tbl ) local headers = tbl or { } local count = 0 -- Iterate line by line for line in reader do -- Look for a valid header format local hdr, val = line:match( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r?\n$" ) if type(hdr) == "string" and hdr:len() > 0 and type(val) == "string" and val:len() > 0 then count = count + line:len() headers[hdr] = val elseif line:match("^\r?\n$") then return count + line:len(), headers else -- junk data, don't add length return count, headers end end return count, headers end -- Parse a http message function parse_message( data, filecb ) -- Create a line reader local reader = _linereader( data ) local message = { } -- Try to extract magic local method, arg1, arg2 = extract_magic( reader ) -- Does it looks like a valid message? if method then message.request_method = method message.status_code = arg2 and arg1 or 200 message.status_message = arg2 or nil message.request_uri = arg2 and nil or arg1 if method == "response" then message.type = "response" else message.type = "request" end -- Parse headers? local hlen, hdrs = extract_headers( reader ) -- Valid headers? if hlen > 2 and type(hdrs) == "table" then message.headers = hdrs -- Get content local clen = ( hdrs['Content-Length'] or HTTP_MAX_CONTENT ) + 0 -- Process get parameters if ( method == "get" or method == "post" ) and message.request_uri:match("?") then message.params = urldecode_params( message.request_uri ) else message.params = { } end -- Process post method if method == "post" and hdrs['Content-Type'] then -- Is it multipart/form-data ? if hdrs['Content-Type']:match("^multipart/form%-data") then for k, v in pairs( mimedecode( reader, hdrs['Content-Type']:match("boundary=(.+)"), filecb ) ) do message.params[k] = v end -- Is it x-www-urlencoded? elseif hdrs['Content-Type'] == 'application/x-www-urlencoded' then -- XXX: readline isn't the best solution here for chunk in reader do for k, v in pairs( urldecode_params( chunk ) ) do message.params[k] = v end -- XXX: unreliable (undefined line length) if clen + chunk:len() >= HTTP_MAX_CONTENT then break end clen = clen + chunk:len() end -- Unhandled encoding -- If a file callback is given then feed it line by line, else -- store whole buffer in message.content else for chunk in reader do -- We have a callback, feed it. if type(filecb) == "function" then filecb( "_post", nil, chunk, false ) -- Append to .content buffer. else message.content = type(message.content) == "string" and message.content .. chunk or chunk end -- XXX: unreliable if clen + chunk:len() >= HTTP_MAX_CONTENT then break end clen = clen + chunk:len() end -- Send eof to callback if type(filecb) == "function" then filecb( "_post", nil, "", true ) end end end -- Populate common environment variables message.env = { CONTENT_LENGTH = hdrs['Content-Length']; CONTENT_TYPE = hdrs['Content-Type']; REQUEST_METHOD = message.request_method; REQUEST_URI = message.request_uri; SCRIPT_NAME = message.request_uri:gsub("?.+$",""); SCRIPT_FILENAME = "" -- XXX implement me } -- Populate HTTP_* environment variables for i, hdr in ipairs( { 'Accept', 'Accept-Charset', 'Accept-Encoding', 'Accept-Language', 'Connection', 'Cookie', 'Host', 'Referer', 'User-Agent', } ) do local var = 'HTTP_' .. hdr:upper():gsub("%-","_") local val = hdrs[hdr] message.env[var] = val end return message end end end function _linereader( obj ) -- object is string if type(obj) == "string" then return obj:gmatch( "[^\r\n]*\r?\n" ) -- object is a function elseif type(obj) == "function" then return obj -- object is a table and implements a readline() function elseif type(obj) == "table" and type(obj.readline) == "function" then return obj.readline -- object is a table and has a lines property elseif type(obj) == "table" and obj.lines then -- decide wheather to use "lines" as function or table local _lns = ( type(obj.lines) == "function" ) and obj.lines() or obj.lines local _pos = 1 return function() if _pos <= #_lns then _pos = _pos + 1 return _lns[_pos] end end -- no usable data type else -- dummy iterator return function() return nil end end end