* Added preliminary HTTPD construct
[project/luci.git] / libs / web / luasrc / http / protocol.lua
index 2c83257..6901291 100644 (file)
@@ -18,7 +18,9 @@ module("luci.http.protocol", package.seeall)
 require("luci.util")
 
 
-HTTP_MAX_CONTENT     = 1048576         -- 1 MB
+HTTP_MAX_CONTENT     = 1024^2          -- 1 MB maximum content size
+HTTP_MAX_READBUF     = 1024            -- 1 kB read buffer size
+
 HTTP_DEFAULT_CTYPE   = "text/html"     -- default content type
 HTTP_DEFAULT_VERSION = "1.0"           -- HTTP default version
 
@@ -116,7 +118,7 @@ function mimedecode( data, boundary, filecb )
        local params = { }
 
        -- create a line reader
-       local reader = _linereader( data )
+       local reader = _linereader( data, HTTP_MAX_READBUF )
 
        -- state variables
        local in_part = false
@@ -129,9 +131,8 @@ function mimedecode( data, boundary, filecb )
        local field
        local clen = 0
 
-
        -- try to read all mime parts
-       for line in reader do
+       for line, eol in reader do
 
                -- update content length
                clen = clen + line:len()
@@ -327,8 +328,22 @@ end
 -- Parse a http message
 function parse_message( data, filecb )
 
+       local reader  = _linereader( data, HTTP_MAX_READBUF )
+       local message = parse_message_header( reader )
+
+       if message then
+               parse_message_body( reader, message, filecb )
+       end
+
+       return message
+end
+
+
+-- Parse a http message header
+function parse_message_header( data )
+
        -- Create a line reader
-       local reader  = _linereader( data )
+       local reader  = _linereader( data, HTTP_MAX_READBUF )
        local message = { }
 
        -- Try to extract magic
@@ -356,9 +371,6 @@ function parse_message( data, filecb )
 
                        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("?")
@@ -368,71 +380,6 @@ function parse_message( data, filecb )
                                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'];
@@ -467,42 +414,158 @@ function parse_message( data, filecb )
        end
 end
 
-function _linereader( obj )
+
+-- Parse a http message body
+function parse_message_body( reader, message, filecb )
+
+       if type(message) == "table" then
+               local env = message.env
+
+               local clen = ( env.CONTENT_LENGTH or HTTP_MAX_CONTENT ) + 0
+               
+               -- Process post method
+               if env.REQUEST_METHOD:lower() == "post" and env.CONTENT_TYPE then
+
+                       -- Is it multipart/form-data ?
+                       if env.CONTENT_TYPE:match("^multipart/form%-data") then
+                               
+                               -- Read multipart/mime data
+                               for k, v in pairs( mimedecode(
+                                       reader,
+                                       env.CONTENT_TYPE:match("boundary=(.+)"),
+                                       filecb
+                               ) ) do
+                                       message.params[k] = v
+                               end
+
+                       -- Is it x-www-form-urlencoded?
+                       elseif env.CONTENT_TYPE:match('^application/x%-www%-form%-urlencoded') then
+
+                               -- Read post data
+                               local post_data = ""
+
+                               for chunk, eol in reader do
+
+                                       post_data = post_data .. chunk
+
+                                       -- Abort on eol or if maximum allowed size or content length is reached
+                                       if eol or #post_data >= HTTP_MAX_CONTENT or #post_data > clen then
+                                               break
+                                       end
+                               end
+
+                               -- Parse params
+                               for k, v in pairs( urldecode_params( post_data ) ) do
+                                       message.params[k] = v
+                               end
+
+                       -- Unhandled encoding
+                       -- If a file callback is given then feed it line by line, else
+                       -- store whole buffer in message.content
+                       else
+
+                               local len = 0
+
+                               for chunk in reader do
+
+                                       len = len + #chunk
+
+                                       -- 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
+
+                                       -- Abort if maximum allowed size or content length is reached
+                                       if len >= HTTP_MAX_CONTENT or len >= clen then
+                                               break
+                                       end
+                               end
+
+                               -- Send eof to callback
+                               if type(filecb) == "function" then
+                                       filecb( "_post", nil, "", true )
+                               end
+                       end
+               end
+       end
+end
+
+
+-- Wrap given object into a line read iterator
+function _linereader( obj, bufsz )
+
+       bufsz = ( bufsz and bufsz >= 256 ) and bufsz or 256
+
+       local __read = function()  return nil end
+       local __eof  = function(x) return type(x) ~= "string" or #x == 0 end
+
+       local _pos = 1
+       local _buf = ""
+       local _eof = nil
 
        -- object is string
        if type(obj) == "string" then
 
-               return obj:gmatch( "[^\r\n]*\r?\n" )
+               __read = function() return obj:sub( _pos, _pos + bufsz - #_buf - 1 ) end
+
+       -- object implements a receive() or read() function
+       elseif (type(obj) == "userdata" or type(obj) == "table") and ( type(obj.receive) == "function" or type(obj.read) == "function" ) then
+
+               if type(obj.read) == "function" then
+                       __read = function() return obj:read( bufsz - #_buf ) end
+               else
+                       __read = function() return obj:receive( bufsz - #_buf ) end
+               end
 
        -- 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
+       -- no usable data type
+       else
 
-               return obj.readline
+               -- dummy iterator
+               return __read
+       end
 
-       -- 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]
+       -- generic block to line algorithm
+       return function()
+               if not _eof then
+                       local buffer = __read()
+
+                       if __eof( buffer ) then
+                               buffer = ""
                        end
-               end
 
-       -- no usable data type
-       else
+                       _pos   = _pos + #buffer
+                       buffer = _buf .. buffer
 
-               -- dummy iterator
-               return function()
+                       local crlf, endpos = buffer:find("\r?\n")
+
+
+                       if crlf then
+                               _buf = buffer:sub( endpos + 1, #buffer )
+                               return buffer:sub( 1, endpos ), true
+                       else
+                               -- check for eof
+                               _eof = __eof( buffer )
+
+                               -- clear overflow buffer
+                               _buf = ""
+
+                               return buffer, false
+                       end
+               else
                        return nil
                end
        end