diff --git a/bridge.go b/bridge.go index baab546..8c3e62d 100644 --- a/bridge.go +++ b/bridge.go @@ -29,10 +29,7 @@ func forwardChannel(dc *downstreamConn, ch *upstreamChannel) { // TODO: send multiple members in each message for nick, membership := range ch.Members { - s := dc.marshalNick(ch.conn, nick) - if membership != 0 { - s = string(membership) + s - } + s := membership.String() + dc.marshalNick(ch.conn, nick) dc.SendMessage(&irc.Message{ Prefix: dc.srv.prefix(), diff --git a/downstream.go b/downstream.go index 0552e4e..d6dbeb8 100644 --- a/downstream.go +++ b/downstream.go @@ -851,41 +851,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error { modeStr = msg.Params[1] } - uc, upstreamName, err := dc.unmarshalEntity(name) - if err != nil { - return err - } - - if uc.isChannel(upstreamName) { - // TODO: handle MODE channel mode arguments - if modeStr != "" { - uc.SendMessage(&irc.Message{ - Command: "MODE", - Params: []string{upstreamName, modeStr}, - }) - } else { - ch, ok := uc.channels[upstreamName] - if !ok { - return ircError{&irc.Message{ - Command: irc.ERR_NOSUCHCHANNEL, - Params: []string{dc.nick, name, "No such channel"}, - }} - } - - dc.SendMessage(&irc.Message{ - Prefix: dc.srv.prefix(), - Command: irc.RPL_CHANNELMODEIS, - Params: []string{dc.nick, name, string(ch.modes)}, - }) - } - } else { - if name != dc.nick { - return ircError{&irc.Message{ - Command: irc.ERR_USERSDONTMATCH, - Params: []string{dc.nick, "Cannot change mode for other users"}, - }} - } - + if name == dc.nick { if modeStr != "" { dc.forEachUpstream(func(uc *upstreamConn) { uc.SendMessage(&irc.Message{ @@ -900,6 +866,52 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error { Params: []string{dc.nick, ""}, // TODO }) } + return nil + } + + uc, upstreamName, err := dc.unmarshalEntity(name) + if err != nil { + return err + } + + if !uc.isChannel(upstreamName) { + return ircError{&irc.Message{ + Command: irc.ERR_USERSDONTMATCH, + Params: []string{dc.nick, "Cannot change mode for other users"}, + }} + } + + if modeStr != "" { + params := []string{upstreamName, modeStr} + params = append(params, msg.Params[2:]...) + uc.SendMessage(&irc.Message{ + Command: "MODE", + Params: params, + }) + } else { + ch, ok := uc.channels[upstreamName] + if !ok { + return ircError{&irc.Message{ + Command: irc.ERR_NOSUCHCHANNEL, + Params: []string{dc.nick, name, "No such channel"}, + }} + } + + if ch.modes == nil { + // we haven't received the initial RPL_CHANNELMODEIS yet + // ignore the request, we will broadcast the modes later when we receive RPL_CHANNELMODEIS + return nil + } + + modeStr, modeParams := ch.modes.Format() + params := []string{dc.nick, name, modeStr} + params = append(params, modeParams...) + + dc.SendMessage(&irc.Message{ + Prefix: dc.srv.prefix(), + Command: irc.RPL_CHANNELMODEIS, + Params: params, + }) } case "WHO": if len(msg.Params) == 0 { diff --git a/irc.go b/irc.go index 8df3a2a..426b225 100644 --- a/irc.go +++ b/irc.go @@ -15,26 +15,26 @@ const ( err_invalidcapcmd = "410" ) -type modeSet string +type userModes string -func (ms modeSet) Has(c byte) bool { +func (ms userModes) Has(c byte) bool { return strings.IndexByte(string(ms), c) >= 0 } -func (ms *modeSet) Add(c byte) { +func (ms *userModes) Add(c byte) { if !ms.Has(c) { - *ms += modeSet(c) + *ms += userModes(c) } } -func (ms *modeSet) Del(c byte) { +func (ms *userModes) Del(c byte) { i := strings.IndexByte(string(*ms), c) if i >= 0 { *ms = (*ms)[:i] + (*ms)[i+1:] } } -func (ms *modeSet) Apply(s string) error { +func (ms *userModes) Apply(s string) error { var plusMinus byte for i := 0; i < len(s); i++ { switch c := s[i]; c { @@ -54,6 +54,94 @@ func (ms *modeSet) Apply(s string) error { return nil } +type channelModeType byte + +// standard channel mode types, as explained in https://modern.ircdocs.horse/#mode-message +const ( + // modes that add or remove an address to or from a list + modeTypeA channelModeType = iota + // modes that change a setting on a channel, and must always have a parameter + modeTypeB + // modes that change a setting on a channel, and must have a parameter when being set, and no parameter when being unset + modeTypeC + // modes that change a setting on a channel, and must not have a parameter + modeTypeD +) + +var stdChannelModes = map[byte]channelModeType{ + 'b': modeTypeA, // ban list + 'e': modeTypeA, // ban exception list + 'I': modeTypeA, // invite exception list + 'k': modeTypeB, // channel key + 'l': modeTypeC, // channel user limit + 'i': modeTypeD, // channel is invite-only + 'm': modeTypeD, // channel is moderated + 'n': modeTypeD, // channel has no external messages + 's': modeTypeD, // channel is secret + 't': modeTypeD, // channel has protected topic +} + +type channelModes map[byte]string + +func (cm channelModes) Apply(modeTypes map[byte]channelModeType, modeStr string, arguments ...string) error { + nextArgument := 0 + var plusMinus byte + for i := 0; i < len(modeStr); i++ { + mode := modeStr[i] + if mode == '+' || mode == '-' { + plusMinus = mode + continue + } + if plusMinus != '+' && plusMinus != '-' { + return fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr) + } + + mt, ok := modeTypes[mode] + if !ok { + continue + } + if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') { + if plusMinus == '+' { + var argument string + // some sentitive arguments (such as channel keys) can be omitted for privacy + // (this will only happen for RPL_CHANNELMODEIS, never for MODE messages) + if nextArgument < len(arguments) { + argument = arguments[nextArgument] + } + cm[mode] = argument + } else { + delete(cm, mode) + } + nextArgument++ + } else if mt == modeTypeC || mt == modeTypeD { + if plusMinus == '+' { + cm[mode] = "" + } else { + delete(cm, mode) + } + } + } + return nil +} + +func (cm channelModes) Format() (modeString string, parameters []string) { + var modesWithValues strings.Builder + var modesWithoutValues strings.Builder + parameters = make([]string, 0, 16) + for mode, value := range cm { + if value != "" { + modesWithValues.WriteString(string(mode)) + parameters = append(parameters, value) + } else { + modesWithoutValues.WriteString(string(mode)) + } + } + modeString = "+" + modesWithValues.String() + modesWithoutValues.String() + return +} + +const stdChannelTypes = "#&+!" + type channelStatus byte const ( @@ -74,32 +162,24 @@ func parseChannelStatus(s string) (channelStatus, error) { } } -type membership byte - -const ( - membershipFounder membership = '~' - membershipProtected membership = '&' - membershipOperator membership = '@' - membershipHalfOp membership = '%' - membershipVoice membership = '+' -) - -const stdMembershipPrefixes = "~&@%+" - -func (m membership) String() string { - if m == 0 { - return "" - } - return string(m) +type membership struct { + Mode byte + Prefix byte } -func parseMembershipPrefix(s string) (prefix membership, nick string) { - // TODO: any prefix from PREFIX RPL_ISUPPORT - if strings.IndexByte(stdMembershipPrefixes, s[0]) >= 0 { - return membership(s[0]), s[1:] - } else { - return 0, s +var stdMemberships = []membership{ + {'q', '~'}, // founder + {'a', '&'}, // protected + {'o', '@'}, // operator + {'h', '%'}, // halfop + {'v', '+'}, // voice +} + +func (m *membership) String() string { + if m == nil { + return "" } + return string(m.Prefix) } func parseMessageParams(msg *irc.Message, out ...*string) error { diff --git a/upstream.go b/upstream.go index e35574b..87ef279 100644 --- a/upstream.go +++ b/upstream.go @@ -21,8 +21,8 @@ type upstreamChannel struct { TopicWho string TopicTime time.Time Status channelStatus - modes modeSet - Members map[string]membership + modes channelModes + Members map[string]*membership complete bool } @@ -38,15 +38,16 @@ type upstreamConn struct { serverName string availableUserModes string - availableChannelModes string - channelModesWithParam string + availableChannelModes map[byte]channelModeType + availableChannelTypes string + availableMemberships []membership registered bool nick string username string realname string closed bool - modes modeSet + modes userModes channels map[string]*upstreamChannel caps map[string]string @@ -72,16 +73,19 @@ func connectToUpstream(network *network) (*upstreamConn, error) { outgoing := make(chan *irc.Message, 64) uc := &upstreamConn{ - network: network, - logger: logger, - net: netConn, - irc: irc.NewConn(netConn), - srv: network.user.srv, - user: network.user, - outgoing: outgoing, - ring: NewRing(network.user.srv.RingCap), - channels: make(map[string]*upstreamChannel), - caps: make(map[string]string), + network: network, + logger: logger, + net: netConn, + irc: irc.NewConn(netConn), + srv: network.user.srv, + user: network.user, + outgoing: outgoing, + ring: NewRing(network.user.srv.RingCap), + channels: make(map[string]*upstreamChannel), + caps: make(map[string]string), + availableChannelTypes: stdChannelTypes, + availableChannelModes: stdChannelModes, + availableMemberships: stdMemberships, } go func() { @@ -130,17 +134,21 @@ func (uc *upstreamConn) getChannel(name string) (*upstreamChannel, error) { } func (uc *upstreamConn) isChannel(entity string) bool { - for _, r := range entity { - switch r { - // TODO: support upstream ISUPPORT channel prefixes - case '#', '&', '+', '!': - return true - } - break + if i := strings.IndexByte(uc.availableChannelTypes, entity[0]); i >= 0 { + return true } return false } +func (uc *upstreamConn) parseMembershipPrefix(s string) (membership *membership, nick string) { + for _, m := range uc.availableMemberships { + if m.Prefix == s[0] { + return &m, s[1:] + } + } + return nil, s +} + func (uc *upstreamConn) handleMessage(msg *irc.Message) error { switch msg.Command { case "PING": @@ -149,35 +157,6 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error { Params: msg.Params, }) return nil - case "MODE": - var name, modeStr string - if err := parseMessageParams(msg, &name, &modeStr); err != nil { - return err - } - - if !uc.isChannel(name) { // user mode change - if name != uc.nick { - return fmt.Errorf("received MODE message for unknown nick %q", name) - } - return uc.modes.Apply(modeStr) - } else { // channel mode change - // TODO: handle MODE channel mode arguments - ch, err := uc.getChannel(name) - if err != nil { - return err - } - if err := ch.modes.Apply(modeStr); err != nil { - return err - } - - uc.forEachDownstream(func(dc *downstreamConn) { - dc.SendMessage(&irc.Message{ - Prefix: dc.marshalUserPrefix(uc, msg.Prefix), - Command: "MODE", - Params: []string{dc.marshalChannel(uc, name), modeStr}, - }) - }) - } case "NOTICE": uc.logger.Print(msg) @@ -346,11 +325,67 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error { }) } case irc.RPL_MYINFO: - if err := parseMessageParams(msg, nil, &uc.serverName, nil, &uc.availableUserModes, &uc.availableChannelModes); err != nil { + if err := parseMessageParams(msg, nil, &uc.serverName, nil, &uc.availableUserModes, nil); err != nil { return err } - if len(msg.Params) > 5 { - uc.channelModesWithParam = msg.Params[5] + case irc.RPL_ISUPPORT: + if err := parseMessageParams(msg, nil, nil); err != nil { + return err + } + for _, token := range msg.Params[1 : len(msg.Params)-1] { + negate := false + parameter := token + value := "" + if strings.HasPrefix(token, "-") { + negate = true + token = token[1:] + } else { + if i := strings.IndexByte(token, '='); i >= 0 { + parameter = token[:i] + value = token[i+1:] + } + } + if !negate { + switch parameter { + case "CHANMODES": + parts := strings.SplitN(value, ",", 5) + if len(parts) < 4 { + return fmt.Errorf("malformed ISUPPORT CHANMODES value: %v", value) + } + modes := make(map[byte]channelModeType) + for i, mt := range []channelModeType{modeTypeA, modeTypeB, modeTypeC, modeTypeD} { + for j := 0; j < len(parts[i]); j++ { + mode := parts[i][j] + modes[mode] = mt + } + } + uc.availableChannelModes = modes + case "CHANTYPES": + uc.availableChannelTypes = value + case "PREFIX": + if value == "" { + uc.availableMemberships = nil + } else { + if value[0] != '(' { + return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value) + } + sep := strings.IndexByte(value, ')') + if sep < 0 || len(value) != sep*2 { + return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value) + } + memberships := make([]membership, len(value)/2-1) + for i := range memberships { + memberships[i] = membership{ + Mode: value[i+1], + Prefix: value[sep+i+1], + } + } + uc.availableMemberships = memberships + } + } + } else { + // TODO: handle ISUPPORT negations + } } case "NICK": if msg.Prefix == nil { @@ -399,14 +434,19 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error { uc.channels[ch] = &upstreamChannel{ Name: ch, conn: uc, - Members: make(map[string]membership), + Members: make(map[string]*membership), } + + uc.SendMessage(&irc.Message{ + Command: "MODE", + Params: []string{ch}, + }) } else { ch, err := uc.getChannel(ch) if err != nil { return err } - ch.Members[msg.Prefix.Name] = 0 + ch.Members[msg.Prefix.Name] = nil } uc.forEachDownstream(func(dc *downstreamConn) { @@ -508,6 +548,89 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error { Params: params, }) }) + case "MODE": + var name, modeStr string + if err := parseMessageParams(msg, &name, &modeStr); err != nil { + return err + } + + if !uc.isChannel(name) { // user mode change + if name != uc.nick { + return fmt.Errorf("received MODE message for unknown nick %q", name) + } + return uc.modes.Apply(modeStr) + // TODO: notify downstreams about user mode change? + } else { // channel mode change + ch, err := uc.getChannel(name) + if err != nil { + return err + } + + if ch.modes != nil { + if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[2:]...); err != nil { + return err + } + } + + uc.forEachDownstream(func(dc *downstreamConn) { + params := []string{dc.marshalChannel(uc, name), modeStr} + params = append(params, msg.Params[2:]...) + + dc.SendMessage(&irc.Message{ + Prefix: dc.marshalUserPrefix(uc, msg.Prefix), + Command: "MODE", + Params: params, + }) + }) + } + case irc.RPL_UMODEIS: + if err := parseMessageParams(msg, nil); err != nil { + return err + } + modeStr := "" + if len(msg.Params) > 1 { + modeStr = msg.Params[1] + } + + uc.modes = "" + if err := uc.modes.Apply(modeStr); err != nil { + return err + } + // TODO: send RPL_UMODEIS to downstream connections when applicable + case irc.RPL_CHANNELMODEIS: + var channel string + if err := parseMessageParams(msg, nil, &channel); err != nil { + return err + } + modeStr := "" + if len(msg.Params) > 2 { + modeStr = msg.Params[2] + } + + ch, err := uc.getChannel(channel) + if err != nil { + return err + } + + firstMode := ch.modes == nil + ch.modes = make(map[byte]string) + if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[3:]...); err != nil { + return err + } + if firstMode { + modeStr, modeParams := ch.modes.Format() + + uc.forEachDownstream(func(dc *downstreamConn) { + params := []string{dc.nick, dc.marshalChannel(uc, channel), modeStr} + params = append(params, modeParams...) + + dc.SendMessage(&irc.Message{ + Prefix: dc.srv.prefix(), + Command: irc.RPL_CHANNELMODEIS, + Params: params, + }) + }) + } case rpl_topicwhotime: var name, who, timeStr string if err := parseMessageParams(msg, nil, &name, &who, &timeStr); err != nil { @@ -540,7 +663,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error { ch.Status = status for _, s := range strings.Split(members, " ") { - membership, nick := parseMembershipPrefix(s) + membership, nick := uc.parseMembershipPrefix(s) ch.Members[nick] = membership } case irc.RPL_ENDOFNAMES: @@ -679,7 +802,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error { nick := dc.marshalNick(uc, nick) channelList := make([]string, len(channels)) for i, channel := range channels { - prefix, channel := parseMembershipPrefix(channel) + prefix, channel := uc.parseMembershipPrefix(channel) channel = dc.marshalChannel(uc, channel) channelList[i] = prefix.String() + channel }