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:
@@ -67,6 +67,3 @@ unreachable_code = "allow"
|
||||
inherent_to_string = "allow"
|
||||
non_canonical_partial_ord_impl = "allow"
|
||||
|
||||
[[bench]]
|
||||
name = "parser_bench"
|
||||
harness = false
|
||||
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}")]
|
||||
|
||||
@@ -9,7 +9,6 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod dylib;
|
||||
pub mod fetch;
|
||||
mod internal;
|
||||
mod metadata;
|
||||
mod rhai;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod fetch;
|
||||
pub mod fetch_plugin;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
cert.toml
|
||||
target
|
||||
certs
|
||||
Generated
-1250
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mod config;
|
||||
mod generator;
|
||||
|
||||
pub use config::*;
|
||||
pub use generator::*;
|
||||
|
||||
pub type Result<T> = anyhow::Result<T>;
|
||||
@@ -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 +0,0 @@
|
||||
/target
|
||||
Generated
-587
@@ -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",
|
||||
]
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Generated
+1683
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 ¶ms.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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod file;
|
||||
mod git;
|
||||
mod http;
|
||||
|
||||
pub use file::FileGetter;
|
||||
pub use git::GitGetter;
|
||||
pub use http::HttpGetter;
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Generated
-4003
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user