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