Skip to content

Commit

Permalink
Add support for sending error responses with gRPC status codes.
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 committed Jul 19, 2024
1 parent 442edc3 commit 2500068
Show file tree
Hide file tree
Showing 10 changed files with 334 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
22 changes: 22 additions & 0 deletions examples/grpc_auth_random/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
publish = false
name = "proxy-wasm-example-grpc-auth-random"
version = "0.0.1"
authors = ["Piotr Sikora <[email protected]>"]
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.
```
37 changes: 37 additions & 0 deletions examples/grpc_auth_random/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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"
- "9001:9001"
networks:
- envoymesh
networks:
envoymesh: {}
82 changes: 82 additions & 0 deletions examples/grpc_auth_random/envoy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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: httpbin
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin
port_value: 8080
- 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,
Uninmplemented = 12,
Internal = 13,
Unavailable = 14,
DataLoss = 15,
Unauthenticated = 16,
}

pub type Bytes = Vec<u8>;

0 comments on commit 2500068

Please sign in to comment.