Remove remaining references to boa and lucid
[project/luci.git] / libs / web / luasrc / http / protocol.lua
index 6901291..0d41550 100644 (file)
@@ -1,57 +1,68 @@
---[[                                                                            
-                                                                                
+--[[
+
 HTTP protocol implementation for LuCI
-(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>           
-                                                                                
-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$                             
-                                                                                
-]]--
+(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>
 
-module("luci.http.protocol", package.seeall)
+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
 
-require("luci.util")
+        http://www.apache.org/licenses/LICENSE-2.0
 
+$Id$
 
-HTTP_MAX_CONTENT     = 1024^2          -- 1 MB maximum content size
-HTTP_MAX_READBUF     = 1024            -- 1 kB read buffer size
+]]--
+
+--- 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)
 
-HTTP_DEFAULT_CTYPE   = "text/html"     -- default content type
-HTTP_DEFAULT_VERSION = "1.0"           -- HTTP default version
+local ltn12 = require("luci.ltn12")
 
+HTTP_MAX_CONTENT      = 1024*8         -- 8 kB maximum content size
 
--- Decode an urlencoded string.
--- Returns the decoded value.
-function urldecode( str )
+--- 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 )
                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
 end
 
+--- 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 )
 
--- 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 = { }
+       local params = tbl or { }
 
        if url:find("?") then
                url = url:gsub( "^.+%?([^?]+)", "%1" )
        end
 
-       for i, pair in ipairs(luci.util.split( url, "[&;]+", nil, true )) do
+       for pair in url:gmatch( "[^&;]+" ) do
 
                -- find key and value
                local key = urldecode( pair:match("^([^=]+)")     )
@@ -74,9 +85,10 @@ function urldecode_params( url )
        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 )
@@ -87,7 +99,7 @@ function urlencode( str )
 
        if type(str) == "string" then
                str = str:gsub(
-                       "([^a-zA-Z0-9$_%-%.+!*'(),])",
+                       "([^a-zA-Z0-9$_%-%.%+!*'(),])",
                        __chrenc
                )
        end
@@ -95,478 +107,582 @@ 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
 
