Import apache2 for testing

Create `971-Robot-Code/ldap.json`. It is a JSON file with the LDAP URL
and password.

    {
        "ldap_bind_dn": "...",
        "ldap_url": "...",
        "ldap_password": "..."
    }

Run like this:

    $ bazel run //build_tests:apache_https_demo

Then you can navigate to `https://localhost:7000`.

If the ports are taken, customize with:
* `--https_port`
* `--wrapped_port`

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I56432388b774932fc6cf295da52ac8bc9e974cd7
diff --git a/tools/build_rules/apache_runner.py b/tools/build_rules/apache_runner.py
new file mode 100644
index 0000000..3364216
--- /dev/null
+++ b/tools/build_rules/apache_runner.py
@@ -0,0 +1,121 @@
+"""Starts up Apache to provide HTTPS + LDAP for another web server.
+
+This script is used by the apache_wrapper() rule as the main entrypoint for its
+"executable". This script sets up a minimal Apache environment in a directory
+in /tmp.
+
+Both Apache and the wrapped server binary are started by this script. The
+wrapped server should bind to the port specified by the APACHE_WRAPPED_PORT
+environment variable.
+
+See the documentation for apache_wrapper() for more information.
+"""
+
+import argparse
+import json
+import os
+from pathlib import Path
+import signal
+import subprocess
+import sys
+import tempfile
+
+import jinja2
+
+DUMMY_CERT_ANSWERS = """\
+US
+California
+Mountain View
+FRC971
+Software
+frc971.org
+dummy@frc971.org
+"""
+
+def main(argv):
+  parser = argparse.ArgumentParser()
+  parser.add_argument("--binary", type=str, required=True)
+  parser.add_argument("--https_port", type=int, default=7000)
+  parser.add_argument("--wrapped_port", type=int, default=7500)
+  parser.add_argument(
+    "--ldap_info",
+    type=str,
+    help="JSON file containing 'ldap_bind_dn', 'ldap_url', and 'ldap_password' entries.",
+    default="",
+  )
+  args = parser.parse_args(argv[1:])
+
+  if not args.ldap_info:
+    args.ldap_info = os.path.join(os.environ["BUILD_WORKSPACE_DIRECTORY"], "ldap.json")
+
+  with open("tools/build_rules/apache_template.conf", "r") as file:
+    template = jinja2.Template(file.read())
+
+  with open(args.ldap_info, "r") as file:
+    substitutions = json.load(file)
+
+  for key in ("ldap_bind_dn", "ldap_url", "ldap_password"):
+    if key not in substitutions:
+      raise KeyError(f"The ldap_info JSON file must contain key '{key}'.")
+
+  substitutions.update({
+    "https_port": args.https_port,
+    "wrapped_port": args.wrapped_port,
+  })
+
+  config_text = template.render(substitutions)
+
+  with tempfile.TemporaryDirectory() as temp_dir:
+    temp_dir = Path(temp_dir)
+    with open(temp_dir / "apache2.conf", "w") as file:
+      file.write(config_text)
+
+    # Create a directory for error logs and such.
+    logs_dir = temp_dir / "logs"
+    os.mkdir(logs_dir)
+
+    print("-" * 60)
+    print(f"Logs are in {logs_dir}/")
+    print("-" * 60)
+
+    # Make modules available.
+    modules_path = Path("external/apache2/usr/lib/apache2/modules")
+    os.symlink(modules_path.resolve(), temp_dir / "modules")
+
+    # Generate a testing cert.
+    subprocess.run([
+        "openssl",
+        "req",
+        "-x509",
+        "-nodes",
+        "-days=365",
+        "-newkey=rsa:2048",
+        "-keyout=" + str(temp_dir / "apache-selfsigned.key"),
+        "-out="  + str(temp_dir / "apache-selfsigned.crt"),
+      ],
+      check=True,
+      input=DUMMY_CERT_ANSWERS,
+      text=True,
+    )
+
+    # Start the wrapped binary in the background.
+    # Tell it via the environment what port to listen on.
+    env = os.environ.copy()
+    env["APACHE_WRAPPED_PORT"] = str(args.wrapped_port)
+    wrapped_binary = subprocess.Popen([args.binary], env=env)
+
+    # Start the apache server.
+    env = os.environ.copy()
+    env["LD_LIBRARY_PATH"] = "external/apache2/usr/lib/x86_64-linux-gnu"
+    try:
+      subprocess.run(
+        ["external/apache2/usr/sbin/apache2", "-X", "-d", str(temp_dir)],
+        check=True,
+        env=env,
+      )
+    finally:
+      wrapped_binary.send_signal(signal.SIGINT)
+      wrapped_binary.wait()
+
+if __name__ == "__main__":
+  sys.exit(main(sys.argv))