Philipp Schrader | d0e33a4 | 2022-01-22 21:55:15 -0800 | [diff] [blame] | 1 | """Starts up Apache to provide HTTPS + LDAP for another web server. |
| 2 | |
| 3 | This script is used by the apache_wrapper() rule as the main entrypoint for its |
| 4 | "executable". This script sets up a minimal Apache environment in a directory |
| 5 | in /tmp. |
| 6 | |
| 7 | Both Apache and the wrapped server binary are started by this script. The |
| 8 | wrapped server should bind to the port specified by the APACHE_WRAPPED_PORT |
| 9 | environment variable. |
| 10 | |
| 11 | See the documentation for apache_wrapper() for more information. |
| 12 | """ |
| 13 | |
| 14 | import argparse |
| 15 | import json |
| 16 | import os |
| 17 | from pathlib import Path |
| 18 | import signal |
Philipp Schrader | 4e661d6 | 2022-03-13 22:15:56 -0700 | [diff] [blame^] | 19 | import socket |
Philipp Schrader | d0e33a4 | 2022-01-22 21:55:15 -0800 | [diff] [blame] | 20 | import subprocess |
| 21 | import sys |
| 22 | import tempfile |
Philipp Schrader | 4e661d6 | 2022-03-13 22:15:56 -0700 | [diff] [blame^] | 23 | import time |
Philipp Schrader | d0e33a4 | 2022-01-22 21:55:15 -0800 | [diff] [blame] | 24 | |
| 25 | import jinja2 |
| 26 | |
| 27 | DUMMY_CERT_ANSWERS = """\ |
| 28 | US |
| 29 | California |
| 30 | Mountain View |
| 31 | FRC971 |
| 32 | Software |
| 33 | frc971.org |
| 34 | dummy@frc971.org |
| 35 | """ |
| 36 | |
Philipp Schrader | 4e661d6 | 2022-03-13 22:15:56 -0700 | [diff] [blame^] | 37 | def wait_for_server(port: int): |
| 38 | """Waits for the server at the specified port to respond to TCP connections.""" |
| 39 | while True: |
| 40 | try: |
| 41 | connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 42 | connection.connect(("localhost", port)) |
| 43 | connection.close() |
| 44 | break |
| 45 | except ConnectionRefusedError: |
| 46 | connection.close() |
| 47 | time.sleep(0.01) |
| 48 | |
Philipp Schrader | d0e33a4 | 2022-01-22 21:55:15 -0800 | [diff] [blame] | 49 | def main(argv): |
| 50 | parser = argparse.ArgumentParser() |
| 51 | parser.add_argument("--binary", type=str, required=True) |
| 52 | parser.add_argument("--https_port", type=int, default=7000) |
| 53 | parser.add_argument("--wrapped_port", type=int, default=7500) |
| 54 | parser.add_argument( |
| 55 | "--ldap_info", |
| 56 | type=str, |
| 57 | help="JSON file containing 'ldap_bind_dn', 'ldap_url', and 'ldap_password' entries.", |
| 58 | default="", |
| 59 | ) |
Philipp Schrader | 4e661d6 | 2022-03-13 22:15:56 -0700 | [diff] [blame^] | 60 | args, unknown_args = parser.parse_known_args(argv[1:]) |
Philipp Schrader | d0e33a4 | 2022-01-22 21:55:15 -0800 | [diff] [blame] | 61 | |
| 62 | if not args.ldap_info: |
| 63 | args.ldap_info = os.path.join(os.environ["BUILD_WORKSPACE_DIRECTORY"], "ldap.json") |
| 64 | |
| 65 | with open("tools/build_rules/apache_template.conf", "r") as file: |
| 66 | template = jinja2.Template(file.read()) |
| 67 | |
| 68 | with open(args.ldap_info, "r") as file: |
| 69 | substitutions = json.load(file) |
| 70 | |
| 71 | for key in ("ldap_bind_dn", "ldap_url", "ldap_password"): |
| 72 | if key not in substitutions: |
| 73 | raise KeyError(f"The ldap_info JSON file must contain key '{key}'.") |
| 74 | |
| 75 | substitutions.update({ |
| 76 | "https_port": args.https_port, |
| 77 | "wrapped_port": args.wrapped_port, |
| 78 | }) |
| 79 | |
| 80 | config_text = template.render(substitutions) |
| 81 | |
| 82 | with tempfile.TemporaryDirectory() as temp_dir: |
| 83 | temp_dir = Path(temp_dir) |
| 84 | with open(temp_dir / "apache2.conf", "w") as file: |
| 85 | file.write(config_text) |
| 86 | |
| 87 | # Create a directory for error logs and such. |
| 88 | logs_dir = temp_dir / "logs" |
| 89 | os.mkdir(logs_dir) |
| 90 | |
| 91 | print("-" * 60) |
| 92 | print(f"Logs are in {logs_dir}/") |
| 93 | print("-" * 60) |
| 94 | |
| 95 | # Make modules available. |
| 96 | modules_path = Path("external/apache2/usr/lib/apache2/modules") |
| 97 | os.symlink(modules_path.resolve(), temp_dir / "modules") |
| 98 | |
| 99 | # Generate a testing cert. |
| 100 | subprocess.run([ |
| 101 | "openssl", |
| 102 | "req", |
| 103 | "-x509", |
| 104 | "-nodes", |
| 105 | "-days=365", |
| 106 | "-newkey=rsa:2048", |
| 107 | "-keyout=" + str(temp_dir / "apache-selfsigned.key"), |
| 108 | "-out=" + str(temp_dir / "apache-selfsigned.crt"), |
| 109 | ], |
| 110 | check=True, |
| 111 | input=DUMMY_CERT_ANSWERS, |
| 112 | text=True, |
| 113 | ) |
| 114 | |
| 115 | # Start the wrapped binary in the background. |
| 116 | # Tell it via the environment what port to listen on. |
| 117 | env = os.environ.copy() |
| 118 | env["APACHE_WRAPPED_PORT"] = str(args.wrapped_port) |
Philipp Schrader | 4e661d6 | 2022-03-13 22:15:56 -0700 | [diff] [blame^] | 119 | wrapped_binary = subprocess.Popen([args.binary] + unknown_args, env=env) |
Philipp Schrader | d0e33a4 | 2022-01-22 21:55:15 -0800 | [diff] [blame] | 120 | |
| 121 | # Start the apache server. |
| 122 | env = os.environ.copy() |
| 123 | env["LD_LIBRARY_PATH"] = "external/apache2/usr/lib/x86_64-linux-gnu" |
Philipp Schrader | 4e661d6 | 2022-03-13 22:15:56 -0700 | [diff] [blame^] | 124 | apache = subprocess.Popen( |
| 125 | ["external/apache2/usr/sbin/apache2", "-X", "-d", str(temp_dir)], |
| 126 | env=env, |
| 127 | ) |
| 128 | |
| 129 | wait_for_server(args.https_port) |
| 130 | wait_for_server(args.wrapped_port) |
| 131 | # Sleep to attempt to get the HTTPS message after the webserver message. |
| 132 | time.sleep(1) |
| 133 | print(f"Serving HTTPS on port {args.https_port}") |
| 134 | |
| 135 | # Wait until we see a request to shut down. |
| 136 | signal.signal(signal.SIGINT, lambda signum, frame: None) |
| 137 | signal.signal(signal.SIGTERM, lambda signum, frame: None) |
| 138 | signal.pause() |
| 139 | |
| 140 | print("\nShutting down apache and wrapped binary.") |
| 141 | apache.terminate() |
| 142 | wrapped_binary.terminate() |
| 143 | apache.wait() |
| 144 | wrapped_binary.wait() |
Philipp Schrader | d0e33a4 | 2022-01-22 21:55:15 -0800 | [diff] [blame] | 145 | |
| 146 | if __name__ == "__main__": |
| 147 | sys.exit(main(sys.argv)) |