use anyhow::Result;
use bollard::{
Docker, body_full,
models::ContainerCreateBody,
query_parameters::{
BuildImageOptionsBuilder, CreateContainerOptionsBuilder, InspectContainerOptions,
RemoveContainerOptions, StartContainerOptions, StopContainerOptions,
},
secret::{BuildInfo, HostConfig},
};
use colored::*;
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, 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)?; tar_builder.finish()?;
}
let mut buf = Vec::new();
File::open(tmp.path())?.read_to_end(&mut buf)?;
let body = body_full(Bytes::from(buf));
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?;
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)
}
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)?;
let src_dir = challenge_dir.join("src");
fs::create_dir_all(&src_dir)?;
let attachment_dir = challenge_dir.join("attachment");
fs::create_dir_all(&attachment_dir)?;
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)?;
let flag_content = "flag{test_flag}";
fs::write(src_dir.join("flag"), flag_content)?;
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)?;
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)?;
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(())
}
}