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 let fut = acme_mgr.ensure_cert_for_resolver(domain, &resolver_clone);
230 match tokio::time::timeout(PER_DOMAIN_PROVISION_TIMEOUT, fut).await {
231 Ok(Ok(())) => {}
232 Ok(Err(e)) => {
233 error!(domain = %domain, error = %e, "Failed to provision cert");
234 }
235 Err(_) => {
236 warn!(
237 domain = %domain,
238 timeout_secs = PER_DOMAIN_PROVISION_TIMEOUT.as_secs(),
239 "Cert provisioning timed out — skipping (HTTPS will start without this cert; reconciler may retry on demand)"
240 );
241 }
242 }
243 }
244
245 let config = rustls::ServerConfig::builder()
247 .with_no_client_auth()
248 .with_cert_resolver(resolver_clone);
249
250 let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
251 info!(
252 "Starting HTTPS with SNI resolver ({} domains)",
253 domains.len()
254 );
255
256 let routes = routes_clone;
257 let triggers = triggers_clone;
258 let invoker = invoker_clone;
259 if let Err(e) = run_proxy_with_fallback(
260 routes,
261 triggers,
262 invoker,
263 443,
264 Some(acceptor),
265 Some(acme_mgr),
266 fallback_tls,
267 )
268 .await
269 {
270 error!("HTTPS listener failed: {e}");
271 }
272 });
273
274 tokio::spawn(async move {
277 tokio::select! {
278 _ = http_handle => warn!("HTTP listener exited"),
279 _ = https_handle => warn!("HTTPS listener exited"),
280 }
281 });
282
283 Ok(resolver)
284}
285
286async fn serve_loop(
288 listener: TcpListener,
289 route_table: Arc<RwLock<HashMap<String, Vec<RouteTarget>>>>,
290 wasm_triggers: SharedWasmTriggers,
291 wasm_invoker: Option<WasmInvoker>,
292 tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
293 acme_manager: Option<AcmeManager>,
294) -> anyhow::Result<()> {
295 serve_loop_with_fallback(
296 listener,
297 route_table,
298 wasm_triggers,
299 wasm_invoker,
300 tls_acceptor,
301 acme_manager,
302 None,
303 )
304 .await
305}
306
307#[allow(clippy::too_many_arguments)]
309pub(crate) async fn serve_loop_with_fallback(
310 listener: TcpListener,
311 route_table: Arc<RwLock<HashMap<String, Vec<RouteTarget>>>>,
312 wasm_triggers: SharedWasmTriggers,
313 wasm_invoker: Option<WasmInvoker>,
314 tls_acceptor: Option<tokio_rustls::TlsAcceptor>,
315 acme_manager: Option<AcmeManager>,
316 fallback: Option<FallbackConfig>,
317) -> anyhow::Result<()> {
318 let counter = Arc::new(AtomicUsize::new(0));
319 let client = Arc::new(
320 reqwest::Client::builder()
321 .no_proxy()
322 .redirect(reqwest::redirect::Policy::none())
323 .connect_timeout(std::time::Duration::from_secs(10))
333 .read_timeout(std::time::Duration::from_secs(120))
334 .pool_idle_timeout(std::time::Duration::from_secs(90))
335 .build()
336 .expect("failed to build HTTP client"),
337 );
338 let https_enabled = tls_acceptor.is_some() || acme_manager.is_some();
346 let acme = acme_manager.map(Arc::new);
347 let is_tls = tls_acceptor.is_some();
348 let rate_limiter = RateLimiter::new();
349
350 let fallback = Arc::new(fallback);
351 loop {
352 let (stream, peer) = match listener.accept().await {
353 Ok(conn) => conn,
354 Err(e) => {
355 warn!("Proxy accept error: {e}");
356 continue;
357 }
358 };
359
360 let routes = route_table.clone();
361 let triggers = wasm_triggers.clone();
362 let invoker = wasm_invoker.clone();
363 let counter = counter.clone();
364 let client = client.clone();
365 let acme = acme.clone();
366 let tls = tls_acceptor.clone();
367 let rl = rate_limiter.clone();
368 let fb = fallback.clone();
369 let routes_for_sni = routes.clone();
370
371 let fb_for_service = fb.clone();
372 tokio::spawn(async move {
373 let service = service_fn(move |req: Request<Incoming>| {
374 let routes = routes.clone();
375 let triggers = triggers.clone();
376 let invoker = invoker.clone();
377 let counter = counter.clone();
378 let client = client.clone();
379 let acme = acme.clone();
380 let rl = rl.clone();
381 let fb = fb_for_service.clone();
382 async move {
383 if let Some(resp) = handle_acme_challenge(&req, acme.as_deref()).await {
384 return Ok(resp);
385 }
386 let mut resp = handle_request(
387 req,
388 &routes,
389 &triggers,
390 invoker.as_ref(),
391 &counter,
392 &client,
393 is_tls,
394 https_enabled,
395 &rl,
396 peer,
397 fb.as_ref().as_ref(),
398 )
399 .await?;
400 security_headers::apply(&mut resp, is_tls);
403 Ok::<_, hyper::Error>(resp)
404 }
405 });
406 if let Some(acceptor) = tls {
407 let mut stream = stream;
408 let sni = sni::peek_sni(&mut stream).await;
410 let should_passthrough = if let Some(ref host) = sni {
411 let routes_lock = routes_for_sni.read().await;
412 let known = routes_lock.contains_key(host);
413 drop(routes_lock);
414 !known && fb.as_ref().as_ref().and_then(|f| f.tls.as_ref()).is_some()
415 } else {
416 false
417 };
418
419 if should_passthrough {
420 let target = fb
421 .as_ref()
422 .as_ref()
423 .and_then(|f| f.tls.clone())
424 .expect("checked above");
425 debug!(?sni, %target, "SNI passthrough");
426 match tokio::net::TcpStream::connect(&target).await {
427 Ok(mut backend) => {
428 if let Err(e) =
429 tokio::io::copy_bidirectional(&mut stream, &mut backend).await
430 {
431 debug!("Passthrough copy error from {peer}: {e}");
432 }
433 }
434 Err(e) => warn!("Failed to connect to TLS fallback {target}: {e}"),
435 }
436 return;
437 }
438
439 match acceptor.accept(stream).await {
440 Ok(tls_stream) => {
441 let io = TokioIo::new(tls_stream);
442 if let Err(e) = http1::Builder::new()
443 .serve_connection(io, service)
444 .with_upgrades()
445 .await
446 {
447 debug!("TLS proxy error from {peer}: {e}");
448 }
449 }
450 Err(e) => debug!("TLS handshake failed from {peer}: {e}"),
451 }
452 } else {
453 let io = TokioIo::new(stream);
454 if let Err(e) = http1::Builder::new()
455 .serve_connection(io, service)
456 .with_upgrades()
457 .await
458 {
459 debug!("Proxy connection error from {peer}: {e}");
460 }
461 }
462 });
463 }
464}