1use std::future::Future;
42use std::net::SocketAddr;
43use std::sync::Arc;
44
45use axum::{
46 extract::State,
47 http::StatusCode,
48 response::IntoResponse,
49 routing::{get, post},
50 Json, Router,
51};
52use dig_rpc_protocol::envelope::{JsonRpcRequest, JsonRpcResponse};
53use serde_json::Value;
54
55use crate::dispatch::{dispatch, Surface};
56use crate::error::RpcServerError;
57use crate::handler::RpcHandler;
58use crate::middleware::{RateLimitConfig, RateLimitOutcome, RateLimitState};
59use crate::tls::TlsConfig;
60
61#[derive(Clone)]
63pub enum RpcServerMode {
64 Loopback {
67 bind: SocketAddr,
69 },
70 PublicRead {
72 bind: SocketAddr,
74 tls: TlsConfig,
76 },
77 Peer {
79 bind: SocketAddr,
81 tls: TlsConfig,
83 },
84}
85
86impl RpcServerMode {
87 pub fn loopback(bind: SocketAddr) -> Self {
89 Self::Loopback { bind }
90 }
91
92 pub fn surface(&self) -> Surface {
94 match self {
95 Self::Loopback { .. } => Surface::Loopback,
96 Self::PublicRead { .. } => Surface::PublicRead,
97 Self::Peer { .. } => Surface::Peer,
98 }
99 }
100
101 pub fn bind(&self) -> SocketAddr {
103 match self {
104 Self::Loopback { bind } | Self::PublicRead { bind, .. } | Self::Peer { bind, .. } => {
105 *bind
106 }
107 }
108 }
109}
110
111impl std::fmt::Debug for RpcServerMode {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.debug_struct("RpcServerMode")
114 .field("surface", &self.surface())
115 .field("bind", &self.bind())
116 .finish()
117 }
118}
119
120pub struct RpcServer<H: RpcHandler + ?Sized> {
122 handler: Arc<H>,
123 mode: RpcServerMode,
124 rate_limit: RateLimitState,
125}
126
127impl<H: RpcHandler + ?Sized> RpcServer<H> {
128 pub fn new(handler: Arc<H>, mode: RpcServerMode) -> Self {
130 Self {
131 handler,
132 mode,
133 rate_limit: RateLimitState::new(RateLimitConfig::defaults()),
134 }
135 }
136
137 pub fn with_rate_limit(mut self, state: RateLimitState) -> Self {
139 self.rate_limit = state;
140 self
141 }
142
143 pub fn bind_addr(&self) -> SocketAddr {
145 self.mode.bind()
146 }
147
148 pub fn surface(&self) -> Surface {
150 self.mode.surface()
151 }
152}
153
154impl<H: RpcHandler> RpcServer<H> {
155 pub fn router(&self) -> Router {
159 let state = AppState {
160 handler: self.handler.clone(),
161 surface: self.mode.surface(),
162 rate_limit: self.rate_limit.clone(),
163 };
164 Router::new()
165 .route("/", post(handle_post::<H>))
166 .route("/healthz", get(handle_healthz::<H>))
167 .with_state(state)
168 }
169
170 pub async fn serve<F>(self, shutdown: F) -> Result<(), RpcServerError>
172 where
173 F: Future<Output = ()> + Send + 'static,
174 {
175 let bind = self.mode.bind();
176 if matches!(self.mode, RpcServerMode::Loopback { .. }) && !bind.ip().is_loopback() {
179 return Err(RpcServerError::Fatal(Arc::new(anyhow::anyhow!(
180 "loopback control server refused non-loopback bind {bind}"
181 ))));
182 }
183 let router = self.router();
184
185 match self.mode {
186 RpcServerMode::Loopback { .. } => {
187 let listener = tokio::net::TcpListener::bind(bind).await.map_err(|e| {
188 RpcServerError::BindFailed {
189 addr: bind,
190 source: Arc::new(e),
191 }
192 })?;
193 axum::serve(listener, router)
194 .with_graceful_shutdown(shutdown)
195 .await
196 .map_err(|e| RpcServerError::Fatal(Arc::new(anyhow::anyhow!("axum: {e}"))))
197 }
198 RpcServerMode::PublicRead { tls, .. } | RpcServerMode::Peer { tls, .. } => {
199 let rustls = axum_server::tls_rustls::RustlsConfig::from_config(tls.server_config);
200 let handle = axum_server::Handle::new();
201 let h2 = handle.clone();
202 tokio::spawn(async move {
203 shutdown.await;
204 h2.graceful_shutdown(Some(std::time::Duration::from_secs(10)));
205 });
206 axum_server::bind_rustls(bind, rustls)
207 .handle(handle)
208 .serve(router.into_make_service())
209 .await
210 .map_err(|e| {
211 RpcServerError::Fatal(Arc::new(anyhow::anyhow!("axum-server: {e}")))
212 })
213 }
214 }
215 }
216}
217
218struct AppState<H: RpcHandler + ?Sized> {
220 handler: Arc<H>,
221 surface: Surface,
222 rate_limit: RateLimitState,
223}
224
225impl<H: RpcHandler + ?Sized> Clone for AppState<H> {
226 fn clone(&self) -> Self {
227 Self {
228 handler: self.handler.clone(),
229 surface: self.surface,
230 rate_limit: self.rate_limit.clone(),
231 }
232 }
233}
234
235async fn handle_post<H: RpcHandler>(
236 State(state): State<AppState<H>>,
237 Json(req): Json<JsonRpcRequest<Value>>,
238) -> Json<JsonRpcResponse<Value>> {
239 if let Some(method) = dig_rpc_protocol::Method::from_name(&req.method) {
243 let peer_key = vec![state.surface.discriminant()];
247 if let RateLimitOutcome::Deny { retry_after_secs } =
248 state.rate_limit.check(&peer_key, method.tier())
249 {
250 let err = dig_rpc_protocol::RpcError::of(
251 dig_rpc_protocol::ErrorCode::ServerError,
252 format!("rate limited; retry after {retry_after_secs}s"),
253 )
254 .with_extra("retry_after_secs", serde_json::json!(retry_after_secs));
255 return Json(JsonRpcResponse::error(req.id, err));
256 }
257 }
258 Json(dispatch(&*state.handler, state.surface, req).await)
259}
260
261async fn handle_healthz<H: RpcHandler>(State(state): State<AppState<H>>) -> impl IntoResponse {
262 match state.handler.healthz().await {
263 Ok(()) => (StatusCode::OK, "OK"),
264 Err(_) => (StatusCode::SERVICE_UNAVAILABLE, "unavailable"),
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use dig_rpc_protocol::{RpcError, Tier};
272
273 #[test]
274 fn loopback_mode_reports_loopback_surface() {
275 let m = RpcServerMode::loopback("127.0.0.1:9778".parse().unwrap());
276 assert_eq!(m.surface(), Surface::Loopback);
277 assert_eq!(m.bind().port(), 9778);
278 }
279
280 #[tokio::test]
281 async fn loopback_server_refuses_routable_bind() {
282 struct N;
283 impl RpcHandler for N {}
284 let server = RpcServer::new(
285 Arc::new(N),
286 RpcServerMode::loopback("0.0.0.0:0".parse().unwrap()),
287 );
288 let err = server.serve(async {}).await.unwrap_err();
289 assert!(matches!(err, RpcServerError::Fatal(_)));
290 }
291
292 #[test]
293 fn accessors_report_mode() {
294 struct N;
295 impl RpcHandler for N {}
296 let server = RpcServer::new(
297 Arc::new(N),
298 RpcServerMode::loopback("127.0.0.1:1234".parse().unwrap()),
299 );
300 assert_eq!(server.bind_addr().port(), 1234);
301 assert_eq!(server.surface(), Surface::Loopback);
302 let s = format!("{:?}", server.mode);
304 assert!(s.contains("Loopback"));
305 }
306
307 #[tokio::test]
308 async fn rate_limit_denies_when_exhausted() {
309 use crate::middleware::{BucketSpec, RateLimitConfig, RateLimitState};
310 use axum::body::Body;
311 use axum::http::Request;
312 use http_body_util::BodyExt;
313 use std::collections::HashMap;
314 use tower::ServiceExt;
315
316 struct N;
317 #[async_trait::async_trait]
318 impl RpcHandler for N {
319 async fn handle(
320 &self,
321 _m: dig_rpc_protocol::Method,
322 _p: Value,
323 ) -> Result<Value, RpcError> {
324 Ok(serde_json::json!({}))
325 }
326 }
327 let mut buckets = HashMap::new();
328 buckets.insert(
329 Tier::PublicRead,
330 BucketSpec {
331 fill_per_sec: 0.0,
332 capacity: 1.0,
333 },
334 );
335 let state = RateLimitState::new(RateLimitConfig { buckets });
336 let server = RpcServer::new(
337 Arc::new(N),
338 RpcServerMode::loopback("127.0.0.1:0".parse().unwrap()),
339 )
340 .with_rate_limit(state);
341 let router = server.router();
342
343 let call = |r: Router| async move {
344 let req = Request::builder()
345 .method("POST")
346 .uri("/")
347 .header("content-type", "application/json")
348 .body(Body::from(
349 serde_json::to_vec(&serde_json::json!({
350 "jsonrpc": "2.0", "id": 1, "method": "dig.health"
351 }))
352 .unwrap(),
353 ))
354 .unwrap();
355 let resp = r.oneshot(req).await.unwrap();
356 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
357 serde_json::from_slice::<Value>(&bytes).unwrap()
358 };
359 let first = call(router.clone()).await;
361 assert!(first.get("result").is_some(), "first should pass: {first}");
362 let second = call(router).await;
363 assert_eq!(
364 second["error"]["code"], -32000,
365 "second should be rate-limited: {second}"
366 );
367 assert!(
368 second["error"]["data"]["retry_after_secs"]
369 .as_u64()
370 .unwrap()
371 >= 1
372 );
373 }
374
375 #[tokio::test]
376 async fn healthz_unavailable_when_handler_unhealthy() {
377 use axum::body::Body;
378 use axum::http::Request;
379 use tower::ServiceExt;
380
381 struct Sick;
382 #[async_trait::async_trait]
383 impl RpcHandler for Sick {
384 async fn healthz(&self) -> Result<(), RpcError> {
385 Err(RpcError::of(
386 dig_rpc_protocol::ErrorCode::ServerError,
387 "down",
388 ))
389 }
390 }
391 let server = RpcServer::new(
392 Arc::new(Sick),
393 RpcServerMode::loopback("127.0.0.1:0".parse().unwrap()),
394 );
395 let req = Request::builder()
396 .uri("/healthz")
397 .body(Body::empty())
398 .unwrap();
399 let resp = server.router().oneshot(req).await.unwrap();
400 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
401 }
402}