1use crate::proxy::ProxyManager;
2use hyper::service::{make_service_fn, service_fn};
3use hyper::{Body, Client, Request, Response, Server, Uri};
4use std::convert::Infallible;
5use std::sync::{Arc, OnceLock, RwLock};
6
7static PROXY_TLS_ACCEPTOR: OnceLock<RwLock<tokio_rustls::TlsAcceptor>> = OnceLock::new();
9
10pub fn reload_proxy_tls(domain: &str) {
11 let tls_manager = match crate::server::tls::TlsManager::new(".rss/certs", 365) {
12 Ok(m) => m,
13 Err(e) => {
14 log::error!("TLS reload: manager creation failed: {}", e);
15 return;
16 }
17 };
18
19 let config = match tls_manager.get_production_config(domain) {
23 Ok(c) => {
24 log::info!("TLS reload: loaded Let's Encrypt certificate for {}", domain);
25 c
26 }
27 Err(e) => {
28 log::warn!("TLS reload: LE cert not available ({}), trying self-signed for {}", e, domain);
29 let proxy_port = crate::server::handlers::web::get_proxy_https_port();
31 match tls_manager.get_rustls_config_for_domain("proxy", proxy_port, domain) {
32 Ok(c) => {
33 log::info!("TLS reload: using self-signed certificate for {}", domain);
34 c
35 }
36 Err(e2) => {
37 log::error!("TLS reload: all cert loading failed: {}", e2);
38 return;
39 }
40 }
41 }
42 };
43
44 let new_acceptor = tokio_rustls::TlsAcceptor::from(config);
45 match PROXY_TLS_ACCEPTOR.get() {
46 Some(lock) => {
47 if let Ok(mut guard) = lock.write() {
48 *guard = new_acceptor;
49 log::info!("TLS: Proxy certificate hot-reloaded for {}", domain);
50 }
51 }
52 None => {
53 let _ = PROXY_TLS_ACCEPTOR.set(RwLock::new(new_acceptor));
54 }
55 }
56}
57
58pub struct ProxyServer {
59 manager: Arc<ProxyManager>,
60}
61
62impl ProxyServer {
63 pub fn new(manager: Arc<ProxyManager>) -> Self {
64 Self { manager }
65 }
66
67 pub async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
68 let config = self.manager.get_config();
69 let addr: std::net::SocketAddr = format!("{}:{}", config.bind_address, config.port)
70 .parse()
71 .map_err(|e| format!("Invalid bind address: {}", e))?;
72
73 let manager = Arc::clone(&self.manager);
74
75 let make_svc = make_service_fn(move |conn: &hyper::server::conn::AddrStream| {
76 let manager = Arc::clone(&manager);
77 let client = Client::new();
78 let remote_addr = conn.remote_addr();
79
80 async move {
81 Ok::<_, Infallible>(service_fn(move |req| {
82 let manager = Arc::clone(&manager);
83 let client = client.clone();
84 handle_proxy_request(req, manager, client, remote_addr)
85 }))
86 }
87 });
88
89 let server = Server::bind(&addr).serve(make_svc);
90
91 log::info!(
92 "Reverse Proxy listening on http://{}:{}",
93 config.bind_address,
94 config.port
95 );
96 log::info!(
97 "Route pattern: {{servername}}.{{domain}} -> {}:{{port}}",
98 config.bind_address
99 );
100
101 if let Err(e) = server.await {
102 log::error!("Proxy server error: {}", e);
103 }
104
105 Ok(())
106 }
107
108 pub async fn start_with_https(&self) -> crate::core::prelude::Result<()> {
109 let config = self.manager.get_config();
110 let https_port = config.port + config.https_port_offset;
111
112 let manager_for_http = Arc::clone(&self.manager);
113 let manager_for_https = Arc::clone(&self.manager);
114 let config_clone = config.clone();
115 let production_domain = config.production_domain.clone();
117 let use_lets_encrypt = config.use_lets_encrypt;
118
119 log::info!("Starting HTTP + HTTPS proxy servers...");
120 log::info!(" HTTP: http://{}:{}", config.bind_address, config.port);
121 log::info!(" HTTPS: https://{}:{}", config.bind_address, https_port);
122 if use_lets_encrypt {
123 log::info!(" TLS: Let's Encrypt for *.{}", production_domain);
124 }
125
126 let http_task = tokio::spawn(async move {
127 let proxy_server = ProxyServer::new(manager_for_http);
128 if let Err(e) = proxy_server.start().await {
129 log::error!("HTTP proxy failed: {}", e);
130 }
131 });
132
133 let https_task =
134 tokio::spawn(async move {
135 let tls_manager = match crate::server::tls::TlsManager::new(".rss/certs", 365) {
136 Ok(manager) => manager,
137 Err(e) => {
138 log::error!("TLS manager creation failed: {}", e);
139 return;
140 }
141 };
142
143 if production_domain != "localhost" {
147 let _ = tls_manager.remove_certificate("proxy", config_clone.port);
148 let _ = tls_manager.remove_certificate("proxy", 443);
150 }
151
152 let tls_config = if use_lets_encrypt && production_domain != "localhost" {
154 match tls_manager.get_production_config(&production_domain) {
155 Ok(config) => {
156 log::info!("TLS: Using Let's Encrypt certificate for {}", production_domain);
157 config
158 }
159 Err(e) => {
160 log::warn!("TLS: Let's Encrypt cert not ready ({}), using self-signed for {}", e, production_domain);
161 match tls_manager.get_rustls_config_for_domain(
162 "proxy", config_clone.port, &production_domain,
163 ) {
164 Ok(c) => c,
165 Err(e) => { log::error!("TLS config failed: {}", e); return; }
166 }
167 }
168 }
169 } else {
170 match tls_manager.get_rustls_config_for_domain(
171 "proxy", config_clone.port, &production_domain,
172 ) {
173 Ok(config) => config,
174 Err(e) => { log::error!("TLS config failed: {}", e); return; }
175 }
176 };
177
178 let listener =
179 match tokio::net::TcpListener::bind((&*config_clone.bind_address, https_port))
180 .await
181 {
182 Ok(listener) => listener,
183 Err(e) => {
184 log::error!("HTTPS bind failed: {}", e);
185 return;
186 }
187 };
188
189 let initial_acceptor = tokio_rustls::TlsAcceptor::from(tls_config);
190 match PROXY_TLS_ACCEPTOR.get() {
192 Some(lock) => { if let Ok(mut g) = lock.write() { *g = initial_acceptor.clone(); } }
193 None => { let _ = PROXY_TLS_ACCEPTOR.set(RwLock::new(initial_acceptor.clone())); }
194 }
195 log::info!(
196 "HTTPS proxy listening on https://{}:{}",
197 config_clone.bind_address,
198 https_port
199 );
200
201 loop {
202 let (stream, remote_addr) = match listener.accept().await {
203 Ok(conn) => conn,
204 Err(e) => {
205 log::warn!("HTTPS accept failed: {}", e);
206 continue;
207 }
208 };
209
210 let acceptor = PROXY_TLS_ACCEPTOR
212 .get()
213 .and_then(|lock| lock.read().ok())
214 .map(|a| a.clone())
215 .unwrap_or_else(|| initial_acceptor.clone());
216 let manager = Arc::clone(&manager_for_https);
217
218 tokio::spawn(async move {
219 let tls_stream = match acceptor.accept(stream).await {
220 Ok(stream) => stream,
221 Err(e) => {
222 log::debug!("TLS handshake failed: {}", e);
223 return;
224 }
225 };
226
227 let client = hyper::Client::new();
228 let service = hyper::service::service_fn(move |req| {
229 let manager = Arc::clone(&manager);
230 let client = client.clone();
231 handle_proxy_request(req, manager, client, remote_addr)
232 });
233
234 if let Err(e) = hyper::server::conn::Http::new()
235 .serve_connection(tls_stream, service)
236 .await
237 {
238 log::debug!("HTTPS connection error: {}", e);
239 }
240 });
241 }
242 });
243
244 tokio::select! {
246 _ = http_task => log::error!("HTTP task ended"),
247 _ = https_task => log::error!("HTTPS task ended"),
248 }
249
250 Ok(())
251 }
252}
253
254use crate::core::helpers::html_escape;
255
256pub async fn handle_proxy_request(
257 req: Request<Body>,
258 manager: Arc<ProxyManager>,
259 client: Client<hyper::client::HttpConnector>,
260 remote_addr: std::net::SocketAddr,
261) -> Result<Response<Body>, hyper::Error> {
262 let config = manager.get_config();
263 let domain = config.production_domain.clone();
264
265 let host = req
266 .headers()
267 .get("host")
268 .and_then(|h| h.to_str().ok())
269 .map(|s| s.to_string())
270 .unwrap_or_else(|| domain.clone());
271
272 let (host_no_port, external_port_suffix) = if let Some(colon) = host.rfind(':') {
274 let port_str = &host[colon + 1..];
275 if port_str.parse::<u16>().is_ok() {
276 (host[..colon].to_string(), format!(":{}", port_str))
277 } else {
278 (host.clone(), String::new())
279 }
280 } else {
281 (host.clone(), String::new())
282 };
283
284 let subdomain = if host_no_port == domain
286 || host_no_port == format!("www.{}", domain)
287 || host_no_port == "localhost"
288 {
289 String::new()
291 } else if let Some(stripped) = host_no_port.strip_suffix(&format!(".{}", domain)) {
292 stripped.to_string()
293 } else if let Some(stripped) = host_no_port.strip_suffix(".localhost") {
294 stripped.to_string()
295 } else {
296 if let Some(dot_pos) = host_no_port.find('.') {
298 host_no_port[..dot_pos].to_string()
299 } else {
300 host_no_port.clone()
301 }
302 };
303
304 let path_and_query = req
305 .uri()
306 .path_and_query()
307 .map(|pq| pq.as_str())
308 .unwrap_or("/")
309 .to_string();
310
311 log::info!(
312 "Proxy Request: Host='{}' -> Subdomain='{}' Path='{}'",
313 host,
314 if subdomain.is_empty() { "(bare domain)" } else { &subdomain },
315 path_and_query
316 );
317
318 let peer_ip = remote_addr.ip().to_string();
322 let client_ip = req
323 .headers()
324 .get("x-forwarded-for")
325 .or_else(|| req.headers().get("x-real-ip"))
326 .and_then(|h| h.to_str().ok())
327 .map(|s| s.split(',').next().unwrap_or(&peer_ip).trim().to_string())
328 .unwrap_or(peer_ip);
329 let proxy_user_agent = req
330 .headers()
331 .get("user-agent")
332 .and_then(|h| h.to_str().ok())
333 .unwrap_or("")
334 .to_string();
335 crate::server::analytics::track_request(
336 &subdomain,
337 &path_and_query,
338 &client_ip,
339 &proxy_user_agent,
340 );
341
342 if path_and_query.starts_with("/.well-known/acme-challenge/") {
344 let token = path_and_query
345 .strip_prefix("/.well-known/acme-challenge/")
346 .unwrap_or("");
347 if let Some(key_auth) = crate::server::acme::get_challenge_response(token) {
348 log::info!("ACME: Serving challenge response for token {}", token);
349 return Ok(Response::builder()
350 .status(200)
351 .header("content-type", "text/plain")
352 .body(Body::from(key_auth))
353 .unwrap_or_else(|_| Response::new(Body::empty())));
354 }
355 }
356
357 if subdomain.is_empty() {
359 if manager.get_target_port("default").await.is_some() {
360 return Ok(Response::builder()
361 .status(302)
362 .header(
363 "location",
364 format!("http://default.{}{}/", domain, external_port_suffix),
365 )
366 .body(Body::empty())
367 .expect("redirect response"));
368 }
369 let routes = manager.get_routes().await;
371 let route_links = routes
372 .iter()
373 .map(|r| {
374 format!(
375 r#"<a href="http://{sub}.{dom}{port}/" style="display:inline-block;padding:10px 20px;background:rgba(108,99,255,0.15);border:1px solid rgba(108,99,255,0.3);border-radius:8px;color:#6c63ff;text-decoration:none;font-weight:500;margin:4px;">{sub}.{dom}</a>"#,
376 sub = r.subdomain,
377 dom = domain,
378 port = external_port_suffix
379 )
380 })
381 .collect::<Vec<_>>()
382 .join("\n");
383
384 return Ok(Response::builder()
385 .status(200)
386 .header("content-type", "text/html")
387 .body(Body::from(format!(
388 r#"<!DOCTYPE html>
389<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
390<title>Rush Sync Server</title>
391<style>*{{margin:0;padding:0;box-sizing:border-box}}body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0a0f;color:#e4e4ef;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px}}.c{{text-align:center;max-width:600px}}h1{{font-size:clamp(32px,5vw,48px);font-weight:800;letter-spacing:-1px;margin-bottom:12px}}h1 span{{color:#6c63ff}}.sub{{color:#8888a0;font-size:16px;margin-bottom:32px}}.routes{{margin:24px 0;display:flex;flex-wrap:wrap;justify-content:center;gap:8px}}.info{{color:#8888a0;font-size:14px;margin-top:24px}}a.gh{{color:#6c63ff;text-decoration:none}}</style>
392</head><body><div class="c">
393<h1>RUSH<span>.</span>SYNC<span>.</span>SERVER</h1>
394<p class="sub">{}</p>
395<div class="routes">{}</div>
396<p class="info">Powered by Rush Sync Server v0.3.9</p>
397<p class="info" style="margin-top:8px"><a class="gh" href="https://github.com/LEVOGNE/rush.sync.server">GitHub</a></p>
398</div></body></html>"#,
399 if routes.is_empty() {
400 "No servers are running yet. Create one to get started.".to_string()
401 } else {
402 format!("{} active server{} on this domain:", routes.len(), if routes.len() == 1 { "" } else { "s" })
403 },
404 if routes.is_empty() {
405 String::new()
406 } else {
407 route_links
408 }
409 )))
410 .expect("welcome response"));
411 }
412
413 if subdomain == "blog" {
415 let blog = include_str!("blog.html")
416 .replace("{{DOMAIN}}", &html_escape(&domain))
417 .replace("{{PORT_SUFFIX}}", &external_port_suffix);
418 return Ok(Response::builder()
419 .status(200)
420 .header("content-type", "text/html; charset=utf-8")
421 .body(Body::from(blog))
422 .expect("blog response"));
423 }
424
425 let routes = manager.get_routes().await;
427 log::info!(
428 "Available routes: {:?}",
429 routes.iter().map(|r| &r.subdomain).collect::<Vec<_>>()
430 );
431
432 if let Some(target_port) = manager.get_target_port(&subdomain).await {
433 let target_uri = format!("http://127.0.0.1:{}{}", target_port, path_and_query);
434
435 match target_uri.parse::<Uri>() {
436 Ok(uri) => {
437 let (mut parts, body) = req.into_parts();
438 parts.uri = uri;
439 parts.headers.insert(
440 "host",
441 format!("127.0.0.1:{}", target_port)
442 .parse()
443 .unwrap_or_else(|_| hyper::header::HeaderValue::from_static("localhost")),
444 );
445 let backend_req = Request::from_parts(parts, body);
446
447 match client.request(backend_req).await {
448 Ok(response) => Ok(response),
449 Err(e) => {
450 log::warn!("Backend request failed for {}.{}: {}", subdomain, domain, e);
451 Ok(Response::builder()
452 .status(502)
453 .header("content-type", "text/html")
454 .body(Body::from(format!(
455 r#"<!DOCTYPE html>
456<html><head><title>Backend Unavailable</title></head>
457<body>
458<h1>502 Bad Gateway</h1>
459<p>Backend server for <strong>{}.{}</strong> is not responding.</p>
460<p>Target: 127.0.0.1:{}</p>
461</body></html>"#,
462 html_escape(&subdomain),
463 html_escape(&domain),
464 target_port
465 )))
466 .expect("static 502 response"))
467 }
468 }
469 }
470 Err(_) => Ok(Response::builder()
471 .status(400)
472 .body(Body::from("Invalid target URI"))
473 .expect("static 400 response")),
474 }
475 } else {
476 let routes_html = if routes.is_empty() {
477 r#"<div class="no-routes">No servers are running on this domain yet.</div>"#.to_string()
478 } else {
479 format!(
480 r#"<p class="lbl">Active Servers on this Domain</p><div class="route-grid">{}</div>"#,
481 routes.iter().map(|r| format!(
482 r#"<a href="http://{sub}.{dom}{port}/">{sub}.{dom} <span class="arrow">→</span></a>"#,
483 sub = r.subdomain, dom = domain, port = external_port_suffix
484 )).collect::<Vec<_>>().join("\n")
485 )
486 };
487
488 let showroom = include_str!("showroom.html")
489 .replace("{{SUBDOMAIN}}", &html_escape(&subdomain))
490 .replace("{{DOMAIN}}", &html_escape(&domain))
491 .replace("{{PORT_SUFFIX}}", &external_port_suffix)
492 .replace("{{ROUTES_HTML}}", &routes_html);
493
494 Ok(Response::builder()
495 .status(200)
496 .header("content-type", "text/html; charset=utf-8")
497 .body(Body::from(showroom))
498 .expect("showroom response"))
499 }
500}