diff --git a/Cargo.toml b/Cargo.toml index bbe5a00b1..3464c1fc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "tonic-build", "tonic-health", "tonic-types", + "tonic-reflection", # Non-published crates "examples", @@ -18,3 +19,4 @@ members = [ "tests/extern_path/my_application", "tests/integration_tests" ] + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 3524c4c89..a706bbec5 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -130,6 +130,10 @@ path = "src/hyper_warp/server.rs" name = "health-server" path = "src/health/server.rs" +[[bin]] +name = "reflection-server" +path = "src/reflection/server.rs" + [[bin]] name = "autoreload-server" path = "src/autoreload/server.rs" @@ -173,6 +177,8 @@ http-body = "0.4" pin-project = "1.0" # Health example tonic-health = { path = "../tonic-health" } +# Reflection example +tonic-reflection = { path = "../tonic-reflection" } listenfd = "0.3" [build-dependencies] diff --git a/examples/README.md b/examples/README.md index b474e76c2..e71c5bdc3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -94,6 +94,13 @@ $ cargo run --bin tls-server $ cargo run --bin health-server ``` +## Server Reflection + +### Server +```bash +$ cargo run --bin reflection-server +``` + ## Tower Middleware ### Server diff --git a/examples/build.rs b/examples/build.rs index 19de0877c..9674a9170 100644 --- a/examples/build.rs +++ b/examples/build.rs @@ -1,10 +1,18 @@ +use std::env; +use std::path::PathBuf; + fn main() { tonic_build::configure() .type_attribute("routeguide.Point", "#[derive(Hash)]") .compile(&["proto/routeguide/route_guide.proto"], &["proto"]) .unwrap(); - tonic_build::compile_protos("proto/helloworld/helloworld.proto").unwrap(); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + tonic_build::configure() + .file_descriptor_set_path(out_dir.join("helloworld_descriptor.bin")) + .compile(&["proto/helloworld/helloworld.proto"], &["proto"]) + .unwrap(); + tonic_build::compile_protos("proto/echo/echo.proto").unwrap(); tonic_build::configure() diff --git a/examples/src/reflection/server.rs b/examples/src/reflection/server.rs new file mode 100644 index 000000000..e0632fe81 --- /dev/null +++ b/examples/src/reflection/server.rs @@ -0,0 +1,46 @@ +use tonic::transport::Server; +use tonic::{Request, Response, Status}; + +mod proto { + tonic::include_proto!("helloworld"); + + pub(crate) const FILE_DESCRIPTOR_SET: &'static [u8] = + tonic::include_file_descriptor_set!("helloworld_descriptor"); +} + +#[derive(Default)] +pub struct MyGreeter {} + +#[tonic::async_trait] +impl proto::greeter_server::Greeter for MyGreeter { + async fn say_hello( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request from {:?}", request.remote_addr()); + + let reply = proto::HelloReply { + message: format!("Hello {}!", request.into_inner().name), + }; + Ok(Response::new(reply)) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let service = tonic_reflection::server::Builder::configure() + .register_encoded_file_descriptor_set(proto::FILE_DESCRIPTOR_SET) + .build() + .unwrap(); + + let addr = "[::1]:50052".parse().unwrap(); + let greeter = MyGreeter::default(); + + Server::builder() + .add_service(service) + .add_service(proto::greeter_server::GreeterServer::new(greeter)) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/tonic-build/src/prost.rs b/tonic-build/src/prost.rs index f8284d4f0..698d87432 100644 --- a/tonic-build/src/prost.rs +++ b/tonic-build/src/prost.rs @@ -12,6 +12,7 @@ pub fn configure() -> Builder { Builder { build_client: true, build_server: true, + file_descriptor_set_path: None, out_dir: None, extern_path: Vec::new(), field_attributes: Vec::new(), @@ -180,6 +181,7 @@ impl prost_build::ServiceGenerator for ServiceGenerator { pub struct Builder { pub(crate) build_client: bool, pub(crate) build_server: bool, + pub(crate) file_descriptor_set_path: Option, pub(crate) extern_path: Vec<(String, String)>, pub(crate) field_attributes: Vec<(String, String)>, pub(crate) type_attributes: Vec<(String, String)>, @@ -203,6 +205,13 @@ impl Builder { self } + /// Generate a file containing the encoded `prost_types::FileDescriptorSet` for protocol buffers + /// modules. This is required for implementing gRPC Server Reflection. + pub fn file_descriptor_set_path(mut self, path: impl AsRef) -> Self { + self.file_descriptor_set_path = Some(path.as_ref().to_path_buf()); + self + } + /// Enable the output to be formated by rustfmt. #[cfg(feature = "rustfmt")] pub fn format(mut self, run: bool) -> Self { @@ -287,6 +296,9 @@ impl Builder { let format = self.format; config.out_dir(out_dir.clone()); + if let Some(path) = self.file_descriptor_set_path.as_ref() { + config.file_descriptor_set_path(path); + } for (proto_path, rust_path) in self.extern_path.iter() { config.extern_path(proto_path, rust_path); } diff --git a/tonic-reflection/Cargo.toml b/tonic-reflection/Cargo.toml new file mode 100644 index 000000000..8c28b3501 --- /dev/null +++ b/tonic-reflection/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "tonic-reflection" +version = "0.1.0" +authors = [ + "James Nugent ", + "Samani G. Gikandi " +] +edition = "2018" +license = "MIT" +repository = "https://github.com/hyperium/tonic" +homepage = "https://github.com/hyperium/tonic" +description = """ +Server Reflection module of `tonic` gRPC implementation. +""" +readme = "README.md" +categories = ["network-programming", "asynchronous"] +keywords = ["rpc", "grpc", "async", "reflection"] + +[dependencies] +bytes = "1.0" +prost = "0.7" +prost-types = "0.7" +tokio = { version = "1.0", features = ["sync"] } +tokio-stream = { version = "0.1", features = ["net"] } +tonic = { version = "0.4", path = "../tonic", features = ["codegen", "prost"] } + +[build-dependencies] +tonic-build = { version = "0.4", path = "../tonic-build" } + +[dev-dependencies] +futures = "0.3" +futures-util = "0.3" diff --git a/tonic-reflection/build.rs b/tonic-reflection/build.rs new file mode 100644 index 000000000..de2f08e1f --- /dev/null +++ b/tonic-reflection/build.rs @@ -0,0 +1,16 @@ +use std::env; +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + let reflection_descriptor = + PathBuf::from(env::var("OUT_DIR").unwrap()).join("reflection_v1alpha1.bin"); + + tonic_build::configure() + .file_descriptor_set_path(&reflection_descriptor) + .build_server(true) + .build_client(true) // Client is only used for tests + .format(true) + .compile(&["proto/reflection.proto"], &["proto/"])?; + + Ok(()) +} diff --git a/tonic-reflection/proto/reflection.proto b/tonic-reflection/proto/reflection.proto new file mode 100644 index 000000000..c2da31461 --- /dev/null +++ b/tonic-reflection/proto/reflection.proto @@ -0,0 +1,136 @@ +// Copyright 2016 gRPC authors. +// +// 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. + +// Service exported by server reflection + +syntax = "proto3"; + +package grpc.reflection.v1alpha; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of extendee_type, and + // appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server sets one of the following fields according to the + // message_request in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. + // As the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requests. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services requests. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} \ No newline at end of file diff --git a/tonic-reflection/src/lib.rs b/tonic-reflection/src/lib.rs new file mode 100644 index 000000000..656797ef0 --- /dev/null +++ b/tonic-reflection/src/lib.rs @@ -0,0 +1,26 @@ +//! A `tonic` based gRPC Server Reflection implementation. + +#![warn( + missing_debug_implementations, + missing_docs, + rust_2018_idioms, + unreachable_pub +)] +#![doc( + html_logo_url = "https://github.com/hyperium/tonic/raw/master/.github/assets/tonic-docs.png" +)] +#![doc(html_root_url = "https://docs.rs/tonic-reflection/0.1.0")] +#![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] +#![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))] +#![cfg_attr(docsrs, feature(doc_cfg))] + +pub(crate) mod proto { + #![allow(unreachable_pub)] + tonic::include_proto!("grpc.reflection.v1alpha"); + + pub(crate) const FILE_DESCRIPTOR_SET: &'static [u8] = + tonic::include_file_descriptor_set!("reflection_v1alpha1"); +} + +/// Implementation of the server component of gRPC Server Reflection. +pub mod server; diff --git a/tonic-reflection/src/server.rs b/tonic-reflection/src/server.rs new file mode 100644 index 000000000..8f94bdf04 --- /dev/null +++ b/tonic-reflection/src/server.rs @@ -0,0 +1,361 @@ +use crate::proto::server_reflection_request::MessageRequest; +use crate::proto::server_reflection_response::MessageResponse; +use crate::proto::server_reflection_server::{ServerReflection, ServerReflectionServer}; +use crate::proto::{ + FileDescriptorResponse, ListServiceResponse, ServerReflectionRequest, ServerReflectionResponse, + ServiceResponse, +}; +use prost::{DecodeError, Message}; +use prost_types::{ + DescriptorProto, EnumDescriptorProto, FieldDescriptorProto, FileDescriptorProto, + FileDescriptorSet, +}; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::sync::Arc; +use tokio::sync::mpsc; +use tokio_stream::{wrappers::ReceiverStream, StreamExt}; +use tonic::{Request, Response, Status, Streaming}; + +/// 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 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) => { + f.write_fmt(format_args!("invalid FileDescriptorSet - {}", s)) + } + } + } +} + +/// A builder used to construct a gRPC Reflection Service. +#[derive(Debug)] +pub struct Builder<'b> { + file_descriptor_sets: Vec, + encoded_file_descriptor_sets: Vec<&'b [u8]>, + include_reflection_service: bool, + + service_names: Vec, + symbols: HashMap>, +} + +impl<'b> Builder<'b> { + /// Create a new builder that can configure a gRPC Reflection Service. + pub fn configure() -> Self { + Builder { + file_descriptor_sets: Vec::new(), + encoded_file_descriptor_sets: Vec::new(), + include_reflection_service: true, + + service_names: Vec::new(), + symbols: HashMap::new(), + } + } + + /// Registers an instance of `prost_types::FileDescriptorSet` with the gRPC Reflection + /// Service builder. + pub fn register_file_descriptor_set(mut self, file_descriptor_set: FileDescriptorSet) -> Self { + self.file_descriptor_sets.push(file_descriptor_set); + self + } + + /// Registers a byte slice containing an encoded `prost_types::FileDescriptorSet` with + /// the gRPC Reflection Service builder. + pub fn register_encoded_file_descriptor_set( + mut self, + encoded_file_descriptor_set: &'b [u8], + ) -> Self { + self.encoded_file_descriptor_sets + .push(encoded_file_descriptor_set); + self + } + + /// Serve the gRPC Refection Service descriptor via the Reflection Service. This is enabled + /// by default - set `include` to false to disable. + pub fn include_reflection_service(mut self, include: bool) -> Self { + self.include_reflection_service = include; + self + } + + /// Build a gRPC Reflection Service to be served via Tonic. + pub fn build(mut self) -> Result, Error> { + if self.include_reflection_service { + self = self.register_encoded_file_descriptor_set(crate::proto::FILE_DESCRIPTOR_SET); + } + + for encoded in &self.encoded_file_descriptor_sets { + let decoded = FileDescriptorSet::decode(*encoded)?; + self.file_descriptor_sets.push(decoded); + } + + let all_fds = self.file_descriptor_sets.clone(); + let mut files: HashMap> = HashMap::new(); + + 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 files.contains_key(&name) { + continue; + } + + let fd = Arc::new(fd); + files.insert(name, fd.clone()); + + self.process_file(fd)?; + } + } + + let service_names = self + .service_names + .iter() + .map(|name| ServiceResponse { name: name.clone() }) + .collect(); + + Ok(ServerReflectionServer::new(ReflectionService { + state: Arc::new(ReflectionServiceState { + service_names, + files, + symbols: self.symbols, + }), + })) + } + + fn process_file(&mut self, fd: Arc) -> Result<(), Error> { + let prefix = &fd.package.clone().unwrap_or_default(); + + for msg in &fd.message_type { + self.process_message(fd.clone(), &prefix, msg)?; + } + + for en in &fd.enum_type { + self.process_enum(fd.clone(), &prefix, en)?; + } + + for service in &fd.service { + let service_name = extract_name(&prefix, "service", service.name.as_ref())?; + self.service_names.push(service_name.clone()); + self.symbols.insert(service_name.clone(), fd.clone()); + + for method in &service.method { + let method_name = extract_name(&service_name, "method", method.name.as_ref())?; + self.symbols.insert(method_name, fd.clone()); + } + } + + Ok(()) + } + + fn process_message( + &mut self, + fd: Arc, + prefix: &str, + msg: &DescriptorProto, + ) -> Result<(), Error> { + let message_name = extract_name(prefix, "message", msg.name.as_ref())?; + self.symbols.insert(message_name.clone(), fd.clone()); + + for nested in &msg.nested_type { + self.process_message(fd.clone(), &message_name, nested)?; + } + + for en in &msg.enum_type { + self.process_enum(fd.clone(), &message_name, en)?; + } + + for field in &msg.field { + self.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())?; + self.symbols.insert(oneof_name, fd.clone()); + } + + Ok(()) + } + + fn process_enum( + &mut self, + fd: Arc, + prefix: &str, + en: &EnumDescriptorProto, + ) -> Result<(), Error> { + let enum_name = extract_name(prefix, "enum", en.name.as_ref())?; + self.symbols.insert(enum_name.clone(), fd.clone()); + + for value in &en.value { + let value_name = extract_name(&enum_name, "enum value", value.name.as_ref())?; + self.symbols.insert(value_name, fd.clone()); + } + + Ok(()) + } + + fn process_field( + &mut self, + fd: Arc, + prefix: &str, + field: &FieldDescriptorProto, + ) -> Result<(), Error> { + let field_name = extract_name(prefix, "field", field.name.as_ref())?; + self.symbols.insert(field_name, fd.clone()); + Ok(()) + } +} + +fn extract_name( + prefix: &str, + name_type: &str, + maybe_name: Option<&String>, +) -> Result { + 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)) + } + } + } +} + +#[derive(Debug)] +struct ReflectionServiceState { + service_names: Vec, + files: HashMap>, + symbols: HashMap>, +} + +impl ReflectionServiceState { + fn list_services(&self) -> MessageResponse { + MessageResponse::ListServicesResponse(ListServiceResponse { + service: self.service_names.clone(), + }) + } + + fn symbol_by_name(&self, symbol: &str) -> Result { + match self.symbols.get(symbol) { + None => Err(Status::not_found(format!("symbol '{}' not found", symbol))), + Some(fd) => { + let mut encoded_fd = Vec::new(); + if let Err(_) = fd.clone().encode(&mut encoded_fd) { + return Err(Status::internal("encoding error")); + }; + + Ok(MessageResponse::FileDescriptorResponse( + FileDescriptorResponse { + file_descriptor_proto: vec![encoded_fd], + }, + )) + } + } + } + + fn file_by_filename(&self, filename: &str) -> Result { + match self.files.get(filename) { + None => Err(Status::not_found(format!("file '{}' not found", filename))), + Some(fd) => { + let mut encoded_fd = Vec::new(); + if let Err(_) = fd.clone().encode(&mut encoded_fd) { + return Err(Status::internal("encoding error")); + } + + Ok(MessageResponse::FileDescriptorResponse( + FileDescriptorResponse { + file_descriptor_proto: vec![encoded_fd], + }, + )) + } + } + } +} + +#[derive(Debug)] +struct ReflectionService { + state: Arc, +} + +#[tonic::async_trait] +impl ServerReflection for ReflectionService { + type ServerReflectionInfoStream = ReceiverStream>; + + async fn server_reflection_info( + &self, + req: Request>, + ) -> Result, Status> { + let mut req_rx = req.into_inner(); + let (resp_tx, resp_rx) = mpsc::channel::>(1); + + let state = self.state.clone(); + + tokio::spawn(async move { + while let Some(req) = req_rx.next().await { + let req = match req { + Ok(req) => req, + Err(_) => { + return; + } + }; + + let resp_msg = match req.message_request.clone() { + None => Err(Status::invalid_argument("invalid MessageRequest")), + Some(msg) => match msg { + MessageRequest::FileByFilename(s) => state.file_by_filename(&s), + MessageRequest::FileContainingSymbol(s) => state.symbol_by_name(&s), + MessageRequest::FileContainingExtension(_) => { + Err(Status::not_found("extensions are not supported")) + } + MessageRequest::AllExtensionNumbersOfType(_) => { + Err(Status::not_found("extensions are not supported")) + } + MessageRequest::ListServices(_) => Ok(state.list_services()), + }, + }; + + match resp_msg { + Ok(resp_msg) => { + let resp = ServerReflectionResponse { + valid_host: req.host.clone(), + original_request: Some(req.clone()), + message_response: Some(resp_msg), + }; + resp_tx.send(Ok(resp)).await.expect("send"); + } + Err(status) => { + resp_tx.send(Err(status)).await.expect("send"); + return; + } + } + } + }); + + Ok(Response::new(ReceiverStream::new(resp_rx))) + } +} diff --git a/tonic-reflection/tests/proto/grpc.reflection.v1alpha.rs b/tonic-reflection/tests/proto/grpc.reflection.v1alpha.rs new file mode 100644 index 000000000..bdd4cba96 --- /dev/null +++ b/tonic-reflection/tests/proto/grpc.reflection.v1alpha.rs @@ -0,0 +1,337 @@ +/// The message sent by the client when calling ServerReflectionInfo method. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ServerReflectionRequest { + #[prost(string, tag = "1")] + pub host: ::prost::alloc::string::String, + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + #[prost( + oneof = "server_reflection_request::MessageRequest", + tags = "3, 4, 5, 6, 7" + )] + pub message_request: ::core::option::Option, +} +/// Nested message and enum types in `ServerReflectionRequest`. +pub mod server_reflection_request { + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum MessageRequest { + /// Find a proto file by the file name. + #[prost(string, tag = "3")] + FileByFilename(::prost::alloc::string::String), + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + #[prost(string, tag = "4")] + FileContainingSymbol(::prost::alloc::string::String), + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + #[prost(message, tag = "5")] + FileContainingExtension(super::ExtensionRequest), + /// Finds the tag numbers used by all known extensions of extendee_type, and + /// appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + #[prost(string, tag = "6")] + AllExtensionNumbersOfType(::prost::alloc::string::String), + /// List the full names of registered services. The content will not be + /// checked. + #[prost(string, tag = "7")] + ListServices(::prost::alloc::string::String), + } +} +/// The type name and extension number sent by the client when requesting +/// file_containing_extension. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExtensionRequest { + /// Fully-qualified type name. The format should be . + #[prost(string, tag = "1")] + pub containing_type: ::prost::alloc::string::String, + #[prost(int32, tag = "2")] + pub extension_number: i32, +} +/// The message sent by the server to answer ServerReflectionInfo method. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ServerReflectionResponse { + #[prost(string, tag = "1")] + pub valid_host: ::prost::alloc::string::String, + #[prost(message, optional, tag = "2")] + pub original_request: ::core::option::Option, + /// The server sets one of the following fields according to the + /// message_request in the request. + #[prost( + oneof = "server_reflection_response::MessageResponse", + tags = "4, 5, 6, 7" + )] + pub message_response: ::core::option::Option, +} +/// Nested message and enum types in `ServerReflectionResponse`. +pub mod server_reflection_response { + /// The server sets one of the following fields according to the + /// message_request in the request. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum MessageResponse { + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. + /// As the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + #[prost(message, tag = "4")] + FileDescriptorResponse(super::FileDescriptorResponse), + /// This message is used to answer all_extension_numbers_of_type requests. + #[prost(message, tag = "5")] + AllExtensionNumbersResponse(super::ExtensionNumberResponse), + /// This message is used to answer list_services requests. + #[prost(message, tag = "6")] + ListServicesResponse(super::ListServiceResponse), + /// This message is used when an error occurs. + #[prost(message, tag = "7")] + ErrorResponse(super::ErrorResponse), + } +} +/// Serialized FileDescriptorProto messages sent by the server answering +/// a file_by_filename, file_containing_symbol, or file_containing_extension +/// request. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FileDescriptorResponse { + /// Serialized FileDescriptorProto messages. We avoid taking a dependency on + /// descriptor.proto, which uses proto2 only features, by making them opaque + /// bytes instead. + #[prost(bytes = "vec", repeated, tag = "1")] + pub file_descriptor_proto: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +/// A list of extension numbers sent by the server answering +/// all_extension_numbers_of_type request. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExtensionNumberResponse { + /// Full name of the base type, including the package name. The format + /// is . + #[prost(string, tag = "1")] + pub base_type_name: ::prost::alloc::string::String, + #[prost(int32, repeated, tag = "2")] + pub extension_number: ::prost::alloc::vec::Vec, +} +/// A list of ServiceResponse sent by the server answering list_services request. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListServiceResponse { + /// The information of each service may be expanded in the future, so we use + /// ServiceResponse message to encapsulate it. + #[prost(message, repeated, tag = "1")] + pub service: ::prost::alloc::vec::Vec, +} +/// The information of a single service used by ListServiceResponse to answer +/// list_services request. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ServiceResponse { + /// Full name of a registered service, including its package name. The format + /// is . + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, +} +/// The error code and error message sent by the server when an error occurs. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ErrorResponse { + /// This field uses the error codes defined in grpc::StatusCode. + #[prost(int32, tag = "1")] + pub error_code: i32, + #[prost(string, tag = "2")] + pub error_message: ::prost::alloc::string::String, +} +#[doc = r" Generated client implementations."] +pub mod server_reflection_client { + #![allow(unused_variables, dead_code, missing_docs)] + use tonic::codegen::*; + pub struct ServerReflectionClient { + inner: tonic::client::Grpc, + } + impl ServerReflectionClient { + #[doc = r" Attempt to create a new client by connecting to a given endpoint."] + pub async fn connect(dst: D) -> Result + where + D: std::convert::TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ServerReflectionClient + where + T: tonic::client::GrpcService, + T::ResponseBody: Body + HttpBody + Send + 'static, + T::Error: Into, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_interceptor(inner: T, interceptor: impl Into) -> Self { + let inner = tonic::client::Grpc::with_interceptor(inner, interceptor); + Self { inner } + } + #[doc = " The reflection service is structured as a bidirectional stream, ensuring"] + #[doc = " all related requests go to a single server."] + pub async fn server_reflection_info( + &mut self, + request: impl tonic::IntoStreamingRequest, + ) -> Result< + tonic::Response>, + tonic::Status, + > { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", + ); + self.inner + .streaming(request.into_streaming_request(), path, codec) + .await + } + } + impl Clone for ServerReflectionClient { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + } + impl std::fmt::Debug for ServerReflectionClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ServerReflectionClient {{ ... }}") + } + } +} +#[doc = r" Generated server implementations."] +pub mod server_reflection_server { + #![allow(unused_variables, dead_code, missing_docs)] + use tonic::codegen::*; + #[doc = "Generated trait containing gRPC methods that should be implemented for use with ServerReflectionServer."] + #[async_trait] + pub trait ServerReflection: Send + Sync + 'static { + #[doc = "Server streaming response type for the ServerReflectionInfo method."] + type ServerReflectionInfoStream: Stream> + + Send + + Sync + + 'static; + #[doc = " The reflection service is structured as a bidirectional stream, ensuring"] + #[doc = " all related requests go to a single server."] + async fn server_reflection_info( + &self, + request: tonic::Request>, + ) -> Result, tonic::Status>; + } + #[derive(Debug)] + pub struct ServerReflectionServer { + inner: _Inner, + } + struct _Inner(Arc, Option); + impl ServerReflectionServer { + pub fn new(inner: T) -> Self { + let inner = Arc::new(inner); + let inner = _Inner(inner, None); + Self { inner } + } + pub fn with_interceptor(inner: T, interceptor: impl Into) -> Self { + let inner = Arc::new(inner); + let inner = _Inner(inner, Some(interceptor.into())); + Self { inner } + } + } + impl Service> for ServerReflectionServer + where + T: ServerReflection, + B: HttpBody + Send + Sync + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = Never; + type Future = BoxFuture; + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" => { + #[allow(non_camel_case_types)] + struct ServerReflectionInfoSvc(pub Arc); + impl + tonic::server::StreamingService + for ServerReflectionInfoSvc + { + type Response = super::ServerReflectionResponse; + type ResponseStream = T::ServerReflectionInfoStream; + type Future = + BoxFuture, tonic::Status>; + fn call( + &mut self, + request: tonic::Request< + tonic::Streaming, + >, + ) -> Self::Future { + let inner = self.0.clone(); + let fut = async move { (*inner).server_reflection_info(request).await }; + Box::pin(fut) + } + } + let inner = self.inner.clone(); + let fut = async move { + let interceptor = inner.1; + let inner = inner.0; + let method = ServerReflectionInfoSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = if let Some(interceptor) = interceptor { + tonic::server::Grpc::with_interceptor(codec, interceptor) + } else { + tonic::server::Grpc::new(codec) + }; + let res = grpc.streaming(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => Box::pin(async move { + Ok(http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(tonic::body::BoxBody::empty()) + .unwrap()) + }), + } + } + } + impl Clone for ServerReflectionServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { inner } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(self.0.clone(), self.1.clone()) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::transport::NamedService for ServerReflectionServer { + const NAME: &'static str = "grpc.reflection.v1alpha.ServerReflection"; + } +} diff --git a/tonic-reflection/tests/proto/mod.rs b/tonic-reflection/tests/proto/mod.rs new file mode 100644 index 000000000..bdd4cba96 --- /dev/null +++ b/tonic-reflection/tests/proto/mod.rs @@ -0,0 +1,337 @@ +/// The message sent by the client when calling ServerReflectionInfo method. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ServerReflectionRequest { + #[prost(string, tag = "1")] + pub host: ::prost::alloc::string::String, + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + #[prost( + oneof = "server_reflection_request::MessageRequest", + tags = "3, 4, 5, 6, 7" + )] + pub message_request: ::core::option::Option, +} +/// Nested message and enum types in `ServerReflectionRequest`. +pub mod server_reflection_request { + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum MessageRequest { + /// Find a proto file by the file name. + #[prost(string, tag = "3")] + FileByFilename(::prost::alloc::string::String), + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + #[prost(string, tag = "4")] + FileContainingSymbol(::prost::alloc::string::String), + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + #[prost(message, tag = "5")] + FileContainingExtension(super::ExtensionRequest), + /// Finds the tag numbers used by all known extensions of extendee_type, and + /// appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + #[prost(string, tag = "6")] + AllExtensionNumbersOfType(::prost::alloc::string::String), + /// List the full names of registered services. The content will not be + /// checked. + #[prost(string, tag = "7")] + ListServices(::prost::alloc::string::String), + } +} +/// The type name and extension number sent by the client when requesting +/// file_containing_extension. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExtensionRequest { + /// Fully-qualified type name. The format should be . + #[prost(string, tag = "1")] + pub containing_type: ::prost::alloc::string::String, + #[prost(int32, tag = "2")] + pub extension_number: i32, +} +/// The message sent by the server to answer ServerReflectionInfo method. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ServerReflectionResponse { + #[prost(string, tag = "1")] + pub valid_host: ::prost::alloc::string::String, + #[prost(message, optional, tag = "2")] + pub original_request: ::core::option::Option, + /// The server sets one of the following fields according to the + /// message_request in the request. + #[prost( + oneof = "server_reflection_response::MessageResponse", + tags = "4, 5, 6, 7" + )] + pub message_response: ::core::option::Option, +} +/// Nested message and enum types in `ServerReflectionResponse`. +pub mod server_reflection_response { + /// The server sets one of the following fields according to the + /// message_request in the request. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum MessageResponse { + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. + /// As the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + #[prost(message, tag = "4")] + FileDescriptorResponse(super::FileDescriptorResponse), + /// This message is used to answer all_extension_numbers_of_type requests. + #[prost(message, tag = "5")] + AllExtensionNumbersResponse(super::ExtensionNumberResponse), + /// This message is used to answer list_services requests. + #[prost(message, tag = "6")] + ListServicesResponse(super::ListServiceResponse), + /// This message is used when an error occurs. + #[prost(message, tag = "7")] + ErrorResponse(super::ErrorResponse), + } +} +/// Serialized FileDescriptorProto messages sent by the server answering +/// a file_by_filename, file_containing_symbol, or file_containing_extension +/// request. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FileDescriptorResponse { + /// Serialized FileDescriptorProto messages. We avoid taking a dependency on + /// descriptor.proto, which uses proto2 only features, by making them opaque + /// bytes instead. + #[prost(bytes = "vec", repeated, tag = "1")] + pub file_descriptor_proto: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +/// A list of extension numbers sent by the server answering +/// all_extension_numbers_of_type request. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExtensionNumberResponse { + /// Full name of the base type, including the package name. The format + /// is . + #[prost(string, tag = "1")] + pub base_type_name: ::prost::alloc::string::String, + #[prost(int32, repeated, tag = "2")] + pub extension_number: ::prost::alloc::vec::Vec, +} +/// A list of ServiceResponse sent by the server answering list_services request. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListServiceResponse { + /// The information of each service may be expanded in the future, so we use + /// ServiceResponse message to encapsulate it. + #[prost(message, repeated, tag = "1")] + pub service: ::prost::alloc::vec::Vec, +} +/// The information of a single service used by ListServiceResponse to answer +/// list_services request. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ServiceResponse { + /// Full name of a registered service, including its package name. The format + /// is . + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, +} +/// The error code and error message sent by the server when an error occurs. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ErrorResponse { + /// This field uses the error codes defined in grpc::StatusCode. + #[prost(int32, tag = "1")] + pub error_code: i32, + #[prost(string, tag = "2")] + pub error_message: ::prost::alloc::string::String, +} +#[doc = r" Generated client implementations."] +pub mod server_reflection_client { + #![allow(unused_variables, dead_code, missing_docs)] + use tonic::codegen::*; + pub struct ServerReflectionClient { + inner: tonic::client::Grpc, + } + impl ServerReflectionClient { + #[doc = r" Attempt to create a new client by connecting to a given endpoint."] + pub async fn connect(dst: D) -> Result + where + D: std::convert::TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ServerReflectionClient + where + T: tonic::client::GrpcService, + T::ResponseBody: Body + HttpBody + Send + 'static, + T::Error: Into, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_interceptor(inner: T, interceptor: impl Into) -> Self { + let inner = tonic::client::Grpc::with_interceptor(inner, interceptor); + Self { inner } + } + #[doc = " The reflection service is structured as a bidirectional stream, ensuring"] + #[doc = " all related requests go to a single server."] + pub async fn server_reflection_info( + &mut self, + request: impl tonic::IntoStreamingRequest, + ) -> Result< + tonic::Response>, + tonic::Status, + > { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", + ); + self.inner + .streaming(request.into_streaming_request(), path, codec) + .await + } + } + impl Clone for ServerReflectionClient { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + } + impl std::fmt::Debug for ServerReflectionClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ServerReflectionClient {{ ... }}") + } + } +} +#[doc = r" Generated server implementations."] +pub mod server_reflection_server { + #![allow(unused_variables, dead_code, missing_docs)] + use tonic::codegen::*; + #[doc = "Generated trait containing gRPC methods that should be implemented for use with ServerReflectionServer."] + #[async_trait] + pub trait ServerReflection: Send + Sync + 'static { + #[doc = "Server streaming response type for the ServerReflectionInfo method."] + type ServerReflectionInfoStream: Stream> + + Send + + Sync + + 'static; + #[doc = " The reflection service is structured as a bidirectional stream, ensuring"] + #[doc = " all related requests go to a single server."] + async fn server_reflection_info( + &self, + request: tonic::Request>, + ) -> Result, tonic::Status>; + } + #[derive(Debug)] + pub struct ServerReflectionServer { + inner: _Inner, + } + struct _Inner(Arc, Option); + impl ServerReflectionServer { + pub fn new(inner: T) -> Self { + let inner = Arc::new(inner); + let inner = _Inner(inner, None); + Self { inner } + } + pub fn with_interceptor(inner: T, interceptor: impl Into) -> Self { + let inner = Arc::new(inner); + let inner = _Inner(inner, Some(interceptor.into())); + Self { inner } + } + } + impl Service> for ServerReflectionServer + where + T: ServerReflection, + B: HttpBody + Send + Sync + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = Never; + type Future = BoxFuture; + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" => { + #[allow(non_camel_case_types)] + struct ServerReflectionInfoSvc(pub Arc); + impl + tonic::server::StreamingService + for ServerReflectionInfoSvc + { + type Response = super::ServerReflectionResponse; + type ResponseStream = T::ServerReflectionInfoStream; + type Future = + BoxFuture, tonic::Status>; + fn call( + &mut self, + request: tonic::Request< + tonic::Streaming, + >, + ) -> Self::Future { + let inner = self.0.clone(); + let fut = async move { (*inner).server_reflection_info(request).await }; + Box::pin(fut) + } + } + let inner = self.inner.clone(); + let fut = async move { + let interceptor = inner.1; + let inner = inner.0; + let method = ServerReflectionInfoSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = if let Some(interceptor) = interceptor { + tonic::server::Grpc::with_interceptor(codec, interceptor) + } else { + tonic::server::Grpc::new(codec) + }; + let res = grpc.streaming(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => Box::pin(async move { + Ok(http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(tonic::body::BoxBody::empty()) + .unwrap()) + }), + } + } + } + impl Clone for ServerReflectionServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { inner } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(self.0.clone(), self.1.clone()) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::transport::NamedService for ServerReflectionServer { + const NAME: &'static str = "grpc.reflection.v1alpha.ServerReflection"; + } +} diff --git a/tonic-reflection/tests/server.rs b/tonic-reflection/tests/server.rs new file mode 100644 index 000000000..48182e819 --- /dev/null +++ b/tonic-reflection/tests/server.rs @@ -0,0 +1,157 @@ +use futures::stream; +use futures_util::FutureExt; +use tokio::sync::oneshot; +use tonic::transport::Server; +use tonic::Request; +use tonic_reflection::server::Builder; + +use pb::server_reflection_client::ServerReflectionClient; +use pb::server_reflection_request::MessageRequest; +use pb::server_reflection_response::MessageResponse; +use pb::ServerReflectionRequest; +use pb::ServiceResponse; +use std::net::SocketAddr; +use tokio_stream::wrappers::TcpListenerStream; +use tokio_stream::StreamExt; + +mod pb { + #![allow(unreachable_pub)] + use prost::Message; + + tonic::include_proto!("grpc.reflection.v1alpha"); + + pub(crate) const REFLECTION_SERVICE_DESCRIPTOR: &'static [u8] = + tonic::include_file_descriptor_set!("reflection_v1alpha1"); + + pub(crate) fn get_encoded_reflection_service_fd() -> Vec { + let mut expected = Vec::new(); + &prost_types::FileDescriptorSet::decode(REFLECTION_SERVICE_DESCRIPTOR) + .expect("decode reflection service file descriptor set") + .file[0] + .encode(&mut expected) + .expect("encode reflection service file descriptor"); + expected + } +} + +#[tokio::test] +async fn test_list_services() { + let response = make_test_reflection_request(ServerReflectionRequest { + host: "".to_string(), + message_request: Some(MessageRequest::ListServices(String::new())), + }) + .await; + + if let MessageResponse::ListServicesResponse(services) = response { + assert_eq!( + services.service, + vec![ServiceResponse { + name: String::from("grpc.reflection.v1alpha.ServerReflection") + }] + ); + } else { + panic!("Expected a ListServicesResponse variant"); + } +} + +#[tokio::test] +async fn test_file_by_filename() { + let response = make_test_reflection_request(ServerReflectionRequest { + host: "".to_string(), + message_request: Some(MessageRequest::FileByFilename(String::from( + "reflection.proto", + ))), + }) + .await; + + if let MessageResponse::FileDescriptorResponse(descriptor) = response { + let file_descriptor_proto = descriptor + .file_descriptor_proto + .first() + .expect("descriptor"); + assert_eq!( + file_descriptor_proto.as_ref(), + pb::get_encoded_reflection_service_fd() + ); + } else { + panic!("Expected a FileDescriptorResponse variant"); + } +} + +#[tokio::test] +async fn test_file_containing_symbol() { + let response = make_test_reflection_request(ServerReflectionRequest { + host: "".to_string(), + message_request: Some(MessageRequest::FileContainingSymbol(String::from( + "grpc.reflection.v1alpha.ServerReflection", + ))), + }) + .await; + + if let MessageResponse::FileDescriptorResponse(descriptor) = response { + let file_descriptor_proto = descriptor + .file_descriptor_proto + .first() + .expect("descriptor"); + assert_eq!( + file_descriptor_proto.as_ref(), + pb::get_encoded_reflection_service_fd() + ); + } else { + panic!("Expected a FileDescriptorResponse variant"); + } +} + +async fn make_test_reflection_request(request: ServerReflectionRequest) -> MessageResponse { + // Run a test server + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let addr: SocketAddr = "127.0.0.1:0".parse().expect("SocketAddr parse"); + let listener = tokio::net::TcpListener::bind(addr).await.expect("bind"); + let local_addr = listener.local_addr().expect("local address"); + let local_addr = format!("http://{}", local_addr.to_string()); + let jh = tokio::spawn(async move { + let service = Builder::configure() + .register_encoded_file_descriptor_set(pb::REFLECTION_SERVICE_DESCRIPTOR) + .build() + .unwrap(); + + Server::builder() + .add_service(service) + .serve_with_incoming_shutdown(TcpListenerStream::new(listener), shutdown_rx.map(drop)) + .await + .unwrap(); + }); + + // Give the test server a few ms to become available + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + // Construct client and send request, extract response + let mut client = ServerReflectionClient::connect(local_addr) + .await + .expect("connect"); + + let request = Request::new(stream::iter(vec![request])); + let mut inbound = client + .server_reflection_info(request) + .await + .expect("request") + .into_inner(); + + let response = inbound + .next() + .await + .expect("steamed response") + .expect("successful response") + .message_response + .expect("some MessageResponse"); + + // We only expect one response per request + assert!(inbound.next().await.is_none()); + + // Shut down test server + shutdown_tx.send(()).expect("send shutdown"); + jh.await.expect("server shutdown"); + + response +} diff --git a/tonic/src/macros.rs b/tonic/src/macros.rs index aaea75f27..f31ed16bd 100644 --- a/tonic/src/macros.rs +++ b/tonic/src/macros.rs @@ -33,3 +33,43 @@ macro_rules! include_proto { include!(concat!(env!("OUT_DIR"), concat!("/", $package, ".rs"))); }; } + +/// Include an encoded `prost_types::FileDescriptorSet` as a `&'static [u8]`. The parameter must be +/// the stem of the filename passed to `file_descriptor_set_path` for the `tonic-build::Builder`, +/// excluding the `.bin` extension. +/// +/// For example, a file descriptor set compiled like this in `build.rs`: +/// +/// ```rust,ignore +/// let descriptor_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("my_descriptor.bin") +/// tonic_build::configure() +/// .file_descriptor_set_path(&descriptor_path) +/// .format(true) +/// .compile(&["proto/reflection.proto"], &["proto/"])?; +/// ``` +/// +/// Can be included like this: +/// +/// ```rust,ignore +/// mod pb { +/// pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("my_descriptor"); +/// } +/// ``` +/// +/// # Note: +/// **This only works if the tonic-build output directory has been unmodified**. +/// The default output directory is set to the [`OUT_DIR`] environment variable. +/// If the output directory has been modified, the following pattern may be used +/// instead of this macro. +/// +/// ```rust,ignore +/// mod pb { +/// pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("/relative/protobuf/directory/descriptor_name.bin"); +/// } +/// ``` +#[macro_export] +macro_rules! include_file_descriptor_set { + ($package: tt) => { + include_bytes!(concat!(env!("OUT_DIR"), concat!("/", $package, ".bin"))) + }; +}