Files
owncast-IRC-bridge/src/config.rs
cottongin 78fec2946c feat: add webhook auth guard and IRC password/username support
Add WEBHOOK_SECRET env var for authenticating incoming Owncast webhooks
via a ?secret= query parameter. Requests with a missing or incorrect
secret are rejected with 401. If unset, all requests are accepted
(with a startup warning).

Also includes previously uncommitted work:
- IRC server password support (IRC_PASSWORD env var, PASS command)
- IRC username/ident field in config
- IRC_PASSWORD and SELinux volume flag in docker-compose.yml

Made-with: Cursor
2026-03-13 00:53:59 -04:00

213 lines
6.2 KiB
Rust

use std::path::Path;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct BridgeConfig {
pub irc: IrcConfig,
pub owncast: OwncastConfig,
#[serde(default)]
pub bridge: BridgeSettings,
#[serde(default)]
pub control: ControlConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct IrcConfig {
pub server: String,
#[serde(default = "default_irc_port")]
pub port: u16,
#[serde(default)]
pub tls: bool,
#[serde(default = "default_nick")]
pub nick: String,
#[serde(default)]
pub username: Option<String>,
pub channel: String,
}
#[derive(Debug, Deserialize)]
pub struct OwncastConfig {
pub url: String,
#[serde(default = "default_webhook_port")]
pub webhook_port: u16,
#[serde(default)]
pub websocket_enabled: bool,
#[serde(default = "default_health_poll_interval")]
pub health_poll_interval_secs: u64,
#[serde(default = "default_ws_display_name")]
pub ws_display_name: String,
}
#[derive(Debug, Deserialize)]
pub struct BridgeSettings {
#[serde(default = "default_irc_prefix")]
pub irc_prefix: String,
#[serde(default = "default_owncast_prefix")]
pub owncast_prefix: String,
#[serde(default)]
pub message_buffer_size: usize,
}
#[derive(Debug, Deserialize)]
pub struct ControlConfig {
#[serde(default = "default_socket_path")]
pub socket_path: String,
}
impl BridgeConfig {
pub fn load(path: &Path) -> anyhow::Result<Self> {
let contents = std::fs::read_to_string(path)?;
let config: BridgeConfig = toml::from_str(&contents)?;
Ok(config)
}
pub fn irc_server_password() -> Option<String> {
std::env::var("IRC_PASSWORD").ok()
}
pub fn owncast_access_token(&self) -> anyhow::Result<String> {
std::env::var("OWNCAST_ACCESS_TOKEN")
.map_err(|_| anyhow::anyhow!("OWNCAST_ACCESS_TOKEN env var not set"))
}
pub fn webhook_secret() -> Option<String> {
std::env::var("WEBHOOK_SECRET").ok()
}
}
fn default_irc_port() -> u16 { 6667 }
fn default_nick() -> String { "owncast-bridge".to_string() }
fn default_webhook_port() -> u16 { 9078 }
fn default_health_poll_interval() -> u64 { 30 }
fn default_irc_prefix() -> String { "[IRC]".to_string() }
fn default_owncast_prefix() -> String { "[OC]".to_string() }
fn default_ws_display_name() -> String { "IRC Bridge".to_string() }
fn default_socket_path() -> String { "/tmp/owncast-irc-bridge.sock".to_string() }
impl Default for BridgeSettings {
fn default() -> Self {
Self {
irc_prefix: default_irc_prefix(),
owncast_prefix: default_owncast_prefix(),
message_buffer_size: 0,
}
}
}
impl Default for ControlConfig {
fn default() -> Self {
Self { socket_path: default_socket_path() }
}
}
#[cfg(test)]
impl BridgeConfig {
pub fn default_for_test() -> Self {
Self {
irc: IrcConfig {
server: "localhost".to_string(),
port: 6667,
tls: false,
nick: "test-bot".to_string(),
username: None,
channel: "#test".to_string(),
},
owncast: OwncastConfig {
url: "http://localhost:8080".to_string(),
webhook_port: 9078,
websocket_enabled: false,
health_poll_interval_secs: 30,
ws_display_name: default_ws_display_name(),
},
bridge: BridgeSettings::default(),
control: ControlConfig::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_config() {
let toml_str = r##"
[irc]
server = "irc.example.com"
channel = "#test"
[owncast]
url = "https://owncast.example.com"
"##;
let config: BridgeConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.irc.server, "irc.example.com");
assert_eq!(config.irc.port, 6667);
assert_eq!(config.irc.tls, false);
assert_eq!(config.irc.nick, "owncast-bridge");
assert!(config.irc.username.is_none());
assert_eq!(config.irc.channel, "#test");
assert_eq!(config.owncast.url, "https://owncast.example.com");
assert_eq!(config.owncast.webhook_port, 9078);
assert_eq!(config.owncast.websocket_enabled, false);
assert_eq!(config.owncast.health_poll_interval_secs, 30);
assert_eq!(config.bridge.irc_prefix, "[IRC]");
assert_eq!(config.bridge.owncast_prefix, "[OC]");
assert_eq!(config.bridge.message_buffer_size, 0);
assert_eq!(config.control.socket_path, "/tmp/owncast-irc-bridge.sock");
}
#[test]
fn test_parse_full_config() {
let toml_str = r##"
[irc]
server = "irc.example.com"
port = 6697
tls = true
nick = "mybot"
username = "myident"
channel = "#mychan"
[owncast]
url = "https://oc.example.com"
webhook_port = 8888
websocket_enabled = true
health_poll_interval_secs = 10
[bridge]
irc_prefix = "<IRC>"
owncast_prefix = "<OC>"
message_buffer_size = 50
[control]
socket_path = "/var/run/bridge.sock"
"##;
let config: BridgeConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.irc.port, 6697);
assert!(config.irc.tls);
assert_eq!(config.irc.nick, "mybot");
assert_eq!(config.irc.username.as_deref(), Some("myident"));
assert_eq!(config.owncast.webhook_port, 8888);
assert!(config.owncast.websocket_enabled);
assert_eq!(config.bridge.message_buffer_size, 50);
assert_eq!(config.control.socket_path, "/var/run/bridge.sock");
}
#[test]
fn test_irc_server_password_from_env() {
std::env::set_var("IRC_PASSWORD", "secret123");
assert_eq!(BridgeConfig::irc_server_password().as_deref(), Some("secret123"));
std::env::remove_var("IRC_PASSWORD");
assert!(BridgeConfig::irc_server_password().is_none());
}
#[test]
fn test_access_token_from_env() {
std::env::set_var("OWNCAST_ACCESS_TOKEN", "test-token-123");
let config = BridgeConfig::default_for_test();
let token = config.owncast_access_token();
assert_eq!(token.unwrap(), "test-token-123");
std::env::remove_var("OWNCAST_ACCESS_TOKEN");
}
}