Files
phishingclub/backend/config/config.go
T
2026-09-19 11:31:42 +02:00

607 lines
16 KiB
Go

package config
import (
"encoding/json"
"fmt"
"io/fs"
"net"
"os"
"strconv"
"github.com/go-errors/errors"
"github.com/phishingclub/phishingclub/data"
"github.com/phishingclub/phishingclub/errs"
"github.com/phishingclub/phishingclub/file"
)
var (
ErrMissingIP = errors.New("missing IP")
ErrMissingPort = errors.New("missing port")
ErrMissingDatabaseDSN = errors.New("missing database DSN")
ErrInvalidIP = errors.New("invalid IP")
ErrInvalidPort = errors.New("invalid port")
ErrInvalidDatabase = errors.New("invalid database")
ErrWriterIsNil = errors.New("writer is nil")
)
const (
DefaultACMEEmail = ""
DefaultDevACMEEmail = ""
DatabaseUsePostgres = "postgres"
DefaultAdministrationUseSqlite = "sqlite3"
DefaultDatabase = DefaultAdministrationUseSqlite
DefaultAdministrationDSN = "file:./db.sqlite3"
DefaultDevAdministrationPort = 0 // 0 uses ephemeral port, random available port
DefaultDevHTTPPhishingPort = 8080
DefaultDevHTTPSPhishingPort = 8443
DefaultProductionAdministrationPort = 0 // 0 uses ephemeral port, random available port
DefaultProductionHTTPPhishingPort = 80
DefaultProductionHTTPSPhishingPort = 443
// empty is none
DefaultLogFilePath = ""
DefaultErrLogFilePath = ""
DefaultTrustedIPHeader = ""
DefaultAdminHost = ""
DefaultAdminAutoTLS = true
DefaultAdminAutoTLSString = "true"
)
var (
defaultTrustedProxies = []string{}
defaultAdminAllowed = []string{}
)
type (
// Config config
Config struct {
acme ACME
tlsHost string
tlsAuto bool
tlsCertPath string
tlsKeyPath string
adminNetAddress net.TCPAddr
phishingHTTPNetAddress net.TCPAddr
phishingHTTPSNetAddress net.TCPAddr
database Database
fileWriter file.Writer
LogPath string
ErrLogPath string
IPSecurity IPSecurityConfig
RemoteBrowser RemoteBrowserServerConfig
Script ScriptServerConfig
Authentication AuthenticationConfig
}
// ConfigDTO config DTO
ConfigDTO struct {
ACME ACME `json:"acme"`
AdministrationServer AdministrationServer `json:"administration"`
PhishingServer PhishingServer `json:"phishing"`
Database Database `json:"database"`
Log Log `json:"log"`
IPSecurity IPSecurityConfig `json:"ip_security"`
RemoteBrowser RemoteBrowserServerConfig `json:"remote_browser"`
Script ScriptServerConfig `json:"script"`
Authentication AuthenticationConfig `json:"authentication"`
}
// AuthenticationConfig holds server level authentication settings.
AuthenticationConfig struct {
// LocalLoginBreakglass keeps username and password login available even
// when exclusive SSO is enabled. Set at the server level only as a
// recovery path if the identity provider is misconfigured and locks
// everyone out. Defaults to false.
LocalLoginBreakglass bool `json:"local_login_breakglass"`
}
Log struct {
Path string `json:"path"`
ErrorPath string `json:"errorPath"`
}
// AdministrationServer ConfigDTO administration
AdministrationServer struct {
TLSHost string `json:"tls_host"`
TLSAuto bool `json:"tls_auto"`
TLSCertPath string `json:"tls_cert_path"`
TLSKeyPath string `json:"tls_key_path"`
Address string `json:"address"`
}
// PhishingServer ConfigDTO phishing
PhishingServer struct {
Http string `json:"http"`
Https string `json:"https"`
}
// Database ConfigDTO database
Database struct {
Engine string `json:"engine"`
DSN string `json:"dsn"`
}
// ACME ConfigDTO acme
ACME struct {
Email string `json:"email"`
}
// RemoteBrowserServerConfig holds server-side remote browser settings.
RemoteBrowserServerConfig struct {
// Enabled controls whether the remote browser feature is available.
// Defaults to false. When false all remote browser endpoints return 404.
// When true all built-in safeguards are removed (Chrome flag blocklist,
// URL scheme restriction) because operators are trusted at server level.
// Only enable on instances where every operator is trusted as a server admin.
Enabled bool `json:"enabled"`
// ExecPath is the path to a Chrome/Chromium binary. When empty Rod uses
// its own auto-downloaded Chromium. Set at the server level only.
ExecPath string `json:"exec_path"`
}
// ScriptServerConfig holds server side script settings.
ScriptServerConfig struct {
// Enabled controls whether the script feature is available.
// Defaults to false. When false all script endpoints return 404 and
// no scripts run. When true admin authored scripts run in a sandboxed
// engine with outbound network access, so only enable on instances where
// every operator is trusted as a server admin.
Enabled bool `json:"enabled"`
}
)
type IPSecurityConfig struct {
// ip/cidr that are allowed to access the admin interface
AdminAllowed []string `json:"admin_allowed"`
// ip/cidr of legitimate reverse proxies (e.g., Nginx, HAProxy, Cloudflare edges)
TrustedProxies []string `json:"trusted_proxies"`
// headers to check for real client IP
// examples: CF-Connecting-IP, X-Real-IP, True-Client-IP, X-Forwarded-For
TrustedIPHeader string `json:"trusted_ip_header"`
}
// ValidateFileWriter validates the file writer
func ValidateFileWriter(fileWriter file.Writer) error {
if fileWriter == nil {
return ErrWriterIsNil
}
return nil
}
// NewConfig factory
func NewConfig(
acmeEmail string,
tlsHost string,
tlsAuto bool,
adminPublicCertPath string,
adminPrivateCertKey string,
adminAddress string,
phishingHTTPAddress string,
phishingHTTPSAddress string,
database Database,
fileWriter file.Writer,
logPath string,
errLogPath string,
ipSecurity IPSecurityConfig,
) (*Config, error) {
if err := ValidateFileWriter(fileWriter); err != nil {
return nil, errs.Wrap(err)
}
adminNetAddress, err := StringAddressToTCPAddr(adminAddress)
if err != nil {
return nil, errs.Wrap(err)
}
phishingHTTPNetAddress, err := StringAddressToTCPAddr(phishingHTTPAddress)
if err != nil {
return nil, errs.Wrap(err)
}
phishingHTTPSNetAddress, err := StringAddressToTCPAddr(phishingHTTPSAddress)
if err != nil {
return nil, errs.Wrap(err)
}
switch database.Engine {
case DatabaseUsePostgres:
case DefaultAdministrationUseSqlite:
default:
return nil, ErrInvalidDatabase
}
return &Config{
acme: ACME{
Email: acmeEmail,
},
tlsHost: tlsHost,
tlsAuto: tlsAuto,
tlsCertPath: adminPublicCertPath,
tlsKeyPath: adminPrivateCertKey,
adminNetAddress: *adminNetAddress,
phishingHTTPNetAddress: *phishingHTTPNetAddress,
phishingHTTPSNetAddress: *phishingHTTPSNetAddress,
database: Database{
Engine: database.Engine,
DSN: database.DSN,
},
fileWriter: &file.FileWriter{},
LogPath: logPath,
ErrLogPath: errLogPath,
IPSecurity: ipSecurity,
}, nil
}
// NewDevDefaultConfig returns a default config
func NewDevDefaultConfig() *Config {
tlsHost := "phish.test"
tlsAuto := false
publicCertPath := fmt.Sprintf(
"%s/%s",
data.DefaultAdminCertDir,
data.DefaultAdminPublicCertFileName,
)
privateCertPath := fmt.Sprintf(
"%s/%s",
data.DefaultAdminCertDir,
data.DefaultAdminPrivateCertFileName,
)
return &Config{
acme: ACME{
Email: DefaultACMEEmail,
},
tlsHost: tlsHost,
tlsAuto: tlsAuto,
tlsCertPath: publicCertPath,
tlsKeyPath: privateCertPath,
adminNetAddress: net.TCPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: DefaultDevAdministrationPort,
},
phishingHTTPNetAddress: net.TCPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: DefaultDevHTTPPhishingPort,
},
phishingHTTPSNetAddress: net.TCPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: DefaultDevHTTPSPhishingPort,
},
database: Database{
Engine: DefaultAdministrationUseSqlite,
DSN: DefaultAdministrationDSN,
},
fileWriter: &file.FileWriter{},
LogPath: DefaultLogFilePath,
ErrLogPath: DefaultErrLogFilePath,
IPSecurity: IPSecurityConfig{
AdminAllowed: []string{},
TrustedProxies: []string{},
TrustedIPHeader: "",
},
}
}
// NewDevDefaultConfig returns a default config
func NewProductionDefaultConfig() *Config {
tlsHost := "localhost"
tlsAuto := DefaultAdminAutoTLS
publicCertPath := fmt.Sprintf(
"%s/%s",
data.DefaultAdminCertDir,
data.DefaultAdminPublicCertFileName,
)
privateCertPath := fmt.Sprintf(
"%s/%s",
data.DefaultAdminCertDir,
data.DefaultAdminPrivateCertFileName,
)
return &Config{
acme: ACME{
Email: DefaultACMEEmail,
},
tlsHost: tlsHost,
tlsAuto: tlsAuto,
tlsCertPath: publicCertPath,
tlsKeyPath: privateCertPath,
adminNetAddress: net.TCPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: DefaultProductionAdministrationPort,
},
phishingHTTPNetAddress: net.TCPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: DefaultProductionHTTPPhishingPort,
},
phishingHTTPSNetAddress: net.TCPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: DefaultProductionHTTPSPhishingPort,
},
database: Database{
Engine: DefaultAdministrationUseSqlite,
DSN: DefaultAdministrationDSN,
},
fileWriter: &file.FileWriter{},
IPSecurity: IPSecurityConfig{
AdminAllowed: []string{},
TrustedProxies: []string{},
TrustedIPHeader: "",
},
}
}
// ACMEEmail returns the acme email
func (c *Config) ACMEEmail() string {
return c.acme.Email
}
// SetACMEEmail sets the acme email
func (c *Config) SetACMEEmail(email string) {
c.acme.Email = email
}
// TLSHost returns the host to use for admin server
func (c *Config) TLSHost() string {
return c.tlsHost
}
// TLSAuto returns if ACME service should handle TLS for the admin server
func (c *Config) TLSAuto() bool {
return c.tlsAuto
}
// TLSCertPath returns the cert path
func (c *Config) TLSCertPath() string {
return c.tlsCertPath
}
// TLSKeyPath returns the private key
func (c *Config) TLSKeyPath() string {
return c.tlsKeyPath
}
// SetTLSHost sets the admin host
func (c *Config) SetTLSHost(host string) {
c.tlsHost = host
}
// SetTLSCertPath sets the TLS certificate path
func (c *Config) SetTLSCertPath(certPath string) {
c.tlsCertPath = certPath
}
// SetTLSKeyPath sets the TLS private key path
func (c *Config) SetTLSKeyPath(keyPath string) {
c.tlsKeyPath = keyPath
}
// SetTLSAuto sets if a ACME service should handle TLS for the admin server
func (c *Config) SetTLSAuto(auto bool) {
c.tlsAuto = auto
}
// SetAdminNetAddress sets the administration network address
func (c *Config) SetAdminNetAddress(adminNetAddress string) error {
newAddr, err := StringAddressToTCPAddr(adminNetAddress)
if err != nil {
return err
}
c.adminNetAddress = *newAddr
return nil
}
// SetPhishingHTTPNetAddress sets the phishing network address
func (c *Config) SetPhishingHTTPNetAddress(addr string) error {
newAddr, err := StringAddressToTCPAddr(addr)
if err != nil {
return err
}
c.phishingHTTPNetAddress = *newAddr
return nil
}
// SetPhishingHTTPNetAddress sets the phishing network address
func (c *Config) SetPhishingHTTPSNetAddress(addr string) error {
newAddr, err := StringAddressToTCPAddr(addr)
if err != nil {
return err
}
c.phishingHTTPSNetAddress = *newAddr
return nil
}
// SetDatabaseEngine sets the database engine
func (c *Config) SetDatabaseEngine(engine string) {
c.database.Engine = engine
}
// SetDatabaseDSN sets the database DSN
func (c *Config) SetDatabaseDSN(dsn string) {
c.database.DSN = dsn
}
// SetLogPath sets the log file path
func (c *Config) SetLogPath(path string) {
c.LogPath = path
}
// SetErrLogPath sets the error log file path
func (c *Config) SetErrLogPath(path string) {
c.ErrLogPath = path
}
// SetRemoteBrowserEnabled enables or disables the remote browser feature.
func (c *Config) SetRemoteBrowserEnabled(enabled bool) {
c.RemoteBrowser.Enabled = enabled
}
// SetRemoteBrowserExecPath sets the Chrome binary path used by the remote browser runner.
func (c *Config) SetRemoteBrowserExecPath(path string) {
c.RemoteBrowser.ExecPath = path
}
// SetFileWriter sets the file writer
func (c *Config) SetFileWriter(fileWriter file.Writer) error {
if err := ValidateFileWriter(fileWriter); err != nil {
return fmt.Errorf("failed to set file writer on config: %w", err)
}
c.fileWriter = fileWriter
return nil
}
// Write writes the config to a writer
func (c *Config) WriteToFile(filepath string) error {
dto := c.ToDTO()
conf, err := json.MarshalIndent(dto, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
// Write the content to the writer
if _, err := c.fileWriter.Write(filepath, conf, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
return nil
}
// StringAddressToTCPAddr converts a string address to a TCPAddr
func StringAddressToTCPAddr(address string) (*net.TCPAddr, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, errs.Wrap(err)
}
ip := net.ParseIP(host)
if ip == nil {
return nil, ErrInvalidIP
}
// convert port to int
p, err := strconv.Atoi(port)
if err != nil {
return nil, errs.Wrap(err)
}
if p < 0 || p > 65535 {
return nil, ErrInvalidPort
}
return &net.TCPAddr{
IP: ip,
Port: p,
}, nil
}
// FromMap creates a *Config from a DTO
func FromDTO(dto *ConfigDTO) (*Config, error) {
cfg, err := NewConfig(
dto.ACME.Email,
dto.AdministrationServer.TLSHost,
dto.AdministrationServer.TLSAuto,
dto.AdministrationServer.TLSCertPath,
dto.AdministrationServer.TLSKeyPath,
dto.AdministrationServer.Address,
dto.PhishingServer.Http,
dto.PhishingServer.Https,
dto.Database,
file.FileWriter{},
dto.Log.Path,
dto.Log.ErrorPath,
dto.IPSecurity,
)
if err != nil {
return nil, err
}
cfg.RemoteBrowser = dto.RemoteBrowser
cfg.Script = dto.Script
cfg.Authentication = dto.Authentication
return cfg, nil
}
// ToDTO converts a *Config to a *ConfigDTO
func (c *Config) ToDTO() *ConfigDTO {
return &ConfigDTO{
ACME: ACME{
Email: c.acme.Email,
},
AdministrationServer: AdministrationServer{
TLSHost: c.TLSHost(),
TLSAuto: c.TLSAuto(),
TLSCertPath: c.TLSCertPath(),
TLSKeyPath: c.TLSKeyPath(),
Address: c.AdminNetAddress(),
},
PhishingServer: PhishingServer{
Http: c.phishingHTTPNetAddress.String(),
Https: c.phishingHTTPSNetAddress.String(),
},
Database: Database{
Engine: c.database.Engine,
DSN: c.database.DSN,
},
Log: Log{
Path: c.LogPath,
ErrorPath: c.ErrLogPath,
},
IPSecurity: c.IPSecurity,
RemoteBrowser: c.RemoteBrowser,
Script: c.Script,
Authentication: c.Authentication,
}
}
// AdminNetAddress returns the administration network address
func (c *Config) AdminNetAddress() string {
return c.adminNetAddress.String()
}
// AdminNetAddressPort returns the administration network address port
func (c *Config) AdminNetAddressPort() int {
return c.adminNetAddress.Port
}
// PhishingHTTPNetAddress returns the phishing network address
func (c *Config) PhishingHTTPNetAddress() string {
return c.phishingHTTPNetAddress.String()
}
// PhishingHTTPNetAddressPort returns the phishing network address port
func (c *Config) PhishingHTTPNetAddressPort() int {
return c.phishingHTTPNetAddress.Port
}
// PhishingHTTPSNetAddress returns the phishing network address
func (c *Config) PhishingHTTPSNetAddress() string {
return c.phishingHTTPSNetAddress.String()
}
// PhishingHTTPSNetAddressPort returns the phishing network address port
func (c *Config) PhishingHTTPSNetAddressPort() int {
return c.phishingHTTPSNetAddress.Port
}
// Database returns the database
func (c *Config) Database() Database {
return c.database
}
// NewDTOFromFile creates a *ConfigDTO from a file
func NewDTOFromFile(filesystem fs.FS, path string) (*ConfigDTO, error) {
var conf ConfigDTO
f, err := filesystem.Open(path)
if err != nil {
return nil, errs.Wrap(err)
}
dec := json.NewDecoder(f)
err = dec.Decode(&conf)
if err != nil {
return nil, errs.Wrap(err)
}
return &conf, nil
}