use config::{Environment, File};
use derivative::Derivative;
use serde::Deserialize;
use std::cmp::min;
use std::fmt::{Debug, Display, Formatter};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use url::Url;
#[derive(Deserialize, Derivative, Clone, PartialEq)]
#[derivative(Debug)]
pub struct Config {
pub listen_address: SocketAddr,
pub relayer_urls: Vec<Url>,
#[serde(default)]
pub relayer_connections: Option<usize>,
pub authorization_token: Option<AuthorizationToken>,
#[derivative(Debug = "ignore")]
pub publish_keypair_path: PathBuf,
#[serde(with = "humantime_serde", default = "default_publish_interval")]
pub publish_interval_duration: Duration,
#[serde(default = "default_history_service_url")]
pub history_service_url: Url,
#[serde(default)]
pub enable_update_deduplication: bool,
#[serde(with = "humantime_serde", default = "default_update_deduplication_ttl")]
pub update_deduplication_ttl: Duration,
pub proxy_url: Option<RedactedUrl>,
#[serde(with = "humantime_serde", default = "default_legacy_sched_interval")]
pub legacy_sched_interval_duration: Duration,
}
#[derive(Deserialize, Clone, PartialEq)]
#[serde(transparent)]
pub struct RedactedUrl(Url);
impl RedactedUrl {
pub fn expose(&self) -> &Url {
&self.0
}
}
fn redact_url(url: &Url) -> String {
let scheme = url.scheme();
match (url.host_str(), url.port()) {
(Some(host), Some(port)) => format!("{scheme}://{host}:{port}"),
(Some(host), None) => format!("{scheme}://{host}"),
(None, _) => scheme.to_string(),
}
}
impl Display for RedactedUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&redact_url(&self.0))
}
}
impl Debug for RedactedUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", redact_url(&self.0))
}
}
#[derive(Deserialize, Derivative, Clone, PartialEq)]
pub struct AuthorizationToken(pub String);
impl Debug for AuthorizationToken {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let token_string = self.0.to_ascii_lowercase();
#[allow(clippy::string_slice, reason = "false positive")]
let last_chars = &token_string[token_string.len() - min(4, token_string.len())..];
write!(f, "\"...{last_chars}\"")
}
}
fn default_publish_interval() -> Duration {
Duration::from_millis(25)
}
fn default_update_deduplication_ttl() -> Duration {
Duration::from_millis(500)
}
fn default_legacy_sched_interval() -> Duration {
Duration::from_millis(500)
}
fn default_history_service_url() -> Url {
#[allow(clippy::expect_used, reason = "hardcoded URL is always valid")]
"https://pyth.dourolabs.app/v1/symbols"
.parse()
.expect("hardcoded URL is valid")
}
pub fn load_config(config_path: String) -> anyhow::Result<Config> {
let config = config::Config::builder()
.add_source(File::with_name(&config_path))
.add_source(Environment::with_prefix("LAZER_AGENT").separator("__"))
.build()?
.try_deserialize()?;
Ok(config)
}
pub const CHANNEL_CAPACITY: usize = 1000;
#[cfg(test)]
mod tests {
use super::RedactedUrl;
use url::Url;
fn redacted(s: &str) -> RedactedUrl {
RedactedUrl(Url::parse(s).unwrap())
}
#[test]
fn redacted_url_hides_credentials() {
let url = redacted("http://alice:s3cr3t@proxy.example.com:8080/path?q=1");
let display = format!("{url}");
let debug = format!("{url:?}");
assert_eq!(display, "http://proxy.example.com:8080");
assert_eq!(debug, "\"http://proxy.example.com:8080\"");
for rendered in [&display, &debug] {
assert!(!rendered.contains("alice"));
assert!(!rendered.contains("s3cr3t"));
}
assert_eq!(url.expose().password(), Some("s3cr3t"));
let no_port = redacted("https://bob:hunter2@proxy.example.com");
assert_eq!(format!("{no_port}"), "https://proxy.example.com");
}
}