X-Git-Url: https://git.archive.openwrt.org/?p=project%2Fluci.git;a=blobdiff_plain;f=libs%2Fhttp%2Fluasrc%2Fhttp%2Fprotocol.lua;h=6240953d0fdc676eea63976b4d425cbd927371e8;hp=95712c9d9f8066dabd45c2d0632487232ee93f62;hb=a957254e79a8cfaf4c79da138b6531c6ec324b36;hpb=8c46333ed19507bde7b854789dcc726e0eed1d36 diff --git a/libs/http/luasrc/http/protocol.lua b/libs/http/luasrc/http/protocol.lua index 95712c9d9..6240953d0 100644 --- a/libs/http/luasrc/http/protocol.lua +++ b/libs/http/luasrc/http/protocol.lua @@ -13,17 +13,21 @@ $Id$ ]]-- +--- LuCI http protocol class. +-- This class contains several functions useful for http message- and content +-- decoding and to retrive form data from raw http messages. module("luci.http.protocol", package.seeall) local ltn12 = require("luci.ltn12") -HTTP_MAX_CONTENT = 1024*4 -- 4 kB maximum content size -HTTP_URLENC_MAXKEYLEN = 1024 -- maximum allowd size of urlencoded parameter names -TSRC_BLOCKSIZE = 2048 -- target block size for throttling sources +HTTP_MAX_CONTENT = 1024*8 -- 8 kB maximum content size - --- Decode an urlencoded string. --- Returns the decoded value. +--- Decode an urlencoded string - optionally without decoding +-- the "+" sign to " " - and return the decoded string. +-- @param str Input string in x-www-urlencoded format +-- @param no_plus Don't decode "+" signs to spaces +-- @return The decoded string +-- @see urlencode function urldecode( str, no_plus ) local function __chrdec( hex ) @@ -41,9 +45,15 @@ function urldecode( str, no_plus ) return str end - --- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url. --- Returns a table value with urldecoded values. +--- Extract and split urlencoded data pairs, separated bei either "&" or ";" +-- from given url or string. Returns a table with urldecoded values. +-- Simple parameters are stored as string values associated with the parameter +-- name within the table. Parameters with multiple values are stored as array +-- containing the corresponding values. +-- @param url The url or string which contains x-www-urlencoded form data +-- @param tbl Use the given table for storing values (optional) +-- @return Table containing the urldecoded parameters +-- @see urlencode_params function urldecode_params( url, tbl ) local params = tbl or { } @@ -75,9 +85,10 @@ function urldecode_params( url, tbl ) return params end - --- Encode given string in urlencoded format. --- Returns the encoded string. +--- Encode given string to x-www-urlencoded format. +-- @param str String to encode +-- @return String containing the encoded data +-- @see urldecode function urlencode( str ) local function __chrenc( chr ) @@ -96,23 +107,36 @@ function urlencode( str ) return str end - --- Encode given table to urlencoded string. --- Returns the encoded string. +--- Encode each key-value-pair in given table to x-www-urlencoded format, +-- separated by "&". Tables are encoded as parameters with multiple values by +-- repeating the parameter name with each value. +-- @param tbl Table with the values +-- @return String containing encoded values +-- @see urldecode_params function urlencode_params( tbl ) local enc = "" for k, v in pairs(tbl) do - enc = enc .. ( enc and "&" or "" ) .. - urlencode(k) .. "=" .. - urlencode(v) + if type(v) == "table" then + for i, v2 in ipairs(v) do + enc = enc .. ( #enc > 0 and "&" or "" ) .. + urlencode(k) .. "=" .. urlencode(v2) + end + else + enc = enc .. ( #enc > 0 and "&" or "" ) .. + urlencode(k) .. "=" .. urlencode(v) + end end return enc end - --- Parameter helper +-- (Internal function) +-- Initialize given parameter and coerce string into table when the parameter +-- already exists. +-- @param tbl Table where parameter should be created +-- @param key Parameter name +-- @return Always nil local function __initval( tbl, key ) if tbl[key] == nil then tbl[key] = "" @@ -123,6 +147,14 @@ local function __initval( tbl, key ) end end +-- (Internal function) +-- Append given data to given parameter, either by extending the string value +-- or by appending it to the last string in the parameter's value table. +-- @param tbl Table containing the previously initialized parameter value +-- @param key Parameter name +-- @param chunk String containing the data to append +-- @return Always nil +-- @see __initval local function __appendval( tbl, key, chunk ) if type(tbl[key]) == "table" then tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk @@ -131,6 +163,16 @@ local function __appendval( tbl, key, chunk ) end end +-- (Internal function) +-- Finish the value of given parameter, either by transforming the string value +-- or - in the case of multi value parameters - the last element in the +-- associated values table. +-- @param tbl Table containing the previously initialized parameter value +-- @param key Parameter name +-- @param handler Function which transforms the parameter value +-- @return Always nil +-- @see __initval +-- @see __appendval local function __finishval( tbl, key, handler ) if handler then if type(tbl[key]) == "table" then @@ -229,158 +271,10 @@ process_states['headers'] = function( msg, chunk ) end --- Init urldecoding stream -process_states['urldecode-init'] = function( msg, chunk, filecb ) - - if chunk ~= nil then - - -- Check for Content-Length - if msg.env.CONTENT_LENGTH then - msg.content_length = tonumber(msg.env.CONTENT_LENGTH) - - if msg.content_length <= HTTP_MAX_CONTENT then - -- Initialize buffer - msg._urldecbuffer = chunk - msg._urldeclength = 0 - - -- Switch to urldecode-key state - return true, function(chunk) - return process_states['urldecode-key']( msg, chunk, filecb ) - end - else - return nil, "Request exceeds maximum allowed size" - end - else - return nil, "Missing Content-Length header" - end - else - return nil, "Unexpected EOF" - end -end - - --- Process urldecoding stream, read and validate parameter key -process_states['urldecode-key'] = function( msg, chunk, filecb ) - if chunk ~= nil then - - -- Prevent oversized requests - if msg._urldeclength >= msg.content_length then - return nil, "Request exceeds maximum allowed size" - end - - -- Combine look-behind buffer with current chunk - local buffer = msg._urldecbuffer .. chunk - local spos, epos = buffer:find("=") - - -- Found param - if spos then - - -- Check that key doesn't exceed maximum allowed key length - if ( spos - 1 ) <= HTTP_URLENC_MAXKEYLEN then - local key = urldecode( buffer:sub( 1, spos - 1 ) ) - - -- Prepare buffers - msg._urldeclength = msg._urldeclength + epos - msg._urldecbuffer = buffer:sub( epos + 1, #buffer ) - - -- Use file callback or store values inside msg.params - if filecb then - msg._urldeccallback = function( chunk, eof ) - filecb( field, chunk, eof ) - end - else - __initval( msg.params, key ) - - msg._urldeccallback = function( chunk, eof ) - __appendval( msg.params, key, chunk ) - - -- FIXME: Use a filter - if eof then - __finishval( msg.params, key, urldecode ) - end - end - end - - -- Proceed with urldecode-value state - return true, function( chunk ) - return process_states['urldecode-value']( msg, chunk, filecb ) - end - else - return nil, "POST parameter exceeds maximum allowed length" - end - else - return nil, "POST data exceeds maximum allowed length" - end - else - return nil, "Unexpected EOF" - end -end - - --- Process urldecoding stream, read parameter value -process_states['urldecode-value'] = function( msg, chunk, filecb ) - - if chunk ~= nil then - - -- Combine look-behind buffer with current chunk - local buffer = msg._urldecbuffer .. chunk - - -- Check for EOF - if #buffer == 0 then - -- Compare processed length - if msg._urldeclength == msg.content_length then - -- Cleanup - msg._urldeclength = nil - msg._urldecbuffer = nil - msg._urldeccallback = nil - - -- We won't accept data anymore - return false - else - return nil, "Content-Length mismatch" - end - end - - -- Check for end of value - local spos, epos = buffer:find("[&;]") - if spos then - - -- Flush buffer, send eof - msg._urldeccallback( buffer:sub( 1, spos - 1 ), true ) - msg._urldecbuffer = buffer:sub( epos + 1, #buffer ) - msg._urldeclength = msg._urldeclength + epos - - -- Back to urldecode-key state - return true, function( chunk ) - return process_states['urldecode-key']( msg, chunk, filecb ) - end - else - -- We're somewhere within a data section and our buffer is full - if #buffer > #chunk then - -- Flush buffered data - msg._urldeccallback( buffer:sub( 1, #buffer - #chunk ), false ) - - -- Store new data - msg._urldeclength = msg._urldeclength + #buffer - #chunk - msg._urldecbuffer = buffer:sub( #buffer - #chunk + 1, #buffer ) - - -- Buffer is not full yet, append new data - else - msg._urldecbuffer = buffer - end - - -- Keep feeding me - return true - end - else - -- Send EOF - msg._urldeccallback( "", true ) - return false - end -end - - --- Creates a header source from a given socket +--- Creates a ltn12 source from the given socket. The source will return it's +-- data line by line with the trailing \r\n stripped of. +-- @param sock Readable network socket +-- @return Ltn12 source function function header_source( sock ) return ltn12.source.simplify( function() @@ -407,8 +301,23 @@ function header_source( sock ) end ) end - --- Decode MIME encoded data. +--- Decode a mime encoded http message body with multipart/form-data +-- Content-Type. Stores all extracted data associated with its parameter name +-- in the params table withing the given message object. Multiple parameter +-- values are stored as tables, ordinary ones as strings. +-- If an optional file callback function is given then it is feeded with the +-- file contents chunk by chunk and only the extracted file name is stored +-- within the params table. The callback function will be called subsequently +-- with three arguments: +-- o Table containing decoded (name, file) and raw (headers) mime header data +-- o String value containing a chunk of the file data +-- o Boolean which indicates wheather the current chunk is the last one (eof) +-- @param src Ltn12 source function +-- @param msg HTTP message object +-- @param filecb File callback function (optional) +-- @return Value indicating successful operation (not nil means "ok") +-- @return String containing the error if unsuccessful +-- @see parse_message_header function mimedecode_message_body( src, msg, filecb ) if msg and msg.env.CONTENT_TYPE then @@ -420,6 +329,12 @@ function mimedecode_message_body( src, msg, filecb ) end + local tlen = 0 + local inhdr = false + local field = nil + local store = nil + local lchunk = nil + local function parse_headers( chunk, field ) local stat @@ -448,24 +363,32 @@ function mimedecode_message_body( src, msg, filecb ) field.headers["Content-Type"] = "text/plain" end + if field.name and field.file and filecb then + __initval( msg.params, field.name ) + __appendval( msg.params, field.name, field.file ) + + store = filecb + elseif field.name then + __initval( msg.params, field.name ) + + store = function( hdr, buf, eof ) + __appendval( msg.params, field.name, buf ) + end + else + store = nil + end + return chunk, true end return chunk, false end - - local tlen = 0 - local inhdr = false - local field = nil - local store = nil - local lchunk = nil - local function snk( chunk ) tlen = tlen + ( chunk and #chunk or 0 ) - if msg.env.CONTENT_LENGTH and tlen > msg.env.CONTENT_LENGTH then + if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then return nil, "Message body size exceeds Content-Length" end @@ -492,15 +415,13 @@ function mimedecode_message_body( src, msg, filecb ) if not eof then return nil, "Invalid MIME section header" - end - - if not field.name then + elseif not field.name then return nil, "Invalid Content-Disposition header" end end if store then - store( field.headers, predata, true ) + store( field, predata, true ) end @@ -509,30 +430,16 @@ function mimedecode_message_body( src, msg, filecb ) data, eof = parse_headers( data:sub( epos + 1, #data ), field ) inhdr = not eof - - if eof then - if field.file and filecb then - msg.params[field.name] = field.file - store = filecb - else - __initval( msg.params, field.name ) - - store = function( hdr, buf, eof ) - __appendval( msg.params, field.name, buf ) - end - end - end end until not spos - if found then if #data > 78 then lchunk = data:sub( #data - 78 + 1, #data ) data = data:sub( 1, #data - 78 ) - if store and field and field.name then - store( field.headers, data ) + if store then + store( field, data, false ) else return nil, "Invalid MIME section header" end @@ -544,7 +451,7 @@ function mimedecode_message_body( src, msg, filecb ) lchunk, eof = parse_headers( data, field ) inhdr = not eof else - store( field.headers, lchunk ) + store( field, lchunk, false ) lchunk, chunk = chunk, nil end end @@ -553,50 +460,74 @@ function mimedecode_message_body( src, msg, filecb ) return true end - return luci.ltn12.pump.all( src, snk ) + return ltn12.pump.all( src, snk ) end +--- Decode an urlencoded http message body with application/x-www-urlencoded +-- Content-Type. Stores all extracted data associated with its parameter name +-- in the params table withing the given message object. Multiple parameter +-- values are stored as tables, ordinary ones as strings. +-- @param src Ltn12 source function +-- @param msg HTTP message object +-- @return Value indicating successful operation (not nil means "ok") +-- @return String containing the error if unsuccessful +-- @see parse_message_header +function urldecode_message_body( src, msg ) --- Decode urlencoded data. -function urldecode_message_body( source, msg ) + local tlen = 0 + local lchunk = nil - -- Create an initial LTN12 sink - -- Return the initial state. - local sink = ltn12.sink.simplify( - function( chunk ) - return process_states['urldecode-init']( msg, chunk ) - end - ) + local function snk( chunk ) - -- Create a throttling LTN12 source - -- See explaination in mimedecode_message_body(). - local tsrc = function() - if msg._urldecbuffer ~= nil and #msg._urldecbuffer > 0 then - return "" - else - return source() + tlen = tlen + ( chunk and #chunk or 0 ) + + if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then + return nil, "Message body size exceeds Content-Length" + elseif tlen > HTTP_MAX_CONTENT then + return nil, "Message body size exceeds maximum allowed length" end - end - -- Pump input data... - while true do - -- get data - local ok, err = ltn12.pump.step( tsrc, sink ) + if not lchunk and chunk then + lchunk = chunk - -- step - if not ok and err then - return nil, err + elseif lchunk then + local data = lchunk .. ( chunk or "&" ) + local spos, epos - -- eof - elseif not ok then - return true + repeat + spos, epos = data:find("^.-[;&]") + + if spos then + local pair = data:sub( spos, epos - 1 ) + local key = pair:match("^(.-)=") + local val = pair:match("=([^%s]*)%s*$") + + if key and #key > 0 then + __initval( msg.params, key ) + __appendval( msg.params, key, val ) + __finishval( msg.params, key, urldecode ) + end + + data = data:sub( epos + 1, #data ) + end + until not spos + + lchunk = data end + + return true end -end + return ltn12.pump.all( src, snk ) +end --- Parse a http message header -function parse_message_header( source ) +--- Try to extract an http message header including information like protocol +-- version, message headers and resulting CGI environment variables from the +-- given ltn12 source. +-- @param src Ltn12 source function +-- @return HTTP message object +-- @see parse_message_body +function parse_message_header( src ) local ok = true local msg = { } @@ -611,7 +542,7 @@ function parse_message_header( source ) while ok do -- get data - ok, err = ltn12.pump.step( source, sink ) + ok, err = ltn12.pump.step( src, sink ) -- error if not ok and err then @@ -631,13 +562,15 @@ function parse_message_header( source ) -- Populate common environment variables msg.env = { - CONTENT_LENGTH = tonumber(msg.headers['Content-Length']); + CONTENT_LENGTH = msg.headers['Content-Length']; CONTENT_TYPE = msg.headers['Content-Type']; REQUEST_METHOD = msg.request_method:upper(); REQUEST_URI = msg.request_uri; SCRIPT_NAME = msg.request_uri:gsub("?.+$",""); SCRIPT_FILENAME = ""; -- XXX implement me - SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version) + SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version); + QUERY_STRING = msg.request_uri:match("?") + and msg.request_uri:gsub("^.+?","") or "" } -- Populate HTTP_* environment variables @@ -663,21 +596,32 @@ function parse_message_header( source ) return msg end - --- Parse a http message body -function parse_message_body( source, msg, filecb ) +--- Try to extract and decode a http message body from the given ltn12 source. +-- This function will examine the Content-Type within the given message object +-- to select the appropriate content decoder. +-- Currently the application/x-www-urlencoded and application/form-data +-- mime types are supported. If the encountered content encoding can't be +-- handled then the whole message body will be stored unaltered as "content" +-- property within the given message object. +-- @param src Ltn12 source function +-- @param msg HTTP message object +-- @param filecb File data callback (optional, see mimedecode_message_body()) +-- @return Value indicating successful operation (not nil means "ok") +-- @return String containing the error if unsuccessful +-- @see parse_message_header +function parse_message_body( src, msg, filecb ) -- Is it multipart/mime ? if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and msg.env.CONTENT_TYPE:match("^multipart/form%-data") then - return mimedecode_message_body( source, msg, filecb ) + return mimedecode_message_body( src, msg, filecb ) -- Is it application/x-www-form-urlencoded ? elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded" then - return urldecode_message_body( source, msg, filecb ) + return urldecode_message_body( src, msg, filecb ) -- Unhandled encoding @@ -696,22 +640,23 @@ function parse_message_body( source, msg, filecb ) msg.content = "" msg.content_length = 0 - sink = function( chunk ) - if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then - - msg.content = msg.content .. chunk - msg.content_length = msg.content_length + #chunk - - return true - else - return nil, "POST data exceeds maximum allowed length" + sink = function( chunk, err ) + if chunk then + if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then + msg.content = msg.content .. chunk + msg.content_length = msg.content_length + #chunk + return true + else + return nil, "POST data exceeds maximum allowed length" + end end + return true end end -- Pump data... while true do - local ok, err = ltn12.pump.step( source, sink ) + local ok, err = ltn12.pump.step( src, sink ) if not ok and err then return nil, err @@ -719,13 +664,17 @@ function parse_message_body( source, msg, filecb ) return true end end + + return true end end --- Status codes +--- Table containing human readable messages for several http status codes. +-- @class table statusmsg = { [200] = "OK", [301] = "Moved Permanently", + [302] = "Found", [304] = "Not Modified", [400] = "Bad Request", [403] = "Forbidden",