librnxengine 1.1.0

implement robust software licensing, activation, and validation systems.
Documentation
use crate::{
    activation::{ActivationRequest, ActivationResponse},
    error::LicenseError,
    license::License,
};
use reqwest::{Client, StatusCode};
use tracing::{debug, error, info, warn};

/// HTTP client for communicating with a license management server.
///
/// This client provides methods to interact with a remote license server
/// for activation, validation, and revocation checking operations.
/// It handles HTTP communication, authentication, and error response parsing.
///
/// # Features
/// - License activation with hardware binding
/// - Remote license validation
/// - Revocation status checking
/// - Bearer token authentication
/// - Comprehensive error handling
#[derive(Debug, Clone)]
pub struct LicenseClient {
    /// Base URL of the license server (e.g., "https://api.license.example.com")
    base_url: String,
    /// Reusable HTTP client with connection pooling
    client: Client,
    /// Optional API key for server authentication
    api_key: Option<String>,
}

impl LicenseClient {
    /// Creates a new license client with the specified server base URL.
    ///
    /// Initializes an HTTP client with default settings. The client uses
    /// connection pooling for efficient repeated requests.
    ///
    /// # Parameters
    /// - `base_url`: Base URL of the license management server
    ///
    /// # Returns
    /// A new `LicenseClient` instance
    ///
    /// # Example
    /// ```rust
    /// let client = LicenseClient::new("https://api.license.example.com");
    /// ```
    pub fn new(base_url: &str) -> Self {
        info!("Initializing LicenseClient with base URL: {}", base_url);
        Self {
            base_url: base_url.to_string(),
            client: Client::new(),
            api_key: None,
        }
    }

    /// Configures the client with an API key for server authentication.
    ///
    /// The API key is used for Bearer token authentication on requests
    /// that require server-side authentication (e.g., license activation).
    ///
    /// # Parameters
    /// - `api_key`: API key/token for server authentication
    ///
    /// # Returns
    /// The same `LicenseClient` instance with API key configured
    ///
    /// # Note
    /// This uses the builder pattern, allowing method chaining.
    ///
    /// # Example
    /// ```rust
    /// let client = LicenseClient::new("https://api.example.com")
    ///     .with_api_key("secret-api-key-123");
    /// ```
    pub fn with_api_key(mut self, api_key: &str) -> Self {
        self.api_key = Some(api_key.to_string());
        self
    }

    /// Activates a license on the remote license server.
    ///
    /// Sends an activation request containing hardware fingerprint and
    /// device information. The server validates the license and returns
    /// activation tokens if successful.
    ///
    /// # Parameters
    /// - `request`: `ActivationRequest` containing license and device details
    ///
    /// # Returns
    /// - `Ok(ActivationResponse)`: Server response with activation tokens
    /// - `Err(LicenseError)`: If activation fails (network, server error, etc.)
    ///
    /// # HTTP Endpoint
    /// `POST /api/v1/licenses/activate`
    ///
    /// # Status Code Handling
    /// - `200 OK`: Activation successful, returns `ActivationResponse`
    /// - `403 Forbidden`: License invalid or expired
    /// - `429 Too Many Requests`: Activation limit exceeded
    /// - Other: Generic server error
    ///
    /// # Example
    /// ```rust
    /// let request = ActivationRequest {
    ///     license_id: Uuid::new_v4(),
    ///     hardware_fingerprint: "fingerprint".to_string(),
    ///     // ... other fields
    /// };
    ///
    /// match client.activate_license(&request).await {
    ///     Ok(response) => println!("Activated: {}", response.activation_id),
    ///     Err(e) => eprintln!("Activation failed: {}", e),
    /// }
    /// ```
    pub async fn activate_license(
        &self,
        request: &ActivationRequest,
    ) -> Result<ActivationResponse, LicenseError> {
        info!("Activating license: {}", request.license_id);
        debug!("Activation request: {:?}", request);

        // Construct activation endpoint URL
        let url = format!("{}/api/v1/licenses/activate", self.base_url);

        // Build HTTP POST request with JSON body
        let mut req = self.client.post(&url).json(request);

        // Add API key authentication if configured
        if let Some(api_key) = &self.api_key {
            req = req.bearer_auth(api_key);
        }

        // Send request and await response
        let response = req.send().await?;

        // Handle different HTTP status codes
        match response.status() {
            StatusCode::OK => {
                // Parse successful activation response
                let activation: ActivationResponse = response.json().await?;
                info!(
                    "License activated successfully: {}",
                    activation.activation_id
                );
                Ok(activation)
            }
            StatusCode::FORBIDDEN => {
                // License is invalid, expired, or not authorized
                error!("Activation forbidden - license may be invalid or expired");
                Err(LicenseError::InvalidLicense(
                    "Activation forbidden".to_string(),
                ))
            }
            StatusCode::TOO_MANY_REQUESTS => {
                // Activation limit exceeded (rate limiting or license limits)
                error!("Activation limit exceeded");
                Err(LicenseError::ActivationLimitExceeded)
            }
            _ => {
                // Handle other error statuses
                let error_text = response.text().await.unwrap_or_default();
                error!("Activation failed: {}", error_text);
                Err(LicenseError::ServerError(error_text))
            }
        }
    }

