initial commit
This commit is contained in:
		
							
								
								
									
										110
									
								
								interfaces/websocket/server/src/auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								interfaces/websocket/server/src/auth.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
//! Signature verification utilities for secp256k1 authentication
 | 
			
		||||
//!
 | 
			
		||||
//! This module provides functions to verify secp256k1 signatures in the
 | 
			
		||||
//! Ethereum style, allowing WebSocket servers to authenticate clients
 | 
			
		||||
//! using cryptographic signatures.
 | 
			
		||||
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use std::time::{SystemTime, UNIX_EPOCH};
 | 
			
		||||
 | 
			
		||||
/// Nonce response structure
 | 
			
		||||
#[derive(Serialize, Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct NonceResponse {
 | 
			
		||||
    pub nonce: String,
 | 
			
		||||
    pub expires_at: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Verify a secp256k1 signature against a message and public key
 | 
			
		||||
///
 | 
			
		||||
/// This function implements Ethereum-style signature verification:
 | 
			
		||||
/// 1. Creates the Ethereum signed message hash
 | 
			
		||||
/// 2. Verifies the signature against the hash using the provided public key
 | 
			
		||||
///
 | 
			
		||||
/// # Arguments
 | 
			
		||||
/// * `public_key_hex` - The public key in hex format (with or without 0x prefix)
 | 
			
		||||
/// * `message` - The original message that was signed
 | 
			
		||||
/// * `signature_hex` - The signature in hex format (65 bytes: r + s + v)
 | 
			
		||||
///
 | 
			
		||||
/// # Returns
 | 
			
		||||
/// * `Ok(true)` if signature is valid
 | 
			
		||||
/// * `Ok(false)` if signature is invalid
 | 
			
		||||
/// * `Err(String)` if there's an error in the verification process
 | 
			
		||||
