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/.gitignore b/.gitignore
index 82b963a..8c6aa64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,10 @@
 /bazel-*
 /compile_commands.json
 
+# The `tools/build_rules/apache_runner.py` helper looks for this file by
+# default. We don't want folks to check it in.
+/ldap.json
+
 # Hide vagrant's files that unfortunately make it into the source tree when you
 # run "vagrant up".
 /vm/.vagrant/
diff --git a/WORKSPACE b/WORKSPACE
index c15c957..4aaad5b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -7,6 +7,10 @@
     python_debs = "files",
 )
 load(
+    "//debian:apache2.bzl",
+    apache2_debs = "files",
+)
+load(
     "//debian:patch.bzl",
     patch_debs = "files",
 )
@@ -82,6 +86,8 @@
 
 generate_repositories_for_debs(ssh_debs)
 
+generate_repositories_for_debs(apache2_debs)
+
 generate_repositories_for_debs(patch_debs)
 
 generate_repositories_for_debs(pandoc_debs)
@@ -490,6 +496,13 @@
 )
 
 http_archive(
+    name = "apache2",
+    build_file = "@//debian:apache2.BUILD",
+    sha256 = "98b0ad6d911751ba0aa486429e6278f995e7bbabd928c7d3d44c888fa2bf371b",
+    url = "https://www.frc971.org/Build-Dependencies/apache2.tar.gz",
+)
+
+http_archive(
     name = "pandoc",
     build_file = "@//debian:pandoc.BUILD",
     sha256 = "9f7a7adb3974a1f14715054c349ff3edc2909e920dbe3438fca437a83845f3c4",
diff --git a/build_tests/BUILD b/build_tests/BUILD
index c74407e..ca34de2 100644
--- a/build_tests/BUILD
+++ b/build_tests/BUILD
@@ -1,6 +1,7 @@
 load("@com_google_protobuf//:protobuf.bzl", "cc_proto_library")
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_py_library")
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+load("//tools/build_rules:apache.bzl", "apache_wrapper")
 
 cc_test(
     name = "gflags_build_test",
@@ -113,3 +114,15 @@
     visibility = ["//visibility:private"],
     deps = ["//build_tests/go_greeter"],
 )
+
+py_binary(
+    name = "dummy_http_server",
+    srcs = ["dummy_http_server.py"],
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+)
+
+apache_wrapper(
+    name = "apache_https_demo",
+    binary = ":dummy_http_server",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+)
diff --git a/build_tests/dummy_http_server.py b/build_tests/dummy_http_server.py
new file mode 100644
index 0000000..f65122a
--- /dev/null
+++ b/build_tests/dummy_http_server.py
@@ -0,0 +1,48 @@
+"""This is a dummy server to demonstrate the apache_wrapper() rule.
+
+Don't run this server on its own. Instead run the
+`//build_tests:apache_https_demo` binary.
+
+When you authenticate against Apache via LDAP, this server prints the username
+you authenticated with and the path from the GET request.
+"""
+
+import base64
+from http.server import BaseHTTPRequestHandler, HTTPServer
+import os
+
+def parse_auth(authorization):
+    auth_type, auth_info = authorization.split(" ", maxsplit=1)
+    if auth_type != "Basic":
+        raise ValueError(f"Unknown auth type: {auth_type}")
+    username, _ = base64.b64decode(auth_info).decode().split(":", 1)
+    return username
+
+class Server(BaseHTTPRequestHandler):
+    def _set_response(self):
+        self.send_response(200)
+        self.send_header("Content-type", "text/html")
+        self.end_headers()
+
+    def _write(self, text):
+        self.wfile.write(text.encode("utf-8"))
+
+    def do_GET(self):
+        self._set_response()
+        self._write(f"GET request for {self.path}")
+        self._write("<br/>")
+        username = parse_auth(self.headers["Authorization"])
+        self._write(f"Hello, {username}")
+
+def main():
+    port = int(os.environ["APACHE_WRAPPED_PORT"])
+    server_address = ("", port)
+    httpd = HTTPServer(server_address, Server)
+    try:
+        httpd.serve_forever()
+    except KeyboardInterrupt:
+        pass
+    httpd.server_close()
+
+if __name__ == "__main__":
+    main()
diff --git a/debian/BUILD b/debian/BUILD
index 0540a28..76bcf28 100644
--- a/debian/BUILD
+++ b/debian/BUILD
@@ -3,6 +3,10 @@
     python_debs = "files",
 )
 load(
+    ":apache2.bzl",
+    apache2_debs = "files",
+)
+load(
     ":patch.bzl",
     patch_debs = "files",
 )
