From 1cb8e4762b4afdd7d70e7cb9d49ad9227f8bb365 Mon Sep 17 00:00:00 2001 From: Daniel Oaks Date: Wed, 10 Feb 2016 21:37:18 +1000 Subject: [PATCH] client: Handle capabilities, improve handling and testing --- client/capabilities.go | 79 ++++++++++++++++++++++++++++++++++++++++++ client/client.go | 29 +++++++++++----- client/handlers.go | 44 +++++++++++++++++++++++ client/reactor.go | 15 +++----- client/reactor_test.go | 54 +++++++++++++++++++++++++---- 5 files changed, 195 insertions(+), 26 deletions(-) create mode 100644 client/capabilities.go create mode 100644 client/handlers.go diff --git a/client/capabilities.go b/client/capabilities.go new file mode 100644 index 0000000..44ceceb --- /dev/null +++ b/client/capabilities.go @@ -0,0 +1,79 @@ +// written by Daniel Oaks +// released under the ISC license + +package gircclient + +import "strings" +import "sort" + +// ClientCapabilities holds the capabilities that can and have been enabled on +// a ServerConnection. +type ClientCapabilities struct { + Avaliable map[string]*string + Enabled map[string]*string + Wanted []string +} + +// NewClientCapabilities returns a newly-initialised ClientCapabilities. +func NewClientCapabilities() ClientCapabilities { + var cc ClientCapabilities + + cc.Avaliable = make(map[string]*string, 0) + cc.Enabled = make(map[string]*string, 0) + cc.Wanted = make([]string, 0) + + return cc +} + +// AddWantedCaps adds the given capabilities to our list of capabilities that +// we want from the server. +func (cc *ClientCapabilities) AddWantedCaps(caps ...string) { + for _, name := range caps { + // I'm not sure how fast this is, but speed isn't too much of a concern + // here. Adding 'wanted capabilities' is something that generally only + // happens at startup anyway. + i := sort.Search(len(cc.Wanted), func(i int) bool { return cc.Wanted[i] >= name }) + + if i >= len(cc.Wanted) || cc.Wanted[i] != name { + cc.Wanted = append(cc.Wanted, name) + sort.Strings(cc.Wanted) + } + } +} + +// AddCaps adds capabilities from LS lists to our Avaliable map. +func (cc *ClientCapabilities) AddCaps(tags []string) { + var name string + var value *string + + for _, tag := range tags { + if strings.Contains(tag, "=") { + vals := strings.SplitN(tag, "=", 2) + name = vals[0] + value = &vals[1] + } else { + name = tag + value = nil + } + + cc.Avaliable[name] = value + } +} + +// ToRequestLine returns a line of capabilities to request, to be used in a +// CAP REQ line. +func (cc *ClientCapabilities) ToRequestLine() string { + var caps []string + caps = make([]string, 0) + + for _, name := range cc.Wanted { + _, capIsAvailable := cc.Avaliable[name] + _, capIsEnabled := cc.Enabled[name] + + if capIsAvailable && !capIsEnabled { + caps = append(caps, name) + } + } + + return strings.Join(caps, " ") +} diff --git a/client/client.go b/client/client.go index fbc5192..fd9d84a 100644 --- a/client/client.go +++ b/client/client.go @@ -17,8 +17,9 @@ import ( // ServerConnection is a connection to a single server. type ServerConnection struct { - Name string - Connected bool + Name string + Connected bool + Registered bool // internal stuff connection net.Conn @@ -26,8 +27,8 @@ type ServerConnection struct { eventsOut eventmgr.EventManager // data we keep track of - //features ServerFeatures - //caps ClientCapabilities + // Features ServerFeatures + Caps ClientCapabilities // details users must supply before connection Nick string @@ -36,6 +37,19 @@ type ServerConnection struct { InitialRealName string } +// newServerConnection returns an initialised ServerConnection, for internal +// use. +func newServerConnection() *ServerConnection { + var sc ServerConnection + + sc.Caps = NewClientCapabilities() + + sc.Caps.AddWantedCaps("account-notify", "away-notify", "extended-join", "multi-prefix", "sasl") + sc.Caps.AddWantedCaps("account-tag", "chghost", "echo-message", "invite-notify", "server-time", "userhost-in-names") + + return &sc +} + // Connect connects to the given address. func (sc *ServerConnection) Connect(address string, ssl bool, tlsconfig *tls.Config) error { var conn net.Conn @@ -54,9 +68,7 @@ func (sc *ServerConnection) Connect(address string, ssl bool, tlsconfig *tls.Con sc.connection = conn sc.Connected = true - sc.Nick = sc.InitialNick - sc.Send(nil, "", "NICK", sc.InitialNick) - sc.Send(nil, "", "USER", sc.InitialUser, "0", "*", sc.InitialRealName) + sc.Send(nil, "", "CAP", "LS", "302") go sc.receiveLoop() @@ -97,7 +109,8 @@ func (sc *ServerConnection) receiveLoop() { info["command"] = message.Command info["params"] = message.Params - sc.dispatchIn(message.Command, info) + // IRC commands are case-insensitive + sc.dispatchIn(strings.ToUpper(message.Command), info) } sc.connection.Close() diff --git a/client/handlers.go b/client/handlers.go new file mode 100644 index 0000000..eadedc0 --- /dev/null +++ b/client/handlers.go @@ -0,0 +1,44 @@ +// written by Daniel Oaks +// released under the ISC license + +package gircclient + +import ( + "strings" + + "github.com/DanielOaks/girc-go/eventmgr" +) + +// welcomeHandler sets the nick to the first parameter of the 001 message. +// This ensures that when we connect to IRCds that silently truncate the +// nickname, we keep the correct one. +func welcomeHandler(event string, info eventmgr.InfoMap) { + sc := info["server"].(*ServerConnection) + sc.Nick = info["params"].([]string)[0] +} + +func capHandler(event string, info eventmgr.InfoMap) { + sc := info["server"].(*ServerConnection) + params := info["params"].([]string) + subcommand := strings.ToUpper(params[1]) + if !sc.Registered && (subcommand == "ACK" || subcommand == "NAK") { + sendRegistration(sc) + } else if subcommand == "LS" { + if len(params) > 3 { + sc.Caps.AddCaps(strings.Split(params[3], " ")) + } else { + sc.Caps.AddCaps(strings.Split(params[2], " ")) + capsToRequest := sc.Caps.ToRequestLine() + + if len(capsToRequest) > 0 { + sc.Send(nil, "", "CAP", "REQ", capsToRequest) + } + } + } +} + +func sendRegistration(sc *ServerConnection) { + sc.Nick = sc.InitialNick + sc.Send(nil, "", "NICK", sc.InitialNick) + sc.Send(nil, "", "USER", sc.InitialUser, "0", "*", sc.InitialRealName) +} diff --git a/client/reactor.go b/client/reactor.go index 379eb39..c8d55e4 100644 --- a/client/reactor.go +++ b/client/reactor.go @@ -20,14 +20,6 @@ type Reactor struct { eventsToRegister []eventRegistration } -// welcomeHandler sets the nick to the first parameter of the 001 message. -// This ensures that when we connect to IRCds that silently truncate the -// nickname, we keep the correct one. -func welcomeHandler(event string, info eventmgr.InfoMap) { - server := info["server"].(*ServerConnection) - server.Nick = info["params"].([]string)[0] -} - // NewReactor returns a new, empty Reactor. func NewReactor() Reactor { var newReactor Reactor @@ -36,6 +28,7 @@ func NewReactor() Reactor { newReactor.eventsToRegister = make([]eventRegistration, 0) // add the default handlers + newReactor.RegisterEvent("in", "CAP", capHandler, -10) newReactor.RegisterEvent("in", "001", welcomeHandler, -10) return newReactor @@ -43,15 +36,15 @@ func NewReactor() Reactor { // CreateServer creates a ServerConnection and returns it. func (r *Reactor) CreateServer(name string) *ServerConnection { - var sc ServerConnection + sc := newServerConnection() - r.ServerConnections[name] = &sc + r.ServerConnections[name] = sc for _, e := range r.eventsToRegister { sc.RegisterEvent(e.Direction, e.Name, e.Handler, e.Priority) } - return &sc + return sc } // Shutdown shuts down all ServerConnections. diff --git a/client/reactor_test.go b/client/reactor_test.go index 8f5e979..51c7d2c 100644 --- a/client/reactor_test.go +++ b/client/reactor_test.go @@ -15,6 +15,8 @@ import ( "runtime" "testing" "time" + + "github.com/DanielOaks/girc-go/ircmsg" ) func TestPlainConnection(t *testing.T) { @@ -95,13 +97,56 @@ func TestTLSConnection(t *testing.T) { testServerConnection(t, reactor, client, listener) } +func sendMessage(conn net.Conn, tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) { + ircmsg := ircmsg.MakeMessage(tags, prefix, command, params...) + line, err := ircmsg.Line() + if err != nil { + return + } + fmt.Fprintf(conn, line) + + // need to wait for a quick moment here for TLS to do this properly + runtime.Gosched() + waitTime, _ := time.ParseDuration("10ms") + time.Sleep(waitTime) +} + func testServerConnection(t *testing.T, reactor Reactor, client *ServerConnection, listener net.Listener) { conn, _ := listener.Accept() reader := bufio.NewReader(conn) - // test each message in sequence var message string + // CAP + message, _ = reader.ReadString('\n') + if message != "CAP LS 302\r\n" { + t.Error( + "Did not receive CAP LS message, received: [", + message, + "]", + ) + return + } + + sendMessage(conn, nil, "example.com", "CAP", "*", "LS", "*", "multi-prefix userhost-in-names sasl=PLAIN") + sendMessage(conn, nil, "example.com", "CAP", "*", "LS", "chghost") + + message, _ = reader.ReadString('\n') + if message != "CAP REQ :chghost multi-prefix sasl userhost-in-names\r\n" { + t.Error( + "Did not receive CAP REQ message, received: [", + message, + "]", + ) + return + } + + // these should be silently ignored + fmt.Fprintf(conn, "\r\n\r\n\r\n") + + sendMessage(conn, nil, "example.com", "CAP", "*", "ACK", "chghost multi-prefix userhost-in-names sasl") + + // NICK/USER message, _ = reader.ReadString('\n') if message != "NICK coolguy\r\n" { t.Error( @@ -123,12 +168,7 @@ func testServerConnection(t *testing.T, reactor Reactor, client *ServerConnectio } // make sure nick changes properly - // need to wait for a quick moment here for TLS to do this properly - fmt.Fprintf(conn, "\r\n\r\n\r\n") // these should be silently ignored - fmt.Fprintf(conn, ":example.com 001 dan :Welcome to the gIRC-Go Test Network!\r\n") - runtime.Gosched() - waitTime, _ := time.ParseDuration("10ms") - time.Sleep(waitTime) + sendMessage(conn, nil, "example.com", "001", "dan", "Welcome to the gIRC-Go Test Network!") if client.Nick != "dan" { t.Error(