1pub mod actions;
3pub mod wire;
4use crate::server::grpc::{
5 self,
6 streaming::{ConnectionTasks, OwnedExecutor, Registry},
7 HttpCore,
8};
9use crate::{
10 protocol::SpawnContext,
11 state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo},
12};
13use anyhow::{ensure, Context, Result};
14use bytes::Bytes;
15use http_body_util::{BodyExt, Full, Limited, StreamBody};
16use hyper::{
17 body::{Frame, Incoming},
18 HeaderMap, Method, Request, Response, StatusCode,
19};
20use std::{convert::Infallible, sync::Arc, time::Duration};
21use tonic::{body::BoxBody, Status};
22use tower::{Layer, Service};
23pub const DEFAULT_RPC_TIMEOUT_SECS: u64 = 300;
24pub const FIRST_BYTE_TIMEOUT: Duration = Duration::from_secs(30);
25pub const IDLE_TIMEOUT: Duration = Duration::from_secs(120);
26pub const MAX_CONNECTIONS: usize = 256;
27const REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
28
29fn empty(status: StatusCode) -> Response<BoxBody> {
30 let mut response = Response::new(tonic::body::empty_body());
31 *response.status_mut() = status;
32 if status != StatusCode::NO_CONTENT {
33 response
34 .headers_mut()
35 .insert("connection", "close".parse().unwrap());
36 }
37 response
38}
39fn closing_status(status: Status) -> Response<BoxBody> {
40 let mut response = status_in_body(status.into_http());
41 response
42 .headers_mut()
43 .insert("connection", "close".parse().unwrap());
44 response
45}
46fn status_in_body(mut response: Response<BoxBody>) -> Response<BoxBody> {
48 if response.headers().contains_key("grpc-status") {
49 let mut trailers = HeaderMap::new();
50 for key in ["grpc-status", "grpc-message", "grpc-status-details-bin"] {
51 if let Some(value) = response.headers_mut().remove(key) {
52 trailers.insert(key, value);
53 }
54 }
55 *response.body_mut() =
56 StreamBody::new(futures::stream::iter([Ok::<_, Status>(
57 Frame::<Bytes>::trailers(trailers),
58 )]))
59 .boxed_unsync();
60 }
61 response
62}
63fn cors(response: &mut Response<BoxBody>, origin: Option<&str>) {
64 if let Some(origin) = origin {
65 response
66 .headers_mut()
67 .insert("access-control-allow-origin", origin.parse().unwrap());
68 response
69 .headers_mut()
70 .insert("vary", "Origin".parse().unwrap());
71 response.headers_mut().insert(
72 "access-control-expose-headers",
73 "grpc-status,grpc-message,grpc-status-details-bin"
74 .parse()
75 .unwrap(),
76 );
77 }
78}
79fn allowed_origin(
80 request: &Request<Incoming>,
81 configured: Option<&str>,
82) -> Result<Option<String>, StatusCode> {
83 let mut origins = request.headers().get_all("origin").iter();
84 let Some(origin) = origins.next() else {
85 return Ok(None);
86 };
87 let origin = origin.to_str().map_err(|_| StatusCode::FORBIDDEN)?;
88 if origins.next().is_some() || configured != Some(origin) {
89 return Err(StatusCode::FORBIDDEN);
90 }
91 Ok(Some(origin.into()))
92}
93async fn request(
94 mut request: Request<Incoming>,
95 core: HttpCore,
96 connection: crate::server::connection::ConnectionId,
97 activity: Arc<crate::server::accept_bounded::ConnectionActivity>,
98 registry: Registry,
99 executor: OwnedExecutor,
100 allow_origin: Option<String>,
101) -> Response<BoxBody> {
102 if !wire::bounded_headers(request.headers()) {
103 return empty(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
104 }
105 let origin = match allowed_origin(&request, allow_origin.as_deref()) {
106 Ok(origin) => origin,
107 Err(code) => return empty(code),
108 };
109 if request.method() == Method::OPTIONS {
110 let allowed = [
111 "content-type",
112 "x-grpc-web",
113 "x-user-agent",
114 "grpc-timeout",
115 "grpc-encoding",
116 "grpc-accept-encoding",
117 ];
118 let headers_ok = request
119 .headers()
120 .get("access-control-request-headers")
121 .map(|value| {
122 value.to_str().is_ok_and(|value| {
123 value.len() <= 1024
124 && value.split(',').count() <= 32
125 && value.split(',').all(|header| {
126 allowed.contains(&header.trim().to_ascii_lowercase().as_str())
127 })
128 })
129 })
130 .unwrap_or(true);
131 if origin.is_none()
132 || request
133 .headers()
134 .get("access-control-request-method")
135 .is_none_or(|value| value != "POST")
136 || request
137 .headers()
138 .get_all("access-control-request-method")
139 .iter()
140 .count()
141 != 1
142 || request
143 .headers()
144 .get_all("access-control-request-headers")
145 .iter()
146 .count()
147 > 1
148 || !headers_ok
149 {
150 return empty(StatusCode::FORBIDDEN);
151 }
152 let mut response = empty(StatusCode::NO_CONTENT);
153 cors(&mut response, origin.as_deref());
154 response
155 .headers_mut()
156 .insert("access-control-allow-methods", "POST".parse().unwrap());
157 response.headers_mut().insert(
158 "access-control-allow-headers",
159 allowed.join(",").parse().unwrap(),
160 );
161 return response;
162 }
163 if request.method() != Method::POST {
164 return empty(StatusCode::METHOD_NOT_ALLOWED);
165 }
166 if !wire::binary_content_type(request.headers())
167 || request.headers().get_all("accept").iter().any(|value| {
168 value
169 .to_str()
170 .map_or(true, |value| value.contains("grpc-web-text"))
171 })
172 || request.headers().contains_key("content-encoding")
173 {
174 return empty(StatusCode::UNSUPPORTED_MEDIA_TYPE);
175 }
176 if request.headers().get_all("grpc-encoding").iter().count() > 1 {
177 return empty(StatusCode::BAD_REQUEST);
178 }
179 if request.uri().path().len() > 2048 || request.uri().query().is_some() {
180 return empty(StatusCode::BAD_REQUEST);
181 }
182 request
184 .headers_mut()
185 .insert("accept", "application/grpc-web+proto".parse().unwrap());
186 let busy = activity.busy();
187 let body = request.map(|body| {
188 body.map_err(|_| Status::unavailable("request transport failed"))
189 .boxed_unsync()
190 });
191 let mut busy = Some(busy);
192 let mut service = tonic_web::GrpcWebLayer::new().layer(tower::service_fn(
193 move |request: Request<BoxBody>| {
194 let core = core.clone();
195 let registry = registry.clone();
196 let executor = executor.clone();
197 let busy = busy.take().expect("single request service invoked once");
199 async move {
200 let admission = match core.prepare(&request, executor) {
201 Ok(admission) => admission,
202 Err(status) => return Ok::<_, Infallible>(closing_status(status)),
203 };
204 let (parts, body) = request.into_parts();
205 let input = tokio::time::timeout_at(
210 admission.deadline,
211 Limited::new(wire::request_body(body), wire::MAX_MESSAGE_BYTES + 6).collect(),
212 )
213 .await;
214 let bytes = match input {
215 Ok(Ok(body)) => body.to_bytes(),
216 Ok(Err(_)) => {
217 return Ok(admission.guard(
218 closing_status(Status::resource_exhausted(
219 "request exceeds framed bound+1 or is malformed",
220 )),
221 busy,
222 ))
223 }
224 Err(_) => {
225 return Ok(admission.guard(
226 closing_status(Status::deadline_exceeded(
227 "RPC request deadline exceeded",
228 )),
229 busy,
230 ))
231 }
232 };
233 if bytes.len() > wire::MAX_MESSAGE_BYTES + 5 {
234 return Ok(admission.guard(
235 status_in_body(
236 Status::resource_exhausted("request exceeds 4 MiB framed message")
237 .into_http(),
238 ),
239 busy,
240 ));
241 }
242 let request = Request::from_parts(
243 parts,
244 Full::new(bytes)
245 .map_err(|never| match never {})
246 .boxed_unsync(),
247 );
248 let response = core
249 .request(request, connection, registry, &admission)
250 .await;
251 Ok(admission.guard(status_in_body(response), busy))
252 }
253 },
254 ));
255 let mut response = match service.call(body).await {
258 Ok(response) => response,
259 Err(never) => match never {},
260 };
261 cors(&mut response, origin.as_deref());
262 response
263}
264
265pub struct GrpcWebServer;
266impl GrpcWebServer {
267 pub async fn spawn(ctx: SpawnContext) -> Result<std::net::SocketAddr> {
268 let p = ctx.startup_params.as_ref();
269 let schema = p
270 .context("proto_schema required")?
271 .get_string("proto_schema")?;
272 let timeout = p
273 .map(|p| p.get_optional_u64("rpc_timeout_secs"))
274 .transpose()?
275 .flatten()
276 .unwrap_or(DEFAULT_RPC_TIMEOUT_SECS);
277 ensure!(
278 (1..=3600).contains(&timeout),
279 "rpc_timeout_secs must be 1..3600"
280 );
281 let allow_origin = p
282 .map(|p| p.get_optional_string("allow_origin"))
283 .transpose()?
284 .flatten();
285 if let Some(origin) = &allow_origin {
286 ensure!(origin.len() <= 2048, "allow_origin exceeds 2048 bytes");
287 let uri: hyper::Uri = origin.parse()?;
288 ensure!(
289 matches!(uri.scheme_str(), Some("http" | "https"))
290 && uri.host().is_some()
291 && uri.path() == "/"
292 && uri.query().is_none()
293 && !origin.contains('@')
294 && !origin.ends_with('/'),
295 "allow_origin must be one exact HTTP(S) origin without a trailing slash"
296 );
297 }
298 let pool = grpc::schema::load(&schema).await?;
299 ensure!(
300 pool.services().next().is_some(),
301 "schema must define services"
302 );
303 let core = HttpCore::new(
304 &ctx,
305 pool,
306 Arc::new(actions::GrpcWebProtocol),
307 Duration::from_secs(timeout),
308 );
309 let listener =
310 crate::server::socket_helpers::create_reusable_tcp_listener(ctx.legacy_listen_addr())
311 .await?;
312 let address = listener.local_addr()?;
313 let registrar = ctx.state.clone();
314 let server_id = ctx.server_id;
315 let task = tokio::spawn(async move {
316 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
317 loop {
318 let (stream, remote, permit) = match crate::server::accept_bounded::accept_bounded(
319 &listener,
320 &limiter,
321 REFUSAL,
322 "gRPC-Web",
323 Some(&ctx.status_tx),
324 )
325 .await
326 {
327 Ok(value) => value,
328 Err(error) => {
329 crate::console_error!(ctx.status_tx, "gRPC-Web accept failed: {error}");
330 break;
331 }
332 };
333 let id = crate::server::connection::ConnectionId::new(
334 ctx.state.get_next_unified_id().await,
335 );
336 let now = crate::utils::clock::Instant::now();
337 ctx.state
338 .add_connection_to_server(
339 ctx.server_id,
340 ConnectionState {
341 id,
342 remote_addr: remote,
343 local_addr: address,
344 bytes_sent: 0,
345 bytes_received: 0,
346 packets_sent: 0,
347 packets_received: 0,
348 last_activity: now,
349 status: ConnectionStatus::Active,
350 status_changed_at: now,
351 protocol_info: ProtocolConnectionInfo::empty(),
352 },
353 )
354 .await;
355 let state = ctx.state.clone();
356 let owner = state.clone();
357 let core = core.clone();
358 let origin = allow_origin.clone();
359 let server = ctx.server_id;
360 owner.spawn_server_task(server, async move {
361 let _permit = permit;
362 if matches!(tokio::time::timeout(FIRST_BYTE_TIMEOUT, stream.peek(&mut [0u8;1])).await, Ok(Ok(n)) if n > 0) {
363 let activity = Arc::new(crate::server::accept_bounded::ConnectionActivity::new());
364 let executor = OwnedExecutor::default(); let _children = ConnectionTasks(executor.clone());
365 let registry = Registry::default();
366 let commands = crate::server::peer_support::register_peer_channel(&state, server, id.as_u32()).await;
367 let _ = executor.spawn(core.clone().commands(commands, registry.clone(), id));
368 let execute = executor.clone(); let active = activity.clone();
369 let (done, ended) = tokio::sync::oneshot::channel();
370 let _ = executor.spawn(async move {
373 let service = hyper::service::service_fn(move |request| {
374 let core = core.clone(); let activity = active.clone(); let registry = registry.clone();
375 let executor = execute.clone(); let origin = origin.clone();
376 async move { Ok::<_, Infallible>(self::request(request, core, id, activity, registry, executor, origin).await) }
377 });
378 let _ = hyper::server::conn::http1::Builder::new().max_headers(64).max_buf_size(32768)
379 .serve_connection(hyper_util::rt::TokioIo::new(stream), service).await;
380 let _ = done.send(());
381 });
382 tokio::select! { _ = ended => {}, _ = crate::server::accept_bounded::watch_idle(activity, IDLE_TIMEOUT) => {} }
383 }
384 state.remove_peer_handle(server, id.as_u32()).await;
385 state.remove_connection_from_server(server, id).await;
386 }).await;
387 }
388 });
389 registrar.register_server_task(server_id, task).await;
390 Ok(address)
391 }
392}