diff --git a/.travis.yml b/.travis.yml index aaef0bd..0b5d58e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,10 @@ language: go go: - - 1.16.2 - - 1.15.10 + - 1.18 + - 1.17.8 + - 1.16.15 + - 1.15.15 sudo : false diff --git a/README.md b/README.md index 75081eb..5c1108c 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,11 @@ and `DisableStateTracking()` respectively. Doing this while connected to an IRC server will probably result in an inconsistent state and a lot of warnings to STDERR ;-) +### Projects using GoIRC + +- [xdcc-cli](https://github.com/ostafen/xdcc-cli): A command line tool for searching and downloading files from the IRC network. + + ### Misc. Sorry the documentation is crap. Use the source, Luke. @@ -92,7 +97,7 @@ indebted to Matt Gruen for his work on the re-organisation and channel-based communication structure of `*Conn.send()` and `*Conn.recv()`. I'm sure things could be more asynchronous, still. -This code is (c) 2009-15 Alex Bramley, and released under the same licence terms +This code is (c) 2009-23 Alex Bramley, and released under the same licence terms as Go itself. Contributions gratefully received from: @@ -109,6 +114,9 @@ Contributions gratefully received from: - [soul9](https://github.com/soul9) - [jakebailey](https://github.com/jakebailey) - [stapelberg](https://github.com/stapelberg) + - [shammash](https://github.com/shammash) + - [ostafen](https://github.com/ostafen) + - [supertassu](https://github.com/supertassu) And thanks to the following for minor doc/fix PRs: diff --git a/client/commands.go b/client/commands.go index 101c7d3..f5d4c58 100644 --- a/client/commands.go +++ b/client/commands.go @@ -10,6 +10,7 @@ const ( CONNECTED = "CONNECTED" DISCONNECTED = "DISCONNECTED" ACTION = "ACTION" + AUTHENTICATE = "AUTHENTICATE" AWAY = "AWAY" CAP = "CAP" CTCP = "CTCP" @@ -84,6 +85,23 @@ func splitMessage(msg string, splitLen int) (msgs []string) { return append(msgs, msg) } +func splitArgs(args []string, maxLen int) []string { + res := make([]string, 0) + + i := 0 + for i < len(args) { + currArg := args[i] + i++ + + for i < len(args) && len(currArg)+len(args[i])+1 < maxLen { + currArg += " " + args[i] + i++ + } + res = append(res, currArg) + } + return res +} + // Raw sends a raw line to the server, should really only be used for // debugging purposes but may well come in handy. func (conn *Conn) Raw(rawline string) { @@ -299,6 +317,14 @@ func (conn *Conn) Cap(subcommmand string, capabilities ...string) { if len(capabilities) == 0 { conn.Raw(CAP + " " + subcommmand) } else { - conn.Raw(CAP + " " + subcommmand + " :" + strings.Join(capabilities, " ")) + cmdPrefix := CAP + " " + subcommmand + " :" + for _, args := range splitArgs(capabilities, defaultSplit-len(cmdPrefix)) { + conn.Raw(cmdPrefix + args) + } } } + +// Authenticate send an AUTHENTICATE command to the server. +func (conn *Conn) Authenticate(message string) { + conn.Raw(AUTHENTICATE + " " + message) +} diff --git a/client/commands_test.go b/client/commands_test.go index 15a8a05..25af371 100644 --- a/client/commands_test.go +++ b/client/commands_test.go @@ -2,6 +2,8 @@ package client import ( "reflect" + "strconv" + "strings" "testing" ) @@ -203,3 +205,20 @@ func TestClientCommands(t *testing.T) { c.VHost("user", "pass") s.nc.Expect("VHOST user pass") } + +func TestSplitCommand(t *testing.T) { + nArgs := 100 + + args := make([]string, 0) + for i := 0; i < nArgs; i++ { + args = append(args, "arg"+strconv.Itoa(i)) + } + + for maxLen := 1; maxLen <= defaultSplit; maxLen *= 2 { + for _, argStr := range splitArgs(args, maxLen) { + if len(argStr) > maxLen && len(strings.Split(argStr, " ")) > 1 { + t.Errorf("maxLen = %d, but len(cmd) = %d", maxLen, len(argStr)) + } + } + } +} diff --git a/client/connection.go b/client/connection.go index f98bc68..8087a38 100644 --- a/client/connection.go +++ b/client/connection.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "crypto/tls" - "errors" "fmt" "io" "net" @@ -13,6 +12,7 @@ import ( "sync" "time" + sasl "github.com/emersion/go-sasl" "github.com/fluffle/goirc/logging" "github.com/fluffle/goirc/state" "golang.org/x/net/proxy" @@ -38,13 +38,22 @@ type Conn struct { // I/O stuff to server dialer *net.Dialer - proxyDialer proxy.ContextDialer + proxyDialer proxy.Dialer sock net.Conn io *bufio.ReadWriter in chan *Line out chan string connected bool + // Capabilities supported by the server + supportedCaps *capSet + + // Capabilites currently enabled + currCaps *capSet + + // SASL internals + saslRemainingData []byte + // CancelFunc and WaitGroup for goroutines die context.CancelFunc wg sync.WaitGroup @@ -89,8 +98,18 @@ type Config struct { // Passed through to https://golang.org/pkg/net/#Dialer DualStack bool + // Enable IRCv3 capability negotiation. + EnableCapabilityNegotiation bool + + // A list of capabilities to request to the server during registration. + Capabilites []string + + // SASL configuration to use to authenticate the connection. + Sasl sasl.Client + // Replaceable function to customise the 433 handler's new nick. - // By default an underscore "_" is appended to the current nick. + // By default the current nick's last character is "incremented". + // See DefaultNewNick implementation below for details. NewNick func(string) string // Client->server ping frequency, in seconds. Defaults to 3m. @@ -125,12 +144,13 @@ type Config struct { // name, but these are optional. func NewConfig(nick string, args ...string) *Config { cfg := &Config{ - Me: &state.Nick{Nick: nick}, - PingFreq: 3 * time.Minute, - NewNick: DefaultNewNick, - Recover: (*Conn).LogPanic, // in dispatch.go - SplitLen: defaultSplit, - Timeout: 60 * time.Second, + Me: &state.Nick{Nick: nick}, + PingFreq: 3 * time.Minute, + NewNick: DefaultNewNick, + Recover: (*Conn).LogPanic, // in dispatch.go + SplitLen: defaultSplit, + Timeout: 60 * time.Second, + EnableCapabilityNegotiation: false, } cfg.Me.Ident = "goirc" if len(args) > 0 && args[0] != "" { @@ -203,14 +223,22 @@ func Client(cfg *Config) *Conn { } } + if cfg.Sasl != nil && !cfg.EnableCapabilityNegotiation { + logging.Warn("Enabling capability negotiation as it's required for SASL") + cfg.EnableCapabilityNegotiation = true + } + conn := &Conn{ - cfg: cfg, - dialer: dialer, - intHandlers: handlerSet(), - fgHandlers: handlerSet(), - bgHandlers: handlerSet(), - stRemovers: make([]Remover, 0, len(stHandlers)), - lastsent: time.Now(), + cfg: cfg, + dialer: dialer, + intHandlers: handlerSet(), + fgHandlers: handlerSet(), + bgHandlers: handlerSet(), + stRemovers: make([]Remover, 0, len(stHandlers)), + lastsent: time.Now(), + supportedCaps: capabilitySet(), + currCaps: capabilitySet(), + saslRemainingData: nil, } conn.addIntHandlers() return conn @@ -230,10 +258,9 @@ func (conn *Conn) Connected() bool { // affect client behaviour. To disable flood protection temporarily, // for example, a handler could do: // -// conn.Config().Flood = true -// // Send many lines to the IRC server, risking "excess flood" -// conn.Config().Flood = false -// +// conn.Config().Flood = true +// // Send many lines to the IRC server, risking "excess flood" +// conn.Config().Flood = false func (conn *Conn) Config() *Config { return conn.cfg } @@ -289,6 +316,16 @@ func (conn *Conn) DisableStateTracking() { } } +// SupportsCapability returns true if the server supports the given capability. +func (conn *Conn) SupportsCapability(cap string) bool { + return conn.supportedCaps.Has(cap) +} + +// HasCapability returns true if the given capability has been acked by the server during negotiation. +func (conn *Conn) HasCapability(cap string) bool { + return conn.currCaps.Has(cap) +} + // Per-connection state initialisation. func (conn *Conn) initialise() { conn.io = nil @@ -367,25 +404,13 @@ func (conn *Conn) internalConnect(ctx context.Context) error { } if conn.cfg.Proxy != "" { - proxyURL, err := url.Parse(conn.cfg.Proxy) + s, err := conn.dialProxy(ctx) if err != nil { + logging.Info("irc.Connect(): Connecting via proxy %q: %v", + conn.cfg.Proxy, err) return err } - proxyDialer, err := proxy.FromURL(proxyURL, conn.dialer) - if err != nil { - return err - } - contextProxyDialer, ok := proxyDialer.(proxy.ContextDialer) - if !ok { - return errors.New("Dialer for proxy does not support context") - } - conn.proxyDialer = contextProxyDialer - logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) - if s, err := conn.proxyDialer.DialContext(ctx, "tcp", conn.cfg.Server); err == nil { - conn.sock = s - } else { - return err - } + conn.sock = s } else { logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) if s, err := conn.dialer.DialContext(ctx, "tcp", conn.cfg.Server); err == nil { @@ -409,6 +434,28 @@ func (conn *Conn) internalConnect(ctx context.Context) error { return nil } +// dialProxy handles dialling via a proxy +func (conn *Conn) dialProxy(ctx context.Context) (net.Conn, error) { + proxyURL, err := url.Parse(conn.cfg.Proxy) + if err != nil { + return nil, fmt.Errorf("parsing url: %v", err) + } + proxyDialer, err := proxy.FromURL(proxyURL, conn.dialer) + if err != nil { + return nil, fmt.Errorf("creating dialer: %v", err) + } + conn.proxyDialer = proxyDialer + contextProxyDialer, ok := proxyDialer.(proxy.ContextDialer) + if ok { + logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) + return contextProxyDialer.DialContext(ctx, "tcp", conn.cfg.Server) + } else { + logging.Warn("Dialer for proxy does not support context, please implement DialContext") + logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) + return conn.proxyDialer.Dial("tcp", conn.cfg.Server) + } +} + // postConnect performs post-connection setup, for ease of testing. func (conn *Conn) postConnect(ctx context.Context, start bool) { conn.io = bufio.NewReadWriter( @@ -503,13 +550,17 @@ func (conn *Conn) ping(ctx context.Context) { // It pulls Lines from the input channel and dispatches them to any // handlers that have been registered for that IRC verb. func (conn *Conn) runLoop(ctx context.Context) { - defer conn.wg.Done() for { select { case line := <-conn.in: conn.dispatch(line) case <-ctx.Done(): - // control channel closed, bail out + // control channel closed, trigger Cancel() to clean + // things up properly and bail out + + // We can't defer this, because Close() waits for it. + conn.wg.Done() + conn.Close() return } } diff --git a/client/connection_test.go b/client/connection_test.go index c6a3b95..50d32bc 100644 --- a/client/connection_test.go +++ b/client/connection_test.go @@ -103,6 +103,28 @@ func TestEOF(t *testing.T) { } } +func TestCleanupOnContextDone(t *testing.T) { + c, s := setUp(t) + // Since we're not using tearDown() here, manually call Finish() + defer s.ctrl.Finish() + + // Close() triggers DISCONNECT handler after cleaning up the state + // use this as a proxy to check that Close() was indeed called + dcon := callCheck(t) + c.Handle(DISCONNECTED, dcon) + + // Simulate context cancelation using our cancel func + c.die() + + // Verify that disconnected handler was called + dcon.assertWasCalled("Conn did not call disconnected handlers.") + + // Verify that the connection no longer thinks it's connected + if c.Connected() { + t.Errorf("Conn still thinks it's connected to the server.") + } +} + func TestClientAndStateTracking(t *testing.T) { ctrl := gomock.NewController(t) st := state.NewMockTracker(ctrl) diff --git a/client/handlers.go b/client/handlers.go index 24165ad..3920c02 100644 --- a/client/handlers.go +++ b/client/handlers.go @@ -4,22 +4,37 @@ package client // to manage tracking an irc connection etc. import ( + "sort" "strings" + "sync" "time" + "encoding/base64" "github.com/fluffle/goirc/logging" ) +// saslCap is the IRCv3 capability used for SASL authentication. +const saslCap = "sasl" + // sets up the internal event handlers to do essential IRC protocol things var intHandlers = map[string]HandlerFunc{ - REGISTER: (*Conn).h_REGISTER, - "001": (*Conn).h_001, - "433": (*Conn).h_433, - CTCP: (*Conn).h_CTCP, - NICK: (*Conn).h_NICK, - PING: (*Conn).h_PING, + REGISTER: (*Conn).h_REGISTER, + "001": (*Conn).h_001, + "433": (*Conn).h_433, + CTCP: (*Conn).h_CTCP, + NICK: (*Conn).h_NICK, + PING: (*Conn).h_PING, + CAP: (*Conn).h_CAP, + "410": (*Conn).h_410, + AUTHENTICATE: (*Conn).h_AUTHENTICATE, + "903": (*Conn).h_903, + "904": (*Conn).h_904, + "908": (*Conn).h_908, } +// set up the ircv3 capabilities supported by this client which will be requested by default to the server. +var defaultCaps = []string{} + func (conn *Conn) addIntHandlers() { for n, h := range intHandlers { // internal handlers are essential for the IRC client @@ -35,6 +50,10 @@ func (conn *Conn) h_PING(line *Line) { // Handler for initial registration with server once tcp connection is made. func (conn *Conn) h_REGISTER(line *Line) { + if conn.cfg.EnableCapabilityNegotiation { + conn.Cap(CAP_LS) + } + if conn.cfg.Pass != "" { conn.Pass(conn.cfg.Pass) } @@ -42,6 +61,214 @@ func (conn *Conn) h_REGISTER(line *Line) { conn.User(conn.cfg.Me.Ident, conn.cfg.Me.Name) } +func (conn *Conn) getRequestCapabilities() *capSet { + s := capabilitySet() + + // add capabilites supported by the client + s.Add(defaultCaps...) + + if conn.cfg.Sasl != nil { + // add the SASL cap if enabled + s.Add(saslCap) + } + + // add capabilites requested by the user + s.Add(conn.cfg.Capabilites...) + + return s +} + +func (conn *Conn) negotiateCapabilities(supportedCaps []string) { + conn.supportedCaps.Add(supportedCaps...) + + reqCaps := conn.getRequestCapabilities() + reqCaps.Intersect(conn.supportedCaps) + + if reqCaps.Size() > 0 { + conn.Cap(CAP_REQ, reqCaps.Slice()...) + } else { + conn.Cap(CAP_END) + } +} + +func (conn *Conn) handleCapAck(caps []string) { + gotSasl := false + for _, cap := range caps { + conn.currCaps.Add(cap) + + if conn.cfg.Sasl != nil && cap == saslCap { + mech, ir, err := conn.cfg.Sasl.Start() + + if err != nil { + logging.Warn("SASL authentication failed: %v", err) + continue + } + + // TODO: when IRC 3.2 capability negotiation is supported, ensure the + // capability value is used to match the chosen mechanism + + gotSasl = true + conn.saslRemainingData = ir + + conn.Authenticate(mech) + } + } + + if !gotSasl { + conn.Cap(CAP_END) + } +} + +func (conn *Conn) handleCapNak(caps []string) { + conn.Cap(CAP_END) +} + +const ( + CAP_LS = "LS" + CAP_REQ = "REQ" + CAP_ACK = "ACK" + CAP_NAK = "NAK" + CAP_END = "END" +) + +type capSet struct { + caps map[string]bool + mu sync.RWMutex +} + +func capabilitySet() *capSet { + return &capSet{ + caps: make(map[string]bool), + } +} + +func (c *capSet) Add(caps ...string) { + c.mu.Lock() + for _, cap := range caps { + if strings.HasPrefix(cap, "-") { + c.caps[cap[1:]] = false + } else { + c.caps[cap] = true + } + } + c.mu.Unlock() +} + +func (c *capSet) Has(cap string) bool { + c.mu.RLock() + defer c.mu.RUnlock() + return c.caps[cap] +} + +// Intersect computes the intersection of two sets. +func (c *capSet) Intersect(other *capSet) { + c.mu.Lock() + + for cap := range c.caps { + if !other.Has(cap) { + delete(c.caps, cap) + } + } + + c.mu.Unlock() +} + +func (c *capSet) Slice() []string { + c.mu.RLock() + defer c.mu.RUnlock() + + capSlice := make([]string, 0, len(c.caps)) + for cap := range c.caps { + capSlice = append(capSlice, cap) + } + + // make output predictable for testing + sort.Strings(capSlice) + return capSlice +} + +func (c *capSet) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.caps) +} + +// This handler is triggered when an invalid cap command is received by the server. +func (conn *Conn) h_410(line *Line) { + logging.Warn("Invalid cap subcommand: ", line.Args[1]) +} + +// Handler for capability negotiation commands. +// Note that even if multiple CAP_END commands may be sent to the server during negotiation, +// only the first will be considered. +func (conn *Conn) h_CAP(line *Line) { + subcommand := line.Args[1] + + caps := strings.Fields(line.Text()) + switch subcommand { + case CAP_LS: + conn.negotiateCapabilities(caps) + case CAP_ACK: + conn.handleCapAck(caps) + case CAP_NAK: + conn.handleCapNak(caps) + } +} + +// Handler for SASL authentication +func (conn *Conn) h_AUTHENTICATE(line *Line) { + if conn.cfg.Sasl == nil { + return + } + + if conn.saslRemainingData != nil { + data := "+" // plus sign representing empty data + if len(conn.saslRemainingData) > 0 { + data = base64.StdEncoding.EncodeToString(conn.saslRemainingData) + } + + // TODO: batch data into chunks of 400 bytes per the spec + + conn.Authenticate(data) + conn.saslRemainingData = nil + return + } + + // TODO: handle data over 400 bytes long (which will be chunked into multiple messages per the spec) + challenge, err := base64.StdEncoding.DecodeString(line.Args[0]) + if err != nil { + logging.Error("Failed to decode SASL challenge: %v", err) + return + } + + response, err := conn.cfg.Sasl.Next(challenge) + if err != nil { + logging.Error("Failed to generate response for SASL challenge: %v", err) + return + } + + // TODO: batch data into chunks of 400 bytes per the spec + data := base64.StdEncoding.EncodeToString(response) + conn.Authenticate(data) +} + +// Handler for RPL_SASLSUCCESS. +func (conn *Conn) h_903(line *Line) { + conn.Cap(CAP_END) +} + +// Handler for RPL_SASLFAILURE. +func (conn *Conn) h_904(line *Line) { + logging.Warn("SASL authentication failed") + conn.Cap(CAP_END) +} + +// Handler for RPL_SASLMECHS. +func (conn *Conn) h_908(line *Line) { + logging.Warn("SASL mechanism not supported, supported mechanisms are: %v", line.Args[1]) + conn.Cap(CAP_END) +} + // Handler to trigger a CONNECTED event on receipt of numeric 001 // : 001 :Welcome message !@ func (conn *Conn) h_001(line *Line) { diff --git a/client/handlers_test.go b/client/handlers_test.go index 171e5c9..1e374d5 100644 --- a/client/handlers_test.go +++ b/client/handlers_test.go @@ -456,3 +456,54 @@ func Test671(t *testing.T) { s.st.EXPECT().GetNick("user2").Return(nil) c.h_671(ParseLine(":irc.server.org 671 test user2 :some ignored text")) } + +func TestCap(t *testing.T) { + c, s := setUp(t) + defer s.tearDown() + + c.Config().EnableCapabilityNegotiation = true + c.Config().Capabilites = []string{"cap1", "cap2", "cap3", "cap4"} + + c.h_REGISTER(&Line{Cmd: REGISTER}) + s.nc.Expect("CAP LS") + s.nc.Expect("NICK test") + s.nc.Expect("USER test 12 * :Testing IRC") + + // Ensure that capabilities not supported by the server are not requested + s.nc.Send("CAP * LS :cap2 cap4") + s.nc.Expect("CAP REQ :cap2 cap4") + + s.nc.Send("CAP * ACK :cap2 cap4") + s.nc.Expect("CAP END") + + for _, cap := range []string{"cap2", "cap4"} { + if !c.SupportsCapability(cap) { + t.Fail() + } + + if !c.HasCapability(cap) { + t.Fail() + } + } + + for _, cap := range []string{"cap1", "cap3"} { + if c.HasCapability(cap) { + t.Fail() + } + } + + // test disable capability after registration + s.c.Cap("REQ", "-cap4") + s.nc.Expect("CAP REQ :-cap4") + + s.nc.Send("CAP * ACK :-cap4") + s.nc.Expect("CAP END") + + if !c.HasCapability("cap2") { + t.Fail() + } + + if c.HasCapability("cap4") { + t.Fail() + } +} diff --git a/client/sasl_test.go b/client/sasl_test.go new file mode 100644 index 0000000..073a074 --- /dev/null +++ b/client/sasl_test.go @@ -0,0 +1,103 @@ +package client + +import ( + "github.com/emersion/go-sasl" + "testing" +) + +func TestSaslPlainSuccessWorkflow(t *testing.T) { + c, s := setUp(t) + defer s.tearDown() + + c.Config().Sasl = sasl.NewPlainClient("", "example", "password") + c.Config().EnableCapabilityNegotiation = true + + c.h_REGISTER(&Line{Cmd: REGISTER}) + s.nc.Expect("CAP LS") + s.nc.Expect("NICK test") + s.nc.Expect("USER test 12 * :Testing IRC") + s.nc.Send("CAP * LS :sasl foobar") + s.nc.Expect("CAP REQ :sasl") + s.nc.Send("CAP * ACK :sasl") + s.nc.Expect("AUTHENTICATE PLAIN") + s.nc.Send("AUTHENTICATE +") + s.nc.Expect("AUTHENTICATE AGV4YW1wbGUAcGFzc3dvcmQ=") + s.nc.Send("904 test :SASL authentication successful") + s.nc.Expect("CAP END") +} + +func TestSaslPlainWrongPassword(t *testing.T) { + c, s := setUp(t) + defer s.tearDown() + + c.Config().Sasl = sasl.NewPlainClient("", "example", "password") + c.Config().EnableCapabilityNegotiation = true + + c.h_REGISTER(&Line{Cmd: REGISTER}) + s.nc.Expect("CAP LS") + s.nc.Expect("NICK test") + s.nc.Expect("USER test 12 * :Testing IRC") + s.nc.Send("CAP * LS :sasl foobar") + s.nc.Expect("CAP REQ :sasl") + s.nc.Send("CAP * ACK :sasl") + s.nc.Expect("AUTHENTICATE PLAIN") + s.nc.Send("AUTHENTICATE +") + s.nc.Expect("AUTHENTICATE AGV4YW1wbGUAcGFzc3dvcmQ=") + s.nc.Send("904 test :SASL authentication failed") + s.nc.Expect("CAP END") +} + +func TestSaslExternalSuccessWorkflow(t *testing.T) { + c, s := setUp(t) + defer s.tearDown() + + c.Config().Sasl = sasl.NewExternalClient("") + c.Config().EnableCapabilityNegotiation = true + + c.h_REGISTER(&Line{Cmd: REGISTER}) + s.nc.Expect("CAP LS") + s.nc.Expect("NICK test") + s.nc.Expect("USER test 12 * :Testing IRC") + s.nc.Send("CAP * LS :sasl foobar") + s.nc.Expect("CAP REQ :sasl") + s.nc.Send("CAP * ACK :sasl") + s.nc.Expect("AUTHENTICATE EXTERNAL") + s.nc.Send("AUTHENTICATE +") + s.nc.Expect("AUTHENTICATE +") + s.nc.Send("904 test :SASL authentication successful") + s.nc.Expect("CAP END") +} + +func TestSaslNoSaslCap(t *testing.T) { + c, s := setUp(t) + defer s.tearDown() + + c.Config().Sasl = sasl.NewPlainClient("", "example", "password") + c.Config().EnableCapabilityNegotiation = true + + c.h_REGISTER(&Line{Cmd: REGISTER}) + s.nc.Expect("CAP LS") + s.nc.Expect("NICK test") + s.nc.Expect("USER test 12 * :Testing IRC") + s.nc.Send("CAP * LS :foobar") + s.nc.Expect("CAP END") +} + +func TestSaslUnsupportedMechanism(t *testing.T) { + c, s := setUp(t) + defer s.tearDown() + + c.Config().Sasl = sasl.NewPlainClient("", "example", "password") + c.Config().EnableCapabilityNegotiation = true + + c.h_REGISTER(&Line{Cmd: REGISTER}) + s.nc.Expect("CAP LS") + s.nc.Expect("NICK test") + s.nc.Expect("USER test 12 * :Testing IRC") + s.nc.Send("CAP * LS :sasl foobar") + s.nc.Expect("CAP REQ :sasl") + s.nc.Send("CAP * ACK :sasl") + s.nc.Expect("AUTHENTICATE PLAIN") + s.nc.Send("908 test external :are available SASL mechanisms") + s.nc.Expect("CAP END") +} diff --git a/go.mod b/go.mod index c9d6b80..c87e5fb 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,11 @@ module github.com/fluffle/goirc require ( + github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead + github.com/fluffle/golog/logging v0.0.0-20180928190033-7d99e85061cb + github.com/golang/glog v1.0.0 github.com/golang/mock v1.5.0 - golang.org/x/net v0.0.0-20210119194325-5f4716e94777 + golang.org/x/net v0.18.0 ) go 1.13 diff --git a/go.sum b/go.sum index 4a29870..6b12740 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,57 @@ +github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= +github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/fluffle/golog/logging v0.0.0-20180928190033-7d99e85061cb h1:r//eMD/5sdzxVy34UP5fFvRWIL2L8QZtaDgVCKfVQLI= +github.com/fluffle/golog/logging v0.0.0-20180928190033-7d99e85061cb/go.mod h1:w8+az2+kPHMcsaKnTnGapWTNToJK8BogkHiAncvqKsM= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/state/nick.go b/state/nick.go index b29d98a..24a03bd 100644 --- a/state/nick.go +++ b/state/nick.go @@ -71,7 +71,7 @@ func (nk *nick) Nick() *Nick { Host: nk.host, Name: nk.name, Modes: nk.modes.Copy(), - Channels: make(map[string]*ChanPrivs), + Channels: make(map[string]*ChanPrivs, len(nk.chans)), } for c, cp := range nk.chans { n.Channels[c.name] = cp.Copy() @@ -157,6 +157,7 @@ func (nm *NickMode) Equals(other *NickMode) bool { } // Returns a string representing the nick. Looks like: +// // Nick: e.g. CowMaster // Hostmask: e.g. moo@cows.org // Real Name: e.g. Steve "CowMaster" Bush @@ -181,6 +182,7 @@ func (nk *nick) String() string { } // Returns a string representing the nick modes. Looks like: +// // +iwx func (nm *NickMode) String() string { if nm == nil { diff --git a/state/nick_test.go b/state/nick_test.go index 1344400..717c8a0 100644 --- a/state/nick_test.go +++ b/state/nick_test.go @@ -1,6 +1,9 @@ package state -import "testing" +import ( + "fmt" + "testing" +) func compareNick(t *testing.T, nk *nick) { n := nk.Nick() @@ -86,3 +89,30 @@ func TestNickParseModes(t *testing.T) { t.Errorf("Modes not flipped correctly by ParseModes.") } } + +func BenchmarkNickSingleChan(b *testing.B) { + nk := newNick("test1") + ch := newChannel("#test1") + cp := new(ChanPrivs) + nk.addChannel(ch, cp) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + nk.Nick() + } +} + +func BenchmarkNickManyChan(b *testing.B) { + const numChans = 1000 + nk := newNick("test1") + for i := 0; i < numChans; i++ { + ch := newChannel(fmt.Sprintf("#test%d", i)) + cp := new(ChanPrivs) + nk.addChannel(ch, cp) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + nk.Nick() + } +}