This commit is contained in:
parent
e8d4521965
commit
5f62762d97
@ -7,7 +7,30 @@ func (c *Config) UsesProxyHTTPS() bool {
|
||||
return c != nil && strings.EqualFold(strings.TrimSpace(c.TLSMode), HTTPSTLSModeProxy)
|
||||
}
|
||||
|
||||
// AutomaticSSLEnabled 保留原证书策略,但反代模式下禁止 Smail 自动签发和续期。
|
||||
func (c *Config) AutomaticSSLEnabled() bool {
|
||||
return c != nil && !c.UsesProxyHTTPS() && (c.SSLType == SSLTypeAutoHTTP || c.SSLType == SSLTypeAutoDNS)
|
||||
// EffectiveSSLType 以证书管理责任为准,仅在旧配置未记录管理方式时兼容旧类型。
|
||||
func (c *Config) EffectiveSSLType() string {
|
||||
if c == nil || c.UsesProxyHTTPS() {
|
||||
return SSLTypeUser
|
||||
}
|
||||
management := c.SSL.Management
|
||||
if management == "" && c.SSL.Mode != "" {
|
||||
management = EffectiveSSLManagement(c.SSL)
|
||||
}
|
||||
switch management {
|
||||
case SSLManagementProject:
|
||||
if c.SSL.Challenge == SSLChallengeDNS || (c.SSL.Challenge == "" && c.SSLType == SSLTypeAutoDNS) {
|
||||
return SSLTypeAutoDNS
|
||||
}
|
||||
return SSLTypeAutoHTTP
|
||||
case "":
|
||||
if c.SSLType == SSLTypeAutoHTTP || c.SSLType == SSLTypeAutoDNS {
|
||||
return c.SSLType
|
||||
}
|
||||
}
|
||||
return SSLTypeUser
|
||||
}
|
||||
|
||||
// AutomaticSSLEnabled 仅允许项目自托管证书自动签发和续期。
|
||||
func (c *Config) AutomaticSSLEnabled() bool {
|
||||
return c.EffectiveSSLType() != SSLTypeUser
|
||||
}
|
||||
|
||||
36
backend/config/ssl_policy_test.go
Normal file
36
backend/config/ssl_policy_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSSLPolicyFollowsManagement(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
want string
|
||||
}{
|
||||
{name: "nil", want: SSLTypeUser},
|
||||
{name: "empty", cfg: &Config{}, want: SSLTypeUser},
|
||||
{name: "legacy_http", cfg: &Config{SSLType: SSLTypeAutoHTTP}, want: SSLTypeAutoHTTP},
|
||||
{name: "legacy_dns", cfg: &Config{SSLType: SSLTypeAutoDNS}, want: SSLTypeAutoDNS},
|
||||
{name: "legacy_manual", cfg: &Config{SSLType: SSLTypeUser}, want: SSLTypeUser},
|
||||
{name: "legacy_upload", cfg: &Config{SSLType: SSLTypeAutoHTTP, SSL: SSLConfig{Mode: ManagedSSLModeUpload}}, want: SSLTypeUser},
|
||||
{name: "legacy_default", cfg: &Config{SSLType: SSLTypeUser, SSL: SSLConfig{Mode: ManagedSSLModeDefault}}, want: SSLTypeAutoHTTP},
|
||||
{name: "project_http", cfg: &Config{SSLType: SSLTypeAutoDNS, SSL: SSLConfig{Management: SSLManagementProject, Challenge: SSLChallengeHTTP}}, want: SSLTypeAutoHTTP},
|
||||
{name: "project_dns", cfg: &Config{SSLType: SSLTypeUser, SSL: SSLConfig{Management: SSLManagementProject, Challenge: SSLChallengeDNS}}, want: SSLTypeAutoDNS},
|
||||
{name: "project_transition", cfg: &Config{SSLType: SSLTypeUser, SSL: SSLConfig{Management: SSLManagementProject, Mode: ManagedSSLModeUpload}}, want: SSLTypeAutoHTTP},
|
||||
{name: "project_legacy_dns", cfg: &Config{SSLType: SSLTypeAutoDNS, SSL: SSLConfig{Management: SSLManagementProject}}, want: SSLTypeAutoDNS},
|
||||
{name: "custom_stale_auto", cfg: &Config{SSLType: SSLTypeAutoHTTP, SSL: SSLConfig{Management: SSLManagementCustom, Mode: ManagedSSLModeDefault}}, want: SSLTypeUser},
|
||||
{name: "web_server_stale_auto", cfg: &Config{SSLType: SSLTypeAutoDNS, SSL: SSLConfig{Management: SSLManagementWebServer}}, want: SSLTypeUser},
|
||||
{name: "proxy_project", cfg: &Config{TLSMode: " PROXY ", SSL: SSLConfig{Management: SSLManagementProject}}, want: SSLTypeUser},
|
||||
{name: "invalid_management", cfg: &Config{SSLType: SSLTypeAutoHTTP, SSL: SSLConfig{Management: "invalid"}}, want: SSLTypeUser},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := test.cfg.EffectiveSSLType(); got != test.want {
|
||||
t.Fatalf("证书策略错误:got=%s want=%s", got, test.want)
|
||||
}
|
||||
if test.cfg.AutomaticSSLEnabled() != (test.want != SSLTypeUser) {
|
||||
t.Fatal("自动续期开关与托管策略不一致")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
58
backend/listen/cron_server/ssl_policy_test.go
Normal file
58
backend/listen/cron_server/ssl_policy_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package cron_server
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
)
|
||||
|
||||
type policyHTTPTransport struct {
|
||||
calls int
|
||||
}
|
||||
|
||||
func (transport *policyHTTPTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
transport.calls++
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("ok")), Header: make(http.Header), Request: request}, nil
|
||||
}
|
||||
|
||||
func TestMaintenanceFollowsManagementSwitch(t *testing.T) {
|
||||
previousRoot, previousSnapshot := config.ROOT_PATH, config.CurrentConfig()
|
||||
previousManager, previousTransport := config.CurrentManager(), http.DefaultTransport
|
||||
config.ROOT_PATH = t.TempDir()
|
||||
config.SetRuntimeManager(nil)
|
||||
transport := &policyHTTPTransport{}
|
||||
http.DefaultTransport = transport
|
||||
t.Cleanup(func() {
|
||||
config.ROOT_PATH = previousRoot
|
||||
config.PublishConfigSnapshot(previousSnapshot)
|
||||
config.SetRuntimeManager(previousManager)
|
||||
http.DefaultTransport = previousTransport
|
||||
})
|
||||
cfg := &config.Config{
|
||||
HttpPort: 8000, SSLType: config.SSLTypeAutoHTTP, SetupState: config.SetupStateReady,
|
||||
SSL: config.SSLConfig{Management: config.SSLManagementProject},
|
||||
}
|
||||
config.PublishConfigSnapshot(cfg)
|
||||
if !waitHTTPReady() || transport.calls != 1 {
|
||||
t.Fatal("项目托管未启用 HTTP 就绪检查")
|
||||
}
|
||||
for _, management := range []string{config.SSLManagementCustom, config.SSLManagementWebServer} {
|
||||
cfg.SSL.Management = management
|
||||
config.PublishConfigSnapshot(cfg)
|
||||
if waitHTTPReady() {
|
||||
t.Fatal("外部托管仍等待 HTTP 验证")
|
||||
}
|
||||
maintainSSL()
|
||||
if transport.calls != 1 {
|
||||
t.Fatal("切换为外部托管后仍发起了网络请求")
|
||||
}
|
||||
}
|
||||
cfg.SSL.Management = config.SSLManagementProject
|
||||
config.PublishConfigSnapshot(cfg)
|
||||
if !waitHTTPReady() || transport.calls != 2 {
|
||||
t.Fatal("切回项目托管未恢复 HTTP 就绪检查")
|
||||
}
|
||||
}
|
||||
@ -46,7 +46,7 @@ func maintainSSL() {
|
||||
sslCheck()
|
||||
return
|
||||
}
|
||||
if cfg.SSLType == config.SSLTypeAutoHTTP && !waitHTTPReady() {
|
||||
if cfg.EffectiveSSLType() == config.SSLTypeAutoHTTP && !waitHTTPReady() {
|
||||
return
|
||||
}
|
||||
ssl.Update(true)
|
||||
|
||||
@ -89,11 +89,11 @@ func GetSSL() (string, error) {
|
||||
if cfg.UsesProxyHTTPS() {
|
||||
return config.SSLTypeUser, nil
|
||||
}
|
||||
if cfg.SSLType == "" {
|
||||
if cfg.SSLType == "" && cfg.SSL.Management == "" && cfg.SSL.Mode == "" {
|
||||
return config.SSLTypeAutoHTTP, nil
|
||||
}
|
||||
|
||||
return cfg.SSLType, nil
|
||||
return cfg.EffectiveSSLType(), nil
|
||||
}
|
||||
|
||||
func SetSSL(sslType, priKey, crtKey string) error {
|
||||
@ -173,6 +173,7 @@ func applyManagedSSLReference(cfg *config.Config, reference config.ManagedSSLRef
|
||||
return
|
||||
}
|
||||
clearLegacySSLPaths(cfg)
|
||||
sslType := cfg.EffectiveSSLType()
|
||||
management := cfg.SSL.Management
|
||||
if reference.Mode == config.ManagedSSLModeDefault {
|
||||
management = config.SSLManagementProject
|
||||
@ -188,7 +189,7 @@ func applyManagedSSLReference(cfg *config.Config, reference config.ManagedSSLRef
|
||||
}
|
||||
if reference.Mode == config.ManagedSSLModeUpload {
|
||||
cfg.SSL.CustomDirectory = "ssl"
|
||||
} else if cfg.SSLType == config.SSLTypeAutoDNS {
|
||||
} else if sslType == config.SSLTypeAutoDNS {
|
||||
cfg.SSL.Challenge = config.SSLChallengeDNS
|
||||
} else {
|
||||
cfg.SSL.Challenge = config.SSLChallengeHTTP
|
||||
@ -330,7 +331,7 @@ func certificateMatchesConfiguredDomains(cert *x509.Certificate, cfg *config.Con
|
||||
|
||||
// 自动 DNS 只申请主域泛域名;不覆盖的服务地址是配置错误,不能靠重复续期修复。
|
||||
func validateWildcardConfiguration(cfg *config.Config) error {
|
||||
if cfg == nil || cfg.SSLType != config.SSLTypeAutoDNS {
|
||||
if cfg == nil || cfg.EffectiveSSLType() != config.SSLTypeAutoDNS {
|
||||
return nil
|
||||
}
|
||||
domains := wildcardCertificateDomains(cfg)
|
||||
@ -370,7 +371,7 @@ func wildcardCertificateDomains(cfg *config.Config) []string {
|
||||
}
|
||||
|
||||
func certificateDomains(cfg *config.Config) []string {
|
||||
if cfg != nil && cfg.SSLType == config.SSLTypeAutoDNS {
|
||||
if cfg != nil && cfg.EffectiveSSLType() == config.SSLTypeAutoDNS {
|
||||
return wildcardCertificateDomains(cfg)
|
||||
}
|
||||
return sslDomains(cfg)
|
||||
@ -393,12 +394,12 @@ func renewCertificate(privateKey *ecdsa.PrivateKey, cfg *config.Config) error {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
if cfg.SSLType == config.SSLTypeAutoHTTP {
|
||||
if cfg.EffectiveSSLType() == config.SSLTypeAutoHTTP {
|
||||
err = client.Challenge.SetHTTP01Provider(GetHttpChallengeInstance())
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
} else if cfg.SSLType == config.SSLTypeAutoDNS {
|
||||
} else if cfg.EffectiveSSLType() == config.SSLTypeAutoDNS {
|
||||
err = client.Challenge.SetDNS01Provider(GetDnsChallengeInstance(), dns01.AddDNSTimeout(60*time.Minute))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
@ -456,12 +457,12 @@ func generateCertificate(privateKey *ecdsa.PrivateKey, cfg *config.Config, newAc
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
if cfg.SSLType == config.SSLTypeAutoHTTP {
|
||||
if cfg.EffectiveSSLType() == config.SSLTypeAutoHTTP {
|
||||
err = client.Challenge.SetHTTP01Provider(GetHttpChallengeInstance())
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
} else if cfg.SSLType == config.SSLTypeAutoDNS {
|
||||
} else if cfg.EffectiveSSLType() == config.SSLTypeAutoDNS {
|
||||
err = client.Challenge.SetDNS01Provider(GetDnsChallengeInstance(), dns01.AddDNSTimeout(60*time.Minute))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
|
||||
78
backend/services/setup/ssl/ssl_policy_test.go
Normal file
78
backend/services/setup/ssl/ssl_policy_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
package ssl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
)
|
||||
|
||||
type renewalBlockedTransport struct {
|
||||
calls int
|
||||
}
|
||||
|
||||
func (transport *renewalBlockedTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
transport.calls++
|
||||
return nil, errors.New("测试禁止外部网络请求")
|
||||
}
|
||||
|
||||
func TestExternalManagementBlocksAutomaticIssuance(t *testing.T) {
|
||||
previousRoot, previousInstance := config.ROOT_PATH, config.Instance
|
||||
previousSnapshot, previousManager := config.CurrentConfig(), config.CurrentManager()
|
||||
previousTransport := http.DefaultTransport
|
||||
config.SetRuntimeManager(nil)
|
||||
t.Cleanup(func() {
|
||||
config.ROOT_PATH, config.Instance = previousRoot, previousInstance
|
||||
config.PublishConfigSnapshot(previousSnapshot)
|
||||
config.SetRuntimeManager(previousManager)
|
||||
http.DefaultTransport = previousTransport
|
||||
})
|
||||
for _, management := range []string{config.SSLManagementCustom, config.SSLManagementWebServer} {
|
||||
for _, sslType := range []string{config.SSLTypeAutoHTTP, config.SSLTypeAutoDNS} {
|
||||
t.Run(management+sslType, func(t *testing.T) {
|
||||
config.ROOT_PATH = t.TempDir()
|
||||
cfg := &config.Config{
|
||||
SSLType: sslType, SetupState: config.SetupStateReady,
|
||||
SSL: config.SSLConfig{Management: management},
|
||||
}
|
||||
if err := config.WriteConfig(cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(config.ROOT_PATH, "config", "config.json")
|
||||
before, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
transport := &renewalBlockedTransport{}
|
||||
http.DefaultTransport = transport
|
||||
if selected, err := GetSSL(); err != nil || selected != config.SSLTypeUser {
|
||||
t.Fatalf("外部托管仍显示自动签发:type=%s err=%v", selected, err)
|
||||
}
|
||||
for _, update := range []bool{false, true} {
|
||||
if err := GenSSL(update); UserCertificateErrorMessage(err) != "当前证书由外部维护,不能自动申请或续期" {
|
||||
t.Fatalf("外部托管未拒绝签发或续期:%v", err)
|
||||
}
|
||||
}
|
||||
Update(true)
|
||||
after, err := os.ReadFile(path)
|
||||
if err != nil || !bytes.Equal(before, after) || transport.calls != 0 {
|
||||
t.Fatalf("外部托管产生了配置写入或网络请求:calls=%d err=%v", transport.calls, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectDNSReferencePreservesChallenge(t *testing.T) {
|
||||
cfg := &config.Config{SSLType: config.SSLTypeUser, SSL: config.SSLConfig{
|
||||
Management: config.SSLManagementProject, Challenge: config.SSLChallengeDNS,
|
||||
}}
|
||||
applyManagedSSLReference(cfg, config.ManagedSSLReference{Mode: config.ManagedSSLModeDefault})
|
||||
if cfg.SSL.Challenge != config.SSLChallengeDNS || cfg.EffectiveSSLType() != config.SSLTypeAutoDNS {
|
||||
t.Fatal("保存签发结果丢失了项目托管的 DNS 验证方式")
|
||||
}
|
||||
}
|
||||
@ -124,14 +124,14 @@ const lang = {
|
||||
"ssl_auto_http": "HTTP请求",
|
||||
"ssl_auto_dns": "DNS记录",
|
||||
"ssl_challenge_type": "验证方式",
|
||||
"ssl_manuallyf": "手动配置SSL证书",
|
||||
"ssl_manuallyf": "手动配置SSL证书(不自动续期)",
|
||||
"challenge_typ_desc": "HTTP验证要求域名可以从公网访问80端口;无法开放80端口时,请选择DNS验证并添加页面生成的TXT记录。",
|
||||
"wait_desc": "请稍等",
|
||||
"dns_challenge_wait": "DNS传播和缓存刷新时间较长,此处可能等待10-30分钟",
|
||||
"ssl_key_path": "SSL 私钥路径",
|
||||
"ssl_crt_path": "SSL 证书路径",
|
||||
"ssl_auto_storage_title": "自动证书保存位置",
|
||||
"ssl_auto_storage_desc": "自动申请成功后,证书由系统统一保存和轮换,不需要手动填写证书路径。",
|
||||
"ssl_auto_storage_desc": "证书由项目自身托管,启用内置自动续期;到期前 30 天内检查并续期,续期时仍需完成所选验证方式。",
|
||||
"ssl_auto_certificate_path": "证书:./data/ssl/managed/default/current/public.crt",
|
||||
"ssl_auto_private_key_path": "私钥:./data/ssl/managed/default/current/private.key",
|
||||
"ssl_auto_account_key_path": "ACME 账户密钥:./data/ssl/account_private.pem",
|
||||
|
||||
@ -169,9 +169,9 @@ const sslGroups = [
|
||||
wide: true,
|
||||
fallback: 'custom',
|
||||
options: options([
|
||||
['web_server', 'Web 服务器管理'],
|
||||
['project', '项目自身管理'],
|
||||
['custom', '自定义']
|
||||
['web_server', '代理服务器托管(不自动续期)'],
|
||||
['project', '项目自身托管(自动续期)'],
|
||||
['custom', '手动配置(不自动续期)']
|
||||
])
|
||||
}),
|
||||
field('tls.challenge', '证书验证方式', 'select', {
|
||||
|
||||
@ -343,9 +343,9 @@ test('证书管理方式只展示三种业务模式并同步底层托管配置',
|
||||
assert.deepEqual(validateFormConfig('ssl', { tls }), {})
|
||||
const management = getFieldGroups('ssl', { tls }).flatMap(group => group.fields).find(field => field.path.join('.') === 'tls.management')
|
||||
assert.deepEqual(management.options.map(option => [option.value, option.label]), [
|
||||
['web_server', 'Web 服务器管理'],
|
||||
['project', '项目自身管理'],
|
||||
['custom', '自定义']
|
||||
['web_server', '代理服务器托管(不自动续期)'],
|
||||
['project', '项目自身托管(自动续期)'],
|
||||
['custom', '手动配置(不自动续期)']
|
||||
])
|
||||
for (const customDirectory of ['C:/certs', '/etc/ssl', '../certs', 'config/../certs', 'config\\ssl', 'config//ssl']) {
|
||||
assert.ok(validateFormConfig('ssl', { tls: { ...tls, customDirectory } })['tls.customDirectory'])
|
||||
|
||||
Loading…
Reference in New Issue
Block a user