From e0c319f8fff54a665d3bea2405cd51289824c3eb Mon Sep 17 00:00:00 2001 From: Luca Bigliardi Date: Sat, 20 Feb 2021 02:54:09 +0100 Subject: [PATCH 01/21] Support context during connect Signed-off-by: Luca Bigliardi --- client/connection.go | 32 ++++++++++++++++++++++++-------- go.mod | 4 ++-- go.sum | 7 +++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/client/connection.go b/client/connection.go index 20a7fa3..e0bbc47 100644 --- a/client/connection.go +++ b/client/connection.go @@ -2,7 +2,9 @@ package client import ( "bufio" + "context" "crypto/tls" + "errors" "fmt" "io" "net" @@ -36,7 +38,7 @@ type Conn struct { // I/O stuff to server dialer *net.Dialer - proxyDialer proxy.Dialer + proxyDialer proxy.ContextDialer sock net.Conn io *bufio.ReadWriter in chan *Line @@ -304,11 +306,16 @@ func (conn *Conn) initialise() { // Config.Server to host, Config.Pass to pass if one is provided, and then // calls Connect. func (conn *Conn) ConnectTo(host string, pass ...string) error { + return conn.ConnectToContext(context.Background(), host, pass...) +} + +// ConnectToContext works like ConnectTo but uses the provided context. +func (conn *Conn) ConnectToContext(ctx context.Context, host string, pass ...string) error { conn.cfg.Server = host if len(pass) > 0 { conn.cfg.Pass = pass[0] } - return conn.Connect() + return conn.ConnectContext(ctx) } // Connect connects the IRC client to the server configured in Config.Server. @@ -323,10 +330,15 @@ func (conn *Conn) ConnectTo(host string, pass ...string) error { // handler for the CONNECTED event is used to perform any initial client work // like joining channels and sending messages. func (conn *Conn) Connect() error { + return conn.ConnectContext(context.Background()) +} + +// ConnectContext works like Connect but uses the provided context. +func (conn *Conn) ConnectContext(ctx context.Context) error { // We don't want to hold conn.mu while firing the REGISTER event, // and it's much easier and less error prone to defer the unlock, // so the connect mechanics have been delegated to internalConnect. - err := conn.internalConnect() + err := conn.internalConnect(ctx) if err == nil { conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()}) } @@ -334,7 +346,7 @@ func (conn *Conn) Connect() error { } // internalConnect handles the work of actually connecting to the server. -func (conn *Conn) internalConnect() error { +func (conn *Conn) internalConnect(ctx context.Context) error { conn.mu.Lock() defer conn.mu.Unlock() conn.initialise() @@ -359,20 +371,24 @@ func (conn *Conn) internalConnect() error { if err != nil { return err } - conn.proxyDialer, err = proxy.FromURL(proxyURL, conn.dialer) + 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.Dial("tcp", conn.cfg.Server); err == nil { + if s, err := conn.proxyDialer.DialContext(ctx, "tcp", conn.cfg.Server); err == nil { conn.sock = s } else { return err } } else { logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) - if s, err := conn.dialer.Dial("tcp", conn.cfg.Server); err == nil { + if s, err := conn.dialer.DialContext(ctx, "tcp", conn.cfg.Server); err == nil { conn.sock = s } else { return err diff --git a/go.mod b/go.mod index f1da5df..c9d6b80 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/fluffle/goirc require ( - github.com/golang/mock v1.1.1 - golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 + github.com/golang/mock v1.5.0 + golang.org/x/net v0.0.0-20210119194325-5f4716e94777 ) go 1.13 diff --git a/go.sum b/go.sum index 42e780a..9d85e55 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,11 @@ github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= 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= golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 h1:dgd4x4kJt7G4k4m93AYLzM8Ni6h2qLTfh9n9vXJT3/0= 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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +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= From cb4b7e5d82f0f13b6003f1c5d3588836a20304e8 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Fri, 26 Mar 2021 09:06:36 +0000 Subject: [PATCH 02/21] Go version update; mod tidy. --- .travis.yml | 4 ++-- go.sum | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9bdd9a6..aaef0bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ language: go go: - - 1.14 - - 1.13.8 + - 1.16.2 + - 1.15.10 sudo : false diff --git a/go.sum b/go.sum index 9d85e55..4a29870 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,21 @@ -github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= -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= -golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 h1:dgd4x4kJt7G4k4m93AYLzM8Ni6h2qLTfh9n9vXJT3/0= +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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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/sync v0.0.0-20190423024810-112230192c58/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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/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/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= From 1bb2dff2988578be620f573643a60260bce13775 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Fri, 26 Mar 2021 11:20:00 +0000 Subject: [PATCH 03/21] Accept nick from 001 message. Fixes #110. --- client/connection_test.go | 34 ++++++++++++++++++---------------- client/handlers.go | 36 +++++++++++++++++++++++++----------- client/handlers_test.go | 9 +++++++-- client/line.go | 18 +++++++++++++----- client/line_test.go | 28 ++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 34 deletions(-) diff --git a/client/connection_test.go b/client/connection_test.go index 3b7e5b7..e56df44 100644 --- a/client/connection_test.go +++ b/client/connection_test.go @@ -458,20 +458,22 @@ func TestRunLoop(t *testing.T) { c, s := setUp(t, false) defer s.tearDown() - // Set up a handler to detect whether 001 handler is called - h001 := callCheck(t) - c.HandleFunc("001", func(conn *Conn, line *Line) { - h001.call() - }) + // Set up a handler to detect whether 002 handler is called. + // Don't use 001 here, since there's already a handler for that + // and it hangs this test unless we mock the state tracker calls. h002 := callCheck(t) - // Set up a handler to detect whether 002 handler is called c.HandleFunc("002", func(conn *Conn, line *Line) { h002.call() }) + h003 := callCheck(t) + // Set up a handler to detect whether 002 handler is called + c.HandleFunc("003", func(conn *Conn, line *Line) { + h003.call() + }) - l1 := ParseLine(":irc.server.org 001 test :First test line.") - c.in <- l1 - h001.assertNotCalled("001 handler called before runLoop started.") + l2 := ParseLine(":irc.server.org 002 test :First test line.") + c.in <- l2 + h002.assertNotCalled("002 handler called before runLoop started.") // We want to test that the a goroutine calling runLoop will exit correctly. // Now, we can expect the call to Dispatch to take place as runLoop starts. @@ -482,13 +484,13 @@ func TestRunLoop(t *testing.T) { c.runLoop() exited.call() }() - h001.assertWasCalled("001 handler not called after runLoop started.") + h002.assertWasCalled("002 handler not called after runLoop started.") // Send another line, just to be sure :-) - h002.assertNotCalled("002 handler called before expected.") - l2 := ParseLine(":irc.server.org 002 test :Second test line.") - c.in <- l2 - h002.assertWasCalled("002 handler not called while runLoop started.") + h003.assertNotCalled("003 handler called before expected.") + l3 := ParseLine(":irc.server.org 003 test :Second test line.") + c.in <- l3 + h003.assertWasCalled("003 handler not called while runLoop started.") // Now, use the control channel to exit send and kill the goroutine. // This sneakily uses the fact that the other two goroutines that would @@ -500,8 +502,8 @@ func TestRunLoop(t *testing.T) { exited.assertWasCalled("Didn't exit after signal.") // Sending more on c.in shouldn't dispatch any further events - c.in <- l1 - h001.assertNotCalled("001 handler called after runLoop ended.") + c.in <- l2 + h002.assertNotCalled("002 handler called after runLoop ended.") } func TestWrite(t *testing.T) { diff --git a/client/handlers.go b/client/handlers.go index b538579..24165ad 100644 --- a/client/handlers.go +++ b/client/handlers.go @@ -6,6 +6,8 @@ package client import ( "strings" "time" + + "github.com/fluffle/goirc/logging" ) // sets up the internal event handlers to do essential IRC protocol things @@ -41,20 +43,32 @@ func (conn *Conn) h_REGISTER(line *Line) { } // Handler to trigger a CONNECTED event on receipt of numeric 001 +// : 001 :Welcome message !@ func (conn *Conn) h_001(line *Line) { - // we're connected! - conn.dispatch(&Line{Cmd: CONNECTED, Time: time.Now()}) - // and we're being given our hostname (from the server's perspective) - t := line.Args[len(line.Args)-1] + // We're connected! Defer this for control flow reasons. + defer conn.dispatch(&Line{Cmd: CONNECTED, Time: time.Now()}) + + // Accept the server's opinion of what our nick actually is + // and record our ident and hostname (from the server's perspective) + me, nick, t := conn.Me(), line.Target(), line.Text() if idx := strings.LastIndex(t, " "); idx != -1 { t = t[idx+1:] - if idx = strings.Index(t, "@"); idx != -1 { - if conn.st != nil { - me := conn.Me() - conn.st.NickInfo(me.Nick, me.Ident, t[idx+1:], me.Name) - } else { - conn.cfg.Me.Host = t[idx+1:] - } + } + _, ident, host, ok := parseUserHost(t) + + if me.Nick != nick { + logging.Warn("Server changed our nick on connect: old=%q new=%q", me.Nick, nick) + } + if conn.st != nil { + if ok { + conn.st.NickInfo(me.Nick, ident, host, me.Name) + } + conn.cfg.Me = conn.st.ReNick(me.Nick, nick) + } else { + conn.cfg.Me.Nick = nick + if ok { + conn.cfg.Me.Ident = ident + conn.cfg.Me.Host = host } } } diff --git a/client/handlers_test.go b/client/handlers_test.go index d706ae6..171e5c9 100644 --- a/client/handlers_test.go +++ b/client/handlers_test.go @@ -44,7 +44,7 @@ func Test001(t *testing.T) { c, s := setUp(t) defer s.tearDown() - l := ParseLine(":irc.server.org 001 test :Welcome to IRC test!ident@somehost.com") + l := ParseLine(":irc.server.org 001 newnick :Welcome to IRC newnick!ident@somehost.com") // Set up a handler to detect whether connected handler is called from 001 hcon := false c.HandleFunc("connected", func(conn *Conn, line *Line) { @@ -54,7 +54,12 @@ func Test001(t *testing.T) { // Test state tracking first. gomock.InOrder( s.st.EXPECT().Me().Return(c.cfg.Me), - s.st.EXPECT().NickInfo("test", "test", "somehost.com", "Testing IRC"), + s.st.EXPECT().NickInfo("test", "ident", "somehost.com", "Testing IRC"), + s.st.EXPECT().ReNick("test", "newnick").Return(&state.Nick{ + Nick: "newnick", + Ident: c.cfg.Me.Ident, + Name: c.cfg.Me.Name, + }), ) // Call handler with a valid 001 line c.h_001(l) diff --git a/client/line.go b/client/line.go index bfa473a..5275767 100644 --- a/client/line.go +++ b/client/line.go @@ -154,11 +154,10 @@ func ParseLine(s string) *Line { // src can be the hostname of the irc server or a nick!user@host line.Host = line.Src - nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@") - if uidx != -1 && nidx != -1 { - line.Nick = line.Src[:nidx] - line.Ident = line.Src[nidx+1 : uidx] - line.Host = line.Src[uidx+1:] + if n, i, h, ok := parseUserHost(line.Src); ok { + line.Nick = n + line.Ident = i + line.Host = h } } @@ -205,6 +204,15 @@ func ParseLine(s string) *Line { return line } +func parseUserHost(uh string) (nick, ident, host string, ok bool) { + uh = strings.TrimSpace(uh) + nidx, uidx := strings.Index(uh, "!"), strings.Index(uh, "@") + if uidx == -1 || nidx == -1 { + return "", "", "", false + } + return uh[:nidx], uh[nidx+1 : uidx], uh[uidx+1:], true +} + func (line *Line) argslen(minlen int) bool { pc, _, _, _ := runtime.Caller(1) fn := runtime.FuncForPC(pc) diff --git a/client/line_test.go b/client/line_test.go index 88b758d..7316525 100644 --- a/client/line_test.go +++ b/client/line_test.go @@ -184,3 +184,31 @@ func TestLineTags(t *testing.T) { } } } + +func TestParseUserHost(t *testing.T) { + tests := []struct { + in, nick, ident, host string + ok bool + }{ + {"", "", "", "", false}, + {" ", "", "", "", false}, + {"somestring", "", "", "", false}, + {" s p ", "", "", "", false}, + {"foo!bar", "", "", "", false}, + {"foo@baz.com", "", "", "", false}, + {"foo!bar@baz.com", "foo", "bar", "baz.com", true}, + {" foo!bar@baz.com", "foo", "bar", "baz.com", true}, + {" foo!bar@baz.com ", "foo", "bar", "baz.com", true}, + } + + for i, test := range tests { + nick, ident, host, ok := parseUserHost(test.in) + if test.nick != nick || + test.ident != ident || + test.host != host || + test.ok != ok { + t.Errorf("%d: parseUserHost(%q) = %q, %q, %q, %t; want %q, %q, %q, %t", + i, test.in, nick, ident, host, ok, test.nick, test.ident, test.host, test.ok) + } + } +} From 27cc39787d9532518606d58daf31a1546cfad137 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Fri, 26 Mar 2021 12:02:36 +0000 Subject: [PATCH 04/21] Use a Context to kill internal goroutines. --- client/connection.go | 35 +++++++++++++++------------- client/connection_test.go | 49 ++++++++++++++++++--------------------- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/client/connection.go b/client/connection.go index e0bbc47..f98bc68 100644 --- a/client/connection.go +++ b/client/connection.go @@ -45,8 +45,8 @@ type Conn struct { out chan string connected bool - // Control channel and WaitGroup for goroutines - die chan struct{} + // CancelFunc and WaitGroup for goroutines + die context.CancelFunc wg sync.WaitGroup // Internal counters for flood protection @@ -295,7 +295,7 @@ func (conn *Conn) initialise() { conn.sock = nil conn.in = make(chan *Line, 32) conn.out = make(chan string, 32) - conn.die = make(chan struct{}) + conn.die = nil if conn.st != nil { conn.st.Wipe() } @@ -404,24 +404,25 @@ func (conn *Conn) internalConnect(ctx context.Context) error { conn.sock = s } - conn.postConnect(true) + conn.postConnect(ctx, true) conn.connected = true return nil } // postConnect performs post-connection setup, for ease of testing. -func (conn *Conn) postConnect(start bool) { +func (conn *Conn) postConnect(ctx context.Context, start bool) { conn.io = bufio.NewReadWriter( bufio.NewReader(conn.sock), bufio.NewWriter(conn.sock)) if start { + ctx, conn.die = context.WithCancel(ctx) conn.wg.Add(3) - go conn.send() + go conn.send(ctx) go conn.recv() - go conn.runLoop() + go conn.runLoop(ctx) if conn.cfg.PingFreq > 0 { conn.wg.Add(1) - go conn.ping() + go conn.ping(ctx) } } } @@ -434,8 +435,8 @@ func hasPort(s string) bool { // send is started as a goroutine after a connection is established. // It shuttles data from the output channel to write(), and is killed -// when Conn.die is closed. -func (conn *Conn) send() { +// when the context is cancelled. +func (conn *Conn) send(ctx context.Context) { for { select { case line := <-conn.out: @@ -446,7 +447,7 @@ func (conn *Conn) send() { conn.Close() return } - case <-conn.die: + case <-ctx.Done(): // control channel closed, bail out conn.wg.Done() return @@ -483,14 +484,14 @@ func (conn *Conn) recv() { // ping is started as a goroutine after a connection is established, as // long as Config.PingFreq >0. It pings the server every PingFreq seconds. -func (conn *Conn) ping() { +func (conn *Conn) ping(ctx context.Context) { defer conn.wg.Done() tick := time.NewTicker(conn.cfg.PingFreq) for { select { case <-tick.C: conn.Ping(fmt.Sprintf("%d", time.Now().UnixNano())) - case <-conn.die: + case <-ctx.Done(): // control channel closed, bail out tick.Stop() return @@ -501,13 +502,13 @@ func (conn *Conn) ping() { // runLoop is started as a goroutine after a connection is established. // 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() { +func (conn *Conn) runLoop(ctx context.Context) { defer conn.wg.Done() for { select { case line := <-conn.in: conn.dispatch(line) - case <-conn.die: + case <-ctx.Done(): // control channel closed, bail out return } @@ -572,7 +573,9 @@ func (conn *Conn) Close() error { logging.Info("irc.Close(): Disconnected from server.") conn.connected = false err := conn.sock.Close() - close(conn.die) + if conn.die != nil { + conn.die() + } // Drain both in and out channels to avoid a deadlock if the buffers // have filled. See TestSendDeadlockOnFullBuffer in connection_test.go. conn.drainIn() diff --git a/client/connection_test.go b/client/connection_test.go index e56df44..b4f3145 100644 --- a/client/connection_test.go +++ b/client/connection_test.go @@ -1,6 +1,7 @@ package client import ( + "context" "runtime" "strings" "testing" @@ -54,6 +55,7 @@ func setUp(t *testing.T, start ...bool) (*Conn, *testState) { nc := MockNetConn(t) c := SimpleClient("test", "test", "Testing IRC") c.initialise() + ctx := context.Background() c.st = st c.sock = nc @@ -61,7 +63,7 @@ func setUp(t *testing.T, start ...bool) (*Conn, *testState) { c.connected = true // If a second argument is passed to setUp, we tell postConnect not to // start the various goroutines that shuttle data around. - c.postConnect(len(start) == 0) + c.postConnect(ctx, len(start) == 0) // Sleep 1ms to allow background routines to start. <-time.After(time.Millisecond) @@ -166,7 +168,7 @@ func TestClientAndStateTracking(t *testing.T) { ctrl.Finish() } -func TestSendExitsOnDie(t *testing.T) { +func TestSendExitsOnCancel(t *testing.T) { // Passing a second value to setUp stops goroutines from starting c, s := setUp(t, false) defer s.tearDown() @@ -178,10 +180,11 @@ func TestSendExitsOnDie(t *testing.T) { // We want to test that the a goroutine calling send will exit correctly. exited := callCheck(t) + ctx, cancel := context.WithCancel(context.Background()) // send() will decrement the WaitGroup, so we must increment it. c.wg.Add(1) go func() { - c.send() + c.send(ctx) exited.call() }() @@ -194,13 +197,9 @@ func TestSendExitsOnDie(t *testing.T) { c.out <- "SENT AFTER START" s.nc.Expect("SENT AFTER START") - // Now, use the control channel to exit send and kill the goroutine. - // This sneakily uses the fact that the other two goroutines that would - // normally be waiting for die to close are not running, so we only send - // to the goroutine started above. Normally Close() closes c.die and - // signals to all three goroutines (send, ping, runLoop) to exit. + // Now, cancel the context to exit send and kill the goroutine. exited.assertNotCalled("Exited before signal sent.") - c.die <- struct{}{} + cancel() exited.assertWasCalled("Didn't exit after signal.") s.nc.ExpectNothing() @@ -221,7 +220,7 @@ func TestSendExitsOnWriteError(t *testing.T) { // send() will decrement the WaitGroup, so we must increment it. c.wg.Add(1) go func() { - c.send() + c.send(context.Background()) exited.call() }() @@ -251,6 +250,7 @@ func TestSendDeadlockOnFullBuffer(t *testing.T) { // We want to test that the a goroutine calling send will exit correctly. loopExit := callCheck(t) sendExit := callCheck(t) + ctx, cancel := context.WithCancel(context.Background()) // send() and runLoop() will decrement the WaitGroup, so we must increment it. c.wg.Add(2) @@ -274,13 +274,13 @@ func TestSendDeadlockOnFullBuffer(t *testing.T) { }) // And trigger it by starting runLoop and inserting a line into conn.in: go func() { - c.runLoop() + c.runLoop(ctx) loopExit.call() }() c.in <- &Line{Cmd: PRIVMSG, Raw: "WRITE THAT CAUSES DEADLOCK"} // At this point the handler should be blocked on a write to conn.out, - // preventing runLoop from looping and thus noticing conn.die is closed. + // preventing runLoop from looping and thus noticng the cancelled context. // // The next part is to force send() to call conn.Close(), which can // be done by closing the fake net.Conn so that it returns an error on @@ -292,8 +292,9 @@ func TestSendDeadlockOnFullBuffer(t *testing.T) { // to write it to the socket. It should immediately receive an error and // call conn.Close(), triggering the deadlock as it waits forever for // runLoop to call conn.wg.Done. + c.die = cancel // Close needs to cancel the context for us. go func() { - c.send() + c.send(ctx) sendExit.call() }() @@ -407,10 +408,11 @@ func TestPing(t *testing.T) { // Start ping loop. exited := callCheck(t) + ctx, cancel := context.WithCancel(context.Background()) // ping() will decrement the WaitGroup, so we must increment it. c.wg.Add(1) go func() { - c.ping() + c.ping(ctx) exited.call() }() @@ -438,13 +440,9 @@ func TestPing(t *testing.T) { t.Errorf("Line not output after another %s.", 2*res) } - // Now kill the ping loop. - // This sneakily uses the fact that the other two goroutines that would - // normally be waiting for die to close are not running, so we only send - // to the goroutine started above. Normally Close() closes c.die and - // signals to all three goroutines (send, ping, runLoop) to exit. + // Now kill the ping loop by cancelling the context. exited.assertNotCalled("Exited before signal sent.") - c.die <- struct{}{} + cancel() exited.assertWasCalled("Didn't exit after signal.") // Make sure we're no longer pinging by waiting >2x PingFreq <-time.After(2*c.cfg.PingFreq + res) @@ -478,10 +476,11 @@ func TestRunLoop(t *testing.T) { // We want to test that the a goroutine calling runLoop will exit correctly. // Now, we can expect the call to Dispatch to take place as runLoop starts. exited := callCheck(t) + ctx, cancel := context.WithCancel(context.Background()) // runLoop() will decrement the WaitGroup, so we must increment it. c.wg.Add(1) go func() { - c.runLoop() + c.runLoop(ctx) exited.call() }() h002.assertWasCalled("002 handler not called after runLoop started.") @@ -492,13 +491,9 @@ func TestRunLoop(t *testing.T) { c.in <- l3 h003.assertWasCalled("003 handler not called while runLoop started.") - // Now, use the control channel to exit send and kill the goroutine. - // This sneakily uses the fact that the other two goroutines that would - // normally be waiting for die to close are not running, so we only send - // to the goroutine started above. Normally Close() closes c.die and - // signals to all three goroutines (send, ping, runLoop) to exit. + // Now, cancel the context to exit runLoop and kill the goroutine. exited.assertNotCalled("Exited before signal sent.") - c.die <- struct{}{} + cancel() exited.assertWasCalled("Didn't exit after signal.") // Sending more on c.in shouldn't dispatch any further events From 1a10eba91a175d04ac581f95bd5d5d87fdd3cc35 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Fri, 26 Mar 2021 12:36:19 +0000 Subject: [PATCH 05/21] Use a Context in mockNetConn too. --- client/mocknetconn_test.go | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/client/mocknetconn_test.go b/client/mocknetconn_test.go index e736c88..fc80de4 100644 --- a/client/mocknetconn_test.go +++ b/client/mocknetconn_test.go @@ -1,6 +1,7 @@ package client import ( + "context" "io" "net" "os" @@ -14,7 +15,7 @@ type mockNetConn struct { In, Out chan string in, out chan []byte - die chan struct{} + die context.CancelFunc closed bool rt, wt time.Time @@ -22,7 +23,8 @@ type mockNetConn struct { func MockNetConn(t *testing.T) *mockNetConn { // Our mock connection is a testing object - m := &mockNetConn{T: t, die: make(chan struct{})} + ctx, cancel := context.WithCancel(context.Background()) + m := &mockNetConn{T: t, die: cancel} // buffer input m.In = make(chan string, 20) @@ -30,7 +32,7 @@ func MockNetConn(t *testing.T) *mockNetConn { go func() { for { select { - case <-m.die: + case <-ctx.Done(): return case s := <-m.In: m.in <- []byte(s) @@ -44,7 +46,7 @@ func MockNetConn(t *testing.T) *mockNetConn { go func() { for { select { - case <-m.die: + case <-ctx.Done(): return case b := <-m.out: m.Out <- string(b) @@ -89,15 +91,12 @@ func (m *mockNetConn) Read(b []byte) (int, error) { if m.Closed() { return 0, os.ErrInvalid } - l := 0 - select { - case s := <-m.in: - l = len(s) - copy(b, s) - case <-m.die: - return 0, io.EOF + s, ok := <-m.in + copy(b, s) + if !ok { + return len(s), io.EOF } - return l, nil + return len(s), nil } func (m *mockNetConn) Write(s []byte) (int, error) { @@ -114,19 +113,16 @@ func (m *mockNetConn) Close() error { if m.Closed() { return os.ErrInvalid } + m.closed = true // Shut down *ALL* the goroutines! // This will trigger an EOF event in Read() too - close(m.die) + m.die() + close(m.in) return nil } func (m *mockNetConn) Closed() bool { - select { - case <-m.die: - return true - default: - return false - } + return m.closed } func (m *mockNetConn) LocalAddr() net.Addr { From 5bb083547bb340673bdf375ba97342fd9ae68500 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Fri, 26 Mar 2021 12:40:28 +0000 Subject: [PATCH 06/21] Make call checker a handler. --- client/connection_test.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/client/connection_test.go b/client/connection_test.go index b4f3145..c6a3b95 100644 --- a/client/connection_test.go +++ b/client/connection_test.go @@ -24,6 +24,10 @@ func (c checker) call() { c.c <- struct{}{} } +func (c checker) Handle(_ *Conn, _ *Line) { + c.call() +} + func (c checker) assertNotCalled(fmt string, args ...interface{}) { select { case <-c.c: @@ -85,9 +89,7 @@ func TestEOF(t *testing.T) { // Set up a handler to detect whether disconnected handlers are called dcon := callCheck(t) - c.HandleFunc(DISCONNECTED, func(conn *Conn, line *Line) { - dcon.call() - }) + c.Handle(DISCONNECTED, dcon) // Simulate EOF from server s.nc.Close() @@ -460,14 +462,10 @@ func TestRunLoop(t *testing.T) { // Don't use 001 here, since there's already a handler for that // and it hangs this test unless we mock the state tracker calls. h002 := callCheck(t) - c.HandleFunc("002", func(conn *Conn, line *Line) { - h002.call() - }) + c.Handle("002", h002) h003 := callCheck(t) // Set up a handler to detect whether 002 handler is called - c.HandleFunc("003", func(conn *Conn, line *Line) { - h003.call() - }) + c.Handle("003", h003) l2 := ParseLine(":irc.server.org 002 test :First test line.") c.in <- l2 From c874d8df171512945bfa3a4d55a5304f744bd0c6 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Fri, 26 Mar 2021 13:17:34 +0000 Subject: [PATCH 07/21] Update README to add shammash as a contributor. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 75081eb..6d7c8ea 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Contributions gratefully received from: - [soul9](https://github.com/soul9) - [jakebailey](https://github.com/jakebailey) - [stapelberg](https://github.com/stapelberg) + - [shammash](https://github.com/shammash) And thanks to the following for minor doc/fix PRs: From 58c9607dfbf015b1db117a8a900546771399a550 Mon Sep 17 00:00:00 2001 From: Luca Bigliardi Date: Sat, 27 Mar 2021 11:35:19 +0100 Subject: [PATCH 08/21] Fix connection cleanup when context is canceled Signed-off-by: Luca Bigliardi --- client/connection.go | 8 ++++++-- client/connection_test.go | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/client/connection.go b/client/connection.go index f98bc68..43bcc24 100644 --- a/client/connection.go +++ b/client/connection.go @@ -503,13 +503,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) From b1565dba18adb1a21ac3fdaa4671b33b78a46310 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 3 Feb 2022 10:09:10 +0100 Subject: [PATCH 09/21] Add section for "Projects using GoIRC" --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 6d7c8ea..687554d 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. From 54099b85a38bcdd828615a146ece7dfc0a435eb7 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 6 Mar 2022 23:20:06 +0100 Subject: [PATCH 10/21] Implement feature request #77: Support IRCv3 capability negotiation during registration --- client/commands.go | 22 ++++++- client/commands_test.go | 19 ++++++ client/connection.go | 51 +++++++++++---- client/handlers.go | 139 ++++++++++++++++++++++++++++++++++++++++ client/handlers_test.go | 51 +++++++++++++++ 5 files changed, 268 insertions(+), 14 deletions(-) diff --git a/client/commands.go b/client/commands.go index 101c7d3..dac7059 100644 --- a/client/commands.go +++ b/client/commands.go @@ -84,6 +84,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 +316,9 @@ 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) + } } } 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 43bcc24..8ed5a84 100644 --- a/client/connection.go +++ b/client/connection.go @@ -45,6 +45,12 @@ type Conn struct { out chan string connected bool + // Capabilities supported by the server + supportedCaps *capSet + + // Capabilites currently enabled + currCaps *capSet + // CancelFunc and WaitGroup for goroutines die context.CancelFunc wg sync.WaitGroup @@ -89,6 +95,12 @@ 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 + // Replaceable function to customise the 433 handler's new nick. // By default an underscore "_" is appended to the current nick. NewNick func(string) string @@ -125,12 +137,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] != "" { @@ -204,13 +217,15 @@ func Client(cfg *Config) *Conn { } 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(), } conn.addIntHandlers() return conn @@ -289,6 +304,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 diff --git a/client/handlers.go b/client/handlers.go index 24165ad..d2317df 100644 --- a/client/handlers.go +++ b/client/handlers.go @@ -4,7 +4,9 @@ package client // to manage tracking an irc connection etc. import ( + "sort" "strings" + "sync" "time" "github.com/fluffle/goirc/logging" @@ -18,8 +20,13 @@ var intHandlers = map[string]HandlerFunc{ CTCP: (*Conn).h_CTCP, NICK: (*Conn).h_NICK, PING: (*Conn).h_PING, + CAP: (*Conn).h_CAP, + "410": (*Conn).h_410, } +// 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 +42,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 +53,134 @@ 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...) + + // 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) { + for _, cap := range caps { + conn.currCaps.Add(cap) + } + 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 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() + } +} From 33c2868b34cc48a130904244a85a6e376be6a25b Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 23 Mar 2022 09:36:35 +0000 Subject: [PATCH 11/21] Add ostafen to contributors. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 687554d..d9cc432 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Contributions gratefully received from: - [jakebailey](https://github.com/jakebailey) - [stapelberg](https://github.com/stapelberg) - [shammash](https://github.com/shammash) + - [ostefan](https://github.com/ostafen) And thanks to the following for minor doc/fix PRs: From 5b481cf00ad4aba4e021eb1ffe285ec47b75a554 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 23 Mar 2022 09:38:41 +0000 Subject: [PATCH 12/21] Update travis for recent go versions. --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 From bbbcc9aa5b1d2fc1f5050fcb259e933088d41b85 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Wed, 23 Mar 2022 09:40:06 +0000 Subject: [PATCH 13/21] Noticed typo as soon as I pushed :-( --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9cc432..7513db1 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Contributions gratefully received from: - [jakebailey](https://github.com/jakebailey) - [stapelberg](https://github.com/stapelberg) - [shammash](https://github.com/shammash) - - [ostefan](https://github.com/ostafen) + - [ostafen](https://github.com/ostafen) And thanks to the following for minor doc/fix PRs: From e64b5d47c376f2e3e8a48e1a9698c6861f3a38d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Taavi=20V=C3=A4=C3=A4n=C3=A4nen?= Date: Sun, 30 Oct 2022 12:01:33 +0200 Subject: [PATCH 14/21] Add SASL authentication support This hacks together support for IRCv3.1 SASL. Currently only SASL PLAIN is supported, but it's implemented in a way that adding support for other types should not require too many changes to the current code. --- client/commands.go | 6 +++ client/connection.go | 38 ++++++++++------ client/handlers.go | 106 +++++++++++++++++++++++++++++++++++++++---- client/sasl_test.go | 103 +++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 3 +- 6 files changed, 234 insertions(+), 23 deletions(-) create mode 100644 client/sasl_test.go diff --git a/client/commands.go b/client/commands.go index dac7059..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" @@ -322,3 +323,8 @@ func (conn *Conn) Cap(subcommmand string, capabilities ...string) { } } } + +// Authenticate send an AUTHENTICATE command to the server. +func (conn *Conn) Authenticate(message string) { + conn.Raw(AUTHENTICATE + " " + message) +} diff --git a/client/connection.go b/client/connection.go index 8ed5a84..aa9f2f5 100644 --- a/client/connection.go +++ b/client/connection.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/emersion/go-sasl" "github.com/fluffle/goirc/logging" "github.com/fluffle/goirc/state" "golang.org/x/net/proxy" @@ -51,6 +52,9 @@ type Conn struct { // Capabilites currently enabled currCaps *capSet + // SASL internals + saslRemainingData []byte + // CancelFunc and WaitGroup for goroutines die context.CancelFunc wg sync.WaitGroup @@ -101,6 +105,9 @@ type Config struct { // 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. NewNick func(string) string @@ -216,16 +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(), - supportedCaps: capabilitySet(), - currCaps: capabilitySet(), + 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 @@ -245,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 } diff --git a/client/handlers.go b/client/handlers.go index d2317df..3920c02 100644 --- a/client/handlers.go +++ b/client/handlers.go @@ -9,19 +9,27 @@ import ( "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, - CAP: (*Conn).h_CAP, - "410": (*Conn).h_410, + 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. @@ -59,6 +67,11 @@ func (conn *Conn) getRequestCapabilities() *capSet { // 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...) @@ -79,10 +92,31 @@ func (conn *Conn) negotiateCapabilities(supportedCaps []string) { } 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) } - conn.Cap(CAP_END) } func (conn *Conn) handleCapNak(caps []string) { @@ -181,6 +215,60 @@ func (conn *Conn) h_CAP(line *Line) { } } +// 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/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..5c388bb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/fluffle/goirc require ( + github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead github.com/golang/mock v1.5.0 golang.org/x/net v0.0.0-20210119194325-5f4716e94777 ) diff --git a/go.sum b/go.sum index 4a29870..d8b7012 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,10 @@ +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/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -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= From 2b7abdce8f4af306fd9e69398868fe1f4d8016f4 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Mon, 28 Nov 2022 13:40:43 +0000 Subject: [PATCH 15/21] Add @supertassu to README, update copyright dates. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7513db1..5c1108c 100644 --- a/README.md +++ b/README.md @@ -97,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: @@ -116,6 +116,7 @@ Contributions gratefully received from: - [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: From 8b460bc60f91860f9a78e571b55cc7df557e40e7 Mon Sep 17 00:00:00 2001 From: Kobe Housen Date: Fri, 10 Nov 2023 15:55:58 +0000 Subject: [PATCH 16/21] Use make size hint when copying channels in Nick() 70% faster when using a high number of channels Old performance ``` $ go test ./state -bench=. goos: linux goarch: amd64 pkg: github.com/fluffle/goirc/state cpu: AMD Ryzen Threadripper PRO 3945WX 12-Cores BenchmarkNickSingleChan-24 15465226 76.35 ns/op BenchmarkNickManyChan-24 5738 195721 ns/op PASS ok github.com/fluffle/goirc/state 2.433s ``` new performance ``` $ go test ./state -bench=. goos: linux goarch: amd64 pkg: github.com/fluffle/goirc/state cpu: AMD Ryzen Threadripper PRO 3945WX 12-Cores BenchmarkNickSingleChan-24 15022640 79.13 ns/op BenchmarkNickManyChan-24 10000 115104 ns/op PASS ok github.com/fluffle/goirc/state 2.456s ``` --- state/nick.go | 4 +++- state/nick_test.go | 32 +++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) 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() + } +} From ceced391f3c7fae6d5b458c4ae102748e7a78c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Solymos?= <15968382+slymas@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:24:19 +0000 Subject: [PATCH 17/21] Allow non-context Dialers Dialers who don't implement DialContext but upgrade past https://github.com/fluffle/goirc/pull/109 (which is 2.5 years old but still) will break. This change falls back to the non-context dial method, where calls may take longer and time out, and goirc would give them a warning as well. --- client/connection.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/client/connection.go b/client/connection.go index aa9f2f5..5ac0918 100644 --- a/client/connection.go +++ b/client/connection.go @@ -413,15 +413,24 @@ func (conn *Conn) internalConnect(ctx context.Context) error { 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 + if ok { + 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 + } } else { - return err + logging.Warn("Dialer for proxy does not support context, please implement DialContext") + + conn.proxyDialer = proxyDialer + logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) + if s, err := conn.proxyDialer.Dial("tcp", conn.cfg.Server); err == nil { + conn.sock = s + } else { + return err + } } } else { logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) From d655f8950c49f55c15d35a5cf9273ce5deb23a3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Feb 2023 02:17:21 +0000 Subject: [PATCH 18/21] Bump golang.org/x/net from 0.0.0-20210119194325-5f4716e94777 to 0.7.0 Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20210119194325-5f4716e94777 to 0.7.0. - [Release notes](https://github.com/golang/net/releases) - [Commits](https://github.com/golang/net/commits/v0.7.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 4 +++- go.sum | 25 +++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5c388bb..40f2769 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,10 @@ 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.7.0 ) go 1.13 diff --git a/go.sum b/go.sum index d8b7012..9814963 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,43 @@ 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/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/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.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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/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/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/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/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/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= From b1a6e3a286263c22f0a69b3fb4cdb8f57fd50161 Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Thu, 23 Nov 2023 20:44:32 +0000 Subject: [PATCH 19/21] Refactor out proxy dialing code; store contextless proxy.Dialer. --- client/connection.go | 56 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/client/connection.go b/client/connection.go index 5ac0918..534ea2a 100644 --- a/client/connection.go +++ b/client/connection.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "crypto/tls" - "errors" "fmt" "io" "net" @@ -13,7 +12,7 @@ import ( "sync" "time" - "github.com/emersion/go-sasl" + sasl "github.com/emersion/go-sasl" "github.com/fluffle/goirc/logging" "github.com/fluffle/goirc/state" "golang.org/x/net/proxy" @@ -39,7 +38,7 @@ 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 @@ -404,34 +403,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 { - 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 - } - } else { - logging.Warn("Dialer for proxy does not support context, please implement DialContext") - - conn.proxyDialer = proxyDialer - logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) - if s, err := conn.proxyDialer.Dial("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 { @@ -455,6 +433,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( From 8f19c2305000ef6735826376e52f0252ca46cb7e Mon Sep 17 00:00:00 2001 From: Alex Bramley Date: Thu, 23 Nov 2023 20:48:11 +0000 Subject: [PATCH 20/21] bump x/net again, dependabot version was still ancient --- go.mod | 2 +- go.sum | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 40f2769..c87e5fb 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ require ( 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.7.0 + golang.org/x/net v0.18.0 ) go 1.13 diff --git a/go.sum b/go.sum index 9814963..6b12740 100644 --- a/go.sum +++ b/go.sum @@ -11,17 +11,24 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t 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-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= @@ -29,15 +36,22 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc 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= From 40e594abf8c403d8b33da0b1d2696e459a126980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Solymos?= <15968382+slymas@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:34:37 +0000 Subject: [PATCH 21/21] Fix NewNick's comment It wasn't updated after fixing https://github.com/fluffle/goirc/issues/108. --- client/connection.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/connection.go b/client/connection.go index 534ea2a..8087a38 100644 --- a/client/connection.go +++ b/client/connection.go @@ -108,7 +108,8 @@ type Config struct { 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.