1use std::{collections::HashMap, sync::RwLock};
36
37use tauri::http::{
38 Method,
39 request::Request,
40 response::{Builder, Response},
41};
42
43use super::ServiceRegistry::ServiceRegistry;
44use crate::dev_log;
45
46static SERVICE_REGISTRY:RwLock<Option<ServiceRegistry>> = RwLock::new(None);
48
49pub fn init_service_registry(registry:ServiceRegistry) {
54 let mut registry_lock = SERVICE_REGISTRY.write().unwrap();
55 *registry_lock = Some(registry);
56}
57
58fn get_service_registry() -> Option<ServiceRegistry> {
70 let guard = SERVICE_REGISTRY.read().ok()?;
71 guard.clone()
72}
73
74#[derive(Clone, Debug)]
79pub struct DnsPort(pub u16);
80
81#[derive(Clone)]
83struct CacheEntry {
84 body:Vec<u8>,
86 content_type:String,
88 cache_control:String,
90 etag:Option<String>,
92 last_modified:Option<String>,
94}
95
96static CACHE:RwLock<Option<HashMap<String, CacheEntry>>> = RwLock::new(None);
104
105fn init_cache() {
107 let mut cache = CACHE.write().unwrap();
108 if cache.is_none() {
109 *cache = Some(HashMap::new());
110 }
111}
112
113fn get_cached(path:&str) -> Option<CacheEntry> {
115 let cache = CACHE.read().unwrap();
116 cache.as_ref()?.get(path).cloned()
117}
118
119fn set_cached(path:&str, entry:CacheEntry) {
121 let mut cache = CACHE.write().unwrap();
122 if let Some(cache) = cache.as_mut() {
123 cache.insert(path.to_string(), entry);
124 }
125}
126
127fn should_cache(path:&str) -> bool {
131 let path_lower = path.to_lowercase();
132 path_lower.ends_with(".css")
133 || path_lower.ends_with(".js")
134 || path_lower.ends_with(".png")
135 || path_lower.ends_with(".jpg")
136 || path_lower.ends_with(".jpeg")
137 || path_lower.ends_with(".gif")
138 || path_lower.ends_with(".svg")
139 || path_lower.ends_with(".woff")
140 || path_lower.ends_with(".woff2")
141 || path_lower.ends_with(".ttf")
142 || path_lower.ends_with(".eot")
143 || path_lower.ends_with(".ico")
144}
145
146fn parse_land_uri(uri:&str) -> Result<(String, String), String> {
166 let without_scheme = uri
168 .strip_prefix("land://")
169 .ok_or_else(|| format!("Invalid land:// URI: {}", uri))?;
170
171 let parts:Vec<&str> = without_scheme.splitn(2, '/').collect();
173
174 let domain = parts.get(0).ok_or_else(|| format!("No domain in URI: {}", uri))?.to_string();
175
176 let path = if parts.len() > 1 { format!("/{}", parts[1]) } else { "/".to_string() };
177
178 dev_log!("lifecycle", "[Scheme] Parsed URI: {} -> domain={}, path={}", uri, domain, path);
179 Ok((domain, path))
180}
181
182fn forward_http_request(
194 url:&str,
195 request:&Request<Vec<u8>>,
196 method:Method,
197) -> Result<(u16, Vec<u8>, HashMap<String, String>), String> {
198 let parsed_url = url.parse::<http::uri::Uri>().map_err(|e| format!("Invalid URL: {}", e))?;
200
201 let host = parsed_url.host().ok_or("No host in URL")?.to_string();
203 let port = parsed_url.port_u16().unwrap_or(80);
204 let path = parsed_url
205 .path_and_query()
206 .map(|p| p.as_str().to_string())
207 .unwrap_or_else(|| "/".to_string());
208
209 let addr = format!("{}:{}", host, port);
210
211 dev_log!("lifecycle", "[Scheme] Connecting to {} at {}", url, addr);
212
213 let body = request.body().clone();
215 let headers:Vec<(String, String)> = request
216 .headers()
217 .iter()
218 .filter_map(|(name, value)| {
219 let header_name = name.as_str().to_lowercase();
220 let hop_by_hop_headers = [
221 "connection",
222 "keep-alive",
223 "proxy-authenticate",
224 "proxy-authorization",
225 "te",
226 "trailers",
227 "transfer-encoding",
228 "upgrade",
229 ];
230 if !hop_by_hop_headers.contains(&header_name.as_str()) {
231 value.to_str().ok().map(|v| (name.as_str().to_string(), v.to_string()))
232 } else {
233 None
234 }
235 })
236 .collect();
237
238 let result = std::thread::spawn(move || {
240 let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Failed to create runtime: {}", e))?;
241
242 rt.block_on(async {
243 use tokio::{
244 io::{AsyncReadExt, AsyncWriteExt},
245 net::TcpStream,
246 };
247
248 let mut stream = TcpStream::connect(&addr)
250 .await
251 .map_err(|e| format!("Failed to connect: {}", e))?;
252
253 let mut request_str = format!("{} {} HTTP/1.1\r\nHost: {}\r\n", method.as_str(), path, host);
255
256 for (name, value) in &headers {
258 request_str.push_str(&format!("{}: {}\r\n", name, value));
259 }
260
261 if !body.is_empty() {
263 request_str.push_str(&format!("Content-Length: {}\r\n", body.len()));
264 }
265
266 request_str.push_str("\r\n");
267
268 stream
270 .write_all(request_str.as_bytes())
271 .await
272 .map_err(|e| format!("Failed to write request: {}", e))?;
273
274 if !body.is_empty() {
275 stream
276 .write_all(&body)
277 .await
278 .map_err(|e| format!("Failed to write body: {}", e))?;
279 }
280
281 let mut buffer = Vec::new();
283 let mut temp_buf = [0u8; 8192];
284
285 loop {
286 let n = stream
287 .read(&mut temp_buf)
288 .await
289 .map_err(|e| format!("Failed to read response: {}", e))?;
290
291 if n == 0 {
292 break;
293 }
294
295 buffer.extend_from_slice(&temp_buf[..n]);
296
297 if buffer.len() > 1024 * 1024 {
300 dev_log!("lifecycle", "warn: [Scheme] Response too large, truncating");
302 break;
303 }
304
305 if let Some(headers_end) = buffer.windows(4).position(|w| w == b"\r\n\r\n") {
308 let headers = String::from_utf8_lossy(&buffer[..headers_end]);
309 if let Some(cl_line) = headers.lines().find(|l| l.to_lowercase().starts_with("content-length:")) {
310 if let Ok(cl) = cl_line.trim_start_matches("content-length:").trim().parse::<usize>() {
311 let body_expected = headers_end + 4 + cl;
312 if buffer.len() >= body_expected {
313 break;
314 }
315 }
316 } else if !headers.contains("Transfer-Encoding: chunked") {
317 continue;
319 }
320 }
321 }
322
323 let response_str = String::from_utf8_lossy(&buffer);
325 parse_http_response(&response_str)
326 })
327 })
328 .join()
329 .map_err(|e| format!("Thread panicked: {:?}", e))?;
330
331 result
332}
333
334fn parse_http_response(response:&str) -> Result<(u16, Vec<u8>, HashMap<String, String>), String> {
336 let headers_end = response
338 .find("\r\n\r\n")
339 .ok_or("Invalid HTTP response: no headers/body separator")?;
340
341 let headers_str = &response[..headers_end];
342 let body = response[headers_end + 4..].as_bytes().to_vec();
343
344 let mut lines = headers_str.lines();
346 let status_line = lines.next().ok_or("Invalid HTTP response: no status line")?;
347
348 let status = status_line
350 .split_whitespace()
351 .nth(1)
352 .and_then(|s| s.parse::<u16>().ok())
353 .ok_or_else(|| format!("Invalid status line: {}", status_line))?;
354
355 let mut headers = HashMap::new();
357 for line in lines {
358 if let Some((name, value)) = line.split_once(':') {
359 headers.insert(name.trim().to_lowercase(), value.trim().to_string());
360 }
361 }
362
363 Ok((status, body, headers))
364}
365
366pub fn land_scheme_handler(request:&Request<Vec<u8>>) -> Response<Vec<u8>> {
406 init_cache();
408
409 let uri = request.uri().to_string();
411 dev_log!("lifecycle", "[Scheme] Handling land:// request: {}", uri);
412
413 let (domain, path) = match parse_land_uri(&uri) {
415 Ok(result) => result,
416 Err(e) => {
417 dev_log!("lifecycle", "error: [Scheme] Failed to parse URI: {}", e);
418 return build_error_response(400, &format!("Bad Request: {}", e));
419 },
420 };
421
422 if request.method() == Method::OPTIONS {
424 dev_log!("lifecycle", "[Scheme] Handling CORS preflight request");
425 return build_cors_preflight_response();
426 }
427
428 if should_cache(&path) {
430 if let Some(cached) = get_cached(&path) {
431 dev_log!("lifecycle", "[Scheme] Cache hit for: {}", path);
432 return build_cached_response(cached);
433 }
434 }
435
436 let registry = match get_service_registry() {
438 Some(r) => r,
439 None => {
440 dev_log!("lifecycle", "error: [Scheme] Service registry not initialized");
441 return build_error_response(503, "Service Unavailable: Registry not initialized");
442 },
443 };
444
445 let service = match registry.lookup(&domain) {
446 Some(s) => s,
447 None => {
448 dev_log!("lifecycle", "warn: [Scheme] Service not found: {}", domain);
449 return build_error_response(404, &format!("Not Found: Service {} not registered", domain));
450 },
451 };
452
453 let local_url = format!("http://127.0.0.1:{}{}", service.port, path);
455
456 dev_log!(
457 "lifecycle",
458 "[Scheme] Routing {} {} to local service at {}",
459 request.method(),
460 uri,
461 local_url
462 );
463
464 let result = forward_http_request(&local_url, request, request.method().clone());
466
467 match result {
468 Ok((status, body, headers)) => {
469 let body_bytes = body.clone();
471
472 let mut response_builder = Builder::new()
474 .status(status)
475 .header("Access-Control-Allow-Origin", "land://code.editor.land")
476 .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
477 .header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
478
479 let important_headers = [
481 "content-type",
482 "content-length",
483 "etag",
484 "last-modified",
485 "cache-control",
486 "expires",
487 "content-encoding",
488 "content-disposition",
489 "location",
490 ];
491
492 for header_name in &important_headers {
493 if let Some(value) = headers.get(*header_name) {
494 response_builder = response_builder.header(*header_name, value);
495 }
496 }
497
498 let response = response_builder.body(body_bytes);
499
500 if status == 200 && should_cache(&path) {
502 let content_type = headers
503 .get("content-type")
504 .unwrap_or(&"application/octet-stream".to_string())
505 .clone();
506 let cache_control = headers
507 .get("cache-control")
508 .unwrap_or(&"public, max-age=3600".to_string())
509 .clone();
510 let etag = headers.get("etag").cloned();
511 let last_modified = headers.get("last-modified").cloned();
512
513 let entry = CacheEntry { body, content_type, cache_control, etag, last_modified };
514 set_cached(&path, entry);
515 dev_log!("lifecycle", "[Scheme] Cached response for: {}", path);
516 }
517
518 response.unwrap_or_else(|_| build_error_response(500, "Internal Server Error"))
519 },
520 Err(e) => {
521 dev_log!("lifecycle", "error: [Scheme] Failed to forward request: {}", e);
522 build_error_response(503, &format!("Service Unavailable: {}", e))
523 },
524 }
525}
526
527fn build_error_response(status:u16, message:&str) -> Response<Vec<u8>> {
529 let body = serde_json::json!({
530 "error": message,
531 "status": status
532 });
533
534 Builder::new()
535 .status(status)
536 .header("Content-Type", "application/json")
537 .header("Access-Control-Allow-Origin", "land://code.editor.land")
538 .body(serde_json::to_vec(&body).unwrap_or_default())
539 .unwrap_or_else(|_| Builder::new().status(500).body(Vec::new()).unwrap())
540}
541
542fn build_cors_preflight_response() -> Response<Vec<u8>> {
544 Builder::new()
545 .status(204)
546 .header("Access-Control-Allow-Origin", "land://code.editor.land")
547 .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
548 .header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
549 .header("Access-Control-Max-Age", "86400")
550 .body(Vec::new())
551 .unwrap()
552}
553
554fn build_cached_response(entry:CacheEntry) -> Response<Vec<u8>> {
556 let mut builder = Builder::new()
557 .status(200)
558 .header("Content-Type", &entry.content_type)
559 .header("Access-Control-Allow-Origin", "land://code.editor.land")
560 .header("Cache-Control", &entry.cache_control);
561
562 if let Some(etag) = &entry.etag {
563 builder = builder.header("ETag", etag);
564 }
565
566 if let Some(last_modified) = &entry.last_modified {
567 builder = builder.header("Last-Modified", last_modified);
568 }
569
570 builder
571 .body(entry.body)
572 .unwrap_or_else(|_| build_error_response(500, "Internal Server Error"))
573}
574
575pub fn register_land_service(name:&str, port:u16) {
584 let registry = get_service_registry().expect("Service registry not initialized. Call init_service_registry first.");
585 registry.register(name.to_string(), port, Some("/health".to_string()));
586 dev_log!("lifecycle", "[Scheme] Registered service: {} -> {}", name, port);
587}
588
589pub fn get_land_port(name:&str) -> Option<u16> {
600 let registry = get_service_registry()?;
601 registry.lookup(name).map(|s| s.port)
602}
603
604pub fn land_scheme_handler_async<R:tauri::Runtime>(
637 _ctx:tauri::UriSchemeContext<'_, R>,
638 request:tauri::http::request::Request<Vec<u8>>,
639 responder:tauri::UriSchemeResponder,
640) {
641 std::thread::spawn(move || {
643 let response = land_scheme_handler(&request);
644 responder.respond(response);
645 });
646}
647
648fn get_cors_origins() -> &'static str {
657 "land://localhost, http://land.localhost, land://code.editor.land"
659}
660
661#[inline]
666pub fn Scheme() {}
667
668fn MimeFromExtension(Path:&str) -> &'static str {
674 if Path.ends_with(".js") || Path.ends_with(".mjs") {
675 "application/javascript"
676 } else if Path.ends_with(".css") {
677 "text/css"
678 } else if Path.ends_with(".html") || Path.ends_with(".htm") {
679 "text/html"
680 } else if Path.ends_with(".json") {
681 "application/json"
682 } else if Path.ends_with(".svg") {
683 "image/svg+xml"
684 } else if Path.ends_with(".png") {
685 "image/png"
686 } else if Path.ends_with(".jpg") || Path.ends_with(".jpeg") {
687 "image/jpeg"
688 } else if Path.ends_with(".gif") {
689 "image/gif"
690 } else if Path.ends_with(".woff") {
691 "font/woff"
692 } else if Path.ends_with(".woff2") {
693 "font/woff2"
694 } else if Path.ends_with(".ttf") {
695 "font/ttf"
696 } else if Path.ends_with(".wasm") {
697 "application/wasm"
698 } else if Path.ends_with(".map") {
699 "application/json"
700 } else if Path.ends_with(".txt") || Path.ends_with(".md") {
701 "text/plain"
702 } else if Path.ends_with(".xml") {
703 "application/xml"
704 } else {
705 "application/octet-stream"
706 }
707}
708
709pub fn VscodeFileSchemeHandler<R:tauri::Runtime>(
738 AppHandle:&tauri::AppHandle<R>,
739 Request:&tauri::http::request::Request<Vec<u8>>,
740) -> Response<Vec<u8>> {
741 let Uri = Request.uri().to_string();
742 dev_log!("lifecycle", "[LandFix:VscodeFile] Request: {}", Uri);
743
744 let FilePath = Uri
747 .strip_prefix("vscode-file://vscode-app/")
748 .or_else(|| Uri.strip_prefix("vscode-file://vscode-app"))
749 .unwrap_or("");
750
751 let CleanPath = if FilePath.starts_with("Static/Application//out/") {
754 FilePath.replacen("Static/Application//out/", "Static/Application/", 1)
755 } else if FilePath.starts_with("Static/Application/out/") {
756 FilePath.replacen("Static/Application/out/", "Static/Application/", 1)
757 } else {
758 FilePath.to_string()
759 };
760
761 let CleanPath = if CleanPath.starts_with("Static/node_modules/") {
765 CleanPath.replacen("Static/node_modules/", "Static/Application/node_modules/", 1)
766 } else {
767 CleanPath
768 };
769
770 if CleanPath.ends_with(".map") {
778 return Builder::new()
779 .status(204)
780 .header("Access-Control-Allow-Origin", "*")
781 .body(Vec::new())
782 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
783 }
784
785 let IsAbsoluteOSPath = [
793 "Volumes/",
794 "Users/",
795 "Library/",
796 "System/",
797 "Applications/",
798 "private/",
799 "tmp/",
800 "var/",
801 "etc/",
802 "opt/",
803 "home/",
804 "usr/",
805 "srv/",
806 "mnt/",
807 "root/",
808 ]
809 .iter()
810 .any(|Prefix| CleanPath.starts_with(Prefix));
811
812 if IsAbsoluteOSPath {
813 let AbsolutePath = format!("/{}", CleanPath);
814 let FilesystemPath = std::path::Path::new(&AbsolutePath);
815 dev_log!(
816 "lifecycle",
817 "[LandFix:VscodeFile] os-abs candidate {} (exists={}, is_file={})",
818 AbsolutePath,
819 FilesystemPath.exists(),
820 FilesystemPath.is_file()
821 );
822 if FilesystemPath.exists() && FilesystemPath.is_file() {
823 match std::fs::read(FilesystemPath) {
824 Ok(Bytes) => {
825 let Mime = MimeFromExtension(&CleanPath);
826 dev_log!(
827 "lifecycle",
828 "[LandFix:VscodeFile] os-abs served {} ({}, {} bytes)",
829 AbsolutePath,
830 Mime,
831 Bytes.len()
832 );
833 return Builder::new()
834 .status(200)
835 .header("Content-Type", Mime)
836 .header("Access-Control-Allow-Origin", "*")
837 .header("Cache-Control", "public, max-age=3600")
838 .body(Bytes)
839 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
840 },
841 Err(Error) => {
842 dev_log!(
843 "lifecycle",
844 "warn: [LandFix:VscodeFile] os-abs read failure {}: {}",
845 AbsolutePath,
846 Error
847 );
848 },
849 }
850 } else {
851 dev_log!("lifecycle", "warn: [LandFix:VscodeFile] os-abs not on disk: {}", AbsolutePath);
852 }
853 }
854
855 dev_log!("lifecycle", "[LandFix:VscodeFile] Resolved path: {}", CleanPath);
856
857 let AssetResult = AppHandle.asset_resolver().get(CleanPath.clone());
861
862 if let Some(Asset) = AssetResult {
863 let Mime = MimeFromExtension(&CleanPath);
864
865 dev_log!(
866 "lifecycle",
867 "[LandFix:VscodeFile] Serving (embedded) {} ({}, {} bytes)",
868 CleanPath,
869 Mime,
870 Asset.bytes.len()
871 );
872
873 return Builder::new()
874 .status(200)
875 .header("Content-Type", Mime)
876 .header("Access-Control-Allow-Origin", "*")
877 .header("Cache-Control", "public, max-age=31536000, immutable")
878 .body(Asset.bytes.to_vec())
879 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
880 }
881
882 let StaticRoot = crate::IPC::WindServiceHandlers::get_static_application_root();
884
885 if let Some(Root) = StaticRoot {
886 let FilesystemPath = std::path::Path::new(&Root).join(&CleanPath);
887
888 if FilesystemPath.exists() && FilesystemPath.is_file() {
889 match std::fs::read(&FilesystemPath) {
890 Ok(Bytes) => {
891 let Mime = MimeFromExtension(&CleanPath);
892
893 dev_log!(
894 "lifecycle",
895 "[LandFix:VscodeFile] Serving (fs) {} ({}, {} bytes)",
896 CleanPath,
897 Mime,
898 Bytes.len()
899 );
900
901 return Builder::new()
902 .status(200)
903 .header("Content-Type", Mime)
904 .header("Access-Control-Allow-Origin", "*")
905 .header("Cache-Control", "public, max-age=3600")
906 .body(Bytes)
907 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
908 },
909 Err(Error) => {
910 dev_log!(
911 "lifecycle",
912 "warn: [LandFix:VscodeFile] Failed to read {}: {}",
913 FilesystemPath.display(),
914 Error
915 );
916 },
917 }
918 }
919 }
920
921 dev_log!(
922 "lifecycle",
923 "warn: [LandFix:VscodeFile] Not found: {} (resolved: {})",
924 Uri,
925 CleanPath
926 );
927 build_error_response(404, &format!("Not Found: {}", CleanPath))
928}