final db models wip
This commit is contained in:
parent
abbed9a1a1
commit
c3f6b91aa0
@ -8,16 +8,6 @@ pub trait BaseModelDataOps: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
fn set_base_created_at(mut self, time: i64) -> Self {
|
||||
self.get_base_data_mut().created_at = time;
|
||||
self
|
||||
}
|
||||
|
||||
fn set_base_modified_at(mut self, time: i64) -> Self {
|
||||
self.get_base_data_mut().modified_at = time;
|
||||
self
|
||||
}
|
||||
|
||||
fn add_base_comment(mut self, comment_id: u32) -> Self {
|
||||
self.get_base_data_mut().comments.push(comment_id);
|
||||
self
|
||||
|
@ -118,7 +118,7 @@ pub trait Index {
|
||||
/// // Retrieve the model with the assigned ID
|
||||
/// let db_user = db.collection().get_by_id(user_id).expect("Failed to get user");
|
||||
/// ```
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
|
||||
pub struct BaseModelData {
|
||||
/// Unique incremental ID - will be auto-generated by OurDB
|
||||
///
|
||||
|
151
prompts/new_rhai_rs_gen.md
Normal file
151
prompts/new_rhai_rs_gen.md
Normal file
@ -0,0 +1,151 @@
|
||||
# AI Prompt: Generate Rhai Module Integration for Rust Models
|
||||
|
||||
## Context
|
||||
You are tasked with creating a Rhai scripting integration for Rust models in the heromodels crate. The integration should follow the builder pattern and provide a comprehensive set of functions for interacting with the models through Rhai scripts.
|
||||
|
||||
## Key Concepts to Understand
|
||||
1. **Builder Pattern vs. Rhai Mutation**: Rust models use builder pattern (methods consume self and return Self), but Rhai requires functions that mutate in place (&mut self).
|
||||
2. **Database Access**: Database functions are exposed as global functions in a 'db' module namespace (e.g., `db::save_flow`, not `db.save_flow`).
|
||||
3. **ID Handling**: New objects should use ID 0 to allow database assignment of real IDs.
|
||||
4. **Function vs. Method Calls**: Rhai scripts must use function calls (e.g., `status(flow, STATUS_DRAFT)`) not method chaining (e.g., `flow.status(STATUS_DRAFT)`).
|
||||
|
||||
## Requirements
|
||||
|
||||
1. **Module Structure and Imports**:
|
||||
- Create a `rhai.rs` file that exports a function named `register_[model]_rhai_module` which takes an Engine and an Arc<OurDB>
|
||||
- **IMPORTANT**: Import `heromodels_core::Model` trait for access to methods like `get_id`
|
||||
- Use the `#[export_module]` macro to define the Rhai module
|
||||
- Register the module globally with the engine using the correct module registration syntax
|
||||
- Create separate database functions that capture the database reference
|
||||
|
||||
2. **Builder Pattern Integration**:
|
||||
- For each model struct marked with `#[model]`, create constructor functions (e.g., `new_[model]`) that accept ID 0 for new objects
|
||||
- For each builder method in the model, create a corresponding Rhai function that:
|
||||
- Takes a mutable reference to the model as first parameter
|
||||
- Uses `std::mem::take` to get ownership (if the model implements Default) or `std::mem::replace` (if it doesn't)
|
||||
- Calls the builder method on the owned object
|
||||
- Assigns the result back to the mutable reference
|
||||
- Returns a clone of the modified object
|
||||
|
||||
3. **Field Access**:
|
||||
- **IMPORTANT**: Do NOT create getter methods for fields that can be accessed directly
|
||||
- Use direct field access in example code (e.g., `flow.name`, not `flow.get_name()`)
|
||||
- Only create getter functions for computed properties or when special conversion is needed
|
||||
- Handle special types appropriately (e.g., convert timestamps, IDs, etc.)
|
||||
|
||||
4. **Database Functions**:
|
||||
- For each model, implement the following database functions in the `db` module namespace:
|
||||
- `db::save_[model]`: Save the model to the database and return the saved model with assigned ID
|
||||
- `db::get_[model]_by_id`: Retrieve a model by ID
|
||||
- `db::delete_[model]`: Delete a model from the database
|
||||
- `db::list_[model]s`: List all models of this type
|
||||
- **IMPORTANT**: Use `db::function_name` syntax in scripts, not `db.function_name`
|
||||
- Handle errors properly with detailed error messages using debug format `{:?}` for error objects
|
||||
- Convert between Rhai's i64 and Rust's u32/u64 types for IDs and timestamps
|
||||
- Return saved objects from save functions to ensure proper ID handling
|
||||
|
||||
5. **Error Handling**:
|
||||
- Use `Box<EvalAltResult>` for error returns
|
||||
- Provide detailed error messages with proper position information
|
||||
- **IMPORTANT**: Use `context.call_position()` not the deprecated `context.position()`
|
||||
- Handle type conversions safely
|
||||
- Format error messages with debug format for error objects: `format!("Error: {:?}", err)`
|
||||
|
||||
6. **Type Conversions**:
|
||||
- Create helper functions for converting between Rhai and Rust types
|
||||
- Handle special cases like enums with string representations
|
||||
|
||||
## Implementation Pattern
|
||||
|
||||
```rust
|
||||
// Helper functions for type conversion
|
||||
fn i64_to_u32(val: i64, position: Position, param_name: &str, function_name: &str) -> Result<u32, Box<EvalAltResult>> {
|
||||
u32::try_from(val).map_err(|_| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!("{} must be a positive integer less than 2^32 in {}", param_name, function_name).into(),
|
||||
position
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// Model constructor
|
||||
#[rhai_fn(name = "new_[model]")]
|
||||
pub fn new_model() -> Model {
|
||||
Model::new()
|
||||
}
|
||||
|
||||
// Builder method integration
|
||||
#[rhai_fn(name = "[method_name]", return_raw, global, pure)]
|
||||
pub fn model_method(model: &mut Model, param: Type) -> Result<Model, Box<EvalAltResult>> {
|
||||
let owned_model = std::mem::take(model); // or replace if Default not implemented
|
||||
*model = owned_model.method_name(param);
|
||||
Ok(model.clone())
|
||||
}
|
||||
|
||||
// Getter method
|
||||
#[rhai_fn(name = "[field]", global)]
|
||||
pub fn get_model_field(model: &mut Model) -> Type {
|
||||
model.field.clone()
|
||||
}
|
||||
|
||||
// Database function
|
||||
db_module.set_native_fn("save_[model]", move |model: Model| -> Result<Model, Box<EvalAltResult>> {
|
||||
db_clone.set(&model)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error: {}", e).into(), Position::NONE)))
|
||||
.map(|(_, model)| model)
|
||||
});
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. All models must follow the builder pattern where methods take ownership of `self` and return `Self`
|
||||
2. If a model doesn't implement `Default`, use `std::mem::replace` instead of `std::mem::take`
|
||||
3. Ensure proper error handling for all operations
|
||||
4. Register all functions with appropriate Rhai names
|
||||
5. Follow the naming conventions established in existing modules
|
||||
6. Ensure all database operations are properly wrapped with error handling
|
||||
7. **CRITICAL**: When creating example scripts, use function calls not method chaining
|
||||
8. **CRITICAL**: Always use ID 0 for new objects in scripts to allow database assignment
|
||||
9. **CRITICAL**: After saving objects, use the returned objects with assigned IDs for further operations
|
||||
10. **CRITICAL**: In Rust examples, include the Model trait import and use direct field access
|
||||
|
||||
## Example Rhai Script
|
||||
|
||||
```rhai
|
||||
// Create a new flow with ID 0 (database will assign real ID)
|
||||
let flow = new_flow(0, "12345678-1234-1234-1234-123456789012");
|
||||
name(flow, "Document Approval Flow");
|
||||
status(flow, STATUS_DRAFT);
|
||||
|
||||
// Save flow to database and get saved object with assigned ID
|
||||
let saved_flow = db::save_flow(flow);
|
||||
print(`Flow saved to database with ID: ${get_flow_id(saved_flow)}`);
|
||||
|
||||
// Retrieve flow from database using assigned ID
|
||||
let retrieved_flow = db::get_flow_by_id(get_flow_id(saved_flow));
|
||||
```
|
||||
|
||||
## Example Rust Code
|
||||
|
||||
```rust
|
||||
use heromodels_core::Model; // Import Model trait for get_id()
|
||||
|
||||
// Create flow using Rhai
|
||||
let flow: Flow = engine.eval("new_flow(0, \"12345678-1234-1234-1234-123456789012\")").unwrap();
|
||||
|
||||
// Access fields directly
|
||||
println!("Flow name: {}", flow.name);
|
||||
|
||||
// Save to database using Rhai function
|
||||
let save_script = "fn save_it(f) { return db::save_flow(f); }";
|
||||
let save_ast = engine.compile(save_script).unwrap();
|
||||
let saved_flow: Flow = engine.call_fn(&mut scope, &save_ast, "save_it", (flow,)).unwrap();
|
||||
println!("Saved flow ID: {}", saved_flow.get_id());
|
||||
```
|
||||
|
||||
## Reference Implementations
|
||||
See the existing implementations for examples:
|
||||
1. `heromodels/src/models/calendar/rhai.rs` - Calendar module Rhai bindings
|
||||
2. `heromodels/src/models/flow/rhai.rs` - Flow module Rhai bindings
|
||||
3. `heromodels_rhai/examples/flow/flow_script.rhai` - Example Rhai script
|
||||
4. `heromodels_rhai/examples/flow/example.rs` - Example Rust code
|
298
rhai_client_example/Cargo.lock
generated
Normal file
298
rhai_client_example/Cargo.lock
generated
Normal file
@ -0,0 +1,298 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"instant",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_client_example"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rhai",
|
||||
"rhai_client_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_client_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rhai",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
8
rhai_client_example/Cargo.toml
Normal file
8
rhai_client_example/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "rhai_client_example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rhai = "1.21.0"
|
||||
rhai_client_macros = { path = "../rhai_client_macros" }
|
62
rhai_client_example/src/main.rs
Normal file
62
rhai_client_example/src/main.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use rhai::Engine;
|
||||
use rhai_client_macros::rhai;
|
||||
|
||||
// Define a Rust function with the #[rhai] attribute
|
||||
#[rhai]
|
||||
fn hello(name: String) -> String {
|
||||
format!("Hello, {}!", name)
|
||||
}
|
||||
|
||||
// Define another function with multiple parameters
|
||||
#[rhai]
|
||||
fn add(a: i32, b: i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
|
||||
// Define a function with different parameter types
|
||||
#[rhai]
|
||||
fn format_person(name: String, age: i32, is_student: bool) -> String {
|
||||
let student_status = if is_student { "is" } else { "is not" };
|
||||
format!("{} is {} years old and {} a student.", name, age, student_status)
|
||||
}
|
||||
|
||||
// Define adapter functions for Rhai that handle i64 to i32 conversion
|
||||
fn add_rhai(a: i64, b: i64) -> i64 {
|
||||
add(a as i32, b as i32) as i64
|
||||
}
|
||||
|
||||
fn format_person_rhai(name: String, age: i64, is_student: bool) -> String {
|
||||
format_person(name, age as i32, is_student)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Create a Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register our functions with the Rhai engine
|
||||
// Note: Rhai uses i64 for integers by default
|
||||
engine.register_fn("hello", hello);
|
||||
engine.register_fn("add", add_rhai);
|
||||
engine.register_fn("format_person", format_person_rhai);
|
||||
|
||||
println!("=== Calling Rust functions directly ===");
|
||||
println!("{}", hello("World".to_string()));
|
||||
println!("{}", add(5, 10));
|
||||
println!("{}", format_person("Alice".to_string(), 25, true));
|
||||
|
||||
println!("\n=== Calling functions through Rhai engine ===");
|
||||
let result1: String = engine.eval("hello(\"Rhai World\")").unwrap();
|
||||
println!("{}", result1);
|
||||
|
||||
let result2: i64 = engine.eval("add(20, 30)").unwrap();
|
||||
println!("{}", result2);
|
||||
|
||||
let result3: String = engine.eval("format_person(\"Bob\", 30, false)").unwrap();
|
||||
println!("{}", result3);
|
||||
|
||||
println!("\n=== Calling functions through generated Rhai client functions ===");
|
||||
// Use the generated client functions
|
||||
println!("{}", hello_rhai_client(&engine, "Client World".to_string()));
|
||||
println!("{}", add_rhai_client(&engine, 100, 200));
|
||||
println!("{}", format_person_rhai_client(&engine, "Charlie".to_string(), 35, true));
|
||||
}
|
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rustfmt", "clippy"]
|
401
sigsocket_architecture.md
Normal file
401
sigsocket_architecture.md
Normal file
@ -0,0 +1,401 @@
|
||||
# SigSocket: WebSocket Signing Server Architecture
|
||||
|
||||
Based on my analysis of the existing Actix application structure, I've designed a comprehensive architecture for implementing a WebSocket server that handles signing operations. This server will integrate seamlessly with the existing hostbasket application.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
SigSocket will:
|
||||
- Accept WebSocket connections from clients
|
||||
- Allow clients to identify themselves with a public key
|
||||
- Provide a `send_to_sign()` function that takes a public key and a message
|
||||
- Forward the message to the appropriate client for signing
|
||||
- Wait for a signed response (with a 1-minute timeout)
|
||||
- Verify the signature using the client's public key
|
||||
- Return the response message and signature
|
||||
|
||||
## 2. Component Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Actix Web Server] --> B[SigSocket Manager]
|
||||
B --> C[Connection Registry]
|
||||
B --> D[Message Handler]
|
||||
D --> E[Signature Verifier]
|
||||
F[Client] <--> B
|
||||
G[Application Code] --> H[SigSocketService]
|
||||
H --> B
|
||||
```
|
||||
|
||||
### Key Components:
|
||||
|
||||
1. **SigSocket Manager**
|
||||
- Handles WebSocket connections
|
||||
- Manages connection lifecycle
|
||||
- Routes messages to appropriate handlers
|
||||
|
||||
2. **Connection Registry**
|
||||
- Maps public keys to active WebSocket connections
|
||||
- Handles connection tracking and cleanup
|
||||
- Provides lookup functionality
|
||||
|
||||
3. **Message Handler**
|
||||
- Processes incoming messages
|
||||
- Implements the message protocol
|
||||
- Manages timeouts for responses
|
||||
|
||||
4. **Signature Verifier**
|
||||
- Verifies signatures using public keys
|
||||
- Implements cryptographic operations
|
||||
- Ensures security of the signing process
|
||||
|
||||
5. **SigSocket Service**
|
||||
- Provides a clean API for the application to use
|
||||
- Abstracts WebSocket complexity from business logic
|
||||
- Handles error cases and timeouts
|
||||
|
||||
## 3. Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib.rs # Main library exports
|
||||
├── manager.rs # WebSocket connection manager
|
||||
├── registry.rs # Connection registry
|
||||
├── handler.rs # Message handling logic
|
||||
├── protocol.rs # Message protocol definitions
|
||||
├── crypto.rs # Cryptographic operations
|
||||
└── service.rs # Service API
|
||||
```
|
||||
|
||||
## 4. Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant SigSocketManager
|
||||
participant Registry
|
||||
participant Application
|
||||
participant SigSocketService
|
||||
|
||||
Client->>SigSocketManager: Connect
|
||||
Client->>SigSocketManager: Introduce(public_key)
|
||||
SigSocketManager->>Registry: Register(connection, public_key)
|
||||
|
||||
Application->>SigSocketService: send_to_sign(public_key, message)
|
||||
SigSocketService->>Registry: Lookup(public_key)
|
||||
Registry-->>SigSocketService: connection
|
||||
SigSocketService->>SigSocketManager: Send message to connection
|
||||
SigSocketManager->>Client: Message to sign
|
||||
|
||||
Client->>SigSocketManager: Signed response
|
||||
SigSocketManager->>SigSocketService: Forward response
|
||||
SigSocketService->>SigSocketService: Verify signature
|
||||
SigSocketService-->>Application: Return verified response
|
||||
```
|
||||
|
||||
## 5. Message Protocol
|
||||
|
||||
We'll define a minimalist protocol for communication:
|
||||
|
||||
```
|
||||
// Client introduction (first message upon connection)
|
||||
<base64_encoded_public_key>
|
||||
|
||||
// Sign request (sent from server to client)
|
||||
<base64_encoded_message>
|
||||
|
||||
// Sign response (sent from client to server)
|
||||
<base64_encoded_message>.<base64_encoded_signature>
|
||||
```
|
||||
|
||||
This simplified protocol reduces overhead and complexity:
|
||||
- The introduction is always the first message, containing only the public key
|
||||
- Sign requests contain only the message to be signed
|
||||
- Responses use a simple "message.signature" format
|
||||
|
||||
## 6. Required Dependencies
|
||||
|
||||
We'll need to add the following dependencies to the project:
|
||||
|
||||
```toml
|
||||
# WebSocket support
|
||||
actix-web-actors = "4.2.0"
|
||||
|
||||
# Cryptography
|
||||
secp256k1 = "0.28.0" # For secp256k1 signatures (used in Bitcoin/Ethereum)
|
||||
sha2 = "0.10.8" # For hashing before signing
|
||||
hex = "0.4.3" # For hex encoding/decoding
|
||||
base64 = "0.21.0" # For base64 encoding/decoding
|
||||
rand = "0.8.5" # For generating random data
|
||||
```
|
||||
|
||||
## 7. Implementation Details
|
||||
|
||||
### 7.1 SigSocket Manager
|
||||
|
||||
The SigSocket Manager will handle the lifecycle of WebSocket connections:
|
||||
|
||||
```rust
|
||||
pub struct SigSocketManager {
|
||||
registry: Arc<RwLock<ConnectionRegistry>>,
|
||||
}
|
||||
|
||||
impl Actor for SigSocketManager {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// Handle connection start
|
||||
}
|
||||
|
||||
fn stopped(&mut self, ctx: &mut Self::Context) {
|
||||
// Handle connection close and cleanup
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for SigSocketManager {
|
||||
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||
// Handle different types of WebSocket messages
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Connection Registry
|
||||
|
||||
The Connection Registry will maintain a mapping of public keys to active connections:
|
||||
|
||||
```rust
|
||||
pub struct ConnectionRegistry {
|
||||
connections: HashMap<String, Addr<SigSocketManager>>,
|
||||
}
|
||||
|
||||
impl ConnectionRegistry {
|
||||
pub fn register(&mut self, public_key: String, addr: Addr<SigSocketManager>) {
|
||||
self.connections.insert(public_key, addr);
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, public_key: &str) {
|
||||
self.connections.remove(public_key);
|
||||
}
|
||||
|
||||
pub fn get(&self, public_key: &str) -> Option<&Addr<SigSocketManager>> {
|
||||
self.connections.get(public_key)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 SigSocket Service
|
||||
|
||||
The SigSocket Service will provide a clean API for controllers:
|
||||
|
||||
```rust
|
||||
pub struct SigSocketService {
|
||||
registry: Arc<RwLock<ConnectionRegistry>>,
|
||||
}
|
||||
|
||||
impl SigSocketService {
|
||||
pub async fn send_to_sign(&self, public_key: &str, message: &[u8])
|
||||
-> Result<(Vec<u8>, Vec<u8>), SigSocketError> {
|
||||
|
||||
// 1. Find the connection for the public key
|
||||
let connection = self.registry.read().await.get(public_key).cloned();
|
||||
|
||||
if let Some(conn) = connection {
|
||||
// 2. Create a response channel
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// 3. Register the response channel with the connection
|
||||
self.pending_requests.write().await.insert(conn.clone(), tx);
|
||||
|
||||
// 4. Send the message to the client (just the raw message)
|
||||
conn.do_send(base64::encode(message));
|
||||
|
||||
// 5. Wait for the response with a timeout
|
||||
match tokio::time::timeout(Duration::from_secs(60), rx).await {
|
||||
Ok(Ok(response)) => {
|
||||
// 6. Parse the response in format "message.signature"
|
||||
let parts: Vec<&str> = response.split('.').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(SigSocketError::InvalidResponseFormat);
|
||||
}
|
||||
|
||||
let message_b64 = parts[0];
|
||||
let signature_b64 = parts[1];
|
||||
|
||||
// 7. Decode the message and signature
|
||||
let message_bytes = match base64::decode(message_b64) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return Err(SigSocketError::DecodingError),
|
||||
};
|
||||
|
||||
let signature_bytes = match base64::decode(signature_b64) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err(SigSocketError::DecodingError),
|
||||
};
|
||||
|
||||
// 8. Verify the signature
|
||||
if self.verify_signature(&signature_bytes, &message_bytes, public_key) {
|
||||
Ok((message_bytes, signature_bytes))
|
||||
} else {
|
||||
Err(SigSocketError::InvalidSignature)
|
||||
}
|
||||
},
|
||||
Ok(Err(_)) => Err(SigSocketError::ChannelClosed),
|
||||
Err(_) => Err(SigSocketError::Timeout),
|
||||
}
|
||||
} else {
|
||||
Err(SigSocketError::ConnectionNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Error Handling
|
||||
|
||||
We'll define a comprehensive error type for the SigSocket service:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SigSocketError {
|
||||
#[error("Connection not found for the provided public key")]
|
||||
ConnectionNotFound,
|
||||
|
||||
#[error("Timeout waiting for signature")]
|
||||
Timeout,
|
||||
|
||||
#[error("Invalid signature")]
|
||||
InvalidSignature,
|
||||
|
||||
#[error("Channel closed unexpectedly")]
|
||||
ChannelClosed,
|
||||
|
||||
#[error("Invalid response format, expected 'message.signature'")]
|
||||
InvalidResponseFormat,
|
||||
|
||||
#[error("Error decoding base64 message or signature")]
|
||||
DecodingError,
|
||||
|
||||
#[error("Invalid public key format")]
|
||||
InvalidPublicKey,
|
||||
|
||||
#[error("Invalid signature format")]
|
||||
InvalidSignature,
|
||||
|
||||
#[error("Internal cryptographic error")]
|
||||
InternalError,
|
||||
|
||||
#[error("WebSocket error: {0}")]
|
||||
WebSocketError(#[from] ws::ProtocolError),
|
||||
|
||||
#[error("Base64 decoding error: {0}")]
|
||||
Base64Error(#[from] base64::DecodeError),
|
||||
|
||||
#[error("Hex decoding error: {0}")]
|
||||
HexError(#[from] hex::FromHexError),
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Cryptographic Operations
|
||||
|
||||
The SigSocket service uses the secp256k1 elliptic curve for cryptographic operations, which is the same curve used in Bitcoin and Ethereum. This makes it compatible with many blockchain applications and wallets.
|
||||
|
||||
```rust
|
||||
use secp256k1::{Secp256k1, Message, PublicKey, Signature};
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
pub fn verify_signature(public_key_hex: &str, message: &[u8], signature_hex: &str) -> Result<bool, SigSocketError> {
|
||||
// 1. Parse the public key
|
||||
let public_key_bytes = hex::decode(public_key_hex)
|
||||
.map_err(|_| SigSocketError::InvalidPublicKey)?;
|
||||
|
||||
let public_key = PublicKey::from_slice(&public_key_bytes)
|
||||
.map_err(|_| SigSocketError::InvalidPublicKey)?;
|
||||
|
||||
// 2. Parse the signature
|
||||
let signature_bytes = hex::decode(signature_hex)
|
||||
.map_err(|_| SigSocketError::InvalidSignature)?;
|
||||
|
||||
let signature = Signature::from_compact(&signature_bytes)
|
||||
.map_err(|_| SigSocketError::InvalidSignature)?;
|
||||
|
||||
// 3. Hash the message (secp256k1 requires a 32-byte hash)
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(message);
|
||||
let message_hash = hasher.finalize();
|
||||
|
||||
// 4. Create a secp256k1 message from the hash
|
||||
let secp_message = Message::from_slice(&message_hash)
|
||||
.map_err(|_| SigSocketError::InternalError)?;
|
||||
|
||||
// 5. Verify the signature
|
||||
let secp = Secp256k1::verification_only();
|
||||
match secp.verify(&secp_message, &signature, &public_key) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This implementation:
|
||||
1. Decodes the hex-encoded public key and signature
|
||||
2. Hashes the message using SHA-256 (required for secp256k1)
|
||||
3. Verifies the signature using the secp256k1 library
|
||||
4. Returns a boolean indicating whether the signature is valid
|
||||
|
||||
## 10. Security Considerations
|
||||
|
||||
1. **Public Key Validation**: Validate public keys upon connection to ensure they are properly formatted secp256k1 keys
|
||||
2. **Message Authentication**: Consider adding a nonce or timestamp to prevent replay attacks
|
||||
3. **Rate Limiting**: Implement rate limiting to prevent DoS attacks
|
||||
4. **Connection Timeouts**: Automatically close inactive connections
|
||||
5. **Error Logging**: Log errors but avoid exposing sensitive information
|
||||
6. **Input Validation**: Validate all inputs to prevent injection attacks
|
||||
7. **Secure Hashing**: Always hash messages before signing to prevent length extension attacks
|
||||
|
||||
## 11. Testing Strategy
|
||||
|
||||
1. **Unit Tests**: Test individual components in isolation
|
||||
2. **Integration Tests**: Test the interaction between components
|
||||
3. **End-to-End Tests**: Test the complete flow from client connection to signature verification
|
||||
4. **Load Tests**: Test the system under high load to ensure stability
|
||||
5. **Security Tests**: Test for common security vulnerabilities
|
||||
|
||||
## 12. Integration with Existing Code
|
||||
|
||||
The SigSocketService can be used by any part of the application that needs signing functionality:
|
||||
|
||||
```rust
|
||||
// Create and initialize the service
|
||||
let registry = Arc::new(RwLock::new(ConnectionRegistry::new()));
|
||||
let sigsocket_service = Arc::new(SigSocketService::new(registry.clone()));
|
||||
|
||||
// Use the service to send a message for signing
|
||||
async fn sign_message(service: Arc<SigSocketService>, public_key: String, message: Vec<u8>) -> Result<(Vec<u8>, Vec<u8>), SigSocketError> {
|
||||
service.send_to_sign(&public_key, &message).await
|
||||
}
|
||||
|
||||
// Example usage in an HTTP handler
|
||||
async fn handle_sign_request(
|
||||
service: web::Data<Arc<SigSocketService>>,
|
||||
req: web::Json<SignRequest>,
|
||||
) -> HttpResponse {
|
||||
match service.send_to_sign(&req.public_key, &req.message).await {
|
||||
Ok((response, signature)) => {
|
||||
HttpResponse::Ok().json(json!({
|
||||
"response": base64::encode(response),
|
||||
"signature": base64::encode(signature),
|
||||
}))
|
||||
},
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(json!({
|
||||
"error": e.to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 13. Deployment Considerations
|
||||
|
||||
1. **Scalability**: The SigSocket server should be designed to scale horizontally
|
||||
2. **Monitoring**: Implement metrics for connection count, message throughput, and error rates
|
||||
3. **Logging**: Log important events for debugging and auditing
|
||||
4. **Documentation**: Document the API and protocol for client implementers
|
327
websocket/architecture.md
Normal file
327
websocket/architecture.md
Normal file
@ -0,0 +1,327 @@
|
||||
# WebSocket Signing Server Architecture Plan
|
||||
|
||||
Based on my analysis of the existing Actix application structure, I've designed a comprehensive architecture for implementing a WebSocket server that handles signing operations. This server will integrate seamlessly with the existing hostbasket application.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The WebSocket Signing Server will:
|
||||
- Accept WebSocket connections from clients
|
||||
- Allow clients to identify themselves with a public key
|
||||
- Provide a `send_to_sign()` function that takes a public key and a message
|
||||
- Forward the message to the appropriate client for signing
|
||||
- Wait for a signed response (with a 1-minute timeout)
|
||||
- Verify the signature using the client's public key
|
||||
- Return the response message and signature
|
||||
|
||||
## 2. Component Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Actix Web Server] --> B[WebSocket Manager]
|
||||
B --> C[Connection Registry]
|
||||
B --> D[Message Handler]
|
||||
D --> E[Signature Verifier]
|
||||
F[Client] <--> B
|
||||
G[Controllers] --> H[SigningService]
|
||||
H --> B
|
||||
```
|
||||
|
||||
### Key Components:
|
||||
|
||||
1. **WebSocket Manager**
|
||||
- Handles WebSocket connections
|
||||
- Manages connection lifecycle
|
||||
- Routes messages to appropriate handlers
|
||||
|
||||
2. **Connection Registry**
|
||||
- Maps public keys to active WebSocket connections
|
||||
- Handles connection tracking and cleanup
|
||||
- Provides lookup functionality
|
||||
|
||||
3. **Message Handler**
|
||||
- Processes incoming messages
|
||||
- Implements the message protocol
|
||||
- Manages timeouts for responses
|
||||
|
||||
4. **Signature Verifier**
|
||||
- Verifies signatures using public keys
|
||||
- Implements cryptographic operations
|
||||
- Ensures security of the signing process
|
||||
|
||||
5. **Signing Service**
|
||||
- Provides a clean API for controllers to use
|
||||
- Abstracts WebSocket complexity from business logic
|
||||
- Handles error cases and timeouts
|
||||
|
||||
## 3. Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── websocket/
|
||||
│ ├── mod.rs # Module exports
|
||||
│ ├── manager.rs # WebSocket connection manager
|
||||
│ ├── registry.rs # Connection registry
|
||||
│ ├── handler.rs # Message handling logic
|
||||
│ ├── protocol.rs # Message protocol definitions
|
||||
│ ├── crypto.rs # Cryptographic operations
|
||||
│ └── service.rs # Service API for controllers
|
||||
├── controllers/
|
||||
│ └── [existing controllers]
|
||||
│ └── websocket.rs # WebSocket controller (if needed)
|
||||
└── routes/
|
||||
└── mod.rs # Updated to include WebSocket routes
|
||||
```
|
||||
|
||||
## 4. Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant WebSocketManager
|
||||
participant Registry
|
||||
participant Controller
|
||||
participant SigningService
|
||||
|
||||
Client->>WebSocketManager: Connect
|
||||
Client->>WebSocketManager: Introduce(public_key)
|
||||
WebSocketManager->>Registry: Register(connection, public_key)
|
||||
|
||||
Controller->>SigningService: send_to_sign(public_key, message)
|
||||
SigningService->>Registry: Lookup(public_key)
|
||||
Registry-->>SigningService: connection
|
||||
SigningService->>WebSocketManager: Send message to connection
|
||||
WebSocketManager->>Client: Message to sign
|
||||
|
||||
Client->>WebSocketManager: Signed response
|
||||
WebSocketManager->>SigningService: Forward response
|
||||
SigningService->>SigningService: Verify signature
|
||||
SigningService-->>Controller: Return verified response
|
||||
```
|
||||
|
||||
## 5. Message Protocol
|
||||
|
||||
We'll define a simple JSON-based protocol for communication:
|
||||
|
||||
```json
|
||||
// Client introduction
|
||||
{
|
||||
"type": "introduction",
|
||||
"public_key": "base64_encoded_public_key"
|
||||
}
|
||||
|
||||
// Sign request
|
||||
{
|
||||
"type": "sign_request",
|
||||
"message": "base64_encoded_message",
|
||||
"request_id": "unique_request_id"
|
||||
}
|
||||
|
||||
// Sign response
|
||||
{
|
||||
"type": "sign_response",
|
||||
"request_id": "unique_request_id",
|
||||
"message": "base64_encoded_message",
|
||||
"signature": "base64_encoded_signature"
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Required Dependencies
|
||||
|
||||
We'll need to add the following dependencies to the project:
|
||||
|
||||
```toml
|
||||
# WebSocket support
|
||||
actix-web-actors = "4.2.0"
|
||||
|
||||
# Cryptography
|
||||
ed25519-dalek = "2.0.0" # For Ed25519 signatures
|
||||
base64 = "0.21.0" # For encoding/decoding
|
||||
rand = "0.8.5" # For generating random data
|
||||
```
|
||||
|
||||
## 7. Implementation Details
|
||||
|
||||
### 7.1 WebSocket Manager
|
||||
|
||||
The WebSocket Manager will handle the lifecycle of WebSocket connections:
|
||||
|
||||
```rust
|
||||
pub struct WebSocketManager {
|
||||
registry: Arc<RwLock<ConnectionRegistry>>,
|
||||
}
|
||||
|
||||
impl Actor for WebSocketManager {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// Handle connection start
|
||||
}
|
||||
|
||||
fn stopped(&mut self, ctx: &mut Self::Context) {
|
||||
// Handle connection close and cleanup
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketManager {
|
||||
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||
// Handle different types of WebSocket messages
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Connection Registry
|
||||
|
||||
The Connection Registry will maintain a mapping of public keys to active connections:
|
||||
|
||||
```rust
|
||||
pub struct ConnectionRegistry {
|
||||
connections: HashMap<String, Addr<WebSocketManager>>,
|
||||
}
|
||||
|
||||
impl ConnectionRegistry {
|
||||
pub fn register(&mut self, public_key: String, addr: Addr<WebSocketManager>) {
|
||||
self.connections.insert(public_key, addr);
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, public_key: &str) {
|
||||
self.connections.remove(public_key);
|
||||
}
|
||||
|
||||
pub fn get(&self, public_key: &str) -> Option<&Addr<WebSocketManager>> {
|
||||
self.connections.get(public_key)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Signing Service
|
||||
|
||||
The Signing Service will provide a clean API for controllers:
|
||||
|
||||
```rust
|
||||
pub struct SigningService {
|
||||
registry: Arc<RwLock<ConnectionRegistry>>,
|
||||
}
|
||||
|
||||
impl SigningService {
|
||||
pub async fn send_to_sign(&self, public_key: &str, message: &[u8])
|
||||
-> Result<(Vec<u8>, Vec<u8>), SigningError> {
|
||||
|
||||
// 1. Find the connection for the public key
|
||||
let connection = self.registry.read().await.get(public_key).cloned();
|
||||
|
||||
if let Some(conn) = connection {
|
||||
// 2. Generate a unique request ID
|
||||
let request_id = Uuid::new_v4().to_string();
|
||||
|
||||
// 3. Create a response channel
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// 4. Register the response channel
|
||||
self.pending_requests.write().await.insert(request_id.clone(), tx);
|
||||
|
||||
// 5. Send the message to the client
|
||||
let sign_request = SignRequest {
|
||||
request_id: request_id.clone(),
|
||||
message: message.to_vec(),
|
||||
};
|
||||
|
||||
conn.do_send(sign_request);
|
||||
|
||||
// 6. Wait for the response with a timeout
|
||||
match tokio::time::timeout(Duration::from_secs(60), rx).await {
|
||||
Ok(Ok(response)) => {
|
||||
// 7. Verify the signature
|
||||
if self.verify_signature(&response.signature, message, public_key) {
|
||||
Ok((response.message, response.signature))
|
||||
} else {
|
||||
Err(SigningError::InvalidSignature)
|
||||
}
|
||||
},
|
||||
Ok(Err(_)) => Err(SigningError::ChannelClosed),
|
||||
Err(_) => Err(SigningError::Timeout),
|
||||
}
|
||||
} else {
|
||||
Err(SigningError::ConnectionNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Error Handling
|
||||
|
||||
We'll define a comprehensive error type for the signing service:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SigningError {
|
||||
#[error("Connection not found for the provided public key")]
|
||||
ConnectionNotFound,
|
||||
|
||||
#[error("Timeout waiting for signature")]
|
||||
Timeout,
|
||||
|
||||
#[error("Invalid signature")]
|
||||
InvalidSignature,
|
||||
|
||||
#[error("Channel closed unexpectedly")]
|
||||
ChannelClosed,
|
||||
|
||||
#[error("WebSocket error: {0}")]
|
||||
WebSocketError(#[from] ws::ProtocolError),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
SerializationError(#[from] serde_json::Error),
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Security Considerations
|
||||
|
||||
1. **Public Key Validation**: Validate public keys upon connection to ensure they are properly formatted
|
||||
2. **Message Authentication**: Consider adding a nonce or timestamp to prevent replay attacks
|
||||
3. **Rate Limiting**: Implement rate limiting to prevent DoS attacks
|
||||
4. **Connection Timeouts**: Automatically close inactive connections
|
||||
5. **Error Logging**: Log errors but avoid exposing sensitive information
|
||||
6. **Input Validation**: Validate all inputs to prevent injection attacks
|
||||
|
||||
## 10. Testing Strategy
|
||||
|
||||
1. **Unit Tests**: Test individual components in isolation
|
||||
2. **Integration Tests**: Test the interaction between components
|
||||
3. **End-to-End Tests**: Test the complete flow from client connection to signature verification
|
||||
4. **Load Tests**: Test the system under high load to ensure stability
|
||||
5. **Security Tests**: Test for common security vulnerabilities
|
||||
|
||||
## 11. Integration with Existing Controllers
|
||||
|
||||
Controllers can use the SigningService through dependency injection:
|
||||
|
||||
```rust
|
||||
pub struct SomeController {
|
||||
signing_service: Arc<SigningService>,
|
||||
}
|
||||
|
||||
impl SomeController {
|
||||
pub async fn some_action(&self, public_key: String, message: Vec<u8>) -> HttpResponse {
|
||||
match self.signing_service.send_to_sign(&public_key, &message).await {
|
||||
Ok((response, signature)) => {
|
||||
HttpResponse::Ok().json(json!({
|
||||
"response": base64::encode(response),
|
||||
"signature": base64::encode(signature),
|
||||
}))
|
||||
},
|
||||
Err(e) => {
|
||||
HttpResponse::InternalServerError().json(json!({
|
||||
"error": e.to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 12. Deployment Considerations
|
||||
|
||||
1. **Scalability**: The WebSocket server should be designed to scale horizontally
|
||||
2. **Monitoring**: Implement metrics for connection count, message throughput, and error rates
|
||||
3. **Logging**: Log important events for debugging and auditing
|
||||
4. **Documentation**: Document the API and protocol for client implementers
|
Loading…
Reference in New Issue
Block a user