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