initial commit

This commit is contained in:
Timur Gordon
2025-07-29 01:15:23 +02:00
commit 7d7ff0f0ab
108 changed files with 24713 additions and 0 deletions

View 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);
}
}

View 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()
}
}

View 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();
}
_ => (),
}
}
}

View 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,
)
}