X-Git-Url: https://git.archive.openwrt.org/?p=project%2Fluci.git;a=blobdiff_plain;f=libs%2Fhttp%2Fluasrc%2Fhttp%2Fprotocol.lua;h=95712c9d9f8066dabd45c2d0632487232ee93f62;hp=8fb5e6f2f154d26339e727db9817f176d2bb44e3;hb=8c46333ed19507bde7b854789dcc726e0eed1d36;hpb=b84259d374b769efbec75ad47f1ea882bd782d5a diff --git a/libs/http/luasrc/http/protocol.lua b/libs/http/luasrc/http/protocol.lua index 8fb5e6f2f..95712c9d9 100644 --- a/libs/http/luasrc/http/protocol.lua +++ b/libs/http/luasrc/http/protocol.lua @@ -15,23 +15,27 @@ $Id$ module("luci.http.protocol", package.seeall) -require("ltn12") -require("luci.http.protocol.filter") +local ltn12 = require("luci.ltn12") HTTP_MAX_CONTENT = 1024*4 -- 4 kB maximum content size HTTP_URLENC_MAXKEYLEN = 1024 -- maximum allowd size of urlencoded parameter names +TSRC_BLOCKSIZE = 2048 -- target block size for throttling sources -- Decode an urlencoded string. -- Returns the decoded value. -function urldecode( str ) +function urldecode( str, no_plus ) local function __chrdec( hex ) return string.char( tonumber( hex, 16 ) ) end if type(str) == "string" then - str = str:gsub( "+", " " ):gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec ) + if not no_plus then + str = str:gsub( "+", " " ) + end + + str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec ) end return str @@ -84,7 +88,7 @@ function urlencode( str ) if type(str) == "string" then str = str:gsub( - "([^a-zA-Z0-9$_%-%.+!*'(),])", + "([^a-zA-Z0-9$_%-%.%+!*'(),])", __chrenc ) end @@ -108,6 +112,36 @@ function urlencode_params( tbl ) end +-- Parameter helper +local function __initval( tbl, key ) + if tbl[key] == nil then + tbl[key] = "" + elseif type(tbl[key]) == "string" then + tbl[key] = { tbl[key], "" } + else + table.insert( tbl[key], "" ) + end +end + +local function __appendval( tbl, key, chunk ) + if type(tbl[key]) == "table" then + tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk + else + tbl[key] = tbl[key] .. chunk + end +end + +local function __finishval( tbl, key, handler ) + if handler then + if type(tbl[key]) == "table" then + tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] ) + else + tbl[key] = handler( tbl[key] ) + end + end +end + + -- Table of our process states local process_states = { } @@ -195,187 +229,6 @@ process_states['headers'] = function( msg, chunk ) end --- Find first MIME boundary -process_states['mime-init'] = function( msg, chunk, filecb ) - - if chunk ~= nil then - if #chunk >= #msg.mime_boundary + 2 then - local boundary = chunk:sub( 1, #msg.mime_boundary + 4 ) - - if boundary == "--" .. msg.mime_boundary .. "\r\n" then - - -- 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 - else - return nil, "Invalid MIME boundary" - end - else - return true - end - else - return nil, "Unexpected EOF" - end -end - - --- Read MIME part headers -process_states['mime-headers'] = function( msg, chunk, filecb ) - - if chunk ~= nil then - - -- Combine look-behind buffer with current chunk - chunk = msg._mimebuffer .. chunk - - if not msg._mimeheaders then - msg._mimeheaders = { } - end - - local function __storehdr( k, v ) - msg._mimeheaders[k] = v - return "" - end - - -- 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 - - -- Headers processed, check for empty line - chunk, ok = chunk:gsub( "^\r\n", "" ) - - -- Store remaining buffer contents - msg._mimebuffer = chunk - - -- End of headers - if ok > 0 then - - -- When no Content-Type header is given assume text/plain - if not msg._mimeheaders['Content-Type'] then - msg._mimeheaders['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 - - -- Treat as form field - else - msg.params[field] = "" - msg._mimecallback = function(chunk,eof) - msg.params[field] = msg.params[field] .. chunk - end - end - - -- 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" - end - else - -- Content-Disposition is required, abort without - return nil, "Missing Content-Disposition MIME section header" - 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 - end - - -- Buffer looks like garbage - return nil, "Malformed MIME section header" - else - return nil, "Unexpected EOF" - end -end - - --- Read MIME part data -process_states['mime-data'] = function( msg, chunk, filecb ) - - if chunk ~= nil then - - -- Combine look-behind buffer with current chunk - local buffer = msg._mimebuffer .. chunk - - -- Look for MIME boundary - local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true ) - - if spos then - -- Content data - msg._mimecallback( buffer:sub( 1, spos - 1 ), true ) - - -- Store remainder - msg._mimebuffer = buffer:sub( epos + 1, #buffer ) - - -- Next state is mime-header processing - return true, function( chunk ) - return process_states['mime-headers']( msg, chunk, filecb ) - end - else - -- Look for EOF? - local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true ) - - if spos then - -- Content data - msg._mimecallback( buffer:sub( 1, spos - 1 ), true ) - - -- We processed the final MIME boundary, cleanup - msg._mimebuffer = nil - msg._mimeheaders = nil - msg._mimecallback = nil - - -- 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 - end - else - return nil, "Unexpected EOF" - end -end - - -- Init urldecoding stream process_states['urldecode-init'] = function( msg, chunk, filecb ) @@ -427,7 +280,6 @@ process_states['urldecode-key'] = function( msg, chunk, filecb ) local key = urldecode( buffer:sub( 1, spos - 1 ) ) -- Prepare buffers - msg.params[key] = "" msg._urldeclength = msg._urldeclength + epos msg._urldecbuffer = buffer:sub( epos + 1, #buffer ) @@ -437,12 +289,14 @@ process_states['urldecode-key'] = function( msg, chunk, filecb ) filecb( field, chunk, eof ) end else + __initval( msg.params, key ) + msg._urldeccallback = function( chunk, eof ) - msg.params[key] = msg.params[key] .. chunk + __appendval( msg.params, key, chunk ) -- FIXME: Use a filter if eof then - msg.params[key] = urldecode( msg.params[key] ) + __finishval( msg.params, key, urldecode ) end end end @@ -536,7 +390,7 @@ function header_source( sock ) if chunk == nil then if err ~= "timeout" then return nil, part - and "Line exceeds maximum allowed length["..part.."]" + and "Line exceeds maximum allowed length" or "Unexpected EOF" else return nil, err @@ -555,59 +409,151 @@ end -- Decode MIME encoded data. -function mimedecode_message_body( source, msg, filecb ) +function mimedecode_message_body( src, msg, filecb ) - -- Find mime boundary if msg and msg.env.CONTENT_TYPE then + msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$") + end - local bound = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)") + if not msg.mime_boundary then + return nil, "Invalid Content-Type found" + end - if bound then - msg.mime_boundary = bound - else - return nil, "No MIME boundary found or invalid content type given" + + local function parse_headers( chunk, field ) + + 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 + + chunk, stat = chunk:gsub("^\r\n","") + + -- 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 + + if not field.headers["Content-Type"] then + field.headers["Content-Type"] = "text/plain" + end + + return chunk, true end + + return chunk, false 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 - ) - -- 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() + local tlen = 0 + local inhdr = false + local field = nil + local store = nil + local lchunk = nil - -- 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() + local function snk( chunk ) + + tlen = tlen + ( chunk and #chunk or 0 ) + + if msg.env.CONTENT_LENGTH and tlen > msg.env.CONTENT_LENGTH then + return nil, "Message body size exceeds Content-Length" end - end - -- Pump input data... - while true do - -- get data - local ok, err = ltn12.pump.step( tsrc, sink ) + if chunk and not lchunk then + lchunk = "\r\n" .. chunk - -- error - if not ok and err then - return nil, err + elseif lchunk then + local data = lchunk .. ( chunk or "" ) + local spos, epos, found - -- eof - elseif not ok then - return true + repeat + spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true ) + + if not spos then + spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true ) + end + + + if spos then + local predata = data:sub( 1, spos - 1 ) + + if inhdr then + predata, eof = parse_headers( predata, field ) + + if not eof then + return nil, "Invalid MIME section header" + end + + if not field.name then + return nil, "Invalid Content-Disposition header" + end + end + + if store then + store( field.headers, predata, true ) + end + + + field = { headers = { } } + found = found or true + + data, eof = parse_headers( data:sub( epos + 1, #data ), field ) + inhdr = not eof + + if eof then + if field.file and filecb then + msg.params[field.name] = field.file + store = filecb + else + __initval( msg.params, field.name ) + + store = function( hdr, buf, eof ) + __appendval( msg.params, field.name, buf ) + end + end + end + end + until not spos + + + if found then + if #data > 78 then + lchunk = data:sub( #data - 78 + 1, #data ) + data = data:sub( 1, #data - 78 ) + + if store and field and field.name then + store( field.headers, data ) + else + return nil, "Invalid MIME section header" + end + else + lchunk, data = data, nil + end + else + if inhdr then + lchunk, eof = parse_headers( data, field ) + inhdr = not eof + else + store( field.headers, lchunk ) + lchunk, chunk = chunk, nil + end + end end + + return true end + + return luci.ltn12.pump.all( src, snk ) end @@ -625,7 +571,7 @@ function urldecode_message_body( source, msg ) -- Create a throttling LTN12 source -- See explaination in mimedecode_message_body(). local tsrc = function() - if msg._urldecbuffer ~= null and #msg._urldecbuffer > 0 then + if msg._urldecbuffer ~= nil and #msg._urldecbuffer > 0 then return "" else return source() @@ -685,7 +631,7 @@ function parse_message_header( source ) -- Populate common environment variables msg.env = { - CONTENT_LENGTH = msg.headers['Content-Length']; + CONTENT_LENGTH = tonumber(msg.headers['Content-Length']); CONTENT_TYPE = msg.headers['Content-Type']; REQUEST_METHOD = msg.request_method:upper(); REQUEST_URI = msg.request_uri;