From aa5ede6fdc9c2c6b36348223b3e265b6decd50c3 Mon Sep 17 00:00:00 2001
From: Garet Halliday <me@garet.holiday>
Date: Fri, 29 Sep 2023 17:42:15 -0500
Subject: [PATCH] pgbouncer almost working

---
 cmd/caddygat/pgbouncer.go                   | 68 +++++++++++++++++++++
 lib/gat/gatcaddyfile/ssl.go                 | 16 +++++
 lib/gat/handlers/pgbouncer/config.go        | 55 +++++++++++++++++
 lib/gat/handlers/pgbouncer/module.go        |  9 ++-
 lib/gat/ssl/servers/x509_key_pair/server.go | 47 ++++++++++++++
 lib/gat/standard/standard.go                |  1 +
 6 files changed, 193 insertions(+), 3 deletions(-)
 create mode 100644 cmd/caddygat/pgbouncer.go
 create mode 100644 lib/gat/ssl/servers/x509_key_pair/server.go

diff --git a/cmd/caddygat/pgbouncer.go b/cmd/caddygat/pgbouncer.go
new file mode 100644
index 00000000..e923db80
--- /dev/null
+++ b/cmd/caddygat/pgbouncer.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+	"errors"
+	"time"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	caddycmd "github.com/caddyserver/caddy/v2/cmd"
+
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/pgbouncer"
+	"gfx.cafe/gfx/pggat/lib/util/dur"
+)
+
+func init() {
+	caddycmd.RegisterCommand(caddycmd.Command{
+		Name:  "pgbouncer",
+		Usage: "<config file>",
+		Short: "Runs in pgbouncer compatibility mode",
+		Func: func(flags caddycmd.Flags) (int, error) {
+			return runPgBouncer(flags)
+		},
+	})
+}
+
+func runPgBouncer(flags caddycmd.Flags) (int, error) {
+	caddy.TrapSignals()
+
+	file := flags.Arg(0)
+	if file == "" {
+		return caddy.ExitCodeFailedStartup, errors.New("usage: pgbouncer <config file>")
+	}
+
+	config, err := pgbouncer.Load(file)
+	if err != nil {
+		return caddy.ExitCodeFailedStartup, err
+	}
+
+	var pggat gat.Config
+	pggat.StatLogPeriod = dur.Duration(time.Second)
+
+	var server gat.ServerConfig
+	server.Listen = config.Listen()
+	server.Routes = append(server.Routes, gat.RouteConfig{
+		Handle: caddyconfig.JSONModuleObject(
+			pgbouncer.Module{
+				Config: config,
+			},
+			"handler",
+			"pgbouncer",
+			nil,
+		),
+	})
+	pggat.Servers = append(pggat.Servers, server)
+
+	caddyConfig := caddy.Config{
+		AppsRaw: caddy.ModuleMap{
+			"pggat": caddyconfig.JSON(pggat, nil),
+		},
+	}
+
+	if err = caddy.Run(&caddyConfig); err != nil {
+		return caddy.ExitCodeFailedStartup, err
+	}
+
+	select {}
+}
diff --git a/lib/gat/gatcaddyfile/ssl.go b/lib/gat/gatcaddyfile/ssl.go
index 798d6bc9..a4ae1063 100644
--- a/lib/gat/gatcaddyfile/ssl.go
+++ b/lib/gat/gatcaddyfile/ssl.go
@@ -7,12 +7,28 @@ import (
 
 	"gfx.cafe/gfx/pggat/lib/gat/ssl/clients/insecure_skip_verify"
 	"gfx.cafe/gfx/pggat/lib/gat/ssl/servers/self_signed"
+	"gfx.cafe/gfx/pggat/lib/gat/ssl/servers/x509_key_pair"
 )
 
 func init() {
 	RegisterDirective(SSLServer, "self_signed", func(_ *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
 		return &self_signed.Server{}, nil
 	})
+	RegisterDirective(SSLServer, "x509_key_pair", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		var module x509_key_pair.Server
+
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		module.CertFile = d.Val()
+
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		module.KeyFile = d.Val()
+
+		return &module, nil
+	})
 
 	RegisterDirective(SSLClient, "insecure_skip_verify", func(_ *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
 		return &insecure_skip_verify.Client{}, nil
diff --git a/lib/gat/handlers/pgbouncer/config.go b/lib/gat/handlers/pgbouncer/config.go
index 1ce4b8ed..6f3b179a 100644
--- a/lib/gat/handlers/pgbouncer/config.go
+++ b/lib/gat/handlers/pgbouncer/config.go
@@ -1,7 +1,16 @@
 package pgbouncer
 
 import (
+	"encoding/json"
+	"net"
+	"strconv"
+	"strings"
+
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+
 	"gfx.cafe/gfx/pggat/lib/bouncer"
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/gat/ssl/servers/x509_key_pair"
 	"gfx.cafe/gfx/pggat/lib/util/encoding/ini"
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
@@ -239,3 +248,49 @@ func Load(config string) (Config, error) {
 	err = ini.Unmarshal(conf, &c)
 	return c, err
 }
+
+func (T Config) Listen() []gat.ListenerConfig {
+	var ssl json.RawMessage
+	if T.PgBouncer.ClientTLSCertFile != "" && T.PgBouncer.ClientTLSKeyFile != "" {
+		ssl = caddyconfig.JSONModuleObject(
+			x509_key_pair.Server{
+				CertFile: T.PgBouncer.ClientTLSCertFile,
+				KeyFile:  T.PgBouncer.ClientTLSKeyFile,
+			},
+			"provider",
+			"x509_key_pair",
+			nil,
+		)
+	}
+
+	var listeners []gat.ListenerConfig
+
+	if T.PgBouncer.ListenAddr != "" {
+		listenAddr := T.PgBouncer.ListenAddr
+		if listenAddr == "*" {
+			listenAddr = ""
+		}
+
+		listen := net.JoinHostPort(listenAddr, strconv.Itoa(T.PgBouncer.ListenPort))
+
+		listeners = append(listeners, gat.ListenerConfig{
+			Address: listen,
+		})
+	}
+
+	// listen on unix socket
+	dir := T.PgBouncer.UnixSocketDir
+	port := T.PgBouncer.ListenPort
+
+	if !strings.HasSuffix(dir, "/") {
+		dir = dir + "/"
+	}
+	dir = dir + ".s.PGSQL." + strconv.Itoa(port)
+
+	listeners = append(listeners, gat.ListenerConfig{
+		Address: dir,
+		SSL:     ssl,
+	})
+
+	return listeners
+}
diff --git a/lib/gat/handlers/pgbouncer/module.go b/lib/gat/handlers/pgbouncer/module.go
index 93a582c9..2792dd5a 100644
--- a/lib/gat/handlers/pgbouncer/module.go
+++ b/lib/gat/handlers/pgbouncer/module.go
@@ -60,9 +60,12 @@ func (*Module) CaddyModule() caddy.ModuleInfo {
 func (T *Module) Provision(ctx caddy.Context) error {
 	T.log = ctx.Logger()
 
-	var err error
-	T.Config, err = Load(T.ConfigFile)
-	return err
+	if T.ConfigFile != "" {
+		var err error
+		T.Config, err = Load(T.ConfigFile)
+		return err
+	}
+	return nil
 }
 
 func (T *Module) Cleanup() error {
diff --git a/lib/gat/ssl/servers/x509_key_pair/server.go b/lib/gat/ssl/servers/x509_key_pair/server.go
new file mode 100644
index 00000000..905e5d99
--- /dev/null
+++ b/lib/gat/ssl/servers/x509_key_pair/server.go
@@ -0,0 +1,47 @@
+package x509_key_pair
+
+import (
+	"crypto/tls"
+
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+type Server struct {
+	CertFile string `json:"cert_file"`
+	KeyFile  string `json:"key_file"`
+
+	tlsConfig *tls.Config
+}
+
+func (T *Server) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.ssl.servers.x509_key_pair",
+		New: func() caddy.Module {
+			return new(Server)
+		},
+	}
+}
+
+func (T *Server) Provision(ctx caddy.Context) error {
+	cert, err := tls.LoadX509KeyPair(T.CertFile, T.KeyFile)
+	if err != nil {
+		return err
+	}
+
+	T.tlsConfig = &tls.Config{
+		Certificates: []tls.Certificate{
+			cert,
+		},
+	}
+	return nil
+}
+
+func (T *Server) ServerTLSConfig() *tls.Config {
+	return T.tlsConfig
+}
+
+var _ gat.SSLServer = (*Server)(nil)
+var _ caddy.Module = (*Server)(nil)
+var _ caddy.Provisioner = (*Server)(nil)
diff --git a/lib/gat/standard/standard.go b/lib/gat/standard/standard.go
index bbe9dfe7..66e343f9 100644
--- a/lib/gat/standard/standard.go
+++ b/lib/gat/standard/standard.go
@@ -9,6 +9,7 @@ import (
 
 	// ssl servers
 	_ "gfx.cafe/gfx/pggat/lib/gat/ssl/servers/self_signed"
+	_ "gfx.cafe/gfx/pggat/lib/gat/ssl/servers/x509_key_pair"
 
 	// ssl clients
 	_ "gfx.cafe/gfx/pggat/lib/gat/ssl/clients/insecure_skip_verify"
-- 
GitLab