Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions audit.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
# audit.toml
# cargo-audit configuration for rusthost
#
# fix G-3 — previously this file contained a bare `ignore` entry with no
# rationale, creating a silent suppression that future developers could not
# evaluate. Rationale is now documented here to match deny.toml.
#
# Standardising on `cargo deny check advisories` as the primary advisory gate
# is recommended; this file is kept for developers who run `cargo audit`
# directly. Both files must be kept in sync when advisories are added or
# the threat model changes (e.g. if RSA decryption is ever added to the code).

[advisories]
ignore = ["RUSTSEC-2023-0071"]
ignore = [
# rsa 0.9.x — Marvin attack: timing side-channel on DECRYPTION only.
# (RUSTSEC-2023-0071, https://rustsec.org/advisories/RUSTSEC-2023-0071)
#
# `rsa` is pulled in transitively by `arti-client` for X.509 certificate
# parsing in Tor directory consensus documents. It is used exclusively
# for RSA *signature verification*, never for decryption. The Marvin
# attack requires an adversary to make thousands of adaptive
# chosen-ciphertext decryption queries — a threat model that does not
# apply here.
#
# No patched version of `rsa` exists as of this writing.
# Revisit when arti upgrades past rsa 0.9.x or a fixed version ships.
# If RSA decryption is ever added to this codebase, remove this ignore
# immediately and treat the advisory as exploitable.
"RUSTSEC-2023-0071",
]
31 changes: 23 additions & 8 deletions src/config/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,29 @@ open_browser_on_start = false
# at the OS TCP backlog level rather than spawning unbounded tasks.
max_connections = 256

# Content-Security-Policy value sent with every HTML response.
# The default allows same-origin resources plus inline scripts and styles,
# which is required for onclick handlers, <style> blocks, and style= attributes.
# Tighten if your site uses no inline code:
# content_security_policy = "default-src 'self'"
# Relax further for third-party CDN resources:
# content_security_policy = "default-src 'self' cdn.example.com; script-src 'self' 'unsafe-inline'"
content_security_policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
# Content-Security-Policy level.
#
# Controls the Content-Security-Policy header sent with every HTML response.
# Three presets are available:
#
# "off" — No CSP header is sent (default). The browser uses its own
# defaults, which allow same-origin and most cross-origin
# resources. Start here; tighten once your site is working.
#
# "relaxed" — Sends: default-src * 'unsafe-inline' 'unsafe-eval' data: blob:
# Allows resources from any origin plus inline scripts/styles,
# eval, data: URIs, and blob URLs. Use when loading assets from
# external CDNs or third-party services.
#
# "strict" — Sends a same-origin-only policy:
# default-src 'self'; script-src 'self' 'unsafe-inline';
# style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;
# font-src 'self' data:
# Suitable for self-contained sites with no external assets.
#
# Note: Referrer-Policy: no-referrer is always sent regardless of this setting,
# so the .onion address never leaks to third-party origins via the Referer header.
csp_level = "off"

# ─── [site] ───────────────────────────────────────────────────────────────────

Expand Down
19 changes: 18 additions & 1 deletion src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ fn validate(cfg: &Config) -> Result<()> {
// bind: IpAddr — invalid IPs are already rejected by serde at parse time (4.2).
// level: LogLevel — invalid levels are already rejected by serde at parse time (4.2).

// fix C-1 — a free-form CSP string with embedded CR/LF could inject
// arbitrary headers. The field is now a typed `CspLevel` enum so serde
// rejects any value that isn't "off", "relaxed", or "strict" at parse time;
// no runtime check is needed here.

// fix C-2 — max_connections = 0 deadlocks (semaphore never grants permits);
// very large values defeat the connection limit entirely.
if cfg.server.max_connections == 0 {
errors.push("[server] max_connections must be at least 1".into());
}
if cfg.server.max_connections > 65_535 {
errors.push(format!(
"[server] max_connections = {} exceeds the practical limit of 65535",
cfg.server.max_connections
));
}

// [site]
// `index_file` must be a bare filename, not a path.
// Use Path::components() rather than checking for MAIN_SEPARATOR:
Expand Down Expand Up @@ -224,7 +241,7 @@ bind = "127.0.0.1"
auto_port_fallback = true
open_browser_on_start = false
max_connections = 256
content_security_policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
csp_level = "off"
{extra}

[site]
Expand Down
89 changes: 68 additions & 21 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,62 @@ fn serialize_ip_addr<S: serde::Serializer>(addr: &IpAddr, s: S) -> Result<S::Ok,
s.serialize_str(&addr.to_string())
}

// ─── CSP level ───────────────────────────────────────────────────────────────

/// Preset Content-Security-Policy levels selectable in `settings.toml`.
///
/// | Level | CSP header sent | Use case |
/// |-----------|-----------------------------------------------------------|-------------------------------|
/// | `off` | *(none)* | Dev / any site, zero friction |
/// | `relaxed` | `default-src * 'unsafe-inline' 'unsafe-eval' data: blob:` | Sites with external CDNs |
/// | `strict` | same-origin only + inline scripts/styles | High-security deployments |
///
/// The default is `off` so pages render correctly out of the box.
/// Tighten once you know which external origins your site actually needs.
///
/// **Tor note:** `Referrer-Policy: no-referrer` is always sent regardless of
/// this setting, preventing the `.onion` address from leaking to third parties.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum CspLevel {
/// No `Content-Security-Policy` header is sent. The browser applies its
/// own defaults. Recommended starting point — tighten once the site works.
#[default]
Off,
/// Sends `default-src * 'unsafe-inline' 'unsafe-eval' data: blob:`.
///
/// Permits resources from any origin, inline scripts/styles, `eval`,
/// `data:` URIs, and blob URLs. Use when loading assets from external CDNs.
Relaxed,
/// Sends a same-origin-only policy with inline scripts and styles permitted.
///
/// Policy: `default-src 'self'; script-src 'self' 'unsafe-inline';
/// style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;
/// font-src 'self' data:`
///
/// Suitable for self-contained sites that serve all assets locally.
Strict,
}

