mod client;
mod conversions;
mod node_provider;
pub mod pool;
#[cfg(with_server)]
mod server;
pub mod transport;
pub use client::*;
pub use conversions::*;
pub use node_provider::*;
#[cfg(with_server)]
pub use server::*;
pub mod api {
tonic::include_proto!("rpc.v1");
}
#[derive(thiserror::Error, Debug)]
pub enum GrpcError {
#[error("failed to connect to address: {0}")]
ConnectionFailed(#[from] transport::Error),
#[error("failed to execute task to completion: {0}")]
Join(#[from] futures::channel::oneshot::Canceled),
#[error("failed to parse socket address: {0}")]
SocketAddr(#[from] std::net::AddrParseError),
#[cfg(with_server)]
#[error(transparent)]
Reflection(#[from] tonic_reflection::server::Error),
}
const MEBIBYTE: usize = 1024 * 1024;
pub const GRPC_MAX_MESSAGE_SIZE: usize = 16 * MEBIBYTE;
pub const GRPC_CHUNKED_MESSAGE_FILL_LIMIT: usize = GRPC_MAX_MESSAGE_SIZE * 7 / 10;
pub const METHOD_NAME_LABEL: &str = "method_name";
pub const TRAFFIC_TYPE_LABEL: &str = "traffic_type";
pub const ERROR_TYPE_LABEL: &str = "error_type";
const MAX_PROTO_IDENT_LEN: usize = 128;
fn is_proto_identifier(s: &str) -> bool {
if s.is_empty() || s.len() > MAX_PROTO_IDENT_LEN {
return false;
}
let mut bytes = s.bytes();
let first = bytes.next().expect("non-empty checked above");
first.is_ascii_alphabetic() && bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
}
fn is_proto_service_name(s: &str) -> bool {
let mut parts = s.split('.');
let Some(first) = parts.next() else {
return false;
};
let Some(second) = parts.next() else {
return false;
};
is_proto_identifier(first) && is_proto_identifier(second) && parts.all(is_proto_identifier)
}
pub fn extract_grpc_method_name(path: &str) -> &str {
let mut parts = path.splitn(3, '/');
let (Some(""), Some(service), Some(method)) = (parts.next(), parts.next(), parts.next()) else {
return "non_grpc";
};
if is_proto_service_name(service) && is_proto_identifier(method) {
method
} else {
"non_grpc"
}
}
#[cfg(test)]
mod method_name_tests {
use super::*;
#[test]
fn grpc_unary_method() {
assert_eq!(
extract_grpc_method_name("/rpc.v1.ValidatorNode/HandleBlockProposal"),
"HandleBlockProposal"
);
}
#[test]
fn grpc_streaming_method() {
assert_eq!(
extract_grpc_method_name("/rpc.v1.ValidatorNode/SubscribeToNotifications"),
"SubscribeToNotifications"
);
}
#[test]
fn health_check_path() {
assert_eq!(
extract_grpc_method_name("/grpc.health.v1.Health/Check"),
"Check"
);
}
#[test]
fn non_grpc_root_path() {
assert_eq!(extract_grpc_method_name("/"), "non_grpc");
}
#[test]
fn non_grpc_plain_path() {
assert_eq!(extract_grpc_method_name("/healthz"), "non_grpc");
}
#[test]
fn non_grpc_no_dot_in_service() {
assert_eq!(extract_grpc_method_name("/NoDotService/Method"), "non_grpc");
}
#[test]
fn empty_path() {
assert_eq!(extract_grpc_method_name(""), "non_grpc");
}
#[test]
fn dot_env_bot_scan_does_not_leak_into_label() {
assert_eq!(
extract_grpc_method_name(
"/.env.local/.env.production/.env.staging/.env.development/.env.test"
),
"non_grpc"
);
}
#[test]
fn service_starting_with_dot_is_rejected() {
assert_eq!(extract_grpc_method_name("/.foo.bar/Method"), "non_grpc");
}
#[test]
fn method_with_extra_path_segments_is_rejected() {
assert_eq!(
extract_grpc_method_name("/foo.bar/Method/extra"),
"non_grpc"
);
}
#[test]
fn method_with_invalid_characters_is_rejected() {
assert_eq!(extract_grpc_method_name("/foo.bar/Method-x"), "non_grpc");
assert_eq!(extract_grpc_method_name("/foo.bar/Method?x"), "non_grpc");
assert_eq!(extract_grpc_method_name("/foo.bar/.Method"), "non_grpc");
}
#[test]
fn identifier_starting_with_underscore_is_rejected() {
assert_eq!(extract_grpc_method_name("/foo.bar/_Method"), "non_grpc");
assert_eq!(extract_grpc_method_name("/_foo.bar/Method"), "non_grpc");
assert_eq!(
extract_grpc_method_name("/foo.bar/________________"),
"non_grpc"
);
}
#[test]
fn empty_method_segment_is_rejected() {
assert_eq!(extract_grpc_method_name("/foo.bar/"), "non_grpc");
}
#[test]
fn empty_service_segment_is_rejected() {
assert_eq!(extract_grpc_method_name("//Method"), "non_grpc");
}
#[test]
fn service_with_consecutive_dots_is_rejected() {
assert_eq!(extract_grpc_method_name("/foo..bar/Method"), "non_grpc");
}
#[test]
fn overlong_method_is_rejected() {
let long_method = "M".repeat(MAX_PROTO_IDENT_LEN + 1);
let path = format!("/foo.bar/{long_method}");
assert_eq!(extract_grpc_method_name(&path), "non_grpc");
}
#[test]
fn path_without_leading_slash_is_rejected() {
assert_eq!(extract_grpc_method_name("foo.bar/Method"), "non_grpc");
}
}