refactor: remove unused crates, refactor fetch

- Move fetch_plugin from finalize::plugin::fetch to utils::fetch_plugin
- Delete unused crates: workshop-cert, workshop-deviceid, workshop-vault
- Remove parser benchmarks and empty daemon/socket modules
- Add new workshop-getterurl crate for resource fetching
This commit is contained in:
Catty Steve
2026-05-19 21:53:41 +08:00
parent 3e80825d58
commit 6e7b96cd66
46 changed files with 2552 additions and 7473 deletions
-3
View File
@@ -67,6 +67,3 @@ unreachable_code = "allow"
inherent_to_string = "allow"
non_canonical_partial_ord_impl = "allow"
[[bench]]
name = "parser_bench"
harness = false
-93
View File
@@ -1,93 +0,0 @@
#!/usr/bin/env python3
import os
import random
import string
OUTPUT_DIR = "/tmp"
DECORATORS = [
"# @pipeline",
"# @fallible",
"# @parallel",
"# @export",
"# @timeout(60)",
"# @retry(3, 5)",
"# @if(condition)",
"# @after(dep)",
"# @pipe(step1, step2)",
"# @health(/health)",
"# @loop(10)",
]
FUNCTION_BODIES = [
"echo 'Processing...';",
"local var=$(date +%s);",
"if [ -f /tmp/test ]; then echo 'exists'; fi;",
"for i in $(seq 1 10); do echo $i; done;",
"case $1 in start) echo 'starting';; stop) echo 'stopping';; esac;",
"read -r line < /dev/stdin;",
"export PATH=$PATH:/usr/local/bin;",
"set -e; set -u;",
"trap 'echo error' ERR;",
"cd /tmp || exit 1;",
"local result=$(grep -r 'pattern' /var/log/ 2>/dev/null);",
'for f in /tmp/*.tmp; do rm -f "$f"; done',
"if [[ $var -gt 100 ]]; then echo 'large'; fi;",
'while read -r line; do echo "$line"; done < /etc/passwd;',
'select opt in a b c; do echo "Selected $opt"; break; done;',
]
def random_string(length=8):
return "".join(random.choices(string.ascii_letters + string.digits, k=length))
def generate_function(body_lines):
lines = []
func_name = f"func_{random_string(6)}"
for _ in range(random.randint(0, 3)):
lines.append(random.choice(DECORATORS))
lines.append(f"{func_name}() {{")
for _ in range(body_lines):
lines.append(" " + random.choice(FUNCTION_BODIES))
lines.append("}")
lines.append("")
return lines
def generate_script(target_lines, num_functions, target_bytes):
lines = []
total_lines = 0
body_per_func = max(1, (target_lines - num_functions * 3) // num_functions)
for _ in range(num_functions):
func_lines = generate_function(body_per_func)
lines.extend(func_lines)
total_lines += len(func_lines)
while (
total_lines < target_lines
or len("\n".join(lines).encode("utf-8")) < target_bytes
):
lines.append(random.choice(FUNCTION_BODIES))
total_lines += 1
return "\n".join(lines)
def main():
sizes = [
("small", 1000, 10, 50_000),
("medium", 5000, 50, 500_000),
("large", 10000, 100, 950_000),
]
for name, lines, funcs, target_bytes in sizes:
script = generate_script(lines, funcs, target_bytes)
filepath = os.path.join(OUTPUT_DIR, f"bench_{name}.sh")
with open(filepath, "w") as f:
f.write(script)
actual_bytes = len(script.encode("utf-8"))
actual_lines = len(script.splitlines())
print(f"Generated {filepath}: lines={actual_lines}, bytes={actual_bytes}")
if __name__ == "__main__":
main()
-61
View File
@@ -1,61 +0,0 @@
use criterion::{Criterion, black_box, criterion_group, criterion_main};
use std::fs;
use std::path::Path;
fn parse_large_script(c: &mut Criterion) {
let script_path = Path::new("/tmp/bench_large.sh");
let script_content = fs::read_to_string(script_path)
.expect("Failed to read benchmark script. Run `python3 benches/generate.py` first.");
let byte_size = script_content.len();
let line_count = script_content.lines().count();
println!("Benchmark file: {} lines, {} bytes", line_count, byte_size);
c.bench_function("parse_script_large", |b| {
b.iter(|| {
let result =
workshop_baker::bake::parser::parse_script(black_box(&script_content.clone()));
black_box(result)
});
});
}
fn parse_medium_script(c: &mut Criterion) {
let script_path = Path::new("/tmp/bench_medium.sh");
let script_content = fs::read_to_string(script_path)
.expect("Failed to read benchmark script. Run `python3 benches/generate.py` first.");
c.bench_function("parse_script_medium", |b| {
b.iter(|| {
let result =
workshop_baker::bake::parser::parse_script(black_box(&script_content.clone()));
black_box(result)
});
});
}
fn parse_small_script(c: &mut Criterion) {
let script_path = Path::new("/tmp/bench_small.sh");
let script_content = fs::read_to_string(script_path)
.expect("Failed to read benchmark script. Run `python3 benches/generate.py` first.");
c.bench_function("parse_script_small", |b| {
b.iter(|| {
let result =
workshop_baker::bake::parser::parse_script(black_box(&script_content.clone()));
black_box(result)
});
});
}
criterion_group!(
benches,
parse_small_script,
parse_medium_script,
parse_large_script
);
criterion_main!(benches);
View File
+2 -2
View File
@@ -9,7 +9,7 @@ pub mod types;
use crate::cli::Cli;
use crate::finalize::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
use crate::finalize::plugin::fetch::{self, FetchArgument};
use crate::utils::fetch_plugin::{self, FetchArgument};
pub use config::*;
pub use error::FinalizeError;
use std::path::Path;
@@ -55,7 +55,7 @@ pub async fn finalize(
checksum: config.checksum.clone(),
shallow: config.shallow,
};
fetch::fetch_plugin(source, name, &plugin_path, fetch_argument).await?;
fetch_plugin::fetch_plugin(source, name, &plugin_path, fetch_argument).await?;
config.runtime.fspath = Some(plugin_path.join(name));
}
}
+7
View File
@@ -1,3 +1,4 @@
use crate::utils::fetch::FetchError;
use std::path::PathBuf;
use thiserror::Error;
@@ -55,6 +56,12 @@ impl HasExitCode for FinalizeError {
}
}
impl From<FetchError> for FinalizeError {
fn from(e: FetchError) -> Self {
FinalizeError::PluginError(PluginError::GeneralError(e.to_string()))
}
}
#[derive(Error, Debug)]
pub enum PluginError {
#[error("Unknown plugin: {0}")]
-1
View File
@@ -9,7 +9,6 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
mod dylib;
pub mod fetch;
mod internal;
mod metadata;
mod rhai;
-2
View File
@@ -1,11 +1,9 @@
pub mod bare;
pub mod bake;
pub mod daemon;
pub mod error;
pub mod finalize;
pub mod notify;
pub mod prebake;
pub mod socket;
pub mod types;
pub mod utils;
+2 -2
View File
@@ -5,7 +5,7 @@ use crate::finalize::config::TriggerItemRaw;
use crate::finalize::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
use crate::finalize::parse;
use crate::finalize::plugin;
use crate::finalize::plugin::fetch;
use crate::utils::fetch_plugin;
use crate::notify::types::Matchable;
use crate::notify::types::Method;
use crate::notify::types::MethodStatus;
@@ -102,7 +102,7 @@ pub async fn notify(
}
if ctx.standalone {
fetch::fetch_plugins(
fetch_plugin::fetch_plugins(
&PathBuf::from(FINALIZE_STANDALONE_PLUGIN_PATH),
&mut finalize.plugin,
Some(&plugin_list),
View File
+1
View File
@@ -1 +1,2 @@
pub mod fetch;
pub mod fetch_plugin;
+12 -12
View File
@@ -11,6 +11,7 @@ use std::path::{Path, PathBuf};
pub use error::FetchError;
pub use scheme::Scheme;
pub use types::FetchArgument;
/// Fetch an external resource to a target directory. Returns the local path.
///
@@ -26,19 +27,18 @@ pub async fn fetch_resource(
) -> Result<PathBuf, FetchError> {
let scheme = scheme::parse_url(url)?;
match scheme {
Scheme::Git { repo, git_ref: _ } => {
// TODO: adapt git.rs to new API
Err(FetchError::General(format!(
"Git fetch for '{}' not yet adapted to new fetch API",
repo
)))
Scheme::Git { repo, git_ref } => {
let full_url = if git_ref.is_empty() {
repo.clone()
} else {
format!("{}@{}", repo, git_ref)
};
git::fetch_git_plugin(&full_url, name, target, None).await?;
Ok(target.join(name))
}
Scheme::Http { url, checksum: _ } => {
// TODO: adapt http.rs to new API
Err(FetchError::General(format!(
"HTTP fetch for '{}' not yet adapted to new fetch API",
url
)))
Scheme::Http { url, checksum } => {
http::fetch_http_plugin(&url, name, target, checksum.as_deref()).await?;
Ok(target.join(name))
}
Scheme::File { path } => file::fetch_file(&path, name, target).await,
}
+73
View File
@@ -0,0 +1,73 @@
use std::collections::HashSet;
use std::path::Path;
use crate::finalize::RawPluginMap;
use crate::utils::fetch::{self, FetchError};
pub use crate::utils::fetch::FetchArgument;
/// Fetch a single plugin by URL.
///
/// Supports:
/// - `workspace://rel/path` → resolved to `file:///abs/path`
/// - `git://host/repo@tag` → git clone
/// - `https://url/archive.tar.gz` → HTTP download + checksum + extract
/// - `file:///path` → local file copy
pub async fn fetch_plugin(
url: &str,
name: &str,
basedir: &Path,
_argument: FetchArgument,
) -> Result<(), FetchError> {
let resolved_url = if let Some(rel) = url.strip_prefix("workspace://") {
format!(
"file://{}",
std::env::current_dir()
.unwrap_or_default()
.join(rel)
.display()
)
} else {
url.to_string()
};
fetch::fetch_resource(name, &resolved_url, basedir)
.await
.map(|_| ())
}
/// Fetch all listed plugins from a RawPluginMap.
///
/// Iterates over the plugin map, fetches each plugin that is in the
/// optional `list`, and updates `config.runtime.fspath` with the
/// downloaded path.
pub async fn fetch_plugins(
plugin_path: &Path,
plugin_config: &mut RawPluginMap,
list: Option<&HashSet<String>>,
) -> Result<(), FetchError> {
log::info!("Preparing to download {} plugins", plugin_config.len());
for (name, data) in plugin_config.iter_mut() {
if let Some(list) = list {
if !list.contains(name) {
log::debug!("Plugin {} not in list, skipping", name);
continue;
}
}
log::debug!("Downloading plugin {}", name);
let (source, config) = data.iter_mut().next().ok_or_else(|| {
FetchError::FetchFailed {
name: name.clone(),
url: String::new(),
reason: "no source configured".to_string(),
}
})?;
let fetch_argument = FetchArgument {
checksum: config.checksum.clone(),
shallow: config.shallow,
};
fetch_plugin(source, name, plugin_path, fetch_argument).await?;
log::debug!("Storing plugin {} to {}", name, plugin_path.display());
config.runtime.fspath = Some(plugin_path.join(name));
}
Ok(())
}
-3
View File
@@ -1,3 +0,0 @@
cert.toml
target
certs
-1250
View File
File diff suppressed because it is too large Load Diff
-24
View File
@@ -1,24 +0,0 @@
[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"
-49
View File
@@ -1,49 +0,0 @@
# workshop-cert/src
**Generated:** 2026-04-22
**Commit:** 7b3b71a
TLS certificate generator (CA, server, client certificates).
## STRUCTURE
```
src/
├── main.rs # CLI entry (gen-ca, gen-server, gen-client, gen-all)
├── lib.rs # Library exports
├── generator.rs # Certificate generation logic
└── config.rs # Certificate config from cert.toml
```
## WHERE TO LOOK
| Task | Location |
|------|----------|
| CLI commands | `main.rs:144` - 4 subcommands |
| Cert generation | `generator.rs` - rcgen-based |
| Config loading | `config.rs` - cert.toml parser |
## BINARY
```bash
workshop_certgen gen-ca # Generate CA cert/key
workshop_certgen gen-server # Generate server cert
workshop_certgen gen-client # Generate client cert
workshop_certgen gen-all # Generate all
```
## ANTI-PATTERNS
**CRITICAL**: `certs/` directory contains private keys in source:
- `ca-key.pem` - CA private key
- `server-key.pem` - Server private key
These should be gitignored and generated at deploy time.
## CONFIG
`cert.toml` defines:
- CA validity: 10 years
- Server/client validity: 1 year
- Subject fields (CN, O, OU)
- Key algorithm (Ed25519)
-57
View File
@@ -1,57 +0,0 @@
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
@@ -1,280 +0,0 @@
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
@@ -1,7 +0,0 @@
mod config;
mod generator;
pub use config::*;
pub use generator::*;
pub type Result<T> = anyhow::Result<T>;
-144
View File
@@ -1,144 +0,0 @@
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(())
}
-1
View File
@@ -1 +0,0 @@
/target
-587
View File
@@ -1,587 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "cc"
version = "1.2.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link 0.2.1",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "js-sys"
version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "raw-cpuid"
version = "11.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sysinfo"
version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "whoami"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
dependencies = [
"libredox",
"wasite",
"web-sys",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-link 0.1.3",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core",
"windows-link 0.1.3",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-link 0.1.3",
]
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "workshop-deviceid"
version = "0.1.0"
dependencies = [
"chrono",
"lazy_static",
"raw-cpuid",
"serde",
"sha2",
"sysinfo",
"whoami",
]
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "workshop-deviceid"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.42"
lazy_static = "1.5.0"
raw-cpuid = "11.6.0"
serde = { version = "1.0.228", features = ["derive"] }
sha2 = "0.10.9"
sysinfo = "0.37.2"
whoami = "1.6.1"
@@ -1,33 +0,0 @@
extern crate workshop_deviceid;
use workshop_deviceid::DeviceIdGenerator;
// server/src/auth/device_id_example.rs
// 主要使用示例
fn main() -> Result<(), ()> {
println!("=== 设备ID生成器 ===");
// 生成标准设备ID
let device_id = DeviceIdGenerator::generate_device_id()?;
println!("设备ID: {}", device_id);
// 生成短设备ID
let short_id = DeviceIdGenerator::generate_short_device_id()?;
println!("短设备ID: {}", short_id);
// 生成详细报告
let report = DeviceIdGenerator::generate_device_report()?;
println!("\n=== 设备详细信息 ===");
println!("用户名: {}", report.device_info.username);
println!("主机名: {}", report.device_info.hostname);
println!("操作系统: {} {}", report.device_info.os_name, report.device_info.os_version);
println!("架构: {}", report.device_info.architecture);
println!("CPU: {} {}", report.device_info.cpu_info.vendor_id, report.device_info.cpu_info.brand);
println!("CPU特性: {}", report.device_info.cpu_info.features.join(", "));
// 验证设备ID
let is_valid = DeviceIdGenerator::verify_device_id(&device_id)?;
println!("\n设备ID验证: {}", if is_valid { "通过" } else { "失败" });
Ok(())
}
-139
View File
@@ -1,139 +0,0 @@
// device_info.rs
use raw_cpuid::{CpuId, CpuIdReaderNative};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use sysinfo::{System};
use whoami::{username};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInfo {
pub username: String,
pub hostname: String,
pub os_name: String,
pub os_version: String,
pub architecture: String,
pub cpu_info: CpuInfo,
pub system_metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CpuInfo {
pub vendor_id: String,
pub brand: String,
pub family: u8,
pub model: u8,
pub stepping: u8,
pub features: Vec<String>,
pub cache_size: Option<u32>,
}
pub struct DeviceInfoCollector;
impl DeviceInfoCollector {
pub fn collect_device_info() -> Result<DeviceInfo, ()> {
let mut system = System::new();
system.refresh_all();
Ok(DeviceInfo {
username: Self::get_username(),
hostname: Self::get_hostname(),
os_name: Self::get_os_name(),
os_version: Self::get_os_version(),
architecture: Self::get_architecture(),
cpu_info: Self::get_cpu_info(),
system_metadata: Self::get_system_metadata(&system),
})
}
fn get_username() -> String {
// 使用 whoami 库获取用户名
username()
}
fn get_hostname() -> String {
// 使用 whoami 库获取主机名
whoami::fallible::hostname().unwrap_or_else(|_| "unknown".to_string())
}
fn get_os_name() -> String {
// 获取操作系统名称
System::name()
.unwrap_or_else(|| "unknown".to_string())
}
fn get_os_version() -> String {
// 获取操作系统版本
System::os_version()
.unwrap_or_else(|| "unknown".to_string())
}
fn get_architecture() -> String {
// 获取系统架构
std::env::consts::ARCH.to_string()
}
fn get_cpu_info() -> CpuInfo {
// 使用 cpuid 库获取详细的CPU信息
let cpuid = CpuId::new();
let vendor_id = cpuid.get_vendor_info()
.map(|v| v.as_str().to_string())
.unwrap_or_else(|| "unknown".to_string());
let brand = cpuid.get_processor_brand_string()
.map(|b| b.as_str().to_string())
.unwrap_or_else(|| "unknown".to_string());
let (family, model, stepping) = cpuid.get_feature_info()
.map(|f| (f.family_id(), f.model_id(), f.stepping_id()))
.unwrap_or((0, 0, 0));
// 获取CPU特性
let features = Self::get_cpu_features(&cpuid);
// 获取缓存信息
let cache_size = Self::get_cache_size(&cpuid);
CpuInfo {
vendor_id,
brand,
family,
model,
stepping,
features,
cache_size,
}
}
fn get_cpu_features(cpuid: &CpuId<CpuIdReaderNative>) -> Vec<String> {
let mut features = Vec::new();
if let Some(extended_features) = cpuid.get_extended_feature_info() {
if extended_features.has_avx2() {
features.push("avx2".to_string());
}
if extended_features.has_avx512f() {
features.push("avx512f".to_string());
}
}
features
}
fn get_cache_size(_cpuid: &raw_cpuid::CpuId<CpuIdReaderNative>) -> Option<u32> {
// 尝试获取L2或L3缓存大小
// if let Some(cache_info) = cpuid.get_cache_info() {
// for cache in cache_info {
// if cache.level() == 2 || cache.level() == 3 {
// return Some(cache.size() as u32);
// }
// }
// }
Some(0)
}
fn get_system_metadata(_system: &System) -> HashMap<String, String> {
let metadata = HashMap::new();
metadata
}
}
-164
View File
@@ -1,164 +0,0 @@
// device_id_generator.rs
use serde::Serialize;
mod device_info;
use crate::device_info::{DeviceInfo, DeviceInfoCollector};
use sha2::{Sha256, Digest};
use std::collections::HashMap;
pub struct DeviceIdGenerator;
impl DeviceIdGenerator {
/// 生成设备ID - 主要方法
pub fn generate_device_id() -> Result<String, ()> {
let device_info = DeviceInfoCollector::collect_device_info()?;
Self::calculate_device_hash(&device_info)
}
/// 生成设备ID - 带自定义元数据
pub fn generate_device_id_with_metadata(
additional_metadata: &HashMap<String, String>
) -> Result<String, ()> {
let mut device_info = DeviceInfoCollector::collect_device_info()?;
// 合并额外的元数据
for (key, value) in additional_metadata {
device_info.system_metadata.insert(key.clone(), value.clone());
}
Self::calculate_device_hash(&device_info)
}
/// 计算设备信息的SHA256哈希
fn calculate_device_hash(device_info: &DeviceInfo) -> Result<String, ()> {
let data_string = Self::serialize_device_info(device_info);
let hash = Self::calculate_sha256(&data_string);
Ok(hash)
}
/// 将设备信息序列化为字符串用于哈希计算
fn serialize_device_info(device_info: &DeviceInfo) -> String {
// 按照固定顺序拼接关键信息,确保一致性
let mut components = Vec::new();
// 基础标识信息
components.push(format!("username:{}", device_info.username));
components.push(format!("hostname:{}", device_info.hostname));
components.push(format!("os_name:{}", device_info.os_name));
components.push(format!("os_version:{}", device_info.os_version));
components.push(format!("architecture:{}", device_info.architecture));
// CPU标识信息 - 这些是相对稳定的
components.push(format!("cpu_vendor:{}", device_info.cpu_info.vendor_id));
components.push(format!("cpu_brand:{}", device_info.cpu_info.brand));
components.push(format!("cpu_family:{}", device_info.cpu_info.family));
components.push(format!("cpu_model:{}", device_info.cpu_info.model));
components.push(format!("cpu_stepping:{}", device_info.cpu_info.stepping));
// 按字母顺序添加CPU特性,确保一致性
let mut sorted_features = device_info.cpu_info.features.clone();
sorted_features.sort();
components.push(format!("cpu_features:{}", sorted_features.join(",")));
if let Some(cache_size) = device_info.cpu_info.cache_size {
components.push(format!("cpu_cache:{}", cache_size));
}
// 按字母顺序添加系统元数据,确保一致性
let mut sorted_metadata: Vec<_> = device_info.system_metadata.iter().collect();
sorted_metadata.sort_by_key(|(k, _)| *k);
for (key, value) in sorted_metadata {
components.push(format!("{}:{}", key, value));
}
components.join("|")
}
/// 计算SHA256哈希值
fn calculate_sha256(data: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(data.as_bytes());
let result = hasher.finalize();
// 转换为十六进制字符串
result.iter()
.map(|byte| format!("{:02x}", byte))
.collect()
}
/// 验证设备ID是否匹配当前设备
pub fn verify_device_id(device_id: &str) -> Result<bool, ()> {
let current_device_id = Self::generate_device_id()?;
Ok(current_device_id == device_id)
}
/// 获取设备ID的短版本(前16字符)
pub fn generate_short_device_id() -> Result<String, ()> {
let full_id = Self::generate_device_id()?;
Ok(full_id[..16].to_string())
}
/// 获取设备信息的详细报告
pub fn generate_device_report() -> Result<DeviceReport, ()> {
let device_info = DeviceInfoCollector::collect_device_info()?;
let device_id = Self::calculate_device_hash(&device_info)?;
Ok(DeviceReport {
device_id,
device_info,
generated_at: chrono::Utc::now().to_string(),
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DeviceReport {
pub device_id: String,
pub device_info: DeviceInfo,
pub generated_at: String,
}
#[cfg(test)]
mod tests {
use DeviceIdGenerator;
use super::*;
#[test]
fn test_device_id_generation() {
let device_id = DeviceIdGenerator::generate_device_id().unwrap();
assert_eq!(device_id.len(), 64); // SHA256应该是64字符
// 验证设备ID一致性
let device_id2 = DeviceIdGenerator::generate_device_id().unwrap();
assert_eq!(device_id, device_id2);
}
#[test]
fn test_device_verification() {
let device_id = DeviceIdGenerator::generate_device_id().unwrap();
let is_valid = DeviceIdGenerator::verify_device_id(&device_id).unwrap();
assert!(is_valid);
// 验证错误的设备ID
let fake_id = "a".repeat(64);
let is_valid = DeviceIdGenerator::verify_device_id(&fake_id).unwrap();
assert!(!is_valid);
}
#[test]
fn test_device_report() {
let report = DeviceIdGenerator::generate_device_report().unwrap();
println!("Device ID: {}", report.device_id);
println!("Username: {}", report.device_info.username);
println!("Hostname: {}", report.device_info.hostname);
println!("OS: {} {}", report.device_info.os_name, report.device_info.os_version);
println!("CPU: {} {}", report.device_info.cpu_info.vendor_id, report.device_info.cpu_info.brand);
assert!(!report.device_id.is_empty());
assert!(!report.device_info.username.is_empty());
assert!(!report.device_info.hostname.is_empty());
}
}
+1683
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "workshop-getterurl"
version = "0.1.0"
edition = "2024"
[dependencies]
log = "0.4.29"
reqwest = { version = "0.13.3", default-features = false, features = ["rustls"] }
sha2 = "0.11.0"
thiserror = "2.0.18"
tokio = { version = "1.52.3", features = ["process", "io-util", "fs", "rt-multi-thread", "macros"] }
url = "2.5.8"
+73
View File
@@ -0,0 +1,73 @@
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use url::Url;
use crate::{Getter, GetterError, Params};
/// Copies a local file or directory to the destination.
pub struct FileGetter;
impl Getter for FileGetter {
fn name(&self) -> &str {
"file"
}
fn fetch<'a>(
&'a self,
url: &'a Url,
_params: &'a Params,
dst: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<PathBuf, GetterError>> + Send + 'a>> {
Box::pin(async move {
let src_path = PathBuf::from(url.path());
if !src_path.exists() {
return Err(GetterError::FileNotFound(src_path.display().to_string()));
}
let name = src_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("resource");
let dest = dst.join(name);
if src_path.is_dir() {
copy_dir_all(&src_path, &dest).await?;
} else {
if let Some(parent) = dest.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::copy(&src_path, &dest).await?;
}
Ok(dest)
})
}
}
async fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), GetterError> {
tokio::fs::create_dir_all(dst).await?;
let mut entries = tokio::fs::read_dir(src).await?;
while let Some(entry) = entries.next_entry().await? {
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if entry.file_type().await?.is_dir() {
Box::pin(copy_dir_all(&src_path, &dst_path)).await?;
} else {
tokio::fs::copy(&src_path, &dst_path).await?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_getter_name() {
assert_eq!(FileGetter.name(), "file");
}
}
+103
View File
@@ -0,0 +1,103 @@
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use url::Url;
use crate::{Getter, GetterError, Params};
/// Fetches a git repository via `git clone` + optional `git checkout`.
pub struct GitGetter;
impl Getter for GitGetter {
fn name(&self) -> &str {
"git"
}
fn fetch<'a>(
&'a self,
url: &'a Url,
params: &'a Params,
dst: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<PathBuf, GetterError>> + Send + 'a>> {
Box::pin(async move {
let repo_dir = dst.join(extract_name(url));
if repo_dir.exists() {
tokio::fs::remove_dir_all(&repo_dir).await.map_err(|e| {
GetterError::GitError {
url: url.to_string(),
reason: format!("Failed to remove existing directory: {}", e),
}
})?;
}
let clone_url = url.as_str();
let mut cmd = tokio::process::Command::new("git");
cmd.arg("clone");
if let Some(depth) = params.depth {
cmd.arg("--depth").arg(depth.to_string());
}
cmd.arg(clone_url).arg(&repo_dir);
let output = cmd.output().await.map_err(|e| GetterError::GitError {
url: clone_url.to_string(),
reason: e.to_string(),
})?;
if !output.status.success() {
return Err(GetterError::GitError {
url: clone_url.to_string(),
reason: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
if let Some(ref git_ref) = params.git_ref {
let mut checkout = tokio::process::Command::new("git");
checkout.arg("checkout").arg(git_ref).current_dir(&repo_dir);
let out = checkout.output().await.map_err(|e| {
GetterError::GitCheckoutError {
git_ref: git_ref.clone(),
reason: e.to_string(),
}
})?;
if !out.status.success() {
return Err(GetterError::GitCheckoutError {
git_ref: git_ref.clone(),
reason: String::from_utf8_lossy(&out.stderr).to_string(),
});
}
}
Ok(repo_dir)
})
}
}
fn extract_name(url: &Url) -> String {
url.path_segments()
.and_then(|s| s.last())
.map(|s| s.trim_end_matches(".git"))
.filter(|s| !s.is_empty())
.unwrap_or("repo")
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_name_from_git_url() {
let url = Url::parse("https://github.com/user/my-repo.git").unwrap();
assert_eq!(extract_name(&url), "my-repo");
}
#[test]
fn test_extract_name_fallback() {
let url = Url::parse("https://example.com").unwrap();
assert_eq!(extract_name(&url), "repo");
}
}
+145
View File
@@ -0,0 +1,145 @@
use sha2::Digest;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use url::Url;
use crate::{Checksum, Getter, GetterError, Params};
pub struct HttpGetter;
impl Getter for HttpGetter {
fn name(&self) -> &str {
"http"
}
fn fetch<'a>(
&'a self,
url: &'a Url,
params: &'a Params,
dst: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<PathBuf, GetterError>> + Send + 'a>> {
Box::pin(async move {
let filename = extract_filename(url);
let dest = dst.join(&filename);
if let Some(parent) = dest.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let response = reqwest::get(url.as_str()).await.map_err(|e| {
GetterError::HttpError {
url: url.to_string(),
reason: e.to_string(),
}
})?;
let status = response.status();
if !status.is_success() {
return Err(GetterError::HttpStatus {
url: url.to_string(),
status: status.as_u16(),
});
}
let content_digest = response
.headers()
.get("content-digest")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let bytes = response.bytes().await.map_err(|e| GetterError::HttpError {
url: url.to_string(),
reason: format!("Failed to read response body: {}", e),
})?;
match &params.checksum {
Checksum::Verify(spec) => verify_checksum(&bytes, spec)?,
Checksum::Default => {
if let Some(ref digest) = content_digest {
verify_checksum(&bytes, digest)?;
}
}
Checksum::Disabled => {}
}
tokio::fs::write(&dest, &bytes).await?;
Ok(dest)
})
}
}
fn extract_filename(url: &Url) -> String {
url.path_segments()
.and_then(|s| s.last())
.filter(|s| !s.is_empty())
.unwrap_or("download")
.to_string()
}
/// Verify data against spec like "sha256:abc123". Prefix optional (guessed by length).
fn verify_checksum(data: &[u8], spec: &str) -> Result<(), GetterError> {
let (algo, expected) = if let Some((prefix, value)) = spec.split_once(':') {
(prefix, value)
} else {
let trimmed = spec.trim();
match trimmed.len() {
64 => ("sha256", trimmed),
128 => ("sha512", trimmed),
_ => ("sha256", trimmed),
}
};
let actual_hex: String = match algo {
"sha256" => sha2::Sha256::digest(data)
.iter()
.map(|b| format!("{:02x}", b))
.collect(),
"sha512" => sha2::Sha512::digest(data)
.iter()
.map(|b| format!("{:02x}", b))
.collect(),
_ => return Err(GetterError::General(format!("Unknown algorithm: {}", algo))),
};
if actual_hex.eq_ignore_ascii_case(expected) {
Ok(())
} else {
Err(GetterError::ChecksumMismatch {
expected: expected.to_string(),
actual: format!("{}...", &actual_hex[..8.min(actual_hex.len())]),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_filename() {
let url = Url::parse("https://example.com/path/file.tar.gz").unwrap();
assert_eq!(extract_filename(&url), "file.tar.gz");
}
#[test]
fn test_verify_sha256() {
let data = b"hello";
let spec = "sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
verify_checksum(data, spec).unwrap();
}
#[test]
fn test_verify_mismatch() {
let data = b"hello";
let spec = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
assert!(verify_checksum(data, spec).is_err());
}
#[test]
fn test_verify_no_prefix() {
let data = b"hello";
let spec = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
verify_checksum(data, spec).unwrap();
}
}
+7
View File
@@ -0,0 +1,7 @@
mod file;
mod git;
mod http;
pub use file::FileGetter;
pub use git::GitGetter;
pub use http::HttpGetter;
+45
View File
@@ -0,0 +1,45 @@
use thiserror::Error;
/// Error during URL parsing.
#[derive(Error, Debug)]
pub enum ParseError {
#[error("invalid URL: {0}")]
InvalidUrl(#[from] url::ParseError),
#[error("missing '::' separator in forced getter URL")]
MissingSeparator,
#[error("unknown getter type: '{0}'")]
UnknownGetter(String),
}
/// Error during resource fetching.
#[derive(Error, Debug)]
pub enum GetterError {
#[error("getter '{name}' not found in registry")]
UnknownGetter { name: String },
#[error("HTTP request failed for '{url}': {reason}")]
HttpError { url: String, reason: String },
#[error("HTTP status {status} for '{url}'")]
HttpStatus { url: String, status: u16 },
#[error("git clone failed for '{url}': {reason}")]
GitError { url: String, reason: String },
#[error("git checkout failed for ref '{git_ref}': {reason}")]
GitCheckoutError { git_ref: String, reason: String },
#[error("file not found: {0}")]
FileNotFound(String),
#[error("checksum mismatch: expected '{expected}', got '{actual}'")]
ChecksumMismatch { expected: String, actual: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
General(String),
}
+110
View File
@@ -0,0 +1,110 @@
use std::collections::HashMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use url::Url;
use crate::{GetterError, Params};
use crate::builtin;
/// A resource getter. Implement this trait to support custom protocols.
///
/// Each getter is responsible for exactly one protocol (e.g., `git`, `http`, `file`).
/// No inference, no unpacking, no side effects.
///
/// The `fetch` method returns a boxed future to maintain dyn-compatibility.
pub trait Getter: Send + Sync {
/// The protocol name this getter handles (e.g., "git", "http", "file").
fn name(&self) -> &str;
/// Fetch the resource identified by `url` to `dst` directory.
///
/// Returns the local path of the fetched resource.
fn fetch<'a>(
&'a self,
url: &'a Url,
params: &'a Params,
dst: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<PathBuf, GetterError>> + Send + 'a>>;
}
/// Registry of getter implementations, keyed by protocol name.
///
/// Use [`default_registry`] for built-in getters, or build your own
/// with [`register`] for custom protocols.
pub struct GetterRegistry {
getters: HashMap<String, Arc<dyn Getter>>,
}
impl GetterRegistry {
/// Create an empty registry.
pub fn new() -> Self {
GetterRegistry {
getters: HashMap::new(),
}
}
/// Register a getter implementation. Replaces any existing getter
/// with the same name.
pub fn register(&mut self, getter: impl Getter + 'static) {
let name = getter.name().to_string();
self.getters.insert(name, Arc::new(getter));
}
/// Look up a getter by protocol name. Returns `None` if not registered.
pub fn get(&self, name: &str) -> Option<Arc<dyn Getter>> {
self.getters.get(name).cloned()
}
}
/// Create a registry with the built-in getters: `git`, `http`, `https`, `file`.
///
/// `http` and `https` share the same getter instance.
pub fn default_registry() -> GetterRegistry {
let mut reg = GetterRegistry::new();
reg.register(builtin::GitGetter);
reg.register(builtin::FileGetter);
// http and https share the same implementation
let http: Arc<dyn Getter> = Arc::new(builtin::HttpGetter);
reg.getters.insert("http".to_string(), Arc::clone(&http));
reg.getters.insert("https".to_string(), http);
reg
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_registry_has_builtins() {
let reg = default_registry();
assert!(reg.get("git").is_some());
assert!(reg.get("http").is_some());
assert!(reg.get("https").is_some());
assert!(reg.get("file").is_some());
}
#[test]
fn test_custom_getter_registration() {
struct Dummy;
impl Getter for Dummy {
fn name(&self) -> &str { "dummy" }
fn fetch<'a>(
&'a self,
_url: &'a Url,
_params: &'a Params,
_dst: &'a Path,
) -> Pin<Box<dyn Future<Output = Result<PathBuf, GetterError>> + Send + 'a>> {
Box::pin(async { Ok(PathBuf::from("/tmp")) })
}
}
let mut reg = GetterRegistry::new();
reg.register(Dummy);
assert!(reg.get("dummy").is_some());
}
}
+160
View File
@@ -0,0 +1,160 @@
pub mod builtin;
pub mod error;
pub mod getter;
pub mod parser;
pub use error::{GetterError, ParseError};
pub use getter::{Getter, GetterRegistry};
use url::Url;
/// Checksum specification extracted from `?checksum=` query parameter.
///
/// Three states:
/// - `Default` — parameter absent, trust Content-Digest header
/// - `Disabled` — `?checksum=` (empty value), explicitly skip all verification
/// - `Verify(_)` — `?checksum=sha256:abc123...`, must match
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Checksum {
Default,
Disabled,
Verify(String),
}
impl Default for Checksum {
fn default() -> Self {
Checksum::Default
}
}
impl Checksum {
/// Parse from a raw query value. Empty string → Disabled.
pub fn from_query_value(v: Option<&str>) -> Self {
match v {
None => Checksum::Default,
Some(s) if s.is_empty() => Checksum::Disabled,
Some(s) => Checksum::Verify(s.to_string()),
}
}
}
/// Query parameters extracted from the URL.
#[derive(Debug, Clone, Default)]
pub struct Params {
/// Version reference (for git: branch, tag, commit)
pub git_ref: Option<String>,
/// Shallow clone depth (None = full clone)
pub depth: Option<u32>,
/// Checksum specification
pub checksum: Checksum,
}
/// The getter type extracted from `scheme::` prefix.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GetterType {
Git,
Http,
Https,
File,
}
impl GetterType {
pub fn as_str(&self) -> &str {
match self {
GetterType::Git => "git",
GetterType::Http => "http",
GetterType::Https => "https",
GetterType::File => "file",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"git" => Some(GetterType::Git),
"http" => Some(GetterType::Http),
"https" => Some(GetterType::Https),
"file" => Some(GetterType::File),
_ => None,
}
}
}
/// A parsed go-getter-style resource URL: `{getter}::{url}?params`
#[derive(Debug, Clone)]
pub struct GetterUrl {
/// The getter type (git, http, file, ...)
pub getter: GetterType,
/// The standard URL portion (after `::`)
pub url: Url,
/// Extracted query parameters
pub params: Params,
}
impl GetterUrl {
/// Parse a go-getter-style URL string.
///
/// Supported formats:
/// - `git::https://host/repo.git?ref=v1.0&depth=1`
/// - `https://host/file.tar.gz?checksum=sha256:abc` (no prefix → Http)
/// - `file:///path/to/resource`
pub fn parse(s: &str) -> Result<Self, ParseError> {
parser::parse(s)
}
}
impl Default for GetterUrl {
fn default() -> Self {
GetterUrl {
getter: GetterType::Http,
url: Url::parse("https://localhost").unwrap(),
params: Params::default(),
}
}
}
/// Convenience: fetch a resource using the default registry.
pub async fn fetch(
url_str: &str,
dst: &std::path::Path,
) -> Result<std::path::PathBuf, GetterError> {
let parsed = GetterUrl::parse(url_str).map_err(|e| GetterError::General(e.to_string()))?;
let reg = getter::default_registry();
let g = reg
.get(parsed.getter.as_str())
.ok_or_else(|| GetterError::UnknownGetter {
name: parsed.getter.as_str().to_string(),
})?;
g.fetch(&parsed.url, &parsed.params, dst).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checksum_default() {
let c = Checksum::from_query_value(None);
assert_eq!(c, Checksum::Default);
}
#[test]
fn test_checksum_disabled() {
let c = Checksum::from_query_value(Some(""));
assert_eq!(c, Checksum::Disabled);
}
#[test]
fn test_checksum_verify() {
let c = Checksum::from_query_value(Some("sha256:abc123"));
assert_eq!(c, Checksum::Verify("sha256:abc123".into()));
}
#[test]
fn test_getter_type_from_str() {
assert_eq!(GetterType::from_str("git"), Some(GetterType::Git));
assert_eq!(GetterType::from_str("http"), Some(GetterType::Http));
assert_eq!(GetterType::from_str("https"), Some(GetterType::Https));
assert_eq!(GetterType::from_str("file"), Some(GetterType::File));
assert_eq!(GetterType::from_str("unknown"), None);
}
}
+117
View File
@@ -0,0 +1,117 @@
use url::Url;
use crate::{Checksum, GetterType, GetterUrl, Params, ParseError};
/// Parse a go-getter-style URL string into a structured `GetterUrl`.
///
/// Formats:
/// - `git::https://host/repo.git?ref=v1.0&depth=1` → forced getter
/// - `https://host/file.tar.gz?checksum=sha256:abc` → default Http getter
/// - `file:///path/to/resource` → default File getter (detected by scheme)
pub fn parse(s: &str) -> Result<GetterUrl, ParseError> {
let (getter, url_str) = if let Some((prefix, rest)) = s.split_once("::") {
// Forced getter: "git::https://..."
let g = GetterType::from_str(prefix)
.ok_or_else(|| ParseError::UnknownGetter(prefix.to_string()))?;
(g, rest)
} else {
// No prefix → detect from URL scheme or default to Http
let parsed = Url::parse(s)?;
let g = detect_getter(&parsed);
(g, s)
};
let url = Url::parse(url_str)?;
let params = extract_params(&url);
Ok(GetterUrl {
getter,
url,
params,
})
}
/// Detect getter type from a URL scheme (used when no explicit `::` prefix).
fn detect_getter(url: &Url) -> GetterType {
match url.scheme() {
"git" => GetterType::Git,
"http" => GetterType::Http,
"https" => GetterType::Https,
"file" => GetterType::File,
_ => GetterType::Http, // fallback
}
}
/// Extract known query parameters from a URL.
fn extract_params(url: &Url) -> Params {
let mut params = Params::default();
for (key, value) in url.query_pairs() {
match key.as_ref() {
"ref" => params.git_ref = Some(value.into_owned()),
"depth" => {
if let Ok(d) = value.parse::<u32>() {
params.depth = Some(d);
}
}
"checksum" => {
params.checksum = Checksum::from_query_value(Some(&value));
}
_ => {} // unknown params ignored
}
}
params
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_git_https() {
let g = parse("git::https://github.com/user/repo.git?ref=v1.0&depth=1").unwrap();
assert_eq!(g.getter, GetterType::Git);
assert_eq!(g.url.scheme(), "https");
assert_eq!(g.url.host_str().unwrap(), "github.com");
assert_eq!(g.params.git_ref.as_deref(), Some("v1.0"));
assert_eq!(g.params.depth, Some(1));
}
#[test]
fn test_parse_http_default() {
let g = parse("https://example.com/plugin.tar.gz?checksum=sha256:abc123").unwrap();
assert_eq!(g.getter, GetterType::Https);
assert_eq!(g.url.scheme(), "https");
assert_eq!(
g.params.checksum,
Checksum::Verify("sha256:abc123".into())
);
}
#[test]
fn test_parse_file_scheme() {
let g = parse("file:///home/user/plugin").unwrap();
assert_eq!(g.getter, GetterType::File);
assert_eq!(g.url.scheme(), "file");
assert_eq!(g.url.path(), "/home/user/plugin");
}
#[test]
fn test_parse_checksum_disabled() {
let g = parse("https://example.com/file?checksum=").unwrap();
assert_eq!(g.params.checksum, Checksum::Disabled);
}
#[test]
fn test_parse_no_checksum() {
let g = parse("https://example.com/file").unwrap();
assert_eq!(g.params.checksum, Checksum::Default);
}
#[test]
fn test_parse_unknown_getter() {
let result = parse("unknown::https://example.com");
assert!(result.is_err());
}
}
-4003
View File
File diff suppressed because it is too large Load Diff
-17
View File
@@ -1,17 +0,0 @@
[package]
name = "workshop-vault"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
async-trait = "0.1.89"
log = "0.4.28"
rusty_vault = "0.2.1"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
[dev-dependencies]
env_logger = "0.11.8"
-37
View File
@@ -1,37 +0,0 @@
use workshop_vault::*;
use env_logger;
use log::info;
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
// 从环境变量创建客户端
let client = create_vault_client_from_env()?;
// 检查是否初始化 Vault
let initialized = client.is_initialized().await?;
info!("Vault initialized: {}", initialized);
// 创建 CI/CD 密钥管理器
let client = create_vault_client_from_env()?;
let secret_manager = WorkshopSecretManager::new(client, "my-ci-pipeline".to_string());
info!("Vault initialized: {}", initialized);
// 存储 CI/CD 变量
secret_manager.store_cicd_variable("DATABASE_URL", "postgres://user:pass@localhost/db").await?;
info!("Vault initialized: {}", initialized);
secret_manager.store_cicd_variable("API_SECRET", "super-secret-key").await?;
// 读取变量
let db_url = secret_manager.get_cicd_variable("DATABASE_URL").await?;
println!("Database URL: {}", db_url);
// 存储密钥文件
secret_manager.store_secret_file("id_rsa", "-----BEGIN PRIVATE KEY-----\n...").await?;
// 列出所有变量
let variables = secret_manager.list_cicd_variables().await?;
println!("Available variables: {:?}", variables);
Ok(())
}
-42
View File
@@ -1,42 +0,0 @@
# workshop-vault/src
**Generated:** 2026-04-22
**Commit:** 7b3b71a
Secret management client for HashiCorp Vault.
## STRUCTURE
```
src/
├── lib.rs # Library entry, re-exports
├── client.rs # WorkshopSecretManager client
├── rusty_vault_client.rs # RustyVault adapter
├── models.rs # Secret, SecretEngine structs
├── error.rs # VaultError enum
└── examples/usage.rs # Usage examples
```
## WHERE TO LOOK
| Symbol | Type | Location | Purpose |
|--------|------|----------|---------|
| create_vault_client | fn | lib.rs:45 | Create client from config |
| create_vault_client_from_env | fn | lib.rs:60 | Create from env vars |
| WorkshopSecretManager | struct | client.rs:16 | Main client |
| Secret | struct | models.rs:5 | Secret data |
| VaultError | enum | error.rs:8 | Error types |
## CONVENTIONS
- **Library only**: No main.rs, only lib.rs
- **Adapter pattern**: rusty_vault_client.rs wraps rusty_vault crate
- **Error handling**: thiserror for VaultError
- **Async**: All operations async via rusty_vault
## DEPENDENCIES
- `rusty_vault` - Vault client library
- `async-trait` - Async trait support
- `tokio` - Async runtime
- `serde` - Serialization
-28
View File
@@ -1,28 +0,0 @@
use std::collections::HashMap;
use async_trait::async_trait;
use crate::models::*;
use crate::error::Result;
#[async_trait]
pub trait VaultClient: Send + Sync {
/// 检查 Vault 是否已初始化
async fn is_initialized(&self) -> Result<bool>;
/// 获取健康状态
async fn health(&self) -> Result<HealthStatus>;
/// 写入密钥
async fn write_secret(&self, path: &str, data: HashMap<String, String>) -> Result<()>;
/// 读取密钥
async fn read_secret(&self, path: &str) -> Result<SecretData>;
/// 列出路径下的所有密钥
async fn list_secrets(&self, path: &str) -> Result<Vec<String>>;
/// 删除密钥
async fn delete_secret(&self, path: &str) -> Result<()>;
/// 检查密钥是否存在
async fn secret_exists(&self, path: &str) -> Result<bool>;
}
-24
View File
@@ -1,24 +0,0 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum VaultError {
#[error("Vault not initialized")]
NotInitialized,
#[error("Vault operation failed: {0}")]
OperationFailed(String),
#[error("Authentication failed: {0}")]
AuthFailed(String),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Vault error: {0}")]
RvError(#[from] rusty_vault::errors::RvError),
}
pub type Result<T> = std::result::Result<T, VaultError>;
-123
View File
@@ -1,123 +0,0 @@
pub mod error;
pub mod models;
pub mod client;
pub mod rusty_vault_client;
pub use error::{VaultError, Result};
pub use models::*;
pub use client::VaultClient;
pub use rusty_vault_client::RustyVaultClient;
use rusty_vault::api::client::TLSConfig;
/// 创建默认的 Vault 客户端
pub fn create_vault_client(
address: Option<String>,
token: Option<String>,
secret_mount: Option<String>,
) -> Result<RustyVaultClient> {
let address = address.unwrap_or_else(|| "https://127.0.0.1:8200".to_string());
let token = token.ok_or_else(|| VaultError::AuthFailed("Vault token is required".to_string()))?;
Ok(RustyVaultClient::new(address, token, secret_mount, None))
}
/// 从环境变量创建 Vault 客户端
pub fn create_vault_client_from_env() -> Result<RustyVaultClient> {
RustyVaultClient::from_env()
}
/// 创建带 TLS 的 Vault 客户端
pub fn create_vault_client_with_tls(
address: String,
token: String,
secret_mount: Option<String>,
tls_config: TLSConfig,
) -> RustyVaultClient {
RustyVaultClient::new(address, token, secret_mount, Some(tls_config))
}
/// CI/CD 密钥管理器的便捷接口
pub struct WorkshopSecretManager<T: VaultClient> {
client: T,
namespace: String,
}
impl<T: VaultClient> WorkshopSecretManager<T> {
pub fn new(client: T, namespace: String) -> Self {
Self { client, namespace }
}
/// 存储 CI/CD 变量
pub async fn store_cicd_variable(&self, key: &str, value: &str) -> Result<()> {
let path = format!("{}/variables/{}", self.namespace, key);
let mut data = std::collections::HashMap::new();
data.insert("value".to_string(), value.to_string());
self.client.write_secret(&path, data).await
}
/// 读取 CI/CD 变量
pub async fn get_cicd_variable(&self, key: &str) -> Result<String> {
let path = format!("{}/variables/{}", self.namespace, key);
let secret = self.client.read_secret(&path).await?;
secret.data.get("value")
.cloned()
.ok_or_else(|| VaultError::OperationFailed("Variable not found".to_string()))
}
/// 存储密钥文件内容
pub async fn store_secret_file(&self, filename: &str, content: &str) -> Result<()> {
let path = format!("{}/files/{}", self.namespace, filename);
let mut data = std::collections::HashMap::new();
data.insert("content".to_string(), content.to_string());
self.client.write_secret(&path, data).await
}
/// 读取密钥文件内容
pub async fn get_secret_file(&self, filename: &str) -> Result<String> {
let path = format!("{}/files/{}", self.namespace, filename);
let secret = self.client.read_secret(&path).await?;
secret.data.get("content")
.cloned()
.ok_or_else(|| VaultError::OperationFailed("File not found".to_string()))
}
/// 列出所有 CI/CD 变量
pub async fn list_cicd_variables(&self) -> Result<Vec<String>> {
let path = format!("{}/variables", self.namespace);
self.client.list_secrets(&path).await
}
/// 删除 CI/CD 变量
pub async fn delete_cicd_variable(&self, key: &str) -> Result<()> {
let path = format!("{}/variables/{}", self.namespace, key);
self.client.delete_secret(&path).await
}
}
#[cfg(test)]
mod tests {
use super::*;
// 注意:这些测试需要实际的 Vault 实例,因此标记为忽略
// 在实际使用中应该使用模拟的测试
#[tokio::test]
#[ignore]
async fn test_vault_operations() {
let client = create_vault_client_from_env().unwrap();
let manager = WorkshopSecretManager::new(client, "test-ci".to_string());
// 测试存储和读取变量
manager.store_cicd_variable("API_KEY", "test-value").await.unwrap();
let value = manager.get_cicd_variable("API_KEY").await.unwrap();
assert_eq!(value, "test-value");
// 清理
manager.delete_cicd_variable("API_KEY").await.unwrap();
}
}
-61
View File
@@ -1,61 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Vault 初始化配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultInitConfig {
pub secret_shares: usize,
pub secret_threshold: usize,
pub pgp_keys: Option<Vec<String>>,
}
/// Vault 初始化响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultInitResponse {
pub keys: Vec<String>,
pub keys_base64: Vec<String>,
pub root_token: String,
}
/// 密钥数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretData {
pub data: HashMap<String, String>,
pub metadata: Option<SecretMetadata>,
}
/// 密钥元数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretMetadata {
pub created_time: Option<String>,
pub deletion_time: Option<String>,
pub destroyed: Option<bool>,
pub version: Option<usize>,
}
/// 列出密钥的响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListSecretsResponse {
pub keys: Vec<String>,
}
/// 写入密钥的请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WriteSecretRequest {
pub data: HashMap<String, String>,
}
/// 健康状态
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
pub initialized: bool,
pub sealed: bool,
pub standby: bool,
pub performance_standby: bool,
pub replication_performance_mode: String,
pub replication_dr_mode: String,
pub server_time_utc: u64,
pub version: String,
pub cluster_name: String,
pub cluster_id: String,
}
-211
View File
@@ -1,211 +0,0 @@
use async_trait::async_trait;
use rusty_vault::api::client::{Client, TLSConfig, TLSConfigBuilder};
use rusty_vault::api::HttpResponse;
use serde_json::{Map, Value};
use std::collections::HashMap;
use crate::client::VaultClient;
use crate::error::{Result, VaultError};
use crate::models::*;
/// RustyVault 客户端实现
pub struct RustyVaultClient {
client: Client,
secret_mount: String,
}
impl RustyVaultClient {
pub fn new(
address: String,
token: String,
secret_mount: Option<String>,
tls_config: Option<TLSConfig>,
) -> Self {
let mut client = Client::new()
.with_addr(&address)
.with_token(&token);
if let Some(tls_config) = tls_config {
client = client.with_tls_config(tls_config);
}
let client = client.build();
Self {
client,
secret_mount: secret_mount.unwrap_or_else(|| "secret".to_string()),
}
}
/// 从环境变量创建客户端
pub fn from_env() -> Result<Self> {
let address = std::env::var("VAULT_ADDR")
.unwrap_or_else(|_| "https://127.0.0.1:8200".to_string());
let token = std::env::var("VAULT_TOKEN")
.map_err(|_| VaultError::AuthFailed("VAULT_TOKEN environment variable not set".to_string()))?;
let secret_mount = std::env::var("VAULT_SECRET_MOUNT").ok();
Ok(Self::new(address, token, secret_mount, None))
}
/// 构建 TLS 配置
pub fn build_tls_config(
ca_cert_path: Option<String>,
client_cert_path: Option<String>,
client_key_path: Option<String>,
insecure: bool,
) -> Result<TLSConfig> {
let mut builder = TLSConfigBuilder::new().with_insecure(insecure);
if let Some(ca_path) = ca_cert_path {
builder = builder.with_server_ca_path(&std::path::PathBuf::from(ca_path))?;
}
if let (Some(cert_path), Some(key_path)) = (client_cert_path, client_key_path) {
builder = builder.with_client_cert_path(
&std::path::PathBuf::from(cert_path),
&std::path::PathBuf::from(key_path),
)?;
}
builder.build().map_err(|e| VaultError::OperationFailed(e.to_string()))
}
fn build_secret_path(&self, path: &str) -> String {
format!("/v1/{}/data/{}", self.secret_mount, path)
}
fn build_secret_metadata_path(&self, path: &str) -> String {
format!("/v1/{}/metadata/{}", self.secret_mount, path)
}
fn handle_response<T: serde::de::DeserializeOwned>(
&self,
response: HttpResponse,
) -> Result<T> {
if response.response_status >= 400 {
let error_msg = if let Some(data) = response.response_data {
serde_json::to_string(&data).unwrap_or_else(|_| "Unknown error".to_string())
} else {
format!("HTTP {}: {}", response.response_status, response.url)
};
return Err(VaultError::OperationFailed(error_msg));
}
match response.response_data {
Some(data) => {
serde_json::from_value(data).map_err(VaultError::SerializationError)
}
None => Err(VaultError::OperationFailed("No response data".to_string())),
}
}
}
#[async_trait]
impl VaultClient for RustyVaultClient {
async fn is_initialized(&self) -> Result<bool> {
let response = self.client.request_get("/v1/sys/init")?;
let data: Map<String, Value> = self.handle_response(response)?;
data.get("initialized")
.and_then(|v| v.as_bool())
.ok_or_else(|| VaultError::OperationFailed("Invalid response format".to_string()))
}
async fn health(&self) -> Result<HealthStatus> {
let response = self.client.request_get("/v1/sys/health")?;
self.handle_response(response)
}
async fn write_secret(&self, path: &str, data: HashMap<String, String>) -> Result<()> {
let secret_path = self.build_secret_path(path);
let mut request_data = Map::new();
let mut data_map = Map::new();
for (key, value) in data {
data_map.insert(key, Value::from(value));
}
request_data.insert("data".to_string(), Value::from(data_map));
let response = self.client.request_put(&secret_path, Some(request_data))?;
if response.response_status >= 400 {
let error_msg = if let Some(data) = response.response_data {
serde_json::to_string(&data).unwrap_or_else(|_| "Unknown error".to_string())
} else {
format!("HTTP {}: {}", response.response_status, response.url)
};
return Err(VaultError::OperationFailed(error_msg));
}
Ok(())
}
async fn read_secret(&self, path: &str) -> Result<SecretData> {
let secret_path = self.build_secret_path(path);
let response = self.client.request_get(&secret_path)?;
let data: Map<String, Value> = self.handle_response(response)?;
// 提取 data 字段
let secret_data = data.get("data")
.and_then(|v| v.get("data"))
.ok_or_else(|| VaultError::OperationFailed("Invalid secret data format".to_string()))?;
let metadata = data.get("data")
.and_then(|v| v.get("metadata"))
.and_then(|v| serde_json::from_value(v.clone()).ok());
let data_map: HashMap<String, String> = serde_json::from_value(secret_data.clone())?;
Ok(SecretData {
data: data_map,
metadata,
})
}
async fn list_secrets(&self, path: &str) -> Result<Vec<String>> {
let list_path = self.build_secret_metadata_path(path);
let response = self.client.request_list(&list_path)?;
let data: Map<String, Value> = self.handle_response(response)?;
data.get("data")
.and_then(|v| v.get("keys"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.ok_or_else(|| VaultError::OperationFailed("Invalid list response format".to_string()))
}
async fn delete_secret(&self, path: &str) -> Result<()> {
let secret_path = self.build_secret_metadata_path(path);
let response = self.client.request_delete(&secret_path, None)?;
if response.response_status >= 400 {
let error_msg = if let Some(data) = response.response_data {
serde_json::to_string(&data).unwrap_or_else(|_| "Unknown error".to_string())
} else {
format!("HTTP {}: {}", response.response_status, response.url)
};
return Err(VaultError::OperationFailed(error_msg));
}
Ok(())
}
async fn secret_exists(&self, path: &str) -> Result<bool> {
match self.read_secret(path).await {
Ok(_) => Ok(true),
Err(VaultError::OperationFailed(msg)) if msg.contains("404") => Ok(false),
Err(e) => Err(e),
}
}
}