mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-24 20:18:06 +09:00
- run wildcard/SAN certbot privileged and fail on non-zero exit instead of reporting unissued cert paths as success - cloudflare DNS-01 credentials: unpredictable mkstemp file, 0600, always deleted (was predictable /tmp path, never cleaned up) - route53 credentials via sudo env A=B (sudo env_reset strips env= kwarg) - validate domains before they reach certbot argv or cert file paths - validate reverse-proxy builder input (domains, upstreams, headers, locations, rate-limit/cache values) against nginx config injection - write_vhost: refuse non-plain-filename site names and roll back the vhost when the reload itself fails, not only when nginx -t fails - create_proxy API returns 400 on service errors instead of always 201
242 lines
11 KiB
Python
242 lines
11 KiB
Python
"""Phase 5 proving tests — wildcard HTTPS for managed subdomains.
|
|
|
|
A one-time setup creates the *.<base> + <base> A records via a connected DNS
|
|
provider and issues a wildcard cert, after which managed subdomain vhosts serve
|
|
TLS from it.
|
|
"""
|
|
from app.services import wordpress_bridge
|
|
|
|
|
|
def _mk_user(db, username='owner'):
|
|
from app.models import User
|
|
from werkzeug.security import generate_password_hash
|
|
u = User(email=f'{username}@test.local', username=username,
|
|
password_hash=generate_password_hash('x'),
|
|
role=User.ROLE_ADMIN, is_active=True)
|
|
db.session.add(u)
|
|
db.session.commit()
|
|
return u
|
|
|
|
|
|
def _mk_app(db, user_id, name='blog', port=8300, host='blog.lvh.me'):
|
|
from app.models import Application
|
|
from app.models.domain import Domain
|
|
a = Application(name=name, app_type='docker', user_id=user_id, root_path=f'/srv/{name}', port=port)
|
|
db.session.add(a)
|
|
db.session.commit()
|
|
db.session.add(Domain(name=host, is_primary=True, application_id=a.id))
|
|
db.session.commit()
|
|
return a
|
|
|
|
|
|
def _mk_provider(db, provider='cloudflare'):
|
|
from app.models.email import DNSProviderConfig
|
|
c = DNSProviderConfig(name='cf', provider=provider, api_key='tok', api_secret='sec')
|
|
db.session.add(c)
|
|
db.session.commit()
|
|
return c
|
|
|
|
|
|
# ── SiteDomainService HTTPS helpers ─────────────────────────────────────────
|
|
|
|
def test_https_helpers(app):
|
|
from app import db
|
|
from app.models.system_settings import SystemSettings
|
|
from app.services.site_domain_service import SiteDomainService
|
|
|
|
assert SiteDomainService.https_enabled() is False
|
|
# Testing base domain is lvh.me.
|
|
assert SiteDomainService.covers('blog.lvh.me') is True
|
|
assert SiteDomainService.covers('lvh.me') is True
|
|
assert SiteDomainService.covers('other.com') is False
|
|
fc, key = SiteDomainService.wildcard_cert_paths()
|
|
assert fc == '/etc/letsencrypt/live/lvh.me/fullchain.pem'
|
|
assert key == '/etc/letsencrypt/live/lvh.me/privkey.pem'
|
|
|
|
SystemSettings.set('sites_https_enabled', True, value_type='boolean')
|
|
db.session.commit()
|
|
assert SiteDomainService.https_enabled() is True
|
|
|
|
|
|
# ── nginx SSL config generation (pure) ──────────────────────────────────────
|
|
|
|
def test_with_ssl_builds_redirect_and_443():
|
|
from app.services.nginx_service import NginxService
|
|
http = NginxService.DOCKER_SITE_TEMPLATE.format(name='blog', domains='blog.lvh.me', port=8300)
|
|
out = NginxService._with_ssl(http, 'blog.lvh.me', '/c/fullchain.pem', '/c/privkey.pem')
|
|
assert 'return 301 https://$server_name$request_uri;' in out # HTTP->HTTPS redirect
|
|
assert 'listen 443 ssl http2;' in out # app block now on 443
|
|
assert 'ssl_certificate /c/fullchain.pem;' in out
|
|
assert 'ssl_certificate_key /c/privkey.pem;' in out
|
|
assert 'proxy_pass http://127.0.0.1:8300;' in out # proxy preserved
|
|
assert out.count('listen 80;') == 1 # only the redirect block on :80
|
|
|
|
|
|
# ── issue_wildcard_cert command construction ────────────────────────────────
|
|
|
|
def test_issue_wildcard_cert_route53(monkeypatch):
|
|
import app.services.advanced_ssl_service as mod
|
|
from app.utils.system import PackageManager
|
|
from app.services.advanced_ssl_service import AdvancedSSLService
|
|
|
|
monkeypatch.setattr(PackageManager, 'is_available', staticmethod(lambda: False))
|
|
captured = {}
|
|
|
|
class FakeProc:
|
|
returncode = 0
|
|
stdout = 'Congratulations'
|
|
stderr = ''
|
|
|
|
def fake_run(cmd, **kw):
|
|
captured['cmd'] = list(cmd)
|
|
captured['kw'] = kw
|
|
return FakeProc()
|
|
|
|
# certbot writes /etc/letsencrypt as root — the privileged door is the
|
|
# contract, and its returncode decides success.
|
|
monkeypatch.setattr(mod, 'run_privileged', fake_run)
|
|
|
|
res = AdvancedSSLService.issue_wildcard_cert('example.com', 'route53',
|
|
{'api_key': 'AK', 'api_secret': 'SK'},
|
|
email='ops@example.com')
|
|
assert res['success'] is True
|
|
assert res['certificate_path'] == '/etc/letsencrypt/live/example.com/fullchain.pem'
|
|
cmd = captured['cmd']
|
|
assert '--dns-route53' in cmd
|
|
assert 'example.com' in cmd and '*.example.com' in cmd
|
|
assert '--email' in cmd and 'ops@example.com' in cmd
|
|
# sudo's env_reset would strip a plain env= kwarg, so the AWS credentials
|
|
# travel as `env A=B` argv entries in front of certbot instead.
|
|
assert cmd[0] == 'env'
|
|
assert 'AWS_ACCESS_KEY_ID=AK' in cmd
|
|
assert 'AWS_SECRET_ACCESS_KEY=SK' in cmd
|
|
assert 'env' not in captured['kw']
|
|
|
|
|
|
# ── setup orchestration ─────────────────────────────────────────────────────
|
|
|
|
def test_setup_creates_records_issues_cert_persists(app, monkeypatch):
|
|
from app import db
|
|
from app.models.system_settings import SystemSettings
|
|
from app.services.dns_provider_service import DNSProviderService
|
|
from app.services.advanced_ssl_service import AdvancedSSLService
|
|
from app.services.sites_https_service import SitesHttpsService
|
|
from app.services.site_domain_service import SiteDomainService
|
|
|
|
provider = _mk_provider(db)
|
|
SystemSettings.set('server_public_ip', '203.0.113.9', value_type='string')
|
|
db.session.commit()
|
|
|
|
created = []
|
|
monkeypatch.setattr(DNSProviderService, 'ensure_a_record',
|
|
classmethod(lambda cls, host, ip: (created.append(host),
|
|
{'created': True, 'record': {'name': host, 'value': ip}})[1]))
|
|
cert_calls = {}
|
|
|
|
def fake_cert(domain, provider_name, creds, email=None):
|
|
cert_calls.update({'domain': domain, 'provider': provider_name, 'creds': creds, 'email': email})
|
|
return {'success': True, 'certificate_path': f'/etc/letsencrypt/live/{domain}/fullchain.pem'}
|
|
|
|
monkeypatch.setattr(AdvancedSSLService, 'issue_wildcard_cert', staticmethod(fake_cert))
|
|
|
|
res = SitesHttpsService.setup(provider.id, email='ops@x.com')
|
|
assert res['success'] is True
|
|
assert res['https_enabled'] is True
|
|
assert set(created) == {'*.lvh.me', 'lvh.me'} # wildcard + apex
|
|
assert cert_calls['domain'] == 'lvh.me'
|
|
assert cert_calls['provider'] == 'cloudflare'
|
|
assert cert_calls['creds'] == {'api_token': 'tok'} # reuses the connected provider's token
|
|
assert cert_calls['email'] == 'ops@x.com'
|
|
assert SiteDomainService.https_enabled() is True # persisted
|
|
|
|
|
|
def test_setup_requires_provider(app):
|
|
from app.services.sites_https_service import SitesHttpsService
|
|
res = SitesHttpsService.setup(None)
|
|
assert res['success'] is False
|
|
assert 'provider' in res['error'].lower()
|
|
|
|
|
|
def test_setup_cert_failure_does_not_enable_https(app, monkeypatch):
|
|
from app import db
|
|
from app.models.system_settings import SystemSettings
|
|
from app.services.dns_provider_service import DNSProviderService
|
|
from app.services.advanced_ssl_service import AdvancedSSLService
|
|
from app.services.sites_https_service import SitesHttpsService
|
|
from app.services.site_domain_service import SiteDomainService
|
|
|
|
provider = _mk_provider(db)
|
|
SystemSettings.set('server_public_ip', '1.2.3.4', value_type='string')
|
|
db.session.commit()
|
|
monkeypatch.setattr(DNSProviderService, 'ensure_a_record',
|
|
classmethod(lambda cls, h, ip: {'created': True, 'record': {}}))
|
|
monkeypatch.setattr(AdvancedSSLService, 'issue_wildcard_cert',
|
|
staticmethod(lambda *a, **k: {'success': False, 'error': 'dns plugin missing'}))
|
|
|
|
res = SitesHttpsService.setup(provider.id)
|
|
assert res['success'] is False
|
|
assert 'dns plugin missing' in res['error']
|
|
assert SiteDomainService.https_enabled() is False
|
|
|
|
|
|
def test_setup_warns_without_server_ip(app, monkeypatch):
|
|
from app import db
|
|
from app.services.advanced_ssl_service import AdvancedSSLService
|
|
from app.services.sites_https_service import SitesHttpsService
|
|
from app.services.site_domain_service import SiteDomainService
|
|
|
|
provider = _mk_provider(db)
|
|
monkeypatch.setattr(SiteDomainService, 'server_ip', classmethod(lambda cls: None))
|
|
monkeypatch.setattr(AdvancedSSLService, 'issue_wildcard_cert',
|
|
staticmethod(lambda *a, **k: {'success': True, 'certificate_path': '/x'}))
|
|
|
|
res = SitesHttpsService.setup(provider.id)
|
|
assert res['success'] is True
|
|
assert res['warning'] and 'manually' in res['warning'].lower()
|
|
|
|
|
|
# ── vhost wiring ────────────────────────────────────────────────────────────
|
|
|
|
def test_write_vhost_uses_wildcard_cert_when_https_enabled(app, monkeypatch, wp_extension_package):
|
|
from app import db
|
|
from app.models.system_settings import SystemSettings
|
|
from app.services import nginx_service
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
|
|
user = _mk_user(db)
|
|
app_row = _mk_app(db, user.id, host='blog.lvh.me') # covered by the lvh.me wildcard
|
|
SystemSettings.set('sites_https_enabled', True, value_type='boolean')
|
|
db.session.commit()
|
|
|
|
captured = {}
|
|
monkeypatch.setattr(nginx_service.NginxService, 'create_site',
|
|
classmethod(lambda cls, **kw: captured.update(kw) or {'success': True}))
|
|
monkeypatch.setattr(nginx_service.NginxService, 'enable_site', classmethod(lambda cls, name: {'success': True}))
|
|
|
|
WordPressService._write_app_vhost(app_row)
|
|
assert captured['ssl_cert'] == '/etc/letsencrypt/live/lvh.me/fullchain.pem'
|
|
assert captured['ssl_key'] == '/etc/letsencrypt/live/lvh.me/privkey.pem'
|
|
|
|
|
|
def test_write_vhost_no_wildcard_for_custom_domain(app, monkeypatch, wp_extension_package):
|
|
from app import db
|
|
from app.models.domain import Domain
|
|
from app.models.system_settings import SystemSettings
|
|
from app.services import nginx_service
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
|
|
user = _mk_user(db, 'o2')
|
|
app_row = _mk_app(db, user.id, name='shop', host='blog.lvh.me')
|
|
db.session.add(Domain(name='myshop.com', is_primary=True, application_id=app_row.id)) # not under base
|
|
db.session.commit()
|
|
SystemSettings.set('sites_https_enabled', True, value_type='boolean')
|
|
db.session.commit()
|
|
|
|
captured = {}
|
|
monkeypatch.setattr(nginx_service.NginxService, 'create_site',
|
|
classmethod(lambda cls, **kw: captured.update(kw) or {'success': True}))
|
|
monkeypatch.setattr(nginx_service.NginxService, 'enable_site', classmethod(lambda cls, name: {'success': True}))
|
|
|
|
WordPressService._write_app_vhost(app_row)
|
|
assert captured['ssl_cert'] is None # a non-covered domain blocks the wildcard
|