crdhcpc 0.1.1

Standalone DHCP Client for Linux with DHCPv4, DHCPv6, PXE, and Dynamic DNS support
Documentation
//! DHCP Client Control Interface
//!
//! Provides JSON-RPC 2.0 control interface for standalone crdhcpc daemon.
//! This allows CLI commands and external programs to control the daemon via Unix socket.

use crate::{DhcpClientManager, DhcpClientConfig};
use crate::protocol::{
    ConnectionHandler, PeerCredentials, ProtocolResult, ProtocolError,
    jsonrpc::{JsonRpcRequest, JsonRpcNotification},
};
use async_trait::async_trait;
use serde_json::{json, Value};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info};

/// DHCP Client control handler
///
/// Implements ConnectionHandler trait to handle JSON-RPC requests from clients
pub struct DhcpControlHandler {
    manager: Arc<RwLock<DhcpClientManager>>,
}

impl DhcpControlHandler {
    /// Create a new control handler with the given manager
    pub fn new(manager: Arc<RwLock<DhcpClientManager>>) -> Self {
        Self { manager }
    }

    /// Handle dhcpc.start method
    async fn handle_start(&self, params: Value) -> ProtocolResult<Value> {
        let interface = params
            .get("interface")
            .and_then(|v| v.as_str())
            .ok_or_else(|| {
                ProtocolError::InvalidParams("Missing or invalid 'interface' parameter".to_string())
            })?;

        let manager = self.manager.read().await;
        manager
            .start_interface(interface)
            .await
            .map_err(|e| {
                ProtocolError::Internal(format!("Failed to start DHCP client: {}", e))
            })?;

        info!("Started DHCP client on interface: {}", interface);
        Ok(json!({
            "success": true,
            "interface": interface
        }))
    }

    /// Handle dhcpc.stop method
    async fn handle_stop(&self, params: Value) -> ProtocolResult<Value> {
        let interface = params
            .get("interface")
            .and_then(|v| v.as_str())
            .ok_or_else(|| {
                ProtocolError::InvalidParams("Missing or invalid 'interface' parameter".to_string())
            })?;

        let manager = self.manager.read().await;
        manager
            .stop_interface(interface)
            .await
            .map_err(|e| {
                ProtocolError::Internal(format!("Failed to stop DHCP client: {}", e))
            })?;

        info!("Stopped DHCP client on interface: {}", interface);
        Ok(json!({
            "success": true,
            "interface": interface
        }))
    }

    /// Handle dhcpc.renew method
    async fn handle_renew(&self, params: Value) -> ProtocolResult<Value> {
        let interface = params
            .get("interface")
            .and_then(|v| v.as_str())
            .ok_or_else(|| {
                ProtocolError::InvalidParams("Missing or invalid 'interface' parameter".to_string())
            })?;

        let manager = self.manager.read().await;
        manager
            .renew_interface(interface)
            .await
            .map_err(|e| {
                ProtocolError::Internal(format!("Failed to renew lease: {}", e))
            })?;

        info!("Renewed DHCP lease on interface: {}", interface);
        Ok(json!({
            "success": true,
            "interface": interface
        }))
    }

    /// Handle dhcpc.release method
    async fn handle_release(&self, params: Value) -> ProtocolResult<Value> {
        let interface = params
            .get("interface")
            .and_then(|v| v.as_str())
            .ok_or_else(|| {
                ProtocolError::InvalidParams("Missing or invalid 'interface' parameter".to_string())
            })?;

        let manager = self.manager.read().await;
        manager
            .release_interface(interface)
            .await
            .map_err(|e| {
                ProtocolError::Internal(format!("Failed to release lease: {}", e))
            })?;

        info!("Released DHCP lease on interface: {}", interface);
        Ok(json!({
            "success": true,
            "interface": interface
        }))
    }

    /// Handle dhcpc.status method
    async fn handle_status(&self, params: Option<Value>) -> ProtocolResult<Value> {
        let interface_filter = params
            .as_ref()
            .and_then(|p| p.get("interface"))
            .and_then(|v| v.as_str());

        let manager = self.manager.read().await;
        let mut status = manager.get_status().await;

        // Filter by interface if specified
        if let Some(interface) = interface_filter {
            status.v4_clients.retain(|c| c.interface == interface);
            status.v6_clients.retain(|c| c.interface == interface);
        }

        serde_json::to_value(status).map_err(|e| {
            ProtocolError::Internal(format!("Failed to serialize status: {}", e))
        })
    }

