helix_core/
server.rs

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}