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