| -- |
| -- Licensed to the Apache Software Foundation (ASF) under one or more |
| -- contributor license agreements. See the NOTICE file distributed with |
| -- this work for additional information regarding copyright ownership. |
| -- The ASF licenses this file to You 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 |
| -- |
| -- Unless required by applicable law or agreed to in writing, software |
| -- distributed under the License is distributed on an "AS IS" BASIS, |
| -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| -- See the License for the specific language governing permissions and |
| -- limitations under the License. |
| -- |
| local json_decode = require("toolkit.json").decode |
| local json_encode = require("toolkit.json").encode |
| |
| local rsa_public_key = [[ |
| -----BEGIN PUBLIC KEY----- |
| MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw86xcJwNxL2MkWnjIGiw |
| 94QY78Sq89dLqMdV/Ku2GIX9lYkbS0VDGtmxDGJLBOYW4cKTX+pigJyzglLgE+nD |
| z3VJf2oCqSV74gTyEdi7sw9e1rCyR6dR8VA7LEpIHwmhnDhhjXy1IYSKRdiVHLS5 |
| sYmaAGckpUo3MLqUrgydGj5tFzvK/R/ELuZBdlZM+XuWxYry05r860E3uL+VdVCO |
| oU4RJQknlJnTRd7ht8KKcZb6uM14C057i26zX/xnOJpaVflA4EyEo99hKQAdr8Sh |
| G70MOLYvGCZxl1o8S3q4X67MxcPlfJaXnbog2AOOGRaFar88XiLFWTbXMCLuz7xD |
| zQIDAQAB |
| -----END PUBLIC KEY-----]] |
| |
| local rsa_private_key = [[ |
| -----BEGIN PRIVATE KEY----- |
| MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDzrFwnA3EvYyR |
| aeMgaLD3hBjvxKrz10uox1X8q7YYhf2ViRtLRUMa2bEMYksE5hbhwpNf6mKAnLOC |
| UuAT6cPPdUl/agKpJXviBPIR2LuzD17WsLJHp1HxUDssSkgfCaGcOGGNfLUhhIpF |
| 2JUctLmxiZoAZySlSjcwupSuDJ0aPm0XO8r9H8Qu5kF2Vkz5e5bFivLTmvzrQTe4 |
| v5V1UI6hThElCSeUmdNF3uG3wopxlvq4zXgLTnuLbrNf/Gc4mlpV+UDgTISj32Ep |
| AB2vxKEbvQw4ti8YJnGXWjxLerhfrszFw+V8lpeduiDYA44ZFoVqvzxeIsVZNtcw |
| Iu7PvEPNAgMBAAECggEAVpyN9m7A1F631/aLheFpLgMbeKt4puV7zQtnaJ2XrZ9P |
| PR7pmNDpTu4uF3k/D8qrIm+L+uhVa+hkquf3wDct6w1JVnfQ93riImbnoKdK13ic |
| DcEZCwLjByfjFMNCxZ/gAZca55fbExlqhFy6EHmMjhB8s2LsXcTHRuGxNI/Vyi49 |
| sxECibe0U53aqdJbVWrphIS67cpwl4TUkN6mrHsNuDYNJ9dgkpapoqp4FTFQsBqC |
| afOK5qgJ68dWZ47FBUng+AZjdCncqAIuJxxItGVQP6YPsFs+OXcivIVHJr363TpC |
| l85FfdvqWV5OGBbwSKhNwiTNUVvfSQVmtURGWG/HbQKBgQD4gZ1z9+Lx19kT9WTz |
| lw93lxso++uhAPDTKviyWSRoEe5aN3LCd4My+/Aj+sk4ON/s2BV3ska5Im93j+vC |
| rCv3uPn1n2jUhWuJ3bDqipeTW4n/CQA2m/8vd26TMk22yOkkqw2MIA8sjJ//SD7g |
| tdG7up6DgGMP4hgbO89uGU7DAwKBgQDJtkKd0grh3u52Foeh9YaiAgYRwc65IE16 |
| UyD1OJxIuX/dYQDLlo5KyyngFa1ZhWIs7qC7r3xXH+10kfJY+Q+5YMjmZjlL8SR1 |
| Ujqd02R9F2//6OeswyReachJZbZdtiEw3lPa4jVFYfhSe0M2ZPxMwvoXb25eyCNI |
| 1lYjSKq87wKBgHnLTNghjeDp4UKe6rNYPgRm0rDrhziJtX5JeUov1mALKb6dnmkh |
| GfRK9g8sQqKDfXwfC6Z2gaMK9YaryujGaWYoCpoPXtmJ6oLPXH4XHuLh4mhUiP46 |
| xn8FEfSimuQS4/FMxH8A128GHQSI7AhGFFzlwfrBWcvXC+mNDsTvMmLxAoGARc+4 |
| upppfccETQZ7JsitMgD1TMwA2f2eEwoWTAitvlXFNT9PYSbYVHaAJbga6PLLCbYF |
| FzAjHpxEOKYSdEyu7n/ayDL0/Z2V+qzc8KarDsg/0RgwppBbU/nUgeKb/U79qcYo |
| y4ai3UKNCS70Ei1dTMvmdpnwXwlxfNIBufB6dy0CgYBMYq9Lc31GkC6PcGEEbx6W |
| vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK |
| Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S |
| kEJQcmfVew5mFXyxuEn3zA== |
| -----END PRIVATE KEY-----]] |
| |
| local _M = {} |
| |
| |
| local function inject_headers() |
| local hdrs = ngx.req.get_headers() |
| for k, v in pairs(hdrs) do |
| if k:sub(1, 5) == "resp-" then |
| ngx.header[k:sub(6)] = v |
| end |
| end |
| end |
| |
| |
| function _M.hello() |
| ngx.req.read_body() |
| local s = "hello world" |
| ngx.header['Content-Length'] = #s + 1 |
| ngx.say(s) |
| end |
| |
| |
| function _M.hello_chunked() |
| ngx.print("hell") |
| ngx.flush(true) |
| ngx.print("o w") |
| ngx.flush(true) |
| ngx.say("orld") |
| end |
| |
| |
| function _M.hello1() |
| ngx.say("hello1 world") |
| end |
| |
| |
| -- Fake endpoint, needed for testing authz-keycloak plugin. |
| function _M.course_foo() |
| ngx.say("course foo") |
| end |
| |
| |
| function _M.server_port() |
| ngx.print(ngx.var.server_port) |
| end |
| _M.server_port_route2 = _M.server_port |
| _M.server_port_hello = _M.server_port |
| _M.server_port_aa = _M.server_port |
| |
| |
| function _M.limit_conn() |
| ngx.sleep(0.3) |
| ngx.say("hello world") |
| end |
| |
| |
| function _M.plugin_proxy_rewrite() |
| ngx.say("uri: ", ngx.var.uri) |
| ngx.say("host: ", ngx.var.host) |
| ngx.say("scheme: ", ngx.var.scheme) |
| ngx.log(ngx.WARN, "plugin_proxy_rewrite get method: ", ngx.req.get_method()) |
| end |
| |
| |
| function _M.plugin_proxy_rewrite_args() |
| ngx.say("uri: ", ngx.var.uri) |
| local args = ngx.req.get_uri_args() |
| |
| local keys = {} |
| for k, _ in pairs(args) do |
| table.insert(keys, k) |
| end |
| table.sort(keys) |
| |
| for _, key in ipairs(keys) do |
| if type(args[key]) == "table" then |
| ngx.say(key, ": ", table.concat(args[key], ',')) |
| else |
| ngx.say(key, ": ", args[key]) |
| end |
| end |
| end |
| |
| |
| function _M.specific_status() |
| local status = ngx.var.http_x_test_upstream_status |
| if status ~= nil then |
| ngx.status = status |
| ngx.say("upstream status: ", status) |
| end |
| end |
| |
| |
| function _M.status() |
| ngx.log(ngx.WARN, "client request host: ", ngx.var.http_host) |
| ngx.say("ok") |
| end |
| |
| |
| function _M.ewma() |
| if ngx.var.server_port == "1981" |
| or ngx.var.server_port == "1982" then |
| ngx.sleep(0.2) |
| else |
| ngx.sleep(0.1) |
| end |
| ngx.print(ngx.var.server_port) |
| end |
| |
| |
| local builtin_hdr_ignore_list = { |
| ["x-forwarded-for"] = true, |
| ["x-forwarded-proto"] = true, |
| ["x-forwarded-host"] = true, |
| ["x-forwarded-port"] = true, |
| } |
| |
| function _M.uri() |
| ngx.say("uri: ", ngx.var.uri) |
| local headers = ngx.req.get_headers() |
| |
| local keys = {} |
| for k in pairs(headers) do |
| if not builtin_hdr_ignore_list[k] then |
| table.insert(keys, k) |
| end |
| end |
| table.sort(keys) |
| |
| for _, key in ipairs(keys) do |
| ngx.say(key, ": ", headers[key]) |
| end |
| end |
| _M.uri_plugin_proxy_rewrite = _M.uri |
| _M.uri_plugin_proxy_rewrite_args = _M.uri |
| |
| |
| function _M.old_uri() |
| ngx.say("uri: ", ngx.var.uri) |
| local headers = ngx.req.get_headers() |
| |
| local keys = {} |
| for k in pairs(headers) do |
| table.insert(keys, k) |
| end |
| table.sort(keys) |
| |
| for _, key in ipairs(keys) do |
| ngx.say(key, ": ", headers[key]) |
| end |
| end |
| |
| |
| function _M.opentracing() |
| ngx.say("opentracing") |
| end |
| |
| |
| function _M.with_header() |
| --split into multiple chunk |
| ngx.say("hello") |
| ngx.say("world") |
| ngx.say("!") |
| end |
| |
| |
| function _M.mock_zipkin() |
| ngx.req.read_body() |
| local data = ngx.req.get_body_data() |
| ngx.log(ngx.NOTICE, data) |
| |
| local spans = json_decode(data) |
| local ver = ngx.req.get_uri_args()['span_version'] |
| if ver == "1" then |
| if #spans ~= 5 then |
| ngx.log(ngx.ERR, "wrong number of spans: ", #spans) |
| ngx.exit(400) |
| end |
| else |
| if #spans ~= 3 then |
| -- request/proxy/response |
| ngx.log(ngx.ERR, "wrong number of spans: ", #spans) |
| ngx.exit(400) |
| end |
| end |
| |
| for _, span in pairs(spans) do |
| local prefix = string.sub(span.name, 1, 6) |
| if prefix ~= 'apisix' then |
| ngx.log(ngx.ERR, "wrong prefix of name", prefix) |
| ngx.exit(400) |
| end |
| if not span.traceId then |
| ngx.log(ngx.ERR, "missing trace id") |
| ngx.exit(400) |
| end |
| |
| if not span.localEndpoint then |
| ngx.log(ngx.ERR, "missing local endpoint") |
| ngx.exit(400) |
| end |
| |
| if span.localEndpoint.serviceName ~= 'APISIX' |
| and span.localEndpoint.serviceName ~= 'apisix' then |
| ngx.log(ngx.ERR, "wrong serviceName: ", span.localEndpoint.serviceName) |
| ngx.exit(400) |
| end |
| |
| if span.localEndpoint.port ~= 1984 then |
| ngx.log(ngx.ERR, "wrong port: ", span.localEndpoint.port) |
| ngx.exit(400) |
| end |
| |
| local server_addr = ngx.req.get_uri_args()['server_addr'] |
| if server_addr then |
| if span.localEndpoint.ipv4 ~= server_addr then |
| ngx.log(ngx.ERR, "server_addr mismatched") |
| ngx.exit(400) |
| end |
| end |
| |
| end |
| end |
| |
| |
| function _M.wolf_rbac_login_rest() |
| ngx.req.read_body() |
| local data = ngx.req.get_body_data() |
| local args = json_decode(data) |
| if not args.username then |
| ngx.say(json_encode({ok=false, reason="ERR_USERNAME_MISSING"})) |
| ngx.exit(0) |
| end |
| if not args.password then |
| ngx.say(json_encode({ok=false, reason="ERR_PASSWORD_MISSING"})) |
| ngx.exit(0) |
| end |
| if args.username ~= "admin" then |
| ngx.say(json_encode({ok=false, reason="ERR_USER_NOT_FOUND"})) |
| ngx.exit(0) |
| end |
| if args.password ~= "123456" then |
| ngx.say(json_encode({ok=false, reason="ERR_PASSWORD_ERROR"})) |
| ngx.exit(0) |
| end |
| |
| ngx.say(json_encode({ok=true, data={token="wolf-rbac-token", |
| userInfo={nickname="administrator",username="admin", id="100"}}})) |
| end |
| |
| |
| function _M.wolf_rbac_access_check() |
| local headers = ngx.req.get_headers() |
| local token = headers['x-rbac-token'] |
| if token ~= 'wolf-rbac-token' then |
| ngx.say(json_encode({ok=false, reason="ERR_TOKEN_INVALID"})) |
| ngx.exit(0) |
| end |
| |
| local args = ngx.req.get_uri_args() |
| local resName = args.resName |
| ngx.log(ngx.WARN, "wolf_rbac_access_check clientIP: ", args.clientIP or "") |
| if resName == '/hello' or resName == '/wolf/rbac/custom/headers' then |
| ngx.say(json_encode({ok=true, |
| data={ userInfo={nickname="administrator", |
| username="admin", id="100"} }})) |
| elseif resName == '/hello/500' then |
| ngx.status = 500 |
| ngx.say(json_encode({ok=false, reason="ERR_SERVER_ERROR"})) |
| elseif resName == '/hello/401' then |
| ngx.status = 401 |
| ngx.say(json_encode({ok=false, reason="ERR_TOKEN_INVALID"})) |
| else |
| ngx.status = 403 |
| ngx.say(json_encode({ok=false, reason="ERR_ACCESS_DENIED"})) |
| end |
| end |
| |
| |
| function _M.wolf_rbac_user_info() |
| local headers = ngx.req.get_headers() |
| local token = headers['x-rbac-token'] |
| if token ~= 'wolf-rbac-token' then |
| ngx.say(json_encode({ok=false, reason="ERR_TOKEN_INVALID"})) |
| ngx.exit(0) |
| end |
| |
| ngx.say(json_encode({ok=true, |
| data={ userInfo={nickname="administrator", username="admin", id="100"} }})) |
| end |
| |
| |
| function _M.wolf_rbac_change_pwd() |
| ngx.req.read_body() |
| local data = ngx.req.get_body_data() |
| local args = json_decode(data) |
| if args.oldPassword ~= "123456" then |
| ngx.say(json_encode({ok=false, reason="ERR_OLD_PASSWORD_INCORRECT"})) |
| ngx.exit(0) |
| end |
| |
| ngx.say(json_encode({ok=true, data={ }})) |
| end |
| |
| |
| function _M.wolf_rbac_custom_headers() |
| local headers = ngx.req.get_headers() |
| ngx.say('id:' .. headers['X-UserId'] .. ',username:' .. headers['X-Username'] |
| .. ',nickname:' .. headers['X-Nickname']) |
| end |
| |
| |
| function _M.websocket_handshake() |
| local websocket = require "resty.websocket.server" |
| local wb, err = websocket:new() |
| if not wb then |
| ngx.log(ngx.ERR, "failed to new websocket: ", err) |
| return ngx.exit(400) |
| end |
| |
| local bytes, err = wb:send_text("hello") |
| if not bytes then |
| ngx.log(ngx.ERR, "failed to send text: ", err) |
| return ngx.exit(444) |
| end |
| end |
| _M.websocket_handshake_route = _M.websocket_handshake |
| |
| |
| function _M.api_breaker() |
| ngx.exit(tonumber(ngx.var.arg_code)) |
| end |
| |
| |
| function _M.mysleep() |
| ngx.sleep(tonumber(ngx.var.arg_seconds)) |
| if ngx.var.arg_abort then |
| ngx.exit(ngx.ERROR) |
| else |
| ngx.say(ngx.var.arg_seconds) |
| end |
| end |
| |
| |
| local function print_uri() |
| ngx.say(ngx.var.uri) |
| end |
| for i = 1, 100 do |
| _M["print_uri_" .. i] = print_uri |
| end |
| |
| function _M.print_uri_detailed() |
| ngx.say("ngx.var.uri: ", ngx.var.uri) |
| ngx.say("ngx.var.request_uri: ", ngx.var.request_uri) |
| end |
| |
| -- echo back exactly what the upstream received: the full request URI (with |
| -- query string) and every request header. Lets tests assert on what was |
| -- actually proxied upstream instead of scanning the error log. |
| function _M.print_request_received() |
| ngx.say("request_uri: ", ngx.var.request_uri) |
| local headers = ngx.req.get_headers() |
| local keys = {} |
| for k in pairs(headers) do |
| keys[#keys + 1] = k |
| end |
| table.sort(keys) |
| for _, k in ipairs(keys) do |
| ngx.say(k, ": ", headers[k]) |
| end |
| end |
| |
| function _M.headers() |
| local args = ngx.req.get_uri_args() |
| for name, val in pairs(args) do |
| ngx.header[name] = nil |
| ngx.header[name] = val |
| end |
| |
| ngx.say("/headers") |
| end |
| |
| |
| function _M.echo() |
| ngx.req.read_body() |
| local hdrs = ngx.req.get_headers() |
| for k, v in pairs(hdrs) do |
| ngx.header[k] = v |
| end |
| ngx.print(ngx.req.get_body_data() or "") |
| end |
| |
| _M.v1_responses = _M.echo |
| |
| |
| function _M.log() |
| ngx.req.read_body() |
| local body = ngx.req.get_body_data() |
| local ct = ngx.var.content_type |
| if ct ~= "text/plain" then |
| body = json_decode(body) |
| body = json_encode(body) |
| end |
| ngx.log(ngx.WARN, "request log: ", body or "nil") |
| end |
| |
| |
| function _M.server_error() |
| error("500 Internal Server Error") |
| end |
| |
| |
| function _M.log_request() |
| ngx.log(ngx.WARN, "uri: ", ngx.var.uri) |
| local headers = ngx.req.get_headers() |
| |
| local keys = {} |
| for k in pairs(headers) do |
| table.insert(keys, k) |
| end |
| table.sort(keys) |
| |
| for _, key in ipairs(keys) do |
| ngx.log(ngx.WARN, key, ": ", headers[key]) |
| end |
| end |
| |
| |
| function _M.v3_auth_authenticate() |
| ngx.log(ngx.WARN, "etcd auth failed!") |
| end |
| |
| |
| function _M._well_known_openid_configuration() |
| local t = require("lib.test_admin") |
| local openid_data = t.read_file("t/plugin/openid-connect/configuration.json") |
| ngx.say(openid_data) |
| end |
| |
| function _M.google_logging_token() |
| local args = ngx.req.get_uri_args() |
| local args_token_type = args.token_type or "Bearer" |
| ngx.req.read_body() |
| local data = ngx.decode_args(ngx.req.get_body_data()) |
| local jwt = require("resty.jwt") |
| local access_scopes = "https://apisix.apache.org/logs:admin" |
| local verify = jwt:verify(rsa_public_key, data["assertion"]) |
| if not verify.verified then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "identity authentication failed" })) |
| return |
| end |
| |
| local scopes_valid = type(verify.payload.scope) == "string" and |
| verify.payload.scope:find(access_scopes) |
| if not scopes_valid then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "no access to this scopes" })) |
| return |
| end |
| |
| local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() |
| if expire_time <= 0 then |
| expire_time = 0 |
| end |
| |
| local jwt_token = jwt:sign(rsa_private_key, { |
| header = { typ = "JWT", alg = "RS256" }, |
| payload = { exp = verify.payload.exp, scope = access_scopes } |
| }) |
| |
| ngx.say(json_encode({ |
| access_token = jwt_token, |
| expires_in = expire_time, |
| token_type = args_token_type |
| })) |
| end |
| |
| function _M.google_logging_entries() |
| local args = ngx.req.get_uri_args() |
| local args_token_type = args.token_type or "Bearer" |
| ngx.req.read_body() |
| local data = ngx.req.get_body_data() |
| local jwt = require("resty.jwt") |
| local access_scopes = "https://apisix.apache.org/logs:admin" |
| |
| local headers = ngx.req.get_headers() |
| local token = headers["Authorization"] |
| if not token then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "authentication header not exists" })) |
| return |
| end |
| |
| token = string.sub(token, #args_token_type + 2) |
| local verify = jwt:verify(rsa_public_key, token) |
| if not verify.verified then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "identity authentication failed" })) |
| return |
| end |
| |
| local scopes_valid = type(verify.payload.scope) == "string" and |
| verify.payload.scope:find(access_scopes) |
| if not scopes_valid then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "no access to this scopes" })) |
| return |
| end |
| |
| local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() |
| if expire_time <= 0 then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "token has expired" })) |
| return |
| end |
| |
| ngx.say(data) |
| end |
| |
| function _M.google_secret_token() |
| local args = ngx.req.get_uri_args() |
| local args_token_type = args.token_type or "Bearer" |
| ngx.req.read_body() |
| local data = ngx.decode_args(ngx.req.get_body_data()) |
| local jwt = require("resty.jwt") |
| local access_scopes = "https://www.googleapis.com/auth/cloud" |
| local verify = jwt:verify(rsa_public_key, data["assertion"]) |
| if not verify.verified then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "identity authentication failed" })) |
| return |
| end |
| |
| local scopes_valid = type(verify.payload.scope) == "string" and |
| verify.payload.scope:find(access_scopes) |
| if not scopes_valid then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "no access to this scope" })) |
| return |
| end |
| |
| local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() |
| if expire_time <= 0 then |
| expire_time = 0 |
| end |
| |
| local jwt_token = jwt:sign(rsa_private_key, { |
| header = { typ = "JWT", alg = "RS256" }, |
| payload = { exp = verify.payload.exp, scope = access_scopes } |
| }) |
| |
| ngx.say(json_encode({ |
| access_token = jwt_token, |
| expires_in = expire_time, |
| token_type = args_token_type |
| })) |
| end |
| |
| function _M.google_secret_apisix_jack() |
| local args = ngx.req.get_uri_args() |
| local args_token_type = args.token_type or "Bearer" |
| local jwt = require("resty.jwt") |
| local access_scopes = "https://www.googleapis.com/auth/cloud" |
| |
| local headers = ngx.req.get_headers() |
| local token = headers["Authorization"] |
| if not token then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "authentication header not exists" })) |
| return |
| end |
| |
| token = string.sub(token, #args_token_type + 2) |
| local verify = jwt:verify(rsa_public_key, token) |
| if not verify.verified then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "identity authentication failed" })) |
| return |
| end |
| |
| local scopes_valid = type(verify.payload.scope) == "string" and |
| verify.payload.scope:find(access_scopes) |
| if not scopes_valid then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "no access to this scope" })) |
| return |
| end |
| |
| local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() |
| if expire_time <= 0 then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "token has expired" })) |
| return |
| end |
| |
| local response = { |
| name = "projects/647037004838/secrets/apisix/versions/1", |
| payload = { |
| data = "eyJrZXkiOiJ2YWx1ZSJ9", |
| dataCrc32c = "2296192492" |
| } |
| } |
| |
| ngx.status = 200 |
| ngx.say(json_encode(response)) |
| end |
| |
| function _M.google_secret_apisix_error_jack() |
| local args = ngx.req.get_uri_args() |
| local args_token_type = args.token_type or "Bearer" |
| local jwt = require("resty.jwt") |
| local access_scopes = "https://www.googleapis.com/auth/root/cloud" |
| |
| local headers = ngx.req.get_headers() |
| local token = headers["Authorization"] |
| if not token then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "authentication header not exists" })) |
| return |
| end |
| |
| token = string.sub(token, #args_token_type + 2) |
| local verify = jwt:verify(rsa_public_key, token) |
| if not verify.verified then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "identity authentication failed" })) |
| return |
| end |
| |
| local scopes_valid = type(verify.payload.scope) == "string" and |
| verify.payload.scope:find(access_scopes) |
| if not scopes_valid then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "no access to this scope" })) |
| return |
| end |
| |
| local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() |
| if expire_time <= 0 then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "token has expired" })) |
| return |
| end |
| |
| local response = { |
| name = "projects/647037004838/secrets/apisix_error/versions/1", |
| payload = { |
| data = "eyJrZXkiOiJ2YWx1ZSJ9", |
| dataCrc32c = "2296192492" |
| } |
| } |
| |
| ngx.status = 200 |
| ngx.say(json_encode(response)) |
| end |
| |
| function _M.google_secret_apisix_mysql() |
| local args = ngx.req.get_uri_args() |
| local args_token_type = args.token_type or "Bearer" |
| local jwt = require("resty.jwt") |
| local access_scopes = "https://www.googleapis.com/auth/cloud" |
| |
| local headers = ngx.req.get_headers() |
| local token = headers["Authorization"] |
| if not token then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "authentication header not exists" })) |
| return |
| end |
| |
| token = string.sub(token, #args_token_type + 2) |
| local verify = jwt:verify(rsa_public_key, token) |
| if not verify.verified then |
| ngx.status = 401 |
| ngx.say(json_encode({ error = "identity authentication failed" })) |
| return |
| end |
| |
| local scopes_valid = type(verify.payload.scope) == "string" and |
| verify.payload.scope:find(access_scopes) |
| if not scopes_valid then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "no access to this scope" })) |
| return |
| end |
| |
| local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() |
| if expire_time <= 0 then |
| ngx.status = 403 |
| ngx.say(json_encode({ error = "token has expired" })) |
| return |
| end |
| |
| local response = { |
| name = "projects/647037004838/secrets/apisix/versions/1", |
| payload = { |
| data = "c2VjcmV0", |
| dataCrc32c = "0xB03C4D4D" |
| } |
| } |
| |
| ngx.status = 200 |
| ngx.say(json_encode(response)) |
| end |
| |
| function _M.plugin_proxy_rewrite_resp_header() |
| ngx.req.read_body() |
| local s = "plugin_proxy_rewrite_resp_header" |
| ngx.header['Content-Length'] = #s + 1 |
| ngx.say(s) |
| end |
| |
| -- AI fixture endpoints: serve mock responses from t/fixtures/ files. |
| -- Tests specify the fixture via the X-AI-Fixture request header. |
| -- If the header is absent, a 400 error is returned. |
| |
| local function ai_fixture_dispatch() |
| require("lib.fixture_loader").dispatch() |
| end |
| |
| function _M.v1_chat_completions() |
| local json = require("cjson.safe") |
| local fixture = ngx.req.get_headers()["x-ai-fixture"] |
| |
| if fixture then |
| local test_type = ngx.req.get_headers()["test-type"] |
| if test_type then |
| ngx.req.read_body() |
| local body = json.decode(ngx.req.get_body_data() or "") |
| |
| if test_type == "system-prompt" then |
| local first = body and body.messages and body.messages[1] |
| if not first or first.role ~= "system" then |
| ngx.status = 400 |
| ngx.say([[{"error":"system message not converted"}]]) |
| return |
| end |
| elseif test_type == "tools" then |
| local tool = body and body.tools and body.tools[1] |
| if not tool or tool.type ~= "function" |
| or not tool["function"] or tool["function"].name ~= "get_weather" then |
| ngx.status = 400 |
| ngx.say([[{"error":"tool not converted to openai format"}]]) |
| return |
| end |
| elseif test_type == "vertex-embeddings" then |
| if not body or not body.instances or not body.instances[1] |
| or not body.instances[1].content then |
| ngx.status = 400 |
| ngx.say([[{"error":"vertex instances format missing"}]]) |
| return |
| end |
| end |
| end |
| |
| ai_fixture_dispatch() |
| return |
| end |
| |
| local header_auth = ngx.req.get_headers()["authorization"] |
| local query_auth = ngx.req.get_uri_args()["api_key"] |
| local test_type = ngx.req.get_headers()["test-type"] |
| |
| -- options check: verify model options are merged into request body |
| if test_type == "options" then |
| ngx.req.read_body() |
| local body = json.decode(ngx.req.get_body_data() or "") |
| if body and body.foo == "bar" then |
| ngx.print("options works") |
| else |
| ngx.status = 500 |
| ngx.say("model options feature doesn't work") |
| end |
| return |
| end |
| |
| -- header forwarding: echo all received headers as JSON |
| if test_type == "header_forwarding" then |
| ngx.say(json.encode(ngx.req.get_headers())) |
| return |
| end |
| |
| -- auth check |
| local args = ngx.req.get_uri_args() |
| ngx.log(ngx.INFO, "found query params: ", |
| json.encode(args)) |
| if header_auth ~= "Bearer token" and query_auth ~= "apikey" then |
| ngx.status = 401 |
| ngx.say("Unauthorized") |
| return |
| end |
| |
| -- default: message echo (for ai-request-rewrite prompt tests) |
| ngx.req.read_body() |
| local body = ngx.req.get_body_data() |
| body = json.decode(body) |
| if not body or not body.messages or #body.messages < 1 then |
| ngx.status = 400 |
| ngx.say([[{"error":"bad request"}]]) |
| return |
| end |
| local parts = {} |
| for _, msg in ipairs(body.messages) do |
| if msg.content then |
| table.insert(parts, msg.content) |
| end |
| end |
| local content = table.concat(parts, " ") |
| ngx.say(json.encode({ |
| choices = {{message = {content = content}}} |
| })) |
| end |
| |
| function _M.v1_messages() |
| ai_fixture_dispatch() |
| end |
| |
| function _M.v1_embeddings() |
| ai_fixture_dispatch() |
| end |
| |
| function _M.v1_images_generations() |
| if ngx.req.get_headers()["x-ai-fixture"] then |
| ai_fixture_dispatch() |
| return |
| end |
| ngx.req.read_body() |
| ngx.header["Content-Type"] = "application/json" |
| ngx.print(ngx.req.get_body_data() or "{}") |
| end |
| |
| function _M.v1_responses() |
| if ngx.req.get_headers()["x-ai-fixture"] then |
| ngx.req.read_body() |
| local json = require("cjson.safe") |
| local body = json.decode(ngx.req.get_body_data() or "") |
| if body and body.stream_options then |
| ngx.status = 400 |
| ngx.say([[{"error":"stream_options must not be injected for Responses API"}]]) |
| return |
| end |
| ai_fixture_dispatch() |
| return |
| end |
| -- fallback to echo for non-fixture tests (e.g., ai-prompt-guard) |
| _M.echo() |
| end |
| |
| function _M.delay_v1_chat_completions() |
| ngx.sleep(2) |
| ai_fixture_dispatch() |
| end |
| |
| function _M.random() |
| ngx.header["Content-Type"] = "application/json" |
| ngx.say([[{"choices":[{"message":{"content":"path override works"}}]}]]) |
| end |
| |
| -- Health check probe endpoint for AI proxy tests. |
| function _M.status_gpt4() |
| ngx.say("ok") |
| end |
| |
| -- Aliyun content moderation mock: checks request body for "kill" keyword |
| -- and returns the appropriate risk/safe fixture response. |
| function _M.aliyun_moderation() |
| ngx.req.read_body() |
| local body = ngx.req.get_body_data() or "" |
| local fixture_loader = require("lib.fixture_loader") |
| local fixture_name |
| if body:find("kill") then |
| fixture_name = "aliyun/moderation-risk.json" |
| else |
| fixture_name = "aliyun/moderation-safe.json" |
| end |
| local content, err = fixture_loader.load(fixture_name) |
| if not content then |
| ngx.status = 500 |
| ngx.say(err) |
| return |
| end |
| ngx.header["Content-Type"] = "application/json" |
| ngx.print(content) |
| end |
| |
| -- Bedrock Converse mock: validates SigV4 headers (algorithm, credential |
| -- scope, signed headers, signature length, X-Amz-Date format) and the |
| -- request body shape (no `model` field, has `messages`), then serves a |
| -- canned response. If the SigV4 signer attached x-amz-security-token, the |
| -- token is echoed back in the assistant text so tests can assert |
| -- auth.aws.session_token end-to-end. |
| function _M.bedrock_converse() |
| local json = require("cjson.safe") |
| |
| -- Log raw request URI so tests can assert path encoding (e.g., that ARN |
| -- model IDs are URL-encoded as a single path segment). |
| ngx.log(ngx.WARN, "[test] received uri: ", ngx.var.request_uri) |
| |
| if ngx.req.get_method() ~= "POST" then |
| ngx.status = 400 |
| ngx.say("Unsupported request method: ", ngx.req.get_method()) |
| return |
| end |
| |
| local headers = ngx.req.get_headers() |
| local auth_header = headers["authorization"] |
| local amz_date = headers["x-amz-date"] |
| if not auth_header or not amz_date then |
| ngx.status = 403 |
| ngx.say(json.encode({message = "Missing Authentication Token"})) |
| return |
| end |
| |
| if not auth_header:match("^AWS4%-HMAC%-SHA256 ") then |
| ngx.status = 403 |
| ngx.say(json.encode({ |
| message = "Authorization header missing AWS4-HMAC-SHA256 algorithm prefix" |
| })) |
| return |
| end |
| |
| -- Strict credential scope: access_key/<date>/us-east-1/bedrock/aws4_request |
| if not auth_header:match( |
| "Credential=AKIAIOSFODNN7EXAMPLE/%d%d%d%d%d%d%d%d/us%-east%-1/bedrock/aws4_request" |
| ) then |
| ngx.status = 403 |
| ngx.say(json.encode({ |
| message = "Authorization Credential scope does not match expected " |
| .. "AKIAIOSFODNN7EXAMPLE/<DATE>/us-east-1/bedrock/aws4_request" |
| })) |
| return |
| end |
| |
| if not auth_header:match("SignedHeaders=[^,]+") then |
| ngx.status = 403 |
| ngx.say(json.encode({ |
| message = "Authorization header missing SignedHeaders component" |
| })) |
| return |
| end |
| |
| -- Lua patterns don't support {n} quantifiers, so match exactly 64 hex |
| -- chars by repeating %x sixty-four times. |
| local hex64 = string.rep("%x", 64) |
| if not auth_header:match("Signature=" .. hex64) then |
| ngx.status = 403 |
| ngx.say(json.encode({ |
| message = "Authorization Signature is missing or not 64 hex chars" |
| })) |
| return |
| end |
| |
| if not amz_date:match("^%d%d%d%d%d%d%d%dT%d%d%d%d%d%dZ$") then |
| ngx.status = 403 |
| ngx.say(json.encode({ |
| message = "X-Amz-Date header does not match YYYYMMDDTHHMMSSZ format" |
| })) |
| return |
| end |
| |
| ngx.req.read_body() |
| local body, err = json.decode(ngx.req.get_body_data() or "") |
| if not body then |
| ngx.status = 400 |
| ngx.say(json.encode({message = "Invalid JSON: " .. (err or "")})) |
| return |
| end |
| |
| -- remove_model = true: bedrock provider must strip `model` from body. |
| if body.model then |
| ngx.status = 400 |
| ngx.say(json.encode({ |
| message = "model field should not be in request body" |
| })) |
| return |
| end |
| |
| if not body.messages or #body.messages < 1 then |
| ngx.status = 400 |
| ngx.say(json.encode({message = "messages is required"})) |
| return |
| end |
| |
| local fixture_loader = require("lib.fixture_loader") |
| local content, ferr = fixture_loader.load("bedrock/converse-basic.json") |
| if not content then |
| ngx.status = 500 |
| ngx.say(ferr) |
| return |
| end |
| |
| local session_token = headers["x-amz-security-token"] |
| if session_token then |
| local response = json.decode(content) |
| local text = response.output.message.content[1].text |
| response.output.message.content[1].text = text |
| .. " session_token_seen=" .. session_token |
| content = json.encode(response) |
| end |
| |
| ngx.header["Content-Type"] = "application/json" |
| ngx.print(content) |
| end |
| |
| |
| -- Mock for Bedrock /converse-stream. Reuses the same SigV4 + body shape |
| -- validation as bedrock_converse(), then serves the recorded EventStream |
| -- binary fixture so streaming tests can assert end-to-end framing, |
| -- response-text aggregation, and token-usage extraction. |
| function _M.bedrock_converse_stream() |
| local json = require("cjson.safe") |
| |
| ngx.log(ngx.WARN, "[test] received uri: ", ngx.var.request_uri) |
| |
| if ngx.req.get_method() ~= "POST" then |
| ngx.status = 400 |
| ngx.say("Unsupported request method: ", ngx.req.get_method()) |
| return |
| end |
| |
| local headers = ngx.req.get_headers() |
| local auth_header = headers["authorization"] |
| local amz_date = headers["x-amz-date"] |
| if not auth_header or not amz_date then |
| ngx.status = 403 |
| ngx.say(json.encode({message = "Missing Authentication Token"})) |
| return |
| end |
| |
| if not auth_header:match("^AWS4%-HMAC%-SHA256 ") then |
| ngx.status = 403 |
| ngx.say(json.encode({ |
| message = "Authorization header missing AWS4-HMAC-SHA256 algorithm prefix" |
| })) |
| return |
| end |
| |
| if not auth_header:match( |
| "Credential=AKIAIOSFODNN7EXAMPLE/%d%d%d%d%d%d%d%d/us%-east%-1/bedrock/aws4_request" |
| ) then |
| ngx.status = 403 |
| ngx.say(json.encode({ |
| message = "Authorization Credential scope does not match expected " |
| .. "AKIAIOSFODNN7EXAMPLE/<DATE>/us-east-1/bedrock/aws4_request" |
| })) |
| return |
| end |
| |
| local hex64 = string.rep("%x", 64) |
| if not auth_header:match("Signature=" .. hex64) then |
| ngx.status = 403 |
| ngx.say(json.encode({ |
| message = "Authorization Signature is missing or not 64 hex chars" |
| })) |
| return |
| end |
| |
| if not amz_date:match("^%d%d%d%d%d%d%d%dT%d%d%d%d%d%dZ$") then |
| ngx.status = 403 |
| ngx.say(json.encode({ |
| message = "X-Amz-Date header does not match YYYYMMDDTHHMMSSZ format" |
| })) |
| return |
| end |
| |
| ngx.req.read_body() |
| local body, err = json.decode(ngx.req.get_body_data() or "") |
| if not body then |
| ngx.status = 400 |
| ngx.say(json.encode({message = "Invalid JSON: " .. (err or "")})) |
| return |
| end |
| |
| -- Bedrock decides streaming purely by URL; the gateway must strip its |
| -- internal `stream` flag from the body before forwarding. |
| if body.stream ~= nil then |
| ngx.status = 400 |
| ngx.say(json.encode({ |
| message = "stream field should not be in request body for /converse-stream" |
| })) |
| return |
| end |
| |
| if body.model then |
| ngx.status = 400 |
| ngx.say(json.encode({ |
| message = "model field should not be in request body" |
| })) |
| return |
| end |
| |
| if not body.messages or #body.messages < 1 then |
| ngx.status = 400 |
| ngx.say(json.encode({message = "messages is required"})) |
| return |
| end |
| |
| local fixture_loader = require("lib.fixture_loader") |
| local content, ferr = fixture_loader.load("bedrock/bedrock-converse-streaming.bin") |
| if not content then |
| ngx.status = 500 |
| ngx.say(ferr) |
| return |
| end |
| |
| ngx.header["Content-Type"] = "application/vnd.amazon.eventstream" |
| ngx.header["Cache-Control"] = "no-cache" |
| ngx.header["Transfer-Encoding"] = "chunked" |
| ngx.print(content) |
| ngx.flush(true) |
| end |
| |
| |
| -- Error endpoints for ai-request-rewrite tests. |
| function _M.bad_request() |
| ngx.status = 400 |
| ngx.say("Bad Request") |
| end |
| |
| function _M.internalservererror() |
| ngx.status = 500 |
| ngx.say("Internal Server Error") |
| end |
| |
| -- Endpoint that validates extra_option in request body for ai-request-rewrite2 tests. |
| function _M.check_extra_options() |
| ngx.req.read_body() |
| local body = ngx.req.get_body_data() |
| local json = require("cjson.safe") |
| local data = json.decode(body) |
| if not data or data.extra_option ~= "extra option" then |
| ngx.status = 400 |
| ngx.say("extra option not match") |
| return |
| end |
| local response = json.encode({choices = {{message = {content = "ok"}}}}) |
| ngx.say(response) |
| end |
| |
| -- Endpoint that validates query params for ai-request-rewrite2 tests. |
| function _M.test_params_in_overridden_endpoint() |
| local args = ngx.req.get_uri_args() |
| if args["api_key"] ~= "apikey" then |
| ngx.status = 401 |
| ngx.say("Unauthorized") |
| return |
| end |
| ngx.say("passed") |
| end |
| |
| |
| -- Please add your fake upstream above |
| function _M.go() |
| local uri = ngx.var.uri |
| |
| -- Bedrock Converse API: /model/<model>/converse(-stream) where <model> |
| -- can contain ':' and percent-encoded sequences (URL-encoded ARNs), |
| -- which the path-to-function-name conversion below can't represent. |
| -- Dispatch directly. Match streaming first since /converse is a suffix |
| -- of /converse-stream. |
| if uri:match("^/model/.+/converse%-stream$") then |
| inject_headers() |
| return _M.bedrock_converse_stream() |
| end |
| if uri:match("^/model/.+/converse$") then |
| inject_headers() |
| return _M.bedrock_converse() |
| end |
| |
| local action = string.sub(uri, 2) |
| action = string.gsub(action, "[/\\.-]", "_") |
| if not action or not _M[action] then |
| ngx.log(ngx.WARN, "undefined path in test server, uri: ", ngx.var.request_uri) |
| return ngx.exit(404) |
| end |
| |
| inject_headers() |
| return _M[action]() |
| end |
| |
| |
| function _M.clickhouse_logger_server() |
| ngx.req.read_body() |
| local data = ngx.req.get_body_data() |
| local headers = ngx.req.get_headers() |
| ngx.log(ngx.WARN, "clickhouse body: ", data) |
| for k, v in pairs(headers) do |
| ngx.log(ngx.WARN, "clickhouse headers: " .. k .. ":" .. v) |
| end |
| ngx.say("ok") |
| end |
| |
| |
| function _M.mock_compressed_upstream_response() |
| local s = "compressed_response" |
| ngx.header['Content-Encoding'] = 'gzip' |
| ngx.say(s) |
| end |
| |
| |
| return _M |