* luci/libs/http: use aliased ltn12. instead of luci.ltn12.
[project/luci.git] / libs / http / luasrc / http / protocol.lua
index ac58578..acf47d2 100644 (file)
@@ -16,22 +16,23 @@ $Id$
 module("luci.http.protocol", package.seeall)
 
 local ltn12 = require("luci.ltn12")
-require("luci.http.protocol.filter")
-
-HTTP_MAX_CONTENT      = 1024*4         -- 4 kB maximum content size
-HTTP_URLENC_MAXKEYLEN = 1024           -- maximum allowd size of urlencoded parameter names
 
+HTTP_MAX_CONTENT      = 1024*8         -- 8 kB maximum content size
 
 -- Decode an urlencoded string.
 -- Returns the decoded value.
-function urldecode( str )
+function urldecode( str, no_plus )
 
        local function __chrdec( hex )
                return string.char( tonumber( hex, 16 ) )
        end
 
        if type(str) == "string" then
-               str = str:gsub( "+", " " ):gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
+               if not no_plus then
+                       str = str:gsub( "+", " " )
+               end
+
+               str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
        end
 
        return str
@@ -84,7 +85,7 @@ function urlencode( str )
 
        if type(str) == "string" then
                str = str:gsub(
-                       "([^a-zA-Z0-9$_%-%.+!*'(),])",
+                       "([^a-zA-Z0-9$_%-%.%+!*'(),])",
                        __chrenc
                )
        end
@@ -108,6 +109,36 @@ function urlencode_params( tbl )
 end
 
 
+-- Parameter helper
+local function __initval( tbl, key )
+       if tbl[key] == nil then
+               tbl[key] = ""
+       elseif type(tbl[key]) == "string" then
+               tbl[key] = { tbl[key], "" }
+       else
+               table.insert( tbl[key], "" )
+       end
+end
+
+local function __appendval( tbl, key, chunk )
+       if type(tbl[key]) == "table" then
+               tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
+       else
+               tbl[key] = tbl[key] .. chunk
+       end
+end
+
+local function __finishval( tbl, key, handler )
+       if handler then
+               if type(tbl[key]) == "table" then
+                       tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
+               else
+                       tbl[key] = handler( tbl[key] )
+               end
+       end
+end
+
+
 -- Table of our process states
 local process_states = { }
 
@@ -195,457 +226,229 @@ process_states['headers'] = function( msg, chunk )
 end
 
 
