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/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>