With only the decoder routines remaining in luci.http.protocol, it makes no sense to keep the low level protocol class around, so fold the remaining code into the central luci.http class. Also adjust the few direct users of luci.http.protocol accordingly. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
539 lines
13 KiB
Lua
539 lines
13 KiB
Lua
-- Copyright 2008 Steven Barth <steven@midlink.org>
|
|
-- Copyright 2010-2018 Jo-Philipp Wich <jo@mein.io>
|
|
-- Licensed to the public under the Apache License 2.0.
|
|
|
|
local util = require "luci.util"
|
|
local coroutine = require "coroutine"
|
|
local table = require "table"
|
|
local lhttp = require "lucihttp"
|
|
local nixio = require "nixio"
|
|
local ltn12 = require "luci.ltn12"
|
|
|
|
local table, ipairs, pairs, type, tostring, tonumber, error =
|
|
table, ipairs, pairs, type, tostring, tonumber, error
|
|
|
|
module "luci.http"
|
|
|
|
HTTP_MAX_CONTENT = 1024*8 -- 8 kB maximum content size
|
|
|
|
context = util.threadlocal()
|
|
|
|
Request = util.class()
|
|
function Request.__init__(self, env, sourcein, sinkerr)
|
|
self.input = sourcein
|
|
self.error = sinkerr
|
|
|
|
|
|
-- File handler nil by default to let .content() work
|
|
self.filehandler = nil
|
|
|
|
-- HTTP-Message table
|
|
self.message = {
|
|
env = env,
|
|
headers = {},
|
|
params = urldecode_params(env.QUERY_STRING or ""),
|
|
}
|
|
|
|
self.parsed_input = false
|
|
end
|
|
|
|
function Request.formvalue(self, name, noparse)
|
|
if not noparse and not self.parsed_input then
|
|
self:_parse_input()
|
|
end
|
|
|
|
if name then
|
|
return self.message.params[name]
|
|
else
|
|
return self.message.params
|
|
end
|
|
end
|
|
|
|
function Request.formvaluetable(self, prefix)
|
|
local vals = {}
|
|
prefix = prefix and prefix .. "." or "."
|
|
|
|
if not self.parsed_input then
|
|
self:_parse_input()
|
|
end
|
|
|
|
local void = self.message.params[nil]
|
|
for k, v in pairs(self.message.params) do
|
|
if k:find(prefix, 1, true) == 1 then
|
|
vals[k:sub(#prefix + 1)] = tostring(v)
|
|
end
|
|
end
|
|
|
|
return vals
|
|
end
|
|
|
|
function Request.content(self)
|
|
if not self.parsed_input then
|
|
self:_parse_input()
|
|
end
|
|
|
|
return self.message.content, self.message.content_length
|
|
end
|
|
|
|
function Request.getcookie(self, name)
|
|
return lhttp.header_attribute("cookie; " .. (self:getenv("HTTP_COOKIE") or ""), name)
|
|
end
|
|
|
|
function Request.getenv(self, name)
|
|
if name then
|
|
return self.message.env[name]
|
|
else
|
|
return self.message.env
|
|
end
|
|
end
|
|
|
|
function Request.setfilehandler(self, callback)
|
|
self.filehandler = callback
|
|
|
|
if not self.parsed_input then
|
|
return
|
|
end
|
|
|
|
-- If input has already been parsed then uploads are stored as unlinked
|
|
-- temporary files pointed to by open file handles in the parameter
|
|
-- value table. Loop all params, and invoke the file callback for any
|
|
-- param with an open file handle.
|
|
local name, value
|
|
for name, value in pairs(self.message.params) do
|
|
if type(value) == "table" then
|
|
while value.fd do
|
|
local data = value.fd:read(1024)
|
|
local eof = (not data or data == "")
|
|
|
|
callback(value, data, eof)
|
|
|
|
if eof then
|
|
value.fd:close()
|
|
value.fd = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Request._parse_input(self)
|
|
parse_message_body(
|
|
self.input,
|
|
self.message,
|
|
self.filehandler
|
|
)
|
|
self.parsed_input = true
|
|
end
|
|
|
|
function close()
|
|
if not context.eoh then
|
|
context.eoh = true
|
|
coroutine.yield(3)
|
|
end
|
|
|
|
if not context.closed then
|
|
context.closed = true
|
|
coroutine.yield(5)
|
|
end
|
|
end
|
|
|
|
function content()
|
|
return context.request:content()
|
|
end
|
|
|
|
function formvalue(name, noparse)
|
|
return context.request:formvalue(name, noparse)
|
|
end
|
|
|
|
function formvaluetable(prefix)
|
|
return context.request:formvaluetable(prefix)
|
|
end
|
|
|
|
function getcookie(name)
|
|
return context.request:getcookie(name)
|
|
end
|
|
|
|
-- or the environment table itself.
|
|
function getenv(name)
|
|
return context.request:getenv(name)
|
|
end
|
|
|
|
function setfilehandler(callback)
|
|
return context.request:setfilehandler(callback)
|
|
end
|
|
|
|
function header(key, value)
|
|
if not context.headers then
|
|
context.headers = {}
|
|
end
|
|
context.headers[key:lower()] = value
|
|
coroutine.yield(2, key, value)
|
|
end
|
|
|
|
function prepare_content(mime)
|
|
if not context.headers or not context.headers["content-type"] then
|
|
if mime == "application/xhtml+xml" then
|
|
if not getenv("HTTP_ACCEPT") or
|
|
not getenv("HTTP_ACCEPT"):find("application/xhtml+xml", nil, true) then
|
|
mime = "text/html; charset=UTF-8"
|
|
end
|
|
header("Vary", "Accept")
|
|
end
|
|
header("Content-Type", mime)
|
|
end
|
|
end
|
|
|
|
function source()
|
|
return context.request.input
|
|
end
|
|
|
|
function status(code, message)
|
|
code = code or 200
|
|
message = message or "OK"
|
|
context.status = code
|
|
coroutine.yield(1, code, message)
|
|
end
|
|
|
|
-- This function is as a valid LTN12 sink.
|
|
-- If the content chunk is nil this function will automatically invoke close.
|
|
function write(content, src_err)
|
|
if not content then
|
|
if src_err then
|
|
error(src_err)
|
|
else
|
|
close()
|
|
end
|
|
return true
|
|
elseif #content == 0 then
|
|
return true
|
|
else
|
|
if not context.eoh then
|
|
if not context.status then
|
|
status()
|
|
end
|
|
if not context.headers or not context.headers["content-type"] then
|
|
header("Content-Type", "text/html; charset=utf-8")
|
|
end
|
|
if not context.headers["cache-control"] then
|
|
header("Cache-Control", "no-cache")
|
|
header("Expires", "0")
|
|
end
|
|
if not context.headers["x-frame-options"] then
|
|
header("X-Frame-Options", "SAMEORIGIN")
|
|
end
|
|
if not context.headers["x-xss-protection"] then
|
|
header("X-XSS-Protection", "1; mode=block")
|
|
end
|
|
if not context.headers["x-content-type-options"] then
|
|
header("X-Content-Type-Options", "nosniff")
|
|
end
|
|
|
|
context.eoh = true
|
|
coroutine.yield(3)
|
|
end
|
|
coroutine.yield(4, content)
|
|
return true
|
|
end
|
|
end
|
|
|
|
function splice(fd, size)
|
|
coroutine.yield(6, fd, size)
|
|
end
|
|
|
|
function redirect(url)
|
|
if url == "" then url = "/" end
|
|
status(302, "Found")
|
|
header("Location", url)
|
|
close()
|
|
end
|
|
|
|
function build_querystring(q)
|
|
local s, n, k, v = {}, 1, nil, nil
|
|
|
|
for k, v in pairs(q) do
|
|
s[n+0] = (n == 1) and "?" or "&"
|
|
s[n+1] = util.urlencode(k)
|
|
s[n+2] = "="
|
|
s[n+3] = util.urlencode(v)
|
|
n = n + 4
|
|
end
|
|
|
|
return table.concat(s, "")
|
|
end
|
|
|
|
urldecode = util.urldecode
|
|
|
|
urlencode = util.urlencode
|
|
|
|
function write_json(x)
|
|
util.serialize_json(x, write)
|
|
end
|
|
|
|
-- from given url or string. Returns a table with urldecoded values.
|
|
-- Simple parameters are stored as string values associated with the parameter
|
|
-- name within the table. Parameters with multiple values are stored as array
|
|
-- containing the corresponding values.
|
|
function urldecode_params(url, tbl)
|
|
local parser, name
|
|
local params = tbl or { }
|
|
|
|
parser = lhttp.urlencoded_parser(function (what, buffer, length)
|
|
if what == parser.TUPLE then
|
|
name, value = nil, nil
|
|
elseif what == parser.NAME then
|
|
name = lhttp.urldecode(buffer)
|
|
elseif what == parser.VALUE and name then
|
|
params[name] = lhttp.urldecode(buffer) or ""
|
|
end
|
|
|
|
return true
|
|
end)
|
|
|
|
if parser then
|
|
parser:parse((url or ""):match("[^?]*$"))
|
|
parser:parse(nil)
|
|
end
|
|
|
|
return params
|
|
end
|
|
|
|
-- separated by "&". Tables are encoded as parameters with multiple values by
|
|
-- repeating the parameter name with each value.
|
|
function urlencode_params(tbl)
|
|
local k, v
|
|
local n, enc = 1, {}
|
|
for k, v in pairs(tbl) do
|
|
if type(v) == "table" then
|
|
local i, v2
|
|
for i, v2 in ipairs(v) do
|
|
if enc[1] then
|
|
enc[n] = "&"
|
|
n = n + 1
|
|
end
|
|
|
|
enc[n+0] = lhttp.urlencode(k)
|
|
enc[n+1] = "="
|
|
enc[n+2] = lhttp.urlencode(v2)
|
|
n = n + 3
|
|
end
|
|
else
|
|
if enc[1] then
|
|
enc[n] = "&"
|
|
n = n + 1
|
|
end
|
|
|
|
enc[n+0] = lhttp.urlencode(k)
|
|
enc[n+1] = "="
|
|
enc[n+2] = lhttp.urlencode(v)
|
|
n = n + 3
|
|
end
|
|
end
|
|
|
|
return table.concat(enc, "")
|
|
end
|
|
|
|
-- Content-Type. Stores all extracted data associated with its parameter name
|
|
-- in the params table within the given message object. Multiple parameter
|
|
-- values are stored as tables, ordinary ones as strings.
|
|
-- If an optional file callback function is given then it is feeded with the
|
|
-- file contents chunk by chunk and only the extracted file name is stored
|
|
-- within the params table. The callback function will be called subsequently
|
|
-- with three arguments:
|
|
-- o Table containing decoded (name, file) and raw (headers) mime header data
|
|
-- o String value containing a chunk of the file data
|
|
-- o Boolean which indicates wheather the current chunk is the last one (eof)
|
|
function mimedecode_message_body(src, msg, file_cb)
|
|
local parser, header, field
|
|
local len, maxlen = 0, tonumber(msg.env.CONTENT_LENGTH or nil)
|
|
|
|
parser, err = lhttp.multipart_parser(msg.env.CONTENT_TYPE, function (what, buffer, length)
|
|
if what == parser.PART_INIT then
|
|
field = { }
|
|
|
|
elseif what == parser.HEADER_NAME then
|
|
header = buffer:lower()
|
|
|
|
elseif what == parser.HEADER_VALUE and header then
|
|
if header:lower() == "content-disposition" and
|
|
lhttp.header_attribute(buffer, nil) == "form-data"
|
|
then
|
|
field.name = lhttp.header_attribute(buffer, "name")
|
|
field.file = lhttp.header_attribute(buffer, "filename")
|
|
end
|
|
|
|
if field.headers then
|
|
field.headers[header] = buffer
|
|
else
|
|
field.headers = { [header] = buffer }
|
|
end
|
|
|
|
elseif what == parser.PART_BEGIN then
|
|
return not field.file
|
|
|
|
elseif what == parser.PART_DATA and field.name and length > 0 then
|
|
if field.file then
|
|
if file_cb then
|
|
file_cb(field, buffer, false)
|
|
msg.params[field.name] = msg.params[field.name] or field
|
|
else
|
|
if not field.fd then
|
|
field.fd = nixio.mkstemp(field.name)
|
|
end
|
|
|
|
if field.fd then
|
|
field.fd:write(buffer)
|
|
msg.params[field.name] = msg.params[field.name] or field
|
|
end
|
|
end
|
|
else
|
|
field.value = buffer
|
|
end
|
|
|
|
elseif what == parser.PART_END and field.name then
|
|
if field.file and msg.params[field.name] then
|
|
if file_cb then
|
|
file_cb(field, "", true)
|
|
elseif field.fd then
|
|
field.fd:seek(0, "set")
|
|
end
|
|
else
|
|
msg.params[field.name] = field.value or ""
|
|
end
|
|
|
|
field = nil
|
|
|
|
elseif what == parser.ERROR then
|
|
err = buffer
|
|
end
|
|
|
|
return true
|
|
end)
|
|
|
|
return ltn12.pump.all(src, function (chunk)
|
|
len = len + (chunk and #chunk or 0)
|
|
|
|
if maxlen and len > maxlen + 2 then
|
|
return nil, "Message body size exceeds Content-Length"
|
|
end
|
|
|
|
if not parser or not parser:parse(chunk) then
|
|
return nil, err
|
|
end
|
|
|
|
return true
|
|
end)
|
|
end
|
|
|
|
-- Content-Type. Stores all extracted data associated with its parameter name
|
|
-- in the params table within the given message object. Multiple parameter
|
|
-- values are stored as tables, ordinary ones as strings.
|
|
function urldecode_message_body(src, msg)
|
|
local err, name, value, parser
|
|
local len, maxlen = 0, tonumber(msg.env.CONTENT_LENGTH or nil)
|
|
|
|
parser = lhttp.urlencoded_parser(function (what, buffer, length)
|
|
if what == parser.TUPLE then
|
|
name, value = nil, nil
|
|
elseif what == parser.NAME then
|
|
name = lhttp.urldecode(buffer)
|
|
elseif what == parser.VALUE and name then
|
|
msg.params[name] = lhttp.urldecode(buffer) or ""
|
|
elseif what == parser.ERROR then
|
|
err = buffer
|
|
end
|
|
|
|
return true
|
|
end)
|
|
|
|
return ltn12.pump.all(src, function (chunk)
|
|
len = len + (chunk and #chunk or 0)
|
|
|
|
if maxlen and len > maxlen + 2 then
|
|
return nil, "Message body size exceeds Content-Length"
|
|
elseif len > HTTP_MAX_CONTENT then
|
|
return nil, "Message body size exceeds maximum allowed length"
|
|
end
|
|
|
|
if not parser or not parser:parse(chunk) then
|
|
return nil, err
|
|
end
|
|
|
|
return true
|
|
end)
|
|
end
|
|
|
|
-- This function will examine the Content-Type within the given message object
|
|
-- to select the appropriate content decoder.
|
|
-- Currently the application/x-www-urlencoded and application/form-data
|
|
-- mime types are supported. If the encountered content encoding can't be
|
|
-- handled then the whole message body will be stored unaltered as "content"
|
|
-- property within the given message object.
|
|
function parse_message_body(src, msg, filecb)
|
|
local ctype = lhttp.header_attribute(msg.env.CONTENT_TYPE, nil)
|
|
|
|
-- Is it multipart/mime ?
|
|
if msg.env.REQUEST_METHOD == "POST" and
|
|
ctype == "multipart/form-data"
|
|
then
|
|
return mimedecode_message_body(src, msg, filecb)
|
|
|
|
-- Is it application/x-www-form-urlencoded ?
|
|
elseif msg.env.REQUEST_METHOD == "POST" and
|
|
ctype == "application/x-www-form-urlencoded"
|
|
then
|
|
return urldecode_message_body(src, msg)
|
|
|
|
|
|
-- Unhandled encoding
|
|
-- If a file callback is given then feed it chunk by chunk, else
|
|
-- store whole buffer in message.content
|
|
else
|
|
|
|
local sink
|
|
|
|
-- If we have a file callback then feed it
|
|
if type(filecb) == "function" then
|
|
local meta = {
|
|
name = "raw",
|
|
encoding = msg.env.CONTENT_TYPE
|
|
}
|
|
sink = function( chunk )
|
|
if chunk then
|
|
return filecb(meta, chunk, false)
|
|
else
|
|
return filecb(meta, nil, true)
|
|
end
|
|
end
|
|
-- ... else append to .content
|
|
else
|
|
msg.content = ""
|
|
msg.content_length = 0
|
|
|
|
sink = function( chunk )
|
|
if chunk then
|
|
if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
|
|
msg.content = msg.content .. chunk
|
|
msg.content_length = msg.content_length + #chunk
|
|
return true
|
|
else
|
|
return nil, "POST data exceeds maximum allowed length"
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- Pump data...
|
|
while true do
|
|
local ok, err = ltn12.pump.step( src, sink )
|
|
|
|
if not ok and err then
|
|
return nil, err
|
|
elseif not ok then -- eof
|
|
return true
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
end
|