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 }