Skip to main content

Mountain/Binary/Build/
ServiceRegistry.rs

1//! # Service Registry Module
2//!
3//! ## RESPONSIBILITIES
4//!
5//! - Track mapping from land:// domain names to local HTTP service ports
6//! - Provide thread-safe access using Arc<RwLock<>>
7//! - Support service registration and lookup
8//! - Enable health checks for registered services
9//!
10//! ## ARCHITECTURAL ROLE
11//!
12//! The ServiceRegistry provides the bridge between land:// URIs and local
13//! services:
14//!
15//! ```text
16//! land://code.editor.land/path ──► ServiceRegistry ──► http://127.0.0.1:PORT/path
17//! ```
18//!
19//! ## THREAD SAFETY
20//!
21//! - Uses Arc<RwLock<HashMap<String, LocalService>>> for concurrent access
22//! - Multiple readers allowed concurrently
23//! - Writers lock exclusively
24//!
25//! ## USAGE
26//!
27//! ```rust
28//! let registry = ServiceRegistry::new();
29//! registry.register("code.editor.land".to_string(), 8080, Some("/health".to_string()));
30//!
31//! let service = registry.lookup("code.editor.land").unwrap();
32//! assert_eq!(service.port, 8080);
33//! ```
34
35use std::{
36	collections::HashMap,
37	sync::{Arc, RwLock},
38};
39
40#[allow(unused_imports)]
41use http::{Request as HttpRequest, Response as HttpResponse, header};
42use tokio::{
43	io::{AsyncReadExt, AsyncWriteExt},
44	net::TcpStream,
45};
46
47use crate::dev_log;
48
49/// Represents a local HTTP/HTTPS service registered with the land:// scheme
50///
51/// # Fields
52///
53/// - `name`: Domain name (e.g., "code.editor.land")
54/// - `port`: Local port where the service is listening
55/// - `tls_port`: Optional TLS port for HTTPS (defaults to port + 1000 if not
56///   specified)
57/// - `use_tls`: Whether the service uses HTTPS
58/// - `health_check_path`: Optional path for health check endpoint
59#[derive(Debug, Clone)]
60pub struct LocalService {
61	pub name:String,
62	pub port:u16,
63	pub tls_port:Option<u16>,
64	pub use_tls:bool,
65	pub health_check_path:Option<String>,
66}
67
68impl LocalService {
69	/// Get the appropriate port based on TLS configuration
70	pub fn get_port(&self) -> u16 {
71		if self.use_tls {
72			self.tls_port.unwrap_or_else(|| self.port + 1000)
73		} else {
74			self.port
75		}
76	}
77}
78
79/// Registry for tracking local HTTP/HTTPS services
80///
81/// Provides thread-safe methods to register and lookup services by domain name.
82/// Supports both HTTP and HTTPS protocols with automatic TLS certificate
83/// provisioning.
84#[derive(Clone)]
85pub struct ServiceRegistry {
86	/// Inner storage using `Arc<RwLock>` for thread-safe concurrent access
87	services:Arc<RwLock<HashMap<String, LocalService>>>,
88	/// Optional certificate manager for HTTPS support
89	cert_manager:Option<std::sync::Arc<std::sync::Mutex<super::CertificateManager::CertificateManager>>>,
90}
91
92impl ServiceRegistry {
93	/// Create a new ServiceRegistry instance
94	///
95	/// Returns an empty registry ready to accept service registrations.
96	pub fn new() -> Self {
97		dev_log!("lifecycle", "[ServiceRegistry] Creating new ServiceRegistry");
98		Self { services:Arc::new(RwLock::new(HashMap::new())), cert_manager:None }
99	}
100
101	/// Create a new ServiceRegistry instance with TLS support
102	///
103	/// # Parameters
104	///
105	/// * `cert_manager` - Certificate manager for provisioning TLS certificates
106	///
107	/// Returns a registry ready to accept both HTTP and HTTPS service
108	/// registrations.
109	pub fn with_tls(
110		cert_manager:std::sync::Arc<std::sync::Mutex<super::CertificateManager::CertificateManager>>,
111	) -> Self {
112		dev_log!("lifecycle", "[ServiceRegistry] Creating new ServiceRegistry with TLS support");
113		Self { services:Arc::new(RwLock::new(HashMap::new())), cert_manager:Some(cert_manager) }
114	}
115
116	/// Register a local HTTP service
117	///
118	/// # Parameters
119	///
120	/// - `name`: Domain name (e.g., "code.editor.land")
121	/// - `port`: Local port where the service is listening
122	/// - `health_check_path`: Optional path for health check endpoint (e.g.,
123	///   "/health")
124	///
125	/// # Example
126	///
127	/// ```rust
128	/// registry.register("code.editor.land".to_string(), 8080, Some("/health".to_string())); 
129	/// ```
130	pub fn register(&self, name:String, port:u16, health_check_path:Option<String>) {
131		self.register_with_options(name, port, None, false, health_check_path);
132	}
133
134	/// Register a local service with TLS options
135	///
136	/// # Parameters
137	///
138	/// - `name`: Domain name (e.g., "code.editor.land")
139	/// - `port`: Local HTTP port
140	/// - `tls_port`: Optional TLS port (defaults to port + 1000)
141	/// - `use_tls`: Whether to enable HTTPS
142	/// - `health_check_path`: Optional path for health check endpoint
143	///
144	/// # Example
145	///
146	/// ```rust
147	/// // Register with TLS enabled
148	/// registry.register_with_options(
149	/// 	"code.editor.land".to_string(),
150	/// 	8080,
151	/// 	None, // Use default TLS port (9080)
152	/// 	true,
153	/// 	Some("/health".to_string()),
154	/// );
155	/// ```
156	pub fn register_with_options(
157		&self,
158		name:String,
159		port:u16,
160		tls_port:Option<u16>,
161		use_tls:bool,
162		health_check_path:Option<String>,
163	) {
164		dev_log!(
165			"lifecycle",
166			"[ServiceRegistry] Registering service: {} -> HTTP:{}, TLS:{}, use_tls:{}",
167			name,
168			port,
169			tls_port.unwrap_or(port + 1000),
170			use_tls
171		);
172
173		let service = LocalService { name:name.clone(), port, tls_port, use_tls, health_check_path };
174
175		// Pre-provision TLS certificate if needed
176		if use_tls {
177			if let Some(cert_manager) = &self.cert_manager {
178				// NOTE: TLS certificate is generated on-demand when needed
179				dev_log!("lifecycle", "[ServiceRegistry] TLS will be provisioned on-demand for {}", name);
180			} else {
181				dev_log!(
182					"lifecycle",
183					"warn: [ServiceRegistry] Service {} requested TLS but no certificate manager available",
184					name
185				);
186			}
187		}
188
189		if let Ok(mut services) = self.services.write() {
190			// Check if service already exists
191			if services.contains_key(&name) {
192				dev_log!(
193					"lifecycle",
194					"warn: [ServiceRegistry] Service {} already registered, overwriting",
195					name
196				);
197			}
198			services.insert(name.clone(), service);
199			dev_log!("lifecycle", "[ServiceRegistry] Service {} registered successfully", name);
200		} else {
201			dev_log!(
202				"lifecycle",
203				"error: [ServiceRegistry] Failed to acquire write lock for registration"
204			);
205		}
206	}
207
208	/// Look up a service by domain name
209	///
210	/// # Parameters
211	///
212	/// - `name`: Domain name to look up
213	///
214	/// # Returns
215	///
216	/// - `Some(LocalService)` if found
217	/// - `None` if not registered
218	///
219	/// # Example
220	///
221	/// ```rust
222	/// let service = registry.lookup("code.editor.land");
223	/// if let Some(svc) = service {
224	/// 	println!("Service running on port {}", svc.port);
225	/// }
226	/// ```
227	pub fn lookup(&self, name:&str) -> Option<LocalService> {
228		dev_log!("lifecycle", "[ServiceRegistry] Looking up service: {}", name);
229
230		if let Ok(services) = self.services.read() {
231			let service = services.get(name).cloned();
232			if service.is_some() {
233				dev_log!("lifecycle", "[ServiceRegistry] Service {} found", name);
234			} else {
235				dev_log!("lifecycle", "[ServiceRegistry] Service {} not found", name);
236			}
237			service
238		} else {
239			dev_log!("lifecycle", "error: [ServiceRegistry] Failed to acquire read lock for lookup");
240			None
241		}
242	}
243
244	/// Get all registered services
245	///
246	/// # Returns
247	///
248	/// A vector of all registered LocalService instances
249	pub fn all_services(&self) -> Vec<LocalService> {
250		if let Ok(services) = self.services.read() {
251			services.values().cloned().collect()
252		} else {
253			dev_log!(
254				"lifecycle",
255				"error: [ServiceRegistry] Failed to acquire read lock for all_services"
256			);
257			Vec::new()
258		}
259	}
260
261	/// Perform a health check on a registered service
262	///
263	/// # Parameters
264	///
265	/// - `name`: Domain name of the service to check
266	///
267	/// # Returns
268	///
269	/// - `Ok(true)` if service is healthy and responding
270	/// - `Ok(false)` if service is not healthy
271	/// - `Err` if service not found or health check fails
272	pub async fn health_check(&self, name:&str) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
273		let service = self.lookup(name).ok_or_else(|| format!("Service {} not found", name))?;
274
275		let health_path = service.health_check_path.as_deref().unwrap_or("/health");
276		let addr = format!("127.0.0.1:{}", service.port);
277
278		dev_log!(
279			"lifecycle",
280			"[ServiceRegistry] Performing health check for {} at {}:{}",
281			name,
282			addr,
283			health_path
284		);
285
286		// Try to connect to the service
287		match TcpStream::connect(&addr).await {
288			Ok(mut stream) => {
289				// Send simple HTTP GET request
290				let request = format!("GET {} HTTP/1.1\r\nHost: 127.0.0.1:{}\r\n\r\n", health_path, service.port);
291
292				match stream.write_all(request.as_bytes()).await {
293					Ok(_) => {
294						// Try to read response
295						let mut buffer = [0u8; 1024];
296						match stream.read(&mut buffer).await {
297							Ok(n) => {
298								let response = String::from_utf8_lossy(&buffer[..n]);
299								let is_healthy = response.contains("HTTP/1.1 200") || response.contains("HTTP/1.0 200");
300								if is_healthy {
301									dev_log!("lifecycle", "[ServiceRegistry] Service {} is healthy", name);
302								} else {
303									dev_log!(
304										"lifecycle",
305										"warn: [ServiceRegistry] Service {} health check failed: not 200",
306										name
307									);
308								}
309								Ok(is_healthy)
310							},
311							Err(e) => {
312								dev_log!(
313									"lifecycle",
314									"warn: [ServiceRegistry] Service {} health check failed to read: {}",
315									name,
316									e
317								);
318								Ok(false)
319							},
320						}
321					},
322					Err(e) => {
323						dev_log!(
324							"lifecycle",
325							"warn: [ServiceRegistry] Service {} health check failed to write: {}",
326							name,
327							e
328						);
329						Ok(false)
330					},
331				}
332			},
333			Err(e) => {
334				dev_log!(
335					"lifecycle",
336					"warn: [ServiceRegistry] Service {} health check failed to connect: {}",
337					name,
338					e
339				);
340				Ok(false)
341			},
342		}
343	}
344
345	/// Remove a service from the registry
346	///
347	/// # Parameters
348	///
349	/// - `name`: Domain name of the service to remove
350	///
351	/// # Returns
352	///
353	/// - `Some(LocalService)` if service was removed
354	/// - `None` if service was not found
355	pub fn unregister(&self, name:&str) -> Option<LocalService> {
356		dev_log!("lifecycle", "[ServiceRegistry] Unregistering service: {}", name);
357
358		if let Ok(mut services) = self.services.write() {
359			services.remove(name)
360		} else {
361			dev_log!(
362				"lifecycle",
363				"error: [ServiceRegistry] Failed to acquire write lock for unregistration"
364			);
365			None
366		}
367	}
368
369	/// Get TLS configuration for a service (if available)
370	///
371	/// # Parameters
372	///
373	/// - `name`: Domain name of the service
374	///
375	/// # Returns
376	///
377	/// - `Some(Arc<ServerConfig>)` if service uses TLS and certificate manager
378	///   is available
379	/// - `None` if service doesn't use TLS or certificate manager is not
380	///   configured
381	pub async fn get_tls_config(&self, name:&str) -> Option<std::sync::Arc<rustls::ServerConfig>> {
382		let service = self.lookup(name)?;
383
384		if !service.use_tls {
385			return None;
386		}
387
388		let cert_manager = self.cert_manager.as_ref()?;
389		let manager = cert_manager
390			.lock()
391			.map_err(|e| {
392				dev_log!("lifecycle", "error: [ServiceRegistry] Failed to acquire lock: {}", e);
393			})
394			.ok()?;
395		manager.build_server_config(name).await.ok()
396	}
397
398	/// Check if a service uses TLS
399	///
400	/// # Parameters
401	///
402	/// - `name`: Domain name of the service
403	///
404	/// # Returns
405	///
406	/// `true` if the service is configured to use TLS, `false` otherwise
407	pub fn uses_tls(&self, name:&str) -> bool { self.lookup(name).map(|s| s.use_tls).unwrap_or(false) }
408}
409
410impl Default for ServiceRegistry {
411	fn default() -> Self { Self::new() }
412}
413
414#[cfg(test)]
415mod tests {
416	use super::*;
417
418	#[test]
419	fn test_register_and_lookup() {
420		let registry = ServiceRegistry::new();
421
422		registry.register("test.service.land".to_string(), 8080, Some("/health".to_string()));
423
424		let service = registry.lookup("test.service.land").unwrap();
425		assert_eq!(service.name, "test.service.land");
426		assert_eq!(service.port, 8080);
427		assert_eq!(service.health_check_path, Some("/health".to_string()));
428	}
429
430	#[test]
431	fn test_lookup_nonexistent() {
432		let registry = ServiceRegistry::new();
433
434		let service = registry.lookup("nonexistent.service.land");
435		assert!(service.is_none());
436	}
437
438	#[test]
439	fn test_all_services() {
440		let registry = ServiceRegistry::new();
441
442		registry.register("service1.land".to_string(), 8080, None);
443		registry.register("service2.land".to_string(), 8081, None);
444
445		let services = registry.all_services();
446		assert_eq!(services.len(), 2);
447	}
448
449	#[test]
450	fn test_unregister() {
451		let registry = ServiceRegistry::new();
452
453		registry.register("test.service.land".to_string(), 8080, None);
454		assert!(registry.lookup("test.service.land").is_some());
455
456		registry.unregister("test.service.land");
457		assert!(registry.lookup("test.service.land").is_none());
458	}
459
460	#[test]
461	fn test_overwrite_registration() {
462		let registry = ServiceRegistry::new();
463
464		registry.register("test.service.land".to_string(), 8080, None);
465		registry.register("test.service.land".to_string(), 9090, None);
466
467		let service = registry.lookup("test.service.land").unwrap();
468		assert_eq!(service.port, 9090);
469	}
470
471	#[test]
472	fn test_tls_service() {
473		let registry = ServiceRegistry::new();
474
475		registry.register_with_options(
476			"secure.service.land".to_string(),
477			8080,
478			Some(8443),
479			true,
480			Some("/health".to_string()),
481		);
482
483		let service = registry.lookup("secure.service.land").unwrap();
484		assert_eq!(service.name, "secure.service.land");
485		assert_eq!(service.port, 8080);
486		assert_eq!(service.tls_port, Some(8443));
487		assert_eq!(service.use_tls, true);
488		assert_eq!(service.get_port(), 8443);
489	}
490
491	#[test]
492	fn test_default_tls_port() {
493		let registry = ServiceRegistry::new();
494
495		registry.register_with_options(
496			"secure.service.land".to_string(),
497			8080,
498			None, // Use default TLS port (8080 + 1000 = 9080)
499			true,
500			None,
501		);
502
503		let service = registry.lookup("secure.service.land").unwrap();
504		assert_eq!(service.tls_port, None); // Explicitly None
505		assert_eq!(service.get_port(), 9080); // But get_port() returns default
506	}
507}