2016-06-15 13:50:56 +02:00
// Copyright (c) 2012-2014 Jeremy Latt
// Copyright (c) 2014-2015 Edmund Huber
2017-03-27 14:15:02 +02:00
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
2016-06-15 13:50:56 +02:00
// released under the MIT license
2014-02-09 16:53:42 +01:00
package irc
import (
2020-07-01 10:14:30 +02:00
"bytes"
2016-04-13 12:45:09 +02:00
"crypto/tls"
2021-04-08 04:35:54 +02:00
"crypto/x509"
2020-03-16 12:54:50 +01:00
"errors"
2016-10-23 02:47:11 +02:00
"fmt"
2021-02-17 21:11:54 +01:00
"io"
2014-02-24 07:21:39 +01:00
"log"
2019-02-05 06:19:03 +01:00
"net"
2018-08-28 19:34:43 +02:00
"os"
2020-05-12 18:05:40 +02:00
"path/filepath"
2020-10-25 18:58:57 +01:00
"reflect"
2018-04-19 08:48:19 +02:00
"regexp"
2021-04-23 19:54:44 +02:00
"runtime"
2019-08-27 06:51:09 +02:00
"strconv"
2016-10-23 03:01:05 +02:00
"strings"
2017-01-12 08:40:01 +01:00
"time"
2016-04-12 15:00:09 +02:00
2017-10-05 16:03:53 +02:00
"code.cloudfoundry.org/bytefmt"
2021-06-18 08:41:57 +02:00
"github.com/ergochat/irc-go/ircfmt"
2020-07-01 10:14:30 +02:00
"gopkg.in/yaml.v2"
2021-05-25 06:34:38 +02:00
"github.com/ergochat/ergo/irc/caps"
"github.com/ergochat/ergo/irc/cloaks"
"github.com/ergochat/ergo/irc/connection_limits"
"github.com/ergochat/ergo/irc/custime"
"github.com/ergochat/ergo/irc/email"
"github.com/ergochat/ergo/irc/isupport"
"github.com/ergochat/ergo/irc/jwt"
"github.com/ergochat/ergo/irc/languages"
"github.com/ergochat/ergo/irc/logger"
"github.com/ergochat/ergo/irc/modes"
"github.com/ergochat/ergo/irc/mysql"
2024-02-14 00:58:32 +01:00
"github.com/ergochat/ergo/irc/oauth2"
2021-05-25 06:34:38 +02:00
"github.com/ergochat/ergo/irc/passwd"
"github.com/ergochat/ergo/irc/utils"
2014-02-09 16:53:42 +01:00
)
2018-07-16 09:46:40 +02:00
// here's how this works: exported (capitalized) members of the config structs
// are defined in the YAML file and deserialized directly from there. They may
// be postprocessed and overwritten by LoadConfig. Unexported (lowercase) members
// are derived from the exported members in LoadConfig.
2014-03-01 23:34:51 +01:00
2017-04-16 03:31:33 +02:00
// TLSListenConfig defines configuration options for listening on TLS.
2016-04-28 12:12:23 +02:00
type TLSListenConfig struct {
2019-11-20 23:14:42 +01:00
Cert string
Key string
2020-11-19 18:31:58 +01:00
Proxy bool // XXX: legacy key: it's preferred to specify this directly in listenerConfigBlock
2016-04-13 12:45:09 +02:00
}
2019-06-28 16:45:34 +02:00
// This is the YAML-deserializable type of the value of the `Server.Listeners` map
type listenerConfigBlock struct {
2021-04-08 04:49:33 +02:00
// normal TLS configuration, with a single certificate:
TLS TLSListenConfig
// SNI configuration, with multiple certificates:
TLSCertificates [ ] TLSListenConfig ` yaml:"tls-certificates" `
2021-04-19 02:31:11 +02:00
MinTLSVersion string ` yaml:"min-tls-version" `
2021-04-08 04:49:33 +02:00
Proxy bool
Tor bool
STSOnly bool ` yaml:"sts-only" `
WebSocket bool
HideSTS bool ` yaml:"hide-sts" `
2019-06-28 16:45:34 +02:00
}
2021-01-21 03:13:18 +01:00
type HistoryCutoff uint
const (
HistoryCutoffDefault HistoryCutoff = iota
HistoryCutoffNone
HistoryCutoffRegistrationTime
HistoryCutoffJoinTime
)
func historyCutoffToString ( restriction HistoryCutoff ) string {
switch restriction {
case HistoryCutoffDefault :
return "default"
case HistoryCutoffNone :
return "none"
case HistoryCutoffRegistrationTime :
return "registration-time"
case HistoryCutoffJoinTime :
return "join-time"
default :
return ""
}
}
func historyCutoffFromString ( str string ) ( result HistoryCutoff , err error ) {
switch strings . ToLower ( str ) {
case "default" :
return HistoryCutoffDefault , nil
case "none" , "disabled" , "off" , "false" :
return HistoryCutoffNone , nil
case "registration-time" :
return HistoryCutoffRegistrationTime , nil
case "join-time" :
return HistoryCutoffJoinTime , nil
default :
return HistoryCutoffDefault , errInvalidParams
}
}
2020-02-19 01:38:42 +01:00
type PersistentStatus uint
const (
PersistentUnspecified PersistentStatus = iota
PersistentDisabled
PersistentOptIn
PersistentOptOut
PersistentMandatory
)
func persistentStatusToString ( status PersistentStatus ) string {
switch status {
case PersistentUnspecified :
return "default"
case PersistentDisabled :
return "disabled"
case PersistentOptIn :
return "opt-in"
case PersistentOptOut :
return "opt-out"
case PersistentMandatory :
return "mandatory"
default :
return ""
}
}
func persistentStatusFromString ( status string ) ( PersistentStatus , error ) {
switch strings . ToLower ( status ) {
case "default" :
return PersistentUnspecified , nil
case "" :
return PersistentDisabled , nil
case "opt-in" :
return PersistentOptIn , nil
case "opt-out" :
return PersistentOptOut , nil
case "mandatory" :
return PersistentMandatory , nil
default :
b , err := utils . StringToBool ( status )
if b {
return PersistentMandatory , err
} else {
return PersistentDisabled , err
}
}
}
func ( ps * PersistentStatus ) UnmarshalYAML ( unmarshal func ( interface { } ) error ) error {
var orig string
var err error
if err = unmarshal ( & orig ) ; err != nil {
return err
}
result , err := persistentStatusFromString ( orig )
if err == nil {
if result == PersistentUnspecified {
result = PersistentDisabled
}
* ps = result
2021-02-26 07:10:21 +01:00
} else {
err = fmt . Errorf ( "invalid value `%s` for server persistence status: %w" , orig , err )
2020-02-19 01:38:42 +01:00
}
return err
}
func persistenceEnabled ( serverSetting , clientSetting PersistentStatus ) ( enabled bool ) {
if serverSetting == PersistentDisabled {
return false
} else if serverSetting == PersistentMandatory {
return true
} else if clientSetting == PersistentDisabled {
return false
} else if clientSetting == PersistentMandatory {
return true
} else if serverSetting == PersistentOptOut {
return true
} else {
return false
}
}
type HistoryStatus uint
const (
HistoryDefault HistoryStatus = iota
HistoryDisabled
HistoryEphemeral
HistoryPersistent
)
func historyStatusFromString ( str string ) ( status HistoryStatus , err error ) {
switch strings . ToLower ( str ) {
case "default" :
return HistoryDefault , nil
case "ephemeral" :
return HistoryEphemeral , nil
case "persistent" :
return HistoryPersistent , nil
default :
b , err := utils . StringToBool ( str )
if b {
return HistoryPersistent , err
} else {
return HistoryDisabled , err
}
}
}
func historyStatusToString ( status HistoryStatus ) string {
switch status {
case HistoryDefault :
return "default"
case HistoryDisabled :
return "disabled"
case HistoryEphemeral :
return "ephemeral"
case HistoryPersistent :
return "persistent"
default :
return ""
}
}
2020-02-24 20:09:00 +01:00
// XXX you must have already checked History.Enabled before calling this
2020-02-19 01:38:42 +01:00
func historyEnabled ( serverSetting PersistentStatus , localSetting HistoryStatus ) ( result HistoryStatus ) {
2020-02-24 20:09:00 +01:00
switch serverSetting {
case PersistentMandatory :
2020-02-19 01:38:42 +01:00
return HistoryPersistent
2020-02-24 20:09:00 +01:00
case PersistentOptOut :
2020-02-19 01:38:42 +01:00
if localSetting == HistoryDefault {
return HistoryPersistent
} else {
return localSetting
}
2020-02-24 20:09:00 +01:00
case PersistentOptIn :
switch localSetting {
case HistoryPersistent :
return HistoryPersistent
case HistoryEphemeral , HistoryDefault :
return HistoryEphemeral
default :
2020-02-19 01:38:42 +01:00
return HistoryDisabled
}
2020-02-24 20:09:00 +01:00
case PersistentDisabled :
if localSetting == HistoryDisabled {
return HistoryDisabled
} else {
return HistoryEphemeral
}
default :
// PersistentUnspecified: shouldn't happen because the deserializer converts it
// to PersistentDisabled
if localSetting == HistoryDefault {
return HistoryEphemeral
} else {
return localSetting
}
2020-02-19 01:38:42 +01:00
}
}
2020-02-21 05:55:42 +01:00
type MulticlientConfig struct {
2020-12-21 11:11:50 +01:00
Enabled bool
AllowedByDefault bool ` yaml:"allowed-by-default" `
AlwaysOn PersistentStatus ` yaml:"always-on" `
AutoAway PersistentStatus ` yaml:"auto-away" `
AlwaysOnExpiration custime . Duration ` yaml:"always-on-expiration" `
2020-02-21 05:55:42 +01:00
}
2020-03-27 22:52:37 +01:00
type throttleConfig struct {
Enabled bool
Duration time . Duration
MaxAttempts int ` yaml:"max-attempts" `
}
type ThrottleConfig struct {
throttleConfig
}
func ( t * ThrottleConfig ) UnmarshalYAML ( unmarshal func ( interface { } ) error ) ( err error ) {
// note that this technique only works if the zero value of the struct
// doesn't need any postprocessing (because if the field is omitted entirely
// from the YAML, then UnmarshalYAML won't be called at all)
if err = unmarshal ( & t . throttleConfig ) ; err != nil {
return
}
if ! t . Enabled {
t . MaxAttempts = 0 // limit of 0 means disabled
}
return
}
2018-02-11 11:30:40 +01:00
type AccountConfig struct {
2018-02-18 10:46:14 +01:00
Registration AccountRegistrationConfig
2019-01-01 22:45:37 +01:00
AuthenticationEnabled bool ` yaml:"authentication-enabled" `
2023-01-11 15:10:25 +01:00
AdvertiseSCRAM bool ` yaml:"advertise-scram" `
2019-02-05 06:19:03 +01:00
RequireSasl struct {
Enabled bool
Exempted [ ] string
exemptedNets [ ] net . IPNet
} ` yaml:"require-sasl" `
2020-05-18 00:06:20 +02:00
DefaultUserModes * string ` yaml:"default-user-modes" `
2020-05-28 17:53:14 +02:00
defaultUserModes modes . Modes
2020-05-18 00:06:20 +02:00
LoginThrottling ThrottleConfig ` yaml:"login-throttling" `
SkipServerPassword bool ` yaml:"skip-server-password" `
LoginViaPassCommand bool ` yaml:"login-via-pass-command" `
NickReservation struct {
2020-03-16 12:54:50 +01:00
Enabled bool
AdditionalNickLimit int ` yaml:"additional-nick-limit" `
Method NickEnforcementMethod
2020-08-25 20:11:13 +02:00
AllowCustomEnforcement bool ` yaml:"allow-custom-enforcement" `
2020-03-16 12:54:50 +01:00
// RenamePrefix is the legacy field, GuestFormat is the new version
2020-03-17 04:25:50 +01:00
RenamePrefix string ` yaml:"rename-prefix" `
GuestFormat string ` yaml:"guest-nickname-format" `
guestRegexp * regexp . Regexp
guestRegexpFolded * regexp . Regexp
ForceGuestFormat bool ` yaml:"force-guest-format" `
ForceNickEqualsAccount bool ` yaml:"force-nick-equals-account" `
2020-10-19 16:52:38 +02:00
ForbidAnonNickChanges bool ` yaml:"forbid-anonymous-nick-changes" `
2020-03-16 12:54:50 +01:00
} ` yaml:"nick-reservation" `
Multiclient MulticlientConfig
Bouncer * MulticlientConfig // # handle old name for 'multiclient'
VHosts VHostConfig
2024-02-14 00:58:32 +01:00
AuthScript AuthScriptConfig ` yaml:"auth-script" `
OAuth2 oauth2 . OAuth2BearerConfig ` yaml:"oauth2" `
JWTAuth jwt . JWTAuthConfig ` yaml:"jwt-auth" `
2020-06-04 07:18:24 +02:00
}
2020-09-14 10:28:12 +02:00
type ScriptConfig struct {
Enabled bool
Command string
Args [ ] string
Timeout time . Duration
KillTimeout time . Duration ` yaml:"kill-timeout" `
MaxConcurrency uint ` yaml:"max-concurrency" `
}
2020-06-04 07:18:24 +02:00
type AuthScriptConfig struct {
2020-09-14 10:28:12 +02:00
ScriptConfig ` yaml:",inline" `
Autocreate bool
2018-02-11 11:30:40 +01:00
}
2022-01-02 07:51:31 +01:00
type IPCheckScriptConfig struct {
ScriptConfig ` yaml:",inline" `
ExemptSASL bool ` yaml:"exempt-sasl" `
}
2017-04-16 03:31:33 +02:00
// AccountRegistrationConfig controls account registration.
2016-09-04 11:25:33 +02:00
type AccountRegistrationConfig struct {
2020-10-07 00:04:29 +02:00
Enabled bool
AllowBeforeConnect bool ` yaml:"allow-before-connect" `
Throttling ThrottleConfig
// new-style (v2.4 email verification config):
EmailVerification email . MailtoConfig ` yaml:"email-verification" `
// old-style email verification config, with "callbacks":
LegacyEnabledCallbacks [ ] string ` yaml:"enabled-callbacks" `
LegacyCallbacks struct {
2020-04-05 09:48:59 +02:00
Mailto email . MailtoConfig
2020-10-07 00:04:29 +02:00
} ` yaml:"callbacks" `
VerifyTimeout custime . Duration ` yaml:"verify-timeout" `
BcryptCost uint ` yaml:"bcrypt-cost" `
2016-09-04 11:25:33 +02:00
}
2018-04-23 08:38:35 +02:00
type VHostConfig struct {
Enabled bool
MaxLength int ` yaml:"max-length" `
ValidRegexpRaw string ` yaml:"valid-regexp" `
2020-10-22 18:19:19 +02:00
validRegexp * regexp . Regexp
2018-04-19 08:48:19 +02:00
}
2019-05-19 10:27:44 +02:00
type NickEnforcementMethod int
2018-02-18 10:46:14 +01:00
const (
2019-05-19 10:27:44 +02:00
// NickEnforcementOptional is the zero value; it serializes to
2019-01-02 16:08:44 +01:00
// "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`.
// in both cases, it means "defer to the other source of truth", i.e.,
// in the config, defer to the user's custom setting, and as a custom setting,
2019-05-19 10:27:44 +02:00
// defer to the default in the config. if both are NickEnforcementOptional then
2019-01-02 16:08:44 +01:00
// there is no enforcement.
2019-05-19 10:27:44 +02:00
// XXX: these are serialized as numbers in the database, so beware of collisions
// when refactoring (any numbers currently in use must keep their meanings, or
// else be fixed up by a schema change)
NickEnforcementOptional NickEnforcementMethod = iota
NickEnforcementNone
NickEnforcementStrict
2018-02-18 10:46:14 +01:00
)
2019-05-19 10:27:44 +02:00
func nickReservationToString ( method NickEnforcementMethod ) string {
2019-01-02 16:08:44 +01:00
switch method {
2019-05-19 10:27:44 +02:00
case NickEnforcementOptional :
2019-01-02 16:08:44 +01:00
return "default"
2019-05-19 10:27:44 +02:00
case NickEnforcementNone :
2019-01-02 16:08:44 +01:00
return "none"
2019-05-19 10:27:44 +02:00
case NickEnforcementStrict :
2019-01-02 16:08:44 +01:00
return "strict"
default :
return ""
}
}
2019-05-19 10:27:44 +02:00
func nickReservationFromString ( method string ) ( NickEnforcementMethod , error ) {
switch strings . ToLower ( method ) {
2019-01-02 16:08:44 +01:00
case "default" :
2019-05-19 10:27:44 +02:00
return NickEnforcementOptional , nil
2019-01-02 16:08:44 +01:00
case "optional" :
2019-05-19 10:27:44 +02:00
return NickEnforcementOptional , nil
2019-01-02 16:08:44 +01:00
case "none" :
2019-05-19 10:27:44 +02:00
return NickEnforcementNone , nil
2019-01-02 16:08:44 +01:00
case "strict" :
2019-05-19 10:27:44 +02:00
return NickEnforcementStrict , nil
2019-01-02 16:08:44 +01:00
default :
2019-05-19 10:27:44 +02:00
return NickEnforcementOptional , fmt . Errorf ( "invalid nick-reservation.method value: %s" , method )
2019-01-02 16:08:44 +01:00
}
}
2019-05-19 10:27:44 +02:00
func ( nr * NickEnforcementMethod ) UnmarshalYAML ( unmarshal func ( interface { } ) error ) error {
var orig string
2018-02-18 10:46:14 +01:00
var err error
if err = unmarshal ( & orig ) ; err != nil {
return err
}
2019-05-19 10:27:44 +02:00
method , err := nickReservationFromString ( orig )
2019-01-02 16:08:44 +01:00
if err == nil {
* nr = method
2021-02-26 07:10:21 +01:00
} else {
err = fmt . Errorf ( "invalid value `%s` for nick enforcement method: %w" , orig , err )
2018-02-18 10:46:14 +01:00
}
2019-01-02 16:08:44 +01:00
return err
2018-02-18 10:46:14 +01:00
}
2019-12-18 13:06:04 +01:00
func ( cm * Casemapping ) UnmarshalYAML ( unmarshal func ( interface { } ) error ) ( err error ) {
var orig string
if err = unmarshal ( & orig ) ; err != nil {
return err
}
var result Casemapping
switch strings . ToLower ( orig ) {
case "ascii" :
result = CasemappingASCII
case "precis" , "rfc7613" , "rfc8265" :
result = CasemappingPRECIS
case "permissive" , "fun" :
result = CasemappingPermissive
default :
return fmt . Errorf ( "invalid casemapping value: %s" , orig )
}
* cm = result
return nil
}
2017-04-16 03:31:33 +02:00
// OperClassConfig defines a specific operator class.
2016-10-23 02:47:11 +02:00
type OperClassConfig struct {
Title string
2016-10-23 03:01:05 +02:00
WhoisLine string
2016-10-23 02:47:11 +02:00
Extends string
Capabilities [ ] string
}
2017-04-16 03:31:33 +02:00
// OperConfig defines a specific operator's configuration.
2016-10-23 02:47:11 +02:00
type OperConfig struct {
2019-12-19 02:33:50 +01:00
Class string
Vhost string
WhoisLine string ` yaml:"whois-line" `
Password string
2020-06-21 21:46:08 +02:00
Fingerprint * string // legacy name for certfp, #1050
Certfp string
2019-12-19 12:33:43 +01:00
Auto bool
2020-10-09 14:03:26 +02:00
Hidden bool
2019-12-19 02:33:50 +01:00
Modes string
2016-10-23 02:47:11 +02:00
}
2018-07-16 09:46:40 +02:00
// Various server-enforced limits on data size.
type Limits struct {
2020-01-19 05:47:05 +01:00
AwayLen int ` yaml:"awaylen" `
ChanListModes int ` yaml:"chan-list-modes" `
ChannelLen int ` yaml:"channellen" `
IdentLen int ` yaml:"identlen" `
2024-02-08 06:03:12 +01:00
RealnameLen int ` yaml:"realnamelen" `
2020-01-19 05:47:05 +01:00
KickLen int ` yaml:"kicklen" `
MonitorEntries int ` yaml:"monitor-entries" `
NickLen int ` yaml:"nicklen" `
TopicLen int ` yaml:"topiclen" `
WhowasEntries int ` yaml:"whowas-entries" `
RegistrationMessages int ` yaml:"registration-messages" `
2019-12-23 21:26:37 +01:00
Multiline struct {
MaxBytes int ` yaml:"max-bytes" `
MaxLines int ` yaml:"max-lines" `
}
2018-07-16 09:46:40 +02:00
}
2017-04-16 03:31:33 +02:00
// STSConfig controls the STS configuration/
2017-03-09 10:07:35 +01:00
type STSConfig struct {
2020-02-20 06:09:39 +01:00
Enabled bool
Duration custime . Duration
Port int
Preload bool
STSOnlyBanner string ` yaml:"sts-only-banner" `
bannerLines [ ] string
2017-03-09 10:07:35 +01:00
}
// Value returns the STS value to advertise in CAP
func ( sts * STSConfig ) Value ( ) string {
2020-02-20 06:09:39 +01:00
val := fmt . Sprintf ( "duration=%d" , int ( time . Duration ( sts . Duration ) . Seconds ( ) ) )
2017-03-09 10:07:35 +01:00
if sts . Enabled && sts . Port > 0 {
val += fmt . Sprintf ( ",port=%d" , sts . Port )
}
if sts . Enabled && sts . Preload {
val += ",preload"
}
return val
}
2018-03-22 16:04:21 +01:00
type FakelagConfig struct {
Enabled bool
Window time . Duration
BurstLimit uint ` yaml:"burst-limit" `
MessagesPerWindow uint ` yaml:"messages-per-window" `
2018-03-28 19:18:08 +02:00
Cooldown time . Duration
2022-08-23 05:23:17 +02:00
CommandBudgets map [ string ] int ` yaml:"command-budgets" `
2018-03-22 16:04:21 +01:00
}
2019-02-26 03:50:43 +01:00
type TorListenersConfig struct {
2019-06-28 16:45:34 +02:00
Listeners [ ] string // legacy only
RequireSasl bool ` yaml:"require-sasl" `
2019-02-26 03:50:43 +01:00
Vhost string
MaxConnections int ` yaml:"max-connections" `
ThrottleDuration time . Duration ` yaml:"throttle-duration" `
MaxConnectionsPerDuration int ` yaml:"max-connections-per-duration" `
}
2017-04-16 03:31:33 +02:00
// Config defines the overall configuration.
2014-02-09 16:53:42 +01:00
type Config struct {
2020-10-25 18:58:57 +01:00
AllowEnvironmentOverrides bool ` yaml:"allow-environment-overrides" `
2016-04-12 07:44:00 +02:00
Network struct {
Name string
}
2014-03-01 23:34:51 +01:00
Server struct {
2019-06-28 16:45:34 +02:00
Password string
passwordBytes [ ] byte
Name string
nameCasefolded string
2021-04-08 04:49:33 +02:00
Listeners map [ string ] listenerConfigBlock
UnixBindMode os . FileMode ` yaml:"unix-bind-mode" `
TorListeners TorListenersConfig ` yaml:"tor-listeners" `
WebSockets struct {
2020-05-05 04:29:10 +02:00
AllowedOrigins [ ] string ` yaml:"allowed-origins" `
allowedOriginRegexps [ ] * regexp . Regexp
}
2020-02-21 10:11:47 +01:00
// they get parsed into this internal representation:
2020-05-05 04:29:10 +02:00
trueListeners map [ string ] utils . ListenerConfig
2019-12-17 21:10:23 +01:00
STS STSConfig
LookupHostnames * bool ` yaml:"lookup-hostnames" `
lookupHostnames bool
2020-10-20 19:48:19 +02:00
ForwardConfirmHostnames bool ` yaml:"forward-confirm-hostnames" `
CheckIdent bool ` yaml:"check-ident" `
CoerceIdent string ` yaml:"coerce-ident" `
2019-12-17 21:10:23 +01:00
MOTD string
motdLines [ ] string
2020-06-08 07:17:45 +02:00
MOTDFormatting bool ` yaml:"motd-formatting" `
2020-09-09 10:01:46 +02:00
Relaymsg struct {
2020-06-08 07:17:45 +02:00
Enabled bool
Separators string
AvailableToChanops bool ` yaml:"available-to-chanops" `
}
ProxyAllowedFrom [ ] string ` yaml:"proxy-allowed-from" `
proxyAllowedFromNets [ ] net . IPNet
WebIRC [ ] webircConfig ` yaml:"webirc" `
MaxSendQString string ` yaml:"max-sendq" `
MaxSendQBytes int
Compatibility struct {
2019-05-09 20:18:30 +02:00
ForceTrailing * bool ` yaml:"force-trailing" `
forceTrailing bool
2021-03-05 04:29:34 +01:00
SendUnprefixedSasl bool ` yaml:"send-unprefixed-sasl" `
AllowTruncation * bool ` yaml:"allow-truncation" `
allowTruncation bool
2019-05-09 20:18:30 +02:00
}
2020-11-29 05:40:21 +01:00
isupport isupport . List
IPLimits connection_limits . LimiterConfig ` yaml:"ip-limits" `
Cloaks cloaks . CloakConfig ` yaml:"ip-cloaking" `
SecureNetDefs [ ] string ` yaml:"secure-nets" `
secureNets [ ] net . IPNet
supportedCaps * caps . Set
2020-12-06 05:06:23 +01:00
supportedCapsWithoutSTS * caps . Set
2020-11-29 05:40:21 +01:00
capValues caps . Values
Casemapping Casemapping
2022-01-02 07:51:31 +01:00
EnforceUtf8 bool ` yaml:"enforce-utf8" `
OutputPath string ` yaml:"output-path" `
IPCheckScript IPCheckScriptConfig ` yaml:"ip-check-script" `
OverrideServicesHostname string ` yaml:"override-services-hostname" `
MaxLineLen int ` yaml:"max-line-len" `
SuppressLusers bool ` yaml:"suppress-lusers" `
2014-03-01 23:34:51 +01:00
}
2014-02-25 20:11:34 +01:00
2020-03-19 22:09:52 +01:00
Roleplay struct {
2020-09-16 17:32:52 +02:00
Enabled bool
2020-03-19 22:09:52 +01:00
RequireChanops bool ` yaml:"require-chanops" `
RequireOper bool ` yaml:"require-oper" `
AddSuffix * bool ` yaml:"add-suffix" `
addSuffix bool
}
2020-06-15 20:16:02 +02:00
Extjwt struct {
Default jwt . JwtServiceConfig ` yaml:",inline" `
Services map [ string ] jwt . JwtServiceConfig ` yaml:"services" `
}
2018-01-21 07:11:16 +01:00
Languages struct {
Enabled bool
Path string
2018-01-22 08:30:31 +01:00
Default string
2018-01-21 07:11:16 +01:00
}
2019-02-19 08:54:57 +01:00
languageManager * languages . Manager
2022-01-02 00:56:40 +01:00
LockFile string ` yaml:"lock-file" `
2016-09-04 11:25:33 +02:00
Datastore struct {
2018-04-16 22:28:31 +02:00
Path string
2018-04-20 09:57:48 +02:00
AutoUpgrade bool
2020-02-21 00:33:48 +01:00
MySQL mysql . Config
2016-09-04 11:25:33 +02:00
}
2018-02-11 11:30:40 +01:00
Accounts AccountConfig
2016-09-04 11:25:33 +02:00
2017-03-24 03:52:38 +01:00
Channels struct {
2019-02-06 10:55:05 +01:00
DefaultModes * string ` yaml:"default-modes" `
defaultModes modes . Modes
2019-05-30 11:33:59 +02:00
MaxChannelsPerClient int ` yaml:"max-channels-per-client" `
OpOnlyCreation bool ` yaml:"operator-only-creation" `
2020-03-18 11:55:30 +01:00
Registration struct {
Enabled bool
OperatorOnly bool ` yaml:"operator-only" `
MaxChannelsPerAccount int ` yaml:"max-channels-per-account" `
}
2020-10-26 01:40:41 +01:00
ListDelay time . Duration ` yaml:"list-delay" `
InviteExpiration custime . Duration ` yaml:"invite-expiration" `
2023-07-05 03:44:18 +02:00
AutoJoin [ ] string ` yaml:"auto-join" `
2017-03-24 03:52:38 +01:00
}
2016-10-23 02:47:11 +02:00
OperClasses map [ string ] * OperClassConfig ` yaml:"oper-classes" `
Opers map [ string ] * OperConfig
2014-03-13 09:55:46 +01:00
2018-07-16 09:46:40 +02:00
// parsed operator definitions, unexported so they can't be defined
// directly in YAML:
operators map [ string ] * Oper
2017-10-02 05:31:40 +02:00
Logging [ ] logger . LoggingConfig
2017-03-06 04:31:10 +01:00
2017-04-30 04:35:07 +02:00
Debug struct {
2019-05-23 01:07:12 +02:00
RecoverFromErrors * bool ` yaml:"recover-from-errors" `
recoverFromErrors bool
2021-09-19 08:09:43 +02:00
PprofListener string ` yaml:"pprof-listener" `
2017-04-30 04:35:07 +02:00
}
2018-07-16 09:46:40 +02:00
Limits Limits
2017-09-28 07:30:53 +02:00
2018-03-22 16:04:21 +01:00
Fakelag FakelagConfig
2018-11-26 11:23:27 +01:00
History struct {
2018-12-28 19:45:55 +01:00
Enabled bool
2020-05-19 13:57:44 +02:00
ChannelLength int ` yaml:"channel-length" `
ClientLength int ` yaml:"client-length" `
AutoresizeWindow custime . Duration ` yaml:"autoresize-window" `
AutoreplayOnJoin int ` yaml:"autoreplay-on-join" `
ChathistoryMax int ` yaml:"chathistory-maxmessages" `
ZNCMax int ` yaml:"znc-maxmessages" `
2020-02-19 01:38:42 +01:00
Restrictions struct {
2021-01-21 03:13:18 +01:00
ExpireTime custime . Duration ` yaml:"expire-time" `
// legacy key, superceded by QueryCutoff:
EnforceRegistrationDate_ bool ` yaml:"enforce-registration-date" `
QueryCutoff string ` yaml:"query-cutoff" `
queryCutoff HistoryCutoff
GracePeriod custime . Duration ` yaml:"grace-period" `
2020-02-19 01:38:42 +01:00
}
Persistent struct {
Enabled bool
UnregisteredChannels bool ` yaml:"unregistered-channels" `
RegisteredChannels PersistentStatus ` yaml:"registered-channels" `
DirectMessages PersistentStatus ` yaml:"direct-messages" `
}
2020-05-12 18:05:40 +02:00
Retention struct {
AllowIndividualDelete bool ` yaml:"allow-individual-delete" `
EnableAccountIndexing bool ` yaml:"enable-account-indexing" `
}
2020-07-10 00:36:45 +02:00
TagmsgStorage struct {
Default bool
Whitelist [ ] string
Blacklist [ ] string
} ` yaml:"tagmsg-storage" `
2018-11-26 11:23:27 +01:00
}
2017-09-28 07:30:53 +02:00
Filename string
2014-02-24 07:21:39 +01:00
}
2017-04-16 03:31:33 +02:00
// OperClass defines an assembled operator class.
2016-10-23 02:47:11 +02:00
type OperClass struct {
Title string
2022-03-30 06:44:51 +02:00
WhoisLine string ` yaml:"whois-line" `
Capabilities utils . HashSet [ string ] // map to make lookups much easier
2016-10-23 02:47:11 +02:00
}
2017-04-16 03:31:33 +02:00
// OperatorClasses returns a map of assembled operator classes from the given config.
2018-04-19 08:48:19 +02:00
func ( conf * Config ) OperatorClasses ( ) ( map [ string ] * OperClass , error ) {
2020-03-18 10:42:52 +01:00
fixupCapability := func ( capab string ) string {
2021-01-15 14:24:42 +01:00
return strings . TrimPrefix ( strings . TrimPrefix ( capab , "oper:" ) , "local_" ) // #868, #1442
2020-03-18 10:42:52 +01:00
}
2018-04-19 08:48:19 +02:00
ocs := make ( map [ string ] * OperClass )
2016-10-23 02:47:11 +02:00
// loop from no extends to most extended, breaking if we can't add any more
lenOfLastOcs := - 1
for {
if lenOfLastOcs == len ( ocs ) {
2020-08-03 18:55:52 +02:00
return nil , errors . New ( "OperClasses contains a looping dependency, or a class extends from a class that doesn't exist" )
2016-10-23 02:47:11 +02:00
}
lenOfLastOcs = len ( ocs )
var anyMissing bool
for name , info := range conf . OperClasses {
_ , exists := ocs [ name ]
_ , extendsExists := ocs [ info . Extends ]
if exists {
// class already exists
continue
} else if len ( info . Extends ) > 0 && ! extendsExists {
// class we extend on doesn't exist
_ , exists := conf . OperClasses [ info . Extends ]
if ! exists {
return nil , fmt . Errorf ( "Operclass [%s] extends [%s], which doesn't exist" , name , info . Extends )
}
anyMissing = true
continue
}
// create new operclass
var oc OperClass
2022-03-30 06:44:51 +02:00
oc . Capabilities = make ( utils . HashSet [ string ] )
2016-10-23 02:47:11 +02:00
// get inhereted info from other operclasses
if len ( info . Extends ) > 0 {
2019-05-10 07:44:14 +02:00
einfo := ocs [ info . Extends ]
2016-10-23 02:47:11 +02:00
for capab := range einfo . Capabilities {
2020-03-18 10:42:52 +01:00
oc . Capabilities . Add ( fixupCapability ( capab ) )
2016-10-23 02:47:11 +02:00
}
}
// add our own info
oc . Title = info . Title
2021-06-28 07:45:13 +02:00
if oc . Title == "" {
oc . Title = "IRC operator"
}
2016-10-23 02:47:11 +02:00
for _ , capab := range info . Capabilities {
2020-03-18 10:42:52 +01:00
oc . Capabilities . Add ( fixupCapability ( capab ) )
2016-10-23 02:47:11 +02:00
}
2016-10-23 03:01:05 +02:00
if len ( info . WhoisLine ) > 0 {
oc . WhoisLine = info . WhoisLine
} else {
oc . WhoisLine = "is a"
if strings . Contains ( strings . ToLower ( string ( oc . Title [ 0 ] ) ) , "aeiou" ) {
oc . WhoisLine += "n"
}
oc . WhoisLine += " "
oc . WhoisLine += oc . Title
}
2016-10-23 02:47:11 +02:00
2018-04-19 08:48:19 +02:00
ocs [ name ] = & oc
2016-10-23 02:47:11 +02:00
}
if ! anyMissing {
// we've got every operclass!
break
}
}
2018-04-19 08:48:19 +02:00
return ocs , nil
2016-10-23 02:47:11 +02:00
}
2017-04-16 03:31:33 +02:00
// Oper represents a single assembled operator's config.
2016-10-23 02:47:11 +02:00
type Oper struct {
2020-06-21 21:46:08 +02:00
Name string
Class * OperClass
WhoisLine string
Vhost string
Pass [ ] byte
Certfp string
Auto bool
2020-10-09 14:03:26 +02:00
Hidden bool
2020-06-21 21:46:08 +02:00
Modes [ ] modes . ModeChange
2016-10-23 02:47:11 +02:00
}
2021-01-15 15:26:34 +01:00
func ( oper * Oper ) HasRoleCapab ( capab string ) bool {
return oper != nil && oper . Class . Capabilities . Has ( capab )
}
2017-04-16 03:31:33 +02:00
// Operators returns a map of operator configs from the given OperClass and config.
2018-04-19 08:48:19 +02:00
func ( conf * Config ) Operators ( oc map [ string ] * OperClass ) ( map [ string ] * Oper , error ) {
operators := make ( map [ string ] * Oper )
2016-10-23 02:47:11 +02:00
for name , opConf := range conf . Opers {
var oper Oper
// oper name
2016-10-11 15:51:46 +02:00
name , err := CasefoldName ( name )
2016-10-23 02:47:11 +02:00
if err != nil {
return nil , fmt . Errorf ( "Could not casefold oper name: %s" , err . Error ( ) )
}
2018-04-19 08:48:19 +02:00
oper . Name = name
2016-10-23 02:47:11 +02:00
2019-12-19 12:33:43 +01:00
if opConf . Password != "" {
oper . Pass , err = decodeLegacyPasswordHash ( opConf . Password )
if err != nil {
2019-12-19 15:30:49 +01:00
return nil , fmt . Errorf ( "Oper %s has an invalid password hash: %s" , oper . Name , err . Error ( ) )
2019-12-19 12:33:43 +01:00
}
2018-08-06 04:51:39 +02:00
}
2020-06-21 21:46:08 +02:00
certfp := opConf . Certfp
if certfp == "" && opConf . Fingerprint != nil {
certfp = * opConf . Fingerprint
}
if certfp != "" {
oper . Certfp , err = utils . NormalizeCertfp ( certfp )
2019-12-29 17:59:49 +01:00
if err != nil {
return nil , fmt . Errorf ( "Oper %s has an invalid fingerprint: %s" , oper . Name , err . Error ( ) )
}
}
2019-12-19 12:33:43 +01:00
oper . Auto = opConf . Auto
2020-10-09 14:03:26 +02:00
oper . Hidden = opConf . Hidden
2019-12-19 12:33:43 +01:00
2020-06-21 21:46:08 +02:00
if oper . Pass == nil && oper . Certfp == "" {
2019-12-19 12:33:43 +01:00
return nil , fmt . Errorf ( "Oper %s has neither a password nor a fingerprint" , name )
}
2018-08-06 04:51:39 +02:00
2016-10-23 03:28:31 +02:00
oper . Vhost = opConf . Vhost
2021-06-29 16:06:37 +02:00
if oper . Vhost != "" && ! conf . Accounts . VHosts . validRegexp . MatchString ( oper . Vhost ) {
return nil , fmt . Errorf ( "Oper %s has an invalid vhost: `%s`" , name , oper . Vhost )
}
2018-04-19 08:48:19 +02:00
class , exists := oc [ opConf . Class ]
2016-10-23 02:47:11 +02:00
if ! exists {
return nil , fmt . Errorf ( "Could not load operator [%s] - they use operclass [%s] which does not exist" , name , opConf . Class )
2016-10-11 15:51:46 +02:00
}
2018-04-19 08:48:19 +02:00
oper . Class = class
2016-10-23 03:01:05 +02:00
if len ( opConf . WhoisLine ) > 0 {
oper . WhoisLine = opConf . WhoisLine
} else {
oper . WhoisLine = class . WhoisLine
}
2018-04-23 00:47:10 +02:00
modeStr := strings . TrimSpace ( opConf . Modes )
modeChanges , unknownChanges := modes . ParseUserModeChanges ( strings . Split ( modeStr , " " ) ... )
if len ( unknownChanges ) > 0 {
return nil , fmt . Errorf ( "Could not load operator [%s] due to unknown modes %v" , name , unknownChanges )
}
oper . Modes = modeChanges
2016-10-23 02:47:11 +02:00
// successful, attach to list of opers
2018-04-19 08:48:19 +02:00
operators [ name ] = & oper
2014-02-24 18:41:09 +01:00
}
2016-10-23 02:47:11 +02:00
return operators , nil
2014-02-24 18:41:09 +01:00
}
2021-04-08 04:49:33 +02:00
func loadTlsConfig ( config listenerConfigBlock ) ( tlsConfig * tls . Config , err error ) {
var certificates [ ] tls . Certificate
if len ( config . TLSCertificates ) != 0 {
// SNI configuration with multiple certificates
for _ , certPairConf := range config . TLSCertificates {
cert , err := loadCertWithLeaf ( certPairConf . Cert , certPairConf . Key )
if err != nil {
return nil , err
}
certificates = append ( certificates , cert )
}
} else if config . TLS . Cert != "" {
// normal configuration with one certificate
cert , err := loadCertWithLeaf ( config . TLS . Cert , config . TLS . Key )
if err != nil {
return nil , err
}
certificates = append ( certificates , cert )
} else {
// plaintext!
return nil , nil
2019-06-18 04:21:37 +02:00
}
2020-05-05 10:07:57 +02:00
clientAuth := tls . RequestClientCert
2021-04-08 04:49:33 +02:00
if config . WebSocket {
2020-05-05 10:07:57 +02:00
// if Chrome receives a server request for a client certificate
// on a websocket connection, it will immediately disconnect:
// https://bugs.chromium.org/p/chromium/issues/detail?id=329884
// work around this behavior:
clientAuth = tls . NoClientCert
}
2019-06-28 16:45:34 +02:00
result := tls . Config {
2021-04-08 04:49:33 +02:00
Certificates : certificates ,
2020-05-05 10:07:57 +02:00
ClientAuth : clientAuth ,
2021-04-19 02:31:11 +02:00
MinVersion : tlsMinVersionFromString ( config . MinTLSVersion ) ,
2019-06-28 16:45:34 +02:00
}
return & result , nil
}
2019-06-18 04:21:37 +02:00
2021-04-19 02:31:11 +02:00
func tlsMinVersionFromString ( version string ) uint16 {
version = strings . ToLower ( version )
version = strings . TrimPrefix ( version , "v" )
switch version {
case "1" , "1.0" :
return tls . VersionTLS10
case "1.1" :
return tls . VersionTLS11
case "1.2" :
return tls . VersionTLS12
case "1.3" :
return tls . VersionTLS13
default :
// tls package will fill in a sane value, currently 1.0
return 0
}
}
2021-04-08 04:35:54 +02:00
func loadCertWithLeaf ( certFile , keyFile string ) ( cert tls . Certificate , err error ) {
// LoadX509KeyPair: "On successful return, Certificate.Leaf will be nil because
// the parsed form of the certificate is not retained." tls.Config:
// "Note: if there are multiple Certificates, and they don't have the
// optional field Leaf set, certificate selection will incur a significant
// per-handshake performance cost."
cert , err = tls . LoadX509KeyPair ( certFile , keyFile )
if err != nil {
return
}
cert . Leaf , err = x509 . ParseCertificate ( cert . Certificate [ 0 ] )
return
}
2019-06-28 16:45:34 +02:00
// prepareListeners populates Config.Server.trueListeners
func ( conf * Config ) prepareListeners ( ) ( err error ) {
2020-02-21 10:11:47 +01:00
if len ( conf . Server . Listeners ) == 0 {
return fmt . Errorf ( "No listeners were configured" )
}
2020-05-05 04:29:10 +02:00
conf . Server . trueListeners = make ( map [ string ] utils . ListenerConfig )
2020-02-21 10:11:47 +01:00
for addr , block := range conf . Server . Listeners {
2020-05-05 04:29:10 +02:00
var lconf utils . ListenerConfig
2020-05-05 23:20:50 +02:00
lconf . ProxyDeadline = RegisterTimeout
2020-02-21 10:11:47 +01:00
lconf . Tor = block . Tor
lconf . STSOnly = block . STSOnly
if lconf . STSOnly && ! conf . Server . STS . Enabled {
return fmt . Errorf ( "%s is configured as a STS-only listener, but STS is disabled" , addr )
2019-06-28 16:45:34 +02:00
}
2021-04-08 04:49:33 +02:00
lconf . TLSConfig , err = loadTlsConfig ( block )
if err != nil {
return & CertKeyError { Err : err }
2016-04-13 12:45:09 +02:00
}
2020-11-19 18:31:58 +01:00
lconf . RequireProxy = block . TLS . Proxy || block . Proxy
2020-04-30 21:35:17 +02:00
lconf . WebSocket = block . WebSocket
2021-01-15 12:19:13 +01:00
if lconf . WebSocket && ! conf . Server . EnforceUtf8 {
return fmt . Errorf ( "enabling a websocket listener requires the use of server.enforce-utf8" )
}
2020-12-06 05:06:23 +01:00
lconf . HideSTS = block . HideSTS
2020-02-21 10:11:47 +01:00
conf . Server . trueListeners [ addr ] = lconf
2016-04-13 12:45:09 +02:00
}
2019-06-18 04:21:37 +02:00
return nil
2016-04-13 12:45:09 +02:00
}
2020-06-15 20:16:02 +02:00
func ( config * Config ) processExtjwt ( ) ( err error ) {
// first process the default service, which may be disabled
err = config . Extjwt . Default . Postprocess ( )
if err != nil {
return
}
// now process the named services. it is an error if any is disabled
// also, normalize the service names to lowercase
services := make ( map [ string ] jwt . JwtServiceConfig , len ( config . Extjwt . Services ) )
for service , sConf := range config . Extjwt . Services {
err := sConf . Postprocess ( )
if err != nil {
return err
}
if ! sConf . Enabled ( ) {
return fmt . Errorf ( "no keys enabled for extjwt service %s" , service )
}
services [ strings . ToLower ( service ) ] = sConf
}
config . Extjwt . Services = services
return nil
}
2019-06-28 16:45:34 +02:00
// LoadRawConfig loads the config without doing any consistency checks or postprocessing
func LoadRawConfig ( filename string ) ( config * Config , err error ) {
2021-02-17 21:11:54 +01:00
data , err := os . ReadFile ( filename )
2014-02-09 16:53:42 +01:00
if err != nil {
2016-04-12 15:00:09 +02:00
return nil , err
2014-02-09 16:53:42 +01:00
}
2016-04-12 15:00:09 +02:00
err = yaml . Unmarshal ( data , & config )
if err != nil {
return nil , err
}
2019-06-28 16:45:34 +02:00
return
}
2020-10-25 18:58:57 +01:00
// convert, e.g., "ALLOWED_ORIGINS" to "allowed-origins"
func screamingSnakeToKebab ( in string ) ( out string ) {
var buf strings . Builder
for i := 0 ; i < len ( in ) ; i ++ {
c := in [ i ]
switch {
case c == '_' :
buf . WriteByte ( '-' )
case 'A' <= c && c <= 'Z' :
buf . WriteByte ( c + ( 'a' - 'A' ) )
default :
buf . WriteByte ( c )
}
}
return buf . String ( )
}
func isExported ( field reflect . StructField ) bool {
return field . PkgPath == "" // https://golang.org/pkg/reflect/#StructField
}
// errors caused by config overrides
type configPathError struct {
name string
desc string
fatalErr error
}
func ( ce * configPathError ) Error ( ) string {
if ce . fatalErr != nil {
return fmt . Sprintf ( "Couldn't apply config override `%s`: %s: %v" , ce . name , ce . desc , ce . fatalErr )
}
return fmt . Sprintf ( "Couldn't apply config override `%s`: %s" , ce . name , ce . desc )
}
2024-02-25 16:05:36 +01:00
func mungeFromEnvironment ( config * Config , envPair string ) ( applied bool , name string , err * configPathError ) {
2020-10-25 18:58:57 +01:00
equalIdx := strings . IndexByte ( envPair , '=' )
name , value := envPair [ : equalIdx ] , envPair [ equalIdx + 1 : ]
2021-05-25 06:34:38 +02:00
if strings . HasPrefix ( name , "ERGO__" ) {
name = strings . TrimPrefix ( name , "ERGO__" )
} else if strings . HasPrefix ( name , "ORAGONO__" ) {
name = strings . TrimPrefix ( name , "ORAGONO__" )
} else {
2024-02-25 16:05:36 +01:00
return false , "" , nil
2020-10-25 18:58:57 +01:00
}
pathComponents := strings . Split ( name , "__" )
for i , pathComponent := range pathComponents {
pathComponents [ i ] = screamingSnakeToKebab ( pathComponent )
}
v := reflect . Indirect ( reflect . ValueOf ( config ) )
t := v . Type ( )
for _ , component := range pathComponents {
if component == "" {
2024-02-25 16:05:36 +01:00
return false , "" , & configPathError { name , "invalid" , nil }
2020-10-25 18:58:57 +01:00
}
if v . Kind ( ) != reflect . Struct {
2024-02-25 16:05:36 +01:00
return false , "" , & configPathError { name , "index into non-struct" , nil }
2020-10-25 18:58:57 +01:00
}
var nextField reflect . StructField
success := false
n := t . NumField ( )
// preferentially get a field with an exact yaml tag match,
// then fall back to case-insensitive comparison of field names
for i := 0 ; i < n ; i ++ {
field := t . Field ( i )
if isExported ( field ) && field . Tag . Get ( "yaml" ) == component {
nextField = field
success = true
break
}
}
if ! success {
for i := 0 ; i < n ; i ++ {
field := t . Field ( i )
if isExported ( field ) && strings . ToLower ( field . Name ) == component {
nextField = field
success = true
break
}
}
}
if ! success {
2024-02-25 16:05:36 +01:00
return false , "" , & configPathError { name , fmt . Sprintf ( "couldn't resolve path component: `%s`" , component ) , nil }
2020-10-25 18:58:57 +01:00
}
v = v . FieldByName ( nextField . Name )
// dereference pointer field if necessary, initialize new value if necessary
if v . Kind ( ) == reflect . Ptr {
if v . IsNil ( ) {
v . Set ( reflect . New ( v . Type ( ) . Elem ( ) ) )
}
v = reflect . Indirect ( v )
}
t = v . Type ( )
}
yamlErr := yaml . Unmarshal ( [ ] byte ( value ) , v . Addr ( ) . Interface ( ) )
if yamlErr != nil {
2024-02-25 16:05:36 +01:00
return false , "" , & configPathError { name , "couldn't deserialize YAML" , yamlErr }
2020-10-25 18:58:57 +01:00
}
2024-02-25 16:05:36 +01:00
return true , name , nil
2020-10-25 18:58:57 +01:00
}
2019-06-28 16:45:34 +02:00
// LoadConfig loads the given YAML configuration file.
func LoadConfig ( filename string ) ( config * Config , err error ) {
config , err = LoadRawConfig ( filename )
if err != nil {
return nil , err
}
2016-04-12 15:00:09 +02:00
2020-10-25 18:58:57 +01:00
if config . AllowEnvironmentOverrides {
for _ , envPair := range os . Environ ( ) {
2024-02-25 16:05:36 +01:00
applied , name , envErr := mungeFromEnvironment ( config , envPair )
2020-10-25 18:58:57 +01:00
if envErr != nil {
if envErr . fatalErr != nil {
return nil , envErr
} else {
log . Println ( envErr . Error ( ) )
}
} else if applied {
2024-02-25 16:05:36 +01:00
log . Printf ( "applied environment override: %s\n" , name )
2020-10-25 18:58:57 +01:00
}
}
}
2017-09-28 07:30:53 +02:00
config . Filename = filename
2016-04-12 07:44:00 +02:00
if config . Network . Name == "" {
2020-08-03 18:55:52 +02:00
return nil , errors . New ( "Network name missing" )
2016-04-12 07:44:00 +02:00
}
2014-03-01 23:34:51 +01:00
if config . Server . Name == "" {
2020-08-03 18:55:52 +02:00
return nil , errors . New ( "Server name missing" )
2014-03-01 23:34:51 +01:00
}
2019-12-18 15:21:45 +01:00
if ! utils . IsServerName ( config . Server . Name ) {
2020-08-03 18:55:52 +02:00
return nil , errors . New ( "Server name must match the format of a hostname" )
2016-04-21 02:48:15 +02:00
}
2019-12-20 00:41:46 +01:00
config . Server . nameCasefolded = strings . ToLower ( config . Server . Name )
2016-08-19 15:21:52 +02:00
if config . Datastore . Path == "" {
2020-08-03 18:55:52 +02:00
return nil , errors . New ( "Datastore path missing" )
2016-08-19 15:21:52 +02:00
}
2019-02-03 10:24:59 +01:00
//dan: automagically fix identlen until a few releases in the future (from now, 0.12.0), being a newly-introduced limit
if config . Limits . IdentLen < 1 {
2019-02-03 20:01:46 +01:00
config . Limits . IdentLen = 20
2019-02-03 10:24:59 +01:00
}
2016-10-16 12:14:56 +02:00
if config . Limits . NickLen < 1 || config . Limits . ChannelLen < 2 || config . Limits . AwayLen < 1 || config . Limits . KickLen < 1 || config . Limits . TopicLen < 1 {
2020-08-03 18:55:52 +02:00
return nil , errors . New ( "One or more limits values are too low" )
2016-08-12 14:20:32 +02:00
}
2019-05-22 22:15:59 +02:00
if config . Limits . RegistrationMessages == 0 {
config . Limits . RegistrationMessages = 1024
}
2021-05-24 06:38:47 +02:00
if config . Server . MaxLineLen < DefaultMaxLineLen {
config . Server . MaxLineLen = DefaultMaxLineLen
}
2020-02-21 00:19:17 +01:00
if config . Datastore . MySQL . Enabled {
if config . Limits . NickLen > mysql . MaxTargetLength || config . Limits . ChannelLen > mysql . MaxTargetLength {
return nil , fmt . Errorf ( "to use MySQL, nick and channel length limits must be %d or lower" , mysql . MaxTargetLength )
}
}
2019-08-27 06:51:09 +02:00
2020-10-20 19:48:19 +02:00
if config . Server . CoerceIdent != "" {
if config . Server . CheckIdent {
return nil , errors . New ( "Can't configure both check-ident and coerce-ident" )
}
if config . Server . CoerceIdent [ 0 ] != '~' {
return nil , errors . New ( "coerce-ident value must start with a ~" )
}
if ! isIdent ( config . Server . CoerceIdent [ 1 : ] ) {
return nil , errors . New ( "coerce-ident must be valid as an IRC user/ident field" )
}
2020-09-07 11:59:31 +02:00
}
2019-08-27 06:51:09 +02:00
config . Server . supportedCaps = caps . NewCompleteSet ( )
config . Server . capValues = make ( caps . Values )
err = config . prepareListeners ( )
if err != nil {
return nil , fmt . Errorf ( "failed to prepare listeners: %v" , err )
}
2020-05-05 10:07:19 +02:00
for _ , glob := range config . Server . WebSockets . AllowedOrigins {
2020-05-13 09:41:00 +02:00
globre , err := utils . CompileGlob ( glob , false )
2020-05-05 04:29:10 +02:00
if err != nil {
return nil , fmt . Errorf ( "invalid websocket allowed-origin expression: %s" , glob )
}
2020-05-05 10:07:19 +02:00
config . Server . WebSockets . allowedOriginRegexps = append ( config . Server . WebSockets . allowedOriginRegexps , globre )
2020-05-05 04:29:10 +02:00
}
2017-03-09 10:07:35 +01:00
if config . Server . STS . Enabled {
if config . Server . STS . Port < 0 || config . Server . STS . Port > 65535 {
return nil , fmt . Errorf ( "STS port is incorrect, should be 0 if disabled: %d" , config . Server . STS . Port )
}
2019-08-27 06:51:09 +02:00
if config . Server . STS . STSOnlyBanner != "" {
2020-01-19 05:47:05 +01:00
for _ , line := range strings . Split ( config . Server . STS . STSOnlyBanner , "\n" ) {
config . Server . STS . bannerLines = append ( config . Server . STS . bannerLines , strings . TrimSpace ( line ) )
}
2019-08-27 06:51:09 +02:00
} else {
config . Server . STS . bannerLines = [ ] string { fmt . Sprintf ( "This server is only accessible over TLS. Please reconnect using TLS on port %d." , config . Server . STS . Port ) }
}
} else {
config . Server . supportedCaps . Disable ( caps . STS )
config . Server . STS . Duration = 0
2017-03-09 10:07:35 +01:00
}
2019-08-27 06:51:09 +02:00
// set this even if STS is disabled
config . Server . capValues [ caps . STS ] = config . Server . STS . Value ( )
2020-03-19 22:09:52 +01:00
config . Server . lookupHostnames = utils . BoolDefaultTrue ( config . Server . LookupHostnames )
2019-12-17 21:10:23 +01:00
2017-10-15 08:18:14 +02:00
// process webirc blocks
var newWebIRC [ ] webircConfig
for _ , webirc := range config . Server . WebIRC {
// skip webirc blocks with no hosts (such as the example one)
2017-10-15 10:15:18 +02:00
if len ( webirc . Hosts ) == 0 {
2017-10-15 08:18:14 +02:00
continue
}
2017-10-16 00:47:49 +02:00
err = webirc . Populate ( )
2017-10-15 08:18:14 +02:00
if err != nil {
return nil , fmt . Errorf ( "Could not parse WebIRC config: %s" , err . Error ( ) )
}
newWebIRC = append ( newWebIRC , webirc )
}
config . Server . WebIRC = newWebIRC
2019-08-27 06:51:09 +02:00
2019-12-23 21:26:37 +01:00
if config . Limits . Multiline . MaxBytes <= 0 {
config . Server . supportedCaps . Disable ( caps . Multiline )
} else {
var multilineCapValue string
if config . Limits . Multiline . MaxLines == 0 {
multilineCapValue = fmt . Sprintf ( "max-bytes=%d" , config . Limits . Multiline . MaxBytes )
} else {
multilineCapValue = fmt . Sprintf ( "max-bytes=%d,max-lines=%d" , config . Limits . Multiline . MaxBytes , config . Limits . Multiline . MaxLines )
}
config . Server . capValues [ caps . Multiline ] = multilineCapValue
}
2020-02-21 06:02:23 +01:00
// handle legacy name 'bouncer' for 'multiclient' section:
if config . Accounts . Bouncer != nil {
config . Accounts . Multiclient = * config . Accounts . Bouncer
}
2020-02-21 05:55:42 +01:00
if ! config . Accounts . Multiclient . Enabled {
config . Accounts . Multiclient . AlwaysOn = PersistentDisabled
} else if config . Accounts . Multiclient . AlwaysOn >= PersistentOptOut {
config . Accounts . Multiclient . AllowedByDefault = true
2019-09-01 03:29:23 +02:00
}
2020-12-08 08:06:56 +01:00
if ! config . Accounts . NickReservation . Enabled {
config . Accounts . NickReservation . ForceNickEqualsAccount = false
}
2020-05-28 07:18:19 +02:00
if config . Accounts . NickReservation . ForceNickEqualsAccount && ! config . Accounts . Multiclient . Enabled {
return nil , errors . New ( "force-nick-equals-account requires enabling multiclient as well" )
}
2020-03-16 12:54:50 +01:00
// handle guest format, including the legacy key rename-prefix
if config . Accounts . NickReservation . GuestFormat == "" {
renamePrefix := config . Accounts . NickReservation . RenamePrefix
if renamePrefix == "" {
renamePrefix = "Guest-"
}
config . Accounts . NickReservation . GuestFormat = renamePrefix + "*"
}
config . Accounts . NickReservation . guestRegexp , config . Accounts . NickReservation . guestRegexpFolded , err = compileGuestRegexp ( config . Accounts . NickReservation . GuestFormat , config . Server . Casemapping )
if err != nil {
return nil , err
}
2017-10-02 05:31:40 +02:00
var newLogConfigs [ ] logger . LoggingConfig
2017-03-06 04:31:10 +01:00
for _ , logConfig := range config . Logging {
// methods
2017-03-10 13:02:08 +01:00
methods := make ( map [ string ] bool )
2017-03-06 04:31:10 +01:00
for _ , method := range strings . Split ( logConfig . Method , " " ) {
if len ( method ) > 0 {
2017-03-10 13:02:08 +01:00
methods [ strings . ToLower ( method ) ] = true
2017-03-06 04:31:10 +01:00
}
}
2017-03-10 13:02:08 +01:00
if methods [ "file" ] && logConfig . Filename == "" {
2020-08-03 18:55:52 +02:00
return nil , errors . New ( "Logging configuration specifies 'file' method but 'filename' is empty" )
2017-03-06 04:31:10 +01:00
}
2017-03-10 13:02:08 +01:00
logConfig . MethodFile = methods [ "file" ]
2017-05-01 10:51:37 +02:00
logConfig . MethodStdout = methods [ "stdout" ]
2017-03-10 13:02:08 +01:00
logConfig . MethodStderr = methods [ "stderr" ]
2017-03-06 04:31:10 +01:00
// levels
2017-03-10 13:02:08 +01:00
level , exists := logger . LogLevelNames [ strings . ToLower ( logConfig . LevelString ) ]
2017-03-06 04:31:10 +01:00
if ! exists {
return nil , fmt . Errorf ( "Could not translate log leve [%s]" , logConfig . LevelString )
}
logConfig . Level = level
// types
for _ , typeStr := range strings . Split ( logConfig . TypeString , " " ) {
if len ( typeStr ) == 0 {
continue
}
if typeStr == "-" {
2020-08-03 18:55:52 +02:00
return nil , errors . New ( "Encountered logging type '-' with no type to exclude" )
2017-03-06 04:31:10 +01:00
}
if typeStr [ 0 ] == '-' {
typeStr = typeStr [ 1 : ]
2017-03-10 13:02:08 +01:00
logConfig . ExcludedTypes = append ( logConfig . ExcludedTypes , typeStr )
2017-03-06 04:31:10 +01:00
} else {
2017-03-10 13:02:08 +01:00
logConfig . Types = append ( logConfig . Types , typeStr )
2017-03-06 04:31:10 +01:00
}
}
if len ( logConfig . Types ) < 1 {
2020-08-03 18:55:52 +02:00
return nil , errors . New ( "Logger has no types to log" )
2017-03-06 04:31:10 +01:00
}
2017-03-06 06:50:23 +01:00
newLogConfigs = append ( newLogConfigs , logConfig )
2017-03-06 04:31:10 +01:00
}
2017-03-06 06:50:23 +01:00
config . Logging = newLogConfigs
2016-10-23 02:47:11 +02:00
2020-10-07 00:04:29 +02:00
if config . Accounts . Registration . EmailVerification . Enabled {
err := config . Accounts . Registration . EmailVerification . Postprocess ( config . Server . Name )
2020-04-05 09:48:59 +02:00
if err != nil {
return nil , err
}
2020-10-07 00:04:29 +02:00
} else {
// TODO: this processes the legacy "callback" config, clean this up in 2.5 or later
// TODO: also clean up the legacy "inline" MTA config format (from ee05a4324dfde)
mailtoEnabled := false
for _ , name := range config . Accounts . Registration . LegacyEnabledCallbacks {
if name == "mailto" {
mailtoEnabled = true
break
}
}
if mailtoEnabled {
config . Accounts . Registration . EmailVerification = config . Accounts . Registration . LegacyCallbacks . Mailto
config . Accounts . Registration . EmailVerification . Enabled = true
err := config . Accounts . Registration . EmailVerification . Postprocess ( config . Server . Name )
if err != nil {
return nil , err
}
}
2020-04-05 09:48:59 +02:00
}
2020-04-30 05:43:55 +02:00
config . Accounts . defaultUserModes = ParseDefaultUserModes ( config . Accounts . DefaultUserModes )
2021-04-26 01:22:08 +02:00
if config . Server . Password != "" {
config . Server . passwordBytes , err = decodeLegacyPasswordHash ( config . Server . Password )
if err != nil {
return nil , err
}
if config . Accounts . LoginViaPassCommand && ! config . Accounts . SkipServerPassword {
return nil , errors . New ( "Using a server password and login-via-pass-command requires skip-server-password as well" )
}
// #1634: accounts.registration.allow-before-connect is an auth bypass
// for configurations that start from default and then enable server.password
config . Accounts . Registration . AllowBeforeConnect = false
}
2021-04-26 16:26:16 +02:00
if config . Accounts . RequireSasl . Enabled {
// minor gotcha: Tor listeners will typically be loopback and
// therefore exempted from require-sasl. if require-sasl is enabled
// for non-Tor (non-local) connections, enable it for Tor as well:
config . Server . TorListeners . RequireSasl = true
}
2019-02-05 06:19:03 +01:00
config . Accounts . RequireSasl . exemptedNets , err = utils . ParseNetList ( config . Accounts . RequireSasl . Exempted )
if err != nil {
return nil , fmt . Errorf ( "Could not parse require-sasl exempted nets: %v" , err . Error ( ) )
}
config . Server . proxyAllowedFromNets , err = utils . ParseNetList ( config . Server . ProxyAllowedFrom )
if err != nil {
return nil , fmt . Errorf ( "Could not parse proxy-allowed-from nets: %v" , err . Error ( ) )
}
2020-02-19 01:38:42 +01:00
config . Server . secureNets , err = utils . ParseNetList ( config . Server . SecureNetDefs )
if err != nil {
return nil , fmt . Errorf ( "Could not parse secure-nets: %v\n" , err . Error ( ) )
}
2018-04-23 08:38:35 +02:00
rawRegexp := config . Accounts . VHosts . ValidRegexpRaw
2018-04-19 08:48:19 +02:00
if rawRegexp != "" {
regexp , err := regexp . Compile ( rawRegexp )
if err == nil {
2020-10-22 18:19:19 +02:00
config . Accounts . VHosts . validRegexp = regexp
2018-04-19 08:48:19 +02:00
} else {
log . Printf ( "invalid vhost regexp: %s\n" , err . Error ( ) )
}
}
2020-10-22 18:19:19 +02:00
if config . Accounts . VHosts . validRegexp == nil {
config . Accounts . VHosts . validRegexp = defaultValidVhostRegex
2018-04-19 08:48:19 +02:00
}
2024-02-14 00:58:32 +01:00
if config . Accounts . AuthenticationEnabled {
saslCapValues := [ ] string { "PLAIN" , "EXTERNAL" }
if config . Accounts . AdvertiseSCRAM {
saslCapValues = append ( saslCapValues , "SCRAM-SHA-256" )
}
if config . Accounts . OAuth2 . Enabled {
saslCapValues = append ( saslCapValues , "OAUTHBEARER" )
}
2024-05-28 02:40:04 +02:00
if config . Accounts . OAuth2 . Enabled || config . Accounts . JWTAuth . Enabled {
saslCapValues = append ( saslCapValues , "IRCV3BEARER" )
}
2024-02-14 00:58:32 +01:00
config . Server . capValues [ caps . SASL ] = strings . Join ( saslCapValues , "," )
} else {
2019-08-27 06:51:09 +02:00
config . Server . supportedCaps . Disable ( caps . SASL )
}
2024-02-14 00:58:32 +01:00
if err := config . Accounts . OAuth2 . Postprocess ( ) ; err != nil {
return nil , err
}
if err := config . Accounts . JWTAuth . Postprocess ( ) ; err != nil {
return nil , err
}
if config . Accounts . OAuth2 . Enabled && config . Accounts . OAuth2 . AuthScript && ! config . Accounts . AuthScript . Enabled {
return nil , fmt . Errorf ( "oauth2 is enabled with auth-script, but no auth-script is enabled" )
}
2020-10-07 00:04:29 +02:00
if ! config . Accounts . Registration . Enabled {
2021-07-07 13:37:46 +02:00
config . Server . supportedCaps . Disable ( caps . AccountRegistration )
2020-10-07 00:04:29 +02:00
} else {
var registerValues [ ] string
if config . Accounts . Registration . AllowBeforeConnect {
registerValues = append ( registerValues , "before-connect" )
}
if config . Accounts . Registration . EmailVerification . Enabled {
registerValues = append ( registerValues , "email-required" )
}
if config . Accounts . RequireSasl . Enabled {
registerValues = append ( registerValues , "account-required" )
}
if len ( registerValues ) != 0 {
2021-07-07 13:37:46 +02:00
config . Server . capValues [ caps . AccountRegistration ] = strings . Join ( registerValues , "," )
2020-10-07 00:04:29 +02:00
}
}
2018-03-18 02:32:12 +01:00
maxSendQBytes , err := bytefmt . ToBytes ( config . Server . MaxSendQString )
2017-03-13 23:12:39 +01:00
if err != nil {
return nil , fmt . Errorf ( "Could not parse maximum SendQ size (make sure it only contains whole numbers): %s" , err . Error ( ) )
}
2018-03-18 02:32:12 +01:00
config . Server . MaxSendQBytes = int ( maxSendQBytes )
2017-03-13 23:12:39 +01:00
2019-02-19 08:54:57 +01:00
config . languageManager , err = languages . NewManager ( config . Languages . Enabled , config . Languages . Path , config . Languages . Default )
if err != nil {
return nil , fmt . Errorf ( "Could not load languages: %s" , err . Error ( ) )
2018-01-22 08:30:31 +01:00
}
2019-08-27 06:51:09 +02:00
config . Server . capValues [ caps . Languages ] = config . languageManager . CapValue ( )
2018-01-22 08:30:31 +01:00
2022-08-23 05:23:17 +02:00
if len ( config . Fakelag . CommandBudgets ) != 0 {
// normalize command names to uppercase:
commandBudgets := make ( map [ string ] int , len ( config . Fakelag . CommandBudgets ) )
for command , budget := range config . Fakelag . CommandBudgets {
commandBudgets [ strings . ToUpper ( command ) ] = budget
}
config . Fakelag . CommandBudgets = commandBudgets
} else {
config . Fakelag . CommandBudgets = nil
}
2020-09-09 10:01:46 +02:00
if config . Server . Relaymsg . Enabled {
2020-06-08 07:17:45 +02:00
for _ , char := range protocolBreakingNameCharacters {
2020-09-09 10:01:46 +02:00
if strings . ContainsRune ( config . Server . Relaymsg . Separators , char ) {
return nil , fmt . Errorf ( "RELAYMSG separators cannot include the characters %s" , protocolBreakingNameCharacters )
2020-06-08 07:17:45 +02:00
}
}
2020-09-09 10:01:46 +02:00
config . Server . capValues [ caps . Relaymsg ] = config . Server . Relaymsg . Separators
2020-06-08 07:17:45 +02:00
} else {
config . Server . supportedCaps . Disable ( caps . Relaymsg )
}
2020-06-08 02:19:28 +02:00
2020-03-19 22:09:52 +01:00
config . Debug . recoverFromErrors = utils . BoolDefaultTrue ( config . Debug . RecoverFromErrors )
2018-07-16 10:08:24 +02:00
2018-07-16 09:46:40 +02:00
// process operator definitions, store them to config.operators
operclasses , err := config . OperatorClasses ( )
if err != nil {
return nil , err
}
opers , err := config . Operators ( operclasses )
if err != nil {
return nil , err
}
config . operators = opers
// parse default channel modes
2018-08-28 19:34:43 +02:00
config . Channels . defaultModes = ParseDefaultChannelModes ( config . Channels . DefaultModes )
2018-07-16 09:46:40 +02:00
2018-08-06 04:51:39 +02:00
if config . Accounts . Registration . BcryptCost == 0 {
config . Accounts . Registration . BcryptCost = passwd . DefaultCost
2018-07-16 09:46:40 +02:00
}
2019-02-06 10:55:05 +01:00
if config . Channels . MaxChannelsPerClient == 0 {
config . Channels . MaxChannelsPerClient = 100
}
2019-02-06 10:32:04 +01:00
if config . Channels . Registration . MaxChannelsPerAccount == 0 {
2019-02-06 21:47:20 +01:00
config . Channels . Registration . MaxChannelsPerAccount = 15
2019-02-06 10:32:04 +01:00
}
2020-03-19 22:09:52 +01:00
config . Server . Compatibility . forceTrailing = utils . BoolDefaultTrue ( config . Server . Compatibility . ForceTrailing )
2021-03-05 04:29:34 +01:00
config . Server . Compatibility . allowTruncation = utils . BoolDefaultTrue ( config . Server . Compatibility . AllowTruncation )
2019-05-09 20:18:30 +02:00
2019-05-10 06:07:22 +02:00
config . loadMOTD ( )
2018-11-26 11:23:27 +01:00
// in the current implementation, we disable history by creating a history buffer
// with zero capacity. but the `enabled` config option MUST be respected regardless
// of this detail
if ! config . History . Enabled {
config . History . ChannelLength = 0
config . History . ClientLength = 0
2021-02-21 19:25:22 +01:00
config . Server . supportedCaps . Disable ( caps . Chathistory )
config . Server . supportedCaps . Disable ( caps . EventPlayback )
config . Server . supportedCaps . Disable ( caps . ZNCPlayback )
2018-11-26 11:23:27 +01:00
}
2020-02-19 01:38:42 +01:00
if ! config . History . Enabled || ! config . History . Persistent . Enabled {
2020-08-24 00:10:01 +02:00
config . History . Persistent . Enabled = false
2020-02-19 01:38:42 +01:00
config . History . Persistent . UnregisteredChannels = false
config . History . Persistent . RegisteredChannels = PersistentDisabled
config . History . Persistent . DirectMessages = PersistentDisabled
}
2020-08-24 00:10:01 +02:00
if config . History . Persistent . Enabled && ! config . Datastore . MySQL . Enabled {
return nil , fmt . Errorf ( "You must configure a MySQL server in order to enable persistent history" )
}
2020-02-19 01:38:42 +01:00
if config . History . ZNCMax == 0 {
config . History . ZNCMax = config . History . ChathistoryMax
}
2021-01-21 03:13:18 +01:00
if config . History . Restrictions . QueryCutoff != "" {
config . History . Restrictions . queryCutoff , err = historyCutoffFromString ( config . History . Restrictions . QueryCutoff )
if err != nil {
return nil , fmt . Errorf ( "invalid value of history.query-restrictions: %w" , err )
}
} else {
if config . History . Restrictions . EnforceRegistrationDate_ {
config . History . Restrictions . queryCutoff = HistoryCutoffRegistrationTime
} else {
config . History . Restrictions . queryCutoff = HistoryCutoffNone
}
}
2020-03-19 22:09:52 +01:00
config . Roleplay . addSuffix = utils . BoolDefaultTrue ( config . Roleplay . AddSuffix )
2020-02-21 00:33:48 +01:00
config . Datastore . MySQL . ExpireTime = time . Duration ( config . History . Restrictions . ExpireTime )
2020-05-12 18:05:40 +02:00
config . Datastore . MySQL . TrackAccountMessages = config . History . Retention . EnableAccountIndexing
2021-04-23 19:54:44 +02:00
if config . Datastore . MySQL . MaxConns == 0 {
// #1622: not putting an upper limit on the number of MySQL connections is
// potentially dangerous. as a naive heuristic, assume they're running on the
// same machine:
config . Datastore . MySQL . MaxConns = runtime . NumCPU ( )
}
2020-02-21 00:33:48 +01:00
2019-05-12 10:01:47 +02:00
config . Server . Cloaks . Initialize ( )
2019-05-12 22:26:23 +02:00
if config . Server . Cloaks . Enabled {
2019-12-24 22:20:18 +01:00
if ! utils . IsHostname ( config . Server . Cloaks . Netname ) {
return nil , fmt . Errorf ( "Invalid netname for cloaked hostnames: %s" , config . Server . Cloaks . Netname )
}
2019-05-12 22:26:23 +02:00
}
2019-05-12 08:17:57 +02:00
2020-06-15 20:16:02 +02:00
err = config . processExtjwt ( )
if err != nil {
return nil , err
}
2019-09-22 23:26:50 +02:00
// now that all postprocessing is complete, regenerate ISUPPORT:
err = config . generateISupport ( )
if err != nil {
return nil , err
}
2020-12-06 05:06:23 +01:00
// #1428: Tor listeners should never see STS
config . Server . supportedCapsWithoutSTS = caps . NewSet ( )
config . Server . supportedCapsWithoutSTS . Union ( config . Server . supportedCaps )
config . Server . supportedCapsWithoutSTS . Disable ( caps . STS )
2016-04-12 15:00:09 +02:00
return config , nil
2014-02-09 16:53:42 +01:00
}
2019-12-23 21:26:37 +01:00
2020-05-12 18:05:40 +02:00
func ( config * Config ) getOutputPath ( filename string ) string {
return filepath . Join ( config . Server . OutputPath , filename )
}
2020-09-09 10:01:46 +02:00
func ( config * Config ) isRelaymsgIdentifier ( nick string ) bool {
if ! config . Server . Relaymsg . Enabled {
return false
}
2024-01-03 20:37:55 +01:00
if strings . HasPrefix ( nick , "#" ) {
return false // #2114
}
2020-09-09 10:01:46 +02:00
for _ , char := range config . Server . Relaymsg . Separators {
if strings . ContainsRune ( nick , char ) {
return true
}
}
return false
}
2020-01-14 07:21:47 +01:00
// setISupport sets up our RPL_ISUPPORT reply.
func ( config * Config ) generateISupport ( ) ( err error ) {
maxTargetsString := strconv . Itoa ( maxTargets )
// add RPL_ISUPPORT tokens
isupport := & config . Server . isupport
isupport . Initialize ( )
isupport . Add ( "AWAYLEN" , strconv . Itoa ( config . Limits . AwayLen ) )
2020-06-07 08:11:30 +02:00
isupport . Add ( "BOT" , "B" )
2020-01-14 07:21:47 +01:00
isupport . Add ( "CASEMAPPING" , "ascii" )
isupport . Add ( "CHANLIMIT" , fmt . Sprintf ( "%s:%d" , chanTypes , config . Channels . MaxChannelsPerClient ) )
2020-10-21 03:24:47 +02:00
isupport . Add ( "CHANMODES" , chanmodesToken )
2020-01-14 07:21:47 +01:00
if config . History . Enabled && config . History . ChathistoryMax > 0 {
2022-11-30 10:10:47 +01:00
isupport . Add ( "CHATHISTORY" , strconv . Itoa ( config . History . ChathistoryMax ) )
2022-12-02 07:30:46 +01:00
// Kiwi expects this legacy token name:
isupport . Add ( "draft/CHATHISTORY" , strconv . Itoa ( config . History . ChathistoryMax ) )
2020-01-14 07:21:47 +01:00
}
isupport . Add ( "CHANNELLEN" , strconv . Itoa ( config . Limits . ChannelLen ) )
isupport . Add ( "CHANTYPES" , chanTypes )
isupport . Add ( "ELIST" , "U" )
isupport . Add ( "EXCEPTS" , "" )
2020-06-15 20:16:02 +02:00
if config . Extjwt . Default . Enabled ( ) || len ( config . Extjwt . Services ) != 0 {
isupport . Add ( "EXTJWT" , "1" )
}
2020-10-21 17:08:55 +02:00
isupport . Add ( "EXTBAN" , ",m" )
2021-04-08 13:10:05 +02:00
isupport . Add ( "FORWARD" , "f" )
2020-01-14 07:21:47 +01:00
isupport . Add ( "INVEX" , "" )
isupport . Add ( "KICKLEN" , strconv . Itoa ( config . Limits . KickLen ) )
isupport . Add ( "MAXLIST" , fmt . Sprintf ( "beI:%s" , strconv . Itoa ( config . Limits . ChanListModes ) ) )
isupport . Add ( "MAXTARGETS" , maxTargetsString )
2023-02-02 20:28:27 +01:00
isupport . Add ( "MSGREFTYPES" , "msgid,timestamp" )
2020-01-14 07:21:47 +01:00
isupport . Add ( "MODES" , "" )
isupport . Add ( "MONITOR" , strconv . Itoa ( config . Limits . MonitorEntries ) )
isupport . Add ( "NETWORK" , config . Network . Name )
isupport . Add ( "NICKLEN" , strconv . Itoa ( config . Limits . NickLen ) )
isupport . Add ( "PREFIX" , "(qaohv)~&@%+" )
2020-09-16 17:32:52 +02:00
if config . Roleplay . Enabled {
2020-03-19 22:09:52 +01:00
isupport . Add ( "RPCHAN" , "E" )
isupport . Add ( "RPUSER" , "E" )
}
2020-01-14 07:21:47 +01:00
isupport . Add ( "STATUSMSG" , "~&@%+" )
2021-08-13 19:42:00 +02:00
isupport . Add ( "TARGMAX" , fmt . Sprintf ( "NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d" , maxTargetsString , maxTargetsString , maxTargetsString , config . Limits . MonitorEntries ) )
2020-01-14 07:21:47 +01:00
isupport . Add ( "TOPICLEN" , strconv . Itoa ( config . Limits . TopicLen ) )
if config . Server . Casemapping == CasemappingPRECIS {
isupport . Add ( "UTF8MAPPING" , precisUTF8MappingToken )
}
2021-01-15 12:19:13 +01:00
if config . Server . EnforceUtf8 {
isupport . Add ( "UTF8ONLY" , "" )
}
2020-07-11 17:45:02 +02:00
isupport . Add ( "WHOX" , "" )
2020-01-14 07:21:47 +01:00
err = isupport . RegenerateCachedReply ( )
return
}
2019-12-23 21:26:37 +01:00
// Diff returns changes in supported caps across a rehash.
func ( config * Config ) Diff ( oldConfig * Config ) ( addedCaps , removedCaps * caps . Set ) {
addedCaps = caps . NewSet ( )
removedCaps = caps . NewSet ( )
if oldConfig == nil {
return
}
if oldConfig . Server . capValues [ caps . Languages ] != config . Server . capValues [ caps . Languages ] {
// XXX updated caps get a DEL line and then a NEW line with the new value
addedCaps . Add ( caps . Languages )
removedCaps . Add ( caps . Languages )
}
if ! oldConfig . Accounts . AuthenticationEnabled && config . Accounts . AuthenticationEnabled {
addedCaps . Add ( caps . SASL )
} else if oldConfig . Accounts . AuthenticationEnabled && ! config . Accounts . AuthenticationEnabled {
removedCaps . Add ( caps . SASL )
}
if oldConfig . Limits . Multiline . MaxBytes != 0 && config . Limits . Multiline . MaxBytes == 0 {
removedCaps . Add ( caps . Multiline )
} else if oldConfig . Limits . Multiline . MaxBytes == 0 && config . Limits . Multiline . MaxBytes != 0 {
addedCaps . Add ( caps . Multiline )
} else if oldConfig . Limits . Multiline != config . Limits . Multiline {
removedCaps . Add ( caps . Multiline )
addedCaps . Add ( caps . Multiline )
}
if oldConfig . Server . STS . Enabled != config . Server . STS . Enabled || oldConfig . Server . capValues [ caps . STS ] != config . Server . capValues [ caps . STS ] {
// XXX: STS is always removed by CAP NEW sts=duration=0, not CAP DEL
// so the appropriate notify is always a CAP NEW; put it in addedCaps for any change
addedCaps . Add ( caps . STS )
}
return
}
2020-03-16 12:54:50 +01:00
2020-07-10 00:36:45 +02:00
// determine whether we need to resize / create / destroy
// the in-memory history buffers:
func ( config * Config ) historyChangedFrom ( oldConfig * Config ) bool {
return config . History . Enabled != oldConfig . History . Enabled ||
config . History . ChannelLength != oldConfig . History . ChannelLength ||
config . History . ClientLength != oldConfig . History . ClientLength ||
config . History . AutoresizeWindow != oldConfig . History . AutoresizeWindow ||
config . History . Persistent != oldConfig . History . Persistent
}
2020-03-16 12:54:50 +01:00
func compileGuestRegexp ( guestFormat string , casemapping Casemapping ) ( standard , folded * regexp . Regexp , err error ) {
2020-05-05 23:20:50 +02:00
if strings . Count ( guestFormat , "?" ) != 0 || strings . Count ( guestFormat , "*" ) != 1 {
err = errors . New ( "guest format must contain 1 '*' and no '?'s" )
return
}
2020-05-13 09:41:00 +02:00
standard , err = utils . CompileGlob ( guestFormat , true )
2020-05-05 04:29:10 +02:00
if err != nil {
return
}
2020-03-16 12:54:50 +01:00
starIndex := strings . IndexByte ( guestFormat , '*' )
initial := guestFormat [ : starIndex ]
final := guestFormat [ starIndex + 1 : ]
initialFolded , err := casefoldWithSetting ( initial , casemapping )
if err != nil {
return
}
finalFolded , err := casefoldWithSetting ( final , casemapping )
if err != nil {
return
}
2020-05-13 09:41:00 +02:00
folded , err = utils . CompileGlob ( fmt . Sprintf ( "%s*%s" , initialFolded , finalFolded ) , false )
2020-03-16 12:54:50 +01:00
return
}
2020-07-01 10:14:30 +02:00
func ( config * Config ) loadMOTD ( ) error {
if config . Server . MOTD != "" {
file , err := os . Open ( config . Server . MOTD )
if err != nil {
return err
}
defer file . Close ( )
2021-02-17 21:11:54 +01:00
contents , err := io . ReadAll ( file )
2020-07-01 10:14:30 +02:00
if err != nil {
return err
}
lines := bytes . Split ( contents , [ ] byte { '\n' } )
for i , line := range lines {
lineToSend := string ( bytes . TrimRight ( line , "\r\n" ) )
if len ( lineToSend ) == 0 && i == len ( lines ) - 1 {
// if the last line of the MOTD was properly terminated with \n,
// there's no need to send a blank line to clients
continue
}
if config . Server . MOTDFormatting {
lineToSend = ircfmt . Unescape ( lineToSend )
}
// "- " is the required prefix for MOTD
lineToSend = fmt . Sprintf ( "- %s" , lineToSend )
config . Server . motdLines = append ( config . Server . motdLines , lineToSend )
}
}
return nil
}