From 8b72f56ace2b7494f0e395ae8d59b21dc6c6a673 Mon Sep 17 00:00:00 2001 From: smail Date: Thu, 1 Oct 2026 13:52:13 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E9=83=A8=E7=BD=B2=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E5=92=8C=E5=88=9D=E5=A7=8B=E5=8C=96=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/nginx/acme-dns.env.example | 2 +- deploy/nginx/acme-renew.env.example | 4 ++- deploy/nginx/deploy_certificate.py | 30 ++++++++++++++++----- deploy/nginx/renew_certificate.py | 2 +- frontend/src/views/Setup/Setup.js | 2 +- test/deployment/test_certificate_renewal.py | 25 +++++++++++++++-- test/deployment/test_certificate_webroot.py | 3 ++- test/deployment/test_domain_acceptance.py | 8 +++--- 8 files changed, 59 insertions(+), 17 deletions(-) diff --git a/deploy/nginx/acme-dns.env.example b/deploy/nginx/acme-dns.env.example index 5e67db6..0f18286 100644 --- a/deploy/nginx/acme-dns.env.example +++ b/deploy/nginx/acme-dns.env.example @@ -1,6 +1,6 @@ # 仅为阿里云 DNS 配置示例,实际服务商和授权范围必须先确认。 # 正式文件位于 /etc/smail/acme-dns.env,归 root 所有,权限为 0600。 # 禁止将填写后的文件、密钥或环境变量输出提交到仓库。 -SMAIL_ACME_DNS_PROVIDER=alidns +SMAIL_ACME_DNS_PROVIDER= ALICLOUD_ACCESS_KEY= ALICLOUD_SECRET_KEY= diff --git a/deploy/nginx/acme-renew.env.example b/deploy/nginx/acme-renew.env.example index ed741ba..8aaa304 100644 --- a/deploy/nginx/acme-renew.env.example +++ b/deploy/nginx/acme-renew.env.example @@ -2,4 +2,6 @@ # 正式文件为 /etc/smail/acme-renew.env,root 所有,权限 0600。 # HTTP 模式复用 Nginx 的 /var/lib/smail-acme,不监听新端口,不停止服务。 # 使用 DNS 模式时设为 dns,并在 acme-dns.env 中配置专用服务商凭据。 -SMAIL_ACME_CHALLENGE=http +SMAIL_MAIL_DOMAIN= +SMAIL_WEB_DOMAIN= +SMAIL_ACME_CHALLENGE=dns diff --git a/deploy/nginx/deploy_certificate.py b/deploy/nginx/deploy_certificate.py index 7ef22e6..b6c0d75 100644 --- a/deploy/nginx/deploy_certificate.py +++ b/deploy/nginx/deploy_certificate.py @@ -8,6 +8,7 @@ import hashlib import os from pathlib import Path import pwd +import re import shlex import socket import ssl @@ -17,10 +18,25 @@ import time import uuid -DOMAINS = ('cdis.cn', '*.cdis.cn') -VERIFY_HOSTNAMES = ('cdis.cn', 'web.cdis.cn', 'mail.cdis.cn', 'smtp.cdis.cn', - 'imap.cdis.cn', 'pop.cdis.cn') -CERTIFICATE_SNI = 'web.cdis.cn' +def configured_domain(name): + value = os.environ[name].strip().lower() + labels = value.split('.') + if (len(value) > 253 or len(labels) < 2 + or any(not re.fullmatch(r'[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?', label) + for label in labels)): + raise ValueError(name) + return value + + +MAIL_DOMAIN = configured_domain('SMAIL_MAIL_DOMAIN') +CERTIFICATE_SNI = configured_domain('SMAIL_WEB_DOMAIN') +if (CERTIFICATE_SNI != MAIL_DOMAIN + and (not CERTIFICATE_SNI.endswith('.' + MAIL_DOMAIN) + or CERTIFICATE_SNI.count('.') != MAIL_DOMAIN.count('.') + 1)): + raise ValueError('SMAIL_WEB_DOMAIN') +DOMAINS = (MAIL_DOMAIN, '*.' + MAIL_DOMAIN) +VERIFY_HOSTNAMES = (MAIL_DOMAIN, CERTIFICATE_SNI, + *(prefix + '.' + MAIL_DOMAIN for prefix in ('mail', 'smtp', 'imap', 'pop'))) LINEAGE = Path('/var/lib/smail-acme-tls/certificates') TARGET = Path('/opt/smail/ssl') BACKUPS = Path('/var/backups') @@ -147,9 +163,9 @@ def publish(): check_nginx_paths() run('systemctl', 'is-active', '--quiet', 'nginx') identity = pwd.getpwnam('smail') - sources = {'candidate.crt': read_file(LINEAGE / 'cdis.cn.crt', 0), - 'candidate.key': read_file(LINEAGE / 'cdis.cn.key', 0), - 'candidate.issuer.crt': read_file(LINEAGE / 'cdis.cn.issuer.crt', 0), + sources = {'candidate.crt': read_file(LINEAGE / (MAIL_DOMAIN + '.crt'), 0), + 'candidate.key': read_file(LINEAGE / (MAIL_DOMAIN + '.key'), 0), + 'candidate.issuer.crt': read_file(LINEAGE / (MAIL_DOMAIN + '.issuer.crt'), 0), 'previous.crt': read_file(TARGET / 'custom.crt', identity.pw_uid), 'previous.key': read_file(TARGET / 'custom.key', identity.pw_uid)} stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S') + '-' + uuid.uuid4().hex[:8] diff --git a/deploy/nginx/renew_certificate.py b/deploy/nginx/renew_certificate.py index 727b0ab..16f9491 100644 --- a/deploy/nginx/renew_certificate.py +++ b/deploy/nginx/renew_certificate.py @@ -29,7 +29,7 @@ def challenge_arguments(): return ['--dns', dns_provider()] if method != 'http': raise RuntimeError('验证方式必须为 dns 或 http,不允许自动回退到停机验证') - if '*.cdis.cn' in DOMAINS: + if any(domain.startswith('*.') for domain in DOMAINS): raise RuntimeError('泛域名证书必须使用 DNS 验证,不能使用 HTTP 验证') private = ROOT.lstat() if (ROOT.resolve(strict=True) != ROOT or not stat.S_ISDIR(private.st_mode) diff --git a/frontend/src/views/Setup/Setup.js b/frontend/src/views/Setup/Setup.js index 4bf6809..05de797 100644 --- a/frontend/src/views/Setup/Setup.js +++ b/frontend/src/views/Setup/Setup.js @@ -66,7 +66,7 @@ export function usePage() { const waitDesc = ref(lang.wait_desc); const adminSettings = reactive({ - "account": "admin", + "account": "", "password": "", "password2": "", "hadSeted": false diff --git a/test/deployment/test_certificate_renewal.py b/test/deployment/test_certificate_renewal.py index 84abc3d..ee3f055 100644 --- a/test/deployment/test_certificate_renewal.py +++ b/test/deployment/test_certificate_renewal.py @@ -17,11 +17,32 @@ SCRIPTS = BASE if (BASE / 'deploy_certificate.py').exists() else BASE.parents[1] sys.path.insert(0, str(SCRIPTS)) -import deploy_certificate as deploy -import renew_certificate as renew +with patch.dict(os.environ, {'SMAIL_MAIL_DOMAIN': 'cdis.cn', 'SMAIL_WEB_DOMAIN': 'web.cdis.cn'}): + import deploy_certificate as deploy + import renew_certificate as renew import nginx_certificate_cutover as cutover +class DomainConfigurationTests(unittest.TestCase): + def test_domain_requires_explicit_configuration(self): + with patch.dict(os.environ, {}, clear=True), self.assertRaises(KeyError): + deploy.configured_domain('SMAIL_MAIL_DOMAIN') + + def test_domain_rejects_invalid_input(self): + for value in ('', ' ', 'localhost', '../example.test', 'https://example.test', + '*.example.test', 'example.test:443', 'example..test', + '-mail.example.test', 'mail-.example.test', + 'a' * 64 + '.test', '.'.join(['a' * 63] * 4)): + with self.subTest(value=value), \ + patch.dict(os.environ, {'SMAIL_MAIL_DOMAIN': value}), \ + self.assertRaises(ValueError): + deploy.configured_domain('SMAIL_MAIL_DOMAIN') + + def test_domain_uses_user_configuration(self): + with patch.dict(os.environ, {'SMAIL_MAIL_DOMAIN': ' New.Example.Test '}): + self.assertEqual(deploy.configured_domain('SMAIL_MAIL_DOMAIN'), 'new.example.test') + + class CutoverTests(unittest.TestCase): def test_atomic_config_preserves_mode_under_restrictive_umask(self): directory = Path(tempfile.mkdtemp(prefix='cutover-tests-', dir=Path.cwd())) diff --git a/test/deployment/test_certificate_webroot.py b/test/deployment/test_certificate_webroot.py index 704d376..b2a8289 100644 --- a/test/deployment/test_certificate_webroot.py +++ b/test/deployment/test_certificate_webroot.py @@ -13,7 +13,8 @@ from unittest.mock import patch BASE = Path(__file__).resolve().parent SCRIPTS = BASE if (BASE / 'renew_certificate.py').exists() else BASE.parents[1] / 'deploy/nginx' sys.path.insert(0, str(SCRIPTS)) -import renew_certificate as renew +with patch.dict(os.environ, {'SMAIL_MAIL_DOMAIN': 'cdis.cn', 'SMAIL_WEB_DOMAIN': 'web.cdis.cn'}): + import renew_certificate as renew class WebrootTests(unittest.TestCase): diff --git a/test/deployment/test_domain_acceptance.py b/test/deployment/test_domain_acceptance.py index 540e582..ce718ad 100644 --- a/test/deployment/test_domain_acceptance.py +++ b/test/deployment/test_domain_acceptance.py @@ -18,9 +18,11 @@ class DomainAcceptanceTest(unittest.TestCase): self.assertNotIn('return 308 https://__SMAIL_MAIL_DOMAIN__$request_uri;', NGINX) def test_certificate_requests_root_and_wildcard(self): - self.assertIn("DOMAINS = ('cdis.cn', '*.cdis.cn')", CERTIFICATE) - self.assertIn("CERTIFICATE_SNI = 'web.cdis.cn'", CERTIFICATE) - self.assertIn("'web.cdis.cn'", CERTIFICATE) + self.assertIn("DOMAINS = (MAIL_DOMAIN, '*.' + MAIL_DOMAIN)", CERTIFICATE) + self.assertIn("configured_domain('SMAIL_MAIL_DOMAIN')", CERTIFICATE) + self.assertIn("configured_domain('SMAIL_WEB_DOMAIN')", CERTIFICATE) + self.assertNotIn('cdis.cn', CERTIFICATE) + self.assertIn("any(domain.startswith('*.') for domain in DOMAINS)", RENEWAL) self.assertIn('DOMAINS = deploy.DOMAINS', RENEWAL)