X-Git-Url: https://git.archive.openwrt.org/?p=project%2Fluci.git;a=blobdiff_plain;f=libs%2Fweb%2Fluasrc%2Fhttp%2Fprotocol.lua;h=0d41550b23709029935b6aec1a2136febec0d587;hp=6901291b9ad69b5feefe4a91a6cee2a37d13e0eb;hb=bbb44cf245c11bc0c1d59e836007c9e8c3bfa209;hpb=dfe85d7e518cc35a55e372b0ac31345788a486a8 diff --git a/libs/web/luasrc/http/protocol.lua b/libs/web/luasrc/http/protocol.lua index 6901291b9..0d41550b2 100644 --- a/libs/web/luasrc/http/protocol.lua +++ b/libs/web/luasrc/http/protocol.lua @@ -1,57 +1,68 @@ ---[[ - +--[[ + HTTP protocol implementation for LuCI -(c) 2008 Freifunk Leipzig / Jo-Philipp Wich - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -$Id$ - -]]-- +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich -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", +}