Skip to content

Commit

Permalink
tonic-reflection: expose both v1alpha and v1 reflection server builders
Browse files Browse the repository at this point in the history
While testing the new tonic 0.12 release, we discovered that
hyperium#1701 upgraded the reflection
protocol to `v1` from `v1alpha`. This breaks compatibility with popular
gRPC debugging tools like `Postman` and `Kreya`, who only use the
`v1alpha` protocol.

Changes:
* Expose V1Builder and V1AlphaBuilder for the different protocols
* Add a type alias Builder for backwards compatibility
* Move the descriptor parser outside the builder
* Add basic tests for the different protocol versions
  • Loading branch information
ttkjesper committed Jul 10, 2024
1 parent 941726c commit 1b86eff
Show file tree
Hide file tree
Showing 5 changed files with 671 additions and 192 deletions.
43 changes: 43 additions & 0 deletions tonic-reflection/src/server/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
pub use crate::pb::v1::server_reflection_server as server_reflection_server_v1;
pub use crate::pb::v1alpha::server_reflection_server as server_reflection_server_v1alpha;

pub use crate::server::v1::V1Builder;
pub use crate::server::v1alpha::V1AlphaBuilder;

use prost::DecodeError;
use std::fmt::{Display, Formatter};

mod parser;
mod v1;
mod v1alpha;

/// Represents an error in the construction of a gRPC Reflection Service.
#[derive(Debug)]
pub enum Error {
/// An error was encountered decoding a `prost_types::FileDescriptorSet` from a buffer.
DecodeError(prost::DecodeError),
/// An invalid `prost_types::FileDescriptorProto` was encountered.
InvalidFileDescriptorSet(String),
}

impl From<DecodeError> for Error {
fn from(e: DecodeError) -> Self {
Error::DecodeError(e)
}
}

impl std::error::Error for Error {}

impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Error::DecodeError(_) => f.write_str("error decoding FileDescriptorSet from buffer"),
Error::InvalidFileDescriptorSet(s) => {
write!(f, "invalid FileDescriptorSet - {}", s)
}
}
}
}

/// A builder used to construct a gRPC Reflection Service (backwards compatibility).
pub type Builder<'b> = V1Builder<'b>;
171 changes: 171 additions & 0 deletions tonic-reflection/src/server/parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use crate::server::Error;
use prost::Message;
use prost_types::{
DescriptorProto, EnumDescriptorProto, FieldDescriptorProto, FileDescriptorProto,
FileDescriptorSet,
};
use std::collections::HashMap;
use std::sync::Arc;

#[derive(Default)]
pub(crate) struct ParserInfo {
pub(crate) service_names: Vec<String>,
pub(crate) symbols: HashMap<String, Arc<FileDescriptorProto>>,
pub(crate) files: HashMap<String, Arc<FileDescriptorProto>>,
}

pub(crate) fn process(
encoded_file_descriptor_sets: Vec<&[u8]>,
file_descriptor_sets: Vec<FileDescriptorSet>,
) -> Result<ParserInfo, Error> {
let mut all_fds = file_descriptor_sets.clone();

for encoded in &encoded_file_descriptor_sets {
let decoded = FileDescriptorSet::decode(*encoded)?;
all_fds.push(decoded);
}

let mut info = ParserInfo::default();

for fds in all_fds {
for fd in fds.file {
let name = match fd.name.clone() {
None => {
return Err(Error::InvalidFileDescriptorSet("missing name".to_string()));
}
Some(n) => n,
};

if info.files.contains_key(&name) {
continue;
}

let fd = Arc::new(fd);
info.files.insert(name, fd.clone());

let result = process_file(fd)?;

info.service_names.extend(result.service_names);
info.symbols.extend(result.symbols.into_iter());
}
}

Ok(info)
}

type SymbolArray = Vec<(String, Arc<FileDescriptorProto>)>;

struct File {
service_names: Vec<String>,
symbols: SymbolArray,
}

fn process_file(fd: Arc<FileDescriptorProto>) -> Result<File, Error> {
let prefix = &fd.package.clone().unwrap_or_default();
let mut service_names = vec![];
let mut symbols = vec![];

for msg in &fd.message_type {
symbols.extend(process_message(fd.clone(), prefix, msg)?);
}

for en in &fd.enum_type {
symbols.extend(process_enum(fd.clone(), prefix, en)?);
}

for service in &fd.service {
let service_name = extract_name(prefix, "service", service.name.as_ref())?;
service_names.push(service_name.clone());
symbols.extend(vec![(service_name.clone(), fd.clone())]);

for method in &service.method {
let method_name = extract_name(&service_name, "method", method.name.as_ref())?;
symbols.extend(vec![(method_name, fd.clone())]);
}
}

Ok(File {
service_names,
symbols,
})
}

fn process_message(
fd: Arc<FileDescriptorProto>,
prefix: &str,
msg: &DescriptorProto,
) -> Result<SymbolArray, Error> {
let message_name = extract_name(prefix, "message", msg.name.as_ref())?;
let mut symbols = vec![(message_name.clone(), fd.clone())];

for nested in &msg.nested_type {
symbols.extend(process_message(fd.clone(), &message_name, nested)?);
}

for en in &msg.enum_type {
symbols.extend(process_enum(fd.clone(), &message_name, en)?);
}

for field in &msg.field {
symbols.extend(process_field(fd.clone(), &message_name, field)?);
}

for oneof in &msg.oneof_decl {
let oneof_name = extract_name(&message_name, "oneof", oneof.name.as_ref())?;
symbols.extend(vec![(oneof_name, fd.clone())]);
}

Ok(symbols)
}

fn process_enum(
fd: Arc<FileDescriptorProto>,
prefix: &str,
en: &EnumDescriptorProto,
) -> Result<SymbolArray, Error> {
let enum_name = extract_name(prefix, "enum", en.name.as_ref())?;

let enums = (&en.value)
.iter()
.map(|value| {
let value_name = extract_name(&enum_name, "enum value", value.name.as_ref())?;
Ok((value_name, fd.clone()))
})
.collect::<Result<Vec<(String, Arc<FileDescriptorProto>)>, Error>>()?;

let symbols = vec![(enum_name.clone(), fd.clone())]
.into_iter()
.chain(enums.into_iter())
.collect();

Ok(symbols)
}

fn process_field(
fd: Arc<FileDescriptorProto>,
prefix: &str,
field: &FieldDescriptorProto,
) -> Result<SymbolArray, Error> {
let field_name = extract_name(prefix, "field", field.name.as_ref())?;
Ok(vec![(field_name, fd)])
}

fn extract_name(
prefix: &str,
name_type: &str,
maybe_name: Option<&String>,
) -> Result<String, Error> {
match maybe_name {
None => Err(Error::InvalidFileDescriptorSet(format!(
"missing {} name",
name_type
))),
Some(name) => {
if prefix.is_empty() {
Ok(name.to_string())
} else {
Ok(format!("{}.{}", prefix, name))
}
}
}
}
Loading

0 comments on commit 1b86eff

Please sign in to comment.