1use std::collections::HashMap;
2use std::fs;
3use std::net::{TcpListener, TcpStream};
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex};
7use std::thread;
8use std::time::{Duration, SystemTime};
9use anyhow::{Result, Context};
10use serde::{Deserialize, Serialize};
11use chrono;
12#[cfg(feature = "compiler")]
13use crate::compiler::{Compiler, OptimizationLevel};
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ServerConfig {
16 pub port: u16,
17 pub domain: String,
18 pub root_directory: PathBuf,
19 pub auto_convert: bool,
20 pub cache_timeout: u64,
21 pub max_file_size: u64,
22 pub allowed_extensions: Vec<String>,
23 pub cors_enabled: bool,
24 pub verbose: bool,
25}
26impl Default for ServerConfig {
27 fn default() -> Self {
28 Self {
29 port: 4592,
30 domain: "localhost".to_string(),
31 root_directory: PathBuf::from("."),
32 auto_convert: true,
33 cache_timeout: 300,
34 max_file_size: 10 * 1024 * 1024,
35 allowed_extensions: vec!["hlx".to_string(), "hlxb".to_string()],
36 cors_enabled: true,
37 verbose: false,
38 }
39 }
40}
41#[derive(Debug, Clone)]
42struct CacheEntry {
43 content: Vec<u8>,
44 content_type: String,
45 last_modified: SystemTime,
46 etag: String,
47}
48pub struct HelixServer {
49 config: ServerConfig,
50 cache: Arc<Mutex<HashMap<String, CacheEntry>>>,
51}
52impl HelixServer {
53 pub fn new(config: ServerConfig) -> Self {
54 Self {
55 config,
56 cache: Arc::new(Mutex::new(HashMap::new())),
57 }
58 }
59 pub fn start(&self) -> Result<()> {
60 let bind_address = format!("0.0.0.0:{}", self.config.port);
61 let display_address = format!("{}:{}", self.config.domain, self.config.port);
62 let listener = TcpListener::bind(&bind_address)
63 .context(format!("Failed to bind to address {}", bind_address))?;
64 println!("🚀 HELIX Configuration Server started!");
65 println!(" 🌐 Listening on: http://{}", display_address);
66 println!(" 📁 Root Directory: {}", self.config.root_directory.display());
67 println!(" 🔄 Auto-convert .hlx to .hlxb: {}", self.config.auto_convert);
68 println!(" 📋 Available endpoints:");
69 println!(" GET / - Server information and file listing");
70 println!(" GET /<filename> - Serve .hlx or .hlxb file");
71 println!(" GET /<domain>.<hlxb> - Domain-based file serving");
72 println!(" GET /health - Health check");
73 println!(" GET /config - Server configuration");
74 println!(" Press Ctrl+C to stop");
75 let cache_clone = Arc::clone(&self.cache);
76 let cache_timeout = self.config.cache_timeout;
77 thread::spawn(move || {
78 loop {
79 thread::sleep(Duration::from_secs(60));
80 Self::cleanup_cache(&cache_clone, cache_timeout);
81 }
82 });
83 for stream in listener.incoming() {
84 match stream {
85 Ok(stream) => {
86 let server = self.clone();
87 thread::spawn(move || {
88 if let Err(e) = server.handle_connection(stream) {
89 eprintln!("❌ Connection error: {}", e);
90 }
91 });
92 }
93 Err(e) => {
94 eprintln!("❌ Failed to accept connection: {}", e);
95 }
96 }
97 }
98 Ok(())
99 }
100 fn handle_connection(&self, mut stream: TcpStream) -> Result<()> {
101 let mut buffer = [0; 8192];
102 let size = stream.read(&mut buffer).context("Failed to read from stream")?;
103 let request = String::from_utf8_lossy(&buffer[..size]);
104 let response = self.handle_request(&request)?;
105 stream.write_all(response.as_bytes()).context("Failed to write response")?;
106 Ok(())
107 }
108 fn handle_request(&self, request: &str) -> Result<String> {
109 let lines: Vec<&str> = request.lines().collect();
110 if lines.is_empty() {
111 return Ok(self.create_error_response("400 Bad Request", "Empty request"));
112 }
113 let request_line = lines[0];
114 let parts: Vec<&str> = request_line.split_whitespace().collect();
115 if parts.len() < 2 {
116 return Ok(
117 self.create_error_response("400 Bad Request", "Invalid request line"),
118 );
119 }
120 let method = parts[0];
121 let path = parts[1];
122 match method {
123 "GET" => self.handle_get_request(path),
124 "OPTIONS" => Ok(self.create_options_response()),
125 _ => {
126 Ok(
127 self
128 .create_error_response(
129 "405 Method Not Allowed",
130 "Only GET and OPTIONS requests are supported",
131 ),
132 )
133 }
134 }
135 }
136 fn handle_get_request(&self, path: &str) -> Result<String> {
137 match path {
138 "/" => Ok(self.create_info_response()),
139 "/health" => Ok(self.create_health_response()),
140 "/config" => Ok(self.create_config_response()),
141 "/files" => Ok(self.list_available_files()),
142 "/files-html" => {
143 let file_list = self.list_available_files();
144 Ok(self.create_html_response(&file_list))
145 }
146 path => self.serve_file(path),
147 }
148 }
149 fn serve_file(&self, path: &str) -> Result<String> {
150 let filename = path.trim_start_matches('/');
151 if filename.ends_with(".hlxb") && filename.contains('.')
152 && !filename.contains('/')
153 {
154 let parts: Vec<&str> = filename.split('.').collect();
155 if parts.len() >= 2 && parts[parts.len() - 1] == "hlxb" {
156 return self.serve_domain_file(filename);
157 }
158 }
159 if let Some(cached) = self.get_cached_file(filename) {
160 return Ok(
161 self
162 .create_file_response(
163 &cached.content,
164 &cached.content_type,
165 &cached.etag,
166 true,
167 ),
168 );
169 }
170 let file_path = self.config.root_directory.join(filename);
171 if !file_path.exists() {
172 let file_path = self.find_file_with_extension(filename)?;
173 return self.serve_file_from_path(&file_path);
174 }
175 self.serve_file_from_path(&file_path)
176 }
177 fn serve_domain_file(&self, domain_filename: &str) -> Result<String> {
178 let parts: Vec<&str> = domain_filename.split('.').collect();
179 if parts.len() < 2 {
180 return Ok(
181 self
182 .create_error_response(
183 "400 Bad Request",
184 "Invalid domain file format",
185 ),
186 );
187 }
188 let domain = parts[0];
189 let extension = parts[parts.len() - 1];
190 if extension != "hlxb" {
191 return Ok(
192 self
193 .create_error_response(
194 "400 Bad Request",
195 "Domain files must have .hlxb extension",
196 ),
197 );
198 }
199 let filename = format!("{}.hlxb", domain);
200 let file_path = self.config.root_directory.join(&filename);
201 if file_path.exists() {
202 return self.serve_file_from_path(&file_path);
203 }
204 if self.config.auto_convert {
205 let hlx_filename = format!("{}.hlx", domain);
206 let hlx_path = self.config.root_directory.join(&hlx_filename);
207 if hlx_path.exists() {
208 return self.convert_and_serve_file(&hlx_path, &file_path);
209 }
210 }
211 Ok(
212 self
213 .create_error_response(
214 "404 Not Found",
215 &format!("Domain configuration '{}' not found", domain),
216 ),
217 )
218 }
219 fn find_file_with_extension(&self, base_filename: &str) -> Result<PathBuf> {
220 for ext in &self.config.allowed_extensions {
221 let filename = format!("{}.{}", base_filename, ext);
222 let file_path = self.config.root_directory.join(&filename);
223 if file_path.exists() {
224 return Ok(file_path);
225 }
226 }
227 Err(
228 anyhow::anyhow!(
229 "File '{}' not found with any allowed extension", base_filename
230 ),
231 )
232 }
233 fn serve_file_from_path(&self, file_path: &Path) -> Result<String> {
234 let metadata = fs::metadata(file_path)?;
235 if metadata.len() > self.config.max_file_size {
236 return Ok(
237 self
238 .create_error_response(
239 "413 Payload Too Large",
240 "File exceeds maximum size",
241 ),
242 );
243 }
244 let extension = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
245 let content_type = match extension {
246 "hlx" => "application/x-helix",
247 "hlxb" => "application/x-helix-binary",
248 _ => "application/octet-stream",
249 };
250 let content = fs::read(file_path)?;
251 let (final_content, final_content_type) = if extension == "hlx"
252 && self.config.auto_convert
253 {
254 println!("🔄 Converting HLX to HLXB: {}", file_path.display());
255 let hlxb_content = self.convert_hlx_to_hlxb(&content)?;
256 println!(
257 "✅ Conversion complete: {} bytes -> {} bytes", content.len(),
258 hlxb_content.len()
259 );
260 (hlxb_content, "application/x-helix-binary".to_string())
261 } else {
262 (content, content_type.to_string())
263 };
264 let etag = self.generate_etag(&final_content);
265 self.cache_file(
266 file_path.to_string_lossy().as_ref(),
267 &final_content,
268 &final_content_type,
269 etag.clone(),
270 );
271 Ok(self.create_file_response(&final_content, &final_content_type, &etag, false))
272 }
273 #[cfg(feature = "compiler")]
274 fn convert_hlx_to_hlxb(&self, hlx_content: &[u8]) -> Result<Vec<u8>> {
275 let hlx_source = String::from_utf8_lossy(hlx_content);
276 let compiler = Compiler::new(OptimizationLevel::Two);
277 let binary = compiler
278 .compile_source(&hlx_source, None)
279 .map_err(|e| anyhow::anyhow!("Compilation failed: {:?}", e))?;
280 let hlxb_data = bincode::serialize(&binary)
281 .map_err(|e| anyhow::anyhow!("Binary serialization failed: {:?}", e))?;
282 Ok(hlxb_data)
283 }
284 #[cfg(not(feature = "compiler"))]
285 fn convert_hlx_to_hlxb(&self, hlx_content: &[u8]) -> Result<Vec<u8>> {
286 const MAGIC_BYTES: [u8; 4] = *b"HLXB";
287 const BINARY_VERSION: u32 = 1;
288 let mut result = Vec::new();
289 result.extend_from_slice(&MAGIC_BYTES);
290 result.extend_from_slice(&BINARY_VERSION.to_le_bytes());
291 result.extend_from_slice(&(hlx_content.len() as u32).to_le_bytes());
292 result.extend_from_slice(&(0u32).to_le_bytes());
293 result.extend_from_slice(hlx_content);
294 Ok(result)
295 }
296 fn convert_and_serve_file(
297 &self,
298 hlx_path: &Path,
299 hlxb_path: &Path,
300 ) -> Result<String> {
301 let hlx_content = fs::read(hlx_path)?;
302 let hlxb_content = self.convert_hlx_to_hlxb(&hlx_content)?;
303 if let Err(e) = fs::write(hlxb_path, &hlxb_content) {
304 if self.config.verbose {
305 eprintln!("⚠️ Warning: Failed to save converted file: {}", e);
306 }
307 }
308 let etag = self.generate_etag(&hlxb_content);
309 self.cache_file(
310 hlxb_path.to_string_lossy().as_ref(),
311 &hlxb_content,
312 "application/x-helix-binary",
313 etag.clone(),
314 );
315 Ok(
316 self
317 .create_file_response(
318 &hlxb_content,
319 "application/x-helix-binary",
320 &etag,
321 false,
322 ),
323 )
324 }
325 fn generate_etag(&self, content: &[u8]) -> String {
326 use std::collections::hash_map::DefaultHasher;
327 use std::hash::{Hash, Hasher};
328 let mut hasher = DefaultHasher::new();
329 content.hash(&mut hasher);
330 format!("\"{:x}\"", hasher.finish())
331 }
332 fn cache_file(&self, key: &str, content: &[u8], content_type: &str, etag: String) {
333 let entry = CacheEntry {
334 content: content.to_vec(),
335 content_type: content_type.to_string(),
336 last_modified: SystemTime::now(),
337 etag,
338 };
339 if let Ok(mut cache) = self.cache.lock() {
340 cache.insert(key.to_string(), entry);
341 }
342 }
343 fn get_cached_file(&self, key: &str) -> Option<CacheEntry> {
344 if let Ok(cache) = self.cache.lock() { cache.get(key).cloned() } else { None }
345 }
346 fn cleanup_cache(cache: &Arc<Mutex<HashMap<String, CacheEntry>>>, timeout: u64) {
347 if let Ok(mut cache) = cache.lock() {
348 let now = SystemTime::now();
349 cache
350 .retain(|_, entry| {
351 now.duration_since(entry.last_modified)
352 .map(|duration| duration.as_secs() < timeout)
353 .unwrap_or(false)
354 });
355 }
356 }
357 fn create_info_response(&self) -> String {
358 let files = self.list_available_files_json();
359 let server_info = serde_json::json!(
360 { "server" : { "name" : "HELIX Configuration Server", "domain" : self.config
361 .domain, "port" : self.config.port, "root_directory" : self.config
362 .root_directory.display().to_string(), "auto_convert" : self.config
363 .auto_convert, "cache_timeout" : self.config.cache_timeout, "max_file_size" :
364 self.config.max_file_size, "cors_enabled" : self.config.cors_enabled },
365 "endpoints" : { "root" : "GET / - Server information (JSON)", "health" :
366 "GET /health - Health check (JSON)", "config" :
367 "GET /config - Server configuration (JSON)", "files" :
368 "GET /filename.hlx - Serve HLX file", "binary_files" :
369 "GET /filename.hlxb - Serve HLXB file", "domain_files" :
370 "GET /domain.hlxb - Domain-based configuration" }, "available_files" : files
371 }
372 );
373 let json = server_info.to_string();
374 let mut response = String::new();
375 response.push_str("HTTP/1.1 200 OK\r\n");
376 response.push_str("Content-Type: application/json\r\n");
377 response.push_str(&format!("Content-Length: {}\r\n", json.len()));
378 if self.config.cors_enabled {
379 response.push_str("Access-Control-Allow-Origin: *\r\n");
380 }
381 response.push_str("\r\n");
382 response.push_str(&json);
383 response
384 }
385 fn list_available_files_json(&self) -> Vec<serde_json::Value> {
386 let mut files = Vec::new();
387 if let Ok(entries) = fs::read_dir(&self.config.root_directory) {
388 for entry in entries.flatten() {
389 if let Some(filename) = entry.file_name().to_str() {
390 if self
391 .config
392 .allowed_extensions
393 .iter()
394 .any(|ext| filename.ends_with(ext))
395 {
396 let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
397 let extension = filename.rsplit('.').next().unwrap_or("");
398 let name_without_ext = filename
399 .trim_end_matches(&format!(".{}", extension));
400 files
401 .push(
402 serde_json::json!(
403 { "name" : filename, "name_without_extension" :
404 name_without_ext, "extension" : extension, "size" : size,
405 "url" : format!("/{}", filename) }
406 ),
407 );
408 }
409 }
410 }
411 }
412 files
413 }
414 fn list_available_files(&self) -> String {
415 let mut files = Vec::new();
416 if let Ok(entries) = fs::read_dir(&self.config.root_directory) {
417 for entry in entries.flatten() {
418 if let Some(filename) = entry.file_name().to_str() {
419 if self
420 .config
421 .allowed_extensions
422 .iter()
423 .any(|ext| filename.ends_with(ext))
424 {
425 let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
426 files
427 .push(
428 format!(
429 r#"<div class="file-item">📄 <a href="/{}">{}</a> ({} bytes)</div>"#,
430 filename, filename, size
431 ),
432 );
433 }
434 }
435 }
436 }
437 if files.is_empty() {
438 r#"<em>No .hlx or .hlxb files found</em>"#.to_string()
439 } else {
440 files.join("\n")
441 }
442 }
443 fn create_health_response(&self) -> String {
444 let health = serde_json::json!(
445 { "status" : "healthy", "service" : "helix-config-server", "domain" : self
446 .config.domain, "port" : self.config.port, "timestamp" : chrono::Utc::now()
447 .to_rfc3339(), "uptime" : "unknown" }
448 );
449 let json = health.to_string();
450 let mut response = String::new();
451 response.push_str("HTTP/1.1 200 OK\r\n");
452 response.push_str("Content-Type: application/json\r\n");
453 response.push_str(&format!("Content-Length: {}\r\n", json.len()));
454 response.push_str("\r\n");
455 response.push_str(&json);
456 response
457 }
458 fn create_config_response(&self) -> String {
459 let config = serde_json::to_string_pretty(&self.config).unwrap_or_default();
460 let mut response = String::new();
461 response.push_str("HTTP/1.1 200 OK\r\n");
462 response.push_str("Content-Type: application/json\r\n");
463 response.push_str(&format!("Content-Length: {}\r\n", config.len()));
464 response.push_str("\r\n");
465 response.push_str(&config);
466 response
467 }
468 fn create_file_response(
469 &self,
470 content: &[u8],
471 content_type: &str,
472 etag: &str,
473 cached: bool,
474 ) -> String {
475 let mut response = String::new();
476 response.push_str("HTTP/1.1 200 OK\r\n");
477 response.push_str(&format!("Content-Type: {}\r\n", content_type));
478 response.push_str(&format!("Content-Length: {}\r\n", content.len()));
479 response.push_str(&format!("ETag: {}\r\n", etag));
480 response.push_str("Cache-Control: public, max-age=300\r\n");
481 if self.config.cors_enabled {
482 response.push_str("Access-Control-Allow-Origin: *\r\n");
483 response.push_str("Access-Control-Allow-Methods: GET, OPTIONS\r\n");
484 response.push_str("Access-Control-Allow-Headers: *\r\n");
485 }
486 if cached {
487 response.push_str("X-Cache: HIT\r\n");
488 } else {
489 response.push_str("X-Cache: MISS\r\n");
490 }
491 response.push_str("\r\n");
492 response.push_str(&String::from_utf8_lossy(content));
493 response
494 }
495 fn create_options_response(&self) -> String {
496 let mut response = String::new();
497 response.push_str("HTTP/1.1 200 OK\r\n");
498 response.push_str("Access-Control-Allow-Origin: *\r\n");
499 response.push_str("Access-Control-Allow-Methods: GET, OPTIONS\r\n");
500 response.push_str("Access-Control-Allow-Headers: *\r\n");
501 response.push_str("Content-Length: 0\r\n");
502 response.push_str("\r\n");
503 response
504 }
505 fn create_html_response(&self, html: &str) -> String {
506 let mut response = String::new();
507 response.push_str("HTTP/1.1 200 OK\r\n");
508 response.push_str("Content-Type: text/html\r\n");
509 response.push_str(&format!("Content-Length: {}\r\n", html.len()));
510 if self.config.cors_enabled {
511 response.push_str("Access-Control-Allow-Origin: *\r\n");
512 }
513 response.push_str("\r\n");
514 response.push_str(html);
515 response
516 }
517 fn create_error_response(&self, status: &str, message: &str) -> String {
518 let html = format!(
519 r#"<!DOCTYPE html>
520<html>
521<head>
522 <title>Error - {}</title>
523 <style>
524 body {{ font-family: Arial, sans-serif; text-align: center; margin: 50px; }}
525 .error {{ color: #d32f2f; background: #ffebee; padding: 20px; border-radius: 8px; }}
526 </style>
527</head>
528<body>
529 <div class="error">
530 <h1>{}</h1>
531 <p>{}</p>
532 </div>
533</body>
534</html>"#,
535 status, status, message
536 );
537 let mut response = String::new();
538 response.push_str(&format!("HTTP/1.1 {}\r\n", status));
539 response.push_str("Content-Type: text/html\r\n");
540 response.push_str(&format!("Content-Length: {}\r\n", html.len()));
541 if self.config.cors_enabled {
542 response.push_str("Access-Control-Allow-Origin: *\r\n");
543 }
544 response.push_str("\r\n");
545 response.push_str(&html);
546 response
547 }
548}
549impl Clone for HelixServer {
550 fn clone(&self) -> Self {
551 Self {
552 config: self.config.clone(),
553 cache: Arc::clone(&self.cache),
554 }
555 }
556}
557pub fn start_server(config: ServerConfig) -> Result<()> {
558 let server = HelixServer::new(config);
559 server.start()
560}
561pub fn start_default_server() -> Result<()> {
562 let config = ServerConfig::default();
563 start_server(config)
564}
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use std::fs::File;
569 use std::io::Write;
570 use tempfile::TempDir;
571 #[test]
572 fn test_server_config_default() {
573 let config = ServerConfig::default();
574 assert_eq!(config.port, 4592);
575 assert_eq!(config.domain, "localhost");
576 assert!(config.auto_convert);
577 }
578 #[test]
579 fn test_convert_hlx_to_hlxb() {
580 let server = HelixServer::new(ServerConfig::default());
581 let hlx_content = b"agent 'test' { model = 'gpt-4' }";
582 let hlxb_content = server.convert_hlx_to_hlxb(hlx_content).unwrap();
583 assert_eq!(& hlxb_content[0..4], b"HLXB");
584 #[cfg(feature = "compiler")]
585 {
586 assert!(hlxb_content.len() > 4);
587 }
588 #[cfg(not(feature = "compiler"))]
589 {
590 let size = u32::from_le_bytes([
591 hlxb_content[8],
592 hlxb_content[9],
593 hlxb_content[10],
594 hlxb_content[11],
595 ]);
596 assert_eq!(size as usize, hlx_content.len());
597 assert_eq!(& hlxb_content[16..], hlx_content);
598 }
599 }
600 #[test]
601 fn test_generate_etag() {
602 let server = HelixServer::new(ServerConfig::default());
603 let content = b"test content";
604 let etag1 = server.generate_etag(content);
605 let etag2 = server.generate_etag(content);
606 assert_eq!(etag1, etag2);
607 let etag3 = server.generate_etag(b"different content");
608 assert_ne!(etag1, etag3);
609 }
610}