fcmc 0.6.3

A CLI tool to check whether CTF challenge meta.toml configs are valid, and optionally test Docker setup
Documentation
use anyhow::Result;
use bollard::{
    Docker, body_full,
    models::ContainerCreateBody,
    query_parameters::{
        BuildImageOptionsBuilder, CreateContainerOptionsBuilder, InspectContainerOptions,
        RemoveContainerOptions, StartContainerOptions, StopContainerOptions,
    },
    secret::{BuildInfo, HostConfig},
};
use futures_util::StreamExt;
use serde::Deserialize;
use std::fs::File;
use std::{io::Read, path::PathBuf};
use tar::Builder;
use tempfile::NamedTempFile;
use tokio_util::bytes::Bytes;
use tracing::info;

#[derive(Debug, Deserialize)]
pub struct ChallengeMeta {
    pub name: String,
    pub author: String,
    pub category: String,
    pub description: String,

    pub flag: FlagMeta,
    pub docker: Option<DockerMeta>,

    pub attachment: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct FlagMeta {
    pub value: String,
    pub env_var: String,
}

#[derive(Debug, Deserialize)]
pub struct DockerMeta {
    pub image_tag: String,
    pub port: String, // e.g. "80/tcp"
    pub is_nc: Option<bool>,
}

impl ChallengeMeta {
    pub fn from_toml_str(toml: &str) -> Result<Self, toml::de::Error> {
        toml::from_str(toml)
    }

    pub async fn create_and_start(
        &self,
        docker: &Docker,
        identifier: &str,
        flag: &str,
    ) -> Result<u16> {
        let docker_meta = self
            .docker
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("No docker config found"))?;

        let container_name = format!("{}", identifier);
        let container_port = docker_meta.port.as_str();
        info!(
            "Creating container: {}, src_port:{}",
            container_name, container_port
        );
        let options = CreateContainerOptionsBuilder::new()
            .name(&container_name)
            .build();

        let config = ContainerCreateBody {
            image: Some(docker_meta.image_tag.clone()),
            env: Some(vec![format!("{}={}", self.flag.env_var, flag)]),

            host_config: Some(HostConfig {
                auto_remove: Some(true),
                port_bindings: Some({
                    let mut map = std::collections::HashMap::new();
                    map.insert(
                        docker_meta.port.clone(),
                        Some(vec![bollard::models::PortBinding {
                            host_ip: Some("0.0.0.0".to_string()),
                            host_port: None,
                        }]),
                    );
                    map
                }),
                ..Default::default()
            }),
            ..Default::default()
        };

        let container = docker.create_container(Some(options), config).await?;
        info!("Container created: {:?}", container);
        docker
            .start_container(&container.id, None::<StartContainerOptions>)
            .await?;

        let container_info = docker
            .inspect_container(&container.id, None::<InspectContainerOptions>)
            .await?;

        let port_bindings = container_info
            .network_settings
            .as_ref()
            .and_then(|n| n.ports.as_ref())
            .ok_or_else(|| anyhow::anyhow!("No port binding info found"))?;

        let host_port = port_bindings
            .get(container_port)
            .and_then(|v| v.as_ref())
            .and_then(|bindings| bindings.get(0))
            .and_then(|binding| binding.host_port.as_ref())
            .ok_or_else(|| anyhow::anyhow!("Host port not found"))?
            .parse::<u16>()?;

        Ok(host_port)
    }

    pub async fn build_image(
        &self,
        docker: &Docker,
        context_path: &PathBuf,
    ) -> Result<Vec<String>> {
        let docker_meta = self
            .docker
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("No docker config found"))?;

        let options = BuildImageOptionsBuilder::default()
            .t(&docker_meta.image_tag)
            .build();

        let tmp = NamedTempFile::new()?;
        {
            let file = File::create(tmp.path())?;
            let mut tar_builder = Builder::new(file);
            tar_builder.append_dir_all(".", context_path)?; // 把 context_path 整个目录打包进去
            tar_builder.finish()?;
        }

        // 2. 把 tar 文件读到内存
        let mut buf = Vec::new();
        File::open(tmp.path())?.read_to_end(&mut buf)?;

        // 3. 用 body_full 包装成 BodyType
        let body = body_full(Bytes::from(buf));

        // 4. 调用 build_image
        let mut build_stream = docker.build_image(options, None, Some(body));

        let mut infos = Vec::new();
        while let Some(update) = build_stream.next().await {
            let info: BuildInfo = update?; // 拿到 BuildInfo

            if let Some(ref stream_msg) = info.stream {
                infos.push(stream_msg.trim().to_owned());
            }
            if let Some(ref err) = info.error {
                eprintln!("ERROR: {}", err);
            }
        }

        Ok(infos)
    }
}