diff options
Diffstat (limited to 'libs/httpd/luasrc')
-rw-r--r-- | libs/httpd/luasrc/http/protocol.lua | 754 |
1 files changed, 0 insertions, 754 deletions
diff --git a/libs/httpd/luasrc/http/protocol.lua b/libs/httpd/luasrc/http/protocol.lua deleted file mode 100644 index 01d3128b2..000000000 --- a/libs/httpd/luasrc/http/protocol.lua +++ /dev/null @@ -1,754 +0,0 @@ ---[[ - -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$ - -]]-- - -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 - - --- Decode an urlencoded string. --- Returns the decoded value. -function urldecode( str ) - - 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 ) - end - - return str -end - - --- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url. --- Returns a table value with urldecoded values. -function urldecode_params( url, tbl ) - - 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 - - -- find key and value - local key = urldecode( pair:match("^([^=]+)") ) - local val = urldecode( pair:match("^[^=]+=(.+)$") ) - - -- store - if type(key) == "string" and key:len() > 0 then - if type(val) ~= "string" then val = "" end - - if not params[key] then - params[key] = val - elseif type(params[key]) ~= "table" then - params[key] = { params[key], val } - else - table.insert( params[key], val ) - end - end - end - - return params -end - - --- Encode given string in urlencoded format. --- Returns the encoded string. -function urlencode( str ) - - local function __chrenc( chr ) - return string.format( - "%%%02x", string.byte( chr ) - ) - end - - if type(str) == "string" then - str = str:gsub( - "([^a-zA-Z0-9$_%-%.+!*'(),])", - __chrenc - ) - end - - return str -end - - --- Encode given table to urlencoded string. --- Returns the encoded string. -function urlencode_params( tbl ) - local enc = "" - - for k, v in pairs(tbl) do - enc = enc .. ( enc and "&" or "" ) .. - urlencode(k) .. "=" .. - urlencode(v) - end - - return enc -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 ) - - if chunk ~= nil then - - -- Is it a request? - local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$") - - -- Yup, it is - if method then - - msg.type = "request" - msg.request_method = method:lower() - msg.request_uri = uri - msg.http_version = 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 http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$") - - -- Is a response - if code then - - msg.type = "response" - msg.status_code = code - msg.status_message = message - msg.http_version = 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. -process_states['headers'] = function( msg, chunk ) - - if chunk ~= nil then - - -- Look for a valid header format - local hdr, val = chunk:match( "^([A-Z][A-Za-z0-9%-_]+): +(.+)$" ) - - if type(hdr) == "string" and hdr:len() > 0 and - type(val) == "string" and val:len() > 0 - then - msg.headers[hdr] = val - - -- 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 - return nil, "Invalid HTTP header received" - end - else - return nil, "Unexpected EOF" - end -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 ) - - if chunk ~= nil then - - -- Check for Content-Length - if msg.headers['Content-Length'] then - msg.content_length = tonumber(msg.headers['Content-Length']) - - if msg.content_length <= HTTP_MAX_CONTENT then - -- Initialize buffer - msg._urldecbuffer = chunk - msg._urldeclength = 0 - - -- Switch to urldecode-key state - return true, function(chunk) - return process_states['urldecode-key']( msg, chunk, filecb ) - 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 chunk ~= nil then - - -- Prevent oversized requests - if msg._urldeclength >= msg.content_length then - return nil, "Request exceeds maximum allowed size" - end - - -- Combine look-behind buffer with current chunk - local buffer = msg._urldecbuffer .. chunk - local spos, epos = buffer:find("=") - - -- Found param - if spos then - - -- 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 ) ) - - -- Prepare buffers - msg.params[key] = "" - msg._urldeclength = msg._urldeclength + epos - msg._urldecbuffer = buffer:sub( epos + 1, #buffer ) - - -- Use file callback or store values inside msg.params - if filecb then - msg._urldeccallback = function( chunk, eof ) - filecb( field, chunk, eof ) - 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 ) - 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" - 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" - end -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 - ) - - -- 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 - - -- Pump input data... - while true do - -- get data - local ok, err = ltn12.pump.step( tsrc, sink ) - - -- error - if not ok and err then - return nil, err - - -- eof - elseif not ok then - return true - end - end -end - - --- Decode urlencoded data. -function urldecode_message_body( source, msg ) - - -- Create an initial LTN12 sink - -- Return the initial state. - local sink = ltn12.sink.simplify( - function( chunk ) - return process_states['urldecode-init']( msg, chunk ) - end - ) - - -- 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 - - -- Pump input data... - while true do - -- get data - local ok, err = ltn12.pump.step( tsrc, sink ) - - -- step - if not ok and err then - return nil, err - - -- eof - elseif not ok then - return true - 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 ) - end - - return message -end - - --- Parse a http message header -function parse_message_header( source ) - - 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 - - -- get data - ok, err = ltn12.pump.step( source, sink ) - - -- error - if not ok and err then - return nil, err - - -- eof - elseif not ok then - - -- 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 - msg.params = { } - end - - -- Populate common environment variables - msg.env = { - CONTENT_LENGTH = msg.headers['Content-Length']; - CONTENT_TYPE = 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 - } - - -- 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] - - msg.env[var] = val - end - end - end - - return msg -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") - then - - return mimedecode_message_body( source, 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 == "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 - -- 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 - sink = filecb - - -- ... else append to .content - else - msg.content = "" - msg.content_length = 0 - - sink = function( chunk ) - 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 - end - - -- Pump data... - while true do - local ok, err = ltn12.pump.step( source, sink ) - - if not ok and err then - return nil, err - elseif not err then - return true - end - end - end -end |