From: Steven Barth Date: Fri, 20 Jun 2008 16:47:25 +0000 (+0000) Subject: * Moved luci.http.protocol to libs/http X-Git-Tag: 0.8.0~793 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=e2e9e119d670ec80954fc8c018b479b218a7e47e;p=oweals%2Fluci.git * Moved luci.http.protocol to libs/http * Added ltn12 to libs/core * Fixed libs/httpd Makefile --- diff --git a/NOTICE b/NOTICE index e2c3383e3..7d9ed9801 100644 --- a/NOTICE +++ b/NOTICE @@ -6,6 +6,7 @@ Licensed under the Apache License, Version 2.0. Contains code from: BinDecHex - Copyright 2007 Tim Kelly/Dialectronics coxpcall - Copyright 2005 - Kepler Project (www.keplerproject.org) +ltn12/luasocket - Copyright 2004-2007 Diego Nehab Luci-Statistics - Statistics for LuCI diff --git a/contrib/package/luci/Makefile b/contrib/package/luci/Makefile index b2571f750..8b03f7956 100644 --- a/contrib/package/luci/Makefile +++ b/contrib/package/luci/Makefile @@ -132,9 +132,19 @@ define Package/luci-fastindex/install endef +define Package/luci-http + $(call Package/luci/libtemplate) + TITLE:=HTTP Protocol implementation +endef + +define Package/luci-http/install + $(call Package/luci/install/template,$(1),libs/http) +endef + + define Package/luci-web $(call Package/luci/libtemplate) - DEPENDS+=+luci-addons +luci-uci + DEPENDS+=+luci-http +luci-addons +luci-uci TITLE:=MVC Webframework endef @@ -384,6 +394,9 @@ endif ifneq ($(CONFIG_PACKAGE_luci-fastindex),) PKG_SELECTED_MODULES+=libs/fastindex endif +ifneq ($(CONFIG_PACKAGE_luci-http),) + PKG_SELECTED_MODULES+=libs/http +endif ifneq ($(CONFIG_PACKAGE_luci-uci),) PKG_SELECTED_MODULES+=libs/uci endif @@ -455,6 +468,7 @@ MAKE_FLAGS += MODULES="$(PKG_SELECTED_MODULES)" LUA_TARGET="$(LUA_TARGET)" CFLAG $(eval $(call BuildPackage,luci-core)) $(eval $(call BuildPackage,luci-cbi)) $(eval $(call BuildPackage,luci-fastindex)) +$(eval $(call BuildPackage,luci-http)) $(eval $(call BuildPackage,luci-uci)) $(eval $(call BuildPackage,luci-web)) diff --git a/libs/core/lua/ltn12.lua b/libs/core/lua/ltn12.lua new file mode 100644 index 000000000..417da84d5 --- /dev/null +++ b/libs/core/lua/ltn12.lua @@ -0,0 +1,314 @@ +--[[ +LuaSocket 2.0.2 license +Copyright � 2004-2007 Diego Nehab + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +]]-- +----------------------------------------------------------------------------- +-- LTN12 - Filters, sources, sinks and pumps. +-- LuaSocket toolkit. +-- Author: Diego Nehab +-- RCS ID: $Id: ltn12.lua,v 1.31 2006/04/03 04:45:42 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module +----------------------------------------------------------------------------- +local string = require("string") +local table = require("table") +local base = _G +module("ltn12") + +filter = {} +source = {} +sink = {} +pump = {} + +-- 2048 seems to be better in windows... +BLOCKSIZE = 2048 +_VERSION = "LTN12 1.0.1" + +----------------------------------------------------------------------------- +-- Filter stuff +----------------------------------------------------------------------------- +-- returns a high level filter that cycles a low-level filter +function filter.cycle(low, ctx, extra) + base.assert(low) + return function(chunk) + local ret + ret, ctx = low(ctx, chunk, extra) + return ret + end +end + +-- chains a bunch of filters together +-- (thanks to Wim Couwenberg) +function filter.chain(...) + local n = table.getn(arg) + local top, index = 1, 1 + local retry = "" + return function(chunk) + retry = chunk and retry + while true do + if index == top then + chunk = arg[index](chunk) + if chunk == "" or top == n then return chunk + elseif chunk then index = index + 1 + else + top = top+1 + index = top + end + else + chunk = arg[index](chunk or "") + if chunk == "" then + index = index - 1 + chunk = retry + elseif chunk then + if index == n then return chunk + else index = index + 1 end + else base.error("filter returned inappropriate nil") end + end + end + end +end + +----------------------------------------------------------------------------- +-- Source stuff +----------------------------------------------------------------------------- +-- create an empty source +local function empty() + return nil +end + +function source.empty() + return empty +end + +-- returns a source that just outputs an error +function source.error(err) + return function() + return nil, err + end +end + +-- creates a file source +function source.file(handle, io_err) + if handle then + return function() + local chunk = handle:read(BLOCKSIZE) + if not chunk then handle:close() end + return chunk + end + else return source.error(io_err or "unable to open file") end +end + +-- turns a fancy source into a simple source +function source.simplify(src) + base.assert(src) + return function() + local chunk, err_or_new = src() + src = err_or_new or src + if not chunk then return nil, err_or_new + else return chunk end + end +end + +-- creates string source +function source.string(s) + if s then + local i = 1 + return function() + local chunk = string.sub(s, i, i+BLOCKSIZE-1) + i = i + BLOCKSIZE + if chunk ~= "" then return chunk + else return nil end + end + else return source.empty() end +end + +-- creates rewindable source +function source.rewind(src) + base.assert(src) + local t = {} + return function(chunk) + if not chunk then + chunk = table.remove(t) + if not chunk then return src() + else return chunk end + else + table.insert(t, chunk) + end + end +end + +function source.chain(src, f) + base.assert(src and f) + local last_in, last_out = "", "" + local state = "feeding" + local err + return function() + if not last_out then + base.error('source is empty!', 2) + end + while true do + if state == "feeding" then + last_in, err = src() + if err then return nil, err end + last_out = f(last_in) + if not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + elseif last_out ~= "" then + state = "eating" + if last_in then last_in = "" end + return last_out + end + else + last_out = f(last_in) + if last_out == "" then + if last_in == "" then + state = "feeding" + else + base.error('filter returned ""') + end + elseif not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + else + return last_out + end + end + end + end +end + +-- creates a source that produces contents of several sources, one after the +-- other, as if they were concatenated +-- (thanks to Wim Couwenberg) +function source.cat(...) + local src = table.remove(arg, 1) + return function() + while src do + local chunk, err = src() + if chunk then return chunk end + if err then return nil, err end + src = table.remove(arg, 1) + end + end +end + +----------------------------------------------------------------------------- +-- Sink stuff +----------------------------------------------------------------------------- +-- creates a sink that stores into a table +function sink.table(t) + t = t or {} + local f = function(chunk, err) + if chunk then table.insert(t, chunk) end + return 1 + end + return f, t +end + +-- turns a fancy sink into a simple sink +function sink.simplify(snk) + base.assert(snk) + return function(chunk, err) + local ret, err_or_new = snk(chunk, err) + if not ret then return nil, err_or_new end + snk = err_or_new or snk + return 1 + end +end + +-- creates a file sink +function sink.file(handle, io_err) + if handle then + return function(chunk, err) + if not chunk then + handle:close() + return 1 + else return handle:write(chunk) end + end + else return sink.error(io_err or "unable to open file") end +end + +-- creates a sink that discards data +local function null() + return 1 +end + +function sink.null() + return null +end + +-- creates a sink that just returns an error +function sink.error(err) + return function() + return nil, err + end +end + +-- chains a sink with a filter +function sink.chain(f, snk) + base.assert(f and snk) + return function(chunk, err) + if chunk ~= "" then + local filtered = f(chunk) + local done = chunk and "" + while true do + local ret, snkerr = snk(filtered, err) + if not ret then return nil, snkerr end + if filtered == done then return 1 end + filtered = f(done) + end + else return 1 end + end +end + +----------------------------------------------------------------------------- +-- Pump stuff +----------------------------------------------------------------------------- +-- pumps one chunk from the source to the sink +function pump.step(src, snk) + local chunk, src_err = src() + local ret, snk_err = snk(chunk, src_err) + if chunk and ret then return 1 + else return nil, src_err or snk_err end +end + +-- pumps all data from a source to a sink, using a step function +function pump.all(src, snk, step) + base.assert(src and snk) + step = step or pump.step + while true do + local ret, err = step(src, snk) + if not ret then + if err then return nil, err + else return 1 end + end + end +end + diff --git a/libs/http/Makefile b/libs/http/Makefile new file mode 100644 index 000000000..f7fac7740 --- /dev/null +++ b/libs/http/Makefile @@ -0,0 +1,2 @@ +include ../../build/config.mk +include ../../build/module.mk diff --git a/libs/http/luasrc/http/protocol.lua b/libs/http/luasrc/http/protocol.lua new file mode 100644 index 000000000..01d3128b2 --- /dev/null +++ b/libs/http/luasrc/http/protocol.lua @@ -0,0 +1,754 @@ +--[[ + +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$ + +]]-- + +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 diff --git a/libs/httpd/Makefile b/libs/httpd/Makefile index ee1a40ea8..f7fac7740 100644 --- a/libs/httpd/Makefile +++ b/libs/httpd/Makefile @@ -1,13 +1,2 @@ -include ../../build/module.mk include ../../build/config.mk -include ../../build/gccconfig.mk - -%.o: %.c - $(COMPILE) $(LUA_CFLAGS) $(FPIC) -c -o $@ $< - -compile: src/fastindex.o - mkdir -p dist$(LUCI_LIBRARYDIR) - $(LINK) $(SHLIB_FLAGS) -o dist$(LUCI_LIBRARYDIR)/fastindex.so src/fastindex.o $(LUA_SHLIBS) - -clean: - rm -f src/*.o +include ../../build/module.mk 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 - -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