    /// Handle dhcpc.getConfig method
    async fn handle_get_config(&self) -> ProtocolResult<Value> {
        let manager = self.manager.read().await;
        let config = manager.get_config().await;

        serde_json::to_value(config).map_err(|e| {
            ProtocolError::Internal(format!("Failed to serialize config: {}", e))
        })
    }

    /// Handle dhcpc.setConfig method
    async fn handle_set_config(&self, params: Value) -> ProtocolResult<Value> {
        let config: DhcpClientConfig = serde_json::from_value(params).map_err(|e| {
            ProtocolError::InvalidParams(format!("Invalid configuration: {}", e))
        })?;

        let manager = self.manager.read().await;
        manager.set_config(config).await.map_err(|e| {
            ProtocolError::Internal(format!("Failed to set configuration: {}", e))
        })?;

        info!("Configuration updated");
        Ok(json!({"success": true}))
    }
}

#[async_trait]
impl ConnectionHandler for DhcpControlHandler {
    async fn handle_request(
        &self,
        request: JsonRpcRequest,
        credentials: PeerCredentials,
    ) -> ProtocolResult<Value> {
        debug!(
            "Handling request: {} from PID {} (UID {})",
            request.method, credentials.pid, credentials.uid
        );

        match request.method.as_str() {
            "dhcpc.start" => {
                let params = request.params.ok_or_else(|| {
                    ProtocolError::InvalidParams("Missing parameters".to_string())
                })?;
                self.handle_start(params).await
            }
            "dhcpc.stop" => {
                let params = request.params.ok_or_else(|| {
                    ProtocolError::InvalidParams("Missing parameters".to_string())
                })?;
                self.handle_stop(params).await
            }
            "dhcpc.renew" => {
                let params = request.params.ok_or_else(|| {
                    ProtocolError::InvalidParams("Missing parameters".to_string())
                })?;
                self.handle_renew(params).await
            }
            "dhcpc.release" => {
                let params = request.params.ok_or_else(|| {
                    ProtocolError::InvalidParams("Missing parameters".to_string())
                })?;
                self.handle_release(params).await
            }
            "dhcpc.status" => self.handle_status(request.params).await,
            "dhcpc.getConfig" => self.handle_get_config().await,
            "dhcpc.setConfig" => {
                let params = request.params.ok_or_else(|| {
                    ProtocolError::InvalidParams("Missing parameters".to_string())
                })?;
                self.handle_set_config(params).await
            }
            "dhcpc.shutdown" => {
                info!("Shutdown requested by PID {} (UID {})", credentials.pid, credentials.uid);
                // Return success, shutdown will be handled by the server
                Ok(json!({"success": true, "message": "Shutting down"}))
            }
            _ => Err(ProtocolError::MethodNotFound(request.method.clone())),
        }
    }

    async fn handle_notification(
        &self,
        notification: JsonRpcNotification,
        credentials: PeerCredentials,
    ) -> ProtocolResult<()> {
        debug!(
            "Received notification: {} from PID {}",
            notification.method, credentials.pid
        );
        Ok(())
    }

    async fn on_connect(&self, credentials: PeerCredentials) -> ProtocolResult<()> {
        info!(
            "Control client connected: PID {} (UID {}, GID {})",
            credentials.pid, credentials.uid, credentials.gid
        );
        Ok(())
    }

    async fn on_disconnect(&self, credentials: PeerCredentials) {
        info!(
            "Control client disconnected: PID {} (UID {}, GID {})",
            credentials.pid, credentials.uid, credentials.gid
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::DhcpClientConfig;

    #[tokio::test]
    async fn test_handler_creation() {
        let manager = DhcpClientManager::new(DhcpClientConfig::default());
        let handler = DhcpControlHandler::new(Arc::new(RwLock::new(manager)));

        let request = JsonRpcRequest::new(
            "dhcpc.status",
            None,
            json!(1),
        );

        let credentials = PeerCredentials {
            pid: 1234,
            uid: 0,
            gid: 0,
        };

        let result = handler.handle_request(request, credentials).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_unknown_method() {
        let manager = DhcpClientManager::new(DhcpClientConfig::default());
        let handler = DhcpControlHandler::new(Arc::new(RwLock::new(manager)));

        let request = JsonRpcRequest::new(
            "dhcpc.unknown",
            None,
            json!(1),
        );

        let credentials = PeerCredentials {
            pid: 1234,
            uid: 0,
            gid: 0,
        };

        let result = handler.handle_request(request, credentials).await;
        assert!(result.is_err());
    }
}