Skip to content

Commit 5c570fe

Browse files
authored
Add support for sending error responses with gRPC status codes. (#248)
Note that some hosts (e.g. Envoy v1.31+) already map HTTP status codes from send_http_response() to gRPC status codes, when talking with gRPC clients, so this API is needed only when more control is needed. Fixes #148. Signed-off-by: Piotr Sikora <[email protected]>
1 parent ec3ddd2 commit 5c570fe

File tree

10 files changed

+319
-0
lines changed

10 files changed

+319
-0
lines changed

.github/workflows/rust.yml

+2
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ jobs:
238238
- 'http_body'
239239
- 'http_config'
240240
- 'http_headers'
241+
- 'grpc_auth_random'
241242

242243
defaults:
243244
run:
@@ -301,6 +302,7 @@ jobs:
301302
- 'http_body'
302303
- 'http_config'
303304
- 'http_headers'
305+
- 'grpc_auth_random'
304306

305307
defaults:
306308
run:

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- [HTTP Headers](./examples/http_headers/)
2222
- [HTTP Response body](./examples/http_body/)
2323
- [HTTP Configuration](./examples/http_config/)
24+
- [gRPC Auth (random)](./examples/grpc_auth_random/)
2425

2526
## Articles & blog posts from the community
2627

examples/grpc_auth_random/Cargo.toml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
publish = false
3+
name = "proxy-wasm-example-grpc-auth-random"
4+
version = "0.0.1"
5+
description = "Proxy-Wasm plugin example: gRPC auth (random)"
6+
license = "Apache-2.0"
7+
edition = "2018"
8+
9+
[lib]
10+
crate-type = ["cdylib"]
11+
12+
[dependencies]
13+
log = "0.4"
14+
proxy-wasm = { path = "../../" }
15+
16+
[profile.release]
17+
lto = true
18+
opt-level = 3
19+
codegen-units = 1
20+
panic = "abort"
21+
strip = "debuginfo"

examples/grpc_auth_random/README.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
## Proxy-Wasm plugin example: gRPC auth (random)
2+
3+
Proxy-Wasm plugin that grants access based on a result of gRPC callout.
4+
5+
### Building
6+
7+
```sh
8+
$ cargo build --target wasm32-wasi --release
9+
```
10+
11+
### Using in Envoy
12+
13+
This example can be run with [`docker compose`](https://docs.docker.com/compose/install/)
14+
and has a matching Envoy configuration.
15+
16+
```sh
17+
$ docker compose up
18+
```
19+
20+
#### Access granted.
21+
22+
Send gRPC request to `localhost:10000` service `hello.HelloService`:
23+
24+
```sh
25+
$ grpcurl -d '{"greeting": "Rust"}' -plaintext localhost:10000 hello.HelloService/SayHello
26+
{
27+
"reply": "hello Rust"
28+
}
29+
```
30+
31+
Expected Envoy logs:
32+
33+
```console
34+
[...] wasm log grpc_auth_random: Access granted.
35+
```
36+
37+
#### Access forbidden.
38+
39+
Send gRPC request to `localhost:10000` service `hello.HelloService`:
40+
41+
```sh
42+
$ grpcurl -d '{"greeting": "Rust"}' -plaintext localhost:10000 hello.HelloService/SayHello
43+
ERROR:
44+
Code: Aborted
45+
Message: Aborted by Proxy-Wasm!
46+
```
47+
48+
Expected Envoy logs:
49+
50+
```console
51+
[...] wasm log grpc_auth_random: Access forbidden.
52+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
services:
16+
envoy:
17+
image: envoyproxy/envoy:v1.24-latest
18+
hostname: envoy
19+
ports:
20+
- "10000:10000"
21+
volumes:
22+
- ./envoy.yaml:/etc/envoy/envoy.yaml
23+
- ./target/wasm32-wasi/release:/etc/envoy/proxy-wasm-plugins
24+
networks:
25+
- envoymesh
26+
depends_on:
27+
- grpcbin
28+
grpcbin:
29+
image: kong/grpcbin
30+
hostname: grpcbin
31+
ports:
32+
- "9000:9000"
33+
networks:
34+
- envoymesh
35+
networks:
36+
envoymesh: {}

examples/grpc_auth_random/envoy.yaml

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
static_resources:
16+
listeners:
17+
address:
18+
socket_address:
19+
address: 0.0.0.0
20+
port_value: 10000
21+
filter_chains:
22+
- filters:
23+
- name: envoy.filters.network.http_connection_manager
24+
typed_config:
25+
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
26+
stat_prefix: ingress_http
27+
codec_type: AUTO
28+
route_config:
29+
name: local_routes
30+
virtual_hosts:
31+
- name: local_service
32+
domains:
33+
- "*"
34+
routes:
35+
- match:
36+
prefix: "/"
37+
route:
38+
cluster: grpcbin
39+
http_filters:
40+
- name: envoy.filters.http.wasm
41+
typed_config:
42+
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
43+
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
44+
value:
45+
config:
46+
name: "grpc_auth_random"
47+
vm_config:
48+
runtime: "envoy.wasm.runtime.v8"
49+
code:
50+
local:
51+
filename: "/etc/envoy/proxy-wasm-plugins/proxy_wasm_example_grpc_auth_random.wasm"
52+
- name: envoy.filters.http.router
53+
typed_config:
54+
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
55+
clusters:
56+
- name: grpcbin
57+
connect_timeout: 5s
58+
type: STRICT_DNS
59+
lb_policy: ROUND_ROBIN
60+
http2_protocol_options: {}
61+
load_assignment:
62+
cluster_name: grpcbin
63+
endpoints:
64+
- lb_endpoints:
65+
- endpoint:
66+
address:
67+
socket_address:
68+
address: grpcbin
69+
port_value: 9000

examples/grpc_auth_random/src/lib.rs

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use log::info;
16+
use proxy_wasm::traits::*;
17+
use proxy_wasm::types::*;
18+
use std::time::Duration;
19+
20+
proxy_wasm::main! {{
21+
proxy_wasm::set_log_level(LogLevel::Trace);
22+
proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> { Box::new(GrpcAuthRandom) });
23+
}}
24+
25+
struct GrpcAuthRandom;
26+
27+
impl HttpContext for GrpcAuthRandom {
28+
fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
29+
match self.get_http_request_header("content-type") {
30+
Some(value) if value.starts_with("application/grpc") => {}
31+
_ => {
32+
// Reject non-gRPC clients.
33+
self.send_http_response(
34+
503,
35+
vec![("Powered-By", "proxy-wasm")],
36+
Some(b"Service accessible only to gRPC clients.\n"),
37+
);
38+
return Action::Pause;
39+
}
40+
}
41+
42+
match self.get_http_request_header(":path") {
43+
Some(value) if value.starts_with("/grpc.reflection") => {
44+
// Always allow gRPC calls to the reflection API.
45+
Action::Continue
46+
}
47+
_ => {
48+
// Allow other gRPC calls based on the result of grpcbin.GRPCBin/RandomError.
49+
self.dispatch_grpc_call(
50+
"grpcbin",
51+
"grpcbin.GRPCBin",
52+
"RandomError",
53+
vec![],
54+
None,
55+
Duration::from_secs(1),
56+
)
57+
.unwrap();
58+
Action::Pause
59+
}
60+
}
61+
}
62+
63+
fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
64+
self.set_http_response_header("Powered-By", Some("proxy-wasm"));
65+
Action::Continue
66+
}
67+
}
68+
69+
impl Context for GrpcAuthRandom {
70+
fn on_grpc_call_response(&mut self, _: u32, status_code: u32, _: usize) {
71+
if status_code % 2 == 0 {
72+
info!("Access granted.");
73+
self.resume_http_request();
74+
} else {
75+
info!("Access forbidden.");
76+
self.send_grpc_response(
77+
GrpcStatusCode::Aborted,
78+
Some("Aborted by Proxy-Wasm!"),
79+
vec![("Powered-By", b"proxy-wasm")],
80+
);
81+
}
82+
}
83+
}

src/hostcalls.rs

+23
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,29 @@ pub fn send_http_response(
728728
}
729729
}
730730

731+
pub fn send_grpc_response(
732+
grpc_status: GrpcStatusCode,
733+
grpc_status_message: Option<&str>,
734+
custom_metadata: Vec<(&str, &[u8])>,
735+
) -> Result<(), Status> {
736+
let serialized_custom_metadata = utils::serialize_map_bytes(custom_metadata);
737+
unsafe {
738+
match proxy_send_local_response(
739+
200,
740+
null(),
741+
0,
742+
grpc_status_message.map_or(null(), |grpc_status_message| grpc_status_message.as_ptr()),
743+
grpc_status_message.map_or(0, |grpc_status_message| grpc_status_message.len()),
744+
serialized_custom_metadata.as_ptr(),
745+
serialized_custom_metadata.len(),
746+
grpc_status as i32,
747+
) {
748+
Status::Ok => Ok(()),
749+
status => panic!("unexpected status: {}", status as u32),
750+
}
751+
}
752+
}
753+
731754
extern "C" {
732755
fn proxy_http_call(
733756
upstream_data: *const u8,

src/traits.rs

+9
Original file line numberDiff line numberDiff line change
@@ -532,5 +532,14 @@ pub trait HttpContext: Context {
532532
hostcalls::send_http_response(status_code, headers, body).unwrap()
533533
}
534534

535+
fn send_grpc_response(
536+
&self,
537+
grpc_status: GrpcStatusCode,
538+
grpc_status_message: Option<&str>,
539+
custom_metadata: Vec<(&str, &[u8])>,
540+
) {
541+
hostcalls::send_grpc_response(grpc_status, grpc_status_message, custom_metadata).unwrap()
542+
}
543+
535544
fn on_log(&mut self) {}
536545
}

src/types.rs

+23
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,27 @@ pub enum MetricType {
115115
Histogram = 2,
116116
}
117117

118+
#[repr(u32)]
119+
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
120+
#[non_exhaustive]
121+
pub enum GrpcStatusCode {
122+
Ok = 0,
123+
Cancelled = 1,
124+
Unknown = 2,
125+
InvalidArgument = 3,
126+
DeadlineExceeded = 4,
127+
NotFound = 5,
128+
AlreadyExists = 6,
129+
PermissionDenied = 7,
130+
ResourceExhausted = 8,
131+
FailedPrecondition = 9,
132+
Aborted = 10,
133+
OutOfRange = 11,
134+
Unimplemented = 12,
135+
Internal = 13,
136+
Unavailable = 14,
137+
DataLoss = 15,
138+
Unauthenticated = 16,
139+
}
140+
118141
pub type Bytes = Vec<u8>;

0 commit comments

Comments
 (0)