Skip to content

Commit

Permalink
Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
levkk committed Nov 22, 2024
1 parent 6b435f5 commit 6ceb026
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 13 deletions.
12 changes: 12 additions & 0 deletions rwf/src/comms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ use thiserror::Error;
use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender};
use tracing::debug;

/// Error returned by comms.
#[derive(Error, Debug)]
pub enum Error {
/// Error sending message through Tokio channel.
#[error("{0}")]
SendError(#[from] SendError<Message>),
}
Expand Down Expand Up @@ -68,11 +70,13 @@ impl Websocket {
}
}

/// Global messages channel.
pub struct Messages {
websocket: Arc<Mutex<HashMap<SessionId, Websocket>>>,
}

impl Messages {
/// Create new messages channel.
pub fn new() -> Self {
Self {
websocket: Arc::new(Mutex::new(HashMap::new())),
Expand All @@ -84,10 +88,12 @@ impl Messages {
self.websocket.lock().remove(session_id);
}

/// Check that a session has an active WebSocket connection.
pub fn websocket_connected(&self, session_id: &SessionId) -> bool {
self.websocket.lock().get(session_id).is_some()
}

/// Get a websocket message receiver. All messages sent from clients will be sent to the receiver.
pub fn websocket_receiver(&self, session_id: &SessionId, _topic: &str) -> WebsocketReceiver {
let mut guard = self.websocket.lock();
let entry = guard
Expand All @@ -101,6 +107,8 @@ impl Messages {
}
}

/// Get a websocket message sender. This allows to send messages to all websocket connections
/// that this session has.
pub fn websocket_sender(&self, session_id: &SessionId, _topic: &str) -> WebsocketSender {
let mut guard = self.websocket.lock();
let entry = guard
Expand All @@ -111,6 +119,7 @@ impl Messages {
}
}

/// Get a websocket message sender that will send messages to all _other_ sessions.
pub fn websocket_broadcast(&self, session_id: &SessionId, _topic: &str) -> Broadcast {
let guard = self.websocket.lock();
let entries = guard
Expand All @@ -122,6 +131,7 @@ impl Messages {
Broadcast { everyone: entries }
}

/// Get a websocket message sender that will send messages to _everyone_ connected.
pub fn websocket_notify(&self, _topic: &str) -> Broadcast {
let guard = self.websocket.lock();
let entries = guard
Expand All @@ -133,12 +143,14 @@ impl Messages {
}
}

/// WebSocket message sender.
#[derive(Debug)]
pub struct WebsocketSender {
sender: Sender<Message>,
}

impl WebsocketSender {
/// Send a message via WebSocket connection.
pub fn send(&self, message: impl ToMessage) -> Result<usize, Error> {
Ok(self.sender.send(message.to_message())?)
}
Expand Down
78 changes: 74 additions & 4 deletions rwf/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Cryptography wrappers, using AES-128.
//! Cryptographic primitives, wrapped in a simple interface.
//!
//! Can encrypt/decrypt arbitrary data using the application secret key.
//! The cipher used is AES-128.
use aes_gcm_siv::{
aead::{Aead, KeyInit},
Aes128GcmSiv, Nonce,
Expand All @@ -13,17 +13,22 @@ use time::OffsetDateTime;

use crate::config::get_config;

/// Errors returned by the crypto implementation.
#[derive(Error, Debug)]
pub enum Error {
/// JSON (de)serialization failed.
#[error("json error: {0}")]
Json(#[from] serde_json::Error),

/// Base64 (de)serialization failed.
#[error("base64 error: {0}")]
Base64(#[from] base64::DecodeError),

/// AES cipher failed.
#[error("aes error: {0}")]
AesError(aes_gcm_siv::Error),

/// Some other error happened. See contents for description.
#[error("{0}")]
Generic(&'static str),
}
Expand Down Expand Up @@ -63,7 +68,7 @@ impl Encrypted {
}
}

/// Encrypt some bytes using the global configured encryption key.
/// Encrypt data using the application secret key.
///
/// # Example
///
Expand All @@ -86,6 +91,18 @@ pub fn encrypt(data: &[u8]) -> Result<String, Error> {
Encrypted { ciphertext, nonce }.to_bytes()
}

/// Decrypt data encrypted with the application secret key.
///
/// # Example
///
/// ```
/// use rwf::crypto::{encrypt, decrypt};
///
/// let cipher = encrypt("super secret".as_bytes()).unwrap();
/// let plain = decrypt(&cipher).unwrap();
///
/// assert_eq!(plain, "super secret".as_bytes());
/// ```
pub fn decrypt(data: &str) -> Result<Vec<u8>, Error> {
let config = get_config();
let encrypted = Encrypted::from_base64(data)?;
Expand All @@ -98,6 +115,17 @@ pub fn decrypt(data: &str) -> Result<Vec<u8>, Error> {
Ok(plaintext)
}

/// Encrypt an integer using the application secret key and return
/// a user-friendly representation. The number can be used in URLs to hide
/// an identifier for a resource.
///
/// # Example
///
/// ```
/// use rwf::crypto::encrypt_number;
///
/// let id = encrypt_number(1234).unwrap();
/// ```
pub fn encrypt_number(n: i64) -> Result<String, Error> {
let config = get_config();
let nonce = nonce();
Expand Down Expand Up @@ -132,6 +160,18 @@ pub fn encrypt_number(n: i64) -> Result<String, Error> {
Ok(uuid.join("-"))
}

/// Decrypt an integer encrypted using the application secret key.
///
/// # Example
///
/// ```
/// use rwf::crypto::{encrypt_number, decrypt_number};
///
/// let id = encrypt_number(1234).unwrap();
/// let id = decrypt_number(&id).unwrap();
///
/// assert_eq!(id, 1234);
/// ```
pub fn decrypt_number(s: &str) -> Result<i64, Error> {
let config = get_config();

Expand Down Expand Up @@ -171,6 +211,16 @@ pub fn decrypt_number(s: &str) -> Result<i64, Error> {
}

/// Generate a random string of length n.
///
/// # Example
///
/// ```
/// use rwf::crypto::random_string;
///
/// let s = random_string(10);
///
/// assert_eq!(s.len(), 10);
/// ```
pub fn random_string(n: usize) -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
Expand All @@ -180,13 +230,33 @@ pub fn random_string(n: usize) -> String {
}

/// Generate a CSRF protection token.
/// The resulting token will be accepted by the CSRF protection middleware.
///
/// The token is formatted with the Base64 encoding.
///
/// # Example
///
/// ```
/// use rwf::crypto::csrf_token;
///
/// let token = csrf_token().unwrap();
/// ```
pub fn csrf_token() -> Result<String, Error> {
// Our encryption is salted, re-using some known plain text isn't an issue.
let token = format!("{}_csrf", OffsetDateTime::now_utc().unix_timestamp());
encrypt(token.as_bytes())
}

/// Check that the CSRF token was generated by our app.
/// Validate a CSRF token. Checks that the token was generated by the same secret key and
/// hasn't expired.
///
/// # Example
///
/// ```
/// # use rwf::crypto::{csrf_token, csrf_token_validate};
/// let token = csrf_token().unwrap();
/// assert!(csrf_token_validate(&token));
/// ```
pub fn csrf_token_validate(token: &str) -> bool {
match decrypt(token) {
Ok(value) => {
Expand Down
31 changes: 27 additions & 4 deletions rwf/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
//! Global error type.
use thiserror::Error;

/// An error returned by any Rwf module.
#[derive(Error, Debug)]
pub enum Error {
/// IO error.
#[error("io error: {0}")]
Io(#[from] std::io::Error),

#[error("malformed request: {0}")]
MalformedRequest(&'static str),

/// JSON (de)serialization error.
#[error("json")]
Json(#[from] serde_json::Error),

/// Error returned by a controller.
#[error("{0}")]
Controller(#[from] crate::controller::Error),

/// Error returned by the HTTP server.
#[error("{0}")]
Controller(crate::controller::Error),
Http(#[from] crate::http::Error),

/// Error returned by crypto.
#[error("{0}")]
Crypto(#[from] crate::crypto::Error),

/// Error returned by the template engine.
#[error("{0}")]
View(#[from] crate::view::Error),

/// Error returned by the background jobs queue.
#[error("{0}")]
Job(#[from] crate::job::Error),

/// Error returned by comms.
#[error("{0}")]
Comms(#[from] crate::comms::Error),

/// Utf-8 decoding error.
#[error("{0}")]
Utf8(#[from] std::string::FromUtf8Error),

/// Regex error.
#[error("{0}")]
Regex(#[from] regex::Error),

/// Time error.
#[error("{0}")]
Time(time::error::ComponentRange),

/// Format error.
#[error("fmt error: {0}")]
FmtError(#[from] std::fmt::Error),

/// Any error that implements `std::error::Error`.
#[error("{0}")]
Error(#[from] Box<dyn std::error::Error + Sync + Send>),
}
6 changes: 4 additions & 2 deletions rwf/src/hmr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Instant;

/// Hot module reload loader.
///
/// All files that change under the specified path will trigger a page reload event.
#[cfg(debug_assertions)]
pub fn hmr(path: PathBuf) {
use notify::event::ModifyKind;
Expand All @@ -36,8 +39,7 @@ pub fn hmr(path: PathBuf) {

if since_last_reload > Duration::from_millis(250) {
let everyone = Comms::notify();
let reload = TurboStream::new("").action("reload-page").render();
let _ = everyone.send(Message::Text(reload));
let _ = everyone.send(TurboStream::new("").action("reload-page"));
info!("Starting hot reload");
}
}
Expand Down
6 changes: 3 additions & 3 deletions rwf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
//! Rwf is an MVC framework, so **C**ontrollers are fundamental to serving HTTP requests. Defining controllers requires
//! imlementing the [`controller::Controller`] trait for a struct:
//!
//! ```rust
//! ```
//! use rwf::prelude::*;
//!
//! #[derive(Default)]
Expand All @@ -46,7 +46,7 @@
//!
//! Launching the Rwf HTTP server requires mapping routes to controllers, and can be done at application startup:
//!
//! ```rust
//! ```
//! use rwf::http::Server;
//!
//! # use rwf::prelude::*;
Expand All @@ -67,7 +67,7 @@
//! With all the routes mapped to controllers, you can launch the server from anywhere in your app. Typically though,
//! this is done from the main function:
//!
//! ```rust,ignore
//! ```ignore
//! use rwf::http::{Server, self};
//!
//! #[tokio::main]
Expand Down

0 comments on commit 6ceb026

Please sign in to comment.