Files
2026-07-13 12:32:21 +08:00

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,
}