@@ -112,6 +116,26 @@
 )
 
 download_packages(
+    name = "download_apache2_packages",
+    excludes = [
+        "libaprutil1-dbd-mysql",
+        "libaprutil1-dbd-odbc",
+        "libaprutil1-dbd-pgsql",
+        "libaprutil1-dbd-freetds",
+        "libstdc++6",
+        "lsb-base",
+        "debconf",
+        "libc6-dev",
+    ],
+    force_includes = [
+        "libaprutil1",
+    ],
+    packages = [
+        "apache2",
+    ],
+)
+
+download_packages(
     name = "download_python_deps",
     excludes = [
         "libblas.so.3",
@@ -253,6 +277,12 @@
 )
 
 generate_deb_tarball(
+    name = "apache2",
+    files = apache2_debs,
+    target_compatible_with = ["@platforms//os:linux"],
+)
+
+generate_deb_tarball(
     name = "patch",
     files = patch_debs,
     target_compatible_with = ["@platforms//os:linux"],
diff --git a/debian/apache2.BUILD b/debian/apache2.BUILD
new file mode 100644
index 0000000..7f3f87c
--- /dev/null
+++ b/debian/apache2.BUILD
@@ -0,0 +1,5 @@
+filegroup(
+    name = "all_files",
+    srcs = glob(["**"]),
+    visibility = ["//visibility:public"],
+)
diff --git a/debian/apache2.bzl b/debian/apache2.bzl
new file mode 100644
index 0000000..3d557c9
--- /dev/null
+++ b/debian/apache2.bzl
@@ -0,0 +1,59 @@
+files = {
+    "apache2-bin_2.4.52-1~deb11u2_amd64.deb": "dc0b906cb78fb55238bc1ddda5dac6917cbf1027a4372944e24a1a16aa8c9fdc",
+    "apache2-data_2.4.52-1~deb11u2_all.deb": "8fac5b05668e3d01e97f4ec2e8c52d9b138da0e2109425d6eacbcd005651068e",
+    "apache2-utils_2.4.52-1~deb11u2_amd64.deb": "a164cb0109bfbadb932cbd0a7cf8eeb1b4ed1242bc6e44394f5a343589ddca39",
+    "apache2_2.4.52-1~deb11u2_amd64.deb": "00bd5ca827ea67e0d2a10e6191595b26261f902c66096bd9e2b80fe6b606945a",
+    "debconf_1.5.77_all.deb": "d9ee4dff77aaad12674eed3ccefdcccd332424c9e2ac2ac00a37a1e06c84ab70",
+    "dpkg_1.20.9_amd64.deb": "ac90a4705b36f2d952a4a745ad944b33ff60c5ec69a5f89e4559a89bd0c53e01",
+    "gcc-10-base_10.2.1-6_amd64.deb": "be65535e94f95fbf04b104e8ab36790476f063374430f7dfc6c516cbe2d2cd1e",
+    "init-system-helpers_1.60_all.deb": "43420922c5e3aa747f8854236bf381a35179bba3885b242edb104751dad20644",
+    "libacl1_2.2.53-10_amd64.deb": "aa18d721be8aea50fbdb32cd9a319cb18a3f111ea6ad17399aa4ba9324c8e26a",
+    "libapr1_1.7.0-6+deb11u1_amd64.deb": "b95ce92c2530ba9c3a9af906f6ad27a0894efbefe9f0a2d9f2f91c6bdcdaf874",
+    "libaprutil1-dbd-sqlite3_1.6.1-5_amd64.deb": "18778f0cfc866d6230ab4f14ec3248d214e0f2a6efec5ded409aa6709d2d9255",
+    "libaprutil1-ldap_1.6.1-5_amd64.deb": "902a329ef05b94380ba9f7e413bd53086e096dc20cb0a913a4f346febc1e69e5",
+    "libaprutil1_1.6.1-5_amd64.deb": "d6aa77a54ed7533ea5c00240a677009f1b5d4e85f1607980d51ba35768c2eb39",
+    "libbrotli1_1.0.9-2+b2_amd64.deb": "65ca7d8b03e9dac09c5d544a89dd52d1aeb74f6a19583d32e4ff5f0c77624c24",
+    "libbz2-1.0_1.0.8-4_amd64.deb": "16e27c3ebd97981e70db3733f899963362748f178a62644df69d1f247e741379",
+    "libc6_2.31-13+deb11u2_amd64.deb": "3d9421c3fc0ef0d8ce57c0a149e1f8dbad78aba067f120be9e652af28902e346",
+    "libcrypt1_4.4.18-4_amd64.deb": "f617952df0c57b4ee039448e3941bccd3f97bfff71e9b0f87ca6dae15cb3f5ef",
+    "libcurl4_7.74.0-1.3+deb11u1_amd64.deb": "6f9c494eecc920899bb2c72d1a507a34b3703105778b0b9b9ae9aebdbdffcaab",
+    "libdb5.3_5.3.28+dfsg1-0.8_amd64.deb": "00b9e63e287f45300d4a4f59b6b88e25918443c932ae3e5845d5761ae193c530",
+    "libexpat1_2.2.10-2_amd64.deb": "eda6663f34375a9456c8c701002f1271bc90ac2627b9fb0892474e65eae1b668",
+    "libgcc-s1_10.2.1-6_amd64.deb": "e478f2709d8474165bb664de42e16950c391f30eaa55bc9b3573281d83a29daf",
+    "libgcrypt20_1.8.7-6_amd64.deb": "7a2e0eef8e0c37f03f3a5fcf7102a2e3dc70ba987f696ab71949f9abf36f35ef",
+    "libgdbm-compat4_1.19-2_amd64.deb": "e62caed68b0ffaa03b5fa539d6fdc08c4151f66236d5878949bead0b71b7bb09",
+    "libgdbm6_1.19-2_amd64.deb": "e54cfe4d8b8f209bb7df31a404ce040f7c2f9b1045114a927a7e1061cdf90727",
+    "libgpg-error0_1.38-2_amd64.deb": "16a507fb20cc58b5a524a0dc254a9cb1df02e1ce758a2d8abde0bc4a3c9b7c26",
+    "libicu67_67.1-7_amd64.deb": "2bf5c46254f527865bfd6368e1120908755fa57d83634bd7d316c9b3cfd57303",
+    "libjansson4_2.13.1-1.1_amd64.deb": "7025d03e4ad4177a06692bd8e6edcbdebf7fd3897e5f29b70ae968e17e57f6fa",
+    "liblua5.3-0_5.3.3-1.1+b1_amd64.deb": "98d5a85075a936ff014f639552882914064e46bfbbdc4bc80e80db33a9c77602",
+    "liblz4-1_1.9.3-2_amd64.deb": "79ac6e9ca19c483f2e8effcc3401d723dd9dbb3a4ae324714de802adb21a8117",
+    "liblzma5_5.2.5-2_amd64.deb": "eed58eb6b62208685dbe42a05582e78bd48c8d879d7ae0b61f114265d18824db",
+    "libncurses6_6.2+20201114-2_amd64.deb": "dfe45cb6ab048d1182175df55b007a4a188515c6d764a4dd5a44a0b47b6286a1",
+    "libncursesw6_6.2+20201114-2_amd64.deb": "ee3cd315dfa18865cf888ba6813a552077a4f3d1439dd225e4a0d0fee53aadc2",
+    "libnghttp2-14_1.43.0-1_amd64.deb": "a1a8aae24ced43025c94a9cb0c0eabfb3fc070785de9ee51c9a3a4fe86f0d11e",
+    "libpcre2-8-0_10.36-2_amd64.deb": "d31e4d6c04e847194b36b13ab59c578775aa12f04c48a840922796bd1f5eb32a",
+    "libpcre3_8.39-13_amd64.deb": "48efcf2348967c211cd9408539edf7ec3fa9d800b33041f6511ccaecc1ffa9d0",
+    "libperl5.32_5.32.1-4+deb11u2_amd64.deb": "224cafe65968deb83168113b74dff2d2f13b115a41d99eb209ed3b8f981df0b3",
+    "libprocps8_3.3.17-5_amd64.deb": "0a60017f0229cd4eec95b9f354c68312cc4ca4770ba8c01f545fd9c02b34e8a0",
+    "libpsl5_0.21.0-1.2_amd64.deb": "d716f5b4346ec85bb728f4530abeb1da4a79f696c72d7f774c59ba127c202fa7",
+    "librtmp1_2.4+20151223.gitfa8646d.1-2+b2_amd64.deb": "e1f69020dc2c466e421ec6a58406b643be8b5c382abf0f8989011c1d3df91c87",
+    "libselinux1_3.1-3_amd64.deb": "339f5ede10500c16dd7192d73169c31c4b27ab12130347275f23044ec8c7d897",
+    "libsqlite3-0_3.34.1-3_amd64.deb": "a0b8d3acf4a0483048637637d269be93af48d5c16f6f139f53edd13384ad4686",
+    "libssh2-1_1.9.0-2_amd64.deb": "f730fe45716a206003597819ececeeffe0fff754bdbbd0105425a177aa20a2de",
+    "libssl1.1_1.1.1k-1+deb11u1_amd64.deb": "82e6ded36e4fa4c28dcec00369b36ee242975f4c110f755a970a56d03d410ffb",
+    "libsystemd0_247.3-6_amd64.deb": "8c948d9d97178e6617f549822db2b89e23b1bfa1ee745ffbf0e41b6ee64f8737",
+    "libtinfo6_6.2+20201114-2_amd64.deb": "aeaf942c71ecc0ed081efdead1a1de304dcd513a9fc06791f26992e76986597b",
+    "libuuid1_2.36.1-8_amd64.deb": "94f13f58ac45ae850559e6bfe1a02be72566c66761e628a2599cc85066cb84d3",
+    "libxml2_2.9.10+dfsg-6.7_amd64.deb": "023296a15e1a28607609cb15c7ca0dd8a25160f3e89a0da58368319c7e17d4e0",
+    "libzstd1_1.4.8+dfsg-2.1_amd64.deb": "5dcadfbb743bfa1c1c773bff91c018f835e8e8c821d423d3836f3ab84773507b",
+    "mailcap_3.69_all.deb": "63fa5520f05d2aea5ca23eee95981a5e029608e1186ded4143470c8f84184158",
+    "media-types_4.0.0_all.deb": "f9835dcf3cdbaf163104d4e511c9c4e0f41a56822e147e57f28f749fcbf7d44c",
+    "mime-support_3.66_all.deb": "b964e671e6c47674879a3e54130b6737e8760fbd3da6afcc015faa174af98ba0",
+    "perl-base_5.32.1-4+deb11u2_amd64.deb": "018a3e48e58cbc478d3a4365090fb1daa151769f90f9b45984ec9d056ef96adc",
+    "perl-modules-5.32_5.32.1-4+deb11u2_all.deb": "6fa15be322c3c89ec4a07d704ad58d4a2d1aabf866135a859f6d8d58c59e9df4",
+    "perl_5.32.1-4+deb11u2_amd64.deb": "1cebc4516ed7c240b812c7bdd7e6ea0810f513152717ca17ce139ee0dfbc7b0d",
+    "procps_3.3.17-5_amd64.deb": "ac8edf0517abe09637c36651cb6a59e10948b2879f3af9003b9145b2128a7a08",
+    "tar_1.34+dfsg-1_amd64.deb": "bd8e963c6edcf1c806df97cd73560794c347aa94b9aaaf3b88eea585bb2d2f3c",
+    "zlib1g_1.2.11.dfsg-2_amd64.deb": "af36bf9249db4372a014a21ff2b84cc5a6808e3c878feda652c78e871d5964ad",
+}
diff --git a/debian/download_packages.py b/debian/download_packages.py
index e8caebd..be43120 100755
--- a/debian/download_packages.py
+++ b/debian/download_packages.py
@@ -92,6 +92,9 @@
     if package == b'libblas.so.3':
       yield b'libblas3'
       continue
+    if package == b'debconf-2.0':
+      yield b'debconf'
+      continue
     yield package
 
 def download_deps(apt_args, packages, excludes, force_includes):
diff --git a/tools/build_rules/BUILD b/tools/build_rules/BUILD
index cf27ca0..aeb8517 100644
--- a/tools/build_rules/BUILD
+++ b/tools/build_rules/BUILD
@@ -12,3 +12,15 @@
     visibility = ["//visibility:public"],
     deps = ["@python_jinja2"],
 )
+
+py_binary(
+    name = "apache_runner",
+    srcs = ["apache_runner.py"],
+    data = [
+        "apache_template.conf",
+        "@apache2//:all_files",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = ["@python_jinja2"],
+)
diff --git a/tools/build_rules/apache.bzl b/tools/build_rules/apache.bzl
new file mode 100644
index 0000000..9fb2ef5
--- /dev/null
+++ b/tools/build_rules/apache.bzl
@@ -0,0 +1,82 @@
+def _apache_binary_impl(ctx):
+    binary_path = ctx.attr.binary.files_to_run.executable.short_path
+
+    out = ctx.actions.declare_file(ctx.label.name)
+    ctx.actions.write(out, """\
+#!/bin/bash
+
+exec ./tools/build_rules/apache_runner --binary "{}" "$@"
+""".format(binary_path), is_executable = True)
+
+    # Collect files and runfiles for the tools that we're wrapping.
+    files = depset(transitive = [
+        ctx.attr._apache_runner.files,
+        ctx.attr.binary.files,
+    ])
+
+    runfiles = ctx.attr._apache_runner.default_runfiles
+    runfiles = runfiles.merge(ctx.attr.binary.default_runfiles)
+
+    return [
+        DefaultInfo(
+            executable = out,
+            files = files,
+            runfiles = runfiles,
+        ),
+    ]
+
+apache_wrapper = rule(
+    implementation = _apache_binary_impl,
+    attrs = {
+        "binary": attr.label(
+            mandatory = True,
+            executable = True,
+            cfg = "target",
+            doc = "The binary that we're wrapping with LDAP+HTTPS.",
+        ),
+        "_apache_runner": attr.label(
+            default = "@//tools/build_rules:apache_runner",
+            executable = True,
+            cfg = "target",
+        ),
+    },
+    doc = """\
+This rule wraps another web server and provides LDAP and HTTPS support.
+
+It's not intended to be used in production. It's intended to provide team
+members with a way to test their code in a production-like environment. E.g. to
+test whether your server makes use of LDAP credentials correctly.
+
+Write this as a wrapper around another binary like so:
+
+    apache_wrapper(
+        name = "wrapped_server",
+        binary = "//path/to:server_binary",
+    )
+
+Then you can run Apache and the wrapped binary like so:
+
+    $ bazel run :wrapped_server
+
+The wrapped binary can find the port that Apache is wrapping via the
+APACHE_WRAPPED_PORT environment variable.
+
+This rule assumes that you have a file at the root of the workspace called
+"ldap.json". You can customize this path with the `--ldap_info` argument. The
+JSON file has to have these three entries in it:
+
+    {
+        "ldap_bind_dn": "...",
+        "ldap_url": "...",
+        "ldap_password": "..."
+    }
+
+where the "..." values are replaced with the information to connect to an LDAP
+server. If you want to connect to our FRC971 LDAP server, please contact a
+Software mentor. Or ask on the `#coding` Slack channel.
+
+If the default ports of 7000 and 7500 are already taken, you can change them via
+the `--https_port` and `--wrapped_port` arguments.
+""",
+    executable = True,
+)
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))
diff --git a/tools/build_rules/apache_template.conf b/tools/build_rules/apache_template.conf
new file mode 100644
index 0000000..91a8338
--- /dev/null
+++ b/tools/build_rules/apache_template.conf
@@ -0,0 +1,57 @@
+PidFile                 logs/httpd.pid
+
+ServerTokens            Prod
+UseCanonicalName        On
+TraceEnable             Off
+
+Listen                  127.0.0.1:{{ https_port }}
+
+LoadModule              mpm_event_module        modules/mod_mpm_event.so
+LoadModule              authn_core_module       modules/mod_authn_core.so
+LoadModule              authz_core_module       modules/mod_authz_core.so
+LoadModule              authz_user_module       modules/mod_authz_user.so
+LoadModule              auth_basic_module       modules/mod_auth_basic.so
+LoadModule              authnz_ldap_module      modules/mod_authnz_ldap.so
+LoadModule              ldap_module             modules/mod_ldap.so
+LoadModule              proxy_module            modules/mod_proxy.so
+LoadModule              proxy_http_module       modules/mod_proxy_http.so
+LoadModule              ssl_module              modules/mod_ssl.so
+
+{% raw %}
+ErrorLogFormat          "[%{cu}t] [%-m:%-l] %-a %-L %M"
+LogFormat               "%h %l %u [%{%Y-%m-%d %H:%M:%S}t.%{usec_frac}t] \"%r\" %>s %b \
+\"%{Referer}i\" \"%{User-Agent}i\"" combined
+{% endraw %}
+
+LogLevel                debug
+ErrorLog                logs/error.log
+CustomLog               logs/access.log combined
+
+LDAPCacheEntries 1024
+LDAPCacheTTL 600
+LDAPTrustedGlobalCert CA_BASE64 "apache-selfsigned.crt"
+LDAPTrustedMode STARTTLS
+LDAPLibraryDebug 7
+LDAPVerifyServerCert OFF
+
+<VirtualHost *:{{ https_port }}>
+    ServerName              localhost
+    ServerAdmin             root@localhost
+
+    SSLEngine on
+    SSLProxyEngine On
+    SSLCertificateFile apache-selfsigned.crt
+    SSLCertificateKeyFile apache-selfsigned.key
+
+    ProxyPass "/" http://localhost:{{ wrapped_port }}/
+
+    <Location />
+        AuthName "Enter your Robotics 971 credentials"
+        AuthType Basic
+        AuthBasicProvider ldap
+        AuthLDAPBindDN "{{ ldap_bind_dn }}"
+        AuthLDAPBindPassword {{ ldap_password }}
+        AuthLDAPURL "{{ ldap_url }}"
+        Require valid-user
+    </Location>
+</VirtualHost>