Skip to content

Commit 550126d

Browse files
chore: backport grpc-web from latest APISIX (apache#729)
## Description backports: apache#10904 and apache#10851 ## Checklist - [ ] I have explained **WHY** we need this PR and **HOW** the problem it solves - [ ] The title of the PR concisely describes the code modification and complies with APISIX specifications. - [ ] I have explained the changes or the new features added to this PR - [ ] I have added **e2e test cases** corresponding to this change - [ ] I have updated the documentation to reflect this change **NOTE: This PR will not be reviewed and merged until the contents of these checklists are completed.**
1 parent 2f7b082 commit 550126d

File tree

4 files changed

+663
-0
lines changed

4 files changed

+663
-0
lines changed

apisix/plugins/grpc-web.lua

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
--
2+
-- Licensed to the Apache Software Foundation (ASF) under one or more
3+
-- contributor license agreements. See the NOTICE file distributed with
4+
-- this work for additional information regarding copyright ownership.
5+
-- The ASF licenses this file to You under the Apache License, Version 2.0
6+
-- (the "License"); you may not use this file except in compliance with
7+
-- the License. You may obtain a copy of the License at
8+
--
9+
-- http://www.apache.org/licenses/LICENSE-2.0
10+
--
11+
-- Unless required by applicable law or agreed to in writing, software
12+
-- distributed under the License is distributed on an "AS IS" BASIS,
13+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
-- See the License for the specific language governing permissions and
15+
-- limitations under the License.
16+
17+
local ngx = ngx
18+
local ngx_arg = ngx.arg
19+
local core = require("apisix.core")
20+
local req_set_uri = ngx.req.set_uri
21+
local req_set_body_data = ngx.req.set_body_data
22+
local decode_base64 = ngx.decode_base64
23+
local encode_base64 = ngx.encode_base64
24+
local bit = require("bit")
25+
local string = string
26+
27+
28+
local ALLOW_METHOD_OPTIONS = "OPTIONS"
29+
local ALLOW_METHOD_POST = "POST"
30+
local CONTENT_ENCODING_BASE64 = "base64"
31+
local CONTENT_ENCODING_BINARY = "binary"
32+
local DEFAULT_CORS_ALLOW_ORIGIN = "*"
33+
local DEFAULT_CORS_ALLOW_METHODS = ALLOW_METHOD_POST
34+
local DEFAULT_CORS_ALLOW_HEADERS = "content-type,x-grpc-web,x-user-agent"
35+
local DEFAULT_CORS_EXPOSE_HEADERS = "grpc-message,grpc-status"
36+
local DEFAULT_PROXY_CONTENT_TYPE = "application/grpc"
37+
38+
39+
local plugin_name = "grpc-web"
40+
41+
local schema = {
42+
type = "object",
43+
properties = {
44+
cors_allow_headers = {
45+
description =
46+
"multiple header use ',' to split. default: content-type,x-grpc-web,x-user-agent.",
47+
type = "string",
48+
default = DEFAULT_CORS_ALLOW_HEADERS
49+
}
50+
}
51+
}
52+
53+
local grpc_web_content_encoding = {
54+
["application/grpc-web"] = CONTENT_ENCODING_BINARY,
55+
["application/grpc-web-text"] = CONTENT_ENCODING_BASE64,
56+
["application/grpc-web+proto"] = CONTENT_ENCODING_BINARY,
57+
["application/grpc-web-text+proto"] = CONTENT_ENCODING_BASE64,
58+
}
59+
60+
local _M = {
61+
version = 0.1,
62+
priority = 505,
63+
name = plugin_name,
64+
schema = schema,
65+
}
66+
67+
function _M.check_schema(conf)
68+
return core.schema.check(schema, conf)
69+
end
70+
71+
function _M.access(conf, ctx)
72+
-- set context variable mime
73+
-- When processing non gRPC Web requests, `mime` can be obtained in the context
74+
-- and set to the `Content-Type` of the response
75+
ctx.grpc_web_mime = core.request.header(ctx, "Content-Type")
76+
77+
local method = core.request.get_method()
78+
if method == ALLOW_METHOD_OPTIONS then
79+
return 204
80+
end
81+
82+
if method ~= ALLOW_METHOD_POST then
83+
-- https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support
84+
core.log.error("request method: `", method, "` invalid")
85+
return 400
86+
end
87+
88+
local encoding = grpc_web_content_encoding[ctx.grpc_web_mime]
89+
if not encoding then
90+
core.log.error("request Content-Type: `", ctx.grpc_web_mime, "` invalid")
91+
return 400
92+
end
93+
94+
-- set context variable encoding method
95+
ctx.grpc_web_encoding = encoding
96+
97+
-- set grpc path
98+
if not (ctx.curr_req_matched and ctx.curr_req_matched[":ext"]) then
99+
core.log.error("routing configuration error, grpc-web plugin only supports ",
100+
"`prefix matching` pattern routing")
101+
return 400
102+
end
103+
104+
local path = ctx.curr_req_matched[":ext"]
105+
if path:byte(1) ~= core.string.byte("/") then
106+
path = "/" .. path
107+
end
108+
109+
req_set_uri(path)
110+
111+
-- set grpc body
112+
local body, err = core.request.get_body()
113+
if err then
114+
core.log.error("failed to read request body, err: ", err)
115+
return 400
116+
end
117+
118+
if encoding == CONTENT_ENCODING_BASE64 then
119+
body = decode_base64(body)
120+
if not body then
121+
core.log.error("failed to decode request body")
122+
return 400
123+
end
124+
end
125+
126+
-- set grpc content-type
127+
core.request.set_header(ctx, "Content-Type", DEFAULT_PROXY_CONTENT_TYPE)
128+
-- set grpc body
129+
req_set_body_data(body)
130+
end
131+
132+
function _M.header_filter(conf, ctx)
133+
local method = core.request.get_method()
134+
if method == ALLOW_METHOD_OPTIONS then
135+
core.response.set_header("Access-Control-Allow-Methods", DEFAULT_CORS_ALLOW_METHODS)
136+
core.response.set_header("Access-Control-Allow-Headers", conf.cors_allow_headers)
137+
end
138+
139+
if not ctx.cors_allow_origins then
140+
core.response.set_header("Access-Control-Allow-Origin", DEFAULT_CORS_ALLOW_ORIGIN)
141+
end
142+
core.response.set_header("Content-Type", ctx.grpc_web_mime)
143+
core.response.set_header("Access-Control-Expose-Headers", DEFAULT_CORS_EXPOSE_HEADERS)
144+
end
145+
146+
function _M.body_filter(conf, ctx)
147+
-- If the MIME extension type description of the gRPC-Web standard is not obtained,
148+
-- indicating that the request is not based on the gRPC Web specification,
149+
-- the processing of the request body will be ignored
150+
-- https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
151+
-- https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support
152+
if not ctx.grpc_web_mime then
153+
return
154+
end
155+
156+
if ctx.grpc_web_encoding == CONTENT_ENCODING_BASE64 then
157+
local chunk = ngx_arg[1]
158+
chunk = encode_base64(chunk)
159+
ngx_arg[1] = chunk
160+
end
161+
162+
--[[
163+
upstream_trailer_* available since NGINX version 1.13.10 :
164+
https://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_trailer_
165+
166+
grpc-web trailer format reference:
167+
envoyproxy/envoy/source/extensions/filters/http/grpc_web/grpc_web_filter.cc
168+
169+
Format for grpc-web trailer
170+
1 byte: 0x80
171+
4 bytes: length of the trailer
172+
n bytes: trailer
173+
174+
--]]
175+
local status = ctx.var.upstream_trailer_grpc_status
176+
local message = ctx.var.upstream_trailer_grpc_message
177+
if status ~= "" and status ~= nil then
178+
local status_str = "grpc-status:" .. status
179+
local status_msg = "grpc-message:" .. ( message or "")
180+
local grpc_web_trailer = status_str .. "\r\n" .. status_msg .. "\r\n"
181+
local len = #grpc_web_trailer
182+
183+
-- 1 byte: 0x80
184+
local trailer_buf = string.char(0x80)
185+
-- 4 bytes: length of the trailer
186+
trailer_buf = trailer_buf .. string.char(
187+
bit.band(bit.rshift(len, 24), 0xff),
188+
bit.band(bit.rshift(len, 16), 0xff),
189+
bit.band(bit.rshift(len, 8), 0xff),
190+
bit.band(len, 0xff)
191+
)
192+
-- n bytes: trailer
193+
trailer_buf = trailer_buf .. grpc_web_trailer
194+
195+
if ctx.grpc_web_encoding == CONTENT_ENCODING_BINARY then
196+
ngx_arg[1] = ngx_arg[1] .. trailer_buf
197+
else
198+
ngx_arg[1] = ngx_arg[1] .. encode_base64(trailer_buf)
199+
end
200+
201+
-- clear trailer
202+
ctx.var.upstream_trailer_grpc_status = nil
203+
ctx.var.upstream_trailer_grpc_message = nil
204+
end
205+
end
206+
207+
return _M

0 commit comments

Comments
 (0)