use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use base64::Engine as _;
use globset::GlobMatcher;
use prost::Message as _;
use prost_reflect::{
Cardinality, DescriptorPool, DynamicMessage, Kind, MessageDescriptor, ReflectMessage,
SerializeOptions, Value as PbValue,
};
use serde_json::{Map, Value};
const DEBUG_INFO: &str = "google.rpc.DebugInfo";
pub fn grpc_to_http_status(code: tonic::Code) -> StatusCode {
match code {
tonic::Code::Ok => StatusCode::OK,
tonic::Code::Cancelled => StatusCode::from_u16(499).unwrap_or(StatusCode::BAD_REQUEST),
tonic::Code::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
tonic::Code::InvalidArgument => StatusCode::BAD_REQUEST,
tonic::Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
tonic::Code::NotFound => StatusCode::NOT_FOUND,
tonic::Code::AlreadyExists => StatusCode::CONFLICT,
tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
tonic::Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
tonic::Code::FailedPrecondition => StatusCode::BAD_REQUEST,
tonic::Code::Aborted => StatusCode::CONFLICT,
tonic::Code::OutOfRange => StatusCode::BAD_REQUEST,
tonic::Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
tonic::Code::Internal => StatusCode::INTERNAL_SERVER_ERROR,
tonic::Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
tonic::Code::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
tonic::Code::Unauthenticated => StatusCode::UNAUTHORIZED,
}
}
const MALFORMED_STATUS_MESSAGE: &str = "upstream returned a malformed error status";
#[derive(Debug)]
struct MalformedStatus;
pub fn status_to_response(status: tonic::Status) -> Response {
status_to_response_with_details(&status, None)
}
pub fn status_to_response_with_details(
status: &tonic::Status,
details: Option<&StatusDetails>,
) -> Response {
render_response(status, details).0
}
pub(crate) fn render_response(
status: &tonic::Status,
details: Option<&StatusDetails>,
) -> (Response, bool) {
let (code, body, faithful) = render(status, details);
(
(grpc_to_http_status(code), Json(body)).into_response(),
faithful,
)
}
const MALFORMED_RESPONSE_MESSAGE: &str = "upstream returned a malformed response";
pub(crate) fn malformed_response(details: Option<&StatusDetails>) -> Response {
let body = body(
tonic::Code::Internal,
MALFORMED_RESPONSE_MESSAGE,
details.map(|_| RenderedDetails::default()),
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(body)).into_response()
}
pub(crate) fn guard_error_body(code: tonic::Code, message: &str) -> Value {
body(code, message, Some(RenderedDetails::default()))
}
pub fn error_body(status: &tonic::Status, details: Option<&StatusDetails>) -> Value {
render(status, details).1
}
fn render(status: &tonic::Status, details: Option<&StatusDetails>) -> (tonic::Code, Value, bool) {
let Some(details) = details else {
return (
status.code(),
body(status.code(), status.message(), None),
true,
);
};
match details.render(status) {
Ok(rendered) => (
status.code(),
body(status.code(), status.message(), Some(rendered)),
true,
),
Err(MalformedStatus) => (
tonic::Code::Internal,
body(
tonic::Code::Internal,
MALFORMED_STATUS_MESSAGE,
Some(RenderedDetails::default()),
),
false,
),
}
}
fn body(code: tonic::Code, message: &str, details: Option<RenderedDetails>) -> Value {
let mut body = Map::with_capacity(5);
body.insert("error".into(), grpc_code_name(code).into());
body.insert("message".into(), message.into());
body.insert("code".into(), (code as i32).into());
if let Some(rendered) = details {
body.insert("details".into(), Value::Array(rendered.details));
if !rendered.opaque.is_empty() {
body.insert("opaqueDetails".into(), Value::Array(rendered.opaque));
}
}
Value::Object(body)
}
pub(crate) fn grpc_code_name(code: tonic::Code) -> &'static str {
match code {
tonic::Code::Ok => "OK",
tonic::Code::Cancelled => "CANCELLED",
tonic::Code::Unknown => "UNKNOWN",
tonic::Code::InvalidArgument => "INVALID_ARGUMENT",
tonic::Code::DeadlineExceeded => "DEADLINE_EXCEEDED",
tonic::Code::NotFound => "NOT_FOUND",
tonic::Code::AlreadyExists => "ALREADY_EXISTS",
tonic::Code::PermissionDenied => "PERMISSION_DENIED",
tonic::Code::ResourceExhausted => "RESOURCE_EXHAUSTED",
tonic::Code::FailedPrecondition => "FAILED_PRECONDITION",
tonic::Code::Aborted => "ABORTED",
tonic::Code::OutOfRange => "OUT_OF_RANGE",
tonic::Code::Unimplemented => "UNIMPLEMENTED",
tonic::Code::Internal => "INTERNAL",
tonic::Code::Unavailable => "UNAVAILABLE",
tonic::Code::DataLoss => "DATA_LOSS",
tonic::Code::Unauthenticated => "UNAUTHENTICATED",
}
}
#[derive(Debug, Clone)]
pub struct ErrorDetailsPolicy {
enabled: bool,
opaque: bool,
routes: Vec<RouteRule>,
}
#[derive(Debug, Clone)]
struct RouteRule {
matcher: GlobMatcher,
enabled: Option<bool>,
opaque: Option<bool>,
}
impl ErrorDetailsPolicy {
pub fn disabled() -> Self {
Self {
enabled: false,
opaque: false,
routes: Vec::new(),
}
}
pub fn route(self, pattern: &str, enabled: bool) -> Result<Self, String> {
self.rule(pattern, Some(enabled), None)
}
pub fn with_opaque_details(mut self, enabled: bool) -> Self {
self.opaque = enabled;
self
}
pub fn opaque_route(self, pattern: &str, enabled: bool) -> Result<Self, String> {
self.rule(pattern, None, Some(enabled))
}
pub(crate) fn rule(
mut self,
pattern: &str,
enabled: Option<bool>,
opaque: Option<bool>,
) -> Result<Self, String> {
if !pattern.starts_with('/') {
return Err(format!(
"error details route pattern {pattern:?} must start with '/'"
));
}
self.routes.push(RouteRule {
matcher: crate::shield::matcher::path_glob(pattern)?,
enabled,
opaque,
});
Ok(self)
}
pub fn enabled_for(&self, route_path: &str) -> bool {
self.decide(route_path, |rule| rule.enabled)
.unwrap_or(self.enabled)
}
pub fn opaque_for(&self, route_path: &str) -> bool {
self.decide(route_path, |rule| rule.opaque)
.unwrap_or(self.opaque)
}
fn decide(
&self,
route_path: &str,
switch: impl Fn(&RouteRule) -> Option<bool>,
) -> Option<bool> {
self.routes
.iter()
.filter_map(|rule| switch(rule).map(|value| (rule, value)))
.find(|(rule, _)| rule.matcher.is_match(route_path))
.map(|(_, value)| value)
}
}
impl Default for ErrorDetailsPolicy {
fn default() -> Self {
Self {
enabled: true,
opaque: false,
routes: Vec::new(),
}
}
}
fn has_special_json(full_name: &str) -> bool {
matches!(
full_name,
"google.protobuf.Any"
| "google.protobuf.Timestamp"
| "google.protobuf.Duration"
| "google.protobuf.Struct"
| "google.protobuf.FloatValue"
| "google.protobuf.DoubleValue"
| "google.protobuf.Int32Value"
| "google.protobuf.Int64Value"
| "google.protobuf.UInt32Value"
| "google.protobuf.UInt64Value"
| "google.protobuf.BoolValue"
| "google.protobuf.StringValue"
| "google.protobuf.BytesValue"
| "google.protobuf.FieldMask"
| "google.protobuf.ListValue"
| "google.protobuf.Value"
| "google.protobuf.Empty"
)
}
const ANY: &str = "google.protobuf.Any";
fn any_type_name(type_url: &str) -> Option<&str> {
let (_, name) = type_url.rsplit_once('/')?;
is_full_name(name).then_some(name)
}
fn is_full_name(name: &str) -> bool {
!name.is_empty()
&& name.split('.').all(|part| {
let mut chars = part.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
})
}
#[derive(Debug, Default)]
struct RenderedDetails {
details: Vec<Value>,
opaque: Vec<Value>,
}
fn opaque_entry(index: usize, type_url: &str, value: &[u8]) -> Value {
let mut out = Map::with_capacity(3);
out.insert("index".into(), index.into());
out.insert("typeUrl".into(), type_url.into());
out.insert(
"bytes".into(),
base64::engine::general_purpose::STANDARD
.encode(value)
.into(),
);
Value::Object(out)
}
fn complete_with_canonical(pool: &mut DescriptorPool, canonical: &DescriptorPool) {
use std::collections::{BTreeSet, HashMap};
let mut stand_ins: HashMap<String, BTreeSet<String>> = HashMap::new();
for file in canonical.files() {
let mut proto = file.file_descriptor_proto().clone();
let package = proto.package().to_owned();
let full = |name: &str| {
if package.is_empty() {
name.to_owned()
} else {
format!("{package}.{name}")
}
};
let mut defining_files = BTreeSet::new();
proto.message_type.retain(|message| {
match pool.get_message_by_name(&full(message.name())) {
Some(existing) => {
defining_files.insert(existing.parent_file().name().to_owned());
false
}
None => true,
}
});
proto
.enum_type
.retain(|en| match pool.get_enum_by_name(&full(en.name())) {
Some(existing) => {
defining_files.insert(existing.parent_file().name().to_owned());
false
}
None => true,
});
let everything_present = proto.message_type.is_empty() && proto.enum_type.is_empty();
let file_present = pool.get_file_by_name(file.name()).is_some();
if everything_present {
if !file_present {
stand_ins.insert(file.name().to_owned(), defining_files);
}
continue;
}
let renamed = !defining_files.is_empty() || file_present;
let mut provided_by = defining_files.clone();
if renamed {
let name = format!("structured-proxy/canonical/{}", file.name());
proto.name = Some(name.clone());
provided_by.insert(name);
}
let mut dependencies = BTreeSet::new();
for dep in &proto.dependency {
match stand_ins.get(dep) {
Some(files) => {
if pool.get_file_by_name(dep).is_some() {
dependencies.insert(dep.clone());
}
dependencies.extend(files.iter().cloned());
}
None => {
dependencies.insert(dep.clone());
}
}
}
dependencies.extend(defining_files);
proto.dependency = dependencies.into_iter().collect();
proto.public_dependency.clear();
proto.weak_dependency.clear();
proto.source_code_info = None;
match pool.add_file_descriptor_proto(proto) {
Ok(()) => {
if renamed {
stand_ins.insert(file.name().to_owned(), provided_by);
}
}
Err(e) => {
tracing::debug!(file = %file.name(), "canonical descriptor not merged: {e}");
}
}
}
}
#[derive(Debug, Clone)]
pub struct StatusDetails {
pool: DescriptorPool,
opaque: bool,
}
impl StatusDetails {
pub fn new(product: &DescriptorPool) -> Self {
let mut canonical = DescriptorPool::global();
canonical
.decode_file_descriptor_set(tonic_types::pb::FILE_DESCRIPTOR_SET)
.expect("tonic-types ships a valid google.rpc descriptor set");
if canonical
.get_message_by_name(super::httpbody::HTTP_BODY)
.is_none()
{
canonical
.add_file_descriptor_proto(super::httpbody::file_descriptor())
.expect("google/api/httpbody.proto is a valid descriptor");
}
let mut pool = product.clone();
complete_with_canonical(&mut pool, &canonical);
Self {
pool,
opaque: false,
}
}
pub fn with_opaque_details(mut self, enabled: bool) -> Self {
self.opaque = enabled;
self
}
fn render(&self, status: &tonic::Status) -> Result<RenderedDetails, MalformedStatus> {
let mut rendered = RenderedDetails::default();
let raw = status.details();
if raw.is_empty() {
return Ok(rendered);
}
let decoded = tonic_types::pb::Status::decode(raw).map_err(|e| {
tracing::error!("malformed grpc-status-details-bin trailer: {e}");
MalformedStatus
})?;
if decoded.code != status.code() as i32 || decoded.message != status.message() {
tracing::error!(
trailer_code = decoded.code,
status_code = status.code() as i32,
"grpc-status-details-bin disagrees with grpc-status / grpc-message"
);
return Err(MalformedStatus);
}
rendered.details.reserve(decoded.details.len());
let mut index = 0usize;
for any in &decoded.details {
let type_url = any.type_url.as_str();
let Some(type_name) = any_type_name(type_url) else {
tracing::error!(%type_url, "error detail with a malformed type URL");
return Err(MalformedStatus);
};
if type_name == DEBUG_INFO {
continue;
}
match self.resolve(type_name) {
Some(desc) => match self.typed_entry(type_url, type_name, desc, &any.value)? {
Some(entry) => rendered.details.push(entry),
None => continue,
},
None if self.opaque => {
rendered
.opaque
.push(opaque_entry(index, type_url, &any.value));
}
None => {
tracing::debug!(%type_url, "error detail of an unknown type withheld");
continue;
}
}
index += 1;
}
Ok(rendered)
}
fn typed_entry(
&self,
type_url: &str,
type_name: &str,
desc: MessageDescriptor,
value: &[u8],
) -> Result<Option<Value>, MalformedStatus> {
let wrapped = has_special_json(desc.full_name());
let mut msg = DynamicMessage::decode(desc, value).map_err(|e| {
tracing::error!(detail = %type_name, "undecodable error detail: {e}");
MalformedStatus
})?;
if self.scrub(&mut msg)?.is_none() {
return Ok(None);
}
let json = msg
.serialize_with_options(serde_json::value::Serializer, &SerializeOptions::new())
.map_err(|e| {
tracing::error!(detail = %type_name, "error detail has no valid JSON form: {e}");
MalformedStatus
})?;
let mut out = Map::new();
out.insert("@type".into(), type_url.into());
match json {
Value::Object(fields) if !wrapped => {
if fields.contains_key("@type") {
tracing::error!(detail = %type_name, "error detail has a field named @type");
return Err(MalformedStatus);
}
out.extend(fields);
}
other => {
out.insert("value".into(), other);
}
}
Ok(Some(Value::Object(out)))
}
fn resolve(&self, type_name: &str) -> Option<MessageDescriptor> {
self.pool.get_message_by_name(type_name)
}
fn scrub(&self, msg: &mut DynamicMessage) -> Result<Option<bool>, MalformedStatus> {
let desc = msg.descriptor();
if desc.full_name() == ANY {
return self.scrub_any(msg);
}
if desc.full_name() == DEBUG_INFO {
return Ok(None);
}
for field in desc.fields() {
if field.cardinality() == Cardinality::Required && !msg.has_field(&field) {
tracing::error!(
message = %desc.full_name(),
field = %field.name(),
"error detail lacks a required field"
);
return Err(MalformedStatus);
}
}
let mut changed = false;
let mut dropped_fields = Vec::new();
for (field, value) in msg.fields_mut() {
if !matches!(field.kind(), Kind::Message(_)) {
continue;
}
if !self.scrub_value(value, &mut changed)? {
if field.cardinality() == Cardinality::Required {
return Ok(None);
}
dropped_fields.push(field);
}
}
let mut dropped_extensions = Vec::new();
for (extension, value) in msg.extensions_mut() {
if !matches!(extension.kind(), Kind::Message(_)) {
continue;
}
if !self.scrub_value(value, &mut changed)? {
dropped_extensions.push(extension);
}
}
for field in dropped_fields {
msg.clear_field(&field);
changed = true;
}
for extension in dropped_extensions {
msg.clear_extension(&extension);
changed = true;
}
Ok(Some(changed))
}
fn scrub_value(
&self,
value: &mut PbValue,
changed: &mut bool,
) -> Result<bool, MalformedStatus> {
match value {
PbValue::Message(inner) => match self.scrub(inner)? {
Some(inner_changed) => *changed |= inner_changed,
None => return Ok(false),
},
PbValue::List(items) => {
let before = items.len();
let mut failed = None;
items.retain_mut(|item| match item {
PbValue::Message(inner) if failed.is_none() => match self.scrub(inner) {
Ok(Some(inner_changed)) => {
*changed |= inner_changed;
true
}
Ok(None) => false,
Err(e) => {
failed = Some(e);
true
}
},
_ => true,
});
if let Some(e) = failed {
return Err(e);
}
*changed |= items.len() != before;
}
PbValue::Map(entries) => {
let mut dropped = Vec::new();
for (key, value) in entries.iter_mut() {
if let PbValue::Message(inner) = value {
match self.scrub(inner)? {
Some(inner_changed) => *changed |= inner_changed,
None => dropped.push(key.clone()),
}
}
}
for key in dropped {
entries.remove(&key);
*changed = true;
}
}
_ => {}
}
Ok(true)
}
fn scrub_any(&self, any: &mut DynamicMessage) -> Result<Option<bool>, MalformedStatus> {
let type_url = match any.get_field_by_name("type_url").as_deref() {
Some(PbValue::String(url)) => url.clone(),
_ => String::new(),
};
let Some(type_name) = any_type_name(&type_url) else {
tracing::error!(%type_url, "packed Any with a malformed type URL");
return Err(MalformedStatus);
};
if type_name == DEBUG_INFO {
return Ok(None);
}
let Some(desc) = self.resolve(type_name) else {
return Ok(Some(false));
};
let bytes = match any.get_field_by_name("value").as_deref() {
Some(PbValue::Bytes(bytes)) => bytes.clone(),
_ => Default::default(),
};
let mut inner = DynamicMessage::decode(desc, bytes).map_err(|e| {
tracing::error!(detail = %type_name, "undecodable packed Any: {e}");
MalformedStatus
})?;
match self.scrub(&mut inner)? {
None => Ok(None),
Some(false) => Ok(Some(false)),
Some(true) => {
any.set_field_by_name("value", PbValue::Bytes(inner.encode_to_vec().into()));
Ok(Some(true))
}
}
}
}
#[cfg(test)]
mod tests;