1use crate::error::{LicenseError, Result};
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9#[cfg(feature = "cloud-metadata")]
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum RuntimeEnvironment {
15 Standalone,
17 Docker,
19 Kubernetes,
21 AwsEc2,
23 GcpCompute,
25 AzureVm,
27 GenericCloud,
29}
30
31impl RuntimeEnvironment {
32 pub fn detect() -> Self {
34 if std::env::var("KUBERNETES_SERVICE_HOST").is_ok() {
36 return Self::Kubernetes;
37 }
38
39 if std::path::Path::new("/.dockerenv").exists() {
41 return Self::Docker;
42 }
43
44 if let Ok(cgroup) = std::fs::read_to_string("/proc/self/cgroup") {
46 if cgroup.contains("docker")
47 || cgroup.contains("kubepods")
48 || cgroup.contains("containerd")
49 {
50 return Self::Docker;
51 }
52 }
53
54 if std::env::var("AWS_EXECUTION_ENV").is_ok() || std::env::var("AWS_REGION").is_ok() {
57 return Self::AwsEc2;
58 }
59
60 if std::env::var("GOOGLE_CLOUD_PROJECT").is_ok() || std::env::var("GCP_PROJECT").is_ok() {
61 return Self::GcpCompute;
62 }
63
64 if std::env::var("AZURE_CLIENT_ID").is_ok() || std::env::var("WEBSITE_SITE_NAME").is_ok() {
65 return Self::AzureVm;
66 }
67
68 Self::Standalone
69 }
70
71 pub fn has_stable_hardware(&self) -> bool {
73 matches!(
74 self,
75 Self::Standalone | Self::AwsEc2 | Self::GcpCompute | Self::AzureVm
76 )
77 }
78
79 pub fn recommended_id_source(&self) -> Option<InstanceIdSource> {
81 match self {
82 Self::Standalone => None, Self::Docker => Some(InstanceIdSource::DockerContainerId),
84 Self::Kubernetes => Some(InstanceIdSource::KubernetesPodUid),
85 Self::AwsEc2 => Some(InstanceIdSource::AwsInstanceId),
86 Self::GcpCompute => Some(InstanceIdSource::GcpInstanceId),
87 Self::AzureVm => Some(InstanceIdSource::AzureInstanceId),
88 Self::GenericCloud => Some(InstanceIdSource::CustomEnvVar("INSTANCE_ID".to_string())),
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub enum InstanceIdSource {
96 AwsInstanceId,
98 GcpInstanceId,
100 AzureInstanceId,
102 KubernetesPodUid,
104 DockerContainerId,
106 CustomFile(PathBuf),
108 CustomEnvVar(String),
110}
111
112impl InstanceIdSource {
113 pub fn get_id(&self) -> Option<String> {
115 match self {
116 Self::AwsInstanceId => get_aws_instance_id(),
117 Self::GcpInstanceId => get_gcp_instance_id(),
118 Self::AzureInstanceId => get_azure_instance_id(),
119 Self::KubernetesPodUid => get_kubernetes_pod_uid(),
120 Self::DockerContainerId => get_docker_container_id(),
121 Self::CustomFile(path) => std::fs::read_to_string(path)
122 .ok()
123 .map(|s| s.trim().to_string())
124 .filter(|s| !s.is_empty()),
125 Self::CustomEnvVar(var) => std::env::var(var).ok().filter(|s| !s.is_empty()),
126 }
127 }
128}
129
130fn get_aws_instance_id() -> Option<String> {
132 let token = ureq_get_with_header(
134 "http://169.254.169.254/latest/api/token",
135 "X-aws-ec2-metadata-token-ttl-seconds",
136 "21600",
137 )
138 .ok()?;
139
140 ureq_get_with_header(
141 "http://169.254.169.254/latest/meta-data/instance-id",
142 "X-aws-ec2-metadata-token",
143 &token,
144 )
145 .ok()
146}
147
148fn get_gcp_instance_id() -> Option<String> {
150 ureq_get_with_header(
151 "http://metadata.google.internal/computeMetadata/v1/instance/id",
152 "Metadata-Flavor",
153 "Google",
154 )
155 .ok()
156}
157
158fn get_azure_instance_id() -> Option<String> {
160 ureq_get_with_header(
161 "http://169.254.169.254/metadata/instance/compute/vmId?api-version=2021-02-01&format=text",
162 "Metadata",
163 "true",
164 )
165 .ok()
166}
167
168fn get_kubernetes_pod_uid() -> Option<String> {
170 let paths = ["/etc/podinfo/uid", "/var/run/secrets/kubernetes.io/poduid"];
172
173 for path in paths {
174 if let Ok(uid) = std::fs::read_to_string(path) {
175 let uid = uid.trim().to_string();
176 if !uid.is_empty() {
177 return Some(uid);
178 }
179 }
180 }
181
182 std::env::var("POD_UID").ok()
184}
185
186fn get_docker_container_id() -> Option<String> {
188 let cgroup = std::fs::read_to_string("/proc/self/cgroup").ok()?;
190
191 for line in cgroup.lines() {
192 if let Some(pos) = line.rfind('/') {
194 let id = &line[pos + 1..];
195 if id.len() >= 12 && id.chars().all(|c| c.is_ascii_hexdigit()) {
197 return Some(id[..12].to_string()); }
199 }
200 }
201
202 std::env::var("HOSTNAME")
204 .ok()
205 .filter(|h| h.len() == 12 && h.chars().all(|c| c.is_ascii_hexdigit()))
206}
207
208#[allow(unused_variables)]
210fn ureq_get_with_header(url: &str, header_name: &str, header_value: &str) -> Result<String> {
211 #[cfg(feature = "cloud-metadata")]
215 {
216 use std::io::Read;
217 use std::io::Write;
218 use std::net::TcpStream;
219
220 let url = url::Url::parse(url).map_err(|e| {
221 LicenseError::IoError(std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
222 })?;
223
224 let host = url.host_str().ok_or_else(|| {
225 LicenseError::IoError(std::io::Error::new(
226 std::io::ErrorKind::InvalidInput,
227 "No host",
228 ))
229 })?;
230 let port = url.port().unwrap_or(80);
231
232 let mut stream = TcpStream::connect_timeout(
233 &format!("{}:{}", host, port).parse().unwrap(),
234 Duration::from_secs(2),
235 )?;
236 stream.set_read_timeout(Some(Duration::from_secs(2)))?;
237
238 let request = format!(
239 "GET {} HTTP/1.1\r\nHost: {}\r\n{}: {}\r\nConnection: close\r\n\r\n",
240 url.path(),
241 host,
242 header_name,
243 header_value
244 );
245
246 stream.write_all(request.as_bytes())?;
247
248 let mut response = String::new();
249 stream.read_to_string(&mut response)?;
250
251 if let Some(body_start) = response.find("\r\n\r\n") {
253 return Ok(response[body_start + 4..].trim().to_string());
254 }
255 }
256
257 Err(LicenseError::IoError(std::io::Error::other(
258 "Cloud metadata feature not enabled",
259 )))
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct ContainerBinding {
265 pub environment: RuntimeEnvironment,
267 pub id_source: Option<InstanceIdSource>,
269 pub instance_id_hash: Option<String>,
271}
272
273impl ContainerBinding {
274 pub fn detect() -> Self {
276 let environment = RuntimeEnvironment::detect();
277 let id_source = environment.recommended_id_source();
278
279 let instance_id_hash = id_source
280 .as_ref()
281 .and_then(|src| src.get_id())
282 .map(|id| sha256_short(&id));
283
284 Self {
285 environment,
286 id_source,
287 instance_id_hash,
288 }
289 }
290
291 pub fn matches_current(&self) -> bool {
293 if self.instance_id_hash.is_none() {
294 return true;
296 }
297
298 let current = Self::detect();
299 self.instance_id_hash == current.instance_id_hash
300 }
301
302 pub fn should_use_hardware_binding(&self) -> bool {
304 self.environment.has_stable_hardware() && self.instance_id_hash.is_none()
305 }
306}
307
308fn sha256_short(input: &str) -> String {
309 use sha2::{Digest, Sha256};
310 let mut hasher = Sha256::new();
311 hasher.update(input.as_bytes());
312 hex::encode(&hasher.finalize()[..16])
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_environment_detection() {
321 let env = RuntimeEnvironment::detect();
322 println!("Detected environment: {:?}", env);
324 }
325
326 #[test]
327 fn test_custom_env_var_source() {
328 std::env::set_var("TEST_INSTANCE_ID", "test-id-12345");
329
330 let source = InstanceIdSource::CustomEnvVar("TEST_INSTANCE_ID".to_string());
331 let id = source.get_id();
332
333 assert_eq!(id, Some("test-id-12345".to_string()));
334
335 std::env::remove_var("TEST_INSTANCE_ID");
336 }
337
338 #[test]
339 fn test_container_binding() {
340 let binding = ContainerBinding::detect();
341
342 assert!(binding.matches_current());
344
345 println!("Container binding: {:?}", binding);
346 }
347}