#!/usr/bin/env python3 import os import signal import sys import time from urllib.parse import urlsplit import subprocess # This is written in Python instead of Bash because I encountered a weird problem I have no idea to fix. # # In docker-wrapper.sh, we run each daemon in background with `&`, and # use `wait` to wait them to exit gracefully. # This works fine if the daemon is NOT a shell script. # If the daemon is a shell script, the signal IS NOT delivered to the shell script. # The process running docker-wrapper.sh will die, # leaving the shell script running with parent pid becoming pid=1. # # It is observed that if the daemon is written in a different runtime, such as Go or Python, # this problem does not recur. # Thus this is written in Python. def handle_signal(signum, _frame): print(f"certbot: got signal {signum}") sys.exit(0) def parse_domain(val): url = urlsplit(val) hostname = url.hostname if hostname is None: raise ValueError(val) return hostname def get_run_internal_from_env(): run_internal = 86400 run_internal_env_name = "AUTHGEAR_CERTBOT_RUN_INTERVAL" run_internal_env_val = os.getenv(run_internal_env_name) if run_internal_env_val is not None and run_internal_env_val != "": try: run_internal = int(run_internal_env_val) if run_internal < 0: raise ValueError() except ValueError: print(f"certbot: {run_internal_env_name} is invalid: {run_internal_env_val}", file=sys.stderr) sys.exit(1) return run_internal def get_certbot_use_staging_from_env(): certbot_use_staging = True if os.getenv("AUTHGEAR_CERTBOT_ENVIRONMENT") == "production": certbot_use_staging = False return certbot_use_staging def get_email_from_env(): email_env_name = "AUTHGEAR_ONCE_ADMIN_USER_EMAIL" email_env_val = os.getenv(email_env_name) if email_env_val is None: print(f"certbot: {email_env_name} is undefined", file=sys.stderr) sys.exit(1) return email_env_val def get_domains_from_env(): domains = [] env_vars = [ "AUTHGEAR_HTTP_ORIGIN_PORTAL", "AUTHGEAR_HTTP_ORIGIN_ACCOUNTS", "AUTHGEAR_HTTP_ORIGIN_PROJECT", ] for env_var in env_vars: val = os.getenv(env_var) if val is None: print(f"certbot: {env_var} is undefined", file=sys.stderr) sys.exit(1) try: domain = parse_domain(val) domains.append(domain) except ValueError: print(f"certbot: {env_var} is invalid: {val}") sys.exit(1) return domains def get_certbot_enabled_from_env(): enabled = os.getenv("AUTHGEAR_CERTBOT_ENABLED") == "true" return enabled def run_once(): enabled = get_certbot_enabled_from_env() if not enabled: print(f"certbot: disabled by env") return None domains = get_domains_from_env() certbot_use_staging = get_certbot_use_staging_from_env() email_env_val = get_email_from_env() cmd = [] cmd.append("certbot") cmd.append("--config-dir") cmd.append(os.environ["AUTHGEARONCE_CERTBOT_DATA_DIRECTORY"]) cmd.append("run") if certbot_use_staging: cmd.append("--staging") cmd.append("--non-interactive") cmd.append("--nginx") cmd.append("-m") cmd.append(email_env_val) cmd.append("--agree-tos") cmd.append("--no-eff-email") cmd.append("--expand") cmd.append("--keep-until-expiring") cmd.append("--renew-with-new-domains") for domain in domains: cmd.append("--domain") cmd.append(domain) completed_process = subprocess.run(cmd, stdin=None, stdout=None, stderr=None, shell=False, check=False) print(f"certbot: exited with {completed_process.returncode}") return completed_process def main(): # Run once and exit. if len(sys.argv) >= 2 and sys.argv[1] == "--block-on-getting-tls-certificates": completed_process = run_once() if completed_process is not None: if completed_process.returncode != 0: # NOTE(once): exiting with 10 is a public API between the once image and the once command. sys.exit(10) else: sys.exit(0) else: sys.exit(0) else: # SIGINT is usually generated by CTRL-C signal.signal(signal.SIGINT, handle_signal) # SIGTERM is generated by docker container stop. signal.signal(signal.SIGTERM, handle_signal) run_internal = get_run_internal_from_env() print(f"certbot: run_internal={run_internal}") while True: run_once() time.sleep(run_internal) if __name__ == "__main__": main()