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
213 lines
6.2 KiB
Rust
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");
|
|
}
|
|
}
|