Allow users to run the scouting app with HTTPS/LDAP

I find myself needing to experiment with getting the username from the
LDAP login that happens in our HTTPS version of the scouting app. This
patch exposes a new `//scouting:https` target to let me do just that.

This patch also updates the README to let other folks know how to run
it under HTTPS/LDAP.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: Ib9f4b8626cb9adfe178ded2b43677d1dcd30da4f
diff --git a/scouting/BUILD b/scouting/BUILD
index 0c2c641..2a2d0ba 100644
--- a/scouting/BUILD
+++ b/scouting/BUILD
@@ -1,4 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+load("//tools/build_rules:apache.bzl", "apache_wrapper")
 load("//tools/build_rules:js.bzl", "protractor_ts_test", "turn_files_into_runfiles")
 
 go_binary(
@@ -44,3 +45,8 @@
     on_prepare = ":scouting_test.protractor.on-prepare.js",
     server = "//scouting/testing:scouting_test_servers",
 )
+
+apache_wrapper(
+    name = "https",
+    binary = ":scouting",
+)
diff --git a/scouting/README.md b/scouting/README.md
index 7789c8f..28fdc26 100644
--- a/scouting/README.md
+++ b/scouting/README.md
@@ -43,3 +43,24 @@
 where `1234` is the port that your instance of the webserver is using.
 `<build_server>` is the SSH Host entry in your `~/.ssh/config` file for the
 build server.
+
+You can then visit <http://localhost:1234/> to look at the webserver.
+
+
+Running the webserver with HTTPS
+--------------------------------------------------------------------------------
+You can test HTTPS and LDAP interation by running the webserver in a slightly
+different way.
+
+    $ bazel run //scouting:https -- --testdb_port 2345 --https_port 3456
+
+The `--testdb_port` value must match the port you selected when running the
+database.
+
+The `--https_port` value is the port at which the webserver is available via
+HTTPS. See the documentation in
+[`tools/build_rules/apache.bzl`](tools/build_rules/apache.bzl) for more
+information. The documentation tells you how to set up an `ldap.json`
+configuration.
+
+You can then visit <https://localhost:3456/> to look at the webserver.
diff --git a/scouting/webserver/main.go b/scouting/webserver/main.go
index a4d549a..8f4298b 100644
--- a/scouting/webserver/main.go
+++ b/scouting/webserver/main.go
@@ -9,6 +9,7 @@
 	"log"
 	"os"
 	"os/signal"
+	"strconv"
 	"syscall"
 	"time"
 
@@ -39,8 +40,23 @@
 	return &config, nil
 }
 
+// Gets the default port to use for the webserver. If wrapped by
+// apache_wrapper(), we use the port dictated by the wrapper.
+func getDefaultPort() int {
+	port_str := os.Getenv("APACHE_WRAPPED_PORT")
+	if port_str != "" {
+		port, err := strconv.Atoi(port_str)
+		if err != nil {
+			log.Fatalf("Failed to parse \"%s\" as integer: %v", port_str, err)
+		}
+		return port
+	}
+
+	return 8080
+}
+
 func main() {
-	portPtr := flag.Int("port", 8080, "The port number to bind to.")
+	portPtr := flag.Int("port", getDefaultPort(), "The port number to bind to.")
 	dirPtr := flag.String("directory", ".", "The directory to serve at /.")
 	dbConfigPtr := flag.String("db_config", "",
 		"The postgres database JSON config. It needs the following keys: "+
diff --git a/tools/build_rules/apache_runner.py b/tools/build_rules/apache_runner.py
index 3364216..3839cb0 100644
--- a/tools/build_rules/apache_runner.py
+++ b/tools/build_rules/apache_runner.py
@@ -16,9 +16,11 @@
 import os
 from pathlib import Path
 import signal
+import socket
 import subprocess
 import sys
 import tempfile
+import time
 
 import jinja2
 
@@ -32,6 +34,18 @@
 dummy@frc971.org
 """
 
+def wait_for_server(port: int):
+    """Waits for the server at the specified port to respond to TCP connections."""
+    while True:
+        try:
+            connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            connection.connect(("localhost", port))
+            connection.close()
+            break
+        except ConnectionRefusedError:
+            connection.close()
+            time.sleep(0.01)
+
 def main(argv):
   parser = argparse.ArgumentParser()
   parser.add_argument("--binary", type=str, required=True)
@@ -43,7 +57,7 @@
     help="JSON file containing 'ldap_bind_dn', 'ldap_url', and 'ldap_password' entries.",
     default="",
   )
-  args = parser.parse_args(argv[1:])
+  args, unknown_args = parser.parse_known_args(argv[1:])
 
   if not args.ldap_info:
     args.ldap_info = os.path.join(os.environ["BUILD_WORKSPACE_DIRECTORY"], "ldap.json")
@@ -102,20 +116,32 @@
     # 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)
+    wrapped_binary = subprocess.Popen([args.binary] + unknown_args, 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()
+    apache = subprocess.Popen(
+      ["external/apache2/usr/sbin/apache2", "-X", "-d", str(temp_dir)],
+      env=env,
+    )
+
+    wait_for_server(args.https_port)
+    wait_for_server(args.wrapped_port)
+    # Sleep to attempt to get the HTTPS message after the webserver message.
+    time.sleep(1)
+    print(f"Serving HTTPS on port {args.https_port}")
+
+    # Wait until we see a request to shut down.
+    signal.signal(signal.SIGINT, lambda signum, frame: None)
+    signal.signal(signal.SIGTERM, lambda signum, frame: None)
+    signal.pause()
+
+    print("\nShutting down apache and wrapped binary.")
+    apache.terminate()
+    wrapped_binary.terminate()
+    apache.wait()
+    wrapped_binary.wait()
 
 if __name__ == "__main__":
   sys.exit(main(sys.argv))