+-- (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] = ""
+       elseif type(tbl[key]) == "string" then
+               tbl[key] = { tbl[key], "" }
+       else
+               table.insert( tbl[key], "" )
+       end
+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, HTTP_MAX_READBUF )
-
-       -- 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, eol 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
+-- (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
+       else
+               tbl[key] = tbl[key] .. chunk
+       end
+end
 
-                       -- Append date to buffer
-                       buffer = buffer .. line
+-- (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
+                       tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
+               else
+                       tbl[key] = handler( tbl[key] )
                end
        end
-
-       return params
 end
 
 
+-- Table of our process states
+local process_states = { }
+
 -- 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 )
+-- Extracts the message type ("get", "post" or "response"), the requested uri
+-- or the status code if the line descripes a http response.
+process_states['magic'] = function( msg, chunk, err )
+
+       if chunk ~= nil then
+               -- ignore empty lines before request
+               if #chunk == 0 then
+                       return true, nil
+               end
 
-       for line in reader do
                -- Is it a request?
-               local method, uri = line:match("^([A-Z]+) ([^ ]+) HTTP/[01]%.[019]\r?\n$")
+               local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$")
 
                -- Yup, it is
                if method then
-                       return method:lower(), uri, nil
+
+                       msg.type           = "request"
+                       msg.request_method = method:lower()
+                       msg.request_uri    = uri
+                       msg.http_version   = tonumber( http_ver )
+                       msg.headers        = { }
+
+                       -- We're done, next state is header parsing
+                       return true, function( chunk )
+                               return process_states['headers']( msg, chunk )
+                       end
 
                -- Is it a response?
                else
-                       local code, message = line:match("^HTTP/[01]%.[019] ([0-9]+) ([^\r\n]+)\r?\n$")
+
+                       local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$")
 
                        -- Is a response
                        if code then
-                               return "response", code + 0, message
 
-                       -- Can't handle it
-                       else
-                               return nil
+                               msg.type           = "response"
+                               msg.status_code    = code
+                               msg.status_message = message
+                               msg.http_version   = tonumber( http_ver )
+                               msg.headers        = { }
+
+                               -- We're done, next state is header parsing
+                               return true, function( chunk )
+                                       return process_states['headers']( msg, chunk )
+                               end
                        end
                end
        end
+
+       -- Can't handle it
+       return nil, "Invalid HTTP message magic"
 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
+process_states['headers'] = function( msg, chunk )
 
-       -- Iterate line by line
-       for line in reader do
+       if chunk ~= nil then
 
                -- Look for a valid header format
-               local hdr, val = line:match( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r?\n$" )
+               local hdr, val = chunk:match( "^([A-Za-z][A-Za-z0-9%-_]+): +(.+)$" )
 
                if type(hdr) == "string" and hdr:len() > 0 and
                   type(val) == "string" and val:len() > 0
                then
-                       count = count + line:len()
-                       headers[hdr] = val
+                       msg.headers[hdr] = val
 
-               elseif line:match("^\r?\n$") then
-                       
-                       return count + line:len(), headers
+                       -- Valid header line, proceed
+                       return true, nil
 
+               elseif #chunk == 0 then
+                       -- Empty line, we won't accept data anymore
+                       return false, nil
                else
-                       -- junk data, don't add length
-                       return count, headers
+                       -- Junk data
+                       return nil, "Invalid HTTP header received"
                end
+       else
+               return nil, "Unexpected EOF"
        end
-
-       return count, headers
 end
 
 
--- Parse a http message
-function parse_message( data, filecb )
+--- 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()
 
-       local reader  = _linereader( data, HTTP_MAX_READBUF )
-       local message = parse_message_header( reader )
+               local chunk, err, part = sock:receive("*l")
 
-       if message then
-               parse_message_body( reader, message, 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, err
+                       end
 
-       return message
-end
+               -- Line ok
+               elseif chunk ~= nil then
 
+                       -- Strip trailing CR
+                       chunk = chunk:gsub("\r$","")
 
--- Parse a http message header
-function parse_message_header( data )
+                       return chunk, nil
+               end
+       end )
+end
 
-       -- Create a line reader
-       local reader  = _linereader( data, HTTP_MAX_READBUF )
-       local message = { }
+--- 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
+               msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
+       end
 
-       -- Try to extract magic
-       local method, arg1, arg2 = extract_magic( reader )
+       if not msg.mime_boundary then
+               return nil, "Invalid Content-Type found"
+       end
 
-       -- 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
+       local tlen   = 0
+       local inhdr  = false
+       local field  = nil
+       local store  = nil
+       local lchunk = nil
 
-               if method == "response" then
-                       message.type = "response"
-               else
-                       message.type = "request"
-               end
+       local function parse_headers( chunk, field )
 
-               -- Parse headers?
-               local hlen, hdrs = extract_headers( reader )
+               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
 
-               -- Valid headers?
-               if hlen > 2 and type(hdrs) == "table" then
+               chunk, stat = chunk:gsub("^\r\n","")
 
-                       message.headers = hdrs
+               -- End of headers
+               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
 
-                       -- 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 = { }
+                       if not field.headers["Content-Type"] then
+                               field.headers["Content-Type"] = "text/plain"
                        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
-                       }
+                       if field.name and field.file and filecb then
+                               __initval( msg.params, field.name )
+                               __appendval( msg.params, field.name, field.file )
 
-                       -- 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]
+                               store = filecb
+                       elseif field.name then
+                               __initval( msg.params, field.name )
 
-                               message.env[var] = val
+                               store = function( hdr, buf, eof )
+                                       __appendval( msg.params, field.name, buf )
+                               end
+                       else
+                               store = nil
                        end
 
-
-                       return message
+                       return chunk, true
                end
-       end
-end
 
+               return chunk, false
+       end
 
