Skip to content

Commit

Permalink
Add support for sending error responses with gRPC status codes. (#248)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
PiotrSikora authored Jul 21, 2024
1 parent ec3ddd2 commit 5c570fe
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ jobs:
- 'http_body'
- 'http_config'
- 'http_headers'
- 'grpc_auth_random'

defaults:
run:
Expand Down Expand Up @@ -301,6 +302,7 @@ jobs:
- 'http_body'
- 'http_config'
- 'http_headers'
- 'grpc_auth_random'

defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [HTTP Headers](./examples/http_headers/)
- [HTTP Response body](./examples/http_body/)
- [HTTP Configuration](./examples/http_config/)
- [gRPC Auth (random)](./examples/grpc_auth_random/)

## Articles & blog posts from the community

Expand Down
21 changes: 21 additions & 0 deletions examples/grpc_auth_random/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
publish = false
name = "proxy-wasm-example-grpc-auth-random"
version = "0.0.1"
description = "Proxy-Wasm plugin example: gRPC auth (random)"
license = "Apache-2.0"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
log = "0.4"
proxy-wasm = { path = "../../" }

[profile.release]
lto = true
opt-level = 3
codegen-units = 1
panic = "abort"
strip = "debuginfo"
52 changes: 52 additions & 0 deletions examples/grpc_auth_random/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## Proxy-Wasm plugin example: gRPC auth (random)

Proxy-Wasm plugin that grants access based on a result of gRPC callout.

### Building

```sh
$ cargo build --target wasm32-wasi --release
```

### Using in Envoy

This example can be run with [`docker compose`](https://docs.docker.com/compose/install/)
and has a matching Envoy configuration.

```sh
$ docker compose up
```

#### Access granted.

Send gRPC request to `localhost:10000` service `hello.HelloService`:

```sh
$ grpcurl -d '{"greeting": "Rust"}' -plaintext localhost:10000 hello.HelloService/SayHello
{
"reply": "hello Rust"
}
```

Expected Envoy logs:

```console
[...] wasm log grpc_auth_random: Access granted.
```

#### Access forbidden.

Send gRPC request to `localhost:10000` service `hello.HelloService`:

```sh
$ grpcurl -d '{"greeting": "Rust"}' -plaintext localhost:10000 hello.HelloService/SayHello
ERROR:
Code: Aborted
Message: Aborted by Proxy-Wasm!
```

Expected Envoy logs:

```console
[...] wasm log grpc_auth_random: Access forbidden.
```
36 changes: 36 additions & 0 deletions examples/grpc_auth_random/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2022 Google LLC
#
# Licensed 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.

services:
envoy:
image: envoyproxy/envoy:v1.24-latest
hostname: envoy
ports:
- "10000:10000"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
- ./target/wasm32-wasi/release:/etc/envoy/proxy-wasm-plugins
networks:
- envoymesh
depends_on:
- grpcbin
grpcbin:
image: kong/grpcbin
hostname: grpcbin
ports:
- "9000:9000"
networks:
- envoymesh
networks:
envoymesh: {}
69 changes: 69 additions & 0 deletions examples/grpc_auth_random/envoy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Copyright 2022 Google LLC
#
# Licensed 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.

static_resources:
listeners:
address:
socket_address:
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: AUTO
route_config:
name: local_routes
virtual_hosts:
- name: local_service
domains:
- "*"
routes:
- match:
prefix: "/"
route:
cluster: grpcbin
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
name: "grpc_auth_random"
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/etc/envoy/proxy-wasm-plugins/proxy_wasm_example_grpc_auth_random.wasm"
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: grpcbin
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: grpcbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: grpcbin
port_value: 9000
83 changes: 83 additions & 0 deletions examples/grpc_auth_random/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2020 Google LLC
//
// Licensed 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.

use log::info;
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
use std::time::Duration;

proxy_wasm::main! {{
proxy_wasm::set_log_level(LogLevel::Trace);
proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> { Box::new(GrpcAuthRandom) });
}}

struct GrpcAuthRandom;

impl HttpContext for GrpcAuthRandom {
fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
match self.get_http_request_header("content-type") {
Some(value) if value.starts_with("application/grpc") => {}
_ => {
// Reject non-gRPC clients.
self.send_http_response(
503,
vec![("Powered-By", "proxy-wasm")],
Some(b"Service accessible only to gRPC clients.\n"),
);
return Action::Pause;
}
}

match self.get_http_request_header(":path") {
Some(value) if value.starts_with("/grpc.reflection") => {
// Always allow gRPC calls to the reflection API.
Action::Continue
}
_ => {
// Allow other gRPC calls based on the result of grpcbin.GRPCBin/RandomError.
self.dispatch_grpc_call(
"grpcbin",
"grpcbin.GRPCBin",
"RandomError",
vec![],
None,
Duration::from_secs(1),
)
.unwrap();
Action::Pause
}
}
}

fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
self.set_http_response_header("Powered-By", Some("proxy-wasm"));
Action::Continue
}
}

impl Context for GrpcAuthRandom {
fn on_grpc_call_response(&mut self, _: u32, status_code: u32, _: usize) {
if status_code % 2 == 0 {
info!("Access granted.");
self.resume_http_request();
} else {
info!("Access forbidden.");
self.send_grpc_response(
GrpcStatusCode::Aborted,
Some("Aborted by Proxy-Wasm!"),
vec![("Powered-By", b"proxy-wasm")],
);
}
}
}
23 changes: 23 additions & 0 deletions src/hostcalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,29 @@ pub fn send_http_response(
}
}

pub fn send_grpc_response(
grpc_status: GrpcStatusCode,
grpc_status_message: Option<&str>,
custom_metadata: Vec<(&str, &[u8])>,
) -> Result<(), Status> {
let serialized_custom_metadata = utils::serialize_map_bytes(custom_metadata);
unsafe {
match proxy_send_local_response(
200,
null(),
0,
grpc_status_message.map_or(null(), |grpc_status_message| grpc_status_message.as_ptr()),
grpc_status_message.map_or(0, |grpc_status_message| grpc_status_message.len()),
serialized_custom_metadata.as_ptr(),
serialized_custom_metadata.len(),
grpc_status as i32,
) {
Status::Ok => Ok(()),
status => panic!("unexpected status: {}", status as u32),
}
}
}

extern "C" {
fn proxy_http_call(
upstream_data: *const u8,
Expand Down
9 changes: 9 additions & 0 deletions src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,5 +532,14 @@ pub trait HttpContext: Context {
hostcalls::send_http_response(status_code, headers, body).unwrap()
}

fn send_grpc_response(
&self,
grpc_status: GrpcStatusCode,
grpc_status_message: Option<&str>,
custom_metadata: Vec<(&str, &[u8])>,
) {
hostcalls::send_grpc_response(grpc_status, grpc_status_message, custom_metadata).unwrap()
}

fn on_log(&mut self) {}
}
23 changes: 23 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,27 @@ pub enum MetricType {
Histogram = 2,
}

#[repr(u32)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[non_exhaustive]
pub enum GrpcStatusCode {
Ok = 0,
Cancelled = 1,
Unknown = 2,
InvalidArgument = 3,
DeadlineExceeded = 4,
NotFound = 5,
AlreadyExists = 6,
PermissionDenied = 7,
ResourceExhausted = 8,
FailedPrecondition = 9,
Aborted = 10,
OutOfRange = 11,
Unimplemented = 12,
Internal = 13,
Unavailable = 14,
DataLoss = 15,
Unauthenticated = 16,
}

pub type Bytes = Vec<u8>;

0 comments on commit 5c570fe

Please sign in to comment.