    /// Validates a license with the remote server using an activation token.
    ///
    /// Contacts the license server to verify that a license is still valid
    /// and retrieves the latest license details. Requires a valid activation
    /// token obtained during activation.
    ///
    /// # Parameters
    /// - `license_id`: Unique identifier of the license to validate
    /// - `activation_token`: Token received during license activation
    ///
    /// # Returns
    /// - `Ok(License)`: Valid license with current details
    /// - `Err(LicenseError)`: If validation fails
    ///
    /// # HTTP Endpoint
    /// `GET /api/v1/licenses/{license_id}/validate`
    ///
    /// # Authentication
    /// Uses Bearer token authentication with the activation token.
    ///
    /// # Example
    /// ```rust
    /// let license_id = Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000")?;
    /// let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
    ///
    /// let license = client.validate_license(&license_id, token).await?;
    /// println!("License valid until: {}", license.expires_at());
    /// ```
    pub async fn validate_license(
        &self,
        license_id: &uuid::Uuid,
        activation_token: &str,
    ) -> Result<License, LicenseError> {
        info!("Validating license: {}", license_id);

        // Construct validation endpoint URL
        let url = format!("{}/api/v1/licenses/{}/validate", self.base_url, license_id);

        // Send authenticated GET request
        let response = self
            .client
            .get(&url)
            .bearer_auth(activation_token)
            .send()
            .await?;

        // Check if request was successful
        if response.status().is_success() {
            // Parse license from response
            let license: License = response.json().await?;
            debug!("License validation successful");
            Ok(license)
        } else {
            // Handle validation failure
            let error_text = response.text().await.unwrap_or_default();
            error!("License validation failed: {}", error_text);
            Err(LicenseError::ValidationFailed(error_text))
        }
    }

    /// Checks if a license has been revoked on the server.
    ///
    /// Queries the revocation status of a license. Useful for periodic
    /// checks to ensure licenses haven't been revoked due to terms
    /// violation or other issues.
    ///
    /// # Parameters
    /// - `license_id`: Unique identifier of the license to check
    ///
    /// # Returns
    /// - `Ok(true)`: License has been revoked
    /// - `Ok(false)`: License is not revoked (or check failed)
    /// - `Err(LicenseError)`: If network error occurs
    ///
    /// # HTTP Endpoint
    /// `GET /api/v1/licenses/{license_id}/revoked`
    ///
    /// # Note
    /// Returns `false` on HTTP errors to avoid blocking operations
    /// when the revocation server is unavailable. This is a safety
    /// measure to prevent false positives during network issues.
    ///
    /// # Example
    /// ```rust
    /// if client.check_revocation(&license_id).await? {
    ///     warn!("License has been revoked - stopping service");
    ///     std::process::exit(1);
    /// }
    /// ```
    pub async fn check_revocation(&self, license_id: &uuid::Uuid) -> Result<bool, LicenseError> {
        debug!("Checking revocation status for license: {}", license_id);

        // Construct revocation check endpoint URL
        let url = format!("{}/api/v1/licenses/{}/revoked", self.base_url, license_id);

        // Send GET request without authentication (public endpoint)
        let response = self.client.get(&url).send().await?;

        // Check if request was successful
        if response.status().is_success() {
            // Parse revocation status
            let revoked: bool = response.json().await?;
            if revoked {
                warn!("License {} is revoked", license_id);
            }
            Ok(revoked)
        } else {
            // On error, assume not revoked to avoid false positives
            error!("Failed to check revocation status");
            Ok(false)
        }
    }
}

// Note: The client uses `reqwest` for HTTP operations, which provides:
// - Connection pooling for performance
// - Async/await support
// - JSON serialization/deserialization
// - Automatic content-type headers
// - Timeout and retry configuration (not shown in this implementation)

// Note: Error handling strategy:
// - Specific error variants for common HTTP status codes
// - Generic server errors with response body for debugging
// - Network errors automatically converted to `LicenseError::NetworkError`
// - Graceful degradation for revocation checks (returns false on error)

// Note: Security considerations:
// - API keys and activation tokens are transmitted securely via HTTPS
// - Bearer token authentication for stateless server validation
// - No sensitive data (private keys) is ever transmitted
// - All communication should use TLS (HTTPS) in production

// Note: The client is designed for both online and occasional-offline scenarios:
// - Activation requires network connectivity
// - Validation can be cached locally after initial activation
// - Revocation checks can be performed periodically (e.g., daily)
// - Graceful degradation when network is unavailable

// Note: For production use, consider adding:
// 1. Request timeouts to prevent hanging
// 2. Retry logic for transient network failures
// 3. Circuit breaker pattern for server outages
// 4. Request/response logging for debugging
// 5. Metrics collection for monitoring
// 6. Support for HTTP proxies
// 7. Custom user-agent headers

// Example usage in an application:
// ```
// #[tokio::main]
// async fn main() -> Result<(), LicenseError> {
//     // Initialize client
//     let client = LicenseClient::new("https://license-api.example.com")
//         .with_api_key("your-api-key");
//
//     // Activate license
//     let activation_req = ActivationRequest {
//         license_id: load_license_id(),
//         hardware_fingerprint: generate_fingerprint()?,
//         machine_name: hostname::get()?.to_string_lossy().to_string(),
//         timestamp: Utc::now(),
//         nonce: Uuid::new_v4().to_string(),
//         // ... other fields
//     };
//
//     let activation = client.activate_license(&activation_req).await?;
//
//     // Store activation token securely
//     store_token(activation.activation_token);
//
//     // Periodically validate license
//     let license = client.validate_license(
//         &activation.license_id,
//         &load_token()
//     ).await?;
//
//     // Check revocation status occasionally
//     if client.check_revocation(&license.license_id()).await? {
//         handle_revocation();
//     }
//
//     Ok(())
// }
// ```