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,164 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fonu/fonu/internal/proxy"
|
||||
)
|
||||
|
||||
func osReadDir(path string) ([]os.DirEntry, error) {
|
||||
return os.ReadDir(path)
|
||||
}
|
||||
|
||||
type CertSource struct {
|
||||
Domains []string
|
||||
CertPath string
|
||||
KeyPath string
|
||||
}
|
||||
|
||||
func FindCertificateForHosts(certsDir string, hostnames []string, certs []CertSource) *certFiles {
|
||||
return findCertificateForHosts(certsDir, hostnames, certs)
|
||||
}
|
||||
|
||||
func HasCertificateForHosts(certsDir string, hostnames []string, certs []CertSource) bool {
|
||||
return certUsable(findCertificateForHosts(certsDir, hostnames, certs))
|
||||
}
|
||||
|
||||
func findCertificateForHosts(certsDir string, hostnames []string, certs []CertSource) *certFiles {
|
||||
for _, hostname := range hostnames {
|
||||
for _, source := range certs {
|
||||
if !hostCoveredByCert(hostname, source.Domains) {
|
||||
continue
|
||||
}
|
||||
if cert := certFromSource(source); cert != nil {
|
||||
return cert
|
||||
}
|
||||
for _, domain := range source.Domains {
|
||||
if cert := certAt(certsDir, storageDirName(domain)); cert != nil && certUsable(cert) {
|
||||
return cert
|
||||
}
|
||||
}
|
||||
}
|
||||
if cert := findCertificate(certsDir, hostname); cert != nil && certUsable(cert) {
|
||||
return cert
|
||||
}
|
||||
}
|
||||
return scanCertDirectories(certsDir, hostnames)
|
||||
}
|
||||
|
||||
func storageDirName(domain string) string {
|
||||
domain = strings.TrimSpace(domain)
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
return "wildcard." + strings.TrimPrefix(domain, "*.")
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
func certFromSource(source CertSource) *certFiles {
|
||||
cert := &certFiles{
|
||||
CertPath: absNginxPath(source.CertPath),
|
||||
KeyPath: absNginxPath(source.KeyPath),
|
||||
}
|
||||
if !certUsable(cert) {
|
||||
return nil
|
||||
}
|
||||
return cert
|
||||
}
|
||||
|
||||
func hostCoveredByCert(hostname string, domains []string) bool {
|
||||
hostname = strings.ToLower(strings.TrimSpace(hostname))
|
||||
for _, domain := range domains {
|
||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||
if domain == "" {
|
||||
continue
|
||||
}
|
||||
if domain == hostname {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
suffix := strings.TrimPrefix(domain, "*.")
|
||||
if hostname == suffix || strings.HasSuffix(hostname, "."+suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func scanCertDirectories(certsDir string, hostnames []string) *certFiles {
|
||||
entries, err := osReadDir(certsDir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
pattern := dirNameToPattern(entry.Name())
|
||||
for _, hostname := range hostnames {
|
||||
if !hostCoveredByCert(hostname, []string{pattern, entry.Name()}) {
|
||||
continue
|
||||
}
|
||||
if cert := certAt(certsDir, pattern); cert != nil && certUsable(cert) {
|
||||
return cert
|
||||
}
|
||||
if cert := certAt(certsDir, entry.Name()); cert != nil && certUsable(cert) {
|
||||
return cert
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dirNameToPattern(name string) string {
|
||||
if strings.HasPrefix(name, "wildcard.") {
|
||||
return "*." + strings.TrimPrefix(name, "wildcard.")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func certSummary(certs []CertSource) string {
|
||||
seen := map[string]struct{}{}
|
||||
var domains []string
|
||||
for _, cert := range certs {
|
||||
for _, domain := range cert.Domains {
|
||||
if domain == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[domain]; ok {
|
||||
continue
|
||||
}
|
||||
seen[domain] = struct{}{}
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
}
|
||||
if len(domains) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return strings.Join(domains, ", ")
|
||||
}
|
||||
|
||||
func HTTPSCoverageError(hostname string, certs []CertSource) error {
|
||||
return fmt.Errorf("域名 %s 未匹配到可用证书(当前证书覆盖:%s),请检查拼写或先在证书页导入/申请", hostname, certSummary(certs))
|
||||
}
|
||||
|
||||
func ValidateHTTPSRules(rules []proxy.Rule, certs []CertSource, certsDir string) error {
|
||||
for _, rule := range rules {
|
||||
if !rule.Enabled || !rule.HTTPSEnabled {
|
||||
continue
|
||||
}
|
||||
for _, group := range rule.PortGroups() {
|
||||
cert := findCertificateForHosts(certsDir, group.Hostnames, certs)
|
||||
if certUsable(cert) {
|
||||
continue
|
||||
}
|
||||
if len(group.Hostnames) == 0 {
|
||||
return fmt.Errorf("启用 HTTPS 的规则缺少前端域名")
|
||||
}
|
||||
return HTTPSCoverageError(group.Hostnames[0], certs)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user