diff --git a/README.md b/README.md index 8cecee5..d6f8d48 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ flowchart LR - Supports regular form configuration to rewrite Path - Support HTTP 1/2, including h2c - Support static, dns and docker label service discovery +- Support grpc-web reverse proxy - Configuration based on TOML format, the configuration method is very simple, and can be saved to files or etcd - Supports more than 10 Prometheus indicators, pull and push mode - Opentelemetry supports w3c context trace and jaeger trace diff --git a/README_zh.md b/README_zh.md index efa9aab..4892c63 100644 --- a/README_zh.md +++ b/README_zh.md @@ -19,6 +19,7 @@ flowchart LR - 支持正则形式配置重写Path,方便应用按前缀区分转发 - HTTP 1/2 的全链路支持,包括h2c的支持 - 支持静态配置、DNS以及docker label的三种服务发现形式 +- 支持grpc-web反向代理 - 基于TOML格式的配置,配置方式非常简洁,可保存至文件或etcd - 支持10多个Prometheus指标,可以使用pull与push的形式收集相关指标 - Opentelemetry支持w3c context trace与jaeger trace的形式 diff --git a/src/acme/mod.rs b/src/acme/mod.rs index b8fed1d..7c85126 100644 --- a/src/acme/mod.rs +++ b/src/acme/mod.rs @@ -48,7 +48,7 @@ pub enum Error { type Result = std::result::Result; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] pub struct CertificateInfo { pub not_after: i64, pub not_before: i64, diff --git a/src/acme/validity_checker.rs b/src/acme/validity_checker.rs index dca0b8e..cd3e26a 100644 --- a/src/acme/validity_checker.rs +++ b/src/acme/validity_checker.rs @@ -72,14 +72,16 @@ impl ServiceTask for ValidityChecker { None } fn description(&self) -> String { - let certificate_info_list = get_certificate_info_list(); + let mut names = vec![]; + for (name, _) in get_certificate_info_list().iter() { + if !names.contains(name) { + names.push(name.clone()); + } + } let offset_human: humantime::Duration = Duration::from_secs(self.time_offset as u64).into(); - format!( - "ValidityChecker: {offset_human}, {:?}", - certificate_info_list - ) + format!("ValidityChecker: {names:?}, {offset_human}") } } diff --git a/src/plugin/admin.rs b/src/plugin/admin.rs index 4276719..06af90c 100644 --- a/src/plugin/admin.rs +++ b/src/plugin/admin.rs @@ -27,6 +27,7 @@ use crate::config::{ }; use crate::http_extra::{HttpResponse, HTTP_HEADER_WWW_AUTHENTICATE}; use crate::limit::TtlLruLimit; +use crate::proxy::get_certificate_info_list; use crate::state::{ get_process_system_info, get_processing_accepted, get_start_time, }; @@ -45,6 +46,7 @@ use pingora::proxy::Session; use rust_embed::EmbeddedFile; use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::io::Write; use std::time::Duration; use substring::Substring; @@ -554,6 +556,14 @@ impl Plugin for AdminServe { HttpResponse::try_from_json(&AesResp { value }).unwrap_or( HttpResponse::unknown_error("Json serde fail".into()), ) + } else if path == "/certificates" { + let mut infos = HashMap::new(); + for (name, info) in get_certificate_info_list() { + infos.insert(name, info); + } + HttpResponse::try_from_json(&infos).unwrap_or( + HttpResponse::unknown_error("Json serde fail".into()), + ) } else { let mut file = path.substring(1, path.len()); if file.is_empty() { diff --git a/src/proxy/dynamic_certificate.rs b/src/proxy/dynamic_certificate.rs index cedc480..6d58e01 100644 --- a/src/proxy/dynamic_certificate.rs +++ b/src/proxy/dynamic_certificate.rs @@ -146,6 +146,7 @@ fn parse_certificate( domains, certificate: Some((cert, key)), info: Some(info), + ..Default::default() }) } @@ -158,15 +159,16 @@ fn parse_certificates( let mut errors = vec![]; for (name, certificate) in certificate_configs.iter() { match parse_certificate(certificate) { - Ok(dynamic_cert) => { - let mut names = dynamic_cert.domains.clone(); + Ok(mut dynamic_cert) => { + dynamic_cert.name = Some(name.clone()); + let mut domains = dynamic_cert.domains.clone(); let cert = Arc::new(dynamic_cert); if let Some(value) = &certificate.domains { - names = + domains = value.split(',').map(|item| item.to_string()).collect(); } - for name in names.iter() { - dynamic_certs.insert(name.to_string(), cert.clone()); + for domain in domains.iter() { + dynamic_certs.insert(domain.to_string(), cert.clone()); } let is_default = certificate.is_default.unwrap_or_default(); if is_default { @@ -218,7 +220,12 @@ pub fn get_certificate_info_list() -> Vec<(String, CertificateInfo)> { let mut infos = vec![]; for (name, cert) in DYNAMIC_CERTIFICATE_MAP.load().iter() { if let Some(info) = &cert.info { - infos.push((name.to_string(), info.clone())); + let key = if let Some(name) = &cert.name { + name.clone() + } else { + name.clone() + }; + infos.push((key, info.clone())); } } infos @@ -226,6 +233,7 @@ pub fn get_certificate_info_list() -> Vec<(String, CertificateInfo)> { #[derive(Debug, Clone, Default)] pub struct DynamicCertificate { + name: Option, chain_certificate: Option, certificate: Option<(X509, PKey)>, domains: Vec, diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 7e6892a..4a9c63b 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -14,6 +14,8 @@ import useBasicState from "@/states/basic"; import { useI18n } from "@/i18n"; import { listify } from "radash"; import { Button } from "@/components/ui/button"; +import { useAsync } from "react-async-hook"; +import React from "react"; interface Summary { name: string; @@ -23,11 +25,41 @@ interface Summary { export default function Home() { const homeI18n = useI18n("home"); - const [config, initialized] = useConfigState((state) => [ + const [config, initialized, getCertificateInfos] = useConfigState((state) => [ state.data, state.initialized, + state.getCertificateInfos, ]); const [basicInfo] = useBasicState((state) => [state.data]); + const [validity, setValidity] = React.useState({} as Record); + useAsync(async () => { + try { + const infos = await getCertificateInfos(); + const formatDate = (value: number) => { + const date = new Date(value * 1000); + let month = `${date.getMonth()}`; + if (month.length === 1) { + month = `0${month}`; + } + let day = `${date.getDate()}`; + if (day.length === 1) { + day = `0${day}`; + } + return `${month}-${day}`; + }; + const results = {} as Record; + Object.keys(infos).forEach((name) => { + const data = infos[name]; + if (data) { + results[name] = + formatDate(data.not_before) + " : " + formatDate(data.not_after); + } + }); + setValidity(results); + } catch (err) { + console.error(err); + } + }, []); if (!initialized) { return ; } @@ -120,10 +152,14 @@ export default function Home() { ? `${certificateCount} Certificates` : `${certificateCount} Certificate`; listify(config.certificates, (name, value) => { + let date = validity[name] || ""; + if (date) { + date = ` (${date})`; + } certificateSummary.push({ name, link: `${CERTIFICATES}?name=${name}`, - value: value.domains || "", + value: (value.domains || "") + date, }); }); } diff --git a/web/src/states/config.ts b/web/src/states/config.ts index 7ecc0c8..c7b1e01 100644 --- a/web/src/states/config.ts +++ b/web/src/states/config.ts @@ -2,6 +2,11 @@ import request from "@/helpers/request"; import { random } from "@/helpers/util"; import { create } from "zustand"; +export interface CertificateInfo { + not_after: number; + not_before: number; +} + export interface Upstream { addrs: string[]; discovery?: string; @@ -158,6 +163,7 @@ interface ConfigState { ) => Promise; remove: (category: string, name: string) => Promise; getIncludeOptions: () => string[]; + getCertificateInfos: () => Promise>; } const useConfigState = create()((set, get) => ({ @@ -227,6 +233,11 @@ const useConfigState = create()((set, get) => ({ }); return includes; }, + getCertificateInfos: async () => { + const { data } = + await request.get>(`/certificates`); + return data; + }, })); export default useConfigState;