1// Copyright (c) 2012-2014 Jeremy Latt
2// Copyright (c) 2014-2015 Edmund Huber
3// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
4// released under the MIT license
5
6package irc
7
8import (
9	"bytes"
10	"crypto/tls"
11	"crypto/x509"
12	"errors"
13	"fmt"
14	"io"
15	"log"
16	"net"
17	"os"
18	"path/filepath"
19	"reflect"
20	"regexp"
21	"runtime"
22	"strconv"
23	"strings"
24	"time"
25
26	"code.cloudfoundry.org/bytefmt"
27	"github.com/ergochat/irc-go/ircfmt"
28	"gopkg.in/yaml.v2"
29
30	"github.com/ergochat/ergo/irc/caps"
31	"github.com/ergochat/ergo/irc/cloaks"
32	"github.com/ergochat/ergo/irc/connection_limits"
33	"github.com/ergochat/ergo/irc/custime"
34	"github.com/ergochat/ergo/irc/email"
35	"github.com/ergochat/ergo/irc/isupport"
36	"github.com/ergochat/ergo/irc/jwt"
37	"github.com/ergochat/ergo/irc/languages"
38	"github.com/ergochat/ergo/irc/logger"
39	"github.com/ergochat/ergo/irc/modes"
40	"github.com/ergochat/ergo/irc/mysql"
41	"github.com/ergochat/ergo/irc/passwd"
42	"github.com/ergochat/ergo/irc/utils"
43)
44
45// here's how this works: exported (capitalized) members of the config structs
46// are defined in the YAML file and deserialized directly from there. They may
47// be postprocessed and overwritten by LoadConfig. Unexported (lowercase) members
48// are derived from the exported members in LoadConfig.
49
50// TLSListenConfig defines configuration options for listening on TLS.
51type TLSListenConfig struct {
52	Cert  string
53	Key   string
54	Proxy bool // XXX: legacy key: it's preferred to specify this directly in listenerConfigBlock
55}
56
57// This is the YAML-deserializable type of the value of the `Server.Listeners` map
58type listenerConfigBlock struct {
59	// normal TLS configuration, with a single certificate:
60	TLS TLSListenConfig
61	// SNI configuration, with multiple certificates:
62	TLSCertificates []TLSListenConfig `yaml:"tls-certificates"`
63	MinTLSVersion   string            `yaml:"min-tls-version"`
64	Proxy           bool
65	Tor             bool
66	STSOnly         bool `yaml:"sts-only"`
67	WebSocket       bool
68	HideSTS         bool `yaml:"hide-sts"`
69}
70
71type HistoryCutoff uint
72
73const (
74	HistoryCutoffDefault HistoryCutoff = iota
75	HistoryCutoffNone
76	HistoryCutoffRegistrationTime
77	HistoryCutoffJoinTime
78)
79
80func historyCutoffToString(restriction HistoryCutoff) string {
81	switch restriction {
82	case HistoryCutoffDefault:
83		return "default"
84	case HistoryCutoffNone:
85		return "none"
86	case HistoryCutoffRegistrationTime:
87		return "registration-time"
88	case HistoryCutoffJoinTime:
89		return "join-time"
90	default:
91		return ""
92	}
93}
94
95func historyCutoffFromString(str string) (result HistoryCutoff, err error) {
96	switch strings.ToLower(str) {
97	case "default":
98		return HistoryCutoffDefault, nil
99	case "none", "disabled", "off", "false":
100		return HistoryCutoffNone, nil
101	case "registration-time":
102		return HistoryCutoffRegistrationTime, nil
103	case "join-time":
104		return HistoryCutoffJoinTime, nil
105	default:
106		return HistoryCutoffDefault, errInvalidParams
107	}
108}
109
110type PersistentStatus uint
111
112const (
113	PersistentUnspecified PersistentStatus = iota
114	PersistentDisabled
115	PersistentOptIn
116	PersistentOptOut
117	PersistentMandatory
118)
119
120func persistentStatusToString(status PersistentStatus) string {
121	switch status {
122	case PersistentUnspecified:
123		return "default"
124	case PersistentDisabled:
125		return "disabled"
126	case PersistentOptIn:
127		return "opt-in"
128	case PersistentOptOut:
129		return "opt-out"
130	case PersistentMandatory:
131		return "mandatory"
132	default:
133		return ""
134	}
135}
136
137func persistentStatusFromString(status string) (PersistentStatus, error) {
138	switch strings.ToLower(status) {
139	case "default":
140		return PersistentUnspecified, nil
141	case "":
142		return PersistentDisabled, nil
143	case "opt-in":
144		return PersistentOptIn, nil
145	case "opt-out":
146		return PersistentOptOut, nil
147	case "mandatory":
148		return PersistentMandatory, nil
149	default:
150		b, err := utils.StringToBool(status)
151		if b {
152			return PersistentMandatory, err
153		} else {
154			return PersistentDisabled, err
155		}
156	}
157}
158
159func (ps *PersistentStatus) UnmarshalYAML(unmarshal func(interface{}) error) error {
160	var orig string
161	var err error
162	if err = unmarshal(&orig); err != nil {
163		return err
164	}
165	result, err := persistentStatusFromString(orig)
166	if err == nil {
167		if result == PersistentUnspecified {
168			result = PersistentDisabled
169		}
170		*ps = result
171	} else {
172		err = fmt.Errorf("invalid value `%s` for server persistence status: %w", orig, err)
173	}
174	return err
175}
176
177func persistenceEnabled(serverSetting, clientSetting PersistentStatus) (enabled bool) {
178	if serverSetting == PersistentDisabled {
179		return false
180	} else if serverSetting == PersistentMandatory {
181		return true
182	} else if clientSetting == PersistentDisabled {
183		return false
184	} else if clientSetting == PersistentMandatory {
185		return true
186	} else if serverSetting == PersistentOptOut {
187		return true
188	} else {
189		return false
190	}
191}
192
193type HistoryStatus uint
194
195const (
196	HistoryDefault HistoryStatus = iota
197	HistoryDisabled
198	HistoryEphemeral
199	HistoryPersistent
200)
201
202func historyStatusFromString(str string) (status HistoryStatus, err error) {
203	switch strings.ToLower(str) {
204	case "default":
205		return HistoryDefault, nil
206	case "ephemeral":
207		return HistoryEphemeral, nil
208	case "persistent":
209		return HistoryPersistent, nil
210	default:
211		b, err := utils.StringToBool(str)
212		if b {
213			return HistoryPersistent, err
214		} else {
215			return HistoryDisabled, err
216		}
217	}
218}
219
220func historyStatusToString(status HistoryStatus) string {
221	switch status {
222	case HistoryDefault:
223		return "default"
224	case HistoryDisabled:
225		return "disabled"
226	case HistoryEphemeral:
227		return "ephemeral"
228	case HistoryPersistent:
229		return "persistent"
230	default:
231		return ""
232	}
233}
234
235// XXX you must have already checked History.Enabled before calling this
236func historyEnabled(serverSetting PersistentStatus, localSetting HistoryStatus) (result HistoryStatus) {
237	switch serverSetting {
238	case PersistentMandatory:
239		return HistoryPersistent
240	case PersistentOptOut:
241		if localSetting == HistoryDefault {
242			return HistoryPersistent
243		} else {
244			return localSetting
245		}
246	case PersistentOptIn:
247		switch localSetting {
248		case HistoryPersistent:
249			return HistoryPersistent
250		case HistoryEphemeral, HistoryDefault:
251			return HistoryEphemeral
252		default:
253			return HistoryDisabled
254		}
255	case PersistentDisabled:
256		if localSetting == HistoryDisabled {
257			return HistoryDisabled
258		} else {
259			return HistoryEphemeral
260		}
261	default:
262		// PersistentUnspecified: shouldn't happen because the deserializer converts it
263		// to PersistentDisabled
264		if localSetting == HistoryDefault {
265			return HistoryEphemeral
266		} else {
267			return localSetting
268		}
269	}
270}
271
272type MulticlientConfig struct {
273	Enabled            bool
274	AllowedByDefault   bool             `yaml:"allowed-by-default"`
275	AlwaysOn           PersistentStatus `yaml:"always-on"`
276	AutoAway           PersistentStatus `yaml:"auto-away"`
277	AlwaysOnExpiration custime.Duration `yaml:"always-on-expiration"`
278}
279
280type throttleConfig struct {
281	Enabled     bool
282	Duration    time.Duration
283	MaxAttempts int `yaml:"max-attempts"`
284}
285
286type ThrottleConfig struct {
287	throttleConfig
288}
289
290func (t *ThrottleConfig) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {
291	// note that this technique only works if the zero value of the struct
292	// doesn't need any postprocessing (because if the field is omitted entirely
293	// from the YAML, then UnmarshalYAML won't be called at all)
294	if err = unmarshal(&t.throttleConfig); err != nil {
295		return
296	}
297	if !t.Enabled {
298		t.MaxAttempts = 0 // limit of 0 means disabled
299	}
300	return
301}
302
303type AccountConfig struct {
304	Registration          AccountRegistrationConfig
305	AuthenticationEnabled bool `yaml:"authentication-enabled"`
306	AdvertiseSCRAM        bool `yaml:"advertise-scram"` // undocumented, see #1782
307	RequireSasl           struct {
308		Enabled      bool
309		Exempted     []string
310		exemptedNets []net.IPNet
311	} `yaml:"require-sasl"`
312	DefaultUserModes    *string `yaml:"default-user-modes"`
313	defaultUserModes    modes.Modes
314	LoginThrottling     ThrottleConfig `yaml:"login-throttling"`
315	SkipServerPassword  bool           `yaml:"skip-server-password"`
316	LoginViaPassCommand bool           `yaml:"login-via-pass-command"`
317	NickReservation     struct {
318		Enabled                bool
319		AdditionalNickLimit    int `yaml:"additional-nick-limit"`
320		Method                 NickEnforcementMethod
321		AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
322		// RenamePrefix is the legacy field, GuestFormat is the new version
323		RenamePrefix           string `yaml:"rename-prefix"`
324		GuestFormat            string `yaml:"guest-nickname-format"`
325		guestRegexp            *regexp.Regexp
326		guestRegexpFolded      *regexp.Regexp
327		ForceGuestFormat       bool `yaml:"force-guest-format"`
328		ForceNickEqualsAccount bool `yaml:"force-nick-equals-account"`
329		ForbidAnonNickChanges  bool `yaml:"forbid-anonymous-nick-changes"`
330	} `yaml:"nick-reservation"`
331	Multiclient MulticlientConfig
332	Bouncer     *MulticlientConfig // # handle old name for 'multiclient'
333	VHosts      VHostConfig
334	AuthScript  AuthScriptConfig `yaml:"auth-script"`
335}
336
337type ScriptConfig struct {
338	Enabled        bool
339	Command        string
340	Args           []string
341	Timeout        time.Duration
342	KillTimeout    time.Duration `yaml:"kill-timeout"`
343	MaxConcurrency uint          `yaml:"max-concurrency"`
344}
345
346type AuthScriptConfig struct {
347	ScriptConfig `yaml:",inline"`
348	Autocreate   bool
349}
350
351// AccountRegistrationConfig controls account registration.
352type AccountRegistrationConfig struct {
353	Enabled            bool
354	AllowBeforeConnect bool `yaml:"allow-before-connect"`
355	Throttling         ThrottleConfig
356	// new-style (v2.4 email verification config):
357	EmailVerification email.MailtoConfig `yaml:"email-verification"`
358	// old-style email verification config, with "callbacks":
359	LegacyEnabledCallbacks []string `yaml:"enabled-callbacks"`
360	LegacyCallbacks        struct {
361		Mailto email.MailtoConfig
362	} `yaml:"callbacks"`
363	VerifyTimeout custime.Duration `yaml:"verify-timeout"`
364	BcryptCost    uint             `yaml:"bcrypt-cost"`
365}
366
367type VHostConfig struct {
368	Enabled        bool
369	MaxLength      int    `yaml:"max-length"`
370	ValidRegexpRaw string `yaml:"valid-regexp"`
371	validRegexp    *regexp.Regexp
372}
373
374type NickEnforcementMethod int
375
376const (
377	// NickEnforcementOptional is the zero value; it serializes to
378	// "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`.
379	// in both cases, it means "defer to the other source of truth", i.e.,
380	// in the config, defer to the user's custom setting, and as a custom setting,
381	// defer to the default in the config. if both are NickEnforcementOptional then
382	// there is no enforcement.
383	// XXX: these are serialized as numbers in the database, so beware of collisions
384	// when refactoring (any numbers currently in use must keep their meanings, or
385	// else be fixed up by a schema change)
386	NickEnforcementOptional NickEnforcementMethod = iota
387	NickEnforcementNone
388	NickEnforcementStrict
389)
390
391func nickReservationToString(method NickEnforcementMethod) string {
392	switch method {
393	case NickEnforcementOptional:
394		return "default"
395	case NickEnforcementNone:
396		return "none"
397	case NickEnforcementStrict:
398		return "strict"
399	default:
400		return ""
401	}
402}
403
404func nickReservationFromString(method string) (NickEnforcementMethod, error) {
405	switch strings.ToLower(method) {
406	case "default":
407		return NickEnforcementOptional, nil
408	case "optional":
409		return NickEnforcementOptional, nil
410	case "none":
411		return NickEnforcementNone, nil
412	case "strict":
413		return NickEnforcementStrict, nil
414	default:
415		return NickEnforcementOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
416	}
417}
418
419func (nr *NickEnforcementMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
420	var orig string
421	var err error
422	if err = unmarshal(&orig); err != nil {
423		return err
424	}
425	method, err := nickReservationFromString(orig)
426	if err == nil {
427		*nr = method
428	} else {
429		err = fmt.Errorf("invalid value `%s` for nick enforcement method: %w", orig, err)
430	}
431	return err
432}
433
434func (cm *Casemapping) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {
435	var orig string
436	if err = unmarshal(&orig); err != nil {
437		return err
438	}
439
440	var result Casemapping
441	switch strings.ToLower(orig) {
442	case "ascii":
443		result = CasemappingASCII
444	case "precis", "rfc7613", "rfc8265":
445		result = CasemappingPRECIS
446	case "permissive", "fun":
447		result = CasemappingPermissive
448	default:
449		return fmt.Errorf("invalid casemapping value: %s", orig)
450	}
451	*cm = result
452	return nil
453}
454
455// OperClassConfig defines a specific operator class.
456type OperClassConfig struct {
457	Title        string
458	WhoisLine    string
459	Extends      string
460	Capabilities []string
461}
462
463// OperConfig defines a specific operator's configuration.
464type OperConfig struct {
465	Class       string
466	Vhost       string
467	WhoisLine   string `yaml:"whois-line"`
468	Password    string
469	Fingerprint *string // legacy name for certfp, #1050
470	Certfp      string
471	Auto        bool
472	Hidden      bool
473	Modes       string
474}
475
476// Various server-enforced limits on data size.
477type Limits struct {
478	AwayLen              int `yaml:"awaylen"`
479	ChanListModes        int `yaml:"chan-list-modes"`
480	ChannelLen           int `yaml:"channellen"`
481	IdentLen             int `yaml:"identlen"`
482	KickLen              int `yaml:"kicklen"`
483	MonitorEntries       int `yaml:"monitor-entries"`
484	NickLen              int `yaml:"nicklen"`
485	TopicLen             int `yaml:"topiclen"`
486	WhowasEntries        int `yaml:"whowas-entries"`
487	RegistrationMessages int `yaml:"registration-messages"`
488	Multiline            struct {
489		MaxBytes int `yaml:"max-bytes"`
490		MaxLines int `yaml:"max-lines"`
491	}
492}
493
494// STSConfig controls the STS configuration/
495type STSConfig struct {
496	Enabled       bool
497	Duration      custime.Duration
498	Port          int
499	Preload       bool
500	STSOnlyBanner string `yaml:"sts-only-banner"`
501	bannerLines   []string
502}
503
504// Value returns the STS value to advertise in CAP
505func (sts *STSConfig) Value() string {
506	val := fmt.Sprintf("duration=%d", int(time.Duration(sts.Duration).Seconds()))
507	if sts.Enabled && sts.Port > 0 {
508		val += fmt.Sprintf(",port=%d", sts.Port)
509	}
510	if sts.Enabled && sts.Preload {
511		val += ",preload"
512	}
513	return val
514}
515
516type FakelagConfig struct {
517	Enabled           bool
518	Window            time.Duration
519	BurstLimit        uint `yaml:"burst-limit"`
520	MessagesPerWindow uint `yaml:"messages-per-window"`
521	Cooldown          time.Duration
522}
523
524type TorListenersConfig struct {
525	Listeners                 []string // legacy only
526	RequireSasl               bool     `yaml:"require-sasl"`
527	Vhost                     string
528	MaxConnections            int           `yaml:"max-connections"`
529	ThrottleDuration          time.Duration `yaml:"throttle-duration"`
530	MaxConnectionsPerDuration int           `yaml:"max-connections-per-duration"`
531}
532
533// Config defines the overall configuration.
534type Config struct {
535	AllowEnvironmentOverrides bool `yaml:"allow-environment-overrides"`
536
537	Network struct {
538		Name string
539	}
540
541	Server struct {
542		Password       string
543		passwordBytes  []byte
544		Name           string
545		nameCasefolded string
546		Listeners      map[string]listenerConfigBlock
547		UnixBindMode   os.FileMode        `yaml:"unix-bind-mode"`
548		TorListeners   TorListenersConfig `yaml:"tor-listeners"`
549		WebSockets     struct {
550			AllowedOrigins       []string `yaml:"allowed-origins"`
551			allowedOriginRegexps []*regexp.Regexp
552		}
553		// they get parsed into this internal representation:
554		trueListeners           map[string]utils.ListenerConfig
555		STS                     STSConfig
556		LookupHostnames         *bool `yaml:"lookup-hostnames"`
557		lookupHostnames         bool
558		ForwardConfirmHostnames bool   `yaml:"forward-confirm-hostnames"`
559		CheckIdent              bool   `yaml:"check-ident"`
560		CoerceIdent             string `yaml:"coerce-ident"`
561		MOTD                    string
562		motdLines               []string
563		MOTDFormatting          bool `yaml:"motd-formatting"`
564		Relaymsg                struct {
565			Enabled            bool
566			Separators         string
567			AvailableToChanops bool `yaml:"available-to-chanops"`
568		}
569		ProxyAllowedFrom     []string `yaml:"proxy-allowed-from"`
570		proxyAllowedFromNets []net.IPNet
571		WebIRC               []webircConfig `yaml:"webirc"`
572		MaxSendQString       string         `yaml:"max-sendq"`
573		MaxSendQBytes        int
574		Compatibility        struct {
575			ForceTrailing      *bool `yaml:"force-trailing"`
576			forceTrailing      bool
577			SendUnprefixedSasl bool  `yaml:"send-unprefixed-sasl"`
578			AllowTruncation    *bool `yaml:"allow-truncation"`
579			allowTruncation    bool
580		}
581		isupport                 isupport.List
582		IPLimits                 connection_limits.LimiterConfig `yaml:"ip-limits"`
583		Cloaks                   cloaks.CloakConfig              `yaml:"ip-cloaking"`
584		SecureNetDefs            []string                        `yaml:"secure-nets"`
585		secureNets               []net.IPNet
586		supportedCaps            *caps.Set
587		supportedCapsWithoutSTS  *caps.Set
588		capValues                caps.Values
589		Casemapping              Casemapping
590		EnforceUtf8              bool         `yaml:"enforce-utf8"`
591		OutputPath               string       `yaml:"output-path"`
592		IPCheckScript            ScriptConfig `yaml:"ip-check-script"`
593		OverrideServicesHostname string       `yaml:"override-services-hostname"`
594		MaxLineLen               int          `yaml:"max-line-len"`
595		SuppressLusers           bool         `yaml:"suppress-lusers"`
596	}
597
598	Roleplay struct {
599		Enabled        bool
600		RequireChanops bool  `yaml:"require-chanops"`
601		RequireOper    bool  `yaml:"require-oper"`
602		AddSuffix      *bool `yaml:"add-suffix"`
603		addSuffix      bool
604	}
605
606	Extjwt struct {
607		Default  jwt.JwtServiceConfig            `yaml:",inline"`
608		Services map[string]jwt.JwtServiceConfig `yaml:"services"`
609	}
610
611	Languages struct {
612		Enabled bool
613		Path    string
614		Default string
615	}
616
617	languageManager *languages.Manager
618
619	Datastore struct {
620		Path        string
621		AutoUpgrade bool
622		MySQL       mysql.Config
623	}
624
625	Accounts AccountConfig
626
627	Channels struct {
628		DefaultModes         *string `yaml:"default-modes"`
629		defaultModes         modes.Modes
630		MaxChannelsPerClient int  `yaml:"max-channels-per-client"`
631		OpOnlyCreation       bool `yaml:"operator-only-creation"`
632		Registration         struct {
633			Enabled               bool
634			OperatorOnly          bool `yaml:"operator-only"`
635			MaxChannelsPerAccount int  `yaml:"max-channels-per-account"`
636		}
637		ListDelay        time.Duration    `yaml:"list-delay"`
638		InviteExpiration custime.Duration `yaml:"invite-expiration"`
639	}
640
641	OperClasses map[string]*OperClassConfig `yaml:"oper-classes"`
642
643	Opers map[string]*OperConfig
644
645	// parsed operator definitions, unexported so they can't be defined
646	// directly in YAML:
647	operators map[string]*Oper
648
649	Logging []logger.LoggingConfig
650
651	Debug struct {
652		RecoverFromErrors *bool `yaml:"recover-from-errors"`
653		recoverFromErrors bool
654		PprofListener     string `yaml:"pprof-listener"`
655	}
656
657	Limits Limits
658
659	Fakelag FakelagConfig
660
661	History struct {
662		Enabled          bool
663		ChannelLength    int              `yaml:"channel-length"`
664		ClientLength     int              `yaml:"client-length"`
665		AutoresizeWindow custime.Duration `yaml:"autoresize-window"`
666		AutoreplayOnJoin int              `yaml:"autoreplay-on-join"`
667		ChathistoryMax   int              `yaml:"chathistory-maxmessages"`
668		ZNCMax           int              `yaml:"znc-maxmessages"`
669		Restrictions     struct {
670			ExpireTime custime.Duration `yaml:"expire-time"`
671			// legacy key, superceded by QueryCutoff:
672			EnforceRegistrationDate_ bool   `yaml:"enforce-registration-date"`
673			QueryCutoff              string `yaml:"query-cutoff"`
674			queryCutoff              HistoryCutoff
675			GracePeriod              custime.Duration `yaml:"grace-period"`
676		}
677		Persistent struct {
678			Enabled              bool
679			UnregisteredChannels bool             `yaml:"unregistered-channels"`
680			RegisteredChannels   PersistentStatus `yaml:"registered-channels"`
681			DirectMessages       PersistentStatus `yaml:"direct-messages"`
682		}
683		Retention struct {
684			AllowIndividualDelete bool `yaml:"allow-individual-delete"`
685			EnableAccountIndexing bool `yaml:"enable-account-indexing"`
686		}
687		TagmsgStorage struct {
688			Default   bool
689			Whitelist []string
690			Blacklist []string
691		} `yaml:"tagmsg-storage"`
692	}
693
694	Filename string
695}
696
697// OperClass defines an assembled operator class.
698type OperClass struct {
699	Title        string
700	WhoisLine    string          `yaml:"whois-line"`
701	Capabilities utils.StringSet // map to make lookups much easier
702}
703
704// OperatorClasses returns a map of assembled operator classes from the given config.
705func (conf *Config) OperatorClasses() (map[string]*OperClass, error) {
706	fixupCapability := func(capab string) string {
707		return strings.TrimPrefix(strings.TrimPrefix(capab, "oper:"), "local_") // #868, #1442
708	}
709
710	ocs := make(map[string]*OperClass)
711
712	// loop from no extends to most extended, breaking if we can't add any more
713	lenOfLastOcs := -1
714	for {
715		if lenOfLastOcs == len(ocs) {
716			return nil, errors.New("OperClasses contains a looping dependency, or a class extends from a class that doesn't exist")
717		}
718		lenOfLastOcs = len(ocs)
719
720		var anyMissing bool
721		for name, info := range conf.OperClasses {
722			_, exists := ocs[name]
723			_, extendsExists := ocs[info.Extends]
724			if exists {
725				// class already exists
726				continue
727			} else if len(info.Extends) > 0 && !extendsExists {
728				// class we extend on doesn't exist
729				_, exists := conf.OperClasses[info.Extends]
730				if !exists {
731					return nil, fmt.Errorf("Operclass [%s] extends [%s], which doesn't exist", name, info.Extends)
732				}
733				anyMissing = true
734				continue
735			}
736
737			// create new operclass
738			var oc OperClass
739			oc.Capabilities = make(utils.StringSet)
740
741			// get inhereted info from other operclasses
742			if len(info.Extends) > 0 {
743				einfo := ocs[info.Extends]
744
745				for capab := range einfo.Capabilities {
746					oc.Capabilities.Add(fixupCapability(capab))
747				}
748			}
749
750			// add our own info
751			oc.Title = info.Title
752			if oc.Title == "" {
753				oc.Title = "IRC operator"
754			}
755			for _, capab := range info.Capabilities {
756				oc.Capabilities.Add(fixupCapability(capab))
757			}
758			if len(info.WhoisLine) > 0 {
759				oc.WhoisLine = info.WhoisLine
760			} else {
761				oc.WhoisLine = "is a"
762				if strings.Contains(strings.ToLower(string(oc.Title[0])), "aeiou") {
763					oc.WhoisLine += "n"
764				}
765				oc.WhoisLine += " "
766				oc.WhoisLine += oc.Title
767			}
768
769			ocs[name] = &oc
770		}
771
772		if !anyMissing {
773			// we've got every operclass!
774			break
775		}
776	}
777
778	return ocs, nil
779}
780
781// Oper represents a single assembled operator's config.
782type Oper struct {
783	Name      string
784	Class     *OperClass
785	WhoisLine string
786	Vhost     string
787	Pass      []byte
788	Certfp    string
789	Auto      bool
790	Hidden    bool
791	Modes     []modes.ModeChange
792}
793
794func (oper *Oper) HasRoleCapab(capab string) bool {
795	return oper != nil && oper.Class.Capabilities.Has(capab)
796}
797
798// Operators returns a map of operator configs from the given OperClass and config.
799func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error) {
800	operators := make(map[string]*Oper)
801	for name, opConf := range conf.Opers {
802		var oper Oper
803
804		// oper name
805		name, err := CasefoldName(name)
806		if err != nil {
807			return nil, fmt.Errorf("Could not casefold oper name: %s", err.Error())
808		}
809		oper.Name = name
810
811		if opConf.Password != "" {
812			oper.Pass, err = decodeLegacyPasswordHash(opConf.Password)
813			if err != nil {
814				return nil, fmt.Errorf("Oper %s has an invalid password hash: %s", oper.Name, err.Error())
815			}
816		}
817		certfp := opConf.Certfp
818		if certfp == "" && opConf.Fingerprint != nil {
819			certfp = *opConf.Fingerprint
820		}
821		if certfp != "" {
822			oper.Certfp, err = utils.NormalizeCertfp(certfp)
823			if err != nil {
824				return nil, fmt.Errorf("Oper %s has an invalid fingerprint: %s", oper.Name, err.Error())
825			}
826		}
827		oper.Auto = opConf.Auto
828		oper.Hidden = opConf.Hidden
829
830		if oper.Pass == nil && oper.Certfp == "" {
831			return nil, fmt.Errorf("Oper %s has neither a password nor a fingerprint", name)
832		}
833
834		oper.Vhost = opConf.Vhost
835		if oper.Vhost != "" && !conf.Accounts.VHosts.validRegexp.MatchString(oper.Vhost) {
836			return nil, fmt.Errorf("Oper %s has an invalid vhost: `%s`", name, oper.Vhost)
837		}
838		class, exists := oc[opConf.Class]
839		if !exists {
840			return nil, fmt.Errorf("Could not load operator [%s] - they use operclass [%s] which does not exist", name, opConf.Class)
841		}
842		oper.Class = class
843		if len(opConf.WhoisLine) > 0 {
844			oper.WhoisLine = opConf.WhoisLine
845		} else {
846			oper.WhoisLine = class.WhoisLine
847		}
848		modeStr := strings.TrimSpace(opConf.Modes)
849		modeChanges, unknownChanges := modes.ParseUserModeChanges(strings.Split(modeStr, " ")...)
850		if len(unknownChanges) > 0 {
851			return nil, fmt.Errorf("Could not load operator [%s] due to unknown modes %v", name, unknownChanges)
852		}
853		oper.Modes = modeChanges
854
855		// successful, attach to list of opers
856		operators[name] = &oper
857	}
858	return operators, nil
859}
860
861func loadTlsConfig(config listenerConfigBlock) (tlsConfig *tls.Config, err error) {
862	var certificates []tls.Certificate
863	if len(config.TLSCertificates) != 0 {
864		// SNI configuration with multiple certificates
865		for _, certPairConf := range config.TLSCertificates {
866			cert, err := loadCertWithLeaf(certPairConf.Cert, certPairConf.Key)
867			if err != nil {
868				return nil, err
869			}
870			certificates = append(certificates, cert)
871		}
872	} else if config.TLS.Cert != "" {
873		// normal configuration with one certificate
874		cert, err := loadCertWithLeaf(config.TLS.Cert, config.TLS.Key)
875		if err != nil {
876			return nil, err
877		}
878		certificates = append(certificates, cert)
879	} else {
880		// plaintext!
881		return nil, nil
882	}
883	clientAuth := tls.RequestClientCert
884	if config.WebSocket {
885		// if Chrome receives a server request for a client certificate
886		// on a websocket connection, it will immediately disconnect:
887		// https://bugs.chromium.org/p/chromium/issues/detail?id=329884
888		// work around this behavior:
889		clientAuth = tls.NoClientCert
890	}
891	result := tls.Config{
892		Certificates: certificates,
893		ClientAuth:   clientAuth,
894		MinVersion:   tlsMinVersionFromString(config.MinTLSVersion),
895	}
896	return &result, nil
897}
898
899func tlsMinVersionFromString(version string) uint16 {
900	version = strings.ToLower(version)
901	version = strings.TrimPrefix(version, "v")
902	switch version {
903	case "1", "1.0":
904		return tls.VersionTLS10
905	case "1.1":
906		return tls.VersionTLS11
907	case "1.2":
908		return tls.VersionTLS12
909	case "1.3":
910		return tls.VersionTLS13
911	default:
912		// tls package will fill in a sane value, currently 1.0
913		return 0
914	}
915}
916
917func loadCertWithLeaf(certFile, keyFile string) (cert tls.Certificate, err error) {
918	// LoadX509KeyPair: "On successful return, Certificate.Leaf will be nil because
919	// the parsed form of the certificate is not retained." tls.Config:
920	// "Note: if there are multiple Certificates, and they don't have the
921	// optional field Leaf set, certificate selection will incur a significant
922	// per-handshake performance cost."
923	cert, err = tls.LoadX509KeyPair(certFile, keyFile)
924	if err != nil {
925		return
926	}
927	cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
928	return
929}
930
931// prepareListeners populates Config.Server.trueListeners
932func (conf *Config) prepareListeners() (err error) {
933	if len(conf.Server.Listeners) == 0 {
934		return fmt.Errorf("No listeners were configured")
935	}
936
937	conf.Server.trueListeners = make(map[string]utils.ListenerConfig)
938	for addr, block := range conf.Server.Listeners {
939		var lconf utils.ListenerConfig
940		lconf.ProxyDeadline = RegisterTimeout
941		lconf.Tor = block.Tor
942		lconf.STSOnly = block.STSOnly
943		if lconf.STSOnly && !conf.Server.STS.Enabled {
944			return fmt.Errorf("%s is configured as a STS-only listener, but STS is disabled", addr)
945		}
946		lconf.TLSConfig, err = loadTlsConfig(block)
947		if err != nil {
948			return &CertKeyError{Err: err}
949		}
950		lconf.RequireProxy = block.TLS.Proxy || block.Proxy
951		lconf.WebSocket = block.WebSocket
952		if lconf.WebSocket && !conf.Server.EnforceUtf8 {
953			return fmt.Errorf("enabling a websocket listener requires the use of server.enforce-utf8")
954		}
955		lconf.HideSTS = block.HideSTS
956		conf.Server.trueListeners[addr] = lconf
957	}
958	return nil
959}
960
961func (config *Config) processExtjwt() (err error) {
962	// first process the default service, which may be disabled
963	err = config.Extjwt.Default.Postprocess()
964	if err != nil {
965		return
966	}
967	// now process the named services. it is an error if any is disabled
968	// also, normalize the service names to lowercase
969	services := make(map[string]jwt.JwtServiceConfig, len(config.Extjwt.Services))
970	for service, sConf := range config.Extjwt.Services {
971		err := sConf.Postprocess()
972		if err != nil {
973			return err
974		}
975		if !sConf.Enabled() {
976			return fmt.Errorf("no keys enabled for extjwt service %s", service)
977		}
978		services[strings.ToLower(service)] = sConf
979	}
980	config.Extjwt.Services = services
981	return nil
982}
983
984// LoadRawConfig loads the config without doing any consistency checks or postprocessing
985func LoadRawConfig(filename string) (config *Config, err error) {
986	data, err := os.ReadFile(filename)
987	if err != nil {
988		return nil, err
989	}
990
991	err = yaml.Unmarshal(data, &config)
992	if err != nil {
993		return nil, err
994	}
995	return
996}
997
998// convert, e.g., "ALLOWED_ORIGINS" to "allowed-origins"
999func screamingSnakeToKebab(in string) (out string) {
1000	var buf strings.Builder
1001	for i := 0; i < len(in); i++ {
1002		c := in[i]
1003		switch {
1004		case c == '_':
1005			buf.WriteByte('-')
1006		case 'A' <= c && c <= 'Z':
1007			buf.WriteByte(c + ('a' - 'A'))
1008		default:
1009			buf.WriteByte(c)
1010		}
1011	}
1012	return buf.String()
1013}
1014
1015func isExported(field reflect.StructField) bool {
1016	return field.PkgPath == "" // https://golang.org/pkg/reflect/#StructField
1017}
1018
1019// errors caused by config overrides
1020type configPathError struct {
1021	name     string
1022	desc     string
1023	fatalErr error
1024}
1025
1026func (ce *configPathError) Error() string {
1027	if ce.fatalErr != nil {
1028		return fmt.Sprintf("Couldn't apply config override `%s`: %s: %v", ce.name, ce.desc, ce.fatalErr)
1029	}
1030	return fmt.Sprintf("Couldn't apply config override `%s`: %s", ce.name, ce.desc)
1031}
1032
1033func mungeFromEnvironment(config *Config, envPair string) (applied bool, err *configPathError) {
1034	equalIdx := strings.IndexByte(envPair, '=')
1035	name, value := envPair[:equalIdx], envPair[equalIdx+1:]
1036	if strings.HasPrefix(name, "ERGO__") {
1037		name = strings.TrimPrefix(name, "ERGO__")
1038	} else if strings.HasPrefix(name, "ORAGONO__") {
1039		name = strings.TrimPrefix(name, "ORAGONO__")
1040	} else {
1041		return false, nil
1042	}
1043	pathComponents := strings.Split(name, "__")
1044	for i, pathComponent := range pathComponents {
1045		pathComponents[i] = screamingSnakeToKebab(pathComponent)
1046	}
1047
1048	v := reflect.Indirect(reflect.ValueOf(config))
1049	t := v.Type()
1050	for _, component := range pathComponents {
1051		if component == "" {
1052			return false, &configPathError{name, "invalid", nil}
1053		}
1054		if v.Kind() != reflect.Struct {
1055			return false, &configPathError{name, "index into non-struct", nil}
1056		}
1057		var nextField reflect.StructField
1058		success := false
1059		n := t.NumField()
1060		// preferentially get a field with an exact yaml tag match,
1061		// then fall back to case-insensitive comparison of field names
1062		for i := 0; i < n; i++ {
1063			field := t.Field(i)
1064			if isExported(field) && field.Tag.Get("yaml") == component {
1065				nextField = field
1066				success = true
1067				break
1068			}
1069		}
1070		if !success {
1071			for i := 0; i < n; i++ {
1072				field := t.Field(i)
1073				if isExported(field) && strings.ToLower(field.Name) == component {
1074					nextField = field
1075					success = true
1076					break
1077				}
1078			}
1079		}
1080		if !success {
1081			return false, &configPathError{name, fmt.Sprintf("couldn't resolve path component: `%s`", component), nil}
1082		}
1083		v = v.FieldByName(nextField.Name)
1084		// dereference pointer field if necessary, initialize new value if necessary
1085		if v.Kind() == reflect.Ptr {
1086			if v.IsNil() {
1087				v.Set(reflect.New(v.Type().Elem()))
1088			}
1089			v = reflect.Indirect(v)
1090		}
1091		t = v.Type()
1092	}
1093	yamlErr := yaml.Unmarshal([]byte(value), v.Addr().Interface())
1094	if yamlErr != nil {
1095		return false, &configPathError{name, "couldn't deserialize YAML", yamlErr}
1096	}
1097	return true, nil
1098}
1099
1100// LoadConfig loads the given YAML configuration file.
1101func LoadConfig(filename string) (config *Config, err error) {
1102	config, err = LoadRawConfig(filename)
1103	if err != nil {
1104		return nil, err
1105	}
1106
1107	if config.AllowEnvironmentOverrides {
1108		for _, envPair := range os.Environ() {
1109			applied, envErr := mungeFromEnvironment(config, envPair)
1110			if envErr != nil {
1111				if envErr.fatalErr != nil {
1112					return nil, envErr
1113				} else {
1114					log.Println(envErr.Error())
1115				}
1116			} else if applied {
1117				log.Printf("applied environment override: %s\n", envPair)
1118			}
1119		}
1120	}
1121
1122	config.Filename = filename
1123
1124	if config.Network.Name == "" {
1125		return nil, errors.New("Network name missing")
1126	}
1127	if config.Server.Name == "" {
1128		return nil, errors.New("Server name missing")
1129	}
1130	if !utils.IsServerName(config.Server.Name) {
1131		return nil, errors.New("Server name must match the format of a hostname")
1132	}
1133	config.Server.nameCasefolded = strings.ToLower(config.Server.Name)
1134	if config.Datastore.Path == "" {
1135		return nil, errors.New("Datastore path missing")
1136	}
1137	//dan: automagically fix identlen until a few releases in the future (from now, 0.12.0), being a newly-introduced limit
1138	if config.Limits.IdentLen < 1 {
1139		config.Limits.IdentLen = 20
1140	}
1141	if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 {
1142		return nil, errors.New("One or more limits values are too low")
1143	}
1144	if config.Limits.RegistrationMessages == 0 {
1145		config.Limits.RegistrationMessages = 1024
1146	}
1147	if config.Server.MaxLineLen < DefaultMaxLineLen {
1148		config.Server.MaxLineLen = DefaultMaxLineLen
1149	}
1150	if config.Datastore.MySQL.Enabled {
1151		if config.Limits.NickLen > mysql.MaxTargetLength || config.Limits.ChannelLen > mysql.MaxTargetLength {
1152			return nil, fmt.Errorf("to use MySQL, nick and channel length limits must be %d or lower", mysql.MaxTargetLength)
1153		}
1154	}
1155
1156	if config.Server.CoerceIdent != "" {
1157		if config.Server.CheckIdent {
1158			return nil, errors.New("Can't configure both check-ident and coerce-ident")
1159		}
1160		if config.Server.CoerceIdent[0] != '~' {
1161			return nil, errors.New("coerce-ident value must start with a ~")
1162		}
1163		if !isIdent(config.Server.CoerceIdent[1:]) {
1164			return nil, errors.New("coerce-ident must be valid as an IRC user/ident field")
1165		}
1166	}
1167
1168	config.Server.supportedCaps = caps.NewCompleteSet()
1169	config.Server.capValues = make(caps.Values)
1170
1171	err = config.prepareListeners()
1172	if err != nil {
1173		return nil, fmt.Errorf("failed to prepare listeners: %v", err)
1174	}
1175
1176	for _, glob := range config.Server.WebSockets.AllowedOrigins {
1177		globre, err := utils.CompileGlob(glob, false)
1178		if err != nil {
1179			return nil, fmt.Errorf("invalid websocket allowed-origin expression: %s", glob)
1180		}
1181		config.Server.WebSockets.allowedOriginRegexps = append(config.Server.WebSockets.allowedOriginRegexps, globre)
1182	}
1183
1184	if config.Server.STS.Enabled {
1185		if config.Server.STS.Port < 0 || config.Server.STS.Port > 65535 {
1186			return nil, fmt.Errorf("STS port is incorrect, should be 0 if disabled: %d", config.Server.STS.Port)
1187		}
1188		if config.Server.STS.STSOnlyBanner != "" {
1189			for _, line := range strings.Split(config.Server.STS.STSOnlyBanner, "\n") {
1190				config.Server.STS.bannerLines = append(config.Server.STS.bannerLines, strings.TrimSpace(line))
1191			}
1192		} else {
1193			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)}
1194		}
1195	} else {
1196		config.Server.supportedCaps.Disable(caps.STS)
1197		config.Server.STS.Duration = 0
1198	}
1199	// set this even if STS is disabled
1200	config.Server.capValues[caps.STS] = config.Server.STS.Value()
1201
1202	config.Server.lookupHostnames = utils.BoolDefaultTrue(config.Server.LookupHostnames)
1203
1204	// process webirc blocks
1205	var newWebIRC []webircConfig
1206	for _, webirc := range config.Server.WebIRC {
1207		// skip webirc blocks with no hosts (such as the example one)
1208		if len(webirc.Hosts) == 0 {
1209			continue
1210		}
1211
1212		err = webirc.Populate()
1213		if err != nil {
1214			return nil, fmt.Errorf("Could not parse WebIRC config: %s", err.Error())
1215		}
1216		newWebIRC = append(newWebIRC, webirc)
1217	}
1218	config.Server.WebIRC = newWebIRC
1219
1220	if config.Limits.Multiline.MaxBytes <= 0 {
1221		config.Server.supportedCaps.Disable(caps.Multiline)
1222	} else {
1223		var multilineCapValue string
1224		if config.Limits.Multiline.MaxLines == 0 {
1225			multilineCapValue = fmt.Sprintf("max-bytes=%d", config.Limits.Multiline.MaxBytes)
1226		} else {
1227			multilineCapValue = fmt.Sprintf("max-bytes=%d,max-lines=%d", config.Limits.Multiline.MaxBytes, config.Limits.Multiline.MaxLines)
1228		}
1229		config.Server.capValues[caps.Multiline] = multilineCapValue
1230	}
1231
1232	// handle legacy name 'bouncer' for 'multiclient' section:
1233	if config.Accounts.Bouncer != nil {
1234		config.Accounts.Multiclient = *config.Accounts.Bouncer
1235	}
1236
1237	if !config.Accounts.Multiclient.Enabled {
1238		config.Accounts.Multiclient.AlwaysOn = PersistentDisabled
1239	} else if config.Accounts.Multiclient.AlwaysOn >= PersistentOptOut {
1240		config.Accounts.Multiclient.AllowedByDefault = true
1241	}
1242
1243	if !config.Accounts.NickReservation.Enabled {
1244		config.Accounts.NickReservation.ForceNickEqualsAccount = false
1245	}
1246
1247	if config.Accounts.NickReservation.ForceNickEqualsAccount && !config.Accounts.Multiclient.Enabled {
1248		return nil, errors.New("force-nick-equals-account requires enabling multiclient as well")
1249	}
1250
1251	// handle guest format, including the legacy key rename-prefix
1252	if config.Accounts.NickReservation.GuestFormat == "" {
1253		renamePrefix := config.Accounts.NickReservation.RenamePrefix
1254		if renamePrefix == "" {
1255			renamePrefix = "Guest-"
1256		}
1257		config.Accounts.NickReservation.GuestFormat = renamePrefix + "*"
1258	}
1259	config.Accounts.NickReservation.guestRegexp, config.Accounts.NickReservation.guestRegexpFolded, err = compileGuestRegexp(config.Accounts.NickReservation.GuestFormat, config.Server.Casemapping)
1260	if err != nil {
1261		return nil, err
1262	}
1263
1264	var newLogConfigs []logger.LoggingConfig
1265	for _, logConfig := range config.Logging {
1266		// methods
1267		methods := make(map[string]bool)
1268		for _, method := range strings.Split(logConfig.Method, " ") {
1269			if len(method) > 0 {
1270				methods[strings.ToLower(method)] = true
1271			}
1272		}
1273		if methods["file"] && logConfig.Filename == "" {
1274			return nil, errors.New("Logging configuration specifies 'file' method but 'filename' is empty")
1275		}
1276		logConfig.MethodFile = methods["file"]
1277		logConfig.MethodStdout = methods["stdout"]
1278		logConfig.MethodStderr = methods["stderr"]
1279
1280		// levels
1281		level, exists := logger.LogLevelNames[strings.ToLower(logConfig.LevelString)]
1282		if !exists {
1283			return nil, fmt.Errorf("Could not translate log leve [%s]", logConfig.LevelString)
1284		}
1285		logConfig.Level = level
1286
1287		// types
1288		for _, typeStr := range strings.Split(logConfig.TypeString, " ") {
1289			if len(typeStr) == 0 {
1290				continue
1291			}
1292			if typeStr == "-" {
1293				return nil, errors.New("Encountered logging type '-' with no type to exclude")
1294			}
1295			if typeStr[0] == '-' {
1296				typeStr = typeStr[1:]
1297				logConfig.ExcludedTypes = append(logConfig.ExcludedTypes, typeStr)
1298			} else {
1299				logConfig.Types = append(logConfig.Types, typeStr)
1300			}
1301		}
1302		if len(logConfig.Types) < 1 {
1303			return nil, errors.New("Logger has no types to log")
1304		}
1305
1306		newLogConfigs = append(newLogConfigs, logConfig)
1307	}
1308	config.Logging = newLogConfigs
1309
1310	if config.Accounts.Registration.EmailVerification.Enabled {
1311		err := config.Accounts.Registration.EmailVerification.Postprocess(config.Server.Name)
1312		if err != nil {
1313			return nil, err
1314		}
1315	} else {
1316		// TODO: this processes the legacy "callback" config, clean this up in 2.5 or later
1317		// TODO: also clean up the legacy "inline" MTA config format (from ee05a4324dfde)
1318		mailtoEnabled := false
1319		for _, name := range config.Accounts.Registration.LegacyEnabledCallbacks {
1320			if name == "mailto" {
1321				mailtoEnabled = true
1322				break
1323			}
1324		}
1325		if mailtoEnabled {
1326			config.Accounts.Registration.EmailVerification = config.Accounts.Registration.LegacyCallbacks.Mailto
1327			config.Accounts.Registration.EmailVerification.Enabled = true
1328			err := config.Accounts.Registration.EmailVerification.Postprocess(config.Server.Name)
1329			if err != nil {
1330				return nil, err
1331			}
1332		}
1333	}
1334
1335	config.Accounts.defaultUserModes = ParseDefaultUserModes(config.Accounts.DefaultUserModes)
1336
1337	if config.Server.Password != "" {
1338		config.Server.passwordBytes, err = decodeLegacyPasswordHash(config.Server.Password)
1339		if err != nil {
1340			return nil, err
1341		}
1342		if config.Accounts.LoginViaPassCommand && !config.Accounts.SkipServerPassword {
1343			return nil, errors.New("Using a server password and login-via-pass-command requires skip-server-password as well")
1344		}
1345		// #1634: accounts.registration.allow-before-connect is an auth bypass
1346		// for configurations that start from default and then enable server.password
1347		config.Accounts.Registration.AllowBeforeConnect = false
1348	}
1349
1350	if config.Accounts.RequireSasl.Enabled {
1351		// minor gotcha: Tor listeners will typically be loopback and
1352		// therefore exempted from require-sasl. if require-sasl is enabled
1353		// for non-Tor (non-local) connections, enable it for Tor as well:
1354		config.Server.TorListeners.RequireSasl = true
1355	}
1356	config.Accounts.RequireSasl.exemptedNets, err = utils.ParseNetList(config.Accounts.RequireSasl.Exempted)
1357	if err != nil {
1358		return nil, fmt.Errorf("Could not parse require-sasl exempted nets: %v", err.Error())
1359	}
1360
1361	config.Server.proxyAllowedFromNets, err = utils.ParseNetList(config.Server.ProxyAllowedFrom)
1362	if err != nil {
1363		return nil, fmt.Errorf("Could not parse proxy-allowed-from nets: %v", err.Error())
1364	}
1365
1366	config.Server.secureNets, err = utils.ParseNetList(config.Server.SecureNetDefs)
1367	if err != nil {
1368		return nil, fmt.Errorf("Could not parse secure-nets: %v\n", err.Error())
1369	}
1370
1371	rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
1372	if rawRegexp != "" {
1373		regexp, err := regexp.Compile(rawRegexp)
1374		if err == nil {
1375			config.Accounts.VHosts.validRegexp = regexp
1376		} else {
1377			log.Printf("invalid vhost regexp: %s\n", err.Error())
1378		}
1379	}
1380	if config.Accounts.VHosts.validRegexp == nil {
1381		config.Accounts.VHosts.validRegexp = defaultValidVhostRegex
1382	}
1383
1384	saslCapValue := "PLAIN,EXTERNAL,SCRAM-SHA-256"
1385	// TODO(#1782) clean this up:
1386	if !config.Accounts.AdvertiseSCRAM {
1387		saslCapValue = "PLAIN,EXTERNAL"
1388	}
1389	config.Server.capValues[caps.SASL] = saslCapValue
1390	if !config.Accounts.AuthenticationEnabled {
1391		config.Server.supportedCaps.Disable(caps.SASL)
1392	}
1393
1394	if !config.Accounts.Registration.Enabled {
1395		config.Server.supportedCaps.Disable(caps.AccountRegistration)
1396	} else {
1397		var registerValues []string
1398		if config.Accounts.Registration.AllowBeforeConnect {
1399			registerValues = append(registerValues, "before-connect")
1400		}
1401		if config.Accounts.Registration.EmailVerification.Enabled {
1402			registerValues = append(registerValues, "email-required")
1403		}
1404		if config.Accounts.RequireSasl.Enabled {
1405			registerValues = append(registerValues, "account-required")
1406		}
1407		if len(registerValues) != 0 {
1408			config.Server.capValues[caps.AccountRegistration] = strings.Join(registerValues, ",")
1409		}
1410	}
1411
1412	maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
1413	if err != nil {
1414		return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())
1415	}
1416	config.Server.MaxSendQBytes = int(maxSendQBytes)
1417
1418	config.languageManager, err = languages.NewManager(config.Languages.Enabled, config.Languages.Path, config.Languages.Default)
1419	if err != nil {
1420		return nil, fmt.Errorf("Could not load languages: %s", err.Error())
1421	}
1422	config.Server.capValues[caps.Languages] = config.languageManager.CapValue()
1423
1424	if config.Server.Relaymsg.Enabled {
1425		for _, char := range protocolBreakingNameCharacters {
1426			if strings.ContainsRune(config.Server.Relaymsg.Separators, char) {
1427				return nil, fmt.Errorf("RELAYMSG separators cannot include the characters %s", protocolBreakingNameCharacters)
1428			}
1429		}
1430		config.Server.capValues[caps.Relaymsg] = config.Server.Relaymsg.Separators
1431	} else {
1432		config.Server.supportedCaps.Disable(caps.Relaymsg)
1433	}
1434
1435	config.Debug.recoverFromErrors = utils.BoolDefaultTrue(config.Debug.RecoverFromErrors)
1436
1437	// process operator definitions, store them to config.operators
1438	operclasses, err := config.OperatorClasses()
1439	if err != nil {
1440		return nil, err
1441	}
1442	opers, err := config.Operators(operclasses)
1443	if err != nil {
1444		return nil, err
1445	}
1446	config.operators = opers
1447
1448	// parse default channel modes
1449	config.Channels.defaultModes = ParseDefaultChannelModes(config.Channels.DefaultModes)
1450
1451	if config.Accounts.Registration.BcryptCost == 0 {
1452		config.Accounts.Registration.BcryptCost = passwd.DefaultCost
1453	}
1454
1455	if config.Channels.MaxChannelsPerClient == 0 {
1456		config.Channels.MaxChannelsPerClient = 100
1457	}
1458	if config.Channels.Registration.MaxChannelsPerAccount == 0 {
1459		config.Channels.Registration.MaxChannelsPerAccount = 15
1460	}
1461
1462	config.Server.Compatibility.forceTrailing = utils.BoolDefaultTrue(config.Server.Compatibility.ForceTrailing)
1463	config.Server.Compatibility.allowTruncation = utils.BoolDefaultTrue(config.Server.Compatibility.AllowTruncation)
1464
1465	config.loadMOTD()
1466
1467	// in the current implementation, we disable history by creating a history buffer
1468	// with zero capacity. but the `enabled` config option MUST be respected regardless
1469	// of this detail
1470	if !config.History.Enabled {
1471		config.History.ChannelLength = 0
1472		config.History.ClientLength = 0
1473		config.Server.supportedCaps.Disable(caps.Chathistory)
1474		config.Server.supportedCaps.Disable(caps.EventPlayback)
1475		config.Server.supportedCaps.Disable(caps.ZNCPlayback)
1476	}
1477
1478	if !config.History.Enabled || !config.History.Persistent.Enabled {
1479		config.History.Persistent.Enabled = false
1480		config.History.Persistent.UnregisteredChannels = false
1481		config.History.Persistent.RegisteredChannels = PersistentDisabled
1482		config.History.Persistent.DirectMessages = PersistentDisabled
1483	}
1484
1485	if config.History.Persistent.Enabled && !config.Datastore.MySQL.Enabled {
1486		return nil, fmt.Errorf("You must configure a MySQL server in order to enable persistent history")
1487	}
1488
1489	if config.History.ZNCMax == 0 {
1490		config.History.ZNCMax = config.History.ChathistoryMax
1491	}
1492
1493	if config.History.Restrictions.QueryCutoff != "" {
1494		config.History.Restrictions.queryCutoff, err = historyCutoffFromString(config.History.Restrictions.QueryCutoff)
1495		if err != nil {
1496			return nil, fmt.Errorf("invalid value of history.query-restrictions: %w", err)
1497		}
1498	} else {
1499		if config.History.Restrictions.EnforceRegistrationDate_ {
1500			config.History.Restrictions.queryCutoff = HistoryCutoffRegistrationTime
1501		} else {
1502			config.History.Restrictions.queryCutoff = HistoryCutoffNone
1503		}
1504	}
1505
1506	config.Roleplay.addSuffix = utils.BoolDefaultTrue(config.Roleplay.AddSuffix)
1507
1508	config.Datastore.MySQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime)
1509	config.Datastore.MySQL.TrackAccountMessages = config.History.Retention.EnableAccountIndexing
1510	if config.Datastore.MySQL.MaxConns == 0 {
1511		// #1622: not putting an upper limit on the number of MySQL connections is
1512		// potentially dangerous. as a naive heuristic, assume they're running on the
1513		// same machine:
1514		config.Datastore.MySQL.MaxConns = runtime.NumCPU()
1515	}
1516
1517	config.Server.Cloaks.Initialize()
1518	if config.Server.Cloaks.Enabled {
1519		if !utils.IsHostname(config.Server.Cloaks.Netname) {
1520			return nil, fmt.Errorf("Invalid netname for cloaked hostnames: %s", config.Server.Cloaks.Netname)
1521		}
1522	}
1523
1524	err = config.processExtjwt()
1525	if err != nil {
1526		return nil, err
1527	}
1528
1529	// now that all postprocessing is complete, regenerate ISUPPORT:
1530	err = config.generateISupport()
1531	if err != nil {
1532		return nil, err
1533	}
1534
1535	// #1428: Tor listeners should never see STS
1536	config.Server.supportedCapsWithoutSTS = caps.NewSet()
1537	config.Server.supportedCapsWithoutSTS.Union(config.Server.supportedCaps)
1538	config.Server.supportedCapsWithoutSTS.Disable(caps.STS)
1539
1540	return config, nil
1541}
1542
1543func (config *Config) getOutputPath(filename string) string {
1544	return filepath.Join(config.Server.OutputPath, filename)
1545}
1546
1547func (config *Config) isRelaymsgIdentifier(nick string) bool {
1548	if !config.Server.Relaymsg.Enabled {
1549		return false
1550	}
1551
1552	for _, char := range config.Server.Relaymsg.Separators {
1553		if strings.ContainsRune(nick, char) {
1554			return true
1555		}
1556	}
1557	return false
1558}
1559
1560// setISupport sets up our RPL_ISUPPORT reply.
1561func (config *Config) generateISupport() (err error) {
1562	maxTargetsString := strconv.Itoa(maxTargets)
1563
1564	// add RPL_ISUPPORT tokens
1565	isupport := &config.Server.isupport
1566	isupport.Initialize()
1567	isupport.Add("AWAYLEN", strconv.Itoa(config.Limits.AwayLen))
1568	isupport.Add("BOT", "B")
1569	isupport.Add("CASEMAPPING", "ascii")
1570	isupport.Add("CHANLIMIT", fmt.Sprintf("%s:%d", chanTypes, config.Channels.MaxChannelsPerClient))
1571	isupport.Add("CHANMODES", chanmodesToken)
1572	if config.History.Enabled && config.History.ChathistoryMax > 0 {
1573		isupport.Add("draft/CHATHISTORY", strconv.Itoa(config.History.ChathistoryMax))
1574	}
1575	isupport.Add("CHANNELLEN", strconv.Itoa(config.Limits.ChannelLen))
1576	isupport.Add("CHANTYPES", chanTypes)
1577	isupport.Add("ELIST", "U")
1578	isupport.Add("EXCEPTS", "")
1579	if config.Extjwt.Default.Enabled() || len(config.Extjwt.Services) != 0 {
1580		isupport.Add("EXTJWT", "1")
1581	}
1582	isupport.Add("EXTBAN", ",m")
1583	isupport.Add("FORWARD", "f")
1584	isupport.Add("INVEX", "")
1585	isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen))
1586	isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes)))
1587	isupport.Add("MAXTARGETS", maxTargetsString)
1588	isupport.Add("MODES", "")
1589	isupport.Add("MONITOR", strconv.Itoa(config.Limits.MonitorEntries))
1590	isupport.Add("NETWORK", config.Network.Name)
1591	isupport.Add("NICKLEN", strconv.Itoa(config.Limits.NickLen))
1592	isupport.Add("PREFIX", "(qaohv)~&@%+")
1593	if config.Roleplay.Enabled {
1594		isupport.Add("RPCHAN", "E")
1595		isupport.Add("RPUSER", "E")
1596	}
1597	isupport.Add("STATUSMSG", "~&@%+")
1598	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))
1599	isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen))
1600	if config.Server.Casemapping == CasemappingPRECIS {
1601		isupport.Add("UTF8MAPPING", precisUTF8MappingToken)
1602	}
1603	if config.Server.EnforceUtf8 {
1604		isupport.Add("UTF8ONLY", "")
1605	}
1606	isupport.Add("WHOX", "")
1607
1608	err = isupport.RegenerateCachedReply()
1609	return
1610}
1611
1612// Diff returns changes in supported caps across a rehash.
1613func (config *Config) Diff(oldConfig *Config) (addedCaps, removedCaps *caps.Set) {
1614	addedCaps = caps.NewSet()
1615	removedCaps = caps.NewSet()
1616	if oldConfig == nil {
1617		return
1618	}
1619
1620	if oldConfig.Server.capValues[caps.Languages] != config.Server.capValues[caps.Languages] {
1621		// XXX updated caps get a DEL line and then a NEW line with the new value
1622		addedCaps.Add(caps.Languages)
1623		removedCaps.Add(caps.Languages)
1624	}
1625
1626	if !oldConfig.Accounts.AuthenticationEnabled && config.Accounts.AuthenticationEnabled {
1627		addedCaps.Add(caps.SASL)
1628	} else if oldConfig.Accounts.AuthenticationEnabled && !config.Accounts.AuthenticationEnabled {
1629		removedCaps.Add(caps.SASL)
1630	}
1631
1632	if oldConfig.Limits.Multiline.MaxBytes != 0 && config.Limits.Multiline.MaxBytes == 0 {
1633		removedCaps.Add(caps.Multiline)
1634	} else if oldConfig.Limits.Multiline.MaxBytes == 0 && config.Limits.Multiline.MaxBytes != 0 {
1635		addedCaps.Add(caps.Multiline)
1636	} else if oldConfig.Limits.Multiline != config.Limits.Multiline {
1637		removedCaps.Add(caps.Multiline)
1638		addedCaps.Add(caps.Multiline)
1639	}
1640
1641	if oldConfig.Server.STS.Enabled != config.Server.STS.Enabled || oldConfig.Server.capValues[caps.STS] != config.Server.capValues[caps.STS] {
1642		// XXX: STS is always removed by CAP NEW sts=duration=0, not CAP DEL
1643		// so the appropriate notify is always a CAP NEW; put it in addedCaps for any change
1644		addedCaps.Add(caps.STS)
1645	}
1646
1647	return
1648}
1649
1650// determine whether we need to resize / create / destroy
1651// the in-memory history buffers:
1652func (config *Config) historyChangedFrom(oldConfig *Config) bool {
1653	return config.History.Enabled != oldConfig.History.Enabled ||
1654		config.History.ChannelLength != oldConfig.History.ChannelLength ||
1655		config.History.ClientLength != oldConfig.History.ClientLength ||
1656		config.History.AutoresizeWindow != oldConfig.History.AutoresizeWindow ||
1657		config.History.Persistent != oldConfig.History.Persistent
1658}
1659
1660func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, folded *regexp.Regexp, err error) {
1661	if strings.Count(guestFormat, "?") != 0 || strings.Count(guestFormat, "*") != 1 {
1662		err = errors.New("guest format must contain 1 '*' and no '?'s")
1663		return
1664	}
1665
1666	standard, err = utils.CompileGlob(guestFormat, true)
1667	if err != nil {
1668		return
1669	}
1670
1671	starIndex := strings.IndexByte(guestFormat, '*')
1672	initial := guestFormat[:starIndex]
1673	final := guestFormat[starIndex+1:]
1674	initialFolded, err := casefoldWithSetting(initial, casemapping)
1675	if err != nil {
1676		return
1677	}
1678	finalFolded, err := casefoldWithSetting(final, casemapping)
1679	if err != nil {
1680		return
1681	}
1682	folded, err = utils.CompileGlob(fmt.Sprintf("%s*%s", initialFolded, finalFolded), false)
1683	return
1684}
1685
1686func (config *Config) loadMOTD() error {
1687	if config.Server.MOTD != "" {
1688		file, err := os.Open(config.Server.MOTD)
1689		if err != nil {
1690			return err
1691		}
1692		defer file.Close()
1693		contents, err := io.ReadAll(file)
1694		if err != nil {
1695			return err
1696		}
1697
1698		lines := bytes.Split(contents, []byte{'\n'})
1699		for i, line := range lines {
1700			lineToSend := string(bytes.TrimRight(line, "\r\n"))
1701			if len(lineToSend) == 0 && i == len(lines)-1 {
1702				// if the last line of the MOTD was properly terminated with \n,
1703				// there's no need to send a blank line to clients
1704				continue
1705			}
1706			if config.Server.MOTDFormatting {
1707				lineToSend = ircfmt.Unescape(lineToSend)
1708			}
1709			// "- " is the required prefix for MOTD
1710			lineToSend = fmt.Sprintf("- %s", lineToSend)
1711			config.Server.motdLines = append(config.Server.motdLines, lineToSend)
1712		}
1713	}
1714	return nil
1715}
1716