use crate::config::types::ServerConfig;
use std::collections::BTreeSet;
pub(super) fn validate_websocket(config: &ServerConfig, errors: &mut Vec<String>) {
let Some(websocket) = config.websocket.as_ref() else {
return;
};
validate_websocket_endpoint(config, websocket, errors);
validate_websocket_origins(websocket, errors);
validate_websocket_origin_ports(websocket, errors);
validate_websocket_keepalive(websocket, errors);
}
fn validate_websocket_endpoint(
config: &ServerConfig,
websocket: &crate::config::types::WebSocketConfig,
errors: &mut Vec<String>,
) {
if websocket.listen_address.port() == 0 {
errors.push("websocket.listen_address: port must be non-zero".to_owned());
}
if websocket.listen_address == config.listen_address {
errors.push(
"websocket.listen_address: must differ from listen_address (the WebSocket \
acceptor is a sibling listener, not the main wire port)"
.to_owned(),
);
}
if websocket.listen_address == config.health_listen_address {
errors.push("websocket.listen_address: must differ from health_listen_address".to_owned());
}
if let Some(cluster) = config.cluster.as_ref() {
if websocket.listen_address == cluster.listen_address {
errors.push(
"websocket.listen_address: must differ from cluster.listen_address".to_owned(),
);
}
}
if websocket.path.is_empty() {
errors.push("websocket.path: upgrade path must not be empty".to_owned());
} else if !websocket.path.starts_with('/') {
errors.push("websocket.path: upgrade path must start with '/'".to_owned());
}
if websocket
.path
.chars()
.any(|character| character.is_ascii_whitespace() || character.is_ascii_control())
{
errors.push(
"websocket.path: upgrade path must not contain whitespace or control characters"
.to_owned(),
);
}
if websocket.path.contains(['?', '#']) {
errors.push(
"websocket.path: upgrade path is a single exact path and must not carry a query \
or fragment"
.to_owned(),
);
}
}
fn validate_websocket_origins(
websocket: &crate::config::types::WebSocketConfig,
errors: &mut Vec<String>,
) {
let mut seen_origins = BTreeSet::new();
for origin in &websocket.allowed_origins {
if origin.is_empty() {
errors.push("websocket.allowed_origins: origin entries must not be empty".to_owned());
continue;
}
if origin
.chars()
.any(|character| character.is_ascii_whitespace() || character.is_ascii_control())
{
errors.push(format!(
"websocket.allowed_origins: origin '{origin}' must not contain whitespace or \
control characters"
));
}
if origin != "null" {
match origin.split_once("://") {
None => errors.push(format!(
"websocket.allowed_origins: origin '{origin}' must be a serialized origin \
(scheme://host[:port]) or the literal 'null'"
)),
Some((scheme, rest)) => {
if scheme.is_empty() || rest.is_empty() {
errors.push(format!(
"websocket.allowed_origins: origin '{origin}' must name both a \
scheme and a host"
));
}
if rest.contains('/') {
errors.push(format!(
"websocket.allowed_origins: origin '{origin}' must not contain a \
path or trailing slash (a serialized Origin header never does)"
));
}
}
}
}
if !seen_origins.insert(origin.as_str()) {
errors.push(format!(
"websocket.allowed_origins: duplicate origin '{origin}'"
));
}
}
}
fn validate_websocket_origin_ports(
websocket: &crate::config::types::WebSocketConfig,
errors: &mut Vec<String>,
) {
let mut seen = BTreeSet::new();
for port in &websocket.allowed_origin_ports {
if *port == 0 {
errors.push("websocket.allowed_origin_ports: port 0 is not a port".to_owned());
}
if !seen.insert(*port) {
errors.push(format!(
"websocket.allowed_origin_ports: duplicate port {port}"
));
}
}
}
fn validate_websocket_keepalive(
websocket: &crate::config::types::WebSocketConfig,
errors: &mut Vec<String>,
) {
match websocket.ping_interval_ms {
Some(0) => errors.push(
"websocket.ping_interval_ms: must be greater than zero when configured (omit the \
key to disable keepalive pings)"
.to_owned(),
),
Some(interval_ms) => {
let interval = std::time::Duration::from_millis(interval_ms);
if std::time::Instant::now().checked_add(interval).is_none() {
errors.push(format!(
"websocket.ping_interval_ms: {interval_ms} overflows the monotonic clock"
));
}
}
None => {}
}
}