use actix_files as fs; use actix_web::{web, App, HttpServer, Responder, HttpResponse, Result}; use serde::{Deserialize, Serialize}; use tera::{Tera, Context}; use std::sync::{Arc, Mutex}; use tokio::sync::mpsc; use tokio_tungstenite::{connect_async, tungstenite}; use futures_util::{StreamExt, SinkExt}; use secp256k1::{Secp256k1, SecretKey, Message}; use sha2::{Sha256, Digest}; use url::Url; use std::thread; // Struct for representing a sign request #[derive(Serialize, Deserialize, Clone, Debug)] struct SignRequest { id: String, message: String, #[serde(skip)] message_raw: String, // Original base64 message for sending back in the response #[serde(skip)] message_decoded: String, // Decoded message for display } // Struct for representing the application state struct AppState { templates: Tera, keypair: Arc, pending_request: Arc>>, websocket_sender: mpsc::Sender, } // Commands that can be sent to the WebSocket connection enum WebSocketCommand { Sign { id: String, message: String, signature: Vec }, Close, } // Keypair for signing messages struct KeyPair { secret_key: SecretKey, public_key_hex: String, } impl KeyPair { fn new() -> Self { let secp = Secp256k1::new(); let mut rng = rand::thread_rng(); // Generate a new random keypair let (secret_key, public_key) = secp.generate_keypair(&mut rng); // Convert public key to hex for identification let public_key_hex = hex::encode(public_key.serialize()); KeyPair { secret_key, public_key_hex, } } fn sign(&self, message: &[u8]) -> Vec { // Hash the message first (secp256k1 requires a 32-byte hash) let mut hasher = Sha256::new(); hasher.update(message); let message_hash = hasher.finalize(); // Create a secp256k1 message from the hash let secp_message = Message::from_slice(&message_hash).unwrap(); // Sign the message let secp = Secp256k1::new(); let signature = secp.sign_ecdsa(&secp_message, &self.secret_key); // Return the serialized signature signature.serialize_compact().to_vec() } } // Controller for the index page async fn index(data: web::Data) -> Result { let mut context = Context::new(); // Add the keypair to the context context.insert("public_key", &data.keypair.public_key_hex); // Add the pending request if there is one if let Some(request) = &*data.pending_request.lock().unwrap() { context.insert("request", request); } let rendered = data.templates.render("index.html", &context) .map_err(|e| { eprintln!("Template error: {}", e); actix_web::error::ErrorInternalServerError("Template error") })?; Ok(HttpResponse::Ok().content_type("text/html").body(rendered)) } // Controller for the sign endpoint async fn sign_request( data: web::Data, form: web::Form, ) -> impl Responder { println!("SIGN ENDPOINT: Starting sign_request handler for form ID: {}", form.id); // Try to get a lock on the pending request println!("SIGN ENDPOINT: Attempting to acquire lock on pending_request"); match data.pending_request.try_lock() { Ok(mut guard) => { // Check if we have a pending request if let Some(request) = &*guard { println!("SIGN ENDPOINT: Found pending request with ID: {}", request.id); // Get the request ID let id = request.id.clone(); // Verify that the request ID matches if id == form.id { println!("SIGN ENDPOINT: Request ID matches form ID: {}", id); // Sign the message let message = request.message.as_bytes(); println!("SIGN ENDPOINT: About to sign message: {} (length: {})", String::from_utf8_lossy(message), message.len()); let signature = data.keypair.sign(message); println!("SIGN ENDPOINT: Message signed successfully. Signature length: {}", signature.len()); // Send the signature via WebSocket println!("SIGN ENDPOINT: About to send signature via websocket channel"); match data.websocket_sender.send(WebSocketCommand::Sign { id: id.clone(), message: request.message_raw.clone(), // Include the original base64 message signature }).await { Ok(_) => { println!("SIGN ENDPOINT: Successfully sent signature to websocket channel"); }, Err(e) => { let error_msg = format!("Failed to send signature: {}", e); println!("SIGN ENDPOINT ERROR: {}", error_msg); return HttpResponse::InternalServerError() .content_type("text/html") .body(format!("

Error sending signature

{}

Return to home

", error_msg)); } } // Clear the pending request println!("SIGN ENDPOINT: Clearing pending request"); *guard = None; // Return a success page that continues to the next step println!("SIGN ENDPOINT: Returning success response"); return HttpResponse::Ok() .content_type("text/html") .body(r#" Signature Sent

