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