hostbasket/sigsocket/examples/client_app/src/main.rs
2025-05-19 14:48:40 +03:00

475 lines
23 KiB
Rust

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<KeyPair>,
pending_request: Arc<Mutex<Option<SignRequest>>>,
websocket_sender: mpsc::Sender<WebSocketCommand>,
}
// Commands that can be sent to the WebSocket connection
enum WebSocketCommand {
Sign { id: String, message: String, signature: Vec<u8> },
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<u8> {
// 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<AppState>) -> Result<HttpResponse> {
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<AppState>,
form: web::Form<SignRequestForm>,
) -> 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!("<h1>Error sending signature</h1><p>{}</p><p><a href='/'>Return to home</a></p>", 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#"<html>
<head>
<title>Signature Sent</title>
<meta http-equiv="refresh" content="2; url=/" />
<script type="text/javascript">
console.log("Signature sent successfully, redirecting in 2 seconds...");
setTimeout(function() { window.location.href = '/'; }, 2000);
</script>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.success { color: green; }
</style>
</head>
<body>
<h1 class="success">✓ Signature Sent Successfully!</h1>
<p>Redirecting back to home page...</p>
<p><a href="/">Click here if you're not redirected automatically</a></p>
</body>
</html>"#);
} 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!("<h1>Error processing request</h1><p>{}</p><p><a href='/'>Return to home</a></p>", 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<KeyPair>,
pending_request: Arc<Mutex<Option<SignRequest>>>,
mut command_receiver: mpsc::Receiver<WebSocketCommand>,
) {
// 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::<u64>() % 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::<SignRequest>(&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::<WebSocketCommand>(32);
// Create the pending request mutex
let pending_request = Arc::new(Mutex::new(None::<SignRequest>));
// 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
}