diff --git a/irc/channel.go b/irc/channel.go index 8bfa487a..7144da9b 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -346,6 +346,9 @@ func (channel *Channel) IsEmpty() bool { // Join joins the given client to this channel (if they can be joined). func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) { + account := client.Account() + nickMaskCasefolded := client.NickMaskCasefolded() + channel.stateMutex.RLock() chname := channel.name chcfname := channel.nameCasefolded @@ -354,6 +357,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp limit := channel.userLimit chcount := len(channel.members) _, alreadyJoined := channel.members[client] + persistentMode := channel.accountToUMode[account] channel.stateMutex.RUnlock() if alreadyJoined { @@ -361,9 +365,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp return } - account := client.Account() - nickMaskCasefolded := client.NickMaskCasefolded() - hasPrivs := isSajoin || (founder != "" && founder == account) + // the founder can always join (even if they disabled auto +q on join); + // anyone who automatically receives halfop or higher can always join + hasPrivs := isSajoin || (founder != "" && founder == account) || (persistentMode != 0 && persistentMode != modes.Voice) if !hasPrivs && limit != 0 && chcount >= limit { rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l")) @@ -404,7 +408,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp if newChannel { givenMode = modes.ChannelOperator } else { - givenMode = channel.accountToUMode[account] + givenMode = persistentMode } if givenMode != 0 { channel.members[client].SetMode(givenMode, true) @@ -803,6 +807,8 @@ func (channel *Channel) sendSplitMessage(msgid, cmd string, histType history.Ite nickmask := client.NickMaskString() account := client.AccountName() + now := time.Now().UTC() + for _, member := range channel.Members() { if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) { // STATUSMSG @@ -817,11 +823,10 @@ func (channel *Channel) sendSplitMessage(msgid, cmd string, histType history.Ite tagsToUse = clientOnlyTags } - // TODO(slingamn) evaluate an optimization where we reuse `nickmask` and `account` if message == nil { - member.SendFromClient(msgid, client, tagsToUse, cmd, channel.name) + member.sendFromClientInternal(false, now, msgid, nickmask, account, tagsToUse, cmd, channel.name) } else { - member.SendSplitMsgFromClient(msgid, client, tagsToUse, cmd, channel.name, *message) + member.sendSplitMsgFromClientInternal(false, now, msgid, nickmask, account, tagsToUse, cmd, channel.name, *message) } } @@ -831,6 +836,7 @@ func (channel *Channel) sendSplitMessage(msgid, cmd string, histType history.Ite Message: *message, Nick: nickmask, AccountName: account, + Time: now, }) } diff --git a/irc/client.go b/irc/client.go index fe294d3c..ebcaa066 100644 --- a/irc/client.go +++ b/irc/client.go @@ -28,7 +28,7 @@ import ( const ( // IdentTimeoutSeconds is how many seconds before our ident (username) check times out. IdentTimeoutSeconds = 1.5 - IRCv3TimestampFormat = "2006-01-02T15:04:05.999Z" + IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z" ) var ( @@ -332,12 +332,6 @@ func (client *Client) Active() { client.atime = time.Now() } -// Touch marks the client as alive (as it it has a connection to us and we -// can receive messages from it). -func (client *Client) Touch() { - client.idletimer.Touch() -} - // Ping sends the client a PING message. func (client *Client) Ping() { client.Send(nil, "", "PING", client.nick) diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 09a67534..65ed3a4b 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -189,7 +189,7 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) { clients.RLock() defer clients.RUnlock() for _, client := range clients.byNick { - if matcher.Match(client.nickMaskCasefolded) { + if matcher.Match(client.NickMaskCasefolded()) { set.Add(client) } } @@ -209,7 +209,7 @@ func (clients *ClientManager) Find(userhost string) *Client { clients.RLock() defer clients.RUnlock() for _, client := range clients.byNick { - if matcher.Match(client.nickMaskCasefolded) { + if matcher.Match(client.NickMaskCasefolded()) { matchedClient = client break } diff --git a/irc/commands.go b/irc/commands.go index 7fcf1463..af2ee92b 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -12,13 +12,12 @@ import ( // Command represents a command accepted from a client. type Command struct { - handler func(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool - oper bool - usablePreReg bool - leaveClientActive bool // if true, leaves the client active time alone. reversed because we can't default a struct element to True - leaveClientIdle bool - minParams int - capabs []string + handler func(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool + oper bool + usablePreReg bool + leaveClientIdle bool // if true, leaves the client active time alone + minParams int + capabs []string } // Run runs this command with the given client/message. @@ -54,11 +53,10 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b server.tryRegister(client) } - if !cmd.leaveClientIdle { - client.Touch() - } + // most servers do this only for PING/PONG, but we'll do it for any command: + client.idletimer.Touch() - if !cmd.leaveClientActive { + if !cmd.leaveClientIdle { client.Active() } @@ -118,8 +116,9 @@ func init() { minParams: 2, }, "ISON": { - handler: isonHandler, - minParams: 1, + handler: isonHandler, + minParams: 1, + leaveClientIdle: true, }, "JOIN": { handler: joinHandler, @@ -200,16 +199,16 @@ func init() { minParams: 1, }, "PING": { - handler: pingHandler, - usablePreReg: true, - minParams: 1, - leaveClientActive: true, + handler: pingHandler, + usablePreReg: true, + minParams: 1, + leaveClientIdle: true, }, "PONG": { - handler: pongHandler, - usablePreReg: true, - minParams: 1, - leaveClientActive: true, + handler: pongHandler, + usablePreReg: true, + minParams: 1, + leaveClientIdle: true, }, "PRIVMSG": { handler: privmsgHandler, diff --git a/irc/handlers.go b/irc/handlers.go index ee054359..d65d0280 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2478,16 +2478,25 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res return false } + handleService := func(nick string) bool { + cfnick, _ := CasefoldName(nick) + service, ok := OragonoServices[cfnick] + if !ok { + return false + } + clientNick := client.Nick() + rb.Add(nil, client.server.name, RPL_WHOISUSER, clientNick, service.Name, service.Name, "localhost", "*", fmt.Sprintf(client.t("Network service, for more info /msg %s HELP"), service.Name)) + // hehe + if client.HasMode(modes.TLS) { + rb.Add(nil, client.server.name, RPL_WHOISSECURE, clientNick, service.Name, client.t("is using a secure connection")) + } + return true + } + if client.HasMode(modes.Operator) { - masks := strings.Split(masksString, ",") - for _, mask := range masks { - casefoldedMask, err := Casefold(mask) - if err != nil { - rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick")) - continue - } - matches := server.clients.FindAll(casefoldedMask) - if len(matches) == 0 { + for _, mask := range strings.Split(masksString, ",") { + matches := server.clients.FindAll(mask) + if len(matches) == 0 && !handleService(mask) { rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, client.t("No such nick")) continue } @@ -2496,15 +2505,15 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res } } } else { - // only get the first request - casefoldedMask, err := Casefold(strings.Split(masksString, ",")[0]) - mclient := server.clients.Get(casefoldedMask) - if err != nil || mclient == nil { - rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, masksString, client.t("No such nick")) - // fall through, ENDOFWHOIS is always sent - } else { + // only get the first request; also require a nick, not a mask + nick := strings.Split(masksString, ",")[0] + mclient := server.clients.Get(nick) + if mclient != nil { client.getWhoisOf(mclient, rb) + } else if !handleService(nick) { + rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, masksString, client.t("No such nick")) } + // fall through, ENDOFWHOIS is always sent } rb.Add(nil, server.name, RPL_ENDOFWHOIS, client.nick, masksString, client.t("End of /WHOIS list")) return false diff --git a/irc/isupport/list.go b/irc/isupport/list.go index 402e8265..cede85e3 100644 --- a/irc/isupport/list.go +++ b/irc/isupport/list.go @@ -3,8 +3,11 @@ package isupport -import "fmt" -import "sort" +import ( + "fmt" + "sort" + "strings" +) const ( maxLastArgLength = 400 @@ -102,7 +105,7 @@ func (il *List) GetDifference(newil *List) [][]string { } // RegenerateCachedReply regenerates the cached RPL_ISUPPORT reply -func (il *List) RegenerateCachedReply() { +func (il *List) RegenerateCachedReply() (err error) { il.CachedReply = make([][]string, 0) var length int // Length of the current cache var cache []string // Token list cache @@ -116,6 +119,10 @@ func (il *List) RegenerateCachedReply() { for _, name := range tokens { token := getTokenString(name, il.Tokens[name]) + if token[0] == ':' || strings.Contains(token, " ") { + err = fmt.Errorf("bad isupport token (cannot contain spaces or start with :): %s", token) + continue + } if len(token)+length <= maxLastArgLength { // account for the space separating tokens @@ -136,4 +143,6 @@ func (il *List) RegenerateCachedReply() { if len(cache) > 0 { il.CachedReply = append(il.CachedReply, cache) } + + return } diff --git a/irc/isupport/list_test.go b/irc/isupport/list_test.go index 4172760a..47fb72ef 100644 --- a/irc/isupport/list_test.go +++ b/irc/isupport/list_test.go @@ -26,7 +26,10 @@ func TestISUPPORT(t *testing.T) { tListLong.AddNoValue("D") tListLong.AddNoValue("E") tListLong.AddNoValue("F") - tListLong.RegenerateCachedReply() + err := tListLong.RegenerateCachedReply() + if err != nil { + t.Error(err) + } longReplies := [][]string{ {"1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D"}, @@ -44,7 +47,10 @@ func TestISUPPORT(t *testing.T) { tList1.Add("INVEX", "i") tList1.AddNoValue("EXTBAN") tList1.Add("RANDKILL", "whenever") - tList1.RegenerateCachedReply() + err = tList1.RegenerateCachedReply() + if err != nil { + t.Error(err) + } expected := [][]string{{"CASEMAPPING=rfc1459-strict", "EXTBAN", "INVEX=i", "RANDKILL=whenever", "SASL=yes"}} if !reflect.DeepEqual(tList1.CachedReply, expected) { @@ -58,7 +64,10 @@ func TestISUPPORT(t *testing.T) { tList2.AddNoValue("INVEX") tList2.Add("EXTBAN", "TestBah") tList2.AddNoValue("STABLEKILL") - tList2.RegenerateCachedReply() + err = tList2.RegenerateCachedReply() + if err != nil { + t.Error(err) + } expected = [][]string{{"CASEMAPPING=ascii", "EXTBAN=TestBah", "INVEX", "SASL=yes", "STABLEKILL"}} if !reflect.DeepEqual(tList2.CachedReply, expected) { @@ -72,3 +81,26 @@ func TestISUPPORT(t *testing.T) { t.Error("difference reply does not match expected difference reply") } } + +func TestBadToken(t *testing.T) { + list := NewList() + list.Add("NETWORK", "Bad Network Name") + list.Add("SASL", "yes") + list.Add("CASEMAPPING", "rfc1459-strict") + list.Add("INVEX", "i") + list.AddNoValue("EXTBAN") + + err := list.RegenerateCachedReply() + if err == nil { + t.Error("isupport token generation should fail due to space in network name") + } + + // should produce a list containing the other, valid params + numParams := 0 + for _, tokenLine := range list.CachedReply { + numParams += len(tokenLine) + } + if numParams != 4 { + t.Errorf("expected the other 4 params to be generated, got %v", list.CachedReply) + } +} diff --git a/irc/logger/logger.go b/irc/logger/logger.go index c6014482..f3e98c4b 100644 --- a/irc/logger/logger.go +++ b/irc/logger/logger.go @@ -250,7 +250,7 @@ func (logger *singleLogger) Log(level Level, logType string, messageParts ...str } sep := grey(":") - fullStringFormatted := fmt.Sprintf("%s %s %s %s %s %s ", timeGrey(time.Now().UTC().Format("2006-01-02T15:04:05Z")), sep, levelDisplay, sep, section(logType), sep) + fullStringFormatted := fmt.Sprintf("%s %s %s %s %s %s ", timeGrey(time.Now().UTC().Format("2006-01-02T15:04:05.000Z")), sep, levelDisplay, sep, section(logType), sep) fullStringRaw := fmt.Sprintf("%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType) for i, p := range messageParts { fullStringFormatted += p diff --git a/irc/server.go b/irc/server.go index a870db0d..1afee925 100644 --- a/irc/server.go +++ b/irc/server.go @@ -9,7 +9,6 @@ import ( "bufio" "crypto/tls" "fmt" - "math/rand" "net" "net/http" _ "net/http/pprof" @@ -148,7 +147,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) { } // setISupport sets up our RPL_ISUPPORT reply. -func (server *Server) setISupport() { +func (server *Server) setISupport() (err error) { maxTargetsString := strconv.Itoa(maxTargets) config := server.Config() @@ -193,11 +192,15 @@ func (server *Server) setISupport() { isupport.Add("REGCREDTYPES", "passphrase,certfp") } - isupport.RegenerateCachedReply() + 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) { @@ -371,13 +374,7 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, bindMod // generateMessageID returns a network-unique message ID. func (server *Server) generateMessageID() string { - // we don't need the full like 30 chars since the unixnano below handles - // most of our uniqueness requirements, so just truncate at 5 - lastbit := strconv.FormatInt(rand.Int63(), 36) - if 5 < len(lastbit) { - lastbit = lastbit[:4] - } - return fmt.Sprintf("%s%s", strconv.FormatInt(time.Now().UTC().UnixNano(), 36), lastbit) + return utils.GenerateSecretToken() } // @@ -794,7 +791,10 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { // set RPL_ISUPPORT var newISupportReplies [][]string oldISupportList := server.ISupport() - server.setISupport() + err = server.setISupport() + if err != nil { + return err + } if oldISupportList != nil { newISupportReplies = oldISupportList.GetDifference(server.ISupport()) } diff --git a/oragono.go b/oragono.go index b7a9eae7..03242897 100644 --- a/oragono.go +++ b/oragono.go @@ -8,10 +8,8 @@ package main import ( "fmt" "log" - "math/rand" "strings" "syscall" - "time" "github.com/docopt/docopt-go" "github.com/oragono/oragono/irc" @@ -114,7 +112,6 @@ Options: } } } else if arguments["run"].(bool) { - rand.Seed(time.Now().UTC().UnixNano()) if !arguments["--quiet"].(bool) { logman.Info("startup", fmt.Sprintf("Oragono v%s starting", irc.SemVer)) if commit == "" {