chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听) - 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP) - 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点) - 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ImportInfo struct {
|
||||
Primary string
|
||||
Domains []string
|
||||
Wildcard bool
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func InspectImport(certPEM, keyPEM []byte) (ImportInfo, error) {
|
||||
cert, err := parseCertificate(certPEM)
|
||||
if err != nil {
|
||||
return ImportInfo{}, err
|
||||
}
|
||||
key, err := parsePrivateKey(keyPEM)
|
||||
if err != nil {
|
||||
return ImportInfo{}, err
|
||||
}
|
||||
if !publicKeysMatch(cert.PublicKey, key) {
|
||||
return ImportInfo{}, fmt.Errorf("证书与私钥不匹配")
|
||||
}
|
||||
domains, err := ExtractCertDomains(cert)
|
||||
if err != nil {
|
||||
return ImportInfo{}, err
|
||||
}
|
||||
return ImportInfo{
|
||||
Primary: primaryDomain(domains),
|
||||
Domains: domains,
|
||||
Wildcard: hasWildcardDomain(domains),
|
||||
ExpiresAt: cert.NotAfter.UTC(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ValidatePEM(certPEM, keyPEM []byte) (time.Time, error) {
|
||||
info, err := InspectImport(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return info.ExpiresAt, nil
|
||||
}
|
||||
|
||||
func ExtractCertDomains(cert *x509.Certificate) ([]string, error) {
|
||||
seen := map[string]bool{}
|
||||
var domains []string
|
||||
add := func(name string) error {
|
||||
name = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(name)), ".")
|
||||
if name == "" || net.ParseIP(name) != nil {
|
||||
return nil
|
||||
}
|
||||
if seen[name] {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(name, "*.") {
|
||||
if err := validateDomainLabel(strings.TrimPrefix(name, "*.")); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := validateDomainLabel(name); err != nil {
|
||||
return err
|
||||
}
|
||||
seen[name] = true
|
||||
domains = append(domains, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, name := range cert.DNSNames {
|
||||
if err := add(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := add(cert.Subject.CommonName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(domains) == 0 {
|
||||
return nil, fmt.Errorf("证书中未包含有效域名")
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func primaryDomain(domains []string) string {
|
||||
for _, domain := range domains {
|
||||
if !strings.HasPrefix(domain, "*.") {
|
||||
return domain
|
||||
}
|
||||
}
|
||||
return strings.TrimPrefix(domains[0], "*.")
|
||||
}
|
||||
|
||||
func hasWildcardDomain(domains []string) bool {
|
||||
for _, domain := range domains {
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validateDomainLabel(domain string) error {
|
||||
if domain == "" {
|
||||
return fmt.Errorf("域名格式无效")
|
||||
}
|
||||
parts := strings.Split(domain, ".")
|
||||
for _, part := range parts {
|
||||
if part == "" || len(part) > 63 {
|
||||
return fmt.Errorf("域名格式无效")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseCertificate(certPEM []byte) (*x509.Certificate, error) {
|
||||
rest := certPEM
|
||||
for {
|
||||
block, remaining := pem.Decode(rest)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("证书 PEM 格式无效")
|
||||
}
|
||||
if block.Type == "CERTIFICATE" {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析证书失败:%w", err)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
rest = remaining
|
||||
if len(rest) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("证书 PEM 中未找到有效证书")
|
||||
}
|
||||
|
||||
func parsePrivateKey(keyPEM []byte) (crypto.PrivateKey, error) {
|
||||
block, _ := pem.Decode(keyPEM)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("私钥 PEM 格式无效")
|
||||
}
|
||||
switch block.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(block.Bytes)
|
||||
case "PRIVATE KEY":
|
||||
return x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的私钥类型:%s", block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func publicKeysMatch(certKey crypto.PublicKey, privateKey crypto.PrivateKey) bool {
|
||||
switch pub := certKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
priv, ok := privateKey.(*rsa.PrivateKey)
|
||||
return ok && pub.N.Cmp(priv.N) == 0
|
||||
case *ecdsa.PublicKey:
|
||||
priv, ok := privateKey.(*ecdsa.PrivateKey)
|
||||
return ok && pub.X.Cmp(priv.X) == 0 && pub.Y.Cmp(priv.Y) == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizePEM(v string) []byte {
|
||||
return []byte(strings.TrimSpace(v))
|
||||
}
|
||||
|
||||
func ResolvePEMSource(content, path string) ([]byte, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path != "" {
|
||||
return ReadPEMFile(path)
|
||||
}
|
||||
data := NormalizePEM(content)
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("内容不能为空")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func ReadPEMFile(path string) ([]byte, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("文件路径不能为空")
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法读取文件 %s:%w", path, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("%s 是目录,不是文件", path)
|
||||
}
|
||||
if info.Size() > 1<<20 {
|
||||
return nil, fmt.Errorf("文件过大(最大 1MB)")
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取文件失败:%w", err)
|
||||
}
|
||||
if len(strings.TrimSpace(string(data))) == 0 {
|
||||
return nil, fmt.Errorf("文件内容为空")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user