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.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ 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:
Everything is a plugin. The core server only handles:
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>>
Plugins receive requests by value and must return modified versions, ensuring clean data flow and preventing side effects.
Configuration uses semantic HTML with microdata, making it self-documenting and queryable via the same selector API.
Request<Body>
for plugin in pipeline {
match plugin.handle_request(req, app_state).await? {
PluginResponse::Continue(new_req) => req = new_req,
PluginResponse::Done(response) => return response,
}
}
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 |
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)
}
}
Plugins are loaded as dynamic libraries via FFI:
cdylib
with create_plugin
export.so
/.dll
/.dylib
at runtimeArc<dyn Plugin>
Plugins communicate via:
AppState
with thread-safe collectionspub 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>>>>,
}
RwLock
for shared stateAny
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...
}
Configuration reloading via SIGHUP:
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
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 |
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)
}
Bytes
for body handlingThese principles will remain constant: