1pub mod acme;
8mod body;
9mod error_page;
10mod forward;
11mod handler;
12pub mod rate_limit;
13mod routing;
14mod security_headers;
15pub mod sni;
16pub mod tls;
17mod websocket;
18
19pub use orca_core::config::{FallbackConfig, SecurityHeadersConfig};
20pub use security_headers::init as init_security_headers;
23
24use std::collections::HashMap;
25use std::sync::Arc;
26use std::sync::atomic::AtomicUsize;
27
28use hyper::Request;
29use hyper::body::Incoming;
30use hyper::server::conn::http1;
31use hyper::service::service_fn;
32use hyper_util::rt::{TokioExecutor, TokioIo};
33use hyper_util::server::conn::auto;
34use tokio::net::TcpListener;
35use tokio::sync::RwLock;
36use tracing::{debug, error, info, warn};
37
38use acme::AcmeManager;
39use handler::{handle_acme_challenge, handle_request};
40use rate_limit::RateLimiter;
41
42#[derive(Debug, Clone)]
44pub struct RouteTarget {
45 pub address: String,
47 pub service_name: String,
49 pub path_pattern: Option<String>,
53 pub weight: u32,
56 pub strip_prefix: Option<String>,
61}
62
63#[derive(Debug, Clone)]
65pub struct WasmTrigger {
66 pub pattern: String,
68 pub runtime_id: String,
70 pub service_name: String,
72}
73
74pub type WasmInvoker =
77 Arc<dyn Fn(String, String, String, String) -> WasmInvokeFuture + Send + Sync>;
78
79pub type WasmInvokeFuture =
81 std::pin::Pin<Box<dyn std::future::Future<Output = Result<String, String>> + Send>>;
82
83pub type SharedWasmTriggers = Arc<RwLock<Vec<WasmTrigger>>>;
85
86pub async fn run_proxy(
88 route_table: Arc<RwLock<HashMap<String, Vec<RouteTarget>>>>,
89 wasm_triggers: SharedWasmTriggers,
90 wasm_invoker: Option<WasmInvoker>,
91 port: u16,
92 tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
93 acme_manager: Option<AcmeManager>,
94) -> anyhow::Result<()> {
95 let addr = format!("0.0.0.0:{port}");
96 let listener = TcpListener::bind(&addr).await?;
97 let proto = if tls_acceptor.is_some() {
98 "HTTPS"
99 } else {
100 "HTTP"
101 };
102 info!("Reverse proxy listening on {addr} ({proto})");
103
104 serve_loop(
105 listener,
106 route_table,
107 wasm_triggers,
108 wasm_invoker,
109 tls_acceptor,
110 acme_manager,
111 )
112 .await
113}
114
115#[allow(clippy::too_many_arguments)]
117pub async fn run_proxy_with_fallback(
118 route_table: Arc<RwLock<HashMap<String, Vec<RouteTarget>>>>,
119 wasm_triggers: SharedWasmTriggers,
120 wasm_invoker: Option<WasmInvoker>,
121 port: u16,
122 tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
123 acme_manager: Option<AcmeManager>,
124 fallback: Option<FallbackConfig>,
125) -> anyhow::Result<()> {
126 let addr = format!("0.0.0.0:{port}");
127 let listener = TcpListener::bind(&addr).await?;
128 let proto = if tls_acceptor.is_some() {
129 "HTTPS"
130 } else {
131 "HTTP"
132 };
133 info!("Reverse proxy listening on {addr} ({proto})");
134
135 serve_loop_with_fallback(
136 listener,
137 route_table,
138 wasm_triggers,
139 wasm_invoker,
140 tls_acceptor,
141 acme_manager,
142 fallback,
143 )
144 .await
145}
146
147pub type SharedCertResolver = Arc<acme::DynCertResolver>;
149
150pub async fn run_proxy_with_acme(
156 route_table: Arc<RwLock<HashMap<String, Vec<RouteTarget>>>>,
157 wasm_triggers: SharedWasmTriggers,
158 wasm_invoker: Option<WasmInvoker>,
159 acme_manager: AcmeManager,
160 domains: Vec<String>,
161) -> anyhow::Result<SharedCertResolver> {
162 run_proxy_with_acme_and_fallback(
163 route_table,
164 wasm_triggers,
165 wasm_invoker,
166 acme_manager,
167 domains,
168 None,
169 )
170 .await
171}
172
173#[allow(clippy::too_many_arguments)]
175pub async fn run_proxy_with_acme_and_fallback(
176 route_table: Arc<RwLock<HashMap<String, Vec<RouteTarget>>>>,
177 wasm_triggers: SharedWasmTriggers,
178 wasm_invoker: Option<WasmInvoker>,
179 acme_manager: AcmeManager,
180 domains: Vec<String>,
181 fallback: Option<FallbackConfig>,
182) -> anyhow::Result<SharedCertResolver> {
183 let resolver = Arc::new(acme::DynCertResolver::new());
184
185 let acme_mgr = acme_manager.clone();
186 let routes_clone = route_table.clone();
187 let triggers_clone = wasm_triggers.clone();
188 let invoker_clone = wasm_invoker.clone();
189 let fallback_http = fallback.clone();
190 let fallback_tls = fallback.clone();
191
192 let http_handle = tokio::spawn({
194 let acme = acme_mgr.clone();
195 let routes = routes_clone.clone();
196 let triggers = triggers_clone.clone();
197 let invoker = invoker_clone.clone();
198 async move {
199 if let Err(e) = run_proxy_with_fallback(
200 routes,
201 triggers,
202 invoker,
203 80,
204 None,
205 Some(acme),
206 fallback_http,
207 )
208 .await
209 {
210 error!("HTTP listener failed: {e}");
211 }
212 }
213 });
214
215 let resolver_clone = resolver.clone();
217 let https_handle = tokio::spawn(async move {
218 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
219
220 const PER_DOMAIN_PROVISION_TIMEOUT: std::time::Duration =
228 std::time::Duration::from_secs(60);
229 for domain in &domains {
230 acme_mgr.add_domain(domain).await;
235 let fut = acme_mgr.ensure_cert_for_resolver(domain, &resolver_clone);
236 match tokio::time::timeout(PER_DOMAIN_PROVISION_TIMEOUT, fut).await {
237 Ok(Ok(())) => {}
238 Ok(Err(e)) => {
239 error!(domain = %domain, error = %e, "Failed to provision cert");
240 }
241 Err(_) => {
242 warn!(
243 domain = %domain,
244 timeout_secs = PER_DOMAIN_PROVISION_TIMEOUT.as_secs(),
245 "Cert provisioning timed out — skipping (HTTPS will start without this cert; reconciler may retry on demand)"
246 );
247 }
248 }
249 }
250
251 let config = tls::with_h2_alpn(
253 rustls::ServerConfig::builder()
254 .with_no_client_auth()
255 .with_cert_resolver(resolver_clone),
256 );
257
258 let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
259 info!(
260 "Starting HTTPS with SNI resolver ({} domains)",
261 domains.len()
262 );
263
264 let routes = routes_clone;
265 let triggers = triggers_clone;
266 let invoker = invoker_clone;
267 if let Err(e) = run_proxy_with_fallback(
268 routes,
269 triggers,
270 invoker,
271 443,
272 Some(acceptor),
273 Some(acme_mgr),
274 fallback_tls,
275 )
276 .await
277 {
278 error!("HTTPS listener failed: {e}");
279 }
280 });
281
282 tokio::spawn(async move {
285 tokio::select! {
286 _ = http_handle => warn!("HTTP listener exited"),
287 _ = https_handle => warn!("HTTPS listener exited"),
288 }
289 });
290
291 Ok(resolver)
292}
293
294async fn serve_loop(
296 listener: TcpListener,
297 route_table: Arc<RwLock<HashMap<String, Vec<RouteTarget>>>>,
298 wasm_triggers: SharedWasmTriggers,
299 wasm_invoker: Option<WasmInvoker>,
300 tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
301 acme_manager: Option<AcmeManager>,
302) -> anyhow::Result<()> {
303 serve_loop_with_fallback(
304 listener,
305 route_table,
306 wasm_triggers,
307 wasm_invoker,
308 tls_acceptor,
309 acme_manager,
310 None,
311 )
312 .await
313}
314
315#[allow(clippy::too_many_arguments)]
317pub(crate) async fn serve_loop_with_fallback(
318 listener: TcpListener,
319 route_table: Arc<RwLock<HashMap<String, Vec<RouteTarget>>>>,
320 wasm_triggers: SharedWasmTriggers,
321 wasm_invoker: Option<WasmInvoker>,
322 tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
323 acme_manager: Option<AcmeManager>,
324 fallback: Option<FallbackConfig>,
325) -> anyhow::Result<()> {
326 let counter = Arc::new(AtomicUsize::new(0));
327 let client = Arc::new(
328 reqwest::Client::builder()
329 .no_proxy()
330 .redirect(reqwest::redirect::Policy::none())
331 .connect_timeout(std::time::Duration::from_secs(10))
341 .read_timeout(std::time::Duration::from_secs(120))
342 .pool_idle_timeout(std::time::Duration::from_secs(90))
343 .build()
344 .expect("failed to build HTTP client"),
345 );
346 let https_enabled = tls_acceptor.is_some() || acme_manager.is_some();
354 let acme = acme_manager.map(Arc::new);
355 let is_tls = tls_acceptor.is_some();
356 let rate_limiter = RateLimiter::new();
357
358 let fallback = Arc::new(fallback);
359 loop {
360 let (stream, peer) = match listener.accept().await {
361 Ok(conn) => conn,
362 Err(e) => {
363 warn!("Proxy accept error: {e}");
364 continue;
365 }
366 };
367
368 let routes = route_table.clone();
369 let triggers = wasm_triggers.clone();
370 let invoker = wasm_invoker.clone();
371 let counter = counter.clone();
372 let client = client.clone();
373 let acme = acme.clone();
374 let tls = tls_acceptor.clone();
375 let rl = rate_limiter.clone();
376 let fb = fallback.clone();
377 let routes_for_sni = routes.clone();
378
379 let fb_for_service = fb.clone();
380 tokio::spawn(async move {
381 let service = service_fn(move |req: Request<Incoming>| {
382 let routes = routes.clone();
383 let triggers = triggers.clone();
384 let invoker = invoker.clone();
385 let counter = counter.clone();
386 let client = client.clone();
387 let acme = acme.clone();
388 let rl = rl.clone();
389 let fb = fb_for_service.clone();
390 async move {
391 if let Some(resp) = handle_acme_challenge(&req, acme.as_deref()).await {
392 return Ok(resp);
393 }
394 let mut resp = handle_request(
395 req,
396 &routes,
397 &triggers,
398 invoker.as_ref(),
399 &counter,
400 &client,
401 is_tls,
402 https_enabled,
403 &rl,
404 peer,
405 fb.as_ref().as_ref(),
406 )
407 .await?;
408 security_headers::apply(&mut resp, is_tls);
411 Ok::<_, hyper::Error>(resp)
412 }
413 });
414 if let Some(acceptor) = tls {
415 let mut stream = stream;
416 let sni = sni::peek_sni(&mut stream).await;
418 let should_passthrough = if let Some(ref host) = sni {
419 let routes_lock = routes_for_sni.read().await;
420 let known = routes_lock.contains_key(host);
421 drop(routes_lock);
422 !known && fb.as_ref().as_ref().and_then(|f| f.tls.as_ref()).is_some()
423 } else {
424 false
425 };
426
427 if should_passthrough {
428 let target = fb
429 .as_ref()
430 .as_ref()
431 .and_then(|f| f.tls.clone())
432 .expect("checked above");
433 debug!(?sni, %target, "SNI passthrough");
434 match tokio::net::TcpStream::connect(&target).await {
435 Ok(mut backend) => {
436 if let Err(e) =
437 tokio::io::copy_bidirectional(&mut stream, &mut backend).await
438 {
439 debug!("Passthrough copy error from {peer}: {e}");
440 }
441 }
442 Err(e) => warn!("Failed to connect to TLS fallback {target}: {e}"),
443 }
444 return;
445 }
446
447 match acceptor.accept(stream).await {
448 Ok(tls_stream) => {
449 let io = TokioIo::new(tls_stream);
450 if let Err(e) = auto::Builder::new(TokioExecutor::new())
455 .serve_connection_with_upgrades(io, service)
456 .await
457 {
458 debug!("TLS proxy error from {peer}: {e}");
459 }
460 }
461 Err(e) => debug!("TLS handshake failed from {peer}: {e}"),
462 }
463 } else {
464 let io = TokioIo::new(stream);
469 if let Err(e) = http1::Builder::new()
470 .serve_connection(io, service)
471 .with_upgrades()
472 .await
473 {
474 debug!("Proxy connection error from {peer}: {e}");
475 }
476 }
477 });
478 }
479}