toe-beans 0.10.0

DHCP library, client, and server
Documentation
mod lease_time;

use super::RESOLVED_SERVER_PORT;
use crate::v4::MessageOptions;
use ip_network::Ipv4Network;
pub use lease_time::LeaseTime;
use log::{info, warn};
use mac_address::MacAddress;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{OpenOptions, read_to_string};
use std::io::Write;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;
use toml;

/// Representation of a `toe-beans.toml` file. Used by the [Server](crate::v4::Server).
///
/// This is exported for you to use in your application to generate a `toe-beans.toml`.
#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
    /// The path to a directory where this config will exist on disk.
    #[serde(skip)]
    pub path: PathBuf,
    /// See: [LeaseTime](lease_time::LeaseTime)
    pub lease_time: LeaseTime,
    /// Controls whether the server agrees to use [Rapid Commit](crate::v4::MessageOptions::RapidCommit).
    pub rapid_commit: bool,
    /// A range of IP addresses for the server to lease. Specified in CIDR notation. For example: `10.1.9.32/16`
    pub network_cidr: Ipv4Network,
    /// The server will bind to this address and port.
    pub listen_address: SocketAddrV4,
    /// The name of the network interface to broadcast server responses to.
    /// Currently only takes one interface.
    pub interface: Option<String>,
    /// May or may not match the IP address in `listen_address`.
    /// For example, you might be listening to `0.0.0.0`, but your server has another address it can be reached from.
    /// This is used in both the `siaddr` field and `ServerIdentifier` option.
    ///
    /// If `None`, then `listen_address` is used.
    pub server_address: Option<Ipv4Addr>,
    /// DHCP option numbers (as a String) mapped to values that are used to respond to parameter request list options
    /// Please note that this field, in particular, is subject to change.
    pub parameters: HashMap<String, MessageOptions>,
    /// This is passed to `Leases`'s `static_leases` field.
    pub static_leases: HashMap<MacAddress, Ipv4Addr>,
    /// Whether to read and write leases to a file to maintain state between server restarts.
    /// Defaults to `true` (when not in a benchmark or integration test) but you might want to set to `false` for performance, lack of storage, etc.
    pub use_leases_file: bool,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            path: PathBuf::new(),                        // empty, the current directory
            lease_time: LeaseTime::new(86_400).unwrap(), // seconds in a day
            rapid_commit: true,
            network_cidr: Ipv4Network::new(Ipv4Addr::new(10, 0, 0, 0), 16).unwrap(), // 2^(32-16) addresses
            listen_address: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), RESOLVED_SERVER_PORT),
            interface: None,
            server_address: None,
            parameters: HashMap::with_capacity(u8::MAX as usize),
            static_leases: HashMap::new(),
            use_leases_file: cfg!(all(
                not(feature = "benchmark"),
                not(feature = "integration")
            )),
        }
    }
}

impl Config {
    /// The file name that the Config is read from and written to.
    pub const FILE_NAME: &'static str = "toe-beans.toml";

    /// Reads the `toe-beans.toml` file and deserializes it into a Config.
    /// Returns default options if there are any issues.
    pub fn read(from_where: PathBuf) -> Self {
        let read_result = read_to_string(from_where.join(Self::FILE_NAME));
        if let Ok(config_string) = read_result {
            let toml_result = toml::from_str(&config_string);
            match toml_result {
                Ok(config) => {
                    return Self {
                        path: from_where, // from_where might be different if passed through cli args
                        ..config
                    };
                }
                Err(error) => {
                    warn!("{error}");
                    warn!(
                        "Warning: invalid {}. Using default values.",
                        Self::FILE_NAME
                    );
                }
            }
        } else {
            warn!(
                "Warning: can't read {}. Using default values.",
                Self::FILE_NAME
            );
        }

        Self {
            path: from_where,
            ..Config::default()
        }
    }

    /// Serializes a Config and writes it to a `toe-beans.toml` file.
    pub fn write(&self) {
        info!("Writing {}", Self::FILE_NAME);

        let config_content = toml::to_string_pretty(self).expect("Failed to generate toml data");
        let mode = 0o600;

        let mut file = OpenOptions::new()
            .read(false)
            .write(true)
            .create(true)
            .truncate(true)
            .mode(mode) // ensure that file permissions are set before creating file
            .open(self.path.join(Self::FILE_NAME))
            .unwrap_or_else(|_| panic!("Failed to open config {} for writing", Self::FILE_NAME));

        file.write_all(config_content.as_bytes())
            .unwrap_or_else(|_| panic!("Failed to write config to {}", Self::FILE_NAME));
    }
}