#!/usr/bin/env python3 """Request, install, and verify BaoTa-managed HTTPS for xim.bchongw.com.""" import http.client import json import os import socket import ssl import subprocess import sys import time import urllib.request from pathlib import Path PANEL = Path('/www/server/panel') DOMAIN = 'xim.bchongw.com' PROJECT = 'xim_im' EXPECTED_IP = '8.219.70.152' GO_CONF = PANEL / ('vhost/nginx/go_' + PROJECT + '.conf') def request(**values): result = public.dict_obj() for key, value in values.items(): setattr(result, key, value) return result def command(*args): result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) if result.returncode: raise RuntimeError('{} failed: {}'.format(args[0], result.stderr.strip())) return result.stdout.strip() def fetch(url, expected): last_error = None for _ in range(40): try: with urllib.request.urlopen(url, timeout=8) as response: body = response.read().decode('utf-8') if response.status == 200 and expected in body: return body except Exception as error: last_error = error time.sleep(0.5) raise RuntimeError('Public verification failed for {}: {}'.format(url, last_error)) def main(): if os.geteuid() != 0 or not PANEL.is_dir(): raise SystemExit('Run as root on the BaoTa host') os.chdir(str(PANEL)) sys.path.insert(0, str(PANEL)) sys.path.insert(0, str(PANEL / 'class')) global public import public import panelSite from acme_v2 import acme_v2 from projectModel.goModel import main as GoProject addresses = sorted(set(socket.gethostbyname_ex(DOMAIN)[2])) if EXPECTED_IP not in addresses: raise RuntimeError('DNS does not point to this server: ' + json.dumps(addresses)) go = GoProject() project = go.get_project_find(PROJECT) if not project or not go.get_project_run_state(project_name=PROJECT): raise RuntimeError('BaoTa Go project is not running') if DOMAIN + ':80' not in project['project_config']['domains']: raise RuntimeError('Expected domain is not bound to the project') fetch('http://' + DOMAIN + '/healthz', '"status":"ok"') site_id = str(project['id']) args = request(id=site_id, auth_to=site_id, auth_type='http', auto_wildcard='0', domains=json.dumps([DOMAIN]), siteName=PROJECT, ca='letsencrypt') certificate = acme_v2().apply_cert_api(args) if not certificate.get('status'): raise RuntimeError('Certificate request failed: ' + str(certificate.get('msg'))) if 'private_key' not in certificate or 'cert' not in certificate: raise RuntimeError('Certificate authority returned an incomplete result') install = panelSite.panelSite().SetSSL(request( siteName=PROJECT, key=certificate['private_key'], csr=certificate['cert'] + certificate.get('root', ''), )) if not install.get('status'): raise RuntimeError('Certificate install failed: ' + str(install.get('msg'))) redirect = panelSite.panelSite().HttpToHttps(request(siteName=PROJECT)) if not redirect.get('status'): raise RuntimeError('HTTPS redirect enablement failed: ' + str(redirect.get('msg'))) command('/www/server/nginx/sbin/nginx', '-t') command('/etc/init.d/nginx', 'reload') fetch('https://' + DOMAIN + '/healthz', '"status":"ok"') fetch('https://' + DOMAIN + '/admin/', '星遇社交运营中心') connection = http.client.HTTPConnection(DOMAIN, 80, timeout=8) connection.request('GET', '/healthz') response = connection.getresponse() response.read() if response.status not in (301, 308): raise RuntimeError('HTTP is not redirected to HTTPS: ' + str(response.status)) if not (response.getheader('Location') or '').startswith('https://' + DOMAIN): raise RuntimeError('HTTP redirect target is incorrect') context = ssl.create_default_context() with socket.create_connection((DOMAIN, 443), timeout=8) as raw: with context.wrap_socket(raw, server_hostname=DOMAIN) as secure: peer = secure.getpeercert() protocol = secure.version() ssl_state = panelSite.panelSite().GetSSL(request(siteName=PROJECT)) if not ssl_state.get('status') or not ssl_state.get('httpTohttps'): raise RuntimeError('BaoTa does not report SSL and forced HTTPS as enabled') result = { 'status': 'HTTPS_DEPLOYMENT_OK', 'domain': DOMAIN, 'project': PROJECT, 'dns': addresses, 'tls_protocol': protocol, 'certificate_not_after': peer.get('notAfter'), 'http_redirect': response.status, 'nginx_config': str(GO_CONF), } print(json.dumps(result, ensure_ascii=False)) if __name__ == '__main__': main()