mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 代理节点架构重构与功能增强
aether-proxy: - 重构 main.rs,拆分为 app/state/hardware/net 模块 - setup.rs 拆分为 setup/tui.rs + setup/service.rs,支持 systemd 服务管理子命令 - 新增 delegate 端点,支持后端通过代理节点转发请求而非传统 CONNECT 代理 - 注册时上报硬件信息(CPU/内存/fd_limit)和估算最大并发数 - 心跳上报活跃连接数,支持远程下发 node_name 配置 - HTTP 转发时剥离 X-Forwarded-* 等敏感头部 - 切换到 rustls-tls,降低日志级别减少噪音 后端: - 从 http_client.py 提取代理相关逻辑至 proxy_node/resolver.py - 从 routes.py 提取业务逻辑至 proxy_node/service.py - handler 支持 delegate 模式(通过代理节点 HTTP 端点转发而非 CONNECT 隧道) - ProxyNode 模型新增 hardware_info 和 estimated_max_concurrency 字段 前端: - 新增 HardwareTooltip 组件展示节点硬件信息 - 远程配置支持下发 node_name
This commit is contained in:
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
use hyper::body::Incoming;
|
||||
use hyper::{Request, Response};
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::auth;
|
||||
use crate::config::Config;
|
||||
@@ -53,7 +53,7 @@ pub async fn handle_connect(
|
||||
}
|
||||
};
|
||||
|
||||
info!(target = %target_addr, "CONNECT tunnel establishing");
|
||||
debug!(target = %target_addr, "CONNECT tunnel establishing");
|
||||
|
||||
// Connect to target
|
||||
let target_stream = match TcpStream::connect(target_addr).await {
|
||||
@@ -65,28 +65,29 @@ pub async fn handle_connect(
|
||||
};
|
||||
|
||||
// Respond 200 and upgrade connection to raw TCP tunnel
|
||||
let target_display = target_addr.to_string();
|
||||
tokio::task::spawn(async move {
|
||||
match hyper::upgrade::on(req).await {
|
||||
Ok(upgraded) => {
|
||||
let mut upgraded =
|
||||
hyper_util::rt::TokioIo::new(upgraded);
|
||||
let mut upgraded = hyper_util::rt::TokioIo::new(upgraded);
|
||||
let mut target = target_stream;
|
||||
|
||||
match tokio::io::copy_bidirectional(&mut upgraded, &mut target).await {
|
||||
Ok((from_client, from_target)) => {
|
||||
info!(
|
||||
debug!(
|
||||
target = %target_display,
|
||||
from_client,
|
||||
from_target,
|
||||
"CONNECT tunnel closed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(error = %e, "CONNECT tunnel error");
|
||||
debug!(target = %target_display, error = %e, "CONNECT tunnel error");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = %e, "CONNECT upgrade failed");
|
||||
warn!(target = %target_display, error = %e, "CONNECT upgrade failed");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
221
aether-proxy/src/proxy/delegate.rs
Normal file
221
aether-proxy/src/proxy/delegate.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::TryStreamExt;
|
||||
use http_body_util::{BodyExt, Full, Limited, StreamBody};
|
||||
use hyper::body::{Frame, Incoming};
|
||||
use hyper::{Request, Response};
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, warn};
|
||||
use url::Url;
|
||||
|
||||
use crate::auth;
|
||||
use crate::config::Config;
|
||||
use crate::proxy::plain::BoxBody;
|
||||
use crate::proxy::target_filter;
|
||||
|
||||
/// Delegation request payload sent by Aether.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DelegateRequest {
|
||||
method: String,
|
||||
url: String,
|
||||
headers: HashMap<String, String>,
|
||||
body: Option<String>,
|
||||
/// Accepted but not used on the proxy side — Aether controls timeouts.
|
||||
#[allow(dead_code)]
|
||||
timeout: Option<u64>,
|
||||
}
|
||||
|
||||
/// Handle delegation requests: Aether sends a full request description,
|
||||
/// and the proxy issues the actual upstream HTTP call using its own TLS stack.
|
||||
///
|
||||
/// Endpoint: POST /_aether/delegate
|
||||
pub async fn handle_delegate(
|
||||
req: Request<Incoming>,
|
||||
config: Arc<Config>,
|
||||
node_id: &str,
|
||||
allowed_ports: &HashSet<u16>,
|
||||
timestamp_tolerance: u64,
|
||||
http_client: &reqwest::Client,
|
||||
) -> Response<BoxBody> {
|
||||
// Authenticate via Authorization header (same HMAC scheme as Proxy-Authorization)
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
if let Err(e) = auth::validate_proxy_auth(auth_header, &config, node_id, timestamp_tolerance) {
|
||||
warn!(error = %e, "delegate auth failed");
|
||||
return error_response(401, "authentication_failed", &e.to_string());
|
||||
}
|
||||
|
||||
// Read and parse request body (limit to 10 MB to prevent OOM)
|
||||
const MAX_BODY: usize = 10 * 1024 * 1024;
|
||||
let body_bytes = match Limited::new(req.into_body(), MAX_BODY).collect().await {
|
||||
Ok(collected) => collected.to_bytes(),
|
||||
Err(e) => {
|
||||
warn!(error = %e, "failed to read delegate request body");
|
||||
return error_response(413, "payload_too_large", "request body exceeds 10MB limit");
|
||||
}
|
||||
};
|
||||
|
||||
let delegate_req: DelegateRequest = match serde_json::from_slice(&body_bytes) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "invalid delegate request JSON");
|
||||
return error_response(400, "bad_request", &format!("invalid JSON: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
// Target filter: validate the upstream URL against allowed ports and private IP checks
|
||||
let parsed_url = match Url::parse(&delegate_req.url) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
warn!(url = %delegate_req.url, error = %e, "invalid delegate target URL");
|
||||
return error_response(400, "bad_request", &format!("invalid URL: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let host = match parsed_url.host_str() {
|
||||
Some(h) => h.to_string(),
|
||||
None => {
|
||||
warn!(url = %delegate_req.url, "delegate target URL missing host");
|
||||
return error_response(400, "bad_request", "URL missing host");
|
||||
}
|
||||
};
|
||||
|
||||
let port = parsed_url.port_or_known_default().unwrap_or(443);
|
||||
|
||||
if let Err(e) = target_filter::validate_target(&host, port, allowed_ports) {
|
||||
warn!(host = %host, port, error = %e, "delegate target rejected");
|
||||
return error_response(403, "target_not_allowed", &e.to_string());
|
||||
}
|
||||
|
||||
debug!(
|
||||
method = %delegate_req.method,
|
||||
url = %delegate_req.url,
|
||||
"delegate request"
|
||||
);
|
||||
|
||||
// Build upstream request
|
||||
let method = match delegate_req.method.parse::<reqwest::Method>() {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
warn!(error = %e, method = %delegate_req.method, "invalid HTTP method");
|
||||
return error_response(400, "bad_request", &format!("invalid method: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let mut upstream_req = http_client.request(method, &delegate_req.url);
|
||||
|
||||
// NOTE: We intentionally do NOT set a per-request timeout here.
|
||||
// reqwest's `.timeout()` caps the *entire* request including body streaming,
|
||||
// which would truncate long-lived SSE streams. The delegate_client already
|
||||
// has a 30s connect_timeout for connection establishment, and Aether controls
|
||||
// first-byte / idle timeouts on its own side via asyncio.
|
||||
|
||||
// Set headers (skip `host` — reqwest sets it from the URL automatically,
|
||||
// and a duplicate Host header can confuse certain upstreams)
|
||||
for (name, value) in &delegate_req.headers {
|
||||
if name.eq_ignore_ascii_case("host") {
|
||||
continue;
|
||||
}
|
||||
upstream_req = upstream_req.header(name.as_str(), value.as_str());
|
||||
}
|
||||
|
||||
// Set body
|
||||
if let Some(body) = delegate_req.body {
|
||||
upstream_req = upstream_req.body(body);
|
||||
}
|
||||
|
||||
// Send upstream request
|
||||
let upstream_resp = match upstream_req.send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
warn!(url = %delegate_req.url, error = %e, "delegate upstream request failed");
|
||||
// Sanitize: strip URL details from error message to avoid leaking
|
||||
// API keys or paths that may appear in query strings / paths.
|
||||
let safe_detail = sanitize_upstream_error(&e.to_string());
|
||||
if e.is_timeout() {
|
||||
return error_response(504, "upstream_timeout", &safe_detail);
|
||||
}
|
||||
return error_response(502, "upstream_connection_failed", &safe_detail);
|
||||
}
|
||||
};
|
||||
|
||||
// Build response: pass through upstream status + headers, stream body back
|
||||
let status = upstream_resp.status().as_u16();
|
||||
let upstream_headers = upstream_resp.headers().clone();
|
||||
|
||||
debug!(url = %delegate_req.url, status, "delegate upstream response");
|
||||
|
||||
// Stream the response body
|
||||
let body_stream = upstream_resp
|
||||
.bytes_stream()
|
||||
.map_ok(Frame::data)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) });
|
||||
|
||||
let stream_body: BoxBody = StreamBody::new(body_stream).boxed();
|
||||
|
||||
let mut builder = Response::builder().status(status);
|
||||
for (name, value) in upstream_headers.iter() {
|
||||
builder = builder.header(name, value);
|
||||
}
|
||||
|
||||
builder
|
||||
.body(stream_body)
|
||||
.unwrap_or_else(|_| Response::builder().status(500).body(empty_box()).unwrap())
|
||||
}
|
||||
|
||||
// ── Sanitisation ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Strip full URLs from error messages to prevent leaking upstream API keys,
|
||||
/// paths, or query parameters in the delegate error response.
|
||||
///
|
||||
/// Replaces `https://api.example.com/v1/chat?key=xxx` with `api.example.com`.
|
||||
fn sanitize_upstream_error(msg: &str) -> String {
|
||||
// Simple regex-free approach: find "https://..." or "http://..." spans and
|
||||
// replace them with just the host portion.
|
||||
let mut result = msg.to_string();
|
||||
for scheme in &["https://", "http://"] {
|
||||
while let Some(start) = result.find(scheme) {
|
||||
let after_scheme = start + scheme.len();
|
||||
// Host ends at '/', '?', '#', ' ', or end of string
|
||||
let host_end = result[after_scheme..]
|
||||
.find(['/', '?', '#', ' '])
|
||||
.map(|i| after_scheme + i)
|
||||
.unwrap_or(result.len());
|
||||
let host = &result[after_scheme..host_end];
|
||||
result = format!("{}{}{}", &result[..start], host, &result[host_end..]);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// ── Error response helpers ───────────────────────────────────────────────────
|
||||
|
||||
fn empty_box() -> BoxBody {
|
||||
Full::new(bytes::Bytes::new())
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { match e {} })
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn error_response(status: u16, error: &str, detail: &str) -> Response<BoxBody> {
|
||||
let body = serde_json::json!({
|
||||
"error": error,
|
||||
"detail": detail,
|
||||
});
|
||||
let body_bytes = bytes::Bytes::from(body.to_string());
|
||||
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Delegate-Error", "true")
|
||||
.body(
|
||||
Full::new(body_bytes)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { match e {} })
|
||||
.boxed(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod connect;
|
||||
pub mod delegate;
|
||||
pub mod plain;
|
||||
pub mod server;
|
||||
pub mod target_filter;
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::Incoming;
|
||||
use hyper::{Request, Response};
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::auth;
|
||||
use crate::config::Config;
|
||||
@@ -56,22 +56,28 @@ pub async fn handle_plain(
|
||||
}
|
||||
};
|
||||
|
||||
info!(target = %target_addr, method = %req.method(), "HTTP proxy forwarding");
|
||||
let method = req.method().clone();
|
||||
debug!(target = %target_addr, method = %method, "HTTP proxy forwarding");
|
||||
|
||||
// Build outgoing request (strip proxy headers, use relative URI)
|
||||
let path_and_query = uri
|
||||
.path_and_query()
|
||||
.map(|pq| pq.as_str())
|
||||
.unwrap_or("/");
|
||||
let path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/");
|
||||
|
||||
let mut builder = Request::builder()
|
||||
.method(req.method())
|
||||
.uri(path_and_query)
|
||||
.version(req.version());
|
||||
|
||||
// Copy headers, skipping proxy-specific ones
|
||||
// Copy headers, skipping proxy-specific and forwarding-related ones
|
||||
for (name, value) in req.headers() {
|
||||
if name == "proxy-authorization" || name == "proxy-connection" {
|
||||
if name == "proxy-authorization"
|
||||
|| name == "proxy-connection"
|
||||
|| name == "x-forwarded-for"
|
||||
|| name == "x-forwarded-host"
|
||||
|| name == "x-forwarded-proto"
|
||||
|| name == "x-real-ip"
|
||||
|| name == "forwarded"
|
||||
|| name == "via"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
builder = builder.header(name, value);
|
||||
@@ -116,7 +122,7 @@ pub async fn handle_plain(
|
||||
|
||||
match sender.send_request(outgoing).await {
|
||||
Ok(resp) => {
|
||||
info!(target = %target_addr, status = resp.status().as_u16(), "HTTP proxy response");
|
||||
debug!(target = %target_addr, method = %method, status = resp.status().as_u16(), "HTTP proxy response");
|
||||
// Stream the response body directly — no buffering
|
||||
let (parts, body) = resp.into_parts();
|
||||
let body: BoxBody = body
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
use http_body_util::BodyExt;
|
||||
use hyper::body::Incoming;
|
||||
use hyper::rt::{Read, Write};
|
||||
use hyper::server::conn::http1;
|
||||
use hyper::service::service_fn;
|
||||
use hyper::{Method, Request};
|
||||
use hyper::rt::{Read, Write};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::watch;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::proxy::{connect, plain, tls};
|
||||
use crate::runtime::SharedDynamicConfig;
|
||||
use crate::proxy::{connect, delegate, plain, tls};
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Start the proxy server.
|
||||
///
|
||||
@@ -23,20 +22,17 @@ use crate::runtime::SharedDynamicConfig;
|
||||
/// - CONNECT requests -> tunnel handler
|
||||
/// - Other HTTP requests -> plain forward proxy handler
|
||||
///
|
||||
/// When `tls_acceptor` is provided, the server operates in dual-stack mode:
|
||||
/// When TLS is configured, the server operates in dual-stack mode:
|
||||
/// it peeks at the first byte of each connection to distinguish TLS ClientHello
|
||||
/// (0x16) from plain HTTP, and handles both on the same port.
|
||||
pub async fn run(
|
||||
config: Arc<Config>,
|
||||
node_id: Arc<RwLock<String>>,
|
||||
dynamic: SharedDynamicConfig,
|
||||
tls_acceptor: Option<TlsAcceptor>,
|
||||
state: &Arc<AppState>,
|
||||
mut shutdown_rx: watch::Receiver<bool>,
|
||||
) -> anyhow::Result<()> {
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], config.listen_port));
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], state.config.listen_port));
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
|
||||
if tls_acceptor.is_some() {
|
||||
if state.tls_acceptor.is_some() {
|
||||
info!(addr = %addr, "proxy server listening (HTTP+TLS dual-stack)");
|
||||
} else {
|
||||
info!(addr = %addr, "proxy server listening (HTTP only)");
|
||||
@@ -53,26 +49,22 @@ pub async fn run(
|
||||
}
|
||||
};
|
||||
|
||||
info!(peer = %peer_addr, "new connection");
|
||||
debug!(peer = %peer_addr, "new connection");
|
||||
|
||||
let config = Arc::clone(&config);
|
||||
let node_id = Arc::clone(&node_id);
|
||||
let dynamic = Arc::clone(&dynamic);
|
||||
let tls_acceptor = tls_acceptor.clone();
|
||||
let state = Arc::clone(state);
|
||||
state.active_connections.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
// Dual-stack: peek first byte to decide TLS vs plain HTTP
|
||||
if let Some(acceptor) = &tls_acceptor {
|
||||
if let Some(ref acceptor) = state.tls_acceptor {
|
||||
if tls::is_tls_client_hello(&stream).await {
|
||||
match acceptor.accept(stream).await {
|
||||
match acceptor.clone().accept(stream).await {
|
||||
Ok(tls_stream) => {
|
||||
debug!(peer = %peer_addr, "TLS handshake ok");
|
||||
serve_connection(
|
||||
TokioIo::new(tls_stream),
|
||||
peer_addr,
|
||||
config,
|
||||
node_id,
|
||||
dynamic,
|
||||
&state,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -80,6 +72,7 @@ pub async fn run(
|
||||
debug!(peer = %peer_addr, error = %e, "TLS handshake failed");
|
||||
}
|
||||
}
|
||||
state.active_connections.fetch_sub(1, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -88,11 +81,11 @@ pub async fn run(
|
||||
serve_connection(
|
||||
TokioIo::new(stream),
|
||||
peer_addr,
|
||||
config,
|
||||
node_id,
|
||||
dynamic,
|
||||
&state,
|
||||
)
|
||||
.await;
|
||||
|
||||
state.active_connections.fetch_sub(1, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
_ = shutdown_rx.changed() => {
|
||||
@@ -106,22 +99,26 @@ pub async fn run(
|
||||
}
|
||||
|
||||
/// Serve a single HTTP/1.1 connection (works over both plain TCP and TLS).
|
||||
async fn serve_connection<I>(
|
||||
io: I,
|
||||
peer_addr: SocketAddr,
|
||||
config: Arc<Config>,
|
||||
node_id: Arc<RwLock<String>>,
|
||||
dynamic: SharedDynamicConfig,
|
||||
) where
|
||||
async fn serve_connection<I>(io: I, peer_addr: SocketAddr, state: &Arc<AppState>)
|
||||
where
|
||||
I: Read + Write + Unpin + Send + 'static,
|
||||
{
|
||||
let config = Arc::clone(&state.config);
|
||||
let node_id = Arc::clone(&state.node_id);
|
||||
let dynamic = Arc::clone(&state.dynamic);
|
||||
let delegate_client = state.delegate_client.clone();
|
||||
|
||||
let service = service_fn(move |req: Request<Incoming>| {
|
||||
let config = Arc::clone(&config);
|
||||
let node_id = Arc::clone(&node_id);
|
||||
let dynamic = Arc::clone(&dynamic);
|
||||
let delegate_client = delegate_client.clone();
|
||||
|
||||
async move {
|
||||
type BoxBody = http_body_util::combinators::BoxBody<bytes::Bytes, Box<dyn std::error::Error + Send + Sync>>;
|
||||
type BoxBody = http_body_util::combinators::BoxBody<
|
||||
bytes::Bytes,
|
||||
Box<dyn std::error::Error + Send + Sync>,
|
||||
>;
|
||||
|
||||
// Snapshot current dynamic values (may be updated by remote config)
|
||||
let current_node_id = node_id.read().unwrap().clone();
|
||||
@@ -145,6 +142,18 @@ async fn serve_connection<I>(
|
||||
.boxed()
|
||||
});
|
||||
Ok::<_, hyper::Error>(resp)
|
||||
} else if req.uri().path() == "/_aether/delegate" && req.method() == hyper::Method::POST
|
||||
{
|
||||
let resp = delegate::handle_delegate(
|
||||
req,
|
||||
config,
|
||||
¤t_node_id,
|
||||
&allowed_ports,
|
||||
timestamp_tolerance,
|
||||
&delegate_client,
|
||||
)
|
||||
.await;
|
||||
Ok(resp)
|
||||
} else {
|
||||
let resp = plain::handle_plain(
|
||||
req,
|
||||
@@ -154,7 +163,6 @@ async fn serve_connection<I>(
|
||||
timestamp_tolerance,
|
||||
)
|
||||
.await;
|
||||
// plain::handle_plain already returns BoxBody (streaming)
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,7 @@ pub fn ensure_self_signed_cert(cert_path: &Path, key_path: &Path) -> anyhow::Res
|
||||
|
||||
info!("generating self-signed TLS certificate");
|
||||
|
||||
let mut params = CertificateParams::new(vec![
|
||||
"localhost".into(),
|
||||
"aether-proxy".into(),
|
||||
])?;
|
||||
let mut params = CertificateParams::new(vec!["localhost".into(), "aether-proxy".into()])?;
|
||||
params.distinguished_name = rcgen::DistinguishedName::new();
|
||||
params
|
||||
.distinguished_name
|
||||
@@ -65,8 +62,8 @@ pub fn build_tls_acceptor(cert_path: &Path, key_path: &Path) -> anyhow::Result<T
|
||||
let cert_file = fs::File::open(cert_path)?;
|
||||
let key_file = fs::File::open(key_path)?;
|
||||
|
||||
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut BufReader::new(cert_file))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let certs: Vec<CertificateDer<'static>> =
|
||||
rustls_pemfile::certs(&mut BufReader::new(cert_file)).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if certs.is_empty() {
|
||||
anyhow::bail!("no certificates found in {}", cert_path.display());
|
||||
@@ -89,8 +86,7 @@ pub fn build_tls_acceptor(cert_path: &Path, key_path: &Path) -> anyhow::Result<T
|
||||
pub fn cert_sha256_fingerprint(cert_path: &Path) -> anyhow::Result<String> {
|
||||
let cert_file = fs::File::open(cert_path)?;
|
||||
let certs: Vec<CertificateDer<'static>> =
|
||||
rustls_pemfile::certs(&mut BufReader::new(cert_file))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
rustls_pemfile::certs(&mut BufReader::new(cert_file)).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let cert = certs
|
||||
.first()
|
||||
|
||||
Reference in New Issue
Block a user