--- 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
+       local function snk( chunk )
 
-                       -- 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
+               tlen = tlen + ( chunk and #chunk or 0 )
 
-                       -- Is it x-www-form-urlencoded?
-                       elseif env.CONTENT_TYPE:match('^application/x%-www%-form%-urlencoded') then
+               if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
+                       return nil, "Message body size exceeds Content-Length"
+               end
 
-                               -- Read post data
-                               local post_data = ""
+               if chunk and not lchunk then
+                       lchunk = "\r\n" .. chunk
 
-                               for chunk, eol in reader do
+               elseif lchunk then
+                       local data = lchunk .. ( chunk or "" )
+                       local spos, epos, found
 
-                                       post_data = post_data .. chunk
+                       repeat
+                               spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
 
-                                       -- 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
+                               if not spos then
+                                       spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
                                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
+                               if spos then
+                                       local predata = data:sub( 1, spos - 1 )
 
-                               local len = 0
+                                       if inhdr then
+                                               predata, eof = parse_headers( predata, field )
 
-                               for chunk in reader do
+                                               if not eof then
+                                                       return nil, "Invalid MIME section header"
+                                               elseif not field.name then
+                                                       return nil, "Invalid Content-Disposition header"
+                                               end
+                                       end
 
-                                       len = len + #chunk
+                                       if store then
+                                               store( field, predata, true )
+                                       end
 
-                                       -- We have a callback, feed it.
-                                       if type(filecb) == "function" then
 
-                                               filecb( "_post", nil, chunk, false )
+                                       field = { headers = { } }
+                                       found = found or true
 
-                                       -- 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
+                                       data, eof = parse_headers( data:sub( epos + 1, #data ), field )
+                                       inhdr = not eof
                                end
+                       until not spos
 
-                               -- Send eof to callback
-                               if type(filecb) == "function" then
-                                       filecb( "_post", nil, "", true )
+                       if found then
+                               -- We found at least some boundary. Save
+                               -- the unparsed remaining data for the
+                               -- next chunk.
+                               lchunk, data = data, nil
+                       else
+                               -- There was a complete chunk without a boundary. Parse it as headers or
+                               -- append it as data, depending on our current state.
+                               if inhdr then
+                                       lchunk, eof = parse_headers( data, field )
+                                       inhdr = not eof
+                               else
+                                       -- We're inside data, so append the data. Note that we only append
+                                       -- lchunk, not all of data, since there is a chance that chunk
+                                       -- contains half a boundary. Assuming that each chunk is at least the
+                                       -- boundary in size, this should prevent problems
+                                       store( field, lchunk, false )
+                                       lchunk, chunk = chunk, nil
                                end
                        end
                end
+
+               return true
        end
+
+       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 )
 
--- Wrap given object into a line read iterator
-function _linereader( obj, bufsz )
+       local tlen   = 0
+       local lchunk = nil
 
-       bufsz = ( bufsz and bufsz >= 256 ) and bufsz or 256
+       local function snk( chunk )
 
-       local __read = function()  return nil end
-       local __eof  = function(x) return type(x) ~= "string" or #x == 0 end
+               tlen = tlen + ( chunk and #chunk or 0 )
 
-       local _pos = 1
-       local _buf = ""
-       local _eof = nil
+               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
 
-       -- object is string
-       if type(obj) == "string" then
+               if not lchunk and chunk then
+                       lchunk = chunk
 
-               __read = function() return obj:sub( _pos, _pos + bufsz - #_buf - 1 ) end
+               elseif lchunk then
+                       local data = lchunk .. ( chunk or "&" )
+                       local spos, epos
 
-       -- 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
+                       repeat
+                               spos, epos = data:find("^.-[;&]")
 
-               if type(obj.read) == "function" then
-                       __read = function() return obj:read( bufsz - #_buf ) end
-               else
-                       __read = function() return obj:receive( bufsz - #_buf ) end
-               end
+                               if spos then
+                                       local pair = data:sub( spos, epos - 1 )
+                                       local key  = pair:match("^(.-)=")
+                                       local val  = pair:match("=([^%s]*)%s*$")
 
-       -- object is a function
-       elseif type(obj) == "function" then
+                                       if key and #key > 0 then
+                                               __initval( msg.params, key )
+                                               __appendval( msg.params, key, val )
+                                               __finishval( msg.params, key, urldecode )
+                                       end
 
-               return obj
+                                       data = data:sub( epos + 1, #data )
+                               end
+                       until not spos
 
-       -- no usable data type
-       else
+                       lchunk = data
+               end
 
-               -- dummy iterator
-               return __read
+               return true
        end
 
+       return ltn12.pump.all( src, snk )
+end
 
-       -- generic block to line algorithm
-       return function()
-               if not _eof then
-                       local buffer = __read()
+--- 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 )
 
-                       if __eof( buffer ) then
-                               buffer = ""
-                       end
+       local ok   = true
+       local msg  = { }
+
+       local sink = ltn12.sink.simplify(
+               function( chunk )
+                       return process_states['magic']( msg, chunk )
+               end
+       )
+
+       -- Pump input data...
+       while ok do
 
-                       _pos   = _pos + #buffer
-                       buffer = _buf .. buffer
+               -- get data
+               ok, err = ltn12.pump.step( src, sink )
 
-                       local crlf, endpos = buffer:find("\r?\n")
+               -- error
+               if not ok and err then
+                       return nil, err
 
+               -- eof
+               elseif not ok then
 
-                       if crlf then
-                               _buf = buffer:sub( endpos + 1, #buffer )
-                               return buffer:sub( 1, endpos ), true
+                       -- Process get parameters
+                       if ( msg.request_method == "get" or msg.request_method == "post" ) and
+                          msg.request_uri:match("?")
+                       then
+                               msg.params = urldecode_params( msg.request_uri )
                        else
-                               -- check for eof
-                               _eof = __eof( buffer )
+                               msg.params = { }
+                       end
 
-                               -- clear overflow buffer
-                               _buf = ""
+                       -- Populate common environment variables
+                       msg.env = {
+                               CONTENT_LENGTH    = msg.headers['Content-Length'];
+                               CONTENT_TYPE      = msg.headers['Content-Type'] or 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);
+                               QUERY_STRING      = msg.request_uri:match("?")
+                                       and msg.request_uri:gsub("^.+?","") or ""
+                       }
+
+                       -- 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 = msg.headers[hdr]
 
-                               return buffer, false
+                               msg.env[var] = val
                        end
+               end
+       end
+
+       return msg
+end
+
+--- 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( 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:match("^application/x%-www%-form%-urlencoded")
+       then
+               return urldecode_message_body( src, msg, filecb )
+
+
+       -- Unhandled encoding
+       -- If a file callback is given then feed it chunk by chunk, else
+       -- store whole buffer in message.content
+       else
+
+               local sink
+
+               -- If we have a file callback then feed it
+               if type(filecb) == "function" then
+                       sink = filecb
+
+               -- ... else append to .content
                else
-                       return nil
+                       msg.content = ""
+                       msg.content_length = 0
+
+                       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( src, sink )
+
+                       if not ok and err then
+                               return nil, err
+                       elseif not err then
+                               return true
+                       end
+               end
+
+               return true
        end
 end
+
+--- Table containing human readable messages for several http status codes.
+-- @class table
+statusmsg = {
+       [200] = "OK",
+       [206] = "Partial Content",
+       [301] = "Moved Permanently",
+       [302] = "Found",
+       [304] = "Not Modified",
+       [400] = "Bad Request",
+       [403] = "Forbidden",
+       [404] = "Not Found",
+       [405] = "Method Not Allowed",
+       [408] = "Request Time-out",
+       [411] = "Length Required",
+       [412] = "Precondition Failed",
+       [416] = "Requested range not satisfiable",
+       [500] = "Internal Server Error",
+       [503] = "Server Unavailable",
+}