feat: add workshop-agent and workshop-cert

Signed-off-by: Catty Steve <4795515+Catty2014@user.noreply.gitee.com>
This commit is contained in:
Catty Steve
2025-11-13 23:49:16 +08:00
parent 49aa1dc36c
commit f66a36d0f4
18 changed files with 4613 additions and 0 deletions
+1
View File
@@ -1,2 +1,3 @@
target
playground
*.old
+2588
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "workshop-agent"
version = "0.1.0"
edition = "2024"
[dependencies]
actix-web = { version = "4.11.0", features = ["rustls-0_23"] }
actix-web-httpauth = "0.8.2"
anyhow = "1.0.100"
config = "0.15.18"
env_logger = "0.11.8"
log = "0.4.28"
querystring = "1.1.0"
rustls = "0.23.35"
rustls-pemfile = "2.2.0"
serde = { version = "1.0.228", features = ["derive"] }
shellflip = "2.1.2"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
uuid = { version = "1.18.1", features = ["v4"] }
+3
View File
@@ -0,0 +1,3 @@
host = "127.0.0.1"
port = 8080
+36
View File
@@ -0,0 +1,36 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub tls: Option<TLSConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TLSConfig {
pub cert: PathBuf,
pub key: PathBuf,
}
impl ServerConfig {
pub fn init() -> anyhow::Result<Self> {
let mut settings = config::Config::builder();
settings = settings
.add_source(config::Environment::with_prefix("HBWAGENT"))
.add_source(config::File::with_name("agent.toml").required(false));
let config: ServerConfig = settings.build()?.try_deserialize()?;
Ok(config)
}
pub fn is_tls_enabled(&self) -> bool {
self.tls.is_some()
}
pub fn bind_address(&self) -> (String, u16) {
// format!("{}:{}", self.host, self.port)
(self.host.clone(), self.port)
}
}
+19
View File
@@ -0,0 +1,19 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AgentError {
#[error("Failed when loading TLS certificate: {0}")]
LoadTLSCertError(#[from] rustls::Error),
#[error("I/O error: {0}")]
IOError(#[from] std::io::Error),
#[error("General error: {0}")]
GeneralError(String),
#[error("Internal server error: {0}")]
InternalServerError(String),
}
pub type Result<T> = std::result::Result<T, AgentError>;
+79
View File
@@ -0,0 +1,79 @@
use serde::{Serialize, Deserialize};
use crate::state;
use actix_web::{get, post, HttpResponse, Responder, HttpRequest, web};
#[derive(Debug, Serialize, Deserialize)]
struct ResponseData {
message: String,
code: i32,
}
/// Default endpoint
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().json(ResponseData {
message: "HBW agent is running.".to_string(),
code: 0,
})
}
/// Endpoint for initializing.
/// Initialize is divided into %d stages.
#[post("/init")]
async fn init(req: HttpRequest, info: web::Json<crate::init::InitData>, data: web::Data<state::AppState>) -> impl Responder {
// 检查是否启用TLS阻止访问
if data.use_tls {
return HttpResponse::Forbidden().body("Access forbidden: This agent is already configured. If you'd like to reconfigure, remove TLS section in agent.toml");
}
let mut token_verified = false;
// 检查Bearer Token
if let Some(auth_header) = req.headers().get("authorization") {
if let Ok(auth_str) = auth_header.to_str() {
if auth_str.starts_with("Bearer ") {
let token = &auth_str[7..]; // 去掉 "Bearer " 前缀
if token == data.token.as_ref().unwrap() {
// Token验证成功
token_verified = true;
}
}
}
}
if token_verified {
// Token验证成功,执行初始化操作
//
let query = querystring::querify(req.query_string());
let stage = query.get(0).unwrap_or(&("0","0"));
match stage.0 {
"1" => {
// 初始化阶段1
// todo!("实现初始化阶段1");
crate::init::init_stage1(info);
}
"2" => {
// 初始化阶段2
todo!("实现初始化阶段2");
}
_ => {
// 未知的初始化阶段
// HttpResponse::BadRequest().body("Unknown initialization stage")
return HttpResponse::BadRequest().body("Unknown initialization stage");
}
}
todo!("实现初始化操作");
HttpResponse::Ok().body("Initialization successful")
} else {
// Token验证失败
HttpResponse::Unauthorized().body("Invalid or missing Bearer token")
}
}
/// Endpoint for pinging the server.
#[get("/ping")]
async fn ping() -> impl Responder {
HttpResponse::Ok().json(ResponseData {
message: "pong".to_string(),
code: 0,
})
}
+19
View File
@@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
use actix_web::{web, HttpResponse};
use uuid::Uuid;
pub fn generate_token() -> String {
// 生成UUIDv4 token
Uuid::new_v4().to_string()
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InitData {
server_id: String,
timestamp: u64,
}
pub fn init_stage1(data: web::Json<InitData>) -> () {
}
+45
View File
@@ -0,0 +1,45 @@
use log::info;
use actix_web::{web, App, HttpServer};
mod config;
mod tls;
mod error;
mod handler;
mod init;
mod state;
#[actix_web::main]
async fn main() -> Result<(), ()> {
env_logger::init();
info!("Starting workshop-agent...");
let config = config::ServerConfig::init().unwrap();
let token = if config.is_tls_enabled() {
None
} else {
let _token = init::generate_token();
println!("Generated token: {}", _token);
Some(_token)
};
let appstate = web::Data::new(state::AppState {
use_tls: config.is_tls_enabled(),
token,
});
let mut server = HttpServer::new(move || App::new()
.app_data(appstate.clone())
.service(handler::index)
.service(handler::ping)
.service(handler::init));
if config.is_tls_enabled() {
let tls_certs_file = config.tls.clone().unwrap().cert;
let tls_key_file = config.tls.clone().unwrap().key;
let tls_config = tls::load_rustls_config(&tls_certs_file, &tls_key_file).unwrap();
info!("Configuring TLS...");
server = server.bind_rustls_0_23(config.bind_address(), tls_config).unwrap();
}
else {
server = server.bind(config.bind_address()).unwrap();
}
let _ = server.run().await;
Ok(())
}
+5
View File
@@ -0,0 +1,5 @@
#[derive(Clone)]
pub struct AppState {
pub token: Option<String>,
pub use_tls: bool,
}
+33
View File
@@ -0,0 +1,33 @@
use crate::error::AgentError;
use anyhow::Result;
use rustls::ServerConfig;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
pub fn load_rustls_config(
cert_path: &Path,
key_path: &Path,
) -> Result<rustls::ServerConfig, AgentError> {
// 初始化 TLS 配置
let config = ServerConfig::builder()
.with_no_client_auth();
// 加载 TLS 证书
let mut cert_file = &mut BufReader::new(File::open(cert_path)?);
let cert_chain = rustls_pemfile::certs(&mut cert_file)
.collect::<Result<Vec<_>, _>>()?;
// 加载私钥
let mut key_file = &mut BufReader::new(File::open(key_path)?);
let key = rustls_pemfile::pkcs8_private_keys(&mut key_file)
.next()
.unwrap();
if let Ok(key) = key {
Ok(config.with_single_cert(cert_chain, rustls::pki_types::PrivateKeyDer::Pkcs8(key))?)
} else {
// TODO: How to implement better?
Err(AgentError::GeneralError("Failed to load TLS key.".to_string()))
}
}
+3
View File
@@ -0,0 +1,3 @@
cert.toml
target
certs
+1250
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "workshop-cert"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "workshop_certgen"
path = "src/main.rs"
[lib]
name = "workshop_cert"
path = "src/lib.rs"
[dependencies]
anyhow = "1.0.100"
chrono = { version = "0.4.42", features = ["serde"] }
clap = { version = "4.5.51", features = ["derive"] }
env_logger = "0.11.8"
log = "0.4.28"
rcgen = { version = "0.14.5", features = ["x509-parser", "pem"] }
serde = { version = "1.0.228", features = ["derive"] }
time = "0.3.44"
tokio = { version = "1.48.0", features = ["full"] }
toml = "0.9.8"
+57
View File
@@ -0,0 +1,57 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertConfig {
pub ca: CAConfig,
pub server: ServerConfig,
pub client_template: ClientTemplateConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CAConfig {
pub common_name: String,
pub organization: String,
pub organizational_unit: String,
pub country: String,
pub state: String,
pub locality: String,
pub validity_days: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub common_name: String,
pub organization: String,
pub organizational_unit: String,
pub country: String,
pub state: String,
pub locality: String,
pub validity_days: u32,
pub dns_names: Vec<String>,
pub ip_addresses: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientTemplateConfig {
pub common_name_prefix: String,
pub organization: String,
pub organizational_unit: String,
pub country: String,
pub state: String,
pub locality: String,
pub validity_days: u32,
}
impl CertConfig {
pub fn from_file(path: &PathBuf) -> anyhow::Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: CertConfig = toml::from_str(&content)?;
Ok(config)
}
pub fn default_config() -> Self {
let content = include_str!("../cert.toml");
toml::from_str(content).expect("Failed to parse default config")
}
}
+280
View File
@@ -0,0 +1,280 @@
use rcgen::string::Ia5String;
use anyhow::Result;
use rcgen::{CertificateParams, DistinguishedName, DnType, Issuer, KeyPair, SanType};
use std::collections::HashMap;
use std::fs;
use std::path::{PathBuf};
use time::{OffsetDateTime, Duration};
// use log::info;
use crate::config::{CAConfig, CertConfig, ClientTemplateConfig, ServerConfig};
#[derive(Debug, Clone)]
pub struct CertificateBundle {
pub certificate: String,
pub private_key: String,
pub public_key: Option<String>,
}
pub struct CertificateGenerator {
pub config: CertConfig,
pub output_dir: PathBuf,
}
impl CertificateGenerator {
pub fn new(config: CertConfig, output_dir: PathBuf) -> Self {
Self { config, output_dir }
}
/// 生成 CA 证书(只需要生成一次)
pub fn generate_ca(&self) -> Result<CertificateBundle> {
println!("📝 生成 CA 根证书...");
let params = self.build_ca_params(&self.config.ca)?;
let key = KeyPair::generate()?;
let cert = params.self_signed(&key)?;
let public_key = key.public_key_pem();
let bundle = CertificateBundle {
certificate: cert.pem(),
private_key: key.serialize_pem(),
public_key: Some(public_key),
};
// 保存 CA 证书
self.save_certificate("ca-cert.pem", &bundle.certificate)?;
self.save_private_key("ca-key.pem", &bundle.private_key)?;
println!("✅ CA 证书生成完成");
Ok(bundle)
}
/// 生成服务器证书(只需要生成一次)
pub fn generate_server_certificate(&self, ca_bundle: &CertificateBundle) -> Result<CertificateBundle> {
println!("🖥️ 生成服务器证书...");
let ca_issuer = self.load_ca_certificate(&ca_bundle.certificate, &ca_bundle.private_key)?;
let params = self.build_server_params(&self.config.server)?;
let server_key_pair = KeyPair::generate()?;
let server_cert = params.signed_by(&server_key_pair, &ca_issuer)?;
let bundle = CertificateBundle {
certificate: server_cert.pem(),
private_key: server_key_pair.serialize_pem(),
public_key: None,
};
// 保存服务器证书
self.save_certificate("server-cert.pem", &bundle.certificate)?;
self.save_private_key("server-key.pem", &bundle.private_key)?;
println!("✅ 服务器证书生成完成");
Ok(bundle)
}
/// 生成客户端证书(可以生成多次)
pub fn generate_client_certificate(
&self,
ca_bundle: &CertificateBundle,
client_id: &str,
) -> Result<CertificateBundle> {
println!("👤 生成客户端证书: {}...", client_id);
let ca_issuer = self.load_ca_certificate(&ca_bundle.certificate, &ca_bundle.private_key)?;
let params = self.build_client_params(&self.config.client_template, client_id)?;
let client_key_pair = KeyPair::generate()?;
let client_cert = params.signed_by(&client_key_pair, &ca_issuer)?;
let bundle = CertificateBundle {
certificate: client_cert.pem(),
private_key: client_key_pair.serialize_pem(),
public_key: None,
};
// 保存客户端证书,使用 client_id 作为文件名
let cert_filename = format!("client-{}-cert.pem", client_id);
let key_filename = format!("client-{}-key.pem", client_id);
self.save_certificate(&cert_filename, &bundle.certificate)?;
self.save_private_key(&key_filename, &bundle.private_key)?;
println!("✅ 客户端证书 {} 生成完成", client_id);
Ok(bundle)
}
/// 批量生成客户端证书
pub fn generate_client_certificates(
&self,
ca_bundle: &CertificateBundle,
client_ids: &[String],
) -> Result<HashMap<String, CertificateBundle>> {
let mut results = HashMap::new();
for client_id in client_ids {
let bundle = self.generate_client_certificate(ca_bundle, client_id)?;
results.insert(client_id.clone(), bundle);
}
Ok(results)
}
/// 构建 CA 证书参数
fn build_ca_params(&self, config: &CAConfig) -> Result<CertificateParams> {
let mut params = CertificateParams::new(vec![])?;
params.distinguished_name = self.build_distinguished_name(
&config.common_name,
&config.organization,
&config.organizational_unit,
&config.country,
&config.state,
&config.locality,
);
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params.key_usages = vec![
rcgen::KeyUsagePurpose::KeyCertSign,
rcgen::KeyUsagePurpose::CrlSign,
rcgen::KeyUsagePurpose::DigitalSignature,
];
// 设置有效期
params.not_before = OffsetDateTime::now_utc();
let duration = Duration::days(config.validity_days as i64);
params.not_after = params.not_before + duration;
Ok(params)
}
/// 构建服务器证书参数
fn build_server_params(&self, config: &ServerConfig) -> Result<CertificateParams> {
let mut params = CertificateParams::new(vec![config.common_name.clone()])?;
params.distinguished_name = self.build_distinguished_name(
&config.common_name,
&config.organization,
&config.organizational_unit,
&config.country,
&config.state,
&config.locality,
);
params.is_ca = rcgen::IsCa::ExplicitNoCa;
// 添加主题备用名称
let mut san_entries = Vec::new();
for dns_name in &config.dns_names {
let dns_name = Ia5String::try_from(dns_name.clone())?;
san_entries.push(SanType::DnsName(dns_name));
}
for ip_addr in &config.ip_addresses {
if let Ok(ip) = ip_addr.parse() {
san_entries.push(SanType::IpAddress(ip));
}
}
params.subject_alt_names = san_entries;
params.key_usages = vec![
rcgen::KeyUsagePurpose::DigitalSignature,
rcgen::KeyUsagePurpose::KeyEncipherment,
];
params.extended_key_usages = vec![
rcgen::ExtendedKeyUsagePurpose::ServerAuth,
];
// 设置有效期
params.not_before = OffsetDateTime::now_utc();
let duration = Duration::days(config.validity_days as i64);
params.not_after = params.not_before + duration;
Ok(params)
}
/// 构建客户端证书参数
fn build_client_params(&self, config: &ClientTemplateConfig, client_id: &str) -> Result<CertificateParams> {
let common_name = format!("{}-{}", config.common_name_prefix, client_id);
let mut params = CertificateParams::new(vec![common_name.clone()])?;
params.distinguished_name = self.build_distinguished_name(
&common_name,
&config.organization,
&config.organizational_unit,
&config.country,
&config.state,
&config.locality,
);
params.is_ca = rcgen::IsCa::ExplicitNoCa;
params.key_usages = vec![
rcgen::KeyUsagePurpose::DigitalSignature,
rcgen::KeyUsagePurpose::KeyAgreement,
];
params.extended_key_usages = vec![
rcgen::ExtendedKeyUsagePurpose::ClientAuth,
];
// 设置有效期
params.not_before = OffsetDateTime::now_utc();
let duration = Duration::days(config.validity_days as i64);
params.not_after = params.not_before + duration;
Ok(params)
}
/// 构建可分辨名称
fn build_distinguished_name(
&self,
common_name: &str,
organization: &str,
organizational_unit: &str,
country: &str,
state: &str,
locality: &str,
) -> DistinguishedName {
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, common_name);
dn.push(DnType::OrganizationName, organization);
dn.push(DnType::OrganizationalUnitName, organizational_unit);
dn.push(DnType::CountryName, country);
dn.push(DnType::StateOrProvinceName, state);
dn.push(DnType::LocalityName, locality);
dn
}
/// 从 PEM 数据加载 CA 证书
fn load_ca_certificate(&self, cert_pem: &str, key_pem: &str) -> Result<Issuer<'static, KeyPair>> {
let key = KeyPair::from_pem(key_pem)?;
let issuer = Issuer::from_ca_cert_pem(cert_pem, key)?;
Ok(issuer)
}
/// 保存证书到文件
fn save_certificate(&self, filename: &str, pem_data: &str) -> Result<()> {
let path = self.output_dir.join(filename);
fs::write(&path, pem_data)?;
println!(" ✅ 保存证书: {}", path.display());
Ok(())
}
/// 保存私钥到文件
fn save_private_key(&self, filename: &str, pem_data: &str) -> Result<()> {
let path = self.output_dir.join(filename);
fs::write(&path, pem_data)?;
println!(" ✅ 保存私钥: {}", path.display());
Ok(())
}
/// 确保输出目录存在
pub fn ensure_output_dir(&self) -> Result<()> {
if !self.output_dir.exists() {
fs::create_dir_all(&self.output_dir)?;
println!("📁 创建输出目录: {}", self.output_dir.display());
}
Ok(())
}
}
+7
View File
@@ -0,0 +1,7 @@
mod config;
mod generator;
pub use config::*;
pub use generator::*;
pub type Result<T> = anyhow::Result<T>;
+144
View File
@@ -0,0 +1,144 @@
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use workshop_cert::{CertConfig, CertificateGenerator, Result};
#[derive(Parser)]
#[command(name = "workshop-certgen")]
#[command(about = "Generate TLS certificates for Workshop CI/CD system")]
struct Cli {
#[command(subcommand)]
command: Commands,
/// Configuration file path
#[arg(short, long, default_value = "cert.toml")]
config: PathBuf,
/// Output directory for certificates
#[arg(short, long, default_value = "./certs")]
output_dir: PathBuf,
}
#[derive(Subcommand)]
enum Commands {
/// Generate CA certificate (one-time operation)
GenCa,
/// Generate server certificate (one-time operation)
GenServer,
/// Generate client certificate(s)
GenClient {
/// Client ID(s) to generate certificates for
#[arg(required = true)]
client_ids: Vec<String>,
},
/// Generate all certificates (CA, server, and sample client)
GenAll {
/// Also generate sample client certificates
#[arg(short, long)]
with_clients: bool,
/// Number of sample client certificates to generate
#[arg(short, long, default_value = "3")]
sample_count: usize,
},
/// Show current configuration
ShowConfig,
}
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
let args = Cli::parse();
// 加载配置
let config = if args.config.exists() {
CertConfig::from_file(&args.config)?
} else {
println!("⚠️ 配置文件 {} 不存在,使用默认配置", args.config.display());
CertConfig::default_config()
};
let generator = CertificateGenerator::new(config, args.output_dir.clone());
generator.ensure_output_dir()?;
match args.command {
Commands::GenCa => {
println!("🔐 生成 CA 证书...");
generator.generate_ca()?;
}
Commands::GenServer => {
println!("🔐 生成服务器证书...");
// 首先需要 CA 证书
let ca_bundle = if generator.output_dir.join("ca-cert.pem").exists() {
// 从文件加载现有的 CA 证书
let cert_pem = std::fs::read_to_string(generator.output_dir.join("ca-cert.pem"))?;
let key_pem = std::fs::read_to_string(generator.output_dir.join("ca-key.pem"))?;
workshop_cert::CertificateBundle {
certificate: cert_pem,
private_key: key_pem,
public_key: None,
}
} else {
println!("❌ 未找到 CA 证书,请先运行 'gen-ca' 命令");
return Ok(());
};
generator.generate_server_certificate(&ca_bundle)?;
}
Commands::GenClient { client_ids } => {
println!("🔐 生成客户端证书: {:?}...", client_ids);
// 需要 CA 证书
let ca_bundle = if generator.output_dir.join("ca-cert.pem").exists() {
let cert_pem = std::fs::read_to_string(generator.output_dir.join("ca-cert.pem"))?;
let key_pem = std::fs::read_to_string(generator.output_dir.join("ca-key.pem"))?;
workshop_cert::CertificateBundle {
certificate: cert_pem,
private_key: key_pem,
public_key: None,
}
} else {
println!("❌ 未找到 CA 证书,请先运行 'gen-ca' 命令");
return Ok(());
};
generator.generate_client_certificates(&ca_bundle, &client_ids)?;
}
Commands::GenAll { with_clients, sample_count } => {
println!("🔐 生成所有证书...");
// 1. 生成 CA
let ca_bundle = generator.generate_ca()?;
// 2. 生成服务器证书
generator.generate_server_certificate(&ca_bundle)?;
// 3. 可选:生成示例客户端证书
if with_clients {
let sample_clients: Vec<String> = (1..=sample_count)
.map(|i| format!("sample{}", i))
.collect();
println!("👥 生成示例客户端证书: {:?}", sample_clients);
generator.generate_client_certificates(&ca_bundle, &sample_clients)?;
}
}
Commands::ShowConfig => {
println!("📋 当前配置:");
println!("{:#?}", generator.config);
}
}
println!("✅ 操作完成!");
Ok(())
}