fcmc 0.9.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,
    models::ContainerCreateBody,
    query_parameters::{
        CreateContainerOptionsBuilder, InspectContainerOptions, StartContainerOptions,
    },
    secret::HostConfig,
};
use colored::*;
use serde::Deserialize;
use std::path::PathBuf;
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, src_dir: &PathBuf) -> Result<()> {
        let image_tag = self
            .docker
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("No docker config found"))?
            .image_tag
            .clone();
        crate::build_image(&docker, &image_tag, src_dir).await?;

        Ok(())
    }

    pub async fn generate_template(name: &str, output_dir: &str) -> anyhow::Result<()> {
        use std::fs;
        use std::path::Path;

        // 创建挑战目录
        let challenge_dir = Path::new(output_dir).join(name);
        fs::create_dir_all(&challenge_dir)?;

        // 创建src目录
        let src_dir = challenge_dir.join("src");
        fs::create_dir_all(&src_dir)?;

        // 创建attachment目录
        let attachment_dir = challenge_dir.join("attachment");
        fs::create_dir_all(&attachment_dir)?;

        // 创建meta.toml文件
        let meta_content = format!(
            r#"name = "{}"
author = "your_email@example.com" # modify
category = "Web" # modify
description = "Challenge description" # modify

attachment = "attachment/src.zip" # Optional

[flag]
value = ""       # is empty stand for dynamic flag # modify
env_var = "FLAG"


[docker]
image_tag = "floatctf/name:challenge-web_v1.0" # modify
port = "80/tcp"

    "#,
            name
        );
        fs::write(challenge_dir.join("meta.toml"), meta_content)?;

        // 创建flag
        let flag_content = "flag{test_flag}";
        fs::write(src_dir.join("flag"), flag_content)?;

        // 创建flag.sh
        let flag_sh_content = r#"#!/bin/bash
# flag 动态替换脚本
sed -i "s/flag{test_flag}/$FLAG/" /flag

export FLAG=not_flag
FLAG=not_flag

rm -f /flag.sh
    "#;
        fs::write(src_dir.join("flag.sh"), flag_sh_content)?;

        // 创建entrypoint.sh
        let entrypoint_content = r#"#!/bin/bash
if [ -f /flag.sh ]; then
    echo "--- 正在初始化 Flag ---"
    sed -i 's/\r//g' /flag.sh
    /flag.sh
fi

exec "$@"
    "#;
        fs::write(src_dir.join("entrypoint.sh"), entrypoint_content)?;

        let index_php_content = r#"<?php
echo get_file_contents("/flag");
?>
    "#;
        fs::write(src_dir.join("index.php"), index_php_content)?;
        // 创建Dockerfile
        let dockerfile_content = r#"FROM php:5-apache-jessie
LABEL Author="your_name <your_email@example.com>"

COPY flag /flag
COPY flag.sh /flag.sh
COPY entrypoint.sh /entrypoint.sh
COPY index.php /var/www/html/index.php
RUN chmod +x /flag.sh
RUN chmod +x /entrypoint.sh

# 必须
EXPOSE 80
WORKDIR /var/www/html


ENTRYPOINT [ "/entrypoint.sh" ]
CMD [ "apache2-foreground" ]
    "#;
        fs::write(src_dir.join("Dockerfile"), dockerfile_content)?;

        println!("  {}   成功生成挑战模板: {:?}", "OK".green(), challenge_dir);
        println!(
            "  {}   请记得修改meta.toml中的作者和描述信息",
            "INFO".cyan()
        );
        println!(
            "  {}   请记得修改Dockerfile和源代码以实现你的挑战逻辑",
            "INFO".cyan()
        );

        Ok(())
    }
}