Rusty Beam Architecture

This document provides an in-depth look at Rusty Beam's architecture, covering the core server design, request processing pipeline, plugin system, and key architectural decisions.

Table of Contents

Architectural Overview

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  HTTP Client    │────▶│   Rusty Beam     │────▶│  HTML Document  │
└─────────────────┘     │     Server       │     └─────────────────┘
                        └──────────────────┘
                                 │
                    ┌────────────┴───────────┐
                    │   Plugin Pipeline      │
                    ├────────────────────────┤
                    │ • WebSocket            │
                    │ • Selector Handler     │
                    │ • Authentication       │
                    │ • File Handler         │
                    └────────────────────────┘
        

Rusty Beam is built as an asynchronous, event-driven HTTP server using Tokio and Hyper. The architecture emphasizes:

Core Design Principles

1. Plugin-Based Architecture

Everything is a plugin. The core server only handles:

2. Async Pipeline Processing

Requests flow through plugins sequentially but asynchronously:

pub async fn process_request_through_pipeline(
    req: Request<Body>,
    app_state: Arc<AppState>,
    pipeline: Arc<Vec<Arc<dyn Plugin>>>,
) -> Result<Response<Body>, Box<dyn std::error::Error + Send + Sync>>

3. Immutable Request Pattern

Plugins receive requests by value and must return modified versions, ensuring clean data flow and preventing side effects.

4. HTML as Configuration

Configuration uses semantic HTML with microdata, making it self-documenting and queryable via the same selector API.

Request Processing Flow

Detailed Request Lifecycle

  1. Connection Accepted: Hyper accepts TCP connection
  2. Request Parsing: HTTP request parsed into Request<Body>
  3. Host Resolution: Determine which host pipeline to use
  4. Pipeline Execution:
    for plugin in pipeline {
        match plugin.handle_request(req, app_state).await? {
            PluginResponse::Continue(new_req) => req = new_req,
            PluginResponse::Done(response) => return response,
        }
    }
  5. Response Generation: Final plugin generates response
  6. Response Pipeline: Plugins can modify response in reverse order
  7. Send Response: Response sent to client

Plugin Response Types

Response Type Description Use Case
Continue(Request) Pass modified request to next plugin Authentication, logging, header modification
Done(Response) Generate response and stop pipeline File serving, error handling, API endpoints

Plugin System Architecture

Plugin Trait Definition

pub trait Plugin: Send + Sync {
    fn name(&self) -> &str;
    
    async fn handle_request(
        &self,
        req: Request<Body>,
        app_state: Arc<AppState>,
    ) -> Result<PluginResponse, Box<dyn std::error::Error + Send + Sync>>;
    
    async fn handle_response(
        &self,
        response: Response<Body>,
        _app_state: Arc<AppState>,
    ) -> Result<Response<Body>, Box<dyn std::error::Error + Send + Sync>> {
        Ok(response)
    }
}

Dynamic Plugin Loading

Plugins are loaded as dynamic libraries via FFI:

  1. Plugin compiled as cdylib with create_plugin export
  2. Server loads .so/.dll/.dylib at runtime
  3. Plugin instance created and added to pipeline
  4. Type erasure via Arc<dyn Plugin>

Plugin Communication

Plugins communicate via:

Memory Model & State Management

AppState Structure

pub struct AppState {
    pub connections: Arc<RwLock<HashMap<String, Vec<Connection>>>>,
    pub config: Arc<RwLock<Config>>,
    pub plugin_state: Arc<RwLock<HashMap<String, Arc<dyn Any + Send + Sync>>>>,
}

Concurrency Model

Memory Safety Guarantees

Configuration System

HTML Configuration Loading

pub fn load_config_from_html(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
    let html = fs::read_to_string(path)?;
    let doc = dom_query::Document::from(html.as_str());
    
    // Extract server config via CSS selectors
    let server_config = doc.select("[itemtype='https://rustybeam.net/schema/ServerConfig']")?;
    
    // Parse microdata properties
    let bind_address = server_config.select("[itemprop='bindAddress']")?.text();
    let bind_port = server_config.select("[itemprop='bindPort']")?.text().parse()?;
    
    // Load host configurations and plugins...
}

Hot Reload Architecture

Configuration reloading via SIGHUP:

  1. Signal handler receives SIGHUP
  2. New configuration loaded in background
  3. Atomic swap of configuration
  4. New requests use new config
  5. In-flight requests complete with old config

Key Components

Core Server Files

src/
├── main.rs              # Entry point, server setup
├── config.rs            # Configuration parsing
├── plugin.rs            # Plugin trait and types
├── utils.rs             # Path canonicalization, utilities
└── lib.rs               # Public API exports

plugins/
├── selector-handler/    # CSS selector manipulation
├── file-handler/        # Static file serving
├── websocket/           # WebSocket support
├── basic-auth/          # HTTP Basic Authentication
└── [other plugins]/     # Additional functionality
        

Critical Functions

Function Location Purpose
handle_request() src/main.rs:316 Main request entry point
process_request_through_pipeline() src/main.rs:169 Execute plugin pipeline
create_host_pipelines() src/main.rs:70 Build plugin pipelines from config
canonicalize_file_path() src/utils.rs Security-critical path validation

Security Architecture

Path Traversal Prevention

pub fn canonicalize_file_path(
    root: &Path, 
    requested: &str
) -> Result<PathBuf, Box<dyn std::error::Error>> {
    let normalized = requested.trim_start_matches('/');
    let full_path = root.join(normalized).canonicalize()?;
    
    // Ensure path is within root
    if !full_path.starts_with(root) {
        return Err("Path traversal attempt".into());
    }
    
    Ok(full_path)
}

Plugin Isolation

Authentication & Authorization

Performance Considerations

Optimization Strategies

Scalability

Future Architecture Directions

Planned Enhancements

Architectural Invariants

These principles will remain constant: