1mod client;
5mod conversions;
6mod node_provider;
7pub mod pool;
9#[cfg(with_server)]
10mod server;
11pub mod transport;
13
14pub use client::*;
15pub use conversions::*;
16pub use node_provider::*;
17#[cfg(with_server)]
18pub use server::*;
19
20pub mod api {
22 #![allow(missing_docs)]
24 tonic::include_proto!("rpc.v1");
25}
26
27#[derive(thiserror::Error, Debug)]
28#[allow(missing_docs)]
29pub enum GrpcError {
30 #[error("failed to connect to address: {0}")]
31 ConnectionFailed(#[from] transport::Error),
32
33 #[error("failed to execute task to completion: {0}")]
34 Join(#[from] futures::channel::oneshot::Canceled),
35
36 #[error("failed to parse socket address: {0}")]
37 SocketAddr(#[from] std::net::AddrParseError),
38
39 #[cfg(with_server)]
40 #[error(transparent)]
41 Reflection(#[from] tonic_reflection::server::Error),
42}
43
44const MEBIBYTE: usize = 1024 * 1024;
45pub const GRPC_MAX_MESSAGE_SIZE: usize = 16 * MEBIBYTE;
47
48pub const GRPC_CHUNKED_MESSAGE_FILL_LIMIT: usize = GRPC_MAX_MESSAGE_SIZE * 7 / 10;
51
52pub const METHOD_NAME_LABEL: &str = "method_name";
54
55pub const TRAFFIC_TYPE_LABEL: &str = "traffic_type";
57
58pub const ERROR_TYPE_LABEL: &str = "error_type";
60
61const MAX_PROTO_IDENT_LEN: usize = 128;
65
66fn is_proto_identifier(s: &str) -> bool {
69 if s.is_empty() || s.len() > MAX_PROTO_IDENT_LEN {
70 return false;
71 }
72 let mut bytes = s.bytes();
73 let first = bytes.next().expect("non-empty checked above");
74 first.is_ascii_alphabetic() && bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
75}
76
77fn is_proto_service_name(s: &str) -> bool {
80 let mut parts = s.split('.');
81 let Some(first) = parts.next() else {
82 return false;
83 };
84 let Some(second) = parts.next() else {
85 return false;
86 };
87 is_proto_identifier(first) && is_proto_identifier(second) && parts.all(is_proto_identifier)
88}
89
90pub fn extract_grpc_method_name(path: &str) -> &str {
99 let mut parts = path.splitn(3, '/');
100 let (Some(""), Some(service), Some(method)) = (parts.next(), parts.next(), parts.next()) else {
101 return "non_grpc";
102 };
103 if is_proto_service_name(service) && is_proto_identifier(method) {
104 method
105 } else {
106 "non_grpc"
107 }
108}
109
110#[cfg(test)]
111mod method_name_tests {
112 use super::*;
113
114 #[test]
115 fn grpc_unary_method() {
116 assert_eq!(
117 extract_grpc_method_name("/rpc.v1.ValidatorNode/HandleBlockProposal"),
118 "HandleBlockProposal"
119 );
120 }
121
122 #[test]
123 fn grpc_streaming_method() {
124 assert_eq!(
125 extract_grpc_method_name("/rpc.v1.ValidatorNode/SubscribeToNotifications"),
126 "SubscribeToNotifications"
127 );
128 }
129
130 #[test]
131 fn health_check_path() {
132 assert_eq!(
133 extract_grpc_method_name("/grpc.health.v1.Health/Check"),
134 "Check"
135 );
136 }
137
138 #[test]
139 fn non_grpc_root_path() {
140 assert_eq!(extract_grpc_method_name("/"), "non_grpc");
141 }
142
143 #[test]
144 fn non_grpc_plain_path() {
145 assert_eq!(extract_grpc_method_name("/healthz"), "non_grpc");
146 }
147
148 #[test]
149 fn non_grpc_no_dot_in_service() {
150 assert_eq!(extract_grpc_method_name("/NoDotService/Method"), "non_grpc");
151 }
152
153 #[test]
154 fn empty_path() {
155 assert_eq!(extract_grpc_method_name(""), "non_grpc");
156 }
157
158 #[test]
159 fn dot_env_bot_scan_does_not_leak_into_label() {
160 assert_eq!(
161 extract_grpc_method_name(
162 "/.env.local/.env.production/.env.staging/.env.development/.env.test"
163 ),
164 "non_grpc"
165 );
166 }
167
168 #[test]
169 fn service_starting_with_dot_is_rejected() {
170 assert_eq!(extract_grpc_method_name("/.foo.bar/Method"), "non_grpc");
171 }
172
173 #[test]
174 fn method_with_extra_path_segments_is_rejected() {
175 assert_eq!(
176 extract_grpc_method_name("/foo.bar/Method/extra"),
177 "non_grpc"
178 );
179 }
180
181 #[test]
182 fn method_with_invalid_characters_is_rejected() {
183 assert_eq!(extract_grpc_method_name("/foo.bar/Method-x"), "non_grpc");
184 assert_eq!(extract_grpc_method_name("/foo.bar/Method?x"), "non_grpc");
185 assert_eq!(extract_grpc_method_name("/foo.bar/.Method"), "non_grpc");
186 }
187
188 #[test]
189 fn identifier_starting_with_underscore_is_rejected() {
190 assert_eq!(extract_grpc_method_name("/foo.bar/_Method"), "non_grpc");
191 assert_eq!(extract_grpc_method_name("/_foo.bar/Method"), "non_grpc");
192 assert_eq!(
193 extract_grpc_method_name("/foo.bar/________________"),
194 "non_grpc"
195 );
196 }
197
198 #[test]
199 fn empty_method_segment_is_rejected() {
200 assert_eq!(extract_grpc_method_name("/foo.bar/"), "non_grpc");
201 }
202
203 #[test]
204 fn empty_service_segment_is_rejected() {
205 assert_eq!(extract_grpc_method_name("//Method"), "non_grpc");
206 }
207
208 #[test]
209 fn service_with_consecutive_dots_is_rejected() {
210 assert_eq!(extract_grpc_method_name("/foo..bar/Method"), "non_grpc");
211 }
212
213 #[test]
214 fn overlong_method_is_rejected() {
215 let long_method = "M".repeat(MAX_PROTO_IDENT_LEN + 1);
216 let path = format!("/foo.bar/{long_method}");
217 assert_eq!(extract_grpc_method_name(&path), "non_grpc");
218 }
219
220 #[test]
221 fn path_without_leading_slash_is_rejected() {
222 assert_eq!(extract_grpc_method_name("foo.bar/Method"), "non_grpc");
223 }
224}