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