From 36602c9a3cf80c58e7387f1eacae3aff843deecd Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Fri, 28 Feb 2014 19:21:33 -0800 Subject: [PATCH 1/2] basic capability negotiation - multi-prefix is supported as an example --- irc/channel.go | 24 +++++++---- irc/client.go | 69 ++++++++++++++++--------------- irc/commands.go | 27 +++++++++--- irc/constants.go | 33 +++++++++++++-- irc/reply.go | 25 ++++++++--- irc/server.go | 105 ++++++++++++++++++++++++++++------------------- irc/types.go | 23 ++++++++++- 7 files changed, 206 insertions(+), 100 deletions(-) diff --git a/irc/channel.go b/irc/channel.go index a9b45861..3e06b5ed 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -54,18 +54,26 @@ func (channel *Channel) ClientIsOperator(client *Client) bool { return client.flags[Operator] || channel.members.HasMode(client, ChannelOperator) } -func (channel *Channel) Nicks() []string { +func (channel *Channel) Nicks(target *Client) []string { + isMultiPrefix := (target != nil) && target.capabilities[MultiPrefix] nicks := make([]string, len(channel.members)) i := 0 for client, modes := range channel.members { - switch { - case modes[ChannelOperator]: - nicks[i] = "@" + client.Nick() - case modes[Voice]: - nicks[i] = "+" + client.Nick() - default: - nicks[i] = client.Nick() + if isMultiPrefix { + if modes[ChannelOperator] { + nicks[i] += "@" + } + if modes[Voice] { + nicks[i] += "+" + } + } else { + if modes[ChannelOperator] { + nicks[i] += "@" + } else if modes[Voice] { + nicks[i] += "+" + } } + nicks[i] += client.Nick() i += 1 } return nicks diff --git a/irc/client.go b/irc/client.go index 7047f164..d4d1813b 100644 --- a/irc/client.go +++ b/irc/client.go @@ -12,36 +12,41 @@ func IsNickname(nick string) bool { } type Client struct { - atime time.Time - awayMessage string - channels ChannelSet - commands chan editableCommand - ctime time.Time - flags map[UserMode]bool - hasQuit bool - hops uint - hostname string - idleTimer *time.Timer - loginTimer *time.Timer - nick string - phase Phase - quitTimer *time.Timer - realname string - server *Server - socket *Socket - username string + atime time.Time + authorized bool + awayMessage string + capabilities CapabilitySet + capState CapState + channels ChannelSet + commands chan editableCommand + ctime time.Time + flags map[UserMode]bool + hasQuit bool + hops uint + hostname string + idleTimer *time.Timer + loginTimer *time.Timer + nick string + phase Phase + quitTimer *time.Timer + realname string + server *Server + socket *Socket + username string } func NewClient(server *Server, conn net.Conn) *Client { now := time.Now() client := &Client{ - atime: now, - channels: make(ChannelSet), - commands: make(chan editableCommand), - ctime: now, - flags: make(map[UserMode]bool), - phase: server.InitPhase(), - server: server, + atime: now, + capState: CapNone, + capabilities: make(CapabilitySet), + channels: make(ChannelSet), + commands: make(chan editableCommand), + ctime: now, + flags: make(map[UserMode]bool), + phase: Registration, + server: server, } client.socket = NewSocket(conn, client.commands) client.loginTimer = time.AfterFunc(LOGIN_TIMEOUT, client.connectionTimeout) @@ -68,6 +73,12 @@ func (client *Client) run() { } } +func (client *Client) connectionTimeout() { + client.commands <- &QuitCommand{ + message: "connection timeout", + } +} + // // idle timer goroutine // @@ -76,14 +87,6 @@ func (client *Client) connectionIdle() { client.server.idle <- client } -// -// quit timer goroutine -// - -func (client *Client) connectionTimeout() { - client.server.timeout <- client -} - // // server goroutine // diff --git a/irc/commands.go b/irc/commands.go index 8229d217..34f39c95 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -706,20 +706,35 @@ func NewOperCommand(args []string) (editableCommand, error) { return cmd, nil } -// TODO type CapCommand struct { BaseCommand - args []string + subCommand CapSubCommand + args []string } func (msg *CapCommand) String() string { - return fmt.Sprintf("CAP(args=%s)", msg.args) + return fmt.Sprintf("CAP(subCommand=%s, args=%s)", msg.subCommand, msg.args) +} + +func (msg *CapCommand) Capabilities() []Capability { + strs := strings.Split(msg.args[0], " ") + caps := make([]Capability, len(strs)) + for index, str := range strs { + caps[index] = Capability(str) + } + return caps } func NewCapCommand(args []string) (editableCommand, error) { - return &CapCommand{ - args: args, - }, nil + if len(args) < 1 { + return nil, NotEnoughArgsError + } + + cmd := &CapCommand{ + subCommand: CapSubCommand(args[0]), + args: args[1:], + } + return cmd, nil } // HAPROXY support diff --git a/irc/constants.go b/irc/constants.go index 83e73efb..dd84f352 100644 --- a/irc/constants.go +++ b/irc/constants.go @@ -155,6 +155,7 @@ const ( ERR_TOOMANYTARGETS NumericCode = 407 ERR_NOSUCHSERVICE NumericCode = 408 ERR_NOORIGIN NumericCode = 409 + ERR_INVALIDCAPCMD NumericCode = 410 ERR_NORECIPIENT NumericCode = 411 ERR_NOTEXTTOSEND NumericCode = 412 ERR_NOTOPLEVEL NumericCode = 413 @@ -200,6 +201,14 @@ const ( ERR_UMODEUNKNOWNFLAG NumericCode = 501 ERR_USERSDONTMATCH NumericCode = 502 + CAP_LS CapSubCommand = "LS" + CAP_LIST CapSubCommand = "LIST" + CAP_REQ CapSubCommand = "REQ" + CAP_ACK CapSubCommand = "ACK" + CAP_NAK CapSubCommand = "NAK" + CAP_CLEAR CapSubCommand = "CLEAR" + CAP_END CapSubCommand = "END" + Add ModeOp = '+' List ModeOp = '=' Remove ModeOp = '-' @@ -230,10 +239,28 @@ const ( Secret ChannelMode = 's' // flag, deprecated UserLimit ChannelMode = 'l' // flag arg Voice ChannelMode = 'v' // arg + + MultiPrefix Capability = "multi-prefix" + SASL Capability = "sasl" + + Disable CapModifier = '-' + Ack CapModifier = '~' + Sticky CapModifier = '=' +) + +var ( + SupportedCapabilities = CapabilitySet{ + MultiPrefix: true, + } ) const ( - Authorization Phase = iota - Registration Phase = iota - Normal Phase = iota + Registration Phase = iota + Normal Phase = iota +) + +const ( + CapNone CapState = iota + CapNegotiating CapState = iota + CapNegotiated CapState = iota ) diff --git a/irc/reply.go b/irc/reply.go index 970876fb..76def9bb 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -240,11 +240,19 @@ func (target *Client) RplWhoReply(channel *Channel, client *Client) { if channel != nil { channelName = channel.name - - if channel.members[client][ChannelOperator] { - flags += "@" - } else if channel.members[client][Voice] { - flags += "+" + if target.capabilities[MultiPrefix] { + if channel.members[client][ChannelOperator] { + flags += "@" + } + if channel.members[client][Voice] { + flags += "+" + } + } else { + if channel.members[client][ChannelOperator] { + flags += "@" + } else if channel.members[client][Voice] { + flags += "+" + } } } target.NumericReply(RPL_WHOREPLY, @@ -360,7 +368,7 @@ func (target *Client) RplListEnd(server *Server) { } func (target *Client) RplNamReply(channel *Channel) { - target.MultilineReply(channel.Nicks(), RPL_NAMREPLY, + target.MultilineReply(channel.Nicks(target), RPL_NAMREPLY, "= %s :%s", channel) } @@ -502,3 +510,8 @@ func (target *Client) ErrChannelIsFull(channel *Channel) { target.NumericReply(ERR_CHANNELISFULL, "%s :Cannot join channel (+l)", channel) } + +func (target *Client) ErrInvalidCapCmd(subCommand CapSubCommand) { + target.NumericReply(ERR_INVALIDCAPCMD, + "%s :Invalid CAP subcommand", subCommand) +} diff --git a/irc/server.go b/irc/server.go index 49c5f4ad..245dcac2 100644 --- a/irc/server.go +++ b/irc/server.go @@ -32,7 +32,6 @@ type Server struct { operators map[string][]byte password []byte signals chan os.Signal - timeout chan *Client } func NewServer(config *Config) *Server { @@ -54,7 +53,6 @@ func NewServer(config *Config) *Server { operators: config.OperatorsMap(), password: config.PasswordBytes(), signals: make(chan os.Signal, 1), - timeout: make(chan *Client, 16), } signal.Notify(server.signals, os.Interrupt, os.Kill) @@ -101,14 +99,6 @@ func (server *Server) processCommand(cmd Command) { } switch client.phase { - case Authorization: - authCmd, ok := cmd.(AuthServerCommand) - if !ok { - client.Quit("unexpected command") - return - } - authCmd.HandleAuthServer(server) - case Registration: regCmd, ok := cmd.(RegServerCommand) if !ok { @@ -117,7 +107,7 @@ func (server *Server) processCommand(cmd Command) { } regCmd.HandleRegServer(server) - default: + case Normal: srvCmd, ok := cmd.(ServerCommand) if !ok { client.ErrUnknownCommand(cmd.Code()) @@ -155,20 +145,10 @@ func (server *Server) Run() { case client := <-server.idle: client.Idle() - - case client := <-server.timeout: - client.Quit("connection timeout") } } } -func (server *Server) InitPhase() Phase { - if server.password == nil { - return Registration - } - return Authorization -} - func newListener(config ListenerConfig) (net.Listener, error) { if config.IsTLS() { certificate, err := tls.LoadX509KeyPair(config.Certificate, config.Key) @@ -237,7 +217,7 @@ func (s *Server) GenerateGuestNick() string { // func (s *Server) tryRegister(c *Client) { - if c.HasNick() && c.HasUsername() { + if c.HasNick() && c.HasUsername() && (c.capState != CapNegotiating) { c.Register() c.RplWelcome() c.RplYourHost() @@ -297,18 +277,10 @@ func (s *Server) Nick() string { } // -// authorization commands +// registration commands // -func (msg *ProxyCommand) HandleAuthServer(server *Server) { - msg.Client().hostname = msg.hostname -} - -func (msg *CapCommand) HandleAuthServer(server *Server) { - // TODO -} - -func (msg *PassCommand) HandleAuthServer(server *Server) { +func (msg *PassCommand) HandleRegServer(server *Server) { client := msg.Client() if msg.err != nil { client.ErrPasswdMismatch() @@ -316,27 +288,62 @@ func (msg *PassCommand) HandleAuthServer(server *Server) { return } - client.phase = Registration + client.authorized = true } -func (msg *QuitCommand) HandleAuthServer(server *Server) { - msg.Client().Quit(msg.message) -} - -// -// registration commands -// - func (msg *ProxyCommand) HandleRegServer(server *Server) { msg.Client().hostname = msg.hostname } func (msg *CapCommand) HandleRegServer(server *Server) { - // TODO + client := msg.Client() + + switch msg.subCommand { + case CAP_LS: + client.capState = CapNegotiating + client.Reply(fmt.Sprintf("CAP LS :%d", MultiPrefix)) + + case CAP_LIST: + client.Reply(fmt.Sprintf("CAP LIST :%s", client.capabilities)) + + case CAP_REQ: + client.capState = CapNegotiating + caps := msg.Capabilities() + if (len(caps) != 1) && (caps[0] != MultiPrefix) { + client.Reply("CAP NAK :" + msg.args[0]) + return + } + for _, capability := range caps { + client.capabilities[capability] = true + } + client.Reply("CAP ACK :" + msg.args[0]) + + case CAP_CLEAR: + for capability := range client.capabilities { + delete(client.capabilities, capability) + } + client.Reply("CAP ACK :") + + case CAP_END: + client.capState = CapNegotiated + server.tryRegister(client) + + default: + client.ErrInvalidCapCmd(msg.subCommand) + } } func (m *NickCommand) HandleRegServer(s *Server) { client := m.Client() + if !client.authorized { + client.ErrPasswdMismatch() + client.Quit("bad password") + return + } + + if client.capState == CapNegotiating { + client.capState = CapNegotiated + } if m.nickname == "" { client.ErrNoNicknameGiven() @@ -358,11 +365,22 @@ func (m *NickCommand) HandleRegServer(s *Server) { } func (msg *RFC1459UserCommand) HandleRegServer(server *Server) { + client := msg.Client() + if !client.authorized { + client.ErrPasswdMismatch() + client.Quit("bad password") + return + } msg.HandleRegServer2(server) } func (msg *RFC2812UserCommand) HandleRegServer(server *Server) { client := msg.Client() + if !client.authorized { + client.ErrPasswdMismatch() + client.Quit("bad password") + return + } flags := msg.Flags() if len(flags) > 0 { for _, mode := range msg.Flags() { @@ -375,6 +393,9 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) { func (msg *UserCommand) HandleRegServer2(server *Server) { client := msg.Client() + if client.capState == CapNegotiating { + client.capState = CapNegotiated + } client.username, client.realname = msg.username, msg.realname server.tryRegister(client) } diff --git a/irc/types.go b/irc/types.go index 6cb49a60..ec92a6a4 100644 --- a/irc/types.go +++ b/irc/types.go @@ -10,6 +10,25 @@ import ( // simple types // +type CapSubCommand string + +type Capability string + +type CapModifier rune + +type CapState uint + +type CapabilitySet map[Capability]bool + +func (set CapabilitySet) String() string { + strs := make([]string, len(set)) + index := 0 + for capability := range set { + strs[index] = string(capability) + } + return strings.Join(strs, " ") +} + // a string with wildcards type Mask string @@ -24,7 +43,7 @@ func (op ModeOp) String() string { type UserMode rune func (mode UserMode) String() string { - return fmt.Sprintf("%c", mode) + return string(mode) } type Phase uint @@ -49,7 +68,7 @@ func (code NumericCode) String() string { type ChannelMode rune func (mode ChannelMode) String() string { - return fmt.Sprintf("%c", mode) + return string(mode) } type ChannelNameMap map[string]*Channel From 0874692aa890a53f31350fb532752d7a6859c470 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Sun, 2 Mar 2014 12:54:48 -0800 Subject: [PATCH 2/2] send proper replies for cap protocol --- irc/client.go | 5 ++++- irc/commands.go | 27 +++++++++++++-------------- irc/server.go | 26 +++++++++++++++++--------- irc/types.go | 5 +++++ 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/irc/client.go b/irc/client.go index d4d1813b..70f75194 100644 --- a/irc/client.go +++ b/irc/client.go @@ -235,7 +235,10 @@ func (client *Client) ChangeNickname(nickname string) { } } -func (client *Client) Reply(reply string) { +func (client *Client) Reply(reply string, args ...interface{}) { + if len(args) > 0 { + reply = fmt.Sprintf(reply, args...) + } client.socket.Write(reply) } diff --git a/irc/commands.go b/irc/commands.go index 34f39c95..40431d49 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -708,21 +708,13 @@ func NewOperCommand(args []string) (editableCommand, error) { type CapCommand struct { BaseCommand - subCommand CapSubCommand - args []string + subCommand CapSubCommand + capabilities CapabilitySet } func (msg *CapCommand) String() string { - return fmt.Sprintf("CAP(subCommand=%s, args=%s)", msg.subCommand, msg.args) -} - -func (msg *CapCommand) Capabilities() []Capability { - strs := strings.Split(msg.args[0], " ") - caps := make([]Capability, len(strs)) - for index, str := range strs { - caps[index] = Capability(str) - } - return caps + return fmt.Sprintf("CAP(subCommand=%s, capabilities=%s)", + msg.subCommand, msg.capabilities) } func NewCapCommand(args []string) (editableCommand, error) { @@ -731,8 +723,15 @@ func NewCapCommand(args []string) (editableCommand, error) { } cmd := &CapCommand{ - subCommand: CapSubCommand(args[0]), - args: args[1:], + subCommand: CapSubCommand(strings.ToUpper(args[0])), + capabilities: make(CapabilitySet), + } + + if len(args) > 1 { + strs := spacesExpr.Split(args[1], -1) + for _, str := range strs { + cmd.capabilities[Capability(str)] = true + } } return cmd, nil } diff --git a/irc/server.go b/irc/server.go index 245dcac2..d3ac14b5 100644 --- a/irc/server.go +++ b/irc/server.go @@ -301,28 +301,36 @@ func (msg *CapCommand) HandleRegServer(server *Server) { switch msg.subCommand { case CAP_LS: client.capState = CapNegotiating - client.Reply(fmt.Sprintf("CAP LS :%d", MultiPrefix)) + client.Reply("CAP LS * :%s", SupportedCapabilities) case CAP_LIST: - client.Reply(fmt.Sprintf("CAP LIST :%s", client.capabilities)) + client.Reply("CAP LIST * :%s", client.capabilities) case CAP_REQ: client.capState = CapNegotiating - caps := msg.Capabilities() - if (len(caps) != 1) && (caps[0] != MultiPrefix) { - client.Reply("CAP NAK :" + msg.args[0]) - return + for capability := range msg.capabilities { + if !SupportedCapabilities[capability] { + client.Reply("CAP NAK * :%s", msg.capabilities) + return + } } - for _, capability := range caps { + for capability := range msg.capabilities { client.capabilities[capability] = true } - client.Reply("CAP ACK :" + msg.args[0]) + client.Reply("CAP ACK * :%s", msg.capabilities) case CAP_CLEAR: + format := strings.TrimRight( + strings.Repeat("%s%s ", len(client.capabilities)), " ") + args := make([]interface{}, len(client.capabilities)) + index := 0 for capability := range client.capabilities { + args[index] = Disable + args[index+1] = capability + index += 2 delete(client.capabilities, capability) } - client.Reply("CAP ACK :") + client.Reply("CAP ACK * :"+format, args...) case CAP_END: client.capState = CapNegotiated diff --git a/irc/types.go b/irc/types.go index ec92a6a4..fc628bdd 100644 --- a/irc/types.go +++ b/irc/types.go @@ -16,6 +16,10 @@ type Capability string type CapModifier rune +func (mod CapModifier) String() string { + return string(mod) +} + type CapState uint type CapabilitySet map[Capability]bool @@ -25,6 +29,7 @@ func (set CapabilitySet) String() string { index := 0 for capability := range set { strs[index] = string(capability) + index += 1 } return strings.Join(strs, " ") }