use crate::error::{LicenseError, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[cfg(feature = "cloud-metadata")]
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RuntimeEnvironment {
Standalone,
Docker,
Kubernetes,
AwsEc2,
GcpCompute,
AzureVm,
GenericCloud,
}
impl RuntimeEnvironment {
pub fn detect() -> Self {
if std::env::var("KUBERNETES_SERVICE_HOST").is_ok() {
return Self::Kubernetes;
}
if std::path::Path::new("/.dockerenv").exists() {
return Self::Docker;
}
if let Ok(cgroup) = std::fs::read_to_string("/proc/self/cgroup") {
if cgroup.contains("docker")
|| cgroup.contains("kubepods")
|| cgroup.contains("containerd")
{
return Self::Docker;
}
}
if std::env::var("AWS_EXECUTION_ENV").is_ok() || std::env::var("AWS_REGION").is_ok() {
return Self::AwsEc2;
}
if std::env::var("GOOGLE_CLOUD_PROJECT").is_ok() || std::env::var("GCP_PROJECT").is_ok() {
return Self::GcpCompute;
}
if std::env::var("AZURE_CLIENT_ID").is_ok() || std::env::var("WEBSITE_SITE_NAME").is_ok() {
return Self::AzureVm;
}
Self::Standalone
}
pub fn has_stable_hardware(&self) -> bool {
matches!(
self,
Self::Standalone | Self::AwsEc2 | Self::GcpCompute | Self::AzureVm
)
}
pub fn recommended_id_source(&self) -> Option<InstanceIdSource> {
match self {
Self::Standalone => None, Self::Docker => Some(InstanceIdSource::DockerContainerId),
Self::Kubernetes => Some(InstanceIdSource::KubernetesPodUid),
Self::AwsEc2 => Some(InstanceIdSource::AwsInstanceId),
Self::GcpCompute => Some(InstanceIdSource::GcpInstanceId),
Self::AzureVm => Some(InstanceIdSource::AzureInstanceId),
Self::GenericCloud => Some(InstanceIdSource::CustomEnvVar("INSTANCE_ID".to_string())),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InstanceIdSource {
AwsInstanceId,
GcpInstanceId,
AzureInstanceId,
KubernetesPodUid,
DockerContainerId,
CustomFile(PathBuf),
CustomEnvVar(String),
}
impl InstanceIdSource {
pub fn get_id(&self) -> Option<String> {
match self {
Self::AwsInstanceId => get_aws_instance_id(),
Self::GcpInstanceId => get_gcp_instance_id(),
Self::AzureInstanceId => get_azure_instance_id(),
Self::KubernetesPodUid => get_kubernetes_pod_uid(),
Self::DockerContainerId => get_docker_container_id(),
Self::CustomFile(path) => std::fs::read_to_string(path)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()),
Self::CustomEnvVar(var) => std::env::var(var).ok().filter(|s| !s.is_empty()),
}
}
}
fn get_aws_instance_id() -> Option<String> {
let token = ureq_get_with_header(
"http://169.254.169.254/latest/api/token",
"X-aws-ec2-metadata-token-ttl-seconds",
"21600",
)
.ok()?;
ureq_get_with_header(
"http://169.254.169.254/latest/meta-data/instance-id",
"X-aws-ec2-metadata-token",
&token,
)
.ok()
}
fn get_gcp_instance_id() -> Option<String> {
ureq_get_with_header(
"http://metadata.google.internal/computeMetadata/v1/instance/id",
"Metadata-Flavor",
"Google",
)
.ok()
}
fn get_azure_instance_id() -> Option<String> {
ureq_get_with_header(
"http://169.254.169.254/metadata/instance/compute/vmId?api-version=2021-02-01&format=text",
"Metadata",
"true",
)
.ok()
}
fn get_kubernetes_pod_uid() -> Option<String> {
let paths = ["/etc/podinfo/uid", "/var/run/secrets/kubernetes.io/poduid"];
for path in paths {
if let Ok(uid) = std::fs::read_to_string(path) {
let uid = uid.trim().to_string();
if !uid.is_empty() {
return Some(uid);
}
}
}
std::env::var("POD_UID").ok()
}
fn get_docker_container_id() -> Option<String> {
let cgroup = std::fs::read_to_string("/proc/self/cgroup").ok()?;
for line in cgroup.lines() {
if let Some(pos) = line.rfind('/') {
let id = &line[pos + 1..];
if id.len() >= 12 && id.chars().all(|c| c.is_ascii_hexdigit()) {
return Some(id[..12].to_string()); }
}
}
std::env::var("HOSTNAME")
.ok()
.filter(|h| h.len() == 12 && h.chars().all(|c| c.is_ascii_hexdigit()))
}
#[allow(unused_variables)]
fn ureq_get_with_header(url: &str, header_name: &str, header_value: &str) -> Result<String> {
#[cfg(feature = "cloud-metadata")]
{
use std::io::Read;
use std::io::Write;
use std::net::TcpStream;
let url = url::Url::parse(url).map_err(|e| {
LicenseError::IoError(std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
})?;
let host = url.host_str().ok_or_else(|| {
LicenseError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"No host",
))
})?;
let port = url.port().unwrap_or(80);
let mut stream = TcpStream::connect_timeout(
&format!("{}:{}", host, port).parse().unwrap(),
Duration::from_secs(2),
)?;
stream.set_read_timeout(Some(Duration::from_secs(2)))?;
let request = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\n{}: {}\r\nConnection: close\r\n\r\n",
url.path(),
host,
header_name,
header_value
);
stream.write_all(request.as_bytes())?;
let mut response = String::new();
stream.read_to_string(&mut response)?;
if let Some(body_start) = response.find("\r\n\r\n") {
return Ok(response[body_start + 4..].trim().to_string());
}
}
Err(LicenseError::IoError(std::io::Error::other(
"Cloud metadata feature not enabled",
)))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerBinding {
pub environment: RuntimeEnvironment,
pub id_source: Option<InstanceIdSource>,
pub instance_id_hash: Option<String>,
}
impl ContainerBinding {
pub fn detect() -> Self {
let environment = RuntimeEnvironment::detect();
let id_source = environment.recommended_id_source();
let instance_id_hash = id_source
.as_ref()
.and_then(|src| src.get_id())
.map(|id| sha256_short(&id));
Self {
environment,
id_source,
instance_id_hash,
}
}
pub fn matches_current(&self) -> bool {
if self.instance_id_hash.is_none() {
return true;
}
let current = Self::detect();
self.instance_id_hash == current.instance_id_hash
}
pub fn should_use_hardware_binding(&self) -> bool {
self.environment.has_stable_hardware() && self.instance_id_hash.is_none()
}
}
fn sha256_short(input: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
hex::encode(&hasher.finalize()[..16])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_environment_detection() {
let env = RuntimeEnvironment::detect();
println!("Detected environment: {:?}", env);
}
#[test]
fn test_custom_env_var_source() {
std::env::set_var("TEST_INSTANCE_ID", "test-id-12345");
let source = InstanceIdSource::CustomEnvVar("TEST_INSTANCE_ID".to_string());
let id = source.get_id();
assert_eq!(id, Some("test-id-12345".to_string()));
std::env::remove_var("TEST_INSTANCE_ID");
}
#[test]
fn test_container_binding() {
let binding = ContainerBinding::detect();
assert!(binding.matches_current());
println!("Container binding: {:?}", binding);
}
}