settings: introduce settings YAML file to complement cmd arguments
This commit is contained in:
85
lib/settings/backend.go
Normal file
85
lib/settings/backend.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/git/go-away/utils"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
)
|
||||
|
||||
type Backend struct {
|
||||
// URL Target server backend path. Supports http/https/unix protocols.
|
||||
URL string `yaml:"url"`
|
||||
|
||||
// Host Override the Host header and TLS SNI with this value if specified
|
||||
Host string `yaml:"host"`
|
||||
|
||||
//ProxyProtocol uint8 `yaml:"proxy-protocol"`
|
||||
|
||||
// HTTP2Enabled Enable HTTP2 to backend
|
||||
HTTP2Enabled bool `yaml:"http2-enabled"`
|
||||
|
||||
// TLSSkipVerify Disable TLS certificate verification, if any
|
||||
TLSSkipVerify bool `yaml:"tls-skip-verify"`
|
||||
}
|
||||
|
||||
func (b Backend) Create() (*httputil.ReverseProxy, error) {
|
||||
proxy, err := utils.MakeReverseProxy(b.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport := proxy.Transport.(*http.Transport)
|
||||
|
||||
if b.HTTP2Enabled {
|
||||
transport.ForceAttemptHTTP2 = true
|
||||
}
|
||||
|
||||
if b.TLSSkipVerify {
|
||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
if b.Host != "" {
|
||||
transport.TLSClientConfig.ServerName = b.Host
|
||||
director := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
req.Host = b.Host
|
||||
director(req)
|
||||
}
|
||||
}
|
||||
|
||||
/*if b.ProxyProtocol > 0 {
|
||||
dialContext := transport.DialContext
|
||||
if dialContext == nil {
|
||||
dialContext = (&net.Dialer{}).DialContext
|
||||
}
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, err := dialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrPort := utils.GetRemoteAddress(ctx)
|
||||
if addrPort == nil {
|
||||
// pass as is
|
||||
hdr := proxyproto.HeaderProxyFromAddrs(b.ProxyProtocol, conn.LocalAddr(), conn.RemoteAddr())
|
||||
_, err = hdr.WriteTo(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// set proper headers!
|
||||
hdr := proxyproto.HeaderProxyFromAddrs(b.ProxyProtocol, net.TCPAddrFromAddrPort(*addrPort), conn.RemoteAddr())
|
||||
_, err = hdr.WriteTo(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
}*/
|
||||
|
||||
proxy.Transport = transport
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
169
lib/settings/bind.go
Normal file
169
lib/settings/bind.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/git/go-away/utils"
|
||||
"github.com/pires/go-proxyproto"
|
||||
"golang.org/x/crypto/acme"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Bind struct {
|
||||
Address string `yaml:"address"`
|
||||
Network string `yaml:"network"`
|
||||
SocketMode string `yaml:"socket-mode"`
|
||||
Proxy bool `yaml:"proxy"`
|
||||
|
||||
Passthrough bool `yaml:"passthrough"`
|
||||
|
||||
// TLSAcmeAutoCert URL to ACME directory, or letsencrypt
|
||||
TLSAcmeAutoCert string `yaml:"tls-acme-autocert"`
|
||||
|
||||
// TLSCertificate Alternate to TLSAcmeAutoCert
|
||||
TLSCertificate string `yaml:"tls-certificate"`
|
||||
// TLSPrivateKey Alternate to TLSAcmeAutoCert
|
||||
TLSPrivateKey string `yaml:"tls-key"`
|
||||
}
|
||||
|
||||
func (b *Bind) Listener() (net.Listener, string) {
|
||||
return setupListener(b.Network, b.Address, b.SocketMode, b.Proxy)
|
||||
}
|
||||
|
||||
func (b *Bind) Server(backends map[string]http.Handler, acmeCachePath string) (*http.Server, func(http.Handler), error) {
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
if b.TLSAcmeAutoCert != "" {
|
||||
switch b.TLSAcmeAutoCert {
|
||||
case "letsencrypt":
|
||||
b.TLSAcmeAutoCert = acme.LetsEncryptURL
|
||||
}
|
||||
|
||||
acmeManager := newACMEManager(b.TLSAcmeAutoCert, backends)
|
||||
if acmeCachePath != "" {
|
||||
err := os.MkdirAll(acmeCachePath, 0755)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create acme cache directory: %w", err)
|
||||
}
|
||||
acmeManager.Cache = autocert.DirCache(acmeCachePath)
|
||||
}
|
||||
slog.Warn(
|
||||
"acme-autocert enabled",
|
||||
"directory", b.TLSAcmeAutoCert,
|
||||
)
|
||||
tlsConfig = acmeManager.TLSConfig()
|
||||
} else if b.TLSCertificate != "" && b.TLSPrivateKey != "" {
|
||||
tlsConfig = &tls.Config{}
|
||||
var err error
|
||||
tlsConfig.Certificates = make([]tls.Certificate, 1)
|
||||
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(b.TLSCertificate, b.TLSPrivateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
slog.Warn(
|
||||
"TLS enabled",
|
||||
"certificate", b.TLSCertificate,
|
||||
)
|
||||
}
|
||||
|
||||
var serverHandler atomic.Pointer[http.Handler]
|
||||
server := utils.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if handler := serverHandler.Load(); handler == nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
|
||||
} else {
|
||||
(*handler).ServeHTTP(w, r)
|
||||
}
|
||||
}), tlsConfig)
|
||||
|
||||
swap := func(handler http.Handler) {
|
||||
serverHandler.Store(&handler)
|
||||
}
|
||||
|
||||
if b.Passthrough {
|
||||
// setup a passthrough handler temporarily
|
||||
swap(http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
backend := utils.SelectHTTPHandler(backends, r.Host)
|
||||
if backend == nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
|
||||
} else {
|
||||
backend.ServeHTTP(w, r)
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
return server, swap, nil
|
||||
|
||||
}
|
||||
|
||||
func setupListener(network, address, socketMode string, proxy bool) (net.Listener, string) {
|
||||
if network == "proxy" {
|
||||
network = "tcp"
|
||||
proxy = true
|
||||
}
|
||||
|
||||
formattedAddress := ""
|
||||
switch network {
|
||||
case "unix":
|
||||
formattedAddress = "unix:" + address
|
||||
case "tcp":
|
||||
formattedAddress = "http://localhost" + address
|
||||
default:
|
||||
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
|
||||
}
|
||||
|
||||
listener, err := net.Listen(network, address)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Errorf("failed to bind to %s: %w", formattedAddress, err))
|
||||
}
|
||||
|
||||
// additional permission handling for unix sockets
|
||||
if network == "unix" {
|
||||
mode, err := strconv.ParseUint(socketMode, 8, 0)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
log.Fatal(fmt.Errorf("could not parse socket mode %s: %w", socketMode, err))
|
||||
}
|
||||
|
||||
err = os.Chmod(address, os.FileMode(mode))
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
log.Fatal(fmt.Errorf("could not change socket mode: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if proxy {
|
||||
slog.Warn("listener PROXY enabled")
|
||||
formattedAddress += " +PROXY"
|
||||
listener = &proxyproto.Listener{
|
||||
Listener: listener,
|
||||
}
|
||||
}
|
||||
|
||||
return listener, formattedAddress
|
||||
}
|
||||
|
||||
func newACMEManager(clientDirectory string, backends map[string]http.Handler) *autocert.Manager {
|
||||
manager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostPolicy(func(ctx context.Context, host string) error {
|
||||
if utils.SelectHTTPHandler(backends, host) != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("acme/autocert: host %s not configured in backends", host)
|
||||
}),
|
||||
Client: &acme.Client{
|
||||
HTTPClient: http.DefaultClient,
|
||||
DirectoryURL: clientDirectory,
|
||||
},
|
||||
}
|
||||
return manager
|
||||
}
|
||||
51
lib/settings/settings.go
Normal file
51
lib/settings/settings.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package settings
|
||||
|
||||
import "maps"
|
||||
|
||||
type Settings struct {
|
||||
Bind Bind `json:"bind"`
|
||||
|
||||
Backends map[string]Backend `json:"backends"`
|
||||
|
||||
BindDebug string `json:"bind-debug"`
|
||||
BindMetrics string `json:"bind-metrics"`
|
||||
|
||||
Strings Strings `yaml:"strings"`
|
||||
|
||||
// Links to add to challenge/error pages like privacy/impressum.
|
||||
Links []Link `yaml:"links"`
|
||||
|
||||
ChallengeTemplate string `yaml:"challenge-template"`
|
||||
|
||||
// ChallengeTemplateOverrides Key/Value overrides for the current chosen template
|
||||
// Replacements TODO:
|
||||
// Path -> go-away path
|
||||
ChallengeTemplateOverrides map[string]string `yaml:"challenge-template-overrides"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Name string `yaml:"name"`
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
var DefaultSettings = Settings{
|
||||
Strings: DefaultStrings,
|
||||
ChallengeTemplate: "anubis",
|
||||
ChallengeTemplateOverrides: func() map[string]string {
|
||||
m := make(map[string]string)
|
||||
maps.Copy(m, map[string]string{
|
||||
"Theme": "",
|
||||
"Logo": "",
|
||||
})
|
||||
return m
|
||||
}(),
|
||||
|
||||
Bind: Bind{
|
||||
Address: ":8080",
|
||||
Network: "tcp",
|
||||
SocketMode: "0770",
|
||||
Proxy: false,
|
||||
TLSAcmeAutoCert: "",
|
||||
},
|
||||
Backends: make(map[string]Backend),
|
||||
}
|
||||
24
lib/settings/strings.go
Normal file
24
lib/settings/strings.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package settings
|
||||
|
||||
import "maps"
|
||||
|
||||
type Strings map[string]string
|
||||
|
||||
var DefaultStrings = make(Strings).set(map[string]string{
|
||||
"challenge_are_you_bot": "Checking you are not a bot",
|
||||
"error": "Oh no!",
|
||||
})
|
||||
|
||||
func (s Strings) set(v map[string]string) Strings {
|
||||
maps.Copy(s, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s Strings) Get(value string) string {
|
||||
v, ok := (s)[value]
|
||||
if !ok {
|
||||
// fallback
|
||||
return "string:" + value
|
||||
}
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user