418 lines
11 KiB
Lua
418 lines
11 KiB
Lua
local cjson_safe = require "cjson.safe"
|
|
local cjson = require "cjson"
|
|
local ws_server = require "resty.websocket.server"
|
|
local pl_file = require "pl.file"
|
|
local strip = require("kong.tools.string").strip
|
|
local splitn = require("kong.tools.string").splitn
|
|
|
|
|
|
local kong = {
|
|
table = require("kong.pdk.table").new()
|
|
}
|
|
|
|
local ocsp_status = "good"
|
|
|
|
local function parse_multipart_form_params(body, content_type)
|
|
if not content_type then
|
|
return nil, 'missing content-type'
|
|
end
|
|
|
|
local m, err = ngx.re.match(content_type, "boundary=(.+)", "oj")
|
|
if not m or not m[1] or err then
|
|
return nil, "could not find boundary in content type " .. content_type ..
|
|
"error: " .. tostring(err)
|
|
end
|
|
|
|
local boundary = m[1]
|
|
local parts_split, parts_count = splitn(body, '--' .. boundary)
|
|
local params = {}
|
|
local part, from, to, part_value, part_name, part_headers, first_header
|
|
for i = 1, parts_count do
|
|
part = strip(parts_split[i])
|
|
|
|
if part ~= '' and part ~= '--' then
|
|
from, to, err = ngx.re.find(part, '^\\r$', 'ojm')
|
|
if err or (not from and not to) then
|
|
return nil, nil, "could not find part body. Error: " .. tostring(err)
|
|
end
|
|
|
|
part_value = part:sub(to + 2, #part) -- +2: trim leading line jump
|
|
part_headers = part:sub(1, from - 1)
|
|
local part_headers_t = splitn(part_headers, '\\n')
|
|
first_header = part_headers_t[1]
|
|
if first_header:lower():sub(1, 19) == "content-disposition" then
|
|
local m, err = ngx.re.match(first_header, 'name="(.*?)"', "oj")
|
|
|
|
if err or not m or not m[1] then
|
|
return nil, "could not parse part name. Error: " .. tostring(err)
|
|
end
|
|
|
|
part_name = m[1]
|
|
else
|
|
return nil, "could not find part name in: " .. part_headers
|
|
end
|
|
|
|
params[part_name] = part_value
|
|
end
|
|
end
|
|
|
|
return params
|
|
end
|
|
|
|
|
|
local function send_text_response(text, content_type, headers)
|
|
headers = headers or {}
|
|
content_type = content_type or "text/plain"
|
|
|
|
text = ngx.req.get_method() == "HEAD" and "" or tostring(text)
|
|
|
|
ngx.header["X-Powered-By"] = "mock_upstream"
|
|
ngx.header["Server"] = "mock-upstream/1.0.0"
|
|
ngx.header["Content-Length"] = #text + 1
|
|
ngx.header["Content-Type"] = content_type
|
|
|
|
for header,value in pairs(headers) do
|
|
if type(value) == "table" then
|
|
ngx.header[header] = table.concat(value, ", ")
|
|
else
|
|
ngx.header[header] = value
|
|
end
|
|
end
|
|
|
|
return ngx.say(text)
|
|
end
|
|
|
|
|
|
local function filter_access_by_method(method)
|
|
if ngx.req.get_method() ~= method then
|
|
ngx.status = ngx.HTTP_NOT_ALLOWED
|
|
send_text_response("Method not allowed for the requested URL")
|
|
return ngx.exit(ngx.OK)
|
|
end
|
|
end
|
|
|
|
|
|
local function find_http_credentials(authorization_header)
|
|
if not authorization_header then
|
|
return
|
|
end
|
|
|
|
local iterator, iter_err = ngx.re.gmatch(authorization_header,
|
|
"\\s*[Bb]asic\\s*(.+)")
|
|
if not iterator then
|
|
ngx.log(ngx.ERR, iter_err)
|
|
return
|
|
end
|
|
|
|
local m, err = iterator()
|
|
|
|
if err then
|
|
ngx.log(ngx.ERR, err)
|
|
return
|
|
end
|
|
|
|
if m and m[1] then
|
|
local decoded_basic = ngx.decode_base64(m[1])
|
|
|
|
if decoded_basic then
|
|
local user_pass = splitn(decoded_basic, ":", 3)
|
|
return user_pass[1], user_pass[2]
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function filter_access_by_basic_auth(expected_username,
|
|
expected_password)
|
|
local headers = ngx.req.get_headers(0)
|
|
|
|
local username, password =
|
|
find_http_credentials(headers["proxy-authorization"])
|
|
|
|
if not username then
|
|
username, password =
|
|
find_http_credentials(headers["authorization"])
|
|
end
|
|
|
|
if username ~= expected_username or password ~= expected_password then
|
|
ngx.header["WWW-Authenticate"] = "mock_upstream"
|
|
ngx.header["X-Powered-By"] = "mock_upstream"
|
|
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
|
end
|
|
end
|
|
|
|
|
|
local function get_ngx_vars()
|
|
local var = ngx.var
|
|
return {
|
|
uri = var.uri,
|
|
host = var.host,
|
|
hostname = var.hostname,
|
|
https = var.https,
|
|
scheme = var.scheme,
|
|
is_args = var.is_args,
|
|
server_addr = var.server_addr,
|
|
server_port = var.server_port,
|
|
server_name = var.server_name,
|
|
server_protocol = var.server_protocol,
|
|
remote_addr = var.remote_addr,
|
|
remote_port = var.remote_port,
|
|
realip_remote_addr = var.realip_remote_addr,
|
|
realip_remote_port = var.realip_remote_port,
|
|
binary_remote_addr = var.binary_remote_addr,
|
|
request = var.request,
|
|
request_uri = var.request_uri,
|
|
request_time = var.request_time,
|
|
request_length = var.request_length,
|
|
request_method = var.request_method,
|
|
bytes_received = var.bytes_received,
|
|
ssl_server_name = var.ssl_server_name or "no SNI",
|
|
}
|
|
end
|
|
|
|
|
|
local function get_body_data()
|
|
local req = ngx.req
|
|
|
|
req.read_body()
|
|
local data = req.get_body_data()
|
|
if data then
|
|
return data
|
|
end
|
|
|
|
local file_path = req.get_body_file()
|
|
if file_path then
|
|
local file = io.open(file_path, "r")
|
|
data = file:read("*all")
|
|
file:close()
|
|
return data
|
|
end
|
|
|
|
return ""
|
|
end
|
|
|
|
local function get_post_data(content_type)
|
|
local text = get_body_data()
|
|
local kind = "unknown"
|
|
local params = cjson_safe.null
|
|
local err
|
|
|
|
if type(content_type) == "string" then
|
|
if content_type:find("application/x-www-form-urlencoded", nil, true) then
|
|
|
|
kind = "form"
|
|
params, err = ngx.req.get_post_args(0)
|
|
|
|
elseif content_type:find("multipart/form-data", nil, true) then
|
|
kind = "multipart-form"
|
|
params, err = parse_multipart_form_params(text, content_type)
|
|
|
|
elseif content_type:find("application/json", nil, true) then
|
|
kind = "json"
|
|
params, err = cjson_safe.decode(text)
|
|
end
|
|
|
|
params = params or cjson_safe.null
|
|
|
|
if err then
|
|
kind = kind .. " (error)"
|
|
err = tostring(err)
|
|
end
|
|
end
|
|
|
|
return { text = text, kind = kind, params = params, error = err }
|
|
end
|
|
|
|
|
|
local function get_default_json_response()
|
|
local headers = ngx.req.get_headers(0)
|
|
local vars = get_ngx_vars()
|
|
|
|
return {
|
|
headers = headers,
|
|
post_data = get_post_data(headers["Content-Type"]),
|
|
url = ("%s://%s:%s%s"):format(vars.scheme, vars.host,
|
|
vars.server_port, vars.request_uri),
|
|
uri_args = ngx.req.get_uri_args(0),
|
|
vars = vars,
|
|
}
|
|
end
|
|
|
|
|
|
local function send_default_json_response(extra_fields, response_headers)
|
|
local tbl = kong.table.merge(get_default_json_response(), extra_fields)
|
|
return send_text_response(cjson.encode(tbl),
|
|
"application/json", response_headers)
|
|
end
|
|
|
|
|
|
local function serve_web_sockets()
|
|
local wb, err = ws_server:new({
|
|
timeout = 5000,
|
|
max_payload_len = 65535,
|
|
})
|
|
|
|
if not wb then
|
|
ngx.log(ngx.ERR, "failed to open websocket: ", err)
|
|
return ngx.exit(444)
|
|
end
|
|
|
|
while true do
|
|
local data, typ, err = wb:recv_frame()
|
|
if wb.fatal then
|
|
ngx.log(ngx.ERR, "failed to receive frame: ", err)
|
|
return ngx.exit(444)
|
|
end
|
|
|
|
if data then
|
|
if typ == "close" then
|
|
break
|
|
end
|
|
|
|
if typ == "ping" then
|
|
local bytes, err = wb:send_pong(data)
|
|
if not bytes then
|
|
ngx.log(ngx.ERR, "failed to send pong: ", err)
|
|
return ngx.exit(444)
|
|
end
|
|
|
|
elseif typ == "pong" then
|
|
ngx.log(ngx.INFO, "client ponged")
|
|
|
|
elseif typ == "text" then
|
|
local bytes, err = wb:send_text(data)
|
|
if not bytes then
|
|
ngx.log(ngx.ERR, "failed to send text: ", err)
|
|
return ngx.exit(444)
|
|
end
|
|
end
|
|
|
|
else
|
|
local bytes, err = wb:send_ping()
|
|
if not bytes then
|
|
ngx.log(ngx.ERR, "failed to send ping: ", err)
|
|
return ngx.exit(444)
|
|
end
|
|
end
|
|
end
|
|
|
|
wb:send_close()
|
|
end
|
|
|
|
|
|
local function get_logger()
|
|
local logger = ngx.shared.kong_mock_upstream_loggers
|
|
if not logger then
|
|
error("missing 'kong_mock_upstream_loggers' shm declaration")
|
|
end
|
|
|
|
return logger
|
|
end
|
|
|
|
|
|
local function store_log(logname)
|
|
ngx.req.read_body()
|
|
|
|
local raw_entries = ngx.req.get_body_data()
|
|
local logger = get_logger()
|
|
|
|
local entries = cjson.decode(raw_entries)
|
|
if #entries == 0 then
|
|
-- backwards-compatibility for `conf.queue_size == 1`
|
|
entries = { entries }
|
|
end
|
|
|
|
local log_req_headers = ngx.req.get_headers(0)
|
|
|
|
for i = 1, #entries do
|
|
local store = {
|
|
entry = entries[i],
|
|
log_req_headers = log_req_headers,
|
|
}
|
|
|
|
assert(logger:rpush(logname, cjson.encode(store)))
|
|
assert(logger:incr(logname .. "|count", 1, 0))
|
|
end
|
|
|
|
ngx.status = 200
|
|
end
|
|
|
|
|
|
local function retrieve_log(logname)
|
|
local logger = get_logger()
|
|
local len = logger:llen(logname)
|
|
local entries = {}
|
|
|
|
for i = 1, len do
|
|
local encoded_stored = assert(logger:lpop(logname))
|
|
local stored = cjson.decode(encoded_stored)
|
|
entries[i] = stored.entry
|
|
entries[i].log_req_headers = stored.log_req_headers
|
|
assert(logger:rpush(logname, encoded_stored))
|
|
end
|
|
|
|
local count, err = logger:get(logname .. "|count")
|
|
if err then
|
|
error(err)
|
|
end
|
|
|
|
ngx.status = 200
|
|
ngx.say(cjson.encode({
|
|
entries = entries,
|
|
count = count,
|
|
}))
|
|
end
|
|
|
|
|
|
local function count_log(logname)
|
|
local logger = get_logger()
|
|
local count = assert(logger:get(logname .. "|count"))
|
|
|
|
ngx.status = 200
|
|
ngx.say(count)
|
|
end
|
|
|
|
|
|
local function reset_log(logname)
|
|
local logger = get_logger()
|
|
logger:delete(logname)
|
|
logger:delete(logname .. "|count")
|
|
end
|
|
|
|
|
|
local function handle_ocsp()
|
|
if ocsp_status == "good" then
|
|
ngx.print(pl_file.read(ngx.config.prefix() .. "/../spec/fixtures/ocsp_certs/resp-good.dat"))
|
|
|
|
elseif ocsp_status == "revoked" then
|
|
ngx.print(pl_file.read(ngx.config.prefix() .. "/../spec/fixtures/ocsp_certs/resp-revoked.dat"))
|
|
|
|
elseif ocsp_status == "error" then
|
|
ngx.exit(500)
|
|
|
|
else
|
|
assert("unknown ocsp_status:" ..ocsp_status)
|
|
end
|
|
end
|
|
|
|
|
|
local function set_ocsp(status)
|
|
ocsp_status = status
|
|
end
|
|
|
|
|
|
return {
|
|
get_default_json_response = get_default_json_response,
|
|
filter_access_by_method = filter_access_by_method,
|
|
filter_access_by_basic_auth = filter_access_by_basic_auth,
|
|
send_text_response = send_text_response,
|
|
send_default_json_response = send_default_json_response,
|
|
serve_web_sockets = serve_web_sockets,
|
|
store_log = store_log,
|
|
retrieve_log = retrieve_log,
|
|
count_log = count_log,
|
|
reset_log = reset_log,
|
|
handle_ocsp = handle_ocsp,
|
|
set_ocsp = set_ocsp,
|
|
}
|