✓ Signature Sent Successfully!

Redirecting back to home page...

Click here if you're not redirected automatically

"#); } else { println!("SIGN ENDPOINT: Request ID {} does not match form ID {}", request.id, form.id); } } else { println!("SIGN ENDPOINT: No pending request found"); } }, Err(e) => { let error_msg = format!("Failed to acquire lock on pending_request: {}", e); println!("SIGN ENDPOINT ERROR: {}", error_msg); return HttpResponse::InternalServerError() .content_type("text/html") .body(format!("

Error processing request

{}

Return to home

", error_msg)); } } // Redirect back to the index page (if no request was found or ID didn't match) println!("SIGN ENDPOINT: No matching request found, redirecting to home"); HttpResponse::SeeOther() .append_header(("Location", "/")) .finish() } // Form for submitting a signature #[derive(Deserialize)] struct SignRequestForm { id: String, } // WebSocket client task that connects to the SigSocket server async fn websocket_client_task( keypair: Arc, pending_request: Arc>>, mut command_receiver: mpsc::Receiver, ) { // Connect directly to the web app's integrated SigSocket endpoint let sigsocket_url = "ws://127.0.0.1:8080/ws"; // Reconnection settings let mut retry_count = 0; const MAX_RETRY_COUNT: u32 = 10; // Reset retry counter after this many attempts const BASE_RETRY_DELAY_MS: u64 = 1000; // Start with 1 second const MAX_RETRY_DELAY_MS: u64 = 30000; // Cap at 30 seconds loop { // Calculate backoff delay with jitter for retry let delay_ms = if retry_count > 0 { let base_delay = BASE_RETRY_DELAY_MS * 2u64.pow(retry_count.min(6)); let jitter = rand::random::() % 500; // Add up to 500ms of jitter (base_delay + jitter).min(MAX_RETRY_DELAY_MS) } else { 0 // No delay on first attempt }; if retry_count > 0 { println!("Reconnection attempt {} in {} ms...", retry_count, delay_ms); tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await; } // Connect to the SigSocket server with timeout println!("Connecting to SigSocket server at {}", sigsocket_url); let connect_result = tokio::time::timeout( tokio::time::Duration::from_secs(10), // Connection timeout connect_async(Url::parse(sigsocket_url).unwrap()) ).await; match connect_result { // Timeout error Err(_) => { eprintln!("Connection attempt timed out"); retry_count = (retry_count + 1) % MAX_RETRY_COUNT; continue; }, // Connection result Ok(conn_result) => match conn_result { // Connection successful Ok((mut ws_stream, _)) => { println!("Connected to SigSocket server"); // Reset retry counter on successful connection retry_count = 0; // Heartbeat functionality has been removed println!("DEBUG: Running without heartbeat functionality"); // Send the initial message with just the raw public key let intro_message = keypair.public_key_hex.clone(); if let Err(e) = ws_stream.send(tungstenite::Message::Text(intro_message)).await { eprintln!("Failed to send introduction message: {}", e); continue; } println!("Sent introduction with public key: {}", keypair.public_key_hex); // Last time we received a message or pong from the server let mut last_server_response = std::time::Instant::now(); // Process incoming messages and commands loop { tokio::select! { // Handle WebSocket message msg = ws_stream.next() => { match msg { Some(Ok(tungstenite::Message::Text(text))) => { println!("Received message: {}", text); last_server_response = std::time::Instant::now(); // Parse the message as a sign request match serde_json::from_str::(&text) { Ok(mut request) => { println!("DEBUG: Successfully parsed sign request with ID: {}", request.id); println!("DEBUG: Base64 message: {}", request.message); // Save the original base64 message for later use in response request.message_raw = request.message.clone(); // Decode the base64 message content match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &request.message) { Ok(decoded) => { let decoded_text = String::from_utf8_lossy(&decoded).to_string(); println!("DEBUG: Decoded message: {}", decoded_text); // Store the decoded message for display request.message_decoded = decoded_text; // Update the message for displaying in the UI request.message = request.message_decoded.clone(); // Store the request for display in the UI *pending_request.lock().unwrap() = Some(request); println!("Received signing request. Please check the web UI to approve it."); }, Err(e) => { eprintln!("Error decoding base64 message: {}", e); } } }, Err(e) => { eprintln!("Error parsing sign request JSON: {}", e); eprintln!("Raw message: {}", text); } } }, Some(Ok(tungstenite::Message::Ping(data))) => { // Respond to ping with pong last_server_response = std::time::Instant::now(); if let Err(e) = ws_stream.send(tungstenite::Message::Pong(data)).await { eprintln!("Failed to send pong: {}", e); break; } }, Some(Ok(tungstenite::Message::Pong(_))) => { // Got pong response from the server last_server_response = std::time::Instant::now(); }, Some(Ok(_)) => { // Ignore other types of messages last_server_response = std::time::Instant::now(); }, Some(Err(e)) => { eprintln!("WebSocket error: {}", e); break; }, None => { eprintln!("WebSocket connection closed"); break; }, } }, // Heartbeat functionality has been removed // Handle signing command from the web interface cmd = command_receiver.recv() => { match cmd { Some(WebSocketCommand::Sign { id, message, signature }) => { println!("DEBUG: Signing request ID: {}", id); println!("DEBUG: Raw signature bytes: {:?}", signature); println!("DEBUG: Using message from command: {}", message); // Convert signature bytes to base64 let sig_base64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &signature); println!("DEBUG: Base64 signature: {}", sig_base64); // Create a JSON response with explicit ID and message/signature fields let response = format!("{{\"id\": \"{}\", \"message\": \"{}\", \"signature\": \"{}\"}}", id, message, sig_base64); println!("DEBUG: Preparing to send JSON response: {}", response); println!("DEBUG: Response length: {} bytes", response.len()); // Log that we're about to send on the WebSocket connection println!("DEBUG: About to send on WebSocket connection"); // Send the signature response right away - with extra logging println!("!!!! ATTEMPTING TO SEND SIGNATURE RESPONSE NOW !!!!"); match ws_stream.send(tungstenite::Message::Text(response.clone())).await { Ok(_) => { last_server_response = std::time::Instant::now(); println!("!!!! SUCCESSFULLY SENT SIGNATURE RESPONSE !!!!"); println!("!!!! SIGNATURE SENT FOR REQUEST ID: {} !!!!", id); // Clear the pending request after successful signature *pending_request.lock().unwrap() = None; // Send another simple message to confirm the connection is still working if let Err(e) = ws_stream.send(tungstenite::Message::Text("CONFIRM_SIGNATURE_SENT".to_string())).await { println!("DEBUG: Failed to send confirmation message: {}", e); } else { println!("DEBUG: Sent confirmation message after signature"); } }, Err(e) => { eprintln!("!!!! FAILED TO SEND SIGNATURE RESPONSE: {} !!!!", e); // Try to reconnect or recover println!("DEBUG: Attempting to diagnose connection issue..."); break; } } }, Some(WebSocketCommand::Close) => { println!("DEBUG: Received close command, closing connection"); break; }, None => { eprintln!("Command channel closed"); break; } } } } } // Connection loop has ended, will attempt to reconnect println!("WebSocket connection closed, will attempt to reconnect..."); }, // Connection error Err(e) => { eprintln!("Failed to connect to SigSocket server: {}", e); } } } // Increment retry counter but don't exceed MAX_RETRY_COUNT retry_count = (retry_count + 1) % MAX_RETRY_COUNT; } } #[actix_web::main] async fn main() -> std::io::Result<()> { // Setup logger env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); // Initialize templates let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("index.html", include_str!("../templates/index.html")), ]).unwrap(); // Generate a keypair for signing let keypair = Arc::new(KeyPair::new()); println!("Generated keypair with public key: {}", keypair.public_key_hex); // Create a channel for sending commands to the WebSocket client let (command_sender, command_receiver) = mpsc::channel::(32); // Create the pending request mutex let pending_request = Arc::new(Mutex::new(None::)); // Spawn the WebSocket client task let ws_keypair = keypair.clone(); let ws_pending_request = pending_request.clone(); tokio::spawn(async move { websocket_client_task(ws_keypair, ws_pending_request, command_receiver).await; }); // Create the app state let app_state = web::Data::new(AppState { templates: tera, keypair, pending_request, websocket_sender: command_sender, }); println!("Client App server starting on http://127.0.0.1:8082"); // Start the web server HttpServer::new(move || { App::new() .app_data(app_state.clone()) // Register routes .route("/", web::get().to(index)) .route("/sign", web::post().to(sign_request)) // Static files .service(fs::Files::new("/static", "./static")) }) .bind("127.0.0.1:8082")? .run() .await }