use std::num::NonZeroU64;
use std::time::Duration;
use provcfg::{ClapArgs, Configurable};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::compile_time_config;
use crate::constants::MAX_SESSION_AGE;
fn default_data_dir() -> String {
std::env::var("KELLNR_DATA_DIR")
.ok()
.or_else(|| compile_time_config::KELLNR_COMPTIME__DATA_DIR.map(String::from))
.unwrap_or_default()
}
#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs, ToSchema)]
#[serde(default)]
#[configurable(clap_prefix = "registry")]
#[allow(clippy::struct_excessive_bools)]
pub struct Registry {
#[arg(short = 'd')]
pub data_dir: String,
#[arg(long = "registry-session-age")]
pub session_age_seconds: u64,
pub cache_size: u64,
pub cache_max_size: u64,
pub max_crate_size: u64,
pub max_db_connections: u32,
pub auth_required: bool,
#[configurable(env_list)]
#[arg(value_delimiter = ',')]
pub required_crate_fields: Vec<String>,
pub new_crates_restricted: bool,
pub cookie_signing_key: Option<String>,
pub allow_ownerless_crates: bool,
pub token_cache_enabled: bool,
#[arg(long = "registry-token-cache-ttl")]
pub token_cache_ttl_seconds: u64,
pub token_cache_max_capacity: u64,
pub token_db_retry_count: u32,
#[arg(long = "registry-token-db-retry-delay")]
pub token_db_retry_delay_ms: u64,
#[arg(long = "registry-download-timeout")]
pub download_timeout_seconds: u64,
pub download_max_concurrent: usize,
#[arg(long = "registry-download-counter-flush")]
pub download_counter_flush_seconds: u64,
pub auth_max_failed_attempts: u32,
#[arg(long = "registry-auth-failed-attempts-window")]
pub auth_failed_attempts_window_seconds: u64,
pub sponsor_links_enabled: bool,
}
impl Registry {
pub fn session_age(&self) -> SessionAge {
SessionAge::from_secs(self.session_age_seconds)
}
pub fn token_cache_ttl(&self) -> Duration {
Duration::from_secs(self.token_cache_ttl_seconds)
}
pub fn auth_failed_attempts_window(&self) -> Duration {
Duration::from_secs(self.auth_failed_attempts_window_seconds)
}
pub fn max_crate_bytes(&self) -> usize {
mb_to_bytes(self.max_crate_size)
}
pub fn cache_max_bytes(&self) -> Option<u64> {
(self.cache_max_size > 0).then(|| self.cache_max_size.saturating_mul(1_000_000))
}
}
pub(crate) fn mb_to_bytes(mb: impl TryInto<usize>) -> usize {
mb.try_into()
.unwrap_or(usize::MAX)
.saturating_mul(1_000_000)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SessionAge {
Never,
For(NonZeroU64),
}
impl SessionAge {
pub fn from_secs(secs: u64) -> Self {
NonZeroU64::new(secs).map_or(Self::Never, Self::For)
}
pub fn expiry(self) -> Option<Duration> {
match self {
Self::Never => None,
Self::For(secs) => Some(Duration::from_secs(secs.get()).min(MAX_SESSION_AGE)),
}
}
pub fn cookie_max_age(self) -> Duration {
self.expiry().unwrap_or(MAX_SESSION_AGE)
}
}
impl Default for Registry {
fn default() -> Self {
Self {
data_dir: default_data_dir(),
session_age_seconds: 60 * 60 * 8,
cache_size: 1000,
cache_max_size: 0,
max_crate_size: 10,
max_db_connections: 0,
auth_required: false,
required_crate_fields: Vec::new(),
new_crates_restricted: false,
cookie_signing_key: None,
allow_ownerless_crates: false,
token_cache_enabled: true,
token_cache_ttl_seconds: 1800,
token_cache_max_capacity: 10000,
token_db_retry_count: 3,
token_db_retry_delay_ms: 100,
download_timeout_seconds: 60,
download_max_concurrent: 20,
download_counter_flush_seconds: 30,
auth_max_failed_attempts: 10,
auth_failed_attempts_window_seconds: 300,
sponsor_links_enabled: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn registry(session_age_seconds: u64) -> Registry {
Registry {
session_age_seconds,
..Registry::default()
}
}
#[test]
fn session_age_of_zero_never_expires() {
let age = registry(0).session_age();
assert_eq!(age, SessionAge::Never);
assert_eq!(age.expiry(), None);
assert_eq!(age.cookie_max_age(), MAX_SESSION_AGE);
}
#[test]
fn session_age_is_the_configured_age() {
let age = registry(60).session_age();
assert_eq!(age, SessionAge::For(NonZeroU64::new(60).unwrap()));
assert_eq!(age.expiry(), Some(Duration::from_mins(1)));
assert_eq!(age.cookie_max_age(), Duration::from_mins(1));
}
#[test]
fn session_age_is_bounded() {
let age = registry(u64::MAX).session_age();
assert_eq!(age.expiry(), Some(MAX_SESSION_AGE));
assert_eq!(age.cookie_max_age(), MAX_SESSION_AGE);
}
#[test]
fn durations_are_read_in_seconds() {
let registry = Registry {
token_cache_ttl_seconds: 90,
auth_failed_attempts_window_seconds: 300,
..Registry::default()
};
assert_eq!(registry.token_cache_ttl(), Duration::from_secs(90));
assert_eq!(
registry.auth_failed_attempts_window(),
Duration::from_mins(5)
);
}
#[test]
fn sizes_are_read_in_mb_and_saturate() {
let mut registry = Registry {
max_crate_size: 10,
cache_max_size: 0,
..Registry::default()
};
assert_eq!(registry.max_crate_bytes(), 10_000_000);
assert_eq!(registry.cache_max_bytes(), None);
registry.max_crate_size = u64::MAX;
registry.cache_max_size = u64::MAX;
assert_eq!(registry.max_crate_bytes(), usize::MAX);
assert_eq!(registry.cache_max_bytes(), Some(u64::MAX));
registry.cache_max_size = 2;
assert_eq!(registry.cache_max_bytes(), Some(2_000_000));
}
}