- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
229 lines
7.7 KiB
Lua
229 lines
7.7 KiB
Lua
--
|
|
-- 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 ngx = ngx
|
|
local ngx_arg = ngx.arg
|
|
local core = require("apisix.core")
|
|
local req_set_uri = ngx.req.set_uri
|
|
local req_set_body_data = ngx.req.set_body_data
|
|
local decode_base64 = ngx.decode_base64
|
|
local encode_base64 = ngx.encode_base64
|
|
local bit = require("bit")
|
|
local string = string
|
|
|
|
|
|
local ALLOW_METHOD_OPTIONS = "OPTIONS"
|
|
local ALLOW_METHOD_POST = "POST"
|
|
local CONTENT_ENCODING_BASE64 = "base64"
|
|
local CONTENT_ENCODING_BINARY = "binary"
|
|
local DEFAULT_CORS_ALLOW_ORIGIN = "*"
|
|
local DEFAULT_CORS_ALLOW_METHODS = ALLOW_METHOD_POST
|
|
local DEFAULT_CORS_ALLOW_HEADERS = "content-type,x-grpc-web,x-user-agent"
|
|
local DEFAULT_CORS_EXPOSE_HEADERS = "grpc-message,grpc-status"
|
|
local DEFAULT_PROXY_CONTENT_TYPE = "application/grpc"
|
|
|
|
|
|
local plugin_name = "grpc-web"
|
|
|
|
local schema = {
|
|
type = "object",
|
|
properties = {
|
|
cors_allow_headers = {
|
|
description =
|
|
"multiple header use ',' to split. default: content-type,x-grpc-web,x-user-agent.",
|
|
type = "string",
|
|
default = DEFAULT_CORS_ALLOW_HEADERS
|
|
}
|
|
}
|
|
}
|
|
|
|
local grpc_web_content_encoding = {
|
|
["application/grpc-web"] = CONTENT_ENCODING_BINARY,
|
|
["application/grpc-web-text"] = CONTENT_ENCODING_BASE64,
|
|
["application/grpc-web+proto"] = CONTENT_ENCODING_BINARY,
|
|
["application/grpc-web-text+proto"] = CONTENT_ENCODING_BASE64,
|
|
}
|
|
|
|
local _M = {
|
|
version = 0.1,
|
|
priority = 505,
|
|
name = plugin_name,
|
|
schema = schema,
|
|
}
|
|
|
|
function _M.check_schema(conf)
|
|
return core.schema.check(schema, conf)
|
|
end
|
|
|
|
local function exit(ctx, status)
|
|
ctx.grpc_web_skip_body_filter = true
|
|
return status
|
|
end
|
|
|
|
--- Build gRPC-Web trailer chunk
|
|
-- grpc-web trailer format reference:
|
|
-- envoyproxy/envoy/source/extensions/filters/http/grpc_web/grpc_web_filter.cc
|
|
--
|
|
-- Format for grpc-web trailer
|
|
-- 1 byte: 0x80
|
|
-- 4 bytes: length of the trailer
|
|
-- n bytes: trailer
|
|
-- It using upstream_trailer_* variables from nginx, it is available since NGINX version 1.13.10
|
|
-- https://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_trailer_
|
|
--
|
|
-- @param grpc_status number grpc status code
|
|
-- @param grpc_message string grpc message
|
|
-- @return string grpc-web trailer chunk in raw string
|
|
local build_trailer = function (grpc_status, grpc_message)
|
|
local status_str = "grpc-status:" .. grpc_status
|
|
local status_msg = "grpc-message:" .. ( grpc_message or "")
|
|
local grpc_web_trailer = status_str .. "\r\n" .. status_msg .. "\r\n"
|
|
local len = #grpc_web_trailer
|
|
|
|
-- 1 byte: 0x80
|
|
local trailer_buf = string.char(0x80)
|
|
-- 4 bytes: length of the trailer
|
|
trailer_buf = trailer_buf .. string.char(
|
|
bit.band(bit.rshift(len, 24), 0xff),
|
|
bit.band(bit.rshift(len, 16), 0xff),
|
|
bit.band(bit.rshift(len, 8), 0xff),
|
|
bit.band(len, 0xff)
|
|
)
|
|
-- n bytes: trailer
|
|
trailer_buf = trailer_buf .. grpc_web_trailer
|
|
|
|
return trailer_buf
|
|
end
|
|
|
|
function _M.access(conf, ctx)
|
|
-- set context variable mime
|
|
-- When processing non gRPC Web requests, `mime` can be obtained in the context
|
|
-- and set to the `Content-Type` of the response
|
|
ctx.grpc_web_mime = core.request.header(ctx, "Content-Type")
|
|
|
|
local method = core.request.get_method()
|
|
if method == ALLOW_METHOD_OPTIONS then
|
|
return exit(ctx, 204)
|
|
end
|
|
|
|
if method ~= ALLOW_METHOD_POST then
|
|
-- https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support
|
|
core.log.error("request method: `", method, "` invalid")
|
|
return exit(ctx, 405)
|
|
end
|
|
|
|
local encoding = grpc_web_content_encoding[ctx.grpc_web_mime]
|
|
if not encoding then
|
|
core.log.error("request Content-Type: `", ctx.grpc_web_mime, "` invalid")
|
|
return exit(ctx, 400)
|
|
end
|
|
|
|
-- set context variable encoding method
|
|
ctx.grpc_web_encoding = encoding
|
|
|
|
-- set grpc path
|
|
if not (ctx.curr_req_matched and ctx.curr_req_matched[":ext"]) then
|
|
core.log.error("routing configuration error, grpc-web plugin only supports ",
|
|
"`prefix matching` pattern routing")
|
|
return exit(ctx, 400)
|
|
end
|
|
|
|
local path = ctx.curr_req_matched[":ext"]
|
|
if path:byte(1) ~= core.string.byte("/") then
|
|
path = "/" .. path
|
|
end
|
|
|
|
req_set_uri(path)
|
|
|
|
-- set grpc body
|
|
local body, err = core.request.get_body()
|
|
if err or not body then
|
|
core.log.error("failed to read request body, err: ", err)
|
|
return exit(ctx, 400)
|
|
end
|
|
|
|
if encoding == CONTENT_ENCODING_BASE64 then
|
|
body = decode_base64(body)
|
|
if not body then
|
|
core.log.error("failed to decode request body")
|
|
return exit(ctx, 400)
|
|
end
|
|
end
|
|
|
|
-- set grpc content-type
|
|
core.request.set_header(ctx, "Content-Type", DEFAULT_PROXY_CONTENT_TYPE)
|
|
-- set grpc body
|
|
req_set_body_data(body)
|
|
end
|
|
|
|
function _M.header_filter(conf, ctx)
|
|
local method = core.request.get_method()
|
|
if method == ALLOW_METHOD_OPTIONS then
|
|
core.response.set_header("Access-Control-Allow-Methods", DEFAULT_CORS_ALLOW_METHODS)
|
|
core.response.set_header("Access-Control-Allow-Headers", conf.cors_allow_headers)
|
|
end
|
|
|
|
if not ctx.cors_allow_origins then
|
|
core.response.set_header("Access-Control-Allow-Origin", DEFAULT_CORS_ALLOW_ORIGIN)
|
|
end
|
|
core.response.set_header("Access-Control-Expose-Headers", DEFAULT_CORS_EXPOSE_HEADERS)
|
|
|
|
if not ctx.grpc_web_skip_body_filter then
|
|
core.response.set_header("Content-Type", ctx.grpc_web_mime)
|
|
core.response.set_header("Content-Length", nil)
|
|
end
|
|
end
|
|
|
|
function _M.body_filter(conf, ctx)
|
|
if ctx.grpc_web_skip_body_filter then
|
|
return
|
|
end
|
|
|
|
-- If the MIME extension type description of the gRPC-Web standard is not obtained,
|
|
-- indicating that the request is not based on the gRPC Web specification,
|
|
-- the processing of the request body will be ignored
|
|
-- https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
|
|
-- https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support
|
|
if not ctx.grpc_web_mime then
|
|
return
|
|
end
|
|
|
|
if ctx.grpc_web_encoding == CONTENT_ENCODING_BASE64 then
|
|
local chunk = ngx_arg[1]
|
|
chunk = encode_base64(chunk)
|
|
ngx_arg[1] = chunk
|
|
end
|
|
|
|
if ngx_arg[2] then -- if eof
|
|
local status = ctx.var.upstream_trailer_grpc_status
|
|
local message = ctx.var.upstream_trailer_grpc_message
|
|
|
|
-- When the response body completes and still does not receive the grpc status
|
|
local resp_ok = status ~= nil and status ~= ""
|
|
local trailer_buf = build_trailer(
|
|
resp_ok and status or 2,
|
|
resp_ok and message or "upstream grpc status not received"
|
|
)
|
|
if ctx.grpc_web_encoding == CONTENT_ENCODING_BASE64 then
|
|
trailer_buf = encode_base64(trailer_buf)
|
|
end
|
|
|
|
ngx_arg[1] = ngx_arg[1] .. trailer_buf
|
|
end
|
|
end
|
|
|
|
return _M
|