impl CspLevel {
/// Return the literal CSP header value for this level, or an empty string
/// when the level is [`CspLevel::Off`] (no header should be sent).
#[must_use]
pub const fn as_header_value(self) -> &'static str {
match self {
Self::Off => "",
Self::Relaxed => "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:",
Self::Strict => {
"default-src 'self'; \
script-src 'self' 'unsafe-inline'; \
style-src 'self' 'unsafe-inline'; \
img-src 'self' data: blob:; \
font-src 'self' data:"
}
}
}
}

// ─── Config structs ──────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -90,22 +146,11 @@ pub struct ServerConfig {
pub open_browser_on_start: bool,
pub max_connections: u32,

/// Value of the `Content-Security-Policy` header sent with every HTML
/// response (task 5.3). The default `"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"` restricts all
/// content to the same origin.
///
/// Operators serving CDN fonts, analytics scripts, or other third-party
/// resources can relax this without touching source code, e.g.:
///
/// ```toml
/// [server]
/// content_security_policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; script-src 'self' cdn.example.com"
/// ```
///
/// **Tor note:** `Referrer-Policy: no-referrer` is always sent regardless
/// of this setting, preventing the `.onion` URL from leaking to any
/// third-party origin referenced in served HTML.
pub content_security_policy: String,
/// Content-Security-Policy preset. See [`CspLevel`] for available values
/// (`"off"`, `"relaxed"`, `"strict"`) and the header each one sends.
/// Defaults to `"off"` — no CSP header, maximum browser compatibility.
#[serde(default)]
pub csp_level: CspLevel,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -114,10 +159,11 @@ pub struct SiteConfig {
pub directory: String,
pub index_file: String,
pub enable_directory_listing: bool,
// `auto_reload` has been removed: the field was advertised in the default
// config but never implemented. Old config files containing `auto_reload`
// will now be rejected at startup with a clear "unknown field" error,
// prompting the operator to remove the obsolete key (fix 2.6).
/// When `true`, directory listings and direct requests expose dot-files
/// (e.g. `.git/`, `.env`). Defaults to `false` so hidden files are not
/// accidentally served (fix H-10).
#[serde(default)]
pub expose_dotfiles: bool,
}

/// Controls Tor integration.
Expand Down Expand Up @@ -183,12 +229,13 @@ impl Default for Config {
auto_port_fallback: true,
open_browser_on_start: false,
max_connections: 256,
content_security_policy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'".into(),
csp_level: CspLevel::Off,
},
site: SiteConfig {
directory: "site".into(),
index_file: "index.html".into(),
enable_directory_listing: false,
expose_dotfiles: false,
},
tor: TorConfig { enabled: true },
logging: LoggingConfig {
Expand Down
17 changes: 17 additions & 0 deletions src/console/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,23 @@ pub fn render_help() -> String {
out
}

// ─── Confirm quit ─────────────────────────────────────────────────────────────

#[must_use]
pub fn render_confirm_quit() -> String {
let mut out = String::with_capacity(256);
let _ = writeln!(out, "{RULE}\r");
let _ = writeln!(out, " {}\r", bold("Quit RustHost?"));
let _ = writeln!(out, "{RULE}\r");
out.push_str("\r\n");
let _ = writeln!(out, " The server will stop accepting connections.\r");
out.push_str("\r\n");
let _ = writeln!(out, " {} Quit {} Cancel\r", bold("[Y]"), bold("[N]"));
out.push_str("\r\n");
let _ = writeln!(out, "{RULE}\r");
out
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

fn strip_timestamp(line: &str) -> &str {
Expand Down
4 changes: 3 additions & 1 deletion src/console/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub fn spawn(tx: UnboundedSender<KeyEvent>, shutdown: watch::Receiver<bool>) {

fn map_key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
if modifiers.contains(KeyModifiers::CONTROL) && code == KeyCode::Char('c') {
return KeyEvent::Quit;
return KeyEvent::ForceQuit;
}

match code {
Expand All @@ -42,6 +42,8 @@ fn map_key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyCode::Char('o' | 'O') => KeyEvent::Open,
KeyCode::Char('l' | 'L') => KeyEvent::ToggleLogs,
KeyCode::Char('q' | 'Q') | KeyCode::Esc => KeyEvent::Quit,
KeyCode::Char('y' | 'Y') => KeyEvent::Confirm,
KeyCode::Char('n' | 'N') => KeyEvent::Cancel,
_ => KeyEvent::Other,
}
}
1 change: 1 addition & 0 deletions src/console/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ async fn render(
}
ConsoleMode::LogView => dashboard::render_log_view(config.console.show_timestamps),
ConsoleMode::Help => dashboard::render_help(),
ConsoleMode::ConfirmQuit => dashboard::render_confirm_quit(),
};

// 3.3 — Skip all terminal I/O when the frame is identical to the previous
Expand Down
Loading
Loading