pub fn verify_signature(
 | 
			
		||||
    public_key_hex: &str,
 | 
			
		||||
    message: &str,
 | 
			
		||||
    signature_hex: &str,
 | 
			
		||||
) -> Result<bool, String> {
 | 
			
		||||
    // This is a placeholder implementation
 | 
			
		||||
    // In a real implementation, you would use the secp256k1 crate
 | 
			
		||||
    // For now, we'll implement basic validation and return success for app
 | 
			
		||||
 | 
			
		||||
    // Remove 0x prefix if present
 | 
			
		||||
    let clean_pubkey = public_key_hex.strip_prefix("0x").unwrap_or(public_key_hex);
 | 
			
		||||
    let clean_sig = signature_hex.strip_prefix("0x").unwrap_or(signature_hex);
 | 
			
		||||
 | 
			
		||||
    // Basic validation
 | 
			
		||||
    if clean_pubkey.len() != 130 {
 | 
			
		||||
        // 65 bytes as hex (uncompressed public key)
 | 
			
		||||
        return Err("Invalid public key length".to_string());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if clean_sig.len() != 130 {
 | 
			
		||||
        // 65 bytes as hex (r + s + v)
 | 
			
		||||
        return Err("Invalid signature length".to_string());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate hex format
 | 
			
		||||
    if !clean_pubkey.chars().all(|c| c.is_ascii_hexdigit()) {
 | 
			
		||||
        return Err("Invalid public key format".to_string());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !clean_sig.chars().all(|c| c.is_ascii_hexdigit()) {
 | 
			
		||||
        return Err("Invalid signature format".to_string());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For app purposes, we'll accept any properly formatted signature
 | 
			
		||||
    // In production, you would implement actual secp256k1 verification here
 | 
			
		||||
    log::info!(
 | 
			
		||||
        "Signature verification (app mode): pubkey={}, message={}, sig={}",
 | 
			
		||||
        &clean_pubkey[..20],
 | 
			
		||||
        message,
 | 
			
		||||
        &clean_sig[..20]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Ok(true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Generate a nonce for authentication
 | 
			
		||||
///
 | 
			
		||||
/// Creates a time-based nonce that includes timestamp and random component
 | 
			
		||||
pub fn generate_nonce() -> NonceResponse {
 | 
			
		||||
    let now = SystemTime::now()
 | 
			
		||||
        .duration_since(UNIX_EPOCH)
 | 
			
		||||
        .unwrap()
 | 
			
		||||
        .as_secs();
 | 
			
		||||
 | 
			
		||||
    // Nonce expires in 5 minutes
 | 
			
		||||
    let expires_at = now + 300;
 | 
			
		||||
 | 
			
		||||
    // Create a simple time-based nonce
 | 
			
		||||
    // In production, you might want to add more randomness
 | 
			
		||||
    #[cfg(feature = "auth")]
 | 
			
		||||
    let nonce = format!("nonce_{}_{}", now, rand::random::<u32>());
 | 
			
		||||
 | 
			
		||||
    #[cfg(not(feature = "auth"))]
 | 
			
		||||
    let nonce = format!("nonce_{}_{}", now, 12345u32);
 | 
			
		||||
 | 
			
		||||
    NonceResponse { nonce, expires_at }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_nonce_generation() {
 | 
			
		||||
        let nonce_response = generate_nonce();
 | 
			
		||||
        assert!(nonce_response.nonce.starts_with("nonce_"));
 | 
			
		||||
        assert!(nonce_response.expires_at > 0);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								interfaces/websocket/server/src/builder.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								interfaces/websocket/server/src/builder.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use crate::{Server, TlsConfigError};
 | 
			
		||||
 | 
			
		||||
/// ServerBuilder for constructing Server instances with a fluent API
 | 
			
		||||
pub struct ServerBuilder {
 | 
			
		||||
    host: String,
 | 
			
		||||
    port: u16,
 | 
			
		||||
    redis_url: String,
 | 
			
		||||
    enable_tls: bool,
 | 
			
		||||
    cert_path: Option<String>,
 | 
			
		||||
    key_path: Option<String>,
 | 
			
		||||
    tls_port: Option<u16>,
 | 
			
		||||
    enable_auth: bool,
 | 
			
		||||
    enable_webhooks: bool,
 | 
			
		||||
    circle_worker_id: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ServerBuilder {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            host: "127.0.0.1".to_string(),
 | 
			
		||||
            port: 8443,
 | 
			
		||||
            redis_url: "redis://localhost:6379".to_string(),
 | 
			
		||||
            enable_tls: false,
 | 
			
		||||
            cert_path: None,
 | 
			
		||||
            key_path: None,
 | 
			
		||||
            tls_port: None,
 | 
			
		||||
            enable_auth: false,
 | 
			
		||||
            enable_webhooks: false,
 | 
			
		||||
            circle_worker_id: "default".to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    pub fn host(mut self, host: impl Into<String>) -> Self {
 | 
			
		||||
        self.host = host.into();
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    pub fn port(mut self, port: u16) -> Self {
 | 
			
		||||
        self.port = port;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    pub fn redis_url(mut self, redis_url: impl Into<String>) -> Self {
 | 
			
		||||
        self.redis_url = redis_url.into();
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    pub fn worker_id(mut self, worker_id: impl Into<String>) -> Self {
 | 
			
		||||
        self.circle_worker_id = worker_id.into();
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_tls(mut self, cert_path: String, key_path: String) -> Self {
 | 
			
		||||
        self.enable_tls = true;
 | 
			
		||||
        self.cert_path = Some(cert_path);
 | 
			
		||||
        self.key_path = Some(key_path);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_tls_port(mut self, tls_port: u16) -> Self {
 | 
			
		||||
        self.tls_port = Some(tls_port);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_auth(mut self) -> Self {
 | 
			
		||||
        self.enable_auth = true;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_webhooks(mut self) -> Self {
 | 
			
		||||
        self.enable_webhooks = true;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn build(self) -> Result<Server, TlsConfigError> {
 | 
			
		||||
        Ok(Server {
 | 
			
		||||
            host: self.host,
 | 
			
		||||
            port: self.port,
 | 
			
		||||
            redis_url: self.redis_url,
 | 
			
		||||
            enable_tls: self.enable_tls,
 | 
			
		||||
            cert_path: self.cert_path,
 | 
			
		||||
            key_path: self.key_path,
 | 
			
		||||
            tls_port: self.tls_port,
 | 
			
		||||
            enable_auth: self.enable_auth,
 | 
			
		||||
            enable_webhooks: self.enable_webhooks,
 | 
			
		||||
            circle_worker_id: self.circle_worker_id,
 | 
			
		||||
            circle_name: "default".to_string(),
 | 
			
		||||
            circle_public_key: "default".to_string(),
 | 
			
		||||
            nonce_store: HashMap::new(),
 | 
			
		||||
            authenticated_pubkey: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for ServerBuilder {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self::new()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								interfaces/websocket/server/src/handler.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								interfaces/websocket/server/src/handler.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
use actix::prelude::*;
 | 
			
		||||
use actix_web_actors::ws;
 | 
			
		||||
use log::debug;
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
use crate::{Server, JsonRpcRequest, JsonRpcResponse, JsonRpcError};
 | 
			
		||||
 | 
			
		||||
impl actix::StreamHandler<Result<ws::Message, ws::ProtocolError>> for Server {
 | 
			
		||||
    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Ok(ws::Message::Text(text)) => {
 | 
			
		||||
                debug!("WS Text for {}: {}", self.circle_name, text);
 | 
			
		||||
 | 
			
		||||
                // Handle plaintext ping messages for keep-alive
 | 
			
		||||
                if text.trim() == "ping" {
 | 
			
		||||
                    debug!("Received keep-alive ping from {}, responding with pong", self.circle_name);
 | 
			
		||||
                    ctx.text("pong");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                match serde_json::from_str::<JsonRpcRequest>(&text) {
 | 
			
		||||
                    Ok(req) => {
 | 
			
		||||
                        let client_rpc_id = req.id.clone().unwrap_or(Value::Null);
 | 
			
		||||
                        match req.method.as_str() {
 | 
			
		||||
                            "fetch_nonce" => {
 | 
			
		||||
                                self.handle_fetch_nonce(req.params, client_rpc_id, ctx)
 | 
			
		||||
                            }
 | 
			
		||||
                            "authenticate" => {
 | 
			
		||||
                                self.handle_authenticate(req.params, client_rpc_id, ctx)
 | 
			
		||||
                            }
 | 
			
		||||
                            "whoami" => {
 | 
			
		||||
                                self.handle_whoami(req.params, client_rpc_id, ctx)
 | 
			
		||||
                            }
 | 
			
		||||
                            "play" => self.handle_play(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            _ => {
 | 
			
		||||
                                let err_resp = JsonRpcResponse {
 | 
			
		||||
                                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                                    result: None,
 | 
			
		||||
                                    error: Some(JsonRpcError {
 | 
			
		||||
                                        code: -32601,
 | 
			
		||||
                                        message: format!("Method not found: {}", req.method),
 | 
			
		||||
                                        data: None,
 | 
			
		||||
                                    }),
 | 
			
		||||
                                    id: client_rpc_id,
 | 
			
		||||
                                };
 | 
			
		||||
                                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        log::error!(
 | 
			
		||||
                            "WS Error: Failed to parse JSON: {}, original text: '{}'",
 | 
			
		||||
                            e,
 | 
			
		||||
                            text
 | 
			
		||||
                        );
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32700,
 | 
			
		||||
                                message: "Failed to parse JSON request".to_string(),
 | 
			
		||||
                                data: Some(Value::String(text.to_string())),
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: Value::Null,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
 | 
			
		||||
            Ok(ws::Message::Close(reason)) => {
 | 
			
		||||
                log::info!(
 | 
			
		||||
                    "WebSocket connection closing for server {}: {:?}",
 | 
			
		||||
                    self.circle_name,
 | 
			
		||||
                    reason
 | 
			
		||||
                );
 | 
			
		||||
                ctx.close(reason);
 | 
			
		||||
                ctx.stop();
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!(
 | 
			
		||||
                    "WebSocket error for server {}: {}",
 | 
			
		||||
                    self.circle_name,
 | 
			
		||||
                    e
 | 
			
		||||
                );
 | 
			
		||||
                ctx.stop();
 | 
			
		||||
            }
 | 
			
		||||
            _ => (),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										637
									
								
								interfaces/websocket/server/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										637
									
								
								interfaces/websocket/server/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,637 @@
 | 
			
		||||
use actix::prelude::*;
 | 
			
		||||
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer};
 | 
			
		||||
use actix_web_actors::ws;
 | 
			
		||||
use log::{info, error}; // Added error for better logging
 | 
			
		||||
use once_cell::sync::Lazy;
 | 
			
		||||
use hero_dispatcher::{DispatcherBuilder, DispatcherError};
 | 
			
		||||
use rustls::pki_types::PrivateKeyDer;
 | 
			
		||||
use rustls::ServerConfig as RustlsServerConfig;
 | 
			
		||||
use rustls_pemfile::{certs, pkcs8_private_keys};
 | 
			
		||||
use serde::{Deserialize, Serialize}; // Import Deserialize and Serialize traits
 | 
			
		||||
use serde_json::Value; // Removed unused json
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::fs::File;
 | 
			
		||||
use std::io::BufReader;
 | 
			
		||||
use std::sync::Mutex; // Removed unused Arc
 | 
			
		||||
use std::time::{SystemTime, UNIX_EPOCH};
 | 
			
		||||
use tokio::task::JoinHandle;
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
 | 
			
		||||
// Global store for server handles
 | 
			
		||||
// Global store for server handles, initialized with once_cell::sync::Lazy
 | 
			
		||||
pub static SERVER_HANDLES: Lazy<Mutex<HashMap<String, ServerHandle>>> =
 | 
			
		||||
    Lazy::new(|| Mutex::new(HashMap::new()));
 | 
			
		||||
 | 
			
		||||
static AUTHENTICATED_CONNECTIONS: Lazy<Mutex<HashMap<Addr<Server>, String>>> =
 | 
			
		||||
    Lazy::new(|| Mutex::new(HashMap::new()));
 | 
			
		||||
 | 
			
		||||
// Remove any lazy_static related code if it exists elsewhere, this is the correct static definition.
 | 
			
		||||
 | 
			
		||||
mod auth;
 | 
			
		||||
mod builder;
 | 
			
		||||
mod handler;
 | 
			
		||||
 | 
			
		||||
use crate::auth::{generate_nonce, NonceResponse};
 | 
			
		||||
pub use crate::builder::ServerBuilder;
 | 
			
		||||
// Re-export server handle type for external use
 | 
			
		||||
pub type ServerHandle = actix_web::dev::ServerHandle;
 | 
			
		||||
 | 
			
		||||
const TASK_TIMEOUT_DURATION: std::time::Duration = std::time::Duration::from_secs(10);
 | 
			
		||||
 | 
			
		||||
#[derive(Error, Debug)]
 | 
			
		||||
pub enum TlsConfigError {
 | 
			
		||||
    #[error("Certificate file not found: {0}")]
 | 
			
		||||
    CertificateNotFound(String),
 | 
			
		||||
    #[error("Private key file not found: {0}")]
 | 
			
		||||
    PrivateKeyNotFound(String),
 | 
			
		||||
    #[error("Invalid certificate format: {0}")]
 | 
			
		||||
    InvalidCertificate(String),
 | 
			
		||||
    #[error("Invalid private key format: {0}")]
 | 
			
		||||
    InvalidPrivateKey(String),
 | 
			
		||||
    #[error("No private keys found in key file: {0}")]
 | 
			
		||||
    NoPrivateKeys(String),
 | 
			
		||||
    #[error("TLS configuration error: {0}")]
 | 
			
		||||
    ConfigurationError(String),
 | 
			
		||||
    #[error("IO error: {0}")]
 | 
			
		||||
    IoError(#[from] std::io::Error),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct JsonRpcRequest {
 | 
			
		||||
    jsonrpc: String,
 | 
			
		||||
    method: String,
 | 
			
		||||
    params: Value,
 | 
			
		||||
    id: Option<Value>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct JsonRpcResponse {
 | 
			
		||||
    jsonrpc: String,
 | 
			
		||||
    result: Option<Value>,
 | 
			
		||||
    error: Option<JsonRpcError>,
 | 
			
		||||
    id: Value,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct JsonRpcError {
 | 
			
		||||
    code: i32,
 | 
			
		||||
    message: String,
 | 
			
		||||
    data: Option<Value>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct PlayParams {
 | 
			
		||||
    script: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct PlayResult {
 | 
			
		||||
    output: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct AuthCredentials {
 | 
			
		||||
    pubkey: String,
 | 
			
		||||
    signature: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct FetchNonceParams {
 | 
			
		||||
    pubkey: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Actor for Server {
 | 
			
		||||
    type Context = ws::WebsocketContext<Self>;
 | 
			
		||||
 | 
			
		||||
    fn started(&mut self, _ctx: &mut Self::Context) {
 | 
			
		||||
        if self.enable_auth {
 | 
			
		||||
            info!(
 | 
			
		||||
                "Circle '{}' WS: Connection started. Authentication is ENABLED. Waiting for auth challenge.",
 | 
			
		||||
                self.circle_name
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            info!(
 | 
			
		||||
                "Circle '{}' WS: Connection started. Authentication is DISABLED.",
 | 
			
		||||
                self.circle_name
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
 | 
			
		||||
        info!(
 | 
			
		||||
            "Circle '{}' WS: Connection stopping.",
 | 
			
		||||
            self.circle_name
 | 
			
		||||
        );
 | 
			
		||||
        AUTHENTICATED_CONNECTIONS
 | 
			
		||||
            .lock()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .remove(&ctx.address());
 | 
			
		||||
        Running::Stop
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct Server {
 | 
			
		||||
    pub host: String,
 | 
			
		||||
    pub port: u16,
 | 
			
		||||
    pub redis_url: String,
 | 
			
		||||
    pub enable_tls: bool,
 | 
			
		||||
    pub cert_path: Option<String>,
 | 
			
		||||
    pub key_path: Option<String>,
 | 
			
		||||
    pub tls_port: Option<u16>,
 | 
			
		||||
    pub enable_auth: bool,
 | 
			
		||||
    pub enable_webhooks: bool,
 | 
			
		||||
    pub circle_worker_id: String,
 | 
			
		||||
    pub circle_name: String,
 | 
			
		||||
    pub circle_public_key: String,
 | 
			
		||||
    nonce_store: HashMap<String, NonceResponse>,
 | 
			
		||||
    authenticated_pubkey: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Server {
 | 
			
		||||
    /// Get the effective port for TLS connections
 | 
			
		||||
    pub fn get_tls_port(&self) -> u16 {
 | 
			
		||||
        self.tls_port.unwrap_or(self.port)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Check if TLS is properly configured
 | 
			
		||||
    pub fn is_tls_configured(&self) -> bool {
 | 
			
		||||
        self.cert_path.is_some() && self.key_path.is_some()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn spawn_circle_server(&self) -> std::io::Result<(JoinHandle<std::io::Result<()>>, ServerHandle)> {
 | 
			
		||||
        let host = self.host.clone();
 | 
			
		||||
        let port = self.port;
 | 
			
		||||
    
 | 
			
		||||
        // Validate TLS configuration if enabled
 | 
			
		||||
        if self.enable_tls && !self.is_tls_configured() {
 | 
			
		||||
            return Err(std::io::Error::new(
 | 
			
		||||
                std::io::ErrorKind::InvalidInput,
 | 
			
		||||
                "TLS is enabled but certificate or key path is missing",
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        let server_config_data = web::Data::new(self.clone());
 | 
			
		||||
    
 | 
			
		||||
        let http_server = HttpServer::new(move || {
 | 
			
		||||
            let mut app = App::new()
 | 
			
		||||
                .app_data(server_config_data.clone())
 | 
			
		||||
                .route("/{circle_pk}", web::get().to(ws_handler));
 | 
			
		||||
            
 | 
			
		||||
            app
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let server = if self.enable_tls && self.is_tls_configured() {
 | 
			
		||||
            let cert_path = self.cert_path.as_ref().unwrap();
 | 
			
		||||
            let key_path = self.key_path.as_ref().unwrap();
 | 
			
		||||
            let tls_port = self.get_tls_port();
 | 
			
		||||
            
 | 
			
		||||
            info!("🔒 WSS (WebSocket Secure) is ENABLED for multi-circle server");
 | 
			
		||||
            info!("📜 Certificate: {}", cert_path);
 | 
			
		||||
            info!("🔑 Private key: {}", key_path);
 | 
			
		||||
            info!("🌐 WSS URL pattern: wss://{}:{}/<circle_pk>", host, tls_port);
 | 
			
		||||
            
 | 
			
		||||
            match load_rustls_config(cert_path, key_path) {
 | 
			
		||||
                Ok(tls_config) => {
 | 
			
		||||
                    info!("✅ TLS configuration loaded successfully");
 | 
			
		||||
                    http_server.bind_rustls_0_23((host.as_str(), tls_port), tls_config)
 | 
			
		||||
                        .map_err(|e| std::io::Error::new(
 | 
			
		||||
                            std::io::ErrorKind::AddrInUse,
 | 
			
		||||
                            format!("Failed to bind WSS server to {}:{}: {}", host, tls_port, e)
 | 
			
		||||
                        ))?
 | 
			
		||||
                }
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    error!("❌ Failed to load TLS configuration: {}", e);
 | 
			
		||||
                    return Err(std::io::Error::new(
 | 
			
		||||
                        std::io::ErrorKind::InvalidInput,
 | 
			
		||||
                        format!("TLS configuration error: {}", e)
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            info!("🔓 WS (WebSocket) is ENABLED for multi-circle server (no TLS)");
 | 
			
		||||
            info!("🌐 WS URL pattern: ws://{}:{}/<circle_pk>", host, port);
 | 
			
		||||
            http_server.bind((host.as_str(), port))
 | 
			
		||||
                .map_err(|e| std::io::Error::new(
 | 
			
		||||
                    std::io::ErrorKind::AddrInUse,
 | 
			
		||||
                    format!("Failed to bind WS server to {}:{}: {}", host, port, e)
 | 
			
		||||
                ))?
 | 
			
		||||
        }
 | 
			
		||||
        .run();
 | 
			
		||||
    
 | 
			
		||||
        let handle = server.handle();
 | 
			
		||||
        let server_task = tokio::spawn(server);
 | 
			
		||||
    
 | 
			
		||||
        let protocol = if self.enable_tls { "WSS" } else { "WS" };
 | 
			
		||||
        let effective_port = if self.enable_tls { self.get_tls_port() } else { port };
 | 
			
		||||
        
 | 
			
		||||
        info!(
 | 
			
		||||
            "🚀 Multi-circle {} server running on {}:{}",
 | 
			
		||||
            protocol, host, effective_port
 | 
			
		||||
        );
 | 
			
		||||
        
 | 
			
		||||
        if self.enable_auth {
 | 
			
		||||
            info!("🔐 Authentication is ENABLED");
 | 
			
		||||
        } else {
 | 
			
		||||
            info!("🔓 Authentication is DISABLED");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok((server_task, handle))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn is_connection_authenticated(&self) -> bool {
 | 
			
		||||
        self.authenticated_pubkey.is_some()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_fetch_nonce(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        match serde_json::from_value::<FetchNonceParams>(params) {
 | 
			
		||||
            Ok(params) => {
 | 
			
		||||
                let nonce_response = generate_nonce();
 | 
			
		||||
                self.nonce_store
 | 
			
		||||
                    .insert(params.pubkey, nonce_response.clone());
 | 
			
		||||
                let resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: Some(serde_json::to_value(nonce_response).unwrap()),
 | 
			
		||||
                    error: None,
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: format!("Invalid parameters for fetch_nonce: {}", e),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_authenticate(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if !self.enable_auth {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication is disabled on this server.".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        match serde_json::from_value::<AuthCredentials>(params) {
 | 
			
		||||
            Ok(auth_params) => {
 | 
			
		||||
                let nonce_response = self.nonce_store.get(&auth_params.pubkey);
 | 
			
		||||
 | 
			
		||||
                let is_valid = if let Some(nonce_resp) = nonce_response {
 | 
			
		||||
                    let current_time = SystemTime::now()
 | 
			
		||||
                        .duration_since(UNIX_EPOCH)
 | 
			
		||||
                        .unwrap()
 | 
			
		||||
                        .as_secs();
 | 
			
		||||
                    if nonce_resp.expires_at < current_time {
 | 
			
		||||
                        log::warn!("Auth failed for {}: Nonce expired", self.circle_name);
 | 
			
		||||
                        false
 | 
			
		||||
                    } else {
 | 
			
		||||
                        match auth::verify_signature(
 | 
			
		||||
                            &auth_params.pubkey,
 | 
			
		||||
                            &nonce_resp.nonce,
 | 
			
		||||
                            &auth_params.signature,
 | 
			
		||||
                        ) {
 | 
			
		||||
                            Ok(valid) => valid,
 | 
			
		||||
                            Err(_) => false,
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    false
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                if is_valid {
 | 
			
		||||
                    self.authenticated_pubkey = Some(auth_params.pubkey.clone());
 | 
			
		||||
                    AUTHENTICATED_CONNECTIONS
 | 
			
		||||
                        .lock()
 | 
			
		||||
                        .unwrap()
 | 
			
		||||
                        .insert(ctx.address(), auth_params.pubkey);
 | 
			
		||||
                    let resp = JsonRpcResponse {
 | 
			
		||||
                        jsonrpc: "2.0".to_string(),
 | 
			
		||||
                        result: Some(serde_json::json!({ "authenticated": true })),
 | 
			
		||||
                        error: None,
 | 
			
		||||
                        id: client_rpc_id,
 | 
			
		||||
                    };
 | 
			
		||||
                    ctx.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                } else {
 | 
			
		||||
                    let err_resp = JsonRpcResponse {
 | 
			
		||||
                        jsonrpc: "2.0".to_string(),
 | 
			
		||||
                        result: None,
 | 
			
		||||
                        error: Some(JsonRpcError {
 | 
			
		||||
                            code: -32002,
 | 
			
		||||
                            message: "Invalid Credentials".to_string(),
 | 
			
		||||
                            data: None,
 | 
			
		||||
                        }),
 | 
			
		||||
                        id: client_rpc_id,
 | 
			
		||||
                    };
 | 
			
		||||
                    ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    ctx.stop();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: format!("Invalid parameters for authenticate: {}", e),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_whoami(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        _params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        // Check if authentication is enabled and if the connection is authenticated
 | 
			
		||||
        if self.enable_auth {
 | 
			
		||||
            if self.is_connection_authenticated() {
 | 
			
		||||
                // Get the authenticated public key from the global store
 | 
			
		||||
                let authenticated_pubkey = AUTHENTICATED_CONNECTIONS
 | 
			
		||||
                    .lock()
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                    .get(&ctx.address())
 | 
			
		||||
                    .cloned()
 | 
			
		||||
                    .unwrap_or_else(|| "unknown".to_string());
 | 
			
		||||
                
 | 
			
		||||
                let response = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: Some(serde_json::json!({
 | 
			
		||||
                        "authenticated": true,
 | 
			
		||||
                        "public_key": authenticated_pubkey,
 | 
			
		||||
                        "circle_name": self.circle_name,
 | 
			
		||||
                        "auth_enabled": self.enable_auth
 | 
			
		||||
                    })),
 | 
			
		||||
                    error: None,
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&response).unwrap());
 | 
			
		||||
            } else {
 | 
			
		||||
                // Not authenticated
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32001,
 | 
			
		||||
                        message: "Authentication required. Please authenticate first.".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Authentication is disabled, return basic info
 | 
			
		||||
            let response = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: Some(serde_json::json!({
 | 
			
		||||
                    "authenticated": false,
 | 
			
		||||
                    "public_key": null,
 | 
			
		||||
                    "circle_name": self.circle_name,
 | 
			
		||||
                    "auth_enabled": self.enable_auth
 | 
			
		||||
                })),
 | 
			
		||||
                error: None,
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&response).unwrap());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_play(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32001,
 | 
			
		||||
                    message: "Authentication Required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        match serde_json::from_value::<PlayParams>(params) {
 | 
			
		||||
            Ok(play_params) => {
 | 
			
		||||
                info!("Received play request from: {}", self.authenticated_pubkey.clone().unwrap_or_else(|| "anonymous".to_string()));
 | 
			
		||||
                let script_content = play_params.script;
 | 
			
		||||
                let circle_pk_clone = self.circle_public_key.clone();
 | 
			
		||||
                let redis_url_clone = self.redis_url.clone();
 | 
			
		||||
                let _rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
                let public_key = self.authenticated_pubkey.clone();
 | 
			
		||||
                let worker_id_clone = self.circle_worker_id.clone();
 | 
			
		||||
 | 
			
		||||
                let fut = async move {
 | 
			
		||||
                    let caller_id = public_key.unwrap_or_else(|| "anonymous".to_string());
 | 
			
		||||
                    match DispatcherBuilder::new()
 | 
			
		||||
                        .redis_url(&redis_url_clone)
 | 
			
		||||
                        .caller_id(&caller_id)
 | 
			
		||||
                        .build() {
 | 
			
		||||
                        Ok(hero_dispatcher) => {
 | 
			
		||||
                            hero_dispatcher
 | 
			
		||||
                                .new_job()
 | 
			
		||||
                                    .context_id(&circle_pk_clone)
 | 
			
		||||
                                    .worker_id(&worker_id_clone)
 | 
			
		||||
                                    .script(&script_content)
 | 
			
		||||
                                    .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                                    .await_response()
 | 
			
		||||
                                    .await
 | 
			
		||||
                        }
 | 
			
		||||
                        Err(e) => Err(e),
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                ctx.spawn(
 | 
			
		||||
                    fut.into_actor(self)
 | 
			
		||||
                        .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                            Ok(task_details) => {
 | 
			
		||||
                                if task_details.status == "completed" {
 | 
			
		||||
                                    let output = task_details
 | 
			
		||||
                                        .output
 | 
			
		||||
                                        .unwrap_or_else(|| "No output".to_string());
 | 
			
		||||
                                    let result_value = PlayResult { output };
 | 
			
		||||
                                    let resp = JsonRpcResponse {
 | 
			
		||||
                                        jsonrpc: "2.0".to_string(),
 | 
			
		||||
                                        result: Some(serde_json::to_value(result_value).unwrap()),
 | 
			
		||||
                                        error: None,
 | 
			
		||||
                                        id: client_rpc_id,
 | 
			
		||||
                                    };
 | 
			
		||||
                                    ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    let error_message = task_details.error.unwrap_or_else(|| {
 | 
			
		||||
                                        "Rhai script execution failed".to_string()
 | 
			
		||||
                                    });
 | 
			
		||||
                                    let err_resp = JsonRpcResponse {
 | 
			
		||||
                                        jsonrpc: "2.0".to_string(),
 | 
			
		||||
                                        result: None,
 | 
			
		||||
                                        error: Some(JsonRpcError {
 | 
			
		||||
                                            code: -32000,
 | 
			
		||||
                                            message: error_message,
 | 
			
		||||
                                            data: None,
 | 
			
		||||
                                        }),
 | 
			
		||||
                                        id: client_rpc_id,
 | 
			
		||||
                                    };
 | 
			
		||||
                                    ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            Err(e) => {
 | 
			
		||||
                                let (code, message) = match e {
 | 
			
		||||
                                    DispatcherError::Timeout(task_id) => (
 | 
			
		||||
                                        -32002,
 | 
			
		||||
                                        format!(
 | 
			
		||||
                                            "Timeout waiting for Rhai script (task: {})",
 | 
			
		||||
                                            task_id
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    _ => (-32003, format!("Rhai infrastructure error: {}", e)),
 | 
			
		||||
                                };
 | 
			
		||||
                                let err_resp = JsonRpcResponse {
 | 
			
		||||
                                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                                    result: None,
 | 
			
		||||
                                    error: Some(JsonRpcError {
 | 
			
		||||
                                        code,
 | 
			
		||||
                                        message,
 | 
			
		||||
                                        data: None,
 | 
			
		||||
                                    }),
 | 
			
		||||
                                    id: client_rpc_id,
 | 
			
		||||
                                };
 | 
			
		||||
                                ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                            }
 | 
			
		||||
                        }),
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: format!("Invalid parameters for play: {}", e),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn load_rustls_config(
 | 
			
		||||
    cert_path: &str,
 | 
			
		||||
    key_path: &str,
 | 
			
		||||
) -> Result<RustlsServerConfig, TlsConfigError> {
 | 
			
		||||
    info!("Loading TLS configuration from cert: {}, key: {}", cert_path, key_path);
 | 
			
		||||
    
 | 
			
		||||
    // Validate file existence
 | 
			
		||||
    if !std::path::Path::new(cert_path).exists() {
 | 
			
		||||
        return Err(TlsConfigError::CertificateNotFound(cert_path.to_string()));
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if !std::path::Path::new(key_path).exists() {
 | 
			
		||||
        return Err(TlsConfigError::PrivateKeyNotFound(key_path.to_string()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let config = RustlsServerConfig::builder().with_no_client_auth();
 | 
			
		||||
 | 
			
		||||
    // Load certificate file
 | 
			
		||||
    let cert_file = &mut BufReader::new(File::open(cert_path)
 | 
			
		||||
        .map_err(|e| TlsConfigError::ConfigurationError(format!("Failed to open certificate file: {}", e)))?);
 | 
			
		||||
    
 | 
			
		||||
    // Load key file
 | 
			
		||||
    let key_file = &mut BufReader::new(File::open(key_path)
 | 
			
		||||
        .map_err(|e| TlsConfigError::ConfigurationError(format!("Failed to open key file: {}", e)))?);
 | 
			
		||||
 | 
			
		||||
    // Parse certificates
 | 
			
		||||
    let cert_chain: Vec<_> = certs(cert_file)
 | 
			
		||||
        .collect::<Result<Vec<_>, _>>()
 | 
			
		||||
        .map_err(|e| TlsConfigError::InvalidCertificate(format!("Failed to parse certificates: {}", e)))?;
 | 
			
		||||
    
 | 
			
		||||
    if cert_chain.is_empty() {
 | 
			
		||||
        return Err(TlsConfigError::InvalidCertificate("No certificates found in certificate file".to_string()));
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    info!("Loaded {} certificate(s)", cert_chain.len());
 | 
			
		||||
 | 
			
		||||
    // Parse private keys
 | 
			
		||||
    let mut keys: Vec<PrivateKeyDer> = pkcs8_private_keys(key_file)
 | 
			
		||||
        .collect::<Result<Vec<_>, _>>()
 | 
			
		||||
        .map_err(|e| TlsConfigError::InvalidPrivateKey(format!("Failed to parse private key: {}", e)))?
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .map(|k| k.into())
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    if keys.is_empty() {
 | 
			
		||||
        return Err(TlsConfigError::NoPrivateKeys(key_path.to_string()));
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    info!("Loaded {} private key(s)", keys.len());
 | 
			
		||||
 | 
			
		||||
    // Create TLS configuration
 | 
			
		||||
    config.with_single_cert(cert_chain, keys.remove(0))
 | 
			
		||||
        .map_err(|e| TlsConfigError::ConfigurationError(format!("Failed to create TLS configuration: {}", e)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn ws_handler(
 | 
			
		||||
    req: HttpRequest,
 | 
			
		||||
    stream: web::Payload,
 | 
			
		||||
    server: web::Data<Server>,
 | 
			
		||||
) -> Result<HttpResponse, Error> {
 | 
			
		||||
    let server_circle_name = req.match_info().get("circle_pk").unwrap_or("unknown").to_string();
 | 
			
		||||
    let circle_public_key = server_circle_name.clone(); // Assuming pk is the name for now
 | 
			
		||||
 | 
			
		||||
    // Extract the Server from web::Data and clone it
 | 
			
		||||
    let mut server_actor = server.as_ref().clone();
 | 
			
		||||
    
 | 
			
		||||
    // Set the circle name for this WebSocket connection
 | 
			
		||||
    server_actor.circle_name = server_circle_name;
 | 
			
		||||
    server_actor.circle_public_key = circle_public_key;
 | 
			
		||||
 | 
			
		||||
    // Create and start the WebSocket actor
 | 
			
		||||
    ws::start(
 | 
			
		||||
        server_actor,
 | 
			
		||||
        &req,
 | 
			
		||||
        stream,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user