pub const PRODUCT_SDK: &str = "nitro-sim-client";
pub const PRODUCT_CLI: &str = "nitro-sim-cli";
pub const MANAGED_MARKER: &str = "(managed)";
pub const UNKNOWN_VERSION: &str = "unknown";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientProduct {
Sdk,
Cli,
}
impl ClientProduct {
const fn token(self) -> &'static str {
match self {
Self::Sdk => PRODUCT_SDK,
Self::Cli => PRODUCT_CLI,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientSurface {
Raw,
Managed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientKind {
Managed,
Raw,
Cli,
Custom,
Unknown,
}
impl ClientKind {
pub const fn as_metric_value(self) -> &'static str {
match self {
Self::Managed => "managed",
Self::Raw => "raw",
Self::Cli => "cli",
Self::Custom => "custom",
Self::Unknown => "unknown",
}
}
}
pub fn render(
product: ClientProduct,
surface: ClientSurface,
version: &str,
sdk_version: &str,
) -> String {
let mut user_agent = format!("{}/{version}", product.token());
if product == ClientProduct::Cli {
user_agent.push(' ');
user_agent.push_str(PRODUCT_SDK);
user_agent.push('/');
user_agent.push_str(sdk_version);
}
if surface == ClientSurface::Managed {
user_agent.push(' ');
user_agent.push_str(MANAGED_MARKER);
}
user_agent
}
pub fn classify(user_agent: Option<&str>) -> (ClientKind, &str) {
let Some(user_agent) = user_agent.map(str::trim).filter(|value| !value.is_empty()) else {
return (ClientKind::Unknown, UNKNOWN_VERSION);
};
let Some((token, version)) = user_agent
.split_whitespace()
.next()
.and_then(|leading| leading.split_once('/'))
else {
return (ClientKind::Custom, UNKNOWN_VERSION);
};
let version = accepted_version(version);
match token {
PRODUCT_CLI => (ClientKind::Cli, version),
PRODUCT_SDK if user_agent.contains(MANAGED_MARKER) => (ClientKind::Managed, version),
PRODUCT_SDK => (ClientKind::Raw, version),
_ => (ClientKind::Custom, UNKNOWN_VERSION),
}
}
const MAX_VERSION_LEN: usize = 32;
fn accepted_version(version: &str) -> &str {
let valid = !version.is_empty()
&& version.len() <= MAX_VERSION_LEN
&& version
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'));
if valid { version } else { UNKNOWN_VERSION }
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::raw_sdk(ClientProduct::Sdk, ClientSurface::Raw, ClientKind::Raw)]
#[case::managed_sdk(ClientProduct::Sdk, ClientSurface::Managed, ClientKind::Managed)]
#[case::cli_raw(ClientProduct::Cli, ClientSurface::Raw, ClientKind::Cli)]
#[case::cli_managed(ClientProduct::Cli, ClientSurface::Managed, ClientKind::Cli)]
fn rendered_identities_classify_back(
#[case] product: ClientProduct,
#[case] surface: ClientSurface,
#[case] expected: ClientKind,
) {
let user_agent = render(product, surface, "0.12.3", "0.12.3");
assert_eq!(classify(Some(&user_agent)), (expected, "0.12.3"));
}
#[rstest]
#[case::absent(None, ClientKind::Unknown)]
#[case::empty(Some(""), ClientKind::Unknown)]
#[case::python(Some("python-requests/2.31.0"), ClientKind::Custom)]
#[case::garbage(Some("!!!"), ClientKind::Custom)]
#[case::foreign_managed_marker(Some("my-bot/1.0 (managed)"), ClientKind::Custom)]
fn foreign_and_absent_user_agents(
#[case] user_agent: Option<&str>,
#[case] expected: ClientKind,
) {
assert_eq!(classify(user_agent), (expected, UNKNOWN_VERSION));
}
#[test]
fn cli_token_wins_over_the_sdk_token_it_carries() {
let user_agent = format!("{PRODUCT_CLI}/1.2.3 {PRODUCT_SDK}/0.12.3 {MANAGED_MARKER}");
assert_eq!(classify(Some(&user_agent)), (ClientKind::Cli, "1.2.3"));
}
#[rstest]
#[case::semver("1.2.3-rc.1_hotfix", "1.2.3-rc.1_hotfix")]
#[case::injection("1.2.3\",attack=\"x", UNKNOWN_VERSION)]
#[case::overlong("1.0.0-padpadpadpadpadpadpadpadpadpad", UNKNOWN_VERSION)]
#[case::empty_version("", UNKNOWN_VERSION)]
fn version_tag_is_bounded_before_reaching_metrics(
#[case] version: &str,
#[case] expected: &str,
) {
let user_agent = format!("{PRODUCT_CLI}/{version}");
assert_eq!(classify(Some(&user_agent)), (ClientKind::Cli, expected));
}
}