implement signature requests over ws
This commit is contained in:
439
sigsocket/examples/web_app/src/main.rs
Normal file
439
sigsocket/examples/web_app/src/main.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
use actix_files as fs;
|
||||
use actix_web::{web, App, HttpServer, Responder, HttpResponse, Result};
|
||||
use actix_web_actors::ws;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::{Tera, Context};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use sigsocket::service::SigSocketService;
|
||||
use sigsocket::registry::ConnectionRegistry;
|
||||
use std::sync::RwLock;
|
||||
use log::{info, error};
|
||||
use hex;
|
||||
use base64;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::task;
|
||||
use serde_json::json;
|
||||
|
||||
// Status enum to represent the current state of a signature request
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub enum SignatureStatus {
|
||||
Pending, // Request is created but not yet sent to the client
|
||||
Processing, // Request is sent to the client for signing
|
||||
Success, // Signature received and verified successfully
|
||||
Error, // An error occurred during signing
|
||||
Timeout, // Request timed out waiting for signature
|
||||
}
|
||||
|
||||
// Shared state for the application
|
||||
struct AppState {
|
||||
templates: Tera,
|
||||
sigsocket_service: Arc<SigSocketService>,
|
||||
// Store all pending signature requests with their status
|
||||
signature_requests: Arc<Mutex<HashMap<String, PendingSignature>>>,
|
||||
}
|
||||
|
||||
// Structure for incoming sign requests
|
||||
#[derive(Deserialize)]
|
||||
struct SignRequest {
|
||||
public_key: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
// Result structure for API responses
|
||||
#[derive(Serialize, Clone)]
|
||||
struct SignResult {
|
||||
id: String, // Unique ID for this signature request
|
||||
public_key: String, // Public key of the signer
|
||||
message: String, // Original message that was signed
|
||||
status: SignatureStatus, // Current status of the request
|
||||
signature: Option<String>, // Signature if available
|
||||
error: Option<String>, // Error message if any
|
||||
created_at: String, // When the request was created (human readable)
|
||||
updated_at: String, // When the request was last updated (human readable)
|
||||
}
|
||||
|
||||
// Structure to track pending signatures
|
||||
#[derive(Clone)]
|
||||
struct PendingSignature {
|
||||
id: String, // Unique ID for this request
|
||||
public_key: String, // Public key that should sign
|
||||
message: String, // Message to be signed
|
||||
message_bytes: Vec<u8>, // Raw message bytes
|
||||
status: SignatureStatus, // Current status
|
||||
error: Option<String>, // Error message if any
|
||||
signature: Option<String>, // Signature if available
|
||||
created_at: Instant, // When the request was created
|
||||
updated_at: Instant, // When the request was last updated
|
||||
timeout_duration: Duration // How long to wait before timing out
|
||||
}
|
||||
|
||||
impl PendingSignature {
|
||||
fn new(id: String, public_key: String, message: String, message_bytes: Vec<u8>) -> Self {
|
||||
let now = Instant::now();
|
||||
PendingSignature {
|
||||
id,
|
||||
public_key,
|
||||
message,
|
||||
message_bytes,
|
||||
status: SignatureStatus::Pending,
|
||||
signature: None,
|
||||
error: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
timeout_duration: Duration::from_secs(60), // Default 60-second timeout
|
||||
}
|
||||
}
|
||||
|
||||
fn to_result(&self) -> SignResult {
|
||||
SignResult {
|
||||
id: self.id.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
message: self.message.clone(),
|
||||
status: self.status.clone(),
|
||||
signature: self.signature.clone(),
|
||||
error: self.error.clone(),
|
||||
created_at: format!("{}s ago", self.created_at.elapsed().as_secs()),
|
||||
updated_at: format!("{}s ago", self.updated_at.elapsed().as_secs()),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_status(&mut self, status: SignatureStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Instant::now();
|
||||
}
|
||||
|
||||
fn set_success(&mut self, signature: String) {
|
||||
self.signature = Some(signature);
|
||||
self.update_status(SignatureStatus::Success);
|
||||
}
|
||||
|
||||
fn set_error(&mut self, error: String) {
|
||||
self.error = Some(error);
|
||||
self.update_status(SignatureStatus::Error);
|
||||
}
|
||||
|
||||
fn is_timed_out(&self) -> bool {
|
||||
self.created_at.elapsed() > self.timeout_duration
|
||||
}
|
||||
}
|
||||
|
||||
// Controller for the index page
|
||||
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
// Add all signature requests to the context
|
||||
let signature_requests = data.signature_requests.lock().unwrap();
|
||||
|
||||
// Convert the pending signatures to results for the template
|
||||
let mut pending_sigs: Vec<&PendingSignature> = signature_requests.values().collect();
|
||||
|
||||
// Sort by created_at date (newest first)
|
||||
pending_sigs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
// Convert to results after sorting
|
||||
let results: Vec<SignResult> = pending_sigs.iter()
|
||||
.map(|sig| sig.to_result())
|
||||
.collect();
|
||||
|
||||
context.insert("signature_requests", &results);
|
||||
context.insert("has_requests", &!results.is_empty());
|
||||
|
||||
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(
|
||||
data: web::Data<AppState>,
|
||||
form: web::Form<SignRequest>,
|
||||
) -> impl Responder {
|
||||
let message = form.message.clone();
|
||||
let public_key = form.public_key.clone();
|
||||
|
||||
info!("Received sign request for public key: {}", &public_key);
|
||||
info!("Message to sign: {}", &message);
|
||||
|
||||
// Generate a unique ID for this signature request
|
||||
let request_id = Uuid::new_v4().to_string();
|
||||
|
||||
// Log the message bytes
|
||||
let message_bytes = message.as_bytes().to_vec();
|
||||
info!("Message bytes: {:?}", message_bytes);
|
||||
info!("Message hex: {}", hex::encode(&message_bytes));
|
||||
|
||||
// Create a new pending signature request
|
||||
let pending = PendingSignature::new(
|
||||
request_id.clone(),
|
||||
public_key.clone(),
|
||||
message.clone(),
|
||||
message_bytes.clone()
|
||||
);
|
||||
|
||||
// Add the pending request to our state
|
||||
{
|
||||
let mut signature_requests = data.signature_requests.lock().unwrap();
|
||||
signature_requests.insert(request_id.clone(), pending);
|
||||
info!("Added new pending signature request: {}", request_id);
|
||||
}
|
||||
|
||||
// Clone what we need for the async task
|
||||
let request_id_clone = request_id.clone();
|
||||
let service = data.sigsocket_service.clone();
|
||||
let signature_requests = data.signature_requests.clone();
|
||||
|
||||
// Spawn an async task to handle the signature request
|
||||
task::spawn(async move {
|
||||
info!("Starting async signature task for request: {}", request_id_clone);
|
||||
|
||||
// Update status to Processing
|
||||
{
|
||||
let mut requests = signature_requests.lock().unwrap();
|
||||
if let Some(request) = requests.get_mut(&request_id_clone) {
|
||||
request.update_status(SignatureStatus::Processing);
|
||||
}
|
||||
}
|
||||
|
||||
// Send the message to be signed via SigSocket
|
||||
info!("Sending message to SigSocket service for signing...");
|
||||
match service.send_to_sign(&public_key, &message_bytes).await {
|
||||
Ok((response_bytes, signature)) => {
|
||||
// Successfully received a signature
|
||||
let signature_base64 = base64::encode(&signature);
|
||||
let message_base64 = base64::encode(&message_bytes);
|
||||
|
||||
// Format in the expected dot-separated format: base64_message.base64_signature
|
||||
let full_signature = format!("{}.{}", message_base64, signature_base64);
|
||||
|
||||
info!("Successfully received signature response for request: {}", request_id_clone);
|
||||
info!("Message base64: {}", message_base64);
|
||||
info!("Signature base64: {}", signature_base64);
|
||||
info!("Full signature (dot format): {}", full_signature);
|
||||
|
||||
// Update the signature request with the successful result
|
||||
let mut requests = signature_requests.lock().unwrap();
|
||||
if let Some(request) = requests.get_mut(&request_id_clone) {
|
||||
request.set_success(signature_base64);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
// Error occurred
|
||||
error!("Error during signature process for request {}: {:?}", request_id_clone, err);
|
||||
|
||||
// Update the signature request with the error
|
||||
let mut requests = signature_requests.lock().unwrap();
|
||||
if let Some(request) = requests.get_mut(&request_id_clone) {
|
||||
request.set_error(format!("Error: {:?}", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return JSON response if it's an AJAX request, otherwise redirect
|
||||
if is_ajax_request(&form) {
|
||||
// Return JSON response for AJAX requests
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(json!({
|
||||
"status": "pending",
|
||||
"requestId": request_id,
|
||||
"message": "Signature request added to queue"
|
||||
}))
|
||||
} else {
|
||||
// Redirect back to the index page
|
||||
HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/"))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if this is an AJAX request
|
||||
fn is_ajax_request(_form: &web::Form<SignRequest>) -> bool {
|
||||
// For simplicity, we'll always return false for now
|
||||
// In a real application, you would check headers like X-Requested-With
|
||||
false
|
||||
}
|
||||
|
||||
// WebSocket handler for SigSocket connections
|
||||
async fn websocket_handler(
|
||||
req: actix_web::HttpRequest,
|
||||
stream: actix_web::web::Payload,
|
||||
service: web::Data<Arc<SigSocketService>>,
|
||||
) -> Result<HttpResponse> {
|
||||
// Create a new SigSocket handler
|
||||
let handler = service.create_websocket_handler();
|
||||
|
||||
// Start WebSocket connection
|
||||
ws::start(handler, &req, stream)
|
||||
}
|
||||
|
||||
// Status endpoint for SigSocket server
|
||||
async fn status_endpoint(service: web::Data<Arc<SigSocketService>>) -> impl Responder {
|
||||
// Get the connection count
|
||||
match service.connection_count() {
|
||||
Ok(count) => {
|
||||
// Return JSON response with status info
|
||||
web::Json(json!({
|
||||
"status": "online",
|
||||
"active_connections": count,
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
}))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error getting connection count: {:?}", e);
|
||||
// Return error status
|
||||
web::Json(json!({
|
||||
"status": "error",
|
||||
"error": format!("{:?}", e),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get status of a specific signature request or all requests
|
||||
async fn signature_status(
|
||||
data: web::Data<AppState>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> impl Responder {
|
||||
let request_id = &path.0;
|
||||
|
||||
// If the request_id is "all", return all requests
|
||||
if request_id == "all" {
|
||||
let signature_requests = data.signature_requests.lock().unwrap();
|
||||
|
||||
// Convert the pending signatures to results for the API
|
||||
let results: Vec<SignResult> = signature_requests.values()
|
||||
.map(|sig| sig.to_result())
|
||||
.collect();
|
||||
|
||||
return web::Json(json!({
|
||||
"status": "success",
|
||||
"count": results.len(),
|
||||
"requests": results
|
||||
}));
|
||||
}
|
||||
|
||||
// Otherwise, find the specific request
|
||||
let signature_requests = data.signature_requests.lock().unwrap();
|
||||
|
||||
if let Some(request) = signature_requests.get(request_id) {
|
||||
web::Json(json!({
|
||||
"status": "success",
|
||||
"request": request.to_result()
|
||||
}))
|
||||
} else {
|
||||
web::Json(json!({
|
||||
"status": "error",
|
||||
"message": format!("No signature request found with ID: {}", request_id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a signature request
|
||||
async fn delete_signature(
|
||||
data: web::Data<AppState>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> impl Responder {
|
||||
let request_id = &path.0;
|
||||
|
||||
let mut signature_requests = data.signature_requests.lock().unwrap();
|
||||
|
||||
if let Some(_) = signature_requests.remove(request_id) {
|
||||
web::Json(json!({
|
||||
"status": "success",
|
||||
"message": format!("Signature request {} deleted", request_id)
|
||||
}))
|
||||
} else {
|
||||
web::Json(json!({
|
||||
"status": "error",
|
||||
"message": format!("No signature request found with ID: {}", request_id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Task to check for timed-out signature requests
|
||||
async fn check_timeouts(signature_requests: Arc<Mutex<HashMap<String, PendingSignature>>>) {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
|
||||
// Check for timed-out requests
|
||||
let mut requests = signature_requests.lock().unwrap();
|
||||
let timed_out: Vec<String> = requests.iter()
|
||||
.filter(|(_, req)| req.status == SignatureStatus::Pending || req.status == SignatureStatus::Processing)
|
||||
.filter(|(_, req)| req.is_timed_out())
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect();
|
||||
|
||||
// Update timed-out requests
|
||||
for id in timed_out {
|
||||
if let Some(req) = requests.get_mut(&id) {
|
||||
req.error = Some("Request timed out waiting for signature".to_string());
|
||||
req.update_status(SignatureStatus::Timeout);
|
||||
info!("Signature request {} timed out", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
// Initialize SigSocket registry and service
|
||||
let registry = Arc::new(RwLock::new(ConnectionRegistry::new()));
|
||||
let sigsocket_service = Arc::new(SigSocketService::new(registry.clone()));
|
||||
|
||||
// Initialize signature requests tracking
|
||||
let signature_requests = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
// Start the timeout checking task
|
||||
let timeout_checker_requests = signature_requests.clone();
|
||||
tokio::spawn(async move {
|
||||
check_timeouts(timeout_checker_requests).await;
|
||||
});
|
||||
|
||||
// Shared application state
|
||||
let app_state = web::Data::new(AppState {
|
||||
templates: tera,
|
||||
sigsocket_service: sigsocket_service.clone(),
|
||||
signature_requests: signature_requests.clone(),
|
||||
});
|
||||
|
||||
info!("Web App server starting on http://127.0.0.1:8080");
|
||||
info!("SigSocket WebSocket endpoint available at ws://127.0.0.1:8080/ws");
|
||||
|
||||
// Start the web server with both our regular routes and the SigSocket WebSocket handler
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(app_state.clone())
|
||||
.app_data(web::Data::new(sigsocket_service.clone()))
|
||||
// Regular web app routes
|
||||
.route("/", web::get().to(index))
|
||||
.route("/sign", web::post().to(sign))
|
||||
// SigSocket WebSocket handler
|
||||
.route("/ws", web::get().to(websocket_handler))
|
||||
// Status endpoints
|
||||
.route("/sigsocket/status", web::get().to(status_endpoint))
|
||||
// Signature API endpoints
|
||||
.route("/api/signatures/{id}", web::get().to(signature_status))
|
||||
.route("/api/signatures/{id}", web::delete().to(delete_signature))
|
||||
// Static files
|
||||
.service(fs::Files::new("/static", "./static"))
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
Reference in New Issue
Block a user