app/matrix: media repo proxy init

This implements media-repo-proxy, a lil' bit of Go to make our
infrastructure work with matrix-media-repo's concept of Host headers.

For some reason, MMR really wants Host: hackerspace.pl instead of Host:
matrix.hackerspace.pl. We'd fix that in their code, but with no tests
and with complex config reload logic it looks very daunting. We'd just
fix that in our Ingress, but that's not easy (no per-rule host
overrides).

So, we commit a tiny little itty bitty war crime and implement a piece
of Go code that serves as a rewriter for this.

This works, tested on boston:

    $ curl -H "Host: matrix.hackerspace.pl" 10.10.12.46:8080/_matrix/media/r0/download/hackerspace.pl/EwVBulPgCWDWNGMKjcOKGGbk | file -
    /dev/stdin: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 650x300, components 3

(this address is media-repo.matrix.svc.k0.hswaw.net)

But hey, at least it has tests.

Change-Id: Ib6af1988fe8e112c9f3a5577506b18b48d80af62
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1143
Reviewed-by: q3k <q3k@hackerspace.pl>
diff --git a/app/matrix/media-repo-proxy/main.go b/app/matrix/media-repo-proxy/main.go
new file mode 100644
index 0000000..920e89e
--- /dev/null
+++ b/app/matrix/media-repo-proxy/main.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"net/http"
+	"net/http/httputil"
+)
+
+var (
+	flagUpstream       string
+	flagUpstreamHost   string
+	flagDownstreamHost string
+	flagListen         string
+)
+
+func newProxy() http.Handler {
+	proxy := httputil.ReverseProxy{
+		Director: func(r *http.Request) {
+			r.URL.Scheme = "http"
+			r.URL.Host = flagUpstream
+			r.Host = flagUpstreamHost
+			// MMR reads this field and prioritizes it over the Host header.
+			r.Header.Set("X-Forwarded-Host", flagUpstreamHost)
+		},
+	}
+
+	acl := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		remote := r.RemoteAddr
+		sip := r.Header.Get("Hscloud-Nic-Source-IP")
+		sport := r.Header.Get("Hscloud-Nic-Source-Port")
+		if sip != "" && sport != "" {
+			remote = net.JoinHostPort(sip, sport)
+			r.Header.Set("X-Forwarded-For", remote)
+		}
+		log.Printf("%s %s %s", remote, r.Method, r.URL.Path)
+
+		// ... during federation requests, Host is foo.example.com:443, strip
+		// that out if that's the case. Ignore port number, we don't care about
+		// it.
+		host, _, err := net.SplitHostPort(r.Host)
+		if err != nil {
+			// Error can mean many things, but generally it means 'no port', or
+			// a very malformed host. Regardless, just default to the raw
+			// value, we explicitly check it against a required host value
+			// further down
+			host = r.Host
+		}
+
+		if host != flagDownstreamHost {
+			log.Printf("Invalid host requested %q, wanted %q", r.Host, flagDownstreamHost)
+			w.WriteHeader(http.StatusBadRequest)
+			fmt.Fprintf(w, "invalid host\n")
+			return
+		}
+		proxy.ServeHTTP(w, r)
+	})
+
+	return acl
+}
+
+func main() {
+	flag.StringVar(&flagUpstreamHost, "upstream_host", "hackerspace.pl", "Upstream Host header, as sent to upstream")
+	flag.StringVar(&flagUpstream, "upstream", "foo.bar.svc.cluster.local:8080", "Address and port to reach upstream")
+	flag.StringVar(&flagDownstreamHost, "downstream_host", "matrix.hackerspace.pl", "Downstream Host header, as requested by client traffic")
+	flag.StringVar(&flagListen, "listen", ":8080", "Address to listen at for downstream traffic")
+	flag.Parse()
+
+	log.Printf("Starting media-repo-proxy")
+
+	proxy := newProxy()
+
+	log.Printf("Listening on %s...", flagListen)
+	if err := http.ListenAndServe(flagListen, proxy); err != nil {
+		log.Printf("Listen failed: %v", err)
+	}
+}