proxy.go

74 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
package internal

import (
	"cmp"
	"context"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"os"

	"golang.org/x/crypto/acme/autocert"

	"congo.gg/dev/models"
)

// ProxyMiddleware returns middleware that routes requests by Host header.
// Non-system domains matching a Domain record are reverse-proxied to
// the service container via the Docker network (container name resolution).
// System domains and unmatched hosts fall through to the app.
func ProxyMiddleware() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			host := stripPort(r.Host)

			domain, _ := models.Domains.First("WHERE Host = ? AND Active = 1 AND System = 0", host)
			if domain == nil {
				next.ServeHTTP(w, r)
				return
			}

			// Route via Docker network using container name (like coder proxy)
			// Target is the container name, port is the container-internal port
			target := &url.URL{
				Scheme: "http",
				Host:   fmt.Sprintf("%s:%d", domain.Target, domain.Port),
			}
			proxy := httputil.NewSingleHostReverseProxy(target)
			proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
				log.Printf("proxy: %s -> %s: %v", host, target.Host, err)
				http.Error(w, "Service unavailable", http.StatusBadGateway)
			}
			proxy.ServeHTTP(w, r)
		})
	}
}

func stripPort(host string) string {
	if h, _, err := net.SplitHostPort(host); err == nil {
		return h
	}
	return host
}

// NewAutocertManager returns an autocert.Manager that issues TLS certs
// for active domains in the database.
func NewAutocertManager() *autocert.Manager {
	cacheDir := cmp.Or(os.Getenv("DATA_DIR"), "/mnt/data") + "/certs"
	os.MkdirAll(cacheDir, 0700)

	return &autocert.Manager{
		Prompt: autocert.AcceptTOS,
		Cache:  autocert.DirCache(cacheDir),
		HostPolicy: func(ctx context.Context, host string) error {
			domain, _ := models.Domains.First("WHERE Host = ? AND Active = 1", host)
			if domain == nil {
				return fmt.Errorf("unknown host: %s", host)
			}
			return nil
		},
	}
}