diff --git a/irc/accounts.go b/irc/accounts.go index 1345ebe5..c617f14b 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -5,7 +5,6 @@ package irc import ( "encoding/json" - "errors" "fmt" "net/smtp" "strconv" @@ -483,7 +482,7 @@ func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount str } else if callbackNamespace == "mailto" { return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue) } else { - return "", errors.New(fmt.Sprintf("Callback not implemented: %s", callbackNamespace)) + return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace) } } @@ -1265,7 +1264,6 @@ func (am *AccountManager) Logout(client *Client) { } } am.accountToClients[casefoldedAccount] = remainingClients - return } var ( diff --git a/irc/channel.go b/irc/channel.go index 4461ea39..6a93e901 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -1094,34 +1094,6 @@ func (channel *Channel) ShowMaskList(client *Client, mode modes.Mode, rb *Respon rb.Add(nil, client.server.name, rplendoflist, nick, channel.name, client.t("End of list")) } -func (channel *Channel) applyModeMask(client *Client, mode modes.Mode, op modes.ModeOp, mask string, rb *ResponseBuffer) bool { - list := channel.lists[mode] - if list == nil { - // This should never happen, but better safe than panicky. - return false - } - - if (op == modes.List) || (mask == "") { - channel.ShowMaskList(client, mode, rb) - return false - } - - if !channel.ClientIsAtLeast(client, modes.ChannelOperator) { - rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You're not a channel operator")) - return false - } - - if op == modes.Add { - return list.Add(mask) - } - - if op == modes.Remove { - return list.Remove(mask) - } - - return false -} - // Quit removes the given client from the channel func (channel *Channel) Quit(client *Client) { channelEmpty := func() bool { diff --git a/irc/client.go b/irc/client.go index f81e919b..2ed9bcc8 100644 --- a/irc/client.go +++ b/irc/client.go @@ -557,14 +557,12 @@ func (client *Client) tryResume() (success bool) { func (client *Client) tryResumeChannels() { details := client.resumeDetails - channels := make([]*Channel, len(details.Channels)) for _, name := range details.Channels { channel := client.server.channels.Get(name) if channel == nil { continue } channel.Resume(client, details.OldClient, details.Timestamp) - channels = append(channels, channel) } // replay direct PRIVSMG history @@ -868,7 +866,8 @@ func (client *Client) LoggedIntoAccount() bool { func (client *Client) RplISupport(rb *ResponseBuffer) { translatedISupport := client.t("are supported by this server") nick := client.Nick() - for _, cachedTokenLine := range client.server.ISupport().CachedReply { + config := client.server.Config() + for _, cachedTokenLine := range config.Server.isupport.CachedReply { length := len(cachedTokenLine) + 2 tokenline := make([]string, length) tokenline[0] = nick @@ -1122,7 +1121,8 @@ var ( func (session *Session) SendRawMessage(message ircmsg.IrcMessage, blocking bool) error { // use dumb hack to force the last param to be a trailing param if required var usedTrailingHack bool - if commandsThatMustUseTrailing[message.Command] && len(message.Params) > 0 { + config := session.client.server.Config() + if config.Server.Compatibility.forceTrailing && commandsThatMustUseTrailing[message.Command] && len(message.Params) > 0 { lastParam := message.Params[len(message.Params)-1] // to force trailing, we ensure the final param contains a space if strings.IndexByte(lastParam, ' ') == -1 { diff --git a/irc/config.go b/irc/config.go index c585601b..597b9309 100644 --- a/irc/config.go +++ b/irc/config.go @@ -20,6 +20,7 @@ import ( "code.cloudfoundry.org/bytefmt" "github.com/oragono/oragono/irc/connection_limits" "github.com/oragono/oragono/irc/custime" + "github.com/oragono/oragono/irc/isupport" "github.com/oragono/oragono/irc/languages" "github.com/oragono/oragono/irc/logger" "github.com/oragono/oragono/irc/modes" @@ -280,15 +281,22 @@ type Config struct { STS STSConfig CheckIdent bool `yaml:"check-ident"` MOTD string + motdLines []string MOTDFormatting bool `yaml:"motd-formatting"` ProxyAllowedFrom []string `yaml:"proxy-allowed-from"` proxyAllowedFromNets []net.IPNet WebIRC []webircConfig `yaml:"webirc"` MaxSendQString string `yaml:"max-sendq"` MaxSendQBytes int - AllowPlaintextResume bool `yaml:"allow-plaintext-resume"` - ConnectionLimiter connection_limits.LimiterConfig `yaml:"connection-limits"` - ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"` + AllowPlaintextResume bool `yaml:"allow-plaintext-resume"` + Compatibility struct { + ForceTrailing *bool `yaml:"force-trailing"` + forceTrailing bool + SendUnprefixedSasl bool `yaml:"send-unprefixed-sasl"` + } + isupport isupport.List + ConnectionLimiter connection_limits.LimiterConfig `yaml:"connection-limits"` + ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"` } Languages struct { @@ -385,7 +393,7 @@ func (conf *Config) OperatorClasses() (map[string]*OperClass, error) { // get inhereted info from other operclasses if len(info.Extends) > 0 { - einfo, _ := ocs[info.Extends] + einfo := ocs[info.Extends] for capab := range einfo.Capabilities { oc.Capabilities[capab] = true @@ -698,6 +706,20 @@ func LoadConfig(filename string) (config *Config, err error) { config.Channels.Registration.MaxChannelsPerAccount = 15 } + forceTrailingPtr := config.Server.Compatibility.ForceTrailing + if forceTrailingPtr != nil { + config.Server.Compatibility.forceTrailing = *forceTrailingPtr + } else { + config.Server.Compatibility.forceTrailing = true + } + + config.loadMOTD() + + err = config.generateISupport() + if err != nil { + return nil, err + } + // 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 diff --git a/irc/getters.go b/irc/getters.go index 915a652c..cdeadea5 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -5,24 +5,20 @@ package irc import ( "net" + "sync/atomic" "time" + "unsafe" - "github.com/oragono/oragono/irc/isupport" "github.com/oragono/oragono/irc/languages" "github.com/oragono/oragono/irc/modes" ) func (server *Server) Config() (config *Config) { - server.configurableStateMutex.RLock() - config = server.config - server.configurableStateMutex.RUnlock() - return + return (*Config)(atomic.LoadPointer(&server.config)) } -func (server *Server) ISupport() *isupport.List { - server.configurableStateMutex.RLock() - defer server.configurableStateMutex.RUnlock() - return server.isupport +func (server *Server) SetConfig(config *Config) { + atomic.StorePointer(&server.config, unsafe.Pointer(config)) } func (server *Server) Limits() Limits { @@ -54,9 +50,7 @@ func (server *Server) GetOperator(name string) (oper *Oper) { if err != nil { return } - server.configurableStateMutex.RLock() - defer server.configurableStateMutex.RUnlock() - return server.config.operators[name] + return server.Config().operators[name] } func (server *Server) Languages() (lm *languages.Manager) { diff --git a/irc/handlers.go b/irc/handlers.go index 5194a201..77bb2ea0 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -298,7 +298,9 @@ func accVerifyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb // AUTHENTICATE [||*] func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + config := server.Config() details := client.Details() + if details.account != "" { rb.Add(nil, server.name, ERR_SASLALREADY, details.nick, client.t("You're already logged into an account")) return false @@ -321,7 +323,14 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, if mechanismIsEnabled { client.saslInProgress = true client.saslMechanism = mechanism - rb.Add(nil, server.name, "AUTHENTICATE", "+") + if !config.Server.Compatibility.SendUnprefixedSasl { + // normal behavior + rb.Add(nil, server.name, "AUTHENTICATE", "+") + } else { + // gross hack: send a raw message to ensure no tags or prefix + rb.Flush(true) + rb.session.SendRawMessage(ircmsg.MakeMessage(nil, "", "AUTHENTICATE", "+"), true) + } } else { rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed")) } @@ -1175,20 +1184,14 @@ func inviteHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re func isonHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { var nicks = msg.Params - var err error - var casefoldedNick string - ison := make([]string, 0) + ison := make([]string, 0, len(msg.Params)) for _, nick := range nicks { - casefoldedNick, err = CasefoldName(nick) - if err != nil { - continue - } - if iclient := server.clients.Get(casefoldedNick); iclient != nil { - ison = append(ison, iclient.nick) + if iclient := server.clients.Get(nick); iclient != nil { + ison = append(ison, iclient.Nick()) } } - rb.Add(nil, server.name, RPL_ISON, client.nick, strings.Join(nicks, " ")) + rb.Add(nil, server.name, RPL_ISON, client.nick, strings.Join(ison, " ")) return false } @@ -2089,7 +2092,7 @@ func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp // OPER func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - if client.HasMode(modes.Operator) == true { + if client.HasMode(modes.Operator) { rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), "OPER", client.t("You're already opered-up!")) return false } diff --git a/irc/isupport/list.go b/irc/isupport/list.go index cede85e3..61215e54 100644 --- a/irc/isupport/list.go +++ b/irc/isupport/list.go @@ -22,9 +22,13 @@ type List struct { // NewList returns a new List func NewList() *List { var il List + il.Initialize() + return &il +} + +func (il *List) Initialize() { il.Tokens = make(map[string]*string) il.CachedReply = make([][]string, 0) - return &il } // Add adds an RPL_ISUPPORT token to our internal list diff --git a/irc/modes/modes.go b/irc/modes/modes.go index 0aca61c6..05e93938 100644 --- a/irc/modes/modes.go +++ b/irc/modes/modes.go @@ -170,7 +170,7 @@ func SplitChannelMembershipPrefixes(target string) (prefixes string, name string prefixes = target[:i+1] name = target[i+1:] default: - break + return } } diff --git a/irc/semaphores.go b/irc/semaphores.go index d6008c58..1ee4df2c 100644 --- a/irc/semaphores.go +++ b/irc/semaphores.go @@ -36,5 +36,4 @@ func (serversem *ServerSemaphores) Initialize() { capacity = MaxServerSemaphoreCapacity } serversem.ClientDestroy.Initialize(capacity) - return } diff --git a/irc/server.go b/irc/server.go index a5ea566f..87ced2b0 100644 --- a/irc/server.go +++ b/irc/server.go @@ -19,11 +19,11 @@ import ( "sync" "syscall" "time" + "unsafe" "github.com/goshuirc/irc-go/ircfmt" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/connection_limits" - "github.com/oragono/oragono/irc/isupport" "github.com/oragono/oragono/irc/logger" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" @@ -61,37 +61,34 @@ type ListenerWrapper struct { // Server is the main Oragono server. type Server struct { - accounts AccountManager - channels ChannelManager - channelRegistry ChannelRegistry - clients ClientManager - config *Config - configFilename string - configurableStateMutex sync.RWMutex // tier 1; generic protection for server state modified by rehash() - connectionLimiter *connection_limits.Limiter - connectionThrottler *connection_limits.Throttler - ctime time.Time - dlines *DLineManager - helpIndexManager HelpIndexManager - isupport *isupport.List - klines *KLineManager - listeners map[string]*ListenerWrapper - logger *logger.Manager - monitorManager *MonitorManager - motdLines []string - name string - nameCasefolded string - rehashMutex sync.Mutex // tier 4 - rehashSignal chan os.Signal - pprofServer *http.Server - resumeManager ResumeManager - signals chan os.Signal - snomasks *SnoManager - store *buntdb.DB - torLimiter connection_limits.TorLimiter - whoWas WhoWasList - stats Stats - semaphores ServerSemaphores + accounts AccountManager + channels ChannelManager + channelRegistry ChannelRegistry + clients ClientManager + config unsafe.Pointer + configFilename string + connectionLimiter *connection_limits.Limiter + connectionThrottler *connection_limits.Throttler + ctime time.Time + dlines *DLineManager + helpIndexManager HelpIndexManager + klines *KLineManager + listeners map[string]*ListenerWrapper + logger *logger.Manager + monitorManager *MonitorManager + name string + nameCasefolded string + rehashMutex sync.Mutex // tier 4 + rehashSignal chan os.Signal + pprofServer *http.Server + resumeManager ResumeManager + signals chan os.Signal + snomasks *SnoManager + store *buntdb.DB + torLimiter connection_limits.TorLimiter + whoWas WhoWasList + stats Stats + semaphores ServerSemaphores } var ( @@ -140,13 +137,12 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) { } // setISupport sets up our RPL_ISUPPORT reply. -func (server *Server) setISupport() (err error) { +func (config *Config) generateISupport() (err error) { maxTargetsString := strconv.Itoa(maxTargets) - config := server.Config() - // add RPL_ISUPPORT tokens - isupport := isupport.NewList() + isupport := &config.Server.isupport + isupport.Initialize() isupport.Add("AWAYLEN", strconv.Itoa(config.Limits.AwayLen)) isupport.Add("CASEMAPPING", "ascii") isupport.Add("CHANMODES", strings.Join([]string{modes.Modes{modes.BanMask, modes.ExceptMask, modes.InviteMask}.String(), "", modes.Modes{modes.UserLimit, modes.Key}.String(), modes.Modes{modes.InviteOnly, modes.Moderated, modes.NoOutside, modes.OpOnlyTopic, modes.ChanRoleplaying, modes.Secret}.String()}, ",")) @@ -174,23 +170,9 @@ func (server *Server) setISupport() (err error) { isupport.Add("UTF8MAPPING", casemappingName) err = isupport.RegenerateCachedReply() - if err != nil { - return - } - - server.configurableStateMutex.Lock() - server.isupport = isupport - server.configurableStateMutex.Unlock() return } -func loadChannelList(channel *Channel, list string, maskMode modes.Mode) { - if list == "" { - return - } - channel.lists[maskMode].AddAll(strings.Split(list, " ")) -} - // Shutdown shuts down the server. func (server *Server) Shutdown() { //TODO(dan): Make sure we disallow new nicks @@ -474,9 +456,7 @@ func (client *Client) t(originalString string) string { // MOTD serves the Message of the Day. func (server *Server) MOTD(client *Client, rb *ResponseBuffer) { - server.configurableStateMutex.RLock() - motdLines := server.motdLines - server.configurableStateMutex.RUnlock() + motdLines := server.Config().Server.motdLines if len(motdLines) < 1 { rb.Add(nil, server.name, ERR_NOMOTD, client.nick, client.t("MOTD File is missing")) @@ -535,9 +515,7 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) { tLanguages := target.Languages() if 0 < len(tLanguages) { params := []string{cnick, tnick} - for _, str := range client.server.Languages().Codes(tLanguages) { - params = append(params, str) - } + params = append(params, client.server.Languages().Codes(tLanguages)...) params = append(params, client.t("can speak these languages")) rb.Add(nil, client.server.name, RPL_WHOISLANGUAGE, params...) } @@ -609,7 +587,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted") } else if server.name != config.Server.Name { return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted") - } else if server.config.Datastore.Path != config.Datastore.Path { + } else if server.Config().Datastore.Path != config.Datastore.Path { return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted") } } @@ -780,12 +758,8 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { } } - server.loadMOTD(config.Server.MOTD, config.Server.MOTDFormatting) - // save a pointer to the new config - server.configurableStateMutex.Lock() - server.config = config - server.configurableStateMutex.Unlock() + server.SetConfig(config) server.logger.Info("server", "Using datastore", config.Datastore.Path) if initial { @@ -798,13 +772,8 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { // set RPL_ISUPPORT var newISupportReplies [][]string - oldISupportList := server.ISupport() - err = server.setISupport() - if err != nil { - return err - } - if oldISupportList != nil { - newISupportReplies = oldISupportList.GetDifference(server.ISupport()) + if oldConfig != nil { + newISupportReplies = oldConfig.Server.isupport.GetDifference(&config.Server.isupport) } // we are now open for business @@ -859,11 +828,9 @@ func (server *Server) setupPprofListener(config *Config) { } } -func (server *Server) loadMOTD(motdPath string, useFormatting bool) error { - server.logger.Info("server", "Using MOTD", motdPath) - motdLines := make([]string, 0) - if motdPath != "" { - file, err := os.Open(motdPath) +func (config *Config) loadMOTD() (err error) { + if config.Server.MOTD != "" { + file, err := os.Open(config.Server.MOTD) if err == nil { defer file.Close() @@ -875,7 +842,7 @@ func (server *Server) loadMOTD(motdPath string, useFormatting bool) error { } line = strings.TrimRight(line, "\r\n") - if useFormatting { + if config.Server.MOTDFormatting { line = ircfmt.Unescape(line) } @@ -883,17 +850,11 @@ func (server *Server) loadMOTD(motdPath string, useFormatting bool) error { // bursting it out to clients easier line = fmt.Sprintf("- %s", line) - motdLines = append(motdLines, line) + config.Server.motdLines = append(config.Server.motdLines, line) } - } else { - return err } } - - server.configurableStateMutex.Lock() - server.motdLines = motdLines - server.configurableStateMutex.Unlock() - return nil + return } func (server *Server) loadDatastore(config *Config) error { diff --git a/irc/snomanager.go b/irc/snomanager.go index 38ab8d6c..d6f3b657 100644 --- a/irc/snomanager.go +++ b/irc/snomanager.go @@ -52,7 +52,7 @@ func (m *SnoManager) RemoveMasks(client *Client, masks ...sno.Mask) { for _, mask := range masks { currentClientList := m.sendLists[mask] - if currentClientList == nil || len(currentClientList) == 0 { + if len(currentClientList) == 0 { continue } @@ -70,7 +70,7 @@ func (m *SnoManager) RemoveClient(client *Client) { for mask := range m.sendLists { currentClientList := m.sendLists[mask] - if currentClientList == nil || len(currentClientList) == 0 { + if len(currentClientList) == 0 { continue } @@ -87,7 +87,7 @@ func (m *SnoManager) Send(mask sno.Mask, content string) { currentClientList := m.sendLists[mask] - if currentClientList == nil || len(currentClientList) == 0 { + if len(currentClientList) == 0 { return } diff --git a/oragono.yaml b/oragono.yaml index dd38c929..0e5e68e5 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -125,6 +125,21 @@ server: # this should be big enough to hold bursts of channel/direct messages max-sendq: 16k + # compatibility with legacy clients + compatibility: + # many clients require that the final parameter of certain messages be an + # RFC1459 trailing parameter, i.e., prefixed with :, whether or not this is + # actually required. this forces Oragono to send those parameters + # as trailings. this is recommended unless you're testing clients for conformance; + # defaults to true when unset for that reason. + force-trailing: true + + # some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower, Adium) do not + # respond correctly to SASL messages with the server name as a prefix: + # https://github.com/znc/znc/issues/1212 + # this works around that bug, allowing them to use SASL. + send-unprefixed-sasl: true + # maximum number of connections per subnet connection-limits: # whether to enforce connection limits or not