diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bdb4a496..88765afc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: "setup go" uses: "actions/setup-go@v3" with: - go-version: "1.22" + go-version: "1.23" - name: "install python3-pytest" run: "sudo apt install -y python3-pytest" - name: "make install" diff --git a/Dockerfile b/Dockerfile index 09cd32a7..22f566d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ## build ergo binary -FROM docker.io/golang:1.22-alpine AS build-env +FROM docker.io/golang:1.23-alpine AS build-env RUN apk upgrade -U --force-refresh --no-cache RUN apk add --no-cache --purge --clean-protected -l -u make git diff --git a/default.yaml b/default.yaml index 39eef02f..5fd22b6d 100644 --- a/default.yaml +++ b/default.yaml @@ -820,7 +820,7 @@ lock-file: "ircd.lock" # datastore configuration datastore: - # path to the datastore + # path to the database file (used to store account and channel registrations): path: ircd.db # if the database schema requires an upgrade, `autoupgrade` will attempt to diff --git a/gencapdefs.py b/gencapdefs.py index e1cfbc7f..a64ef143 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -219,6 +219,12 @@ CAPDEFS = [ url="https://github.com/ircv3/ircv3-specifications/pull/527", standard="proposed IRCv3", ), + CapDef( + identifier="ExtendedISupport", + name="draft/extended-isupport", + url="https://github.com/ircv3/ircv3-specifications/pull/543", + standard="proposed IRCv3", + ), ] def validate_defs(): diff --git a/go.mod b/go.mod index b08b4a75..d3c8f952 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ergochat/ergo -go 1.22 +go 1.23 require ( code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 @@ -8,7 +8,7 @@ require ( github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 - github.com/ergochat/irc-go v0.5.0-rc1 + github.com/ergochat/irc-go v0.5.0-rc2 github.com/go-sql-driver/mysql v1.7.0 github.com/go-test/deep v1.0.6 // indirect github.com/gofrs/flock v0.8.1 diff --git a/go.sum b/go.sum index ba268555..cfe45432 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5 github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881/go.mod h1:ASYJtQujNitna6cVHsNQTGrfWvMPJ5Sa2lZlmsH65uM= github.com/ergochat/irc-go v0.5.0-rc1 h1:kFoIHExoNFQ2CV+iShAVna/H4xrXQB4t4jK5Sep2j9k= github.com/ergochat/irc-go v0.5.0-rc1/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0= +github.com/ergochat/irc-go v0.5.0-rc2 h1:VuSQJF5K4hWvYSzGa4b8vgL6kzw8HF6LSOejE+RWpAo= +github.com/ergochat/irc-go v0.5.0-rc2/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0= github.com/ergochat/scram v1.0.2-ergo1 h1:2bYXiRFQH636pT0msOG39fmEYl4Eq+OuutcyDsCix/g= github.com/ergochat/scram v1.0.2-ergo1/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/ergochat/websocket v1.4.2-oragono1 h1:plMUunFBM6UoSCIYCKKclTdy/TkkHfUslhOfJQzfueM= diff --git a/irc/caps/constants.go b/irc/caps/constants.go index 57f088e3..38497a2b 100644 --- a/irc/caps/constants.go +++ b/irc/caps/constants.go @@ -64,10 +64,11 @@ const ( BotTagName = "bot" // https://ircv3.net/specs/extensions/chathistory ChathistoryTargetsBatchType = "draft/chathistory-targets" + ExtendedISupportBatchType = "draft/extended-isupport" ) func init() { - nameToCapability = make(map[string]Capability) + nameToCapability = make(map[string]Capability, numCapabs) for capab, name := range capabilityNames { nameToCapability[name] = Capability(capab) } diff --git a/irc/caps/defs.go b/irc/caps/defs.go index e035c541..3a1e25c2 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -53,6 +53,10 @@ const ( // https://github.com/ircv3/ircv3-specifications/pull/362 EventPlayback Capability = iota + // ExtendedISupport is the proposed IRCv3 capability named "draft/extended-isupport": + // https://github.com/ircv3/ircv3-specifications/pull/543 + ExtendedISupport Capability = iota + // Languages is the proposed IRCv3 capability named "draft/languages": // https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6 Languages Capability = iota @@ -165,6 +169,7 @@ var ( "draft/channel-rename", "draft/chathistory", "draft/event-playback", + "draft/extended-isupport", "draft/languages", "draft/message-redaction", "draft/multiline", diff --git a/irc/client.go b/irc/client.go index ad66d9b2..e2af62b3 100644 --- a/irc/client.go +++ b/irc/client.go @@ -179,6 +179,8 @@ type Session struct { batchCounter atomic.Uint32 + isupportSentPrereg bool + quitMessage string awayMessage string diff --git a/irc/commands.go b/irc/commands.go index ccbec383..4bd88dd7 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -152,6 +152,10 @@ func init() { handler: isonHandler, minParams: 1, }, + "ISUPPORT": { + handler: isupportHandler, + usablePreReg: true, + }, "JOIN": { handler: joinHandler, minParams: 1, diff --git a/irc/config.go b/irc/config.go index db07426f..9cf3bee9 100644 --- a/irc/config.go +++ b/irc/config.go @@ -1664,6 +1664,7 @@ func (config *Config) generateISupport() (err error) { isupport.Add("RPCHAN", "E") isupport.Add("RPUSER", "E") } + isupport.Add("SAFELIST", "") isupport.Add("STATUSMSG", "~&@%+") isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d", maxTargetsString, maxTargetsString, maxTargetsString, config.Limits.MonitorEntries)) isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen)) diff --git a/irc/flock/flock.go b/irc/flock/flock.go index 42d89270..5f4f5913 100644 --- a/irc/flock/flock.go +++ b/irc/flock/flock.go @@ -1,4 +1,4 @@ -//go:build !plan9 +//go:build !(plan9 || solaris) package flock diff --git a/irc/flock/flock_plan9.go b/irc/flock/flock_unsupported.go similarity index 79% rename from irc/flock/flock_plan9.go rename to irc/flock/flock_unsupported.go index 06ffeb7c..8cb5e587 100644 --- a/irc/flock/flock_plan9.go +++ b/irc/flock/flock_unsupported.go @@ -1,4 +1,4 @@ -//go:build plan9 +//go:build plan9 || solaris package flock diff --git a/irc/handlers.go b/irc/handlers.go index 6318a783..d498628b 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -873,7 +873,6 @@ func debugHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon switch param { case "GCSTATS": stats := debug.GCStats{ - Pause: make([]time.Duration, 10), PauseQuantiles: make([]time.Duration, 5), } debug.ReadGCStats(&stats) @@ -1350,6 +1349,15 @@ func isonHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons return false } +// ISUPPORT +func isupportHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { + server.RplISupport(client, rb) + if !client.registered { + rb.session.isupportSentPrereg = true + } + return false +} + // JOIN {,} [{,}] func joinHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { // #1417: allow `JOIN 0` with a confirmation code diff --git a/irc/help.go b/irc/help.go index 343bd717..a11bb725 100644 --- a/irc/help.go +++ b/irc/help.go @@ -259,6 +259,11 @@ appropriate channel privs.`, text: `ISON { } Returns whether the given nicks exist on the network.`, + }, + "isupport": { + text: `ISUPPORT + +Returns RPL_ISUPPORT lines describing the server's capabilities.`, }, "join": { text: `JOIN {,} [{,}] diff --git a/irc/server.go b/irc/server.go index 98de78b1..ebec4791 100644 --- a/irc/server.go +++ b/irc/server.go @@ -446,7 +446,9 @@ func (server *Server) playRegistrationBurst(session *Session) { session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, rplMyInfo1, rplMyInfo2, rplMyInfo3) rb := NewResponseBuffer(session) - server.RplISupport(c, rb) + if !(rb.session.capabilities.Has(caps.ExtendedISupport) && rb.session.isupportSentPrereg) { + server.RplISupport(c, rb) + } if d.account != "" && session.capabilities.Has(caps.Persistence) { reportPersistenceStatus(c, rb, false) } @@ -468,10 +470,17 @@ func (server *Server) playRegistrationBurst(session *Session) { // RplISupport outputs our ISUPPORT lines to the client. This is used on connection and in VERSION responses. func (server *Server) RplISupport(client *Client, rb *ResponseBuffer) { + server.sendRplISupportLines(client, rb, server.Config().Server.isupport.CachedReply) +} + +func (server *Server) sendRplISupportLines(client *Client, rb *ResponseBuffer, lines [][]string) { + if rb.session.capabilities.Has(caps.ExtendedISupport) { + batchID := rb.StartNestedBatch(caps.ExtendedISupportBatchType) + defer rb.EndNestedBatch(batchID) + } translatedISupport := client.t("are supported by this server") nick := client.Nick() - config := server.Config() - for _, cachedTokenLine := range config.Server.isupport.CachedReply { + for _, cachedTokenLine := range lines { length := len(cachedTokenLine) + 2 tokenline := make([]string, length) tokenline[0] = nick @@ -806,13 +815,19 @@ func (server *Server) applyConfig(config *Config) (err error) { } if !initial { - // push new info to all of our clients - for _, sClient := range server.clients.AllClients() { - for _, tokenline := range newISupportReplies { - sClient.Send(nil, server.name, RPL_ISUPPORT, append([]string{sClient.nick}, tokenline...)...) + // send 005 updates (somewhat rare) + if len(newISupportReplies) != 0 { + for _, sClient := range server.clients.AllClients() { + for _, session := range sClient.Sessions() { + rb := NewResponseBuffer(session) + server.sendRplISupportLines(sClient, rb, newISupportReplies) + rb.Send(false) + } } + } - if sendRawOutputNotice { + if sendRawOutputNotice { + for _, sClient := range server.clients.AllClients() { sClient.Notice(sClient.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) } } diff --git a/traditional.yaml b/traditional.yaml index 922f5c37..73165adc 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -791,7 +791,7 @@ lock-file: "ircd.lock" # datastore configuration datastore: - # path to the datastore + # path to the database file (used to store account and channel registrations): path: ircd.db # if the database schema requires an upgrade, `autoupgrade` will attempt to diff --git a/vendor/github.com/ergochat/irc-go/ircmsg/message.go b/vendor/github.com/ergochat/irc-go/ircmsg/message.go index 7a7e1305..ae27c37b 100644 --- a/vendor/github.com/ergochat/irc-go/ircmsg/message.go +++ b/vendor/github.com/ergochat/irc-go/ircmsg/message.go @@ -196,6 +196,15 @@ func trimInitialSpaces(str string) string { return str[i:] } +func isASCII(str string) bool { + for i := 0; i < len(str); i++ { + if str[i] > 127 { + return false + } + } + return true +} + func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Message, err error) { // remove either \n or \r\n from the end of the line: line = strings.TrimSuffix(line, "\n") @@ -265,11 +274,16 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Messa commandEnd = len(line) paramStart = len(line) } - // normalize command to uppercase: - ircmsg.Command = strings.ToUpper(line[:commandEnd]) - if len(ircmsg.Command) == 0 { + baseCommand := line[:commandEnd] + if len(baseCommand) == 0 { return ircmsg, ErrorLineIsEmpty } + // technically this must be either letters or a 3-digit numeric: + if !isASCII(baseCommand) { + return ircmsg, ErrorLineContainsBadChar + } + // normalize command to uppercase: + ircmsg.Command = strings.ToUpper(baseCommand) line = line[paramStart:] for { diff --git a/vendor/github.com/ergochat/irc-go/ircutils/sasl.go b/vendor/github.com/ergochat/irc-go/ircutils/sasl.go index a4c612c6..31d3d0de 100644 --- a/vendor/github.com/ergochat/irc-go/ircutils/sasl.go +++ b/vendor/github.com/ergochat/irc-go/ircutils/sasl.go @@ -3,7 +3,6 @@ package ircutils import ( "encoding/base64" "errors" - "strings" ) var ( @@ -25,6 +24,7 @@ func EncodeSASLResponse(raw []byte) (result []string) { } response := base64.StdEncoding.EncodeToString(raw) + result = make([]string, 0, (len(response)/400)+1) lastLen := 0 for len(response) > 0 { // TODO once we require go 1.21, this can be: lastLen = min(len(response), 400) @@ -48,11 +48,11 @@ func EncodeSASLResponse(raw []byte) (result []string) { // Do not copy a SASLBuffer after first use. type SASLBuffer struct { maxLength int - buffer strings.Builder + buf []byte } // NewSASLBuffer returns a new SASLBuffer. maxLength is the maximum amount of -// base64'ed data to buffer (0 for no limit). +// data to buffer (0 for no limit). func NewSASLBuffer(maxLength int) *SASLBuffer { result := new(SASLBuffer) result.Initialize(maxLength) @@ -69,37 +69,43 @@ func (b *SASLBuffer) Initialize(maxLength int) { // response along with any decoding or protocol errors detected. func (b *SASLBuffer) Add(value string) (done bool, output []byte, err error) { if value == "+" { - output, err = b.getAndReset() - return true, output, err + // total size is a multiple of 400 (possibly 0) + output = b.buf + b.Clear() + return true, output, nil } if len(value) > 400 { - b.buffer.Reset() + b.Clear() return true, nil, ErrSASLTooLong } - if b.maxLength != 0 && (b.buffer.Len()+len(value)) > b.maxLength { - b.buffer.Reset() + curLen := len(b.buf) + chunkDecodedLen := base64.StdEncoding.DecodedLen(len(value)) + if b.maxLength != 0 && (curLen+chunkDecodedLen) > b.maxLength { + b.Clear() return true, nil, ErrSASLLimitExceeded } - b.buffer.WriteString(value) + // "append-make pattern" as in the bytes.Buffer implementation: + b.buf = append(b.buf, make([]byte, chunkDecodedLen)...) + n, err := base64.StdEncoding.Decode(b.buf[curLen:], []byte(value)) + b.buf = b.buf[0 : curLen+n] + if err != nil { + b.Clear() + return true, nil, err + } if len(value) < 400 { - output, err = b.getAndReset() - return true, output, err + output = b.buf + b.Clear() + return true, output, nil } else { - // 400 bytes, wait for continuation line or + return false, nil, nil } } // Clear resets the buffer state. func (b *SASLBuffer) Clear() { - b.buffer.Reset() -} - -func (b *SASLBuffer) getAndReset() (output []byte, err error) { - output, err = base64.StdEncoding.DecodeString(b.buffer.String()) - b.buffer.Reset() - return + // we can't reuse this buffer in general since we may have returned it + b.buf = nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index fb83aecc..d4850817 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -25,7 +25,7 @@ github.com/ergochat/confusables # github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 ## explicit; go 1.18 github.com/ergochat/go-ident -# github.com/ergochat/irc-go v0.5.0-rc1 +# github.com/ergochat/irc-go v0.5.0-rc2 ## explicit; go 1.15 github.com/ergochat/irc-go/ircfmt github.com/ergochat/irc-go/ircmsg