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: &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        // Use values from ProxyConfig directly — no need to re-load config
116        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 is set, remove any stale self-signed proxy certs
144                // that may have been generated for a different domain (e.g. *.localhost).
145                // This ensures the fallback cert always matches the current domain.
146                if production_domain != "localhost" {
147                    let _ = tls_manager.remove_certificate("proxy", config_clone.port);
148                    // Also remove stale proxy-443 cert from old buggy fallback code
149                    let _ = tls_manager.remove_certificate("proxy", 443);
150                }
151
152                // Use Let's Encrypt certificate if available, otherwise self-signed
153                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                // Store in global so ACME can hot-reload it later
191                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                    // Read current acceptor (may have been hot-reloaded by ACME)
211                    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        // Run both tasks concurrently
245        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    // Extract port from host header — use the external port the client sees, not the internal config port
273    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    // Extract subdomain by properly matching against the production domain
285    let subdomain = if host_no_port == domain
286        || host_no_port == format!("www.{}", domain)
287        || host_no_port == "localhost"
288    {
289        // Bare domain, www, or localhost — no subdomain
290        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        // Fallback for IP or unknown host — use first segment
297        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    // Analytics tracking — prefer forwarding headers (set by upstream reverse proxy),
319    // fall back to the actual TCP peer address so unique visitors are counted correctly
320    // even when the server is accessed directly.
321    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    // ACME challenges must be served BEFORE any redirects (Let's Encrypt validates on bare domain)
343    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    // Handle bare domain — redirect to default subdomain
358    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        // No default server — show welcome page
370        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    // Special subdomains served directly by the proxy
414    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    // Check if route exists
426    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">&rarr;</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}