X-Git-Url: https://git.archive.openwrt.org/?p=project%2Fluci.git;a=blobdiff_plain;f=libs%2Fhttp%2Fluasrc%2Fhttp%2Fprotocol.lua;h=acf47d23fd74b90c4c94bc5e646319d0f9e6d798;hp=01d3128b2525f4e9ffd9604f655054037598f442;hb=040efb954fd88f9c365dc9e55a6865b6da271704;hpb=e2e9e119d670ec80954fc8c018b479b218a7e47e diff --git a/libs/http/luasrc/http/protocol.lua b/libs/http/luasrc/http/protocol.lua index 01d3128b2..acf47d23f 100644 --- a/libs/http/luasrc/http/protocol.lua +++ b/libs/http/luasrc/http/protocol.lua @@ -15,23 +15,24 @@ $Id$ module("luci.http.protocol", package.seeall) -require("ltn12") -require("luci.util") - -HTTP_MAX_CONTENT = 1024*4 -- 4 kB maximum content size -HTTP_URLENC_MAXKEYLEN = 1024 -- maximum allowd size of urlencoded parameter names +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 ) +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 @@ -48,7 +49,7 @@ function urldecode_params( url, tbl ) 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("^([^=]+)") ) @@ -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,15 +109,49 @@ 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 = { } -- Extract "magic", the first line of a http message. -- 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 ) +process_states['magic'] = function( msg, chunk, err ) if chunk ~= nil then + -- ignore empty lines before request + if #chunk == 0 then + return true, nil + end -- Is it a request? local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$") @@ -127,7 +162,7 @@ process_states['magic'] = function( msg, chunk ) msg.type = "request" msg.request_method = method:lower() msg.request_uri = uri - msg.http_version = http_ver + msg.http_version = tonumber( http_ver ) msg.headers = { } -- We're done, next state is header parsing @@ -146,7 +181,7 @@ process_states['magic'] = function( msg, chunk ) msg.type = "response" msg.status_code = code msg.status_message = message - msg.http_version = http_ver + msg.http_version = tonumber( http_ver ) msg.headers = { } -- We're done, next state is header parsing @@ -191,437 +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 ) +-- Creates a header source from a given socket +function header_source( sock ) + return ltn12.source.simplify( function() - if boundary == "--" .. msg.mime_boundary .. "\r\n" then + local chunk, err, part = sock:receive("*l") - -- Store remaining data in buffer - msg._mimebuffer = chunk:sub( #msg.mime_boundary + 5, #chunk ) - - -- 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 ) + local function snk( chunk ) - if spos then - -- Content data - msg._mimecallback( buffer:sub( 1, spos - 1 ), true ) + tlen = tlen + ( chunk and #chunk or 0 ) - -- 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 - - -- 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 ) - - -- 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.headers['Content-Length'] then - msg.content_length = tonumber(msg.headers['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 spos then + local predata = data:sub( 1, spos - 1 ) - if chunk ~= nil then + if inhdr then + predata, eof = parse_headers( predata, field ) - -- Prevent oversized requests - if msg._urldeclength >= msg.content_length then - return nil, "Request exceeds maximum allowed size" - end + if not eof then + return nil, "Invalid MIME section header" + elseif not field.name then + return nil, "Invalid Content-Disposition header" + end + end + + if store then + store( field.headers, predata, true ) + end - -- Combine look-behind buffer with current chunk - local buffer = msg._urldecbuffer .. chunk - local spos, epos = buffer:find("=") - -- Found param - if spos then + field = { headers = { } } + found = found or true - -- 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 ) ) + data, eof = parse_headers( data:sub( epos + 1, #data ), field ) + inhdr = not eof + end + until not spos - -- Prepare buffers - msg.params[key] = "" - msg._urldeclength = msg._urldeclength + epos - msg._urldecbuffer = buffer:sub( epos + 1, #buffer ) + if found then + if #data > 78 then + lchunk = data:sub( #data - 78 + 1, #data ) + data = data:sub( 1, #data - 78 ) - -- Use file callback or store values inside msg.params - if filecb then - msg._urldeccallback = function( chunk, eof ) - filecb( field, chunk, eof ) + if store then + store( field.headers, data, false ) + else + return nil, "Invalid MIME section header" end else - msg._urldeccallback = function( chunk, eof ) - msg.params[key] = msg.params[key] .. chunk - end - end - - -- Proceed with urldecode-value state - return true, function( chunk ) - return process_states['urldecode-value']( msg, chunk, filecb ) + lchunk, data = data, nil end else - return nil, "POST parameter exceeds maximum allowed length" - end - else - return nil, "POST data exceeds maximum allowed length" - end - else - return nil, "Unexpected EOF" - end -end - - --- Process urldecoding stream, read parameter value -process_states['urldecode-value'] = function( msg, chunk, filecb ) - - if chunk ~= nil then - - -- Combine look-behind buffer with current chunk - local buffer = msg._urldecbuffer .. chunk - - -- Check for EOF - if #buffer == 0 then - -- Compare processed length - if msg._urldeclength == msg.content_length then - -- Cleanup - msg._urldeclength = nil - msg._urldecbuffer = nil - msg._urldeccallback = nil - - -- We won't accept data anymore - return false - else - return nil, "Content-Length mismatch" + 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 - return nil, "Unexpected EOF" + return true end -end + return ltn12.pump.all( src, snk ) +end --- Decode MIME encoded data. -function mimedecode_message_body( source, msg, filecb ) - - -- Find mime boundary - if msg and msg.headers['Content-Type'] then - - local bound = msg.headers['Content-Type']:match("^multipart/form%-data; boundary=(.+)") - - if bound then - msg.mime_boundary = bound - else - return nil, "No MIME boundary found or invalid content type given" - end - end - -- 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 ) - end - ) +-- Decode urlencoded data. +function urldecode_message_body( src, msg ) - -- 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 + local tlen = 0 + local lchunk = nil - -- Pump input data... - while true do - -- get data - local ok, err = ltn12.pump.step( tsrc, sink ) + local function snk( chunk ) - -- error - if not ok and err then - return nil, err + tlen = tlen + ( chunk and #chunk or 0 ) - -- eof - elseif not ok then - return true + if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then + return nil, "Message body size exceeds Content-Length" + elseif tlen > HTTP_MAX_CONTENT then + return nil, "Message body size exceeds maximum allowed length" end - end -end + if not lchunk and chunk then + lchunk = chunk --- Decode urlencoded data. -function urldecode_message_body( source, msg ) + elseif lchunk then + local data = lchunk .. ( chunk or "&" ) + local spos, epos - -- Create an initial LTN12 sink - -- Return the initial state. - local sink = ltn12.sink.simplify( - function( chunk ) - return process_states['urldecode-init']( msg, chunk ) - end - ) + repeat + spos, epos = data:find("^.-[;&]") - -- 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 - end + if spos then + local pair = data:sub( spos, epos - 1 ) + local key = pair:match("^(.-)=") + local val = pair:match("=(.*)$") - -- Pump input data... - while true do - -- get data - local ok, err = ltn12.pump.step( tsrc, sink ) + if key and #key > 0 then + __initval( msg.params, key ) + __appendval( msg.params, key, val ) + __finishval( msg.params, key, urldecode ) + end - -- step - if not ok and err then - return nil, err + data = data:sub( epos + 1, #data ) + end + until not spos - -- eof - elseif not ok then - return true + lchunk = data end - end -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 ) + return true end - return message + return ltn12.pump.all( src, snk ) end @@ -666,7 +493,8 @@ function parse_message_header( source ) REQUEST_METHOD = msg.request_method:upper(); REQUEST_URI = msg.request_uri; SCRIPT_NAME = msg.request_uri:gsub("?.+$",""); - SCRIPT_FILENAME = "" -- XXX implement me + SCRIPT_FILENAME = ""; -- XXX implement me + SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version) } -- Populate HTTP_* environment variables @@ -695,7 +523,6 @@ end -- Parse a http message body function parse_message_body( source, 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") @@ -707,16 +534,15 @@ function parse_message_body( source, msg, filecb ) elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded" then - return urldecode_message_body( source, msg, filecb ) + -- Unhandled encoding - -- If a file callback is given then feed it line by line, else + -- If a file callback is given then feed it chunk by chunk, else -- store whole buffer in message.content else local sink - local length = 0 -- If we have a file callback then feed it if type(filecb) == "function" then @@ -728,7 +554,7 @@ function parse_message_body( source, msg, filecb ) msg.content_length = 0 sink = function( chunk ) - if ( msg.content_length ) + #chunk <= HTTP_MAX_CONTENT then + if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then msg.content = msg.content .. chunk msg.content_length = msg.content_length + #chunk @@ -752,3 +578,18 @@ function parse_message_body( source, msg, filecb ) end end end + +-- Status codes +statusmsg = { + [200] = "OK", + [301] = "Moved Permanently", + [304] = "Not Modified", + [400] = "Bad Request", + [403] = "Forbidden", + [404] = "Not Found", + [405] = "Method Not Allowed", + [411] = "Length Required", + [412] = "Precondition Failed", + [500] = "Internal Server Error", + [503] = "Server Unavailable", +}