Skip to main content

rush_sync_server/proxy/
handler.rs

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
7// Global TLS acceptor that ACME can hot-reload after provisioning a new certificate
8static 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    // Try LE cert first; fall back to self-signed with production domain SANs.
20    // This ensures the proxy always has a cert for the right domain, even if
21    // ACME hasn't succeeded yet.
22    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            // Use the proxy's HTTPS port (read from existing config if possible)
30            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| {
76            let manager = Arc::clone(&manager);
77            let client = Client::new();
78
79            async move {
80                Ok::<_, Infallible>(service_fn(move |req| {
81                    let manager = Arc::clone(&manager);
82                    let client = client.clone();
83                    handle_proxy_request(req, manager, client)
84                }))
85            }
86        });
87
88        let server = Server::bind(&addr).serve(make_svc);
89
90        log::info!(
91            "Reverse Proxy listening on http://{}:{}",
92            config.bind_address,
93            config.port
94        );
95        log::info!(
96            "Route pattern: {{servername}}.{{domain}} -> {}:{{port}}",
97            config.bind_address
98        );
99
100        if let Err(e) = server.await {
101            log::error!("Proxy server error: {}", e);
102        }
103
104        Ok(())
105    }
106
107    pub async fn start_with_https(&self) -> crate::core::prelude::Result<()> {
108        let config = self.manager.get_config();
109        let https_port = config.port + config.https_port_offset;
110
111        let manager_for_http = Arc::clone(&self.manager);
112        let manager_for_https = Arc::clone(&self.manager);
113        let config_clone = config.clone();
114        // Use values from ProxyConfig directly — no need to re-load config
115        let production_domain = config.production_domain.clone();
116        let use_lets_encrypt = config.use_lets_encrypt;
117
118        log::info!("Starting HTTP + HTTPS proxy servers...");
119        log::info!("  HTTP:  http://{}:{}", config.bind_address, config.port);
120        log::info!("  HTTPS: https://{}:{}", config.bind_address, https_port);
121        if use_lets_encrypt {
122            log::info!("  TLS:   Let's Encrypt for *.{}", production_domain);
123        }
124
125        let http_task = tokio::spawn(async move {
126            let proxy_server = ProxyServer::new(manager_for_http);
127            if let Err(e) = proxy_server.start().await {
128                log::error!("HTTP proxy failed: {}", e);
129            }
130        });
131
132        let https_task =
133            tokio::spawn(async move {
134                let tls_manager = match crate::server::tls::TlsManager::new(".rss/certs", 365) {
135                    Ok(manager) => manager,
136                    Err(e) => {
137                        log::error!("TLS manager creation failed: {}", e);
138                        return;
139                    }
140                };
141
142                // If production domain is set, remove any stale self-signed proxy certs
143                // that may have been generated for a different domain (e.g. *.localhost).
144                // This ensures the fallback cert always matches the current domain.
145                if production_domain != "localhost" {
146                    let _ = tls_manager.remove_certificate("proxy", config_clone.port);
147                    // Also remove stale proxy-443 cert from old buggy fallback code
148                    let _ = tls_manager.remove_certificate("proxy", 443);
149                }
150
151                // Use Let's Encrypt certificate if available, otherwise self-signed
152                let tls_config = if use_lets_encrypt && production_domain != "localhost" {
153                    match tls_manager.get_production_config(&production_domain) {
154                        Ok(config) => {
155                            log::info!("TLS: Using Let's Encrypt certificate for {}", production_domain);
156                            config
157                        }
158                        Err(e) => {
159                            log::warn!("TLS: Let's Encrypt cert not ready ({}), using self-signed for {}", e, production_domain);
160                            match tls_manager.get_rustls_config_for_domain(
161                                "proxy", config_clone.port, &production_domain,
162                            ) {
163                                Ok(c) => c,
164                                Err(e) => { log::error!("TLS config failed: {}", e); return; }
165                            }
166                        }
167                    }
168                } else {
169                    match tls_manager.get_rustls_config_for_domain(
170                        "proxy", config_clone.port, &production_domain,
171                    ) {
172                        Ok(config) => config,
173                        Err(e) => { log::error!("TLS config failed: {}", e); return; }
174                    }
175                };
176
177                let listener =
178                    match tokio::net::TcpListener::bind((&*config_clone.bind_address, https_port))
179                        .await
180                    {
181                        Ok(listener) => listener,
182                        Err(e) => {
183                            log::error!("HTTPS bind failed: {}", e);
184                            return;
185                        }
186                    };
187
188                let initial_acceptor = tokio_rustls::TlsAcceptor::from(tls_config);
189                // Store in global so ACME can hot-reload it later
190                match PROXY_TLS_ACCEPTOR.get() {
191                    Some(lock) => { if let Ok(mut g) = lock.write() { *g = initial_acceptor.clone(); } }
192                    None => { let _ = PROXY_TLS_ACCEPTOR.set(RwLock::new(initial_acceptor.clone())); }
193                }
194                log::info!(
195                    "HTTPS proxy listening on https://{}:{}",
196                    config_clone.bind_address,
197                    https_port
198                );
199
200                loop {
201                    let (stream, _) = match listener.accept().await {
202                        Ok(conn) => conn,
203                        Err(e) => {
204                            log::warn!("HTTPS accept failed: {}", e);
205                            continue;
206                        }
207                    };
208
209                    // Read current acceptor (may have been hot-reloaded by ACME)
210                    let acceptor = PROXY_TLS_ACCEPTOR
211                        .get()
212                        .and_then(|lock| lock.read().ok())
213                        .map(|a| a.clone())
214                        .unwrap_or_else(|| initial_acceptor.clone());
215                    let manager = Arc::clone(&manager_for_https);
216
217                    tokio::spawn(async move {
218                        let tls_stream = match acceptor.accept(stream).await {
219                            Ok(stream) => stream,
220                            Err(e) => {
221                                log::debug!("TLS handshake failed: {}", e);
222                                return;
223                            }
224                        };
225
226                        let service = hyper::service::service_fn(move |req| {
227                            let manager = Arc::clone(&manager);
228                            let client = hyper::Client::new();
229                            handle_proxy_request(req, manager, client)
230                        });
231
232                        if let Err(e) = hyper::server::conn::Http::new()
233                            .serve_connection(tls_stream, service)
234                            .await
235                        {
236                            log::debug!("HTTPS connection error: {}", e);
237                        }
238                    });
239                }
240            });
241
242        // Run both tasks concurrently
243        tokio::select! {
244            _ = http_task => log::error!("HTTP task ended"),
245            _ = https_task => log::error!("HTTPS task ended"),
246        }
247
248        Ok(())
249    }
250}
251
252use crate::core::helpers::html_escape;
253
254pub async fn handle_proxy_request(
255    req: Request<Body>,
256    manager: Arc<ProxyManager>,
257    client: Client<hyper::client::HttpConnector>,
258) -> Result<Response<Body>, hyper::Error> {
259    let config = manager.get_config();
260    let domain = config.production_domain.clone();
261
262    let host = req
263        .headers()
264        .get("host")
265        .and_then(|h| h.to_str().ok())
266        .map(|s| s.to_string())
267        .unwrap_or_else(|| domain.clone());
268
269    // Extract port from host header — use the external port the client sees, not the internal config port
270    let (host_no_port, external_port_suffix) = if let Some(colon) = host.rfind(':') {
271        let port_str = &host[colon + 1..];
272        if port_str.parse::<u16>().is_ok() {
273            (host[..colon].to_string(), format!(":{}", port_str))
274        } else {
275            (host.clone(), String::new())
276        }
277    } else {
278        (host.clone(), String::new())
279    };
280
281    // Extract subdomain by properly matching against the production domain
282    let subdomain = if host_no_port == domain
283        || host_no_port == format!("www.{}", domain)
284        || host_no_port == "localhost"
285    {
286        // Bare domain, www, or localhost — no subdomain
287        String::new()
288    } else if let Some(stripped) = host_no_port.strip_suffix(&format!(".{}", domain)) {
289        stripped.to_string()
290    } else if let Some(stripped) = host_no_port.strip_suffix(".localhost") {
291        stripped.to_string()
292    } else {
293        // Fallback for IP or unknown host — use first segment
294        if let Some(dot_pos) = host_no_port.find('.') {
295            host_no_port[..dot_pos].to_string()
296        } else {
297            host_no_port.clone()
298        }
299    };
300
301    let path_and_query = req
302        .uri()
303        .path_and_query()
304        .map(|pq| pq.as_str())
305        .unwrap_or("/")
306        .to_string();
307
308    log::info!(
309        "Proxy Request: Host='{}' -> Subdomain='{}' Path='{}'",
310        host,
311        if subdomain.is_empty() { "(bare domain)" } else { &subdomain },
312        path_and_query
313    );
314
315    // Analytics tracking
316    let client_ip = req
317        .headers()
318        .get("x-forwarded-for")
319        .or_else(|| req.headers().get("x-real-ip"))
320        .and_then(|h| h.to_str().ok())
321        .map(|s| s.split(',').next().unwrap_or("unknown").trim().to_string())
322        .unwrap_or_else(|| "unknown".to_string());
323    let proxy_user_agent = req
324        .headers()
325        .get("user-agent")
326        .and_then(|h| h.to_str().ok())
327        .unwrap_or("")
328        .to_string();
329    crate::server::analytics::track_request(
330        &subdomain,
331        &path_and_query,
332        &client_ip,
333        &proxy_user_agent,
334    );
335
336    // ACME challenges must be served BEFORE any redirects (Let's Encrypt validates on bare domain)
337    if path_and_query.starts_with("/.well-known/acme-challenge/") {
338        let token = path_and_query
339            .strip_prefix("/.well-known/acme-challenge/")
340            .unwrap_or("");
341        if let Some(key_auth) = crate::server::acme::get_challenge_response(token) {
342            log::info!("ACME: Serving challenge response for token {}", token);
343            return Ok(Response::builder()
344                .status(200)
345                .header("content-type", "text/plain")
346                .body(Body::from(key_auth))
347                .unwrap_or_else(|_| Response::new(Body::empty())));
348        }
349    }
350
351    // Handle bare domain — redirect to default subdomain
352    if subdomain.is_empty() {
353        if manager.get_target_port("default").await.is_some() {
354            return Ok(Response::builder()
355                .status(302)
356                .header(
357                    "location",
358                    format!("http://default.{}{}/", domain, external_port_suffix),
359                )
360                .body(Body::empty())
361                .expect("redirect response"));
362        }
363        // No default server — show welcome page
364        let routes = manager.get_routes().await;
365        let route_links = routes
366            .iter()
367            .map(|r| {
368                format!(
369                    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>"#,
370                    sub = r.subdomain,
371                    dom = domain,
372                    port = external_port_suffix
373                )
374            })
375            .collect::<Vec<_>>()
376            .join("\n");
377
378        return Ok(Response::builder()
379            .status(200)
380            .header("content-type", "text/html")
381            .body(Body::from(format!(
382                r#"<!DOCTYPE html>
383<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
384<title>Rush Sync Server</title>
385<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>
386</head><body><div class="c">
387<h1>RUSH<span>.</span>SYNC<span>.</span>SERVER</h1>
388<p class="sub">{}</p>
389<div class="routes">{}</div>
390<p class="info">Powered by Rush Sync Server v0.3.8</p>
391<p class="info" style="margin-top:8px"><a class="gh" href="https://github.com/LEVOGNE/rush.sync.server">GitHub</a></p>
392</div></body></html>"#,
393                if routes.is_empty() {
394                    "No servers are running yet. Create one to get started.".to_string()
395                } else {
396                    format!("{} active server{} on this domain:", routes.len(), if routes.len() == 1 { "" } else { "s" })
397                },
398                if routes.is_empty() {
399                    String::new()
400                } else {
401                    route_links
402                }
403            )))
404            .expect("welcome response"));
405    }
406
407    // Special subdomains served directly by the proxy
408    if subdomain == "blog" {
409        let blog = include_str!("blog.html")
410            .replace("{{DOMAIN}}", &html_escape(&domain))
411            .replace("{{PORT_SUFFIX}}", &external_port_suffix);
412        return Ok(Response::builder()
413            .status(200)
414            .header("content-type", "text/html; charset=utf-8")
415            .body(Body::from(blog))
416            .expect("blog response"));
417    }
418
419    // Check if route exists
420    let routes = manager.get_routes().await;
421    log::info!(
422        "Available routes: {:?}",
423        routes.iter().map(|r| &r.subdomain).collect::<Vec<_>>()
424    );
425
426    if let Some(target_port) = manager.get_target_port(&subdomain).await {
427        let target_uri = format!("http://127.0.0.1:{}{}", target_port, path_and_query);
428
429        match target_uri.parse::<Uri>() {
430            Ok(uri) => {
431                let (mut parts, body) = req.into_parts();
432                parts.uri = uri;
433                parts.headers.insert(
434                    "host",
435                    format!("127.0.0.1:{}", target_port)
436                        .parse()
437                        .unwrap_or_else(|_| hyper::header::HeaderValue::from_static("localhost")),
438                );
439                let backend_req = Request::from_parts(parts, body);
440
441                match client.request(backend_req).await {
442                    Ok(response) => Ok(response),
443                    Err(e) => {
444                        log::warn!("Backend request failed for {}.{}: {}", subdomain, domain, e);
445                        Ok(Response::builder()
446                            .status(502)
447                            .header("content-type", "text/html")
448                            .body(Body::from(format!(
449                                r#"<!DOCTYPE html>
450<html><head><title>Backend Unavailable</title></head>
451<body>
452<h1>502 Bad Gateway</h1>
453<p>Backend server for <strong>{}.{}</strong> is not responding.</p>
454<p>Target: 127.0.0.1:{}</p>
455</body></html>"#,
456                                html_escape(&subdomain),
457                                html_escape(&domain),
458                                target_port
459                            )))
460                            .expect("static 502 response"))
461                    }
462                }
463            }
464            Err(_) => Ok(Response::builder()
465                .status(400)
466                .body(Body::from("Invalid target URI"))
467                .expect("static 400 response")),
468        }
469    } else {
470        let routes = manager.get_routes().await;
471        let routes_html = if routes.is_empty() {
472            r#"<div class="no-routes">No servers are running on this domain yet.</div>"#.to_string()
473        } else {
474            format!(
475                r#"<p class="lbl">Active Servers on this Domain</p><div class="route-grid">{}</div>"#,
476                routes.iter().map(|r| format!(
477                    r#"<a href="http://{sub}.{dom}{port}/">{sub}.{dom} <span class="arrow">&rarr;</span></a>"#,
478                    sub = r.subdomain, dom = domain, port = external_port_suffix
479                )).collect::<Vec<_>>().join("\n")
480            )
481        };
482
483        let showroom = include_str!("showroom.html")
484            .replace("{{SUBDOMAIN}}", &html_escape(&subdomain))
485            .replace("{{DOMAIN}}", &html_escape(&domain))
486            .replace("{{PORT_SUFFIX}}", &external_port_suffix)
487            .replace("{{ROUTES_HTML}}", &routes_html);
488
489        Ok(Response::builder()
490            .status(200)
491            .header("content-type", "text/html; charset=utf-8")
492            .body(Body::from(showroom))
493            .expect("showroom response"))
494    }
495}