--- Find first MIME boundary
-process_states['mime-init'] = function( msg, chunk, filecb )
-
-       if chunk ~= nil then
-               if #chunk >= #msg.mime_boundary + 2 then
-                       local boundary = chunk:sub( 1, #msg.mime_boundary + 4 )
-
-                       if boundary == "--" .. msg.mime_boundary .. "\r\n" then
+-- Creates a header source from a given socket
+function header_source( sock )
+       return ltn12.source.simplify( function()
 
-                               -- Store remaining data in buffer
-                               msg._mimebuffer = chunk:sub( #msg.mime_boundary + 5, #chunk )
+               local chunk, err, part = sock:receive("*l")
 
-                               -- Switch to header processing state
-                               return true, function( chunk )
-                                       return process_states['mime-headers']( msg, chunk, filecb )
-                               end
+               -- Line too long
+               if chunk == nil then
+                       if err ~= "timeout" then
+                               return nil, part
+                                       and "Line exceeds maximum allowed length"
+                                       or  "Unexpected EOF"
                        else
-                               return nil, "Invalid MIME boundary"
+                               return nil, err
                        end
-               else
-                       return true
+
+               -- Line ok
+               elseif chunk ~= nil then
+
+                       -- Strip trailing CR
+                       chunk = chunk:gsub("\r$","")
+
+                       return chunk, nil
                end
-       else
-               return nil, "Unexpected EOF"
-       end
+       end )
 end
 
 
--- Read MIME part headers
-process_states['mime-headers'] = function( msg, chunk, filecb )
+-- Decode MIME encoded data.
+function mimedecode_message_body( src, msg, filecb )
 
-       if chunk ~= nil then
+       if msg and msg.env.CONTENT_TYPE then
+               msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
+       end
 
-               -- Combine look-behind buffer with current chunk
-               chunk = msg._mimebuffer .. chunk
+       if not msg.mime_boundary then
+               return nil, "Invalid Content-Type found"
+       end
 
-               if not msg._mimeheaders then
-                       msg._mimeheaders = { }
-               end
 
-               local function __storehdr( k, v )
-                       msg._mimeheaders[k] = v
-                       return ""
-               end
+       local tlen   = 0
+       local inhdr  = false
+       local field  = nil
+       local store  = nil
+       local lchunk = nil
 
-               -- Read all header lines
-               local ok, count = 1, 0
-               while ok > 0 do
-                       chunk, ok = chunk:gsub( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n", __storehdr )
-                       count = count + ok
-               end
+       local function parse_headers( chunk, field )
 
-               -- Headers processed, check for empty line
-               chunk, ok = chunk:gsub( "^\r\n", "" )
+               local stat
+               repeat
+                       chunk, stat = chunk:gsub(
+                               "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
+                               function(k,v)
+                                       field.headers[k] = v
+                                       return ""
+                               end
+                       )
+               until stat == 0
 
-               -- Store remaining buffer contents
-               msg._mimebuffer = chunk
+               chunk, stat = chunk:gsub("^\r\n","")
 
                -- End of headers
-               if ok > 0 then
+               if stat > 0 then
+                       if field.headers["Content-Disposition"] then
+                               if field.headers["Content-Disposition"]:match("^form%-data; ") then
+                                       field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
+                                       field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
+                               end
+                       end
 
-                       -- When no Content-Type header is given assume text/plain
-                       if not msg._mimeheaders['Content-Type'] then
-                               msg._mimeheaders['Content-Type'] = 'text/plain'
+                       if not field.headers["Content-Type"] then
+                               field.headers["Content-Type"] = "text/plain"
                        end
 
-                       -- Check Content-Disposition
-                       if msg._mimeheaders['Content-Disposition'] then
-                               -- Check for "form-data" token
-                               if msg._mimeheaders['Content-Disposition']:match("^form%-data; ") then
-                                       -- Check for field name, filename
-                                       local field = msg._mimeheaders['Content-Disposition']:match('name="(.-)"')
-                                       local file  = msg._mimeheaders['Content-Disposition']:match('filename="(.+)"$')
-
-                                       -- Is a file field and we have a callback
-                                       if file and filecb then
-                                               msg.params[field] = file
-                                               msg._mimecallback = function(chunk,eof)
-                                                       filecb( {
-                                                               name    = field;
-                                                               file    = file;
-                                                               headers = msg._mimeheaders
-                                                       }, chunk, eof )
-                                               end
+                       if field.name and field.file and filecb then
+                               __initval( msg.params, field.name )
+                               __appendval( msg.params, field.name, field.file )
 
-                                       -- Treat as form field
-                                       else
-                                               msg.params[field] = ""
-                                               msg._mimecallback = function(chunk,eof)
-                                                       msg.params[field] = msg.params[field] .. chunk
-                                               end
-                                       end
+                               store = filecb
+                       elseif field.name then
+                               __initval( msg.params, field.name )
 
-                                       -- Header was valid, continue with mime-data
-                                       return true, function( chunk )
-                                               return process_states['mime-data']( msg, chunk, filecb )
-                                       end
-                               else
-                                       -- Unknown Content-Disposition, abort
-                                       return nil, "Unexpected Content-Disposition MIME section header"
+                               store = function( hdr, buf, eof )
+                                       __appendval( msg.params, field.name, buf )
                                end
                        else
-                               -- Content-Disposition is required, abort without
-                               return nil, "Missing Content-Disposition MIME section header"
+                               store = nil
                        end
 
-               -- We parsed no headers yet and buffer is almost empty
-               elseif count > 0 or #chunk < 128 then
-                       -- Keep feeding me with chunks
-                       return true, nil
+                       return chunk, true
                end
 
-               -- Buffer looks like garbage
-               return nil, "Malformed MIME section header"
-       else
-               return nil, "Unexpected EOF"
+               return chunk, false
        end
-end
-
-
--- Read MIME part data
-process_states['mime-data'] = function( msg, chunk, filecb )
-
-       if chunk ~= nil then
-
-               -- Combine look-behind buffer with current chunk
-               local buffer = msg._mimebuffer .. chunk
-
-               -- Look for MIME boundary
-               local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
-
-               if spos then
-                       -- Content data
-                       msg._mimecallback( buffer:sub( 1, spos - 1 ), true )
-
-                       -- Store remainder
-                       msg._mimebuffer = buffer:sub( epos + 1, #buffer )
-
-                       -- Next state is mime-header processing
-                       return true, function( chunk )
-                               return process_states['mime-headers']( msg, chunk, filecb )
-                       end
-               else
-                       -- Look for EOF?
-                       local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
-
-                       if spos then
-                               -- Content data
-                               msg._mimecallback( buffer:sub( 1, spos - 1 ), true )
 
-                               -- We processed the final MIME boundary, cleanup
-                               msg._mimebuffer   = nil
-                               msg._mimeheaders  = nil
-                               msg._mimecallback = nil
+       local function snk( chunk )
 
-                               -- We won't accept data anymore
-                               return false
-                       else
-                               -- We're somewhere within a data section and our buffer is full
-                               if #buffer > #chunk then
-                                       -- Flush buffered data
-                                       msg._mimecallback( buffer:sub( 1, #buffer - #chunk ), false )
-
-                                       -- Store new data
-                                       msg._mimebuffer = buffer:sub( #buffer - #chunk + 1, #buffer )
+               tlen = tlen + ( chunk and #chunk or 0 )
 
-                               -- Buffer is not full yet, append new data
-                               else
-                                       msg._mimebuffer = buffer
-                               end
-
-                               -- Keep feeding me
-                               return true
-                       end
+               if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
+                       return nil, "Message body size exceeds Content-Length"
                end
-       else
-               return nil, "Unexpected EOF"
-       end
-end
-
-
--- Init urldecoding stream
-process_states['urldecode-init'] = function( msg, chunk, filecb )
 
-       if chunk ~= nil then
+               if chunk and not lchunk then
+                       lchunk = "\r\n" .. chunk
 
-               -- Check for Content-Length
-               if msg.env.CONTENT_LENGTH then
-                       msg.content_length = tonumber(msg.env.CONTENT_LENGTH)
+               elseif lchunk then
+                       local data = lchunk .. ( chunk or "" )
+                       local spos, epos, found
 
-                       if msg.content_length <= HTTP_MAX_CONTENT then
-                               -- Initialize buffer
-                               msg._urldecbuffer = chunk
-                               msg._urldeclength = 0
+                       repeat
+                               spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
 
-                               -- Switch to urldecode-key state
-                               return true, function(chunk)
-                                       return process_states['urldecode-key']( msg, chunk, filecb )
+                               if not spos then
+                                       spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
                                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("=")
+                               if spos then
+                                       local predata = data:sub( 1, spos - 1 )
 
-               -- Found param
-               if spos then
+                                       if inhdr then
+                                               predata, eof = parse_headers( predata, field )
 
-                       -- 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.params[key]         = ""
-                               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
-                                       msg._urldeccallback = function( chunk, eof )
-                                               msg.params[key] = msg.params[key] .. chunk
-
-                                               -- FIXME: Use a filter
-                                               if eof then
-                                                       msg.params[key] = urldecode( msg.params[key] )
+                                               if not eof then
+                                                       return nil, "Invalid MIME section header"
+                                               elseif not field.name then
+                                                       return nil, "Invalid Content-Disposition header"
                                                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
 
+                                       if store then
+                                               store( field.headers, predata, true )
+                                       end
 
--- Process urldecoding stream, read parameter value
-process_states['urldecode-value'] = function( msg, chunk, filecb )
 
-       if chunk ~= nil then
+                                       field = { headers = { } }
+                                       found = found or true
 
-               -- Combine look-behind buffer with current chunk
-               local buffer = msg._urldecbuffer .. chunk
+                                       data, eof = parse_headers( data:sub( epos + 1, #data ), field )
+                                       inhdr = not eof
+                               end
+                       until not spos
 
-               -- 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
+                       if found then
+                               if #data > 78 then
+                                       lchunk = data:sub( #data - 78 + 1, #data )
+                                       data   = data:sub( 1, #data - 78 )
 
-                               -- We won't accept data anymore
-                               return false
+                                       if store then
+                                               store( field.headers, data, false )
+                                       else
+                                               return nil, "Invalid MIME section header"
+                                       end
+                               else
+                                       lchunk, data = data, nil
+                               end
                        else
-                               return nil, "Content-Length mismatch"
+                               if inhdr then
+                                       lchunk, eof = parse_headers( data, field )
+                                       inhdr = not eof
+                               else
+                                       store( field.headers, lchunk, false )
+                                       lchunk, chunk = chunk, nil
+                               end
                        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
+               return true
        end
-end
-
-
--- Creates a header source from a given socket
-function header_source( sock )
-       return ltn12.source.simplify( function()
-
-               local chunk, err, part = sock:receive("*l")
-
-               -- Line too long
-               if chunk == nil then
-                       if err ~= "timeout" then
-                               return nil, part
-                                       and "Line exceeds maximum allowed length["..part.."]"
-                                       or  "Unexpected EOF"
-                       else
-                               return nil, err
-                       end
 
-               -- Line ok
-               elseif chunk ~= nil then
-
-                       -- Strip trailing CR
-                       chunk = chunk:gsub("\r$","")
-
-                       return chunk, nil
-               end
-       end )
+       return ltn12.pump.all( src, snk )
 end
 
 
--- Decode MIME encoded data.
-function mimedecode_message_body( source, msg, filecb )
+-- Decode urlencoded data.
+function urldecode_message_body( src, msg )
 
-       -- Find mime boundary
-       if msg and msg.env.CONTENT_TYPE then
+       local tlen   = 0
+       local lchunk = nil
 
-               local bound = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)")
+       local function snk( chunk )
 
-               if bound then
-                       msg.mime_boundary = bound
-               else
-                       return nil, "No MIME boundary found or invalid content type given"
-               end
-       end
+               tlen = tlen + ( chunk and #chunk or 0 )
 
-       -- Create an initial LTN12 sink
-       -- The whole MIME parsing process is implemented as fancy sink, sinks replace themself
-       -- depending on current processing state (init, header, data). Return the initial state.
-       local sink = ltn12.sink.simplify(
-               function( chunk )
-                       return process_states['mime-init']( msg, chunk, filecb )
+               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
-       )
 
-       -- Create a throttling LTN12 source
-       -- Frequent state switching in the mime parsing process leads to unwanted buffer aggregation.
-       -- This source checks wheather there's still data in our internal read buffer and returns an
-       -- empty string if there's already enough data in the processing queue. If the internal buffer
-       -- runs empty we're calling the original source to get the next chunk of data.
-       local tsrc = function()
-
-               -- XXX: we schould propably keep the maximum buffer size in sync with
-               --      the blocksize of our original source... but doesn't really matter
-               if msg._mimebuffer ~= null and #msg._mimebuffer > 256 then
-                       return ""
-               else
-                       return source()
-               end
-       end
+               if not lchunk and chunk then
+                       lchunk = chunk
 
-       -- Pump input data...
-       while true do
-               -- get data
-               local ok, err = ltn12.pump.step( tsrc, sink )
+               elseif lchunk then
+                       local data = lchunk .. ( chunk or "&" )
+                       local spos, epos
 
-               -- error
-               if not ok and err then
-                       return nil, err
+                       repeat
+                               spos, epos = data:find("^.-[;&]")
 
-               -- eof
-               elseif not ok then
-                       return true
-               end
-       end
-end
+                               if spos then
+                                       local pair = data:sub( spos, epos - 1 )
+                                       local key  = pair:match("^(.-)=")
+                                       local val  = pair:match("=(.*)$")
 
+                                       if key and #key > 0 then
+                                               __initval( msg.params, key )
+                                               __appendval( msg.params, key, val )
+                                               __finishval( msg.params, key, urldecode )
+                                       end
 
--- Decode urlencoded data.
-function urldecode_message_body( source, msg )
+                                       data = data:sub( epos + 1, #data )
+                               end
+                       until not spos
 
-       -- Create an initial LTN12 sink
-       -- Return the initial state.
-       local sink = ltn12.sink.simplify(
-               function( chunk )
-                       return process_states['urldecode-init']( msg, chunk )
+                       lchunk = data
                end
-       )
 
-       -- Create a throttling LTN12 source
-       -- See explaination in mimedecode_message_body().
-       local tsrc = function()
-               if msg._urldecbuffer ~= null and #msg._urldecbuffer > 0 then
-                       return ""
-               else
-                       return source()
-               end
+               return true
        end
 
-       -- Pump input data...
-       while true do
-               -- get data
-               local ok, err = ltn12.pump.step( tsrc, sink )
-
-               -- step
-               if not ok and err then
-                       return nil, err
-
-               -- eof
-               elseif not ok then
-                       return true
-               end
-       end
+       return ltn12.pump.all( src, snk )
 end