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| {
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 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 != "localhost" {
146 let _ = tls_manager.remove_certificate("proxy", config_clone.port);
147 let _ = tls_manager.remove_certificate("proxy", 443);
149 }
150
151 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 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 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 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 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 let subdomain = if host_no_port == domain
283 || host_no_port == format!("www.{}", domain)
284 || host_no_port == "localhost"
285 {
286 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 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 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 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 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 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 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 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">→</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}