From 9b9c39ddd4f7e9a13629e406af1d763d4e52a3eb Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 18 May 2025 01:09:13 -0400 Subject: [PATCH 01/35] changelog entry for API config --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81da32a6..51c7bd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This release includes changes to the config file format, all of which are fully Many thanks to [@csmith](https://github.com/csmith), [@delthas](https://github.com/delthas), donio, [@emersion](https://github.com/emersion), [@KlaasT](https://github.com/KlaasT), [@knolley](https://github.com/knolley), [@Mailaender](https://github.com/Mailaender), and [@prdes](https://github.com/prdes) for reporting issues and helping test. ### Config changes +* Added `api` block for configuring the new HTTP API. If this block is absent, the API is disabled (#2231) * Added `server.additional-isupport` for publishing arbitrary ISUPPORT tokens (#2220, #2240) * Added `server.command-aliases` to configure aliases for server commands (#2229, #2236) * Added options to `roleplay` to customize the NUH's sent for `NPC` and `SCENE`. Roleplay remains deprecated and disabled by default. (#2237) From 0a947115d668b53853cbff532cf3596d5bbe1a9a Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 18 May 2025 01:15:11 -0400 Subject: [PATCH 02/35] set up new development version --- irc/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irc/version.go b/irc/version.go index 929a0960..16dbbd3f 100644 --- a/irc/version.go +++ b/irc/version.go @@ -7,7 +7,7 @@ import "fmt" const ( // SemVer is the semantic version of Ergo. - SemVer = "2.16.0" + SemVer = "2.17.0-unreleased" ) var ( From e404942d833409303687014a739a2252b3b400f9 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 18 May 2025 01:27:52 -0400 Subject: [PATCH 03/35] upgrade x dependencies --- go.mod | 8 +- go.sum | 8 + vendor/golang.org/x/sys/unix/auxv.go | 36 +++ .../golang.org/x/sys/unix/auxv_unsupported.go | 13 + .../golang.org/x/sys/unix/syscall_darwin.go | 149 ++++++++++- vendor/golang.org/x/sys/unix/syscall_linux.go | 42 ++- .../golang.org/x/sys/unix/syscall_solaris.go | 87 +++++++ vendor/golang.org/x/sys/unix/zerrors_linux.go | 20 +- .../x/sys/unix/zerrors_linux_386.go | 3 + .../x/sys/unix/zerrors_linux_amd64.go | 3 + .../x/sys/unix/zerrors_linux_arm.go | 3 + .../x/sys/unix/zerrors_linux_arm64.go | 4 + .../x/sys/unix/zerrors_linux_loong64.go | 3 + .../x/sys/unix/zerrors_linux_mips.go | 3 + .../x/sys/unix/zerrors_linux_mips64.go | 3 + .../x/sys/unix/zerrors_linux_mips64le.go | 3 + .../x/sys/unix/zerrors_linux_mipsle.go | 3 + .../x/sys/unix/zerrors_linux_ppc.go | 3 + .../x/sys/unix/zerrors_linux_ppc64.go | 3 + .../x/sys/unix/zerrors_linux_ppc64le.go | 3 + .../x/sys/unix/zerrors_linux_riscv64.go | 3 + .../x/sys/unix/zerrors_linux_s390x.go | 3 + .../x/sys/unix/zerrors_linux_sparc64.go | 3 + .../x/sys/unix/zsyscall_darwin_amd64.go | 84 ++++++ .../x/sys/unix/zsyscall_darwin_amd64.s | 20 ++ .../x/sys/unix/zsyscall_darwin_arm64.go | 84 ++++++ .../x/sys/unix/zsyscall_darwin_arm64.s | 20 ++ .../x/sys/unix/zsyscall_solaris_amd64.go | 114 +++++++++ .../x/sys/unix/zsysnum_linux_386.go | 4 + .../x/sys/unix/zsysnum_linux_amd64.go | 4 + .../x/sys/unix/zsysnum_linux_arm.go | 4 + .../x/sys/unix/zsysnum_linux_arm64.go | 4 + .../x/sys/unix/zsysnum_linux_loong64.go | 4 + .../x/sys/unix/zsysnum_linux_mips.go | 4 + .../x/sys/unix/zsysnum_linux_mips64.go | 4 + .../x/sys/unix/zsysnum_linux_mips64le.go | 4 + .../x/sys/unix/zsysnum_linux_mipsle.go | 4 + .../x/sys/unix/zsysnum_linux_ppc.go | 4 + .../x/sys/unix/zsysnum_linux_ppc64.go | 4 + .../x/sys/unix/zsysnum_linux_ppc64le.go | 4 + .../x/sys/unix/zsysnum_linux_riscv64.go | 4 + .../x/sys/unix/zsysnum_linux_s390x.go | 4 + .../x/sys/unix/zsysnum_linux_sparc64.go | 4 + vendor/golang.org/x/sys/unix/ztypes_linux.go | 6 +- .../x/sys/windows/security_windows.go | 49 +++- .../x/sys/windows/syscall_windows.go | 6 +- .../golang.org/x/sys/windows/types_windows.go | 239 ++++++++++++++++++ .../x/sys/windows/zsyscall_windows.go | 9 + vendor/golang.org/x/term/terminal.go | 77 +++++- vendor/golang.org/x/text/language/parse.go | 2 +- vendor/modules.txt | 20 +- 51 files changed, 1135 insertions(+), 64 deletions(-) create mode 100644 vendor/golang.org/x/sys/unix/auxv.go create mode 100644 vendor/golang.org/x/sys/unix/auxv_unsupported.go diff --git a/go.mod b/go.mod index 6b3daa4a..416b22a1 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,9 @@ require ( github.com/stretchr/testify v1.4.0 // indirect github.com/tidwall/buntdb v1.3.2 github.com/xdg-go/scram v1.0.2 - golang.org/x/crypto v0.32.0 - golang.org/x/term v0.28.0 - golang.org/x/text v0.21.0 + golang.org/x/crypto v0.38.0 + golang.org/x/term v0.32.0 + golang.org/x/text v0.25.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -39,7 +39,7 @@ require ( github.com/tidwall/rtred v0.1.2 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.33.0 // indirect ) replace github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1 diff --git a/go.sum b/go.sum index 9cc360fc..521e691f 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyh github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= @@ -78,12 +80,18 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/golang.org/x/sys/unix/auxv.go b/vendor/golang.org/x/sys/unix/auxv.go new file mode 100644 index 00000000..37a82528 --- /dev/null +++ b/vendor/golang.org/x/sys/unix/auxv.go @@ -0,0 +1,36 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 && (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos) + +package unix + +import ( + "syscall" + "unsafe" +) + +//go:linkname runtime_getAuxv runtime.getAuxv +func runtime_getAuxv() []uintptr + +// Auxv returns the ELF auxiliary vector as a sequence of key/value pairs. +// The returned slice is always a fresh copy, owned by the caller. +// It returns an error on non-ELF platforms, or if the auxiliary vector cannot be accessed, +// which happens in some locked-down environments and build modes. +func Auxv() ([][2]uintptr, error) { + vec := runtime_getAuxv() + vecLen := len(vec) + + if vecLen == 0 { + return nil, syscall.ENOENT + } + + if vecLen%2 != 0 { + return nil, syscall.EINVAL + } + + result := make([]uintptr, vecLen) + copy(result, vec) + return unsafe.Slice((*[2]uintptr)(unsafe.Pointer(&result[0])), vecLen/2), nil +} diff --git a/vendor/golang.org/x/sys/unix/auxv_unsupported.go b/vendor/golang.org/x/sys/unix/auxv_unsupported.go new file mode 100644 index 00000000..1200487f --- /dev/null +++ b/vendor/golang.org/x/sys/unix/auxv_unsupported.go @@ -0,0 +1,13 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.21 && (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos) + +package unix + +import "syscall" + +func Auxv() ([][2]uintptr, error) { + return nil, syscall.ENOTSUP +} diff --git a/vendor/golang.org/x/sys/unix/syscall_darwin.go b/vendor/golang.org/x/sys/unix/syscall_darwin.go index 099867de..798f61ad 100644 --- a/vendor/golang.org/x/sys/unix/syscall_darwin.go +++ b/vendor/golang.org/x/sys/unix/syscall_darwin.go @@ -602,7 +602,150 @@ func Connectx(fd int, srcIf uint32, srcAddr, dstAddr Sockaddr, associd SaeAssocI return } -//sys connectx(fd int, endpoints *SaEndpoints, associd SaeAssocID, flags uint32, iov []Iovec, n *uintptr, connid *SaeConnID) (err error) +// sys connectx(fd int, endpoints *SaEndpoints, associd SaeAssocID, flags uint32, iov []Iovec, n *uintptr, connid *SaeConnID) (err error) +const minIovec = 8 + +func Readv(fd int, iovs [][]byte) (n int, err error) { + if !darwinKernelVersionMin(11, 0, 0) { + return 0, ENOSYS + } + + iovecs := make([]Iovec, 0, minIovec) + iovecs = appendBytes(iovecs, iovs) + n, err = readv(fd, iovecs) + readvRacedetect(iovecs, n, err) + return n, err +} + +func Preadv(fd int, iovs [][]byte, offset int64) (n int, err error) { + if !darwinKernelVersionMin(11, 0, 0) { + return 0, ENOSYS + } + iovecs := make([]Iovec, 0, minIovec) + iovecs = appendBytes(iovecs, iovs) + n, err = preadv(fd, iovecs, offset) + readvRacedetect(iovecs, n, err) + return n, err +} + +func Writev(fd int, iovs [][]byte) (n int, err error) { + if !darwinKernelVersionMin(11, 0, 0) { + return 0, ENOSYS + } + + iovecs := make([]Iovec, 0, minIovec) + iovecs = appendBytes(iovecs, iovs) + if raceenabled { + raceReleaseMerge(unsafe.Pointer(&ioSync)) + } + n, err = writev(fd, iovecs) + writevRacedetect(iovecs, n) + return n, err +} + +func Pwritev(fd int, iovs [][]byte, offset int64) (n int, err error) { + if !darwinKernelVersionMin(11, 0, 0) { + return 0, ENOSYS + } + + iovecs := make([]Iovec, 0, minIovec) + iovecs = appendBytes(iovecs, iovs) + if raceenabled { + raceReleaseMerge(unsafe.Pointer(&ioSync)) + } + n, err = pwritev(fd, iovecs, offset) + writevRacedetect(iovecs, n) + return n, err +} + +func appendBytes(vecs []Iovec, bs [][]byte) []Iovec { + for _, b := range bs { + var v Iovec + v.SetLen(len(b)) + if len(b) > 0 { + v.Base = &b[0] + } else { + v.Base = (*byte)(unsafe.Pointer(&_zero)) + } + vecs = append(vecs, v) + } + return vecs +} + +func writevRacedetect(iovecs []Iovec, n int) { + if !raceenabled { + return + } + for i := 0; n > 0 && i < len(iovecs); i++ { + m := int(iovecs[i].Len) + if m > n { + m = n + } + n -= m + if m > 0 { + raceReadRange(unsafe.Pointer(iovecs[i].Base), m) + } + } +} + +func readvRacedetect(iovecs []Iovec, n int, err error) { + if !raceenabled { + return + } + for i := 0; n > 0 && i < len(iovecs); i++ { + m := int(iovecs[i].Len) + if m > n { + m = n + } + n -= m + if m > 0 { + raceWriteRange(unsafe.Pointer(iovecs[i].Base), m) + } + } + if err == nil { + raceAcquire(unsafe.Pointer(&ioSync)) + } +} + +func darwinMajorMinPatch() (maj, min, patch int, err error) { + var un Utsname + err = Uname(&un) + if err != nil { + return + } + + var mmp [3]int + c := 0 +Loop: + for _, b := range un.Release[:] { + switch { + case b >= '0' && b <= '9': + mmp[c] = 10*mmp[c] + int(b-'0') + case b == '.': + c++ + if c > 2 { + return 0, 0, 0, ENOTSUP + } + case b == 0: + break Loop + default: + return 0, 0, 0, ENOTSUP + } + } + if c != 2 { + return 0, 0, 0, ENOTSUP + } + return mmp[0], mmp[1], mmp[2], nil +} + +func darwinKernelVersionMin(maj, min, patch int) bool { + actualMaj, actualMin, actualPatch, err := darwinMajorMinPatch() + if err != nil { + return false + } + return actualMaj > maj || actualMaj == maj && (actualMin > min || actualMin == min && actualPatch >= patch) +} + //sys sendfile(infd int, outfd int, offset int64, len *int64, hdtr unsafe.Pointer, flags int) (err error) //sys shmat(id int, addr uintptr, flag int) (ret uintptr, err error) @@ -705,3 +848,7 @@ func Connectx(fd int, srcIf uint32, srcAddr, dstAddr Sockaddr, associd SaeAssocI //sys write(fd int, p []byte) (n int, err error) //sys mmap(addr uintptr, length uintptr, prot int, flag int, fd int, pos int64) (ret uintptr, err error) //sys munmap(addr uintptr, length uintptr) (err error) +//sys readv(fd int, iovecs []Iovec) (n int, err error) +//sys preadv(fd int, iovecs []Iovec, offset int64) (n int, err error) +//sys writev(fd int, iovecs []Iovec) (n int, err error) +//sys pwritev(fd int, iovecs []Iovec, offset int64) (n int, err error) diff --git a/vendor/golang.org/x/sys/unix/syscall_linux.go b/vendor/golang.org/x/sys/unix/syscall_linux.go index 230a9454..4958a657 100644 --- a/vendor/golang.org/x/sys/unix/syscall_linux.go +++ b/vendor/golang.org/x/sys/unix/syscall_linux.go @@ -13,6 +13,7 @@ package unix import ( "encoding/binary" + "slices" "strconv" "syscall" "time" @@ -417,7 +418,7 @@ func (sa *SockaddrUnix) sockaddr() (unsafe.Pointer, _Socklen, error) { return nil, 0, EINVAL } sa.raw.Family = AF_UNIX - for i := 0; i < n; i++ { + for i := range n { sa.raw.Path[i] = int8(name[i]) } // length is family (uint16), name, NUL. @@ -507,7 +508,7 @@ func (sa *SockaddrL2) sockaddr() (unsafe.Pointer, _Socklen, error) { psm := (*[2]byte)(unsafe.Pointer(&sa.raw.Psm)) psm[0] = byte(sa.PSM) psm[1] = byte(sa.PSM >> 8) - for i := 0; i < len(sa.Addr); i++ { + for i := range len(sa.Addr) { sa.raw.Bdaddr[i] = sa.Addr[len(sa.Addr)-1-i] } cid := (*[2]byte)(unsafe.Pointer(&sa.raw.Cid)) @@ -589,11 +590,11 @@ func (sa *SockaddrCAN) sockaddr() (unsafe.Pointer, _Socklen, error) { sa.raw.Family = AF_CAN sa.raw.Ifindex = int32(sa.Ifindex) rx := (*[4]byte)(unsafe.Pointer(&sa.RxID)) - for i := 0; i < 4; i++ { + for i := range 4 { sa.raw.Addr[i] = rx[i] } tx := (*[4]byte)(unsafe.Pointer(&sa.TxID)) - for i := 0; i < 4; i++ { + for i := range 4 { sa.raw.Addr[i+4] = tx[i] } return unsafe.Pointer(&sa.raw), SizeofSockaddrCAN, nil @@ -618,11 +619,11 @@ func (sa *SockaddrCANJ1939) sockaddr() (unsafe.Pointer, _Socklen, error) { sa.raw.Family = AF_CAN sa.raw.Ifindex = int32(sa.Ifindex) n := (*[8]byte)(unsafe.Pointer(&sa.Name)) - for i := 0; i < 8; i++ { + for i := range 8 { sa.raw.Addr[i] = n[i] } p := (*[4]byte)(unsafe.Pointer(&sa.PGN)) - for i := 0; i < 4; i++ { + for i := range 4 { sa.raw.Addr[i+8] = p[i] } sa.raw.Addr[12] = sa.Addr @@ -911,7 +912,7 @@ func (sa *SockaddrIUCV) sockaddr() (unsafe.Pointer, _Socklen, error) { // These are EBCDIC encoded by the kernel, but we still need to pad them // with blanks. Initializing with blanks allows the caller to feed in either // a padded or an unpadded string. - for i := 0; i < 8; i++ { + for i := range 8 { sa.raw.Nodeid[i] = ' ' sa.raw.User_id[i] = ' ' sa.raw.Name[i] = ' ' @@ -1148,7 +1149,7 @@ func anyToSockaddr(fd int, rsa *RawSockaddrAny) (Sockaddr, error) { var user [8]byte var name [8]byte - for i := 0; i < 8; i++ { + for i := range 8 { user[i] = byte(pp.User_id[i]) name[i] = byte(pp.Name[i]) } @@ -1173,11 +1174,11 @@ func anyToSockaddr(fd int, rsa *RawSockaddrAny) (Sockaddr, error) { Ifindex: int(pp.Ifindex), } name := (*[8]byte)(unsafe.Pointer(&sa.Name)) - for i := 0; i < 8; i++ { + for i := range 8 { name[i] = pp.Addr[i] } pgn := (*[4]byte)(unsafe.Pointer(&sa.PGN)) - for i := 0; i < 4; i++ { + for i := range 4 { pgn[i] = pp.Addr[i+8] } addr := (*[1]byte)(unsafe.Pointer(&sa.Addr)) @@ -1188,11 +1189,11 @@ func anyToSockaddr(fd int, rsa *RawSockaddrAny) (Sockaddr, error) { Ifindex: int(pp.Ifindex), } rx := (*[4]byte)(unsafe.Pointer(&sa.RxID)) - for i := 0; i < 4; i++ { + for i := range 4 { rx[i] = pp.Addr[i] } tx := (*[4]byte)(unsafe.Pointer(&sa.TxID)) - for i := 0; i < 4; i++ { + for i := range 4 { tx[i] = pp.Addr[i+4] } return sa, nil @@ -2216,10 +2217,7 @@ func readvRacedetect(iovecs []Iovec, n int, err error) { return } for i := 0; n > 0 && i < len(iovecs); i++ { - m := int(iovecs[i].Len) - if m > n { - m = n - } + m := min(int(iovecs[i].Len), n) n -= m if m > 0 { raceWriteRange(unsafe.Pointer(iovecs[i].Base), m) @@ -2270,10 +2268,7 @@ func writevRacedetect(iovecs []Iovec, n int) { return } for i := 0; n > 0 && i < len(iovecs); i++ { - m := int(iovecs[i].Len) - if m > n { - m = n - } + m := min(int(iovecs[i].Len), n) n -= m if m > 0 { raceReadRange(unsafe.Pointer(iovecs[i].Base), m) @@ -2320,12 +2315,7 @@ func isGroupMember(gid int) bool { return false } - for _, g := range groups { - if g == gid { - return true - } - } - return false + return slices.Contains(groups, gid) } func isCapDacOverrideSet() bool { diff --git a/vendor/golang.org/x/sys/unix/syscall_solaris.go b/vendor/golang.org/x/sys/unix/syscall_solaris.go index 21974af0..abc39554 100644 --- a/vendor/golang.org/x/sys/unix/syscall_solaris.go +++ b/vendor/golang.org/x/sys/unix/syscall_solaris.go @@ -1102,3 +1102,90 @@ func (s *Strioctl) SetInt(i int) { func IoctlSetStrioctlRetInt(fd int, req int, s *Strioctl) (int, error) { return ioctlPtrRet(fd, req, unsafe.Pointer(s)) } + +// Ucred Helpers +// See ucred(3c) and getpeerucred(3c) + +//sys getpeerucred(fd uintptr, ucred *uintptr) (err error) +//sys ucredFree(ucred uintptr) = ucred_free +//sys ucredGet(pid int) (ucred uintptr, err error) = ucred_get +//sys ucredGeteuid(ucred uintptr) (uid int) = ucred_geteuid +//sys ucredGetegid(ucred uintptr) (gid int) = ucred_getegid +//sys ucredGetruid(ucred uintptr) (uid int) = ucred_getruid +//sys ucredGetrgid(ucred uintptr) (gid int) = ucred_getrgid +//sys ucredGetsuid(ucred uintptr) (uid int) = ucred_getsuid +//sys ucredGetsgid(ucred uintptr) (gid int) = ucred_getsgid +//sys ucredGetpid(ucred uintptr) (pid int) = ucred_getpid + +// Ucred is an opaque struct that holds user credentials. +type Ucred struct { + ucred uintptr +} + +// We need to ensure that ucredFree is called on the underlying ucred +// when the Ucred is garbage collected. +func ucredFinalizer(u *Ucred) { + ucredFree(u.ucred) +} + +func GetPeerUcred(fd uintptr) (*Ucred, error) { + var ucred uintptr + err := getpeerucred(fd, &ucred) + if err != nil { + return nil, err + } + result := &Ucred{ + ucred: ucred, + } + // set the finalizer on the result so that the ucred will be freed + runtime.SetFinalizer(result, ucredFinalizer) + return result, nil +} + +func UcredGet(pid int) (*Ucred, error) { + ucred, err := ucredGet(pid) + if err != nil { + return nil, err + } + result := &Ucred{ + ucred: ucred, + } + // set the finalizer on the result so that the ucred will be freed + runtime.SetFinalizer(result, ucredFinalizer) + return result, nil +} + +func (u *Ucred) Geteuid() int { + defer runtime.KeepAlive(u) + return ucredGeteuid(u.ucred) +} + +func (u *Ucred) Getruid() int { + defer runtime.KeepAlive(u) + return ucredGetruid(u.ucred) +} + +func (u *Ucred) Getsuid() int { + defer runtime.KeepAlive(u) + return ucredGetsuid(u.ucred) +} + +func (u *Ucred) Getegid() int { + defer runtime.KeepAlive(u) + return ucredGetegid(u.ucred) +} + +func (u *Ucred) Getrgid() int { + defer runtime.KeepAlive(u) + return ucredGetrgid(u.ucred) +} + +func (u *Ucred) Getsgid() int { + defer runtime.KeepAlive(u) + return ucredGetsgid(u.ucred) +} + +func (u *Ucred) Getpid() int { + defer runtime.KeepAlive(u) + return ucredGetpid(u.ucred) +} diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux.go b/vendor/golang.org/x/sys/unix/zerrors_linux.go index 6ebc48b3..4f432bfe 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux.go @@ -1245,6 +1245,7 @@ const ( FAN_REPORT_DFID_NAME = 0xc00 FAN_REPORT_DFID_NAME_TARGET = 0x1e00 FAN_REPORT_DIR_FID = 0x400 + FAN_REPORT_FD_ERROR = 0x2000 FAN_REPORT_FID = 0x200 FAN_REPORT_NAME = 0x800 FAN_REPORT_PIDFD = 0x80 @@ -1330,8 +1331,10 @@ const ( FUSE_SUPER_MAGIC = 0x65735546 FUTEXFS_SUPER_MAGIC = 0xbad1dea F_ADD_SEALS = 0x409 + F_CREATED_QUERY = 0x404 F_DUPFD = 0x0 F_DUPFD_CLOEXEC = 0x406 + F_DUPFD_QUERY = 0x403 F_EXLCK = 0x4 F_GETFD = 0x1 F_GETFL = 0x3 @@ -1551,6 +1554,7 @@ const ( IPPROTO_ROUTING = 0x2b IPPROTO_RSVP = 0x2e IPPROTO_SCTP = 0x84 + IPPROTO_SMC = 0x100 IPPROTO_TCP = 0x6 IPPROTO_TP = 0x1d IPPROTO_UDP = 0x11 @@ -1623,6 +1627,8 @@ const ( IPV6_UNICAST_IF = 0x4c IPV6_USER_FLOW = 0xe IPV6_V6ONLY = 0x1a + IPV6_VERSION = 0x60 + IPV6_VERSION_MASK = 0xf0 IPV6_XFRM_POLICY = 0x23 IP_ADD_MEMBERSHIP = 0x23 IP_ADD_SOURCE_MEMBERSHIP = 0x27 @@ -1867,6 +1873,7 @@ const ( MADV_UNMERGEABLE = 0xd MADV_WILLNEED = 0x3 MADV_WIPEONFORK = 0x12 + MAP_DROPPABLE = 0x8 MAP_FILE = 0x0 MAP_FIXED = 0x10 MAP_FIXED_NOREPLACE = 0x100000 @@ -1967,6 +1974,7 @@ const ( MSG_PEEK = 0x2 MSG_PROXY = 0x10 MSG_RST = 0x1000 + MSG_SOCK_DEVMEM = 0x2000000 MSG_SYN = 0x400 MSG_TRUNC = 0x20 MSG_TRYHARD = 0x4 @@ -2083,6 +2091,7 @@ const ( NFC_ATR_REQ_MAXSIZE = 0x40 NFC_ATR_RES_GB_MAXSIZE = 0x2f NFC_ATR_RES_MAXSIZE = 0x40 + NFC_ATS_MAXSIZE = 0x14 NFC_COMM_ACTIVE = 0x0 NFC_COMM_PASSIVE = 0x1 NFC_DEVICE_NAME_MAXSIZE = 0x8 @@ -2163,6 +2172,7 @@ const ( NFNL_SUBSYS_QUEUE = 0x3 NFNL_SUBSYS_ULOG = 0x4 NFS_SUPER_MAGIC = 0x6969 + NFT_BITWISE_BOOL = 0x0 NFT_CHAIN_FLAGS = 0x7 NFT_CHAIN_MAXNAMELEN = 0x100 NFT_CT_MAX = 0x17 @@ -2491,6 +2501,7 @@ const ( PR_GET_PDEATHSIG = 0x2 PR_GET_SECCOMP = 0x15 PR_GET_SECUREBITS = 0x1b + PR_GET_SHADOW_STACK_STATUS = 0x4a PR_GET_SPECULATION_CTRL = 0x34 PR_GET_TAGGED_ADDR_CTRL = 0x38 PR_GET_THP_DISABLE = 0x2a @@ -2499,6 +2510,7 @@ const ( PR_GET_TIMING = 0xd PR_GET_TSC = 0x19 PR_GET_UNALIGN = 0x5 + PR_LOCK_SHADOW_STACK_STATUS = 0x4c PR_MCE_KILL = 0x21 PR_MCE_KILL_CLEAR = 0x0 PR_MCE_KILL_DEFAULT = 0x2 @@ -2525,6 +2537,8 @@ const ( PR_PAC_GET_ENABLED_KEYS = 0x3d PR_PAC_RESET_KEYS = 0x36 PR_PAC_SET_ENABLED_KEYS = 0x3c + PR_PMLEN_MASK = 0x7f000000 + PR_PMLEN_SHIFT = 0x18 PR_PPC_DEXCR_CTRL_CLEAR = 0x4 PR_PPC_DEXCR_CTRL_CLEAR_ONEXEC = 0x10 PR_PPC_DEXCR_CTRL_EDITABLE = 0x1 @@ -2592,6 +2606,7 @@ const ( PR_SET_PTRACER = 0x59616d61 PR_SET_SECCOMP = 0x16 PR_SET_SECUREBITS = 0x1c + PR_SET_SHADOW_STACK_STATUS = 0x4b PR_SET_SPECULATION_CTRL = 0x35 PR_SET_SYSCALL_USER_DISPATCH = 0x3b PR_SET_TAGGED_ADDR_CTRL = 0x37 @@ -2602,6 +2617,9 @@ const ( PR_SET_UNALIGN = 0x6 PR_SET_VMA = 0x53564d41 PR_SET_VMA_ANON_NAME = 0x0 + PR_SHADOW_STACK_ENABLE = 0x1 + PR_SHADOW_STACK_PUSH = 0x4 + PR_SHADOW_STACK_WRITE = 0x2 PR_SME_GET_VL = 0x40 PR_SME_SET_VL = 0x3f PR_SME_SET_VL_ONEXEC = 0x40000 @@ -2911,7 +2929,6 @@ const ( RTM_NEWNEXTHOP = 0x68 RTM_NEWNEXTHOPBUCKET = 0x74 RTM_NEWNSID = 0x58 - RTM_NEWNVLAN = 0x70 RTM_NEWPREFIX = 0x34 RTM_NEWQDISC = 0x24 RTM_NEWROUTE = 0x18 @@ -2920,6 +2937,7 @@ const ( RTM_NEWTCLASS = 0x28 RTM_NEWTFILTER = 0x2c RTM_NEWTUNNEL = 0x78 + RTM_NEWVLAN = 0x70 RTM_NR_FAMILIES = 0x1b RTM_NR_MSGTYPES = 0x6c RTM_SETDCB = 0x4f diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go index c0d45e32..75207613 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go @@ -116,6 +116,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 + IPV6_FLOWINFO_MASK = 0xffffff0f + IPV6_FLOWLABEL_MASK = 0xffff0f00 ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -304,6 +306,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go index c731d24f..c68acda5 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go @@ -116,6 +116,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 + IPV6_FLOWINFO_MASK = 0xffffff0f + IPV6_FLOWLABEL_MASK = 0xffff0f00 ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -305,6 +307,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go index 680018a4..a8c607ab 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 + IPV6_FLOWINFO_MASK = 0xffffff0f + IPV6_FLOWLABEL_MASK = 0xffff0f00 ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -310,6 +312,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go index a63909f3..18563dd8 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go @@ -109,6 +109,7 @@ const ( F_SETOWN = 0x8 F_UNLCK = 0x2 F_WRLCK = 0x1 + GCS_MAGIC = 0x47435300 HIDIOCGRAWINFO = 0x80084803 HIDIOCGRDESC = 0x90044802 HIDIOCGRDESCSIZE = 0x80044801 @@ -119,6 +120,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 + IPV6_FLOWINFO_MASK = 0xffffff0f + IPV6_FLOWLABEL_MASK = 0xffff0f00 ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -302,6 +305,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go index 9b0a2573..22912cda 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go @@ -116,6 +116,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 + IPV6_FLOWINFO_MASK = 0xffffff0f + IPV6_FLOWLABEL_MASK = 0xffff0f00 ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -297,6 +299,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go index 958e6e06..29344eb3 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x80 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 + IPV6_FLOWINFO_MASK = 0xfffffff + IPV6_FLOWLABEL_MASK = 0xfffff ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -303,6 +305,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go index 50c7f25b..20d51fb9 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x80 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 + IPV6_FLOWINFO_MASK = 0xfffffff + IPV6_FLOWLABEL_MASK = 0xfffff ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -303,6 +305,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go index ced21d66..321b6090 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x80 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 + IPV6_FLOWINFO_MASK = 0xffffff0f + IPV6_FLOWLABEL_MASK = 0xffff0f00 ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -303,6 +305,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go index 226c0441..9bacdf1e 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x80 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 + IPV6_FLOWINFO_MASK = 0xffffff0f + IPV6_FLOWLABEL_MASK = 0xffff0f00 ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -303,6 +305,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go index 3122737c..c2242726 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 + IPV6_FLOWINFO_MASK = 0xfffffff + IPV6_FLOWLABEL_MASK = 0xfffff ISIG = 0x80 IUCLC = 0x1000 IXOFF = 0x400 @@ -358,6 +360,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go index eb5d3467..6270c8ee 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 + IPV6_FLOWINFO_MASK = 0xfffffff + IPV6_FLOWLABEL_MASK = 0xfffff ISIG = 0x80 IUCLC = 0x1000 IXOFF = 0x400 @@ -362,6 +364,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go index e921ebc6..9966c194 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 + IPV6_FLOWINFO_MASK = 0xffffff0f + IPV6_FLOWLABEL_MASK = 0xffff0f00 ISIG = 0x80 IUCLC = 0x1000 IXOFF = 0x400 @@ -362,6 +364,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go index 38ba81c5..848e5fcc 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 + IPV6_FLOWINFO_MASK = 0xffffff0f + IPV6_FLOWLABEL_MASK = 0xffff0f00 ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -294,6 +296,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go index 71f04009..669b2adb 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go @@ -115,6 +115,8 @@ const ( IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 + IPV6_FLOWINFO_MASK = 0xfffffff + IPV6_FLOWLABEL_MASK = 0xfffff ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -366,6 +368,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x36 SCM_TIMESTAMPING_PKTINFO = 0x3a SCM_TIMESTAMPNS = 0x23 + SCM_TS_OPT_ID = 0x51 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go index c44a3133..4834e575 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go @@ -119,6 +119,8 @@ const ( IN_CLOEXEC = 0x400000 IN_NONBLOCK = 0x4000 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 + IPV6_FLOWINFO_MASK = 0xfffffff + IPV6_FLOWLABEL_MASK = 0xfffff ISIG = 0x1 IUCLC = 0x200 IXOFF = 0x1000 @@ -357,6 +359,7 @@ const ( SCM_TIMESTAMPING_OPT_STATS = 0x38 SCM_TIMESTAMPING_PKTINFO = 0x3c SCM_TIMESTAMPNS = 0x21 + SCM_TS_OPT_ID = 0x5a SCM_TXTIME = 0x3f SCM_WIFI_STATUS = 0x25 SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.go b/vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.go index 24b346e1..813c05b6 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.go @@ -2512,6 +2512,90 @@ var libc_munmap_trampoline_addr uintptr // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func readv(fd int, iovecs []Iovec) (n int, err error) { + var _p0 unsafe.Pointer + if len(iovecs) > 0 { + _p0 = unsafe.Pointer(&iovecs[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall_syscall(libc_readv_trampoline_addr, uintptr(fd), uintptr(_p0), uintptr(len(iovecs))) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_readv_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_readv readv "/usr/lib/libSystem.B.dylib" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func preadv(fd int, iovecs []Iovec, offset int64) (n int, err error) { + var _p0 unsafe.Pointer + if len(iovecs) > 0 { + _p0 = unsafe.Pointer(&iovecs[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall_syscall6(libc_preadv_trampoline_addr, uintptr(fd), uintptr(_p0), uintptr(len(iovecs)), uintptr(offset), 0, 0) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_preadv_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_preadv preadv "/usr/lib/libSystem.B.dylib" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func writev(fd int, iovecs []Iovec) (n int, err error) { + var _p0 unsafe.Pointer + if len(iovecs) > 0 { + _p0 = unsafe.Pointer(&iovecs[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall_syscall(libc_writev_trampoline_addr, uintptr(fd), uintptr(_p0), uintptr(len(iovecs))) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_writev_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_writev writev "/usr/lib/libSystem.B.dylib" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func pwritev(fd int, iovecs []Iovec, offset int64) (n int, err error) { + var _p0 unsafe.Pointer + if len(iovecs) > 0 { + _p0 = unsafe.Pointer(&iovecs[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall_syscall6(libc_pwritev_trampoline_addr, uintptr(fd), uintptr(_p0), uintptr(len(iovecs)), uintptr(offset), 0, 0) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_pwritev_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_pwritev pwritev "/usr/lib/libSystem.B.dylib" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func Fstat(fd int, stat *Stat_t) (err error) { _, _, e1 := syscall_syscall(libc_fstat64_trampoline_addr, uintptr(fd), uintptr(unsafe.Pointer(stat)), 0) if e1 != 0 { diff --git a/vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.s b/vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.s index ebd21310..fda32858 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.s +++ b/vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.s @@ -738,6 +738,26 @@ TEXT libc_munmap_trampoline<>(SB),NOSPLIT,$0-0 GLOBL ·libc_munmap_trampoline_addr(SB), RODATA, $8 DATA ·libc_munmap_trampoline_addr(SB)/8, $libc_munmap_trampoline<>(SB) +TEXT libc_readv_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_readv(SB) +GLOBL ·libc_readv_trampoline_addr(SB), RODATA, $8 +DATA ·libc_readv_trampoline_addr(SB)/8, $libc_readv_trampoline<>(SB) + +TEXT libc_preadv_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_preadv(SB) +GLOBL ·libc_preadv_trampoline_addr(SB), RODATA, $8 +DATA ·libc_preadv_trampoline_addr(SB)/8, $libc_preadv_trampoline<>(SB) + +TEXT libc_writev_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_writev(SB) +GLOBL ·libc_writev_trampoline_addr(SB), RODATA, $8 +DATA ·libc_writev_trampoline_addr(SB)/8, $libc_writev_trampoline<>(SB) + +TEXT libc_pwritev_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_pwritev(SB) +GLOBL ·libc_pwritev_trampoline_addr(SB), RODATA, $8 +DATA ·libc_pwritev_trampoline_addr(SB)/8, $libc_pwritev_trampoline<>(SB) + TEXT libc_fstat64_trampoline<>(SB),NOSPLIT,$0-0 JMP libc_fstat64(SB) GLOBL ·libc_fstat64_trampoline_addr(SB), RODATA, $8 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.go b/vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.go index 824b9c2d..e6f58f3c 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.go @@ -2512,6 +2512,90 @@ var libc_munmap_trampoline_addr uintptr // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func readv(fd int, iovecs []Iovec) (n int, err error) { + var _p0 unsafe.Pointer + if len(iovecs) > 0 { + _p0 = unsafe.Pointer(&iovecs[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall_syscall(libc_readv_trampoline_addr, uintptr(fd), uintptr(_p0), uintptr(len(iovecs))) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_readv_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_readv readv "/usr/lib/libSystem.B.dylib" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func preadv(fd int, iovecs []Iovec, offset int64) (n int, err error) { + var _p0 unsafe.Pointer + if len(iovecs) > 0 { + _p0 = unsafe.Pointer(&iovecs[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall_syscall6(libc_preadv_trampoline_addr, uintptr(fd), uintptr(_p0), uintptr(len(iovecs)), uintptr(offset), 0, 0) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_preadv_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_preadv preadv "/usr/lib/libSystem.B.dylib" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func writev(fd int, iovecs []Iovec) (n int, err error) { + var _p0 unsafe.Pointer + if len(iovecs) > 0 { + _p0 = unsafe.Pointer(&iovecs[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall_syscall(libc_writev_trampoline_addr, uintptr(fd), uintptr(_p0), uintptr(len(iovecs))) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_writev_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_writev writev "/usr/lib/libSystem.B.dylib" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func pwritev(fd int, iovecs []Iovec, offset int64) (n int, err error) { + var _p0 unsafe.Pointer + if len(iovecs) > 0 { + _p0 = unsafe.Pointer(&iovecs[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + r0, _, e1 := syscall_syscall6(libc_pwritev_trampoline_addr, uintptr(fd), uintptr(_p0), uintptr(len(iovecs)), uintptr(offset), 0, 0) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_pwritev_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_pwritev pwritev "/usr/lib/libSystem.B.dylib" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func Fstat(fd int, stat *Stat_t) (err error) { _, _, e1 := syscall_syscall(libc_fstat_trampoline_addr, uintptr(fd), uintptr(unsafe.Pointer(stat)), 0) if e1 != 0 { diff --git a/vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.s b/vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.s index 4f178a22..7f8998b9 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.s +++ b/vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.s @@ -738,6 +738,26 @@ TEXT libc_munmap_trampoline<>(SB),NOSPLIT,$0-0 GLOBL ·libc_munmap_trampoline_addr(SB), RODATA, $8 DATA ·libc_munmap_trampoline_addr(SB)/8, $libc_munmap_trampoline<>(SB) +TEXT libc_readv_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_readv(SB) +GLOBL ·libc_readv_trampoline_addr(SB), RODATA, $8 +DATA ·libc_readv_trampoline_addr(SB)/8, $libc_readv_trampoline<>(SB) + +TEXT libc_preadv_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_preadv(SB) +GLOBL ·libc_preadv_trampoline_addr(SB), RODATA, $8 +DATA ·libc_preadv_trampoline_addr(SB)/8, $libc_preadv_trampoline<>(SB) + +TEXT libc_writev_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_writev(SB) +GLOBL ·libc_writev_trampoline_addr(SB), RODATA, $8 +DATA ·libc_writev_trampoline_addr(SB)/8, $libc_writev_trampoline<>(SB) + +TEXT libc_pwritev_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_pwritev(SB) +GLOBL ·libc_pwritev_trampoline_addr(SB), RODATA, $8 +DATA ·libc_pwritev_trampoline_addr(SB)/8, $libc_pwritev_trampoline<>(SB) + TEXT libc_fstat_trampoline<>(SB),NOSPLIT,$0-0 JMP libc_fstat(SB) GLOBL ·libc_fstat_trampoline_addr(SB), RODATA, $8 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_solaris_amd64.go b/vendor/golang.org/x/sys/unix/zsyscall_solaris_amd64.go index 829b87fe..c6545413 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_solaris_amd64.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_solaris_amd64.go @@ -141,6 +141,16 @@ import ( //go:cgo_import_dynamic libc_getpeername getpeername "libsocket.so" //go:cgo_import_dynamic libc_setsockopt setsockopt "libsocket.so" //go:cgo_import_dynamic libc_recvfrom recvfrom "libsocket.so" +//go:cgo_import_dynamic libc_getpeerucred getpeerucred "libc.so" +//go:cgo_import_dynamic libc_ucred_get ucred_get "libc.so" +//go:cgo_import_dynamic libc_ucred_geteuid ucred_geteuid "libc.so" +//go:cgo_import_dynamic libc_ucred_getegid ucred_getegid "libc.so" +//go:cgo_import_dynamic libc_ucred_getruid ucred_getruid "libc.so" +//go:cgo_import_dynamic libc_ucred_getrgid ucred_getrgid "libc.so" +//go:cgo_import_dynamic libc_ucred_getsuid ucred_getsuid "libc.so" +//go:cgo_import_dynamic libc_ucred_getsgid ucred_getsgid "libc.so" +//go:cgo_import_dynamic libc_ucred_getpid ucred_getpid "libc.so" +//go:cgo_import_dynamic libc_ucred_free ucred_free "libc.so" //go:cgo_import_dynamic libc_port_create port_create "libc.so" //go:cgo_import_dynamic libc_port_associate port_associate "libc.so" //go:cgo_import_dynamic libc_port_dissociate port_dissociate "libc.so" @@ -280,6 +290,16 @@ import ( //go:linkname procgetpeername libc_getpeername //go:linkname procsetsockopt libc_setsockopt //go:linkname procrecvfrom libc_recvfrom +//go:linkname procgetpeerucred libc_getpeerucred +//go:linkname procucred_get libc_ucred_get +//go:linkname procucred_geteuid libc_ucred_geteuid +//go:linkname procucred_getegid libc_ucred_getegid +//go:linkname procucred_getruid libc_ucred_getruid +//go:linkname procucred_getrgid libc_ucred_getrgid +//go:linkname procucred_getsuid libc_ucred_getsuid +//go:linkname procucred_getsgid libc_ucred_getsgid +//go:linkname procucred_getpid libc_ucred_getpid +//go:linkname procucred_free libc_ucred_free //go:linkname procport_create libc_port_create //go:linkname procport_associate libc_port_associate //go:linkname procport_dissociate libc_port_dissociate @@ -420,6 +440,16 @@ var ( procgetpeername, procsetsockopt, procrecvfrom, + procgetpeerucred, + procucred_get, + procucred_geteuid, + procucred_getegid, + procucred_getruid, + procucred_getrgid, + procucred_getsuid, + procucred_getsgid, + procucred_getpid, + procucred_free, procport_create, procport_associate, procport_dissociate, @@ -2029,6 +2059,90 @@ func recvfrom(fd int, p []byte, flags int, from *RawSockaddrAny, fromlen *_Sockl // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func getpeerucred(fd uintptr, ucred *uintptr) (err error) { + _, _, e1 := sysvicall6(uintptr(unsafe.Pointer(&procgetpeerucred)), 2, uintptr(fd), uintptr(unsafe.Pointer(ucred)), 0, 0, 0, 0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func ucredGet(pid int) (ucred uintptr, err error) { + r0, _, e1 := sysvicall6(uintptr(unsafe.Pointer(&procucred_get)), 1, uintptr(pid), 0, 0, 0, 0, 0) + ucred = uintptr(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func ucredGeteuid(ucred uintptr) (uid int) { + r0, _, _ := sysvicall6(uintptr(unsafe.Pointer(&procucred_geteuid)), 1, uintptr(ucred), 0, 0, 0, 0, 0) + uid = int(r0) + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func ucredGetegid(ucred uintptr) (gid int) { + r0, _, _ := sysvicall6(uintptr(unsafe.Pointer(&procucred_getegid)), 1, uintptr(ucred), 0, 0, 0, 0, 0) + gid = int(r0) + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func ucredGetruid(ucred uintptr) (uid int) { + r0, _, _ := sysvicall6(uintptr(unsafe.Pointer(&procucred_getruid)), 1, uintptr(ucred), 0, 0, 0, 0, 0) + uid = int(r0) + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func ucredGetrgid(ucred uintptr) (gid int) { + r0, _, _ := sysvicall6(uintptr(unsafe.Pointer(&procucred_getrgid)), 1, uintptr(ucred), 0, 0, 0, 0, 0) + gid = int(r0) + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func ucredGetsuid(ucred uintptr) (uid int) { + r0, _, _ := sysvicall6(uintptr(unsafe.Pointer(&procucred_getsuid)), 1, uintptr(ucred), 0, 0, 0, 0, 0) + uid = int(r0) + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func ucredGetsgid(ucred uintptr) (gid int) { + r0, _, _ := sysvicall6(uintptr(unsafe.Pointer(&procucred_getsgid)), 1, uintptr(ucred), 0, 0, 0, 0, 0) + gid = int(r0) + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func ucredGetpid(ucred uintptr) (pid int) { + r0, _, _ := sysvicall6(uintptr(unsafe.Pointer(&procucred_getpid)), 1, uintptr(ucred), 0, 0, 0, 0, 0) + pid = int(r0) + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func ucredFree(ucred uintptr) { + sysvicall6(uintptr(unsafe.Pointer(&procucred_free)), 1, uintptr(ucred), 0, 0, 0, 0, 0) + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func port_create() (n int, err error) { r0, _, e1 := sysvicall6(uintptr(unsafe.Pointer(&procport_create)), 0, 0, 0, 0, 0, 0, 0) n = int(r0) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go index 524b0820..c79aaff3 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go @@ -458,4 +458,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go index f485dbf4..5eb45069 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go @@ -381,4 +381,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go index 70b35bf3..05e50297 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go @@ -422,4 +422,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go index 1893e2fe..38c53ec5 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go @@ -325,4 +325,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go index 16a4017d..31d2e71a 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go @@ -321,4 +321,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go index 7e567f1e..f4184a33 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go @@ -442,4 +442,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 4460 SYS_LSM_LIST_MODULES = 4461 SYS_MSEAL = 4462 + SYS_SETXATTRAT = 4463 + SYS_GETXATTRAT = 4464 + SYS_LISTXATTRAT = 4465 + SYS_REMOVEXATTRAT = 4466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go index 38ae55e5..05b99622 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go @@ -372,4 +372,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 5460 SYS_LSM_LIST_MODULES = 5461 SYS_MSEAL = 5462 + SYS_SETXATTRAT = 5463 + SYS_GETXATTRAT = 5464 + SYS_LISTXATTRAT = 5465 + SYS_REMOVEXATTRAT = 5466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go index 55e92e60..43a256e9 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go @@ -372,4 +372,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 5460 SYS_LSM_LIST_MODULES = 5461 SYS_MSEAL = 5462 + SYS_SETXATTRAT = 5463 + SYS_GETXATTRAT = 5464 + SYS_LISTXATTRAT = 5465 + SYS_REMOVEXATTRAT = 5466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go index 60658d6a..eea5ddfc 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go @@ -442,4 +442,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 4460 SYS_LSM_LIST_MODULES = 4461 SYS_MSEAL = 4462 + SYS_SETXATTRAT = 4463 + SYS_GETXATTRAT = 4464 + SYS_LISTXATTRAT = 4465 + SYS_REMOVEXATTRAT = 4466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go index e203e8a7..0d777bfb 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go @@ -449,4 +449,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go index 5944b97d..b4463650 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go @@ -421,4 +421,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go index c66d416d..0c7d21c1 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go @@ -421,4 +421,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go index a5459e76..84053916 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go @@ -326,4 +326,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go index 01d86825..fcf1b790 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go @@ -387,4 +387,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go index 7b703e77..52d15b5f 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go @@ -400,4 +400,8 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index 5537148d..a46abe64 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -4747,7 +4747,7 @@ const ( NL80211_ATTR_MAC_HINT = 0xc8 NL80211_ATTR_MAC_MASK = 0xd7 NL80211_ATTR_MAX_AP_ASSOC_STA = 0xca - NL80211_ATTR_MAX = 0x14c + NL80211_ATTR_MAX = 0x14d NL80211_ATTR_MAX_CRIT_PROT_DURATION = 0xb4 NL80211_ATTR_MAX_CSA_COUNTERS = 0xce NL80211_ATTR_MAX_MATCH_SETS = 0x85 @@ -5519,7 +5519,7 @@ const ( NL80211_MNTR_FLAG_CONTROL = 0x3 NL80211_MNTR_FLAG_COOK_FRAMES = 0x5 NL80211_MNTR_FLAG_FCSFAIL = 0x1 - NL80211_MNTR_FLAG_MAX = 0x6 + NL80211_MNTR_FLAG_MAX = 0x7 NL80211_MNTR_FLAG_OTHER_BSS = 0x4 NL80211_MNTR_FLAG_PLCPFAIL = 0x2 NL80211_MPATH_FLAG_ACTIVE = 0x1 @@ -6174,3 +6174,5 @@ type SockDiagReq struct { Family uint8 Protocol uint8 } + +const RTM_NEWNVLAN = 0x70 diff --git a/vendor/golang.org/x/sys/windows/security_windows.go b/vendor/golang.org/x/sys/windows/security_windows.go index b6e1ab76..a8b0364c 100644 --- a/vendor/golang.org/x/sys/windows/security_windows.go +++ b/vendor/golang.org/x/sys/windows/security_windows.go @@ -1303,7 +1303,10 @@ func (selfRelativeSD *SECURITY_DESCRIPTOR) ToAbsolute() (absoluteSD *SECURITY_DE return nil, err } if absoluteSDSize > 0 { - absoluteSD = (*SECURITY_DESCRIPTOR)(unsafe.Pointer(&make([]byte, absoluteSDSize)[0])) + absoluteSD = new(SECURITY_DESCRIPTOR) + if unsafe.Sizeof(*absoluteSD) < uintptr(absoluteSDSize) { + panic("sizeof(SECURITY_DESCRIPTOR) too small") + } } var ( dacl *ACL @@ -1312,19 +1315,55 @@ func (selfRelativeSD *SECURITY_DESCRIPTOR) ToAbsolute() (absoluteSD *SECURITY_DE group *SID ) if daclSize > 0 { - dacl = (*ACL)(unsafe.Pointer(&make([]byte, daclSize)[0])) + dacl = (*ACL)(unsafe.Pointer(unsafe.SliceData(make([]byte, daclSize)))) } if saclSize > 0 { - sacl = (*ACL)(unsafe.Pointer(&make([]byte, saclSize)[0])) + sacl = (*ACL)(unsafe.Pointer(unsafe.SliceData(make([]byte, saclSize)))) } if ownerSize > 0 { - owner = (*SID)(unsafe.Pointer(&make([]byte, ownerSize)[0])) + owner = (*SID)(unsafe.Pointer(unsafe.SliceData(make([]byte, ownerSize)))) } if groupSize > 0 { - group = (*SID)(unsafe.Pointer(&make([]byte, groupSize)[0])) + group = (*SID)(unsafe.Pointer(unsafe.SliceData(make([]byte, groupSize)))) } + // We call into Windows via makeAbsoluteSD, which sets up + // pointers within absoluteSD that point to other chunks of memory + // we pass into makeAbsoluteSD, and that happens outside the view of the GC. + // We therefore take some care here to then verify the pointers are as we expect + // and set them explicitly in view of the GC. See https://go.dev/issue/73199. + // TODO: consider weak pointers once Go 1.24 is appropriate. See suggestion in https://go.dev/cl/663575. err = makeAbsoluteSD(selfRelativeSD, absoluteSD, &absoluteSDSize, dacl, &daclSize, sacl, &saclSize, owner, &ownerSize, group, &groupSize) + if err != nil { + // Don't return absoluteSD, which might be partially initialized. + return nil, err + } + // Before using any fields, verify absoluteSD is in the format we expect according to Windows. + // See https://learn.microsoft.com/en-us/windows/win32/secauthz/absolute-and-self-relative-security-descriptors + absControl, _, err := absoluteSD.Control() + if err != nil { + panic("absoluteSD: " + err.Error()) + } + if absControl&SE_SELF_RELATIVE != 0 { + panic("absoluteSD not in absolute format") + } + if absoluteSD.dacl != dacl { + panic("dacl pointer mismatch") + } + if absoluteSD.sacl != sacl { + panic("sacl pointer mismatch") + } + if absoluteSD.owner != owner { + panic("owner pointer mismatch") + } + if absoluteSD.group != group { + panic("group pointer mismatch") + } + absoluteSD.dacl = dacl + absoluteSD.sacl = sacl + absoluteSD.owner = owner + absoluteSD.group = group + return } diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index 4a325438..640f6b15 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -870,6 +870,7 @@ const socket_error = uintptr(^uint32(0)) //sys WSARecvFrom(s Handle, bufs *WSABuf, bufcnt uint32, recvd *uint32, flags *uint32, from *RawSockaddrAny, fromlen *int32, overlapped *Overlapped, croutine *byte) (err error) [failretval==socket_error] = ws2_32.WSARecvFrom //sys WSASendTo(s Handle, bufs *WSABuf, bufcnt uint32, sent *uint32, flags uint32, to *RawSockaddrAny, tolen int32, overlapped *Overlapped, croutine *byte) (err error) [failretval==socket_error] = ws2_32.WSASendTo //sys WSASocket(af int32, typ int32, protocol int32, protoInfo *WSAProtocolInfo, group uint32, flags uint32) (handle Handle, err error) [failretval==InvalidHandle] = ws2_32.WSASocketW +//sys WSADuplicateSocket(s Handle, processID uint32, info *WSAProtocolInfo) (err error) [failretval!=0] = ws2_32.WSADuplicateSocketW //sys GetHostByName(name string) (h *Hostent, err error) [failretval==nil] = ws2_32.gethostbyname //sys GetServByName(name string, proto string) (s *Servent, err error) [failretval==nil] = ws2_32.getservbyname //sys Ntohs(netshort uint16) (u uint16) = ws2_32.ntohs @@ -1698,8 +1699,9 @@ func NewNTUnicodeString(s string) (*NTUnicodeString, error) { // Slice returns a uint16 slice that aliases the data in the NTUnicodeString. func (s *NTUnicodeString) Slice() []uint16 { - slice := unsafe.Slice(s.Buffer, s.MaximumLength) - return slice[:s.Length] + // Note: this rounds the length down, if it happens + // to (incorrectly) be odd. Probably safer than rounding up. + return unsafe.Slice(s.Buffer, s.MaximumLength/2)[:s.Length/2] } func (s *NTUnicodeString) String() string { diff --git a/vendor/golang.org/x/sys/windows/types_windows.go b/vendor/golang.org/x/sys/windows/types_windows.go index 9d138de5..958bcf47 100644 --- a/vendor/golang.org/x/sys/windows/types_windows.go +++ b/vendor/golang.org/x/sys/windows/types_windows.go @@ -1074,6 +1074,7 @@ const ( IP_ADD_MEMBERSHIP = 0xc IP_DROP_MEMBERSHIP = 0xd IP_PKTINFO = 0x13 + IP_MTU_DISCOVER = 0x47 IPV6_V6ONLY = 0x1b IPV6_UNICAST_HOPS = 0x4 @@ -1083,6 +1084,7 @@ const ( IPV6_JOIN_GROUP = 0xc IPV6_LEAVE_GROUP = 0xd IPV6_PKTINFO = 0x13 + IPV6_MTU_DISCOVER = 0x47 MSG_OOB = 0x1 MSG_PEEK = 0x2 @@ -1132,6 +1134,15 @@ const ( WSASYS_STATUS_LEN = 128 ) +// enum PMTUD_STATE from ws2ipdef.h +const ( + IP_PMTUDISC_NOT_SET = 0 + IP_PMTUDISC_DO = 1 + IP_PMTUDISC_DONT = 2 + IP_PMTUDISC_PROBE = 3 + IP_PMTUDISC_MAX = 4 +) + type WSABuf struct { Len uint32 Buf *byte @@ -1146,6 +1157,22 @@ type WSAMsg struct { Flags uint32 } +type WSACMSGHDR struct { + Len uintptr + Level int32 + Type int32 +} + +type IN_PKTINFO struct { + Addr [4]byte + Ifindex uint32 +} + +type IN6_PKTINFO struct { + Addr [16]byte + Ifindex uint32 +} + // Flags for WSASocket const ( WSA_FLAG_OVERLAPPED = 0x01 @@ -2673,6 +2700,8 @@ type CommTimeouts struct { // NTUnicodeString is a UTF-16 string for NT native APIs, corresponding to UNICODE_STRING. type NTUnicodeString struct { + // Note: Length and MaximumLength are in *bytes*, not uint16s. + // They should always be even. Length uint16 MaximumLength uint16 Buffer *uint16 @@ -3601,3 +3630,213 @@ const ( KLF_NOTELLSHELL = 0x00000080 KLF_SETFORPROCESS = 0x00000100 ) + +// Virtual Key codes +// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes +const ( + VK_LBUTTON = 0x01 + VK_RBUTTON = 0x02 + VK_CANCEL = 0x03 + VK_MBUTTON = 0x04 + VK_XBUTTON1 = 0x05 + VK_XBUTTON2 = 0x06 + VK_BACK = 0x08 + VK_TAB = 0x09 + VK_CLEAR = 0x0C + VK_RETURN = 0x0D + VK_SHIFT = 0x10 + VK_CONTROL = 0x11 + VK_MENU = 0x12 + VK_PAUSE = 0x13 + VK_CAPITAL = 0x14 + VK_KANA = 0x15 + VK_HANGEUL = 0x15 + VK_HANGUL = 0x15 + VK_IME_ON = 0x16 + VK_JUNJA = 0x17 + VK_FINAL = 0x18 + VK_HANJA = 0x19 + VK_KANJI = 0x19 + VK_IME_OFF = 0x1A + VK_ESCAPE = 0x1B + VK_CONVERT = 0x1C + VK_NONCONVERT = 0x1D + VK_ACCEPT = 0x1E + VK_MODECHANGE = 0x1F + VK_SPACE = 0x20 + VK_PRIOR = 0x21 + VK_NEXT = 0x22 + VK_END = 0x23 + VK_HOME = 0x24 + VK_LEFT = 0x25 + VK_UP = 0x26 + VK_RIGHT = 0x27 + VK_DOWN = 0x28 + VK_SELECT = 0x29 + VK_PRINT = 0x2A + VK_EXECUTE = 0x2B + VK_SNAPSHOT = 0x2C + VK_INSERT = 0x2D + VK_DELETE = 0x2E + VK_HELP = 0x2F + VK_LWIN = 0x5B + VK_RWIN = 0x5C + VK_APPS = 0x5D + VK_SLEEP = 0x5F + VK_NUMPAD0 = 0x60 + VK_NUMPAD1 = 0x61 + VK_NUMPAD2 = 0x62 + VK_NUMPAD3 = 0x63 + VK_NUMPAD4 = 0x64 + VK_NUMPAD5 = 0x65 + VK_NUMPAD6 = 0x66 + VK_NUMPAD7 = 0x67 + VK_NUMPAD8 = 0x68 + VK_NUMPAD9 = 0x69 + VK_MULTIPLY = 0x6A + VK_ADD = 0x6B + VK_SEPARATOR = 0x6C + VK_SUBTRACT = 0x6D + VK_DECIMAL = 0x6E + VK_DIVIDE = 0x6F + VK_F1 = 0x70 + VK_F2 = 0x71 + VK_F3 = 0x72 + VK_F4 = 0x73 + VK_F5 = 0x74 + VK_F6 = 0x75 + VK_F7 = 0x76 + VK_F8 = 0x77 + VK_F9 = 0x78 + VK_F10 = 0x79 + VK_F11 = 0x7A + VK_F12 = 0x7B + VK_F13 = 0x7C + VK_F14 = 0x7D + VK_F15 = 0x7E + VK_F16 = 0x7F + VK_F17 = 0x80 + VK_F18 = 0x81 + VK_F19 = 0x82 + VK_F20 = 0x83 + VK_F21 = 0x84 + VK_F22 = 0x85 + VK_F23 = 0x86 + VK_F24 = 0x87 + VK_NUMLOCK = 0x90 + VK_SCROLL = 0x91 + VK_OEM_NEC_EQUAL = 0x92 + VK_OEM_FJ_JISHO = 0x92 + VK_OEM_FJ_MASSHOU = 0x93 + VK_OEM_FJ_TOUROKU = 0x94 + VK_OEM_FJ_LOYA = 0x95 + VK_OEM_FJ_ROYA = 0x96 + VK_LSHIFT = 0xA0 + VK_RSHIFT = 0xA1 + VK_LCONTROL = 0xA2 + VK_RCONTROL = 0xA3 + VK_LMENU = 0xA4 + VK_RMENU = 0xA5 + VK_BROWSER_BACK = 0xA6 + VK_BROWSER_FORWARD = 0xA7 + VK_BROWSER_REFRESH = 0xA8 + VK_BROWSER_STOP = 0xA9 + VK_BROWSER_SEARCH = 0xAA + VK_BROWSER_FAVORITES = 0xAB + VK_BROWSER_HOME = 0xAC + VK_VOLUME_MUTE = 0xAD + VK_VOLUME_DOWN = 0xAE + VK_VOLUME_UP = 0xAF + VK_MEDIA_NEXT_TRACK = 0xB0 + VK_MEDIA_PREV_TRACK = 0xB1 + VK_MEDIA_STOP = 0xB2 + VK_MEDIA_PLAY_PAUSE = 0xB3 + VK_LAUNCH_MAIL = 0xB4 + VK_LAUNCH_MEDIA_SELECT = 0xB5 + VK_LAUNCH_APP1 = 0xB6 + VK_LAUNCH_APP2 = 0xB7 + VK_OEM_1 = 0xBA + VK_OEM_PLUS = 0xBB + VK_OEM_COMMA = 0xBC + VK_OEM_MINUS = 0xBD + VK_OEM_PERIOD = 0xBE + VK_OEM_2 = 0xBF + VK_OEM_3 = 0xC0 + VK_OEM_4 = 0xDB + VK_OEM_5 = 0xDC + VK_OEM_6 = 0xDD + VK_OEM_7 = 0xDE + VK_OEM_8 = 0xDF + VK_OEM_AX = 0xE1 + VK_OEM_102 = 0xE2 + VK_ICO_HELP = 0xE3 + VK_ICO_00 = 0xE4 + VK_PROCESSKEY = 0xE5 + VK_ICO_CLEAR = 0xE6 + VK_OEM_RESET = 0xE9 + VK_OEM_JUMP = 0xEA + VK_OEM_PA1 = 0xEB + VK_OEM_PA2 = 0xEC + VK_OEM_PA3 = 0xED + VK_OEM_WSCTRL = 0xEE + VK_OEM_CUSEL = 0xEF + VK_OEM_ATTN = 0xF0 + VK_OEM_FINISH = 0xF1 + VK_OEM_COPY = 0xF2 + VK_OEM_AUTO = 0xF3 + VK_OEM_ENLW = 0xF4 + VK_OEM_BACKTAB = 0xF5 + VK_ATTN = 0xF6 + VK_CRSEL = 0xF7 + VK_EXSEL = 0xF8 + VK_EREOF = 0xF9 + VK_PLAY = 0xFA + VK_ZOOM = 0xFB + VK_NONAME = 0xFC + VK_PA1 = 0xFD + VK_OEM_CLEAR = 0xFE +) + +// Mouse button constants. +// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str +const ( + FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001 + RIGHTMOST_BUTTON_PRESSED = 0x0002 + FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004 + FROM_LEFT_3RD_BUTTON_PRESSED = 0x0008 + FROM_LEFT_4TH_BUTTON_PRESSED = 0x0010 +) + +// Control key state constaints. +// https://docs.microsoft.com/en-us/windows/console/key-event-record-str +// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str +const ( + CAPSLOCK_ON = 0x0080 + ENHANCED_KEY = 0x0100 + LEFT_ALT_PRESSED = 0x0002 + LEFT_CTRL_PRESSED = 0x0008 + NUMLOCK_ON = 0x0020 + RIGHT_ALT_PRESSED = 0x0001 + RIGHT_CTRL_PRESSED = 0x0004 + SCROLLLOCK_ON = 0x0040 + SHIFT_PRESSED = 0x0010 +) + +// Mouse event record event flags. +// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str +const ( + MOUSE_MOVED = 0x0001 + DOUBLE_CLICK = 0x0002 + MOUSE_WHEELED = 0x0004 + MOUSE_HWHEELED = 0x0008 +) + +// Input Record Event Types +// https://learn.microsoft.com/en-us/windows/console/input-record-str +const ( + FOCUS_EVENT = 0x0010 + KEY_EVENT = 0x0001 + MENU_EVENT = 0x0008 + MOUSE_EVENT = 0x0002 + WINDOW_BUFFER_SIZE_EVENT = 0x0004 +) diff --git a/vendor/golang.org/x/sys/windows/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/zsyscall_windows.go index 01c0716c..a58bc48b 100644 --- a/vendor/golang.org/x/sys/windows/zsyscall_windows.go +++ b/vendor/golang.org/x/sys/windows/zsyscall_windows.go @@ -511,6 +511,7 @@ var ( procFreeAddrInfoW = modws2_32.NewProc("FreeAddrInfoW") procGetAddrInfoW = modws2_32.NewProc("GetAddrInfoW") procWSACleanup = modws2_32.NewProc("WSACleanup") + procWSADuplicateSocketW = modws2_32.NewProc("WSADuplicateSocketW") procWSAEnumProtocolsW = modws2_32.NewProc("WSAEnumProtocolsW") procWSAGetOverlappedResult = modws2_32.NewProc("WSAGetOverlappedResult") procWSAIoctl = modws2_32.NewProc("WSAIoctl") @@ -4391,6 +4392,14 @@ func WSACleanup() (err error) { return } +func WSADuplicateSocket(s Handle, processID uint32, info *WSAProtocolInfo) (err error) { + r1, _, e1 := syscall.Syscall(procWSADuplicateSocketW.Addr(), 3, uintptr(s), uintptr(processID), uintptr(unsafe.Pointer(info))) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + func WSAEnumProtocols(protocols *int32, protocolBuffer *WSAProtocolInfo, bufferLength *uint32) (n int32, err error) { r0, _, e1 := syscall.Syscall(procWSAEnumProtocolsW.Addr(), 3, uintptr(unsafe.Pointer(protocols)), uintptr(unsafe.Pointer(protocolBuffer)), uintptr(unsafe.Pointer(bufferLength))) n = int32(r0) diff --git a/vendor/golang.org/x/term/terminal.go b/vendor/golang.org/x/term/terminal.go index f636667f..13e9a64a 100644 --- a/vendor/golang.org/x/term/terminal.go +++ b/vendor/golang.org/x/term/terminal.go @@ -6,6 +6,7 @@ package term import ( "bytes" + "fmt" "io" "runtime" "strconv" @@ -36,6 +37,26 @@ var vt100EscapeCodes = EscapeCodes{ Reset: []byte{keyEscape, '[', '0', 'm'}, } +// A History provides a (possibly bounded) queue of input lines read by [Terminal.ReadLine]. +type History interface { + // Add will be called by [Terminal.ReadLine] to add + // a new, most recent entry to the history. + // It is allowed to drop any entry, including + // the entry being added (e.g., if it's deemed an invalid entry), + // the least-recent entry (e.g., to keep the history bounded), + // or any other entry. + Add(entry string) + + // Len returns the number of entries in the history. + Len() int + + // At returns an entry from the history. + // Index 0 is the most-recently added entry and + // index Len()-1 is the least-recently added entry. + // If index is < 0 or >= Len(), it panics. + At(idx int) string +} + // Terminal contains the state for running a VT100 terminal that is capable of // reading lines of input. type Terminal struct { @@ -44,6 +65,8 @@ type Terminal struct { // bytes, as an index into |line|). If it returns ok=false, the key // press is processed normally. Otherwise it returns a replacement line // and the new cursor position. + // + // This will be disabled during ReadPassword. AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) // Escape contains a pointer to the escape codes for this terminal. @@ -84,9 +107,14 @@ type Terminal struct { remainder []byte inBuf [256]byte - // history contains previously entered commands so that they can be - // accessed with the up and down keys. - history stRingBuffer + // History records and retrieves lines of input read by [ReadLine] which + // a user can retrieve and navigate using the up and down arrow keys. + // + // It is not safe to call ReadLine concurrently with any methods on History. + // + // [NewTerminal] sets this to a default implementation that records the + // last 100 lines of input. + History History // historyIndex stores the currently accessed history entry, where zero // means the immediately previous entry. historyIndex int @@ -109,6 +137,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal { termHeight: 24, echo: true, historyIndex: -1, + History: &stRingBuffer{}, } } @@ -448,6 +477,23 @@ func visualLength(runes []rune) int { return length } +// histroryAt unlocks the terminal and relocks it while calling History.At. +func (t *Terminal) historyAt(idx int) (string, bool) { + t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer. + defer t.lock.Lock() // panic in At (or Len) protection. + if idx < 0 || idx >= t.History.Len() { + return "", false + } + return t.History.At(idx), true +} + +// historyAdd unlocks the terminal and relocks it while calling History.Add. +func (t *Terminal) historyAdd(entry string) { + t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer. + defer t.lock.Lock() // panic in Add protection. + t.History.Add(entry) +} + // handleKey processes the given key and, optionally, returns a line of text // that the user has entered. func (t *Terminal) handleKey(key rune) (line string, ok bool) { @@ -495,7 +541,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { t.pos = len(t.line) t.moveCursorToPos(t.pos) case keyUp: - entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) + entry, ok := t.historyAt(t.historyIndex + 1) if !ok { return "", false } @@ -514,7 +560,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { t.setLine(runes, len(runes)) t.historyIndex-- default: - entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) + entry, ok := t.historyAt(t.historyIndex - 1) if ok { t.historyIndex-- runes := []rune(entry) @@ -692,6 +738,8 @@ func (t *Terminal) Write(buf []byte) (n int, err error) { // ReadPassword temporarily changes the prompt and reads a password, without // echo, from the terminal. +// +// The AutoCompleteCallback is disabled during this call. func (t *Terminal) ReadPassword(prompt string) (line string, err error) { t.lock.Lock() defer t.lock.Unlock() @@ -699,6 +747,11 @@ func (t *Terminal) ReadPassword(prompt string) (line string, err error) { oldPrompt := t.prompt t.prompt = []rune(prompt) t.echo = false + oldAutoCompleteCallback := t.AutoCompleteCallback + t.AutoCompleteCallback = nil + defer func() { + t.AutoCompleteCallback = oldAutoCompleteCallback + }() line, err = t.readLine() @@ -772,7 +825,7 @@ func (t *Terminal) readLine() (line string, err error) { if lineOk { if t.echo { t.historyIndex = -1 - t.history.Add(line) + t.historyAdd(line) } if lineIsPasted { err = ErrPasteIndicator @@ -929,19 +982,23 @@ func (s *stRingBuffer) Add(a string) { } } -// NthPreviousEntry returns the value passed to the nth previous call to Add. +func (s *stRingBuffer) Len() int { + return s.size +} + +// At returns the value passed to the nth previous call to Add. // If n is zero then the immediately prior value is returned, if one, then the // next most recent, and so on. If such an element doesn't exist then ok is // false. -func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { +func (s *stRingBuffer) At(n int) string { if n < 0 || n >= s.size { - return "", false + panic(fmt.Sprintf("term: history index [%d] out of range [0,%d)", n, s.size)) } index := s.head - n if index < 0 { index += s.max } - return s.entries[index], true + return s.entries[index] } // readPasswordLine reads from reader until it finds \n or io.EOF. diff --git a/vendor/golang.org/x/text/language/parse.go b/vendor/golang.org/x/text/language/parse.go index 4d57222e..053336e2 100644 --- a/vendor/golang.org/x/text/language/parse.go +++ b/vendor/golang.org/x/text/language/parse.go @@ -59,7 +59,7 @@ func (c CanonType) Parse(s string) (t Tag, err error) { if changed { tt.RemakeString() } - return makeTag(tt), err + return makeTag(tt), nil } // Compose creates a Tag from individual parts, which may be of type Tag, Base, diff --git a/vendor/modules.txt b/vendor/modules.txt index 291ca839..00111422 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -31,8 +31,6 @@ github.com/ergochat/webpush-go/v2 # github.com/go-sql-driver/mysql v1.7.0 ## explicit; go 1.13 github.com/go-sql-driver/mysql -# github.com/go-test/deep v1.0.6 -## explicit; go 1.13 # github.com/gofrs/flock v0.8.1 ## explicit github.com/gofrs/flock @@ -76,31 +74,29 @@ github.com/tidwall/rtred/base # github.com/tidwall/tinyqueue v0.1.1 ## explicit; go 1.15 github.com/tidwall/tinyqueue -# github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 -## explicit # github.com/xdg-go/pbkdf2 v1.0.0 ## explicit; go 1.9 github.com/xdg-go/pbkdf2 # github.com/xdg-go/scram v1.0.2 => github.com/ergochat/scram v1.0.2-ergo1 ## explicit; go 1.11 github.com/xdg-go/scram -# golang.org/x/crypto v0.32.0 -## explicit; go 1.20 +# golang.org/x/crypto v0.38.0 +## explicit; go 1.23.0 golang.org/x/crypto/bcrypt golang.org/x/crypto/blowfish golang.org/x/crypto/ed25519 golang.org/x/crypto/hkdf golang.org/x/crypto/pbkdf2 -# golang.org/x/sys v0.29.0 -## explicit; go 1.18 +# golang.org/x/sys v0.33.0 +## explicit; go 1.23.0 golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows -# golang.org/x/term v0.28.0 -## explicit; go 1.18 +# golang.org/x/term v0.32.0 +## explicit; go 1.23.0 golang.org/x/term -# golang.org/x/text v0.21.0 -## explicit; go 1.18 +# golang.org/x/text v0.25.0 +## explicit; go 1.23.0 golang.org/x/text/cases golang.org/x/text/internal golang.org/x/text/internal/language From e7501ef8475cfe12c882004c2db0cacc17bb0e43 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 18 May 2025 01:28:48 -0400 Subject: [PATCH 04/35] upgrade go-msgauth --- go.mod | 2 +- go.sum | 2 ++ vendor/github.com/emersion/go-msgauth/dkim/header.go | 7 ++++++- vendor/github.com/emersion/go-msgauth/dkim/query.go | 2 +- vendor/modules.txt | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 416b22a1..ab661164 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( ) require ( - github.com/emersion/go-msgauth v0.6.8 + github.com/emersion/go-msgauth v0.7.0 github.com/ergochat/webpush-go/v2 v2.0.0 github.com/golang-jwt/jwt/v5 v5.2.2 ) diff --git a/go.sum b/go.sum index 521e691f..24ef25c3 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A= github.com/emersion/go-msgauth v0.6.8/go.mod h1:YDwuyTCUHu9xxmAeVj0eW4INnwB6NNZoPdLerpSxRrc= +github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc= +github.com/emersion/go-msgauth v0.7.0/go.mod h1:mmS9I6HkSovrNgq0HNXTeu8l3sRAAuQ9RMvbM4KU7Ck= github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons= github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1/go.mod h1:mov+uh1DPWsltdQnOdzn08UO9GsJ3MEvhtu0Ci37fdk= github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5AnBVGzTXM/yHVytt48rXBGcJGzSbms= diff --git a/vendor/github.com/emersion/go-msgauth/dkim/header.go b/vendor/github.com/emersion/go-msgauth/dkim/header.go index dddd81f7..09f90212 100644 --- a/vendor/github.com/emersion/go-msgauth/dkim/header.go +++ b/vendor/github.com/emersion/go-msgauth/dkim/header.go @@ -83,7 +83,12 @@ func parseHeaderParams(s string) (map[string]string, error) { return params, errors.New("dkim: malformed header params") } - params[strings.TrimSpace(key)] = strings.TrimSpace(value) + trimmedKey := strings.TrimSpace(key) + _, present := params[trimmedKey] + if present { + return params, errors.New("dkim: duplicate tag name") + } + params[trimmedKey] = strings.TrimSpace(value) } return params, nil } diff --git a/vendor/github.com/emersion/go-msgauth/dkim/query.go b/vendor/github.com/emersion/go-msgauth/dkim/query.go index 33f8556e..45134cdd 100644 --- a/vendor/github.com/emersion/go-msgauth/dkim/query.go +++ b/vendor/github.com/emersion/go-msgauth/dkim/query.go @@ -100,7 +100,7 @@ func queryDNSTXT(domain, selector string, txtLookup txtLookupFunc) (*queryResult func parsePublicKey(s string) (*queryResult, error) { params, err := parseHeaderParams(s) if err != nil { - return nil, permFailError("key syntax error: " + err.Error()) + return nil, permFailError("key record error: " + err.Error()) } res := new(queryResult) diff --git a/vendor/modules.txt b/vendor/modules.txt index 00111422..1a218695 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -10,7 +10,7 @@ github.com/GehirnInc/crypt/md5_crypt # github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 ## explicit github.com/docopt/docopt-go -# github.com/emersion/go-msgauth v0.6.8 +# github.com/emersion/go-msgauth v0.7.0 ## explicit; go 1.18 github.com/emersion/go-msgauth/dkim # github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 From a1c46a4be75c27f09ae458d5e827ca94116a70a4 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 19 May 2025 00:02:04 -0400 Subject: [PATCH 05/35] clarify channel registration instructions --- docs/USERGUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index 427fd01e..65fe88d4 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -86,7 +86,7 @@ Once you've registered your nickname, you can use it to register channels. By de /msg ChanServ register #myChannel ``` -You must already be an operator (have the `+o` channel mode --- your client may display this as an `@` next to your nickname). If you're not a channel operator in the channel you want to register, ask your server administrator for help. +The channel must exist (if it doesn't, you can create it with `/join #myChannel`) and you must already be an operator (have the `+o` channel mode --- your client may display this as an `@` next to your nickname). If you're not a channel operator in the channel you want to register, ask your server administrator for help. # Always-on From 253972a9d2aee9d8ed48aa4ad79669cfd5641119 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Fri, 23 May 2025 00:18:36 -0400 Subject: [PATCH 06/35] clean up constant redefinition --- irc/client.go | 7 +++---- irc/getters.go | 2 +- irc/handlers.go | 8 ++++---- irc/histserv.go | 2 +- irc/message_cache.go | 2 +- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/irc/client.go b/irc/client.go index 14efecd8..2e814ce9 100644 --- a/irc/client.go +++ b/irc/client.go @@ -41,8 +41,7 @@ const ( DefaultMaxLineLen = 512 // IdentTimeout is how long before our ident (username) check times out. - IdentTimeout = time.Second + 500*time.Millisecond - IRCv3TimestampFormat = utils.IRCv3TimestampFormat + IdentTimeout = time.Second + 500*time.Millisecond // limit the number of device IDs a client can use, as a DoS mitigation maxDeviceIDsPerClient = 64 // maximum total read markers that can be stored @@ -1463,7 +1462,7 @@ func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Ti func composeMultilineBatch(batchID, fromNickMask, fromAccount string, isBot bool, tags map[string]string, command, target string, message utils.SplitMessage) (result []ircmsg.Message) { batchStart := ircmsg.MakeMessage(tags, fromNickMask, "BATCH", "+"+batchID, caps.MultilineBatchType, target) - batchStart.SetTag("time", message.Time.Format(IRCv3TimestampFormat)) + batchStart.SetTag("time", message.Time.Format(utils.IRCv3TimestampFormat)) batchStart.SetTag("msgid", message.Msgid) if fromAccount != "*" { batchStart.SetTag("account", fromAccount) @@ -1571,7 +1570,7 @@ func (session *Session) setTimeTag(msg *ircmsg.Message, serverTime time.Time) { if serverTime.IsZero() { serverTime = time.Now() } - msg.SetTag("time", serverTime.UTC().Format(IRCv3TimestampFormat)) + msg.SetTag("time", serverTime.UTC().Format(utils.IRCv3TimestampFormat)) } } diff --git a/irc/getters.go b/irc/getters.go index 902614f7..29f97c75 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -517,7 +517,7 @@ func (client *Client) GetReadMarker(cfname string) (result string) { t, ok := client.readMarkers[cfname] client.stateMutex.RUnlock() if ok { - return fmt.Sprintf("timestamp=%s", t.Format(IRCv3TimestampFormat)) + return fmt.Sprintf("timestamp=%s", t.Format(utils.IRCv3TimestampFormat)) } return "*" } diff --git a/irc/handlers.go b/irc/handlers.go index d0d619c0..538c2126 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -719,7 +719,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb * for _, target := range targets { name := server.UnfoldName(target.CfName) rb.Add(nil, server.name, "CHATHISTORY", "TARGETS", name, - target.Time.Format(IRCv3TimestampFormat)) + target.Time.Format(utils.IRCv3TimestampFormat)) } } else if channel != nil { channel.replayHistoryItems(rb, items, true) @@ -754,7 +754,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb * msgid, err = history.NormalizeMsgid(value), nil return } else if identifier == "timestamp" { - timestamp, err = time.Parse(IRCv3TimestampFormat, value) + timestamp, err = time.Parse(utils.IRCv3TimestampFormat, value) if err == nil { timestamp = timestamp.UTC() if timestamp.Before(unixEpoch) { @@ -3055,14 +3055,14 @@ func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res // "MARKREAD client set command": MARKREAD readTimestamp := strings.TrimPrefix(msg.Params[1], "timestamp=") - readTime, err := time.Parse(IRCv3TimestampFormat, readTimestamp) + readTime, err := time.Parse(utils.IRCv3TimestampFormat, readTimestamp) if err != nil { rb.Add(nil, server.name, "FAIL", "MARKREAD", "INVALID_PARAMS", utils.SafeErrorParam(readTimestamp), client.t("Invalid timestamp")) return } readTime = readTime.UTC() result := client.SetReadMarker(cftarget, readTime) - readTimestamp = fmt.Sprintf("timestamp=%s", result.Format(IRCv3TimestampFormat)) + readTimestamp = fmt.Sprintf("timestamp=%s", result.Format(utils.IRCv3TimestampFormat)) // inform the originating session whether it was a success or a no-op: rb.Add(nil, server.name, "MARKREAD", unfoldedTarget, readTimestamp) if result.Equal(readTime) { diff --git a/irc/histserv.go b/irc/histserv.go index 8e28dafc..35b2e9b4 100644 --- a/irc/histserv.go +++ b/irc/histserv.go @@ -164,7 +164,7 @@ func histservExportHandler(service *ircService, server *Server, client *Client, config := server.Config() // don't include the account name in the filename because of escaping concerns - filename := fmt.Sprintf("%s-%s.json", utils.GenerateSecretToken(), time.Now().UTC().Format(IRCv3TimestampFormat)) + filename := fmt.Sprintf("%s-%s.json", utils.GenerateSecretToken(), time.Now().UTC().Format(utils.IRCv3TimestampFormat)) pathname := config.getOutputPath(filename) outfile, err := os.Create(pathname) if err != nil { diff --git a/irc/message_cache.go b/irc/message_cache.go index 2d731823..f56568b3 100644 --- a/irc/message_cache.go +++ b/irc/message_cache.go @@ -45,7 +45,7 @@ type MessageCache struct { func addAllTags(msg *ircmsg.Message, tags map[string]string, serverTime time.Time, msgid, accountName string, isBot bool) { msg.UpdateTags(tags) - msg.SetTag("time", serverTime.Format(IRCv3TimestampFormat)) + msg.SetTag("time", serverTime.Format(utils.IRCv3TimestampFormat)) if accountName != "*" { msg.SetTag("account", accountName) } From e4aac56bda31580c751b8354fff282d1f63a2dbc Mon Sep 17 00:00:00 2001 From: Sarah Rose <156587765+SarahRoseLives@users.noreply.github.com> Date: Sun, 25 May 2025 00:47:20 -0400 Subject: [PATCH 07/35] API enhancements (#2261) Fixes #2257 and #2260 * add `/v1/status` endpoint * add `/v1/account_list` endpoint * add fields to `/v1/account_details` response --- docs/API.md | 36 ++++++++++++++++++ irc/api.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 131 insertions(+), 8 deletions(-) diff --git a/docs/API.md b/docs/API.md index 056f88c8..524e8077 100644 --- a/docs/API.md +++ b/docs/API.md @@ -51,6 +51,8 @@ The response is a JSON object with fields: * `success`: whether the account exists or not * `accountName`: canonical, case-unfolded version of the account name * `email`: email address of the account provided +* `registeredAt`: string, registration date/time of the account (in ISO8601 format) +* `channels`: array of strings, list of channels the account is registered on or associated with `/v1/check_auth` ---------------- @@ -86,3 +88,37 @@ The response is a JSON object with fields: * `success`: whether the account creation succeeded * `errorCode`: string, optional, machine-readable description of the error. Possible values include: `ACCOUNT_EXISTS`, `INVALID_PASSPHRASE`, `UNKNOWN_ERROR`. * `error`: string, optional, human-readable description of the failure. + +`/v1/account_list` +------------------- + +This endpoint fetches a list of all accounts. The request body is ignored and can be empty. + +The response is a JSON object with fields: + +* `success`: whether the request succeeded +* `accounts`: array of objects, each with fields: + * `success`: boolean, whether this individual account query succeeded + * `accountName`: string, canonical, case-unfolded version of the account name +* `totalCount`: integer, total number of accounts returned + + +`/v1/status` +------------- + +This endpoint returns status information about the running Ergo server. The request body is ignored and can be empty. + +The response is a JSON object with fields: + +* `success`: whether the request succeeded +* `version`: string, Ergo server version string +* `go_version`: string, version of Go runtime used +* `start_time`: string, server start time in ISO8601 format +* `users`: object with fields: + * `total`: total number of users connected + * `invisible`: number of invisible users + * `operators`: number of operators connected + * `unknown`: number of users with unknown status + * `max`: maximum number of users seen connected at once +* `channels`: integer, number of channels currently active +* `servers`: integer, number of servers connected in the network diff --git a/irc/api.go b/irc/api.go index a25b6537..cb313cbc 100644 --- a/irc/api.go +++ b/irc/api.go @@ -5,7 +5,10 @@ import ( "encoding/json" "fmt" "net/http" + "runtime" "strings" + + "github.com/ergochat/ergo/irc/utils" ) func newAPIHandler(server *Server) http.Handler { @@ -18,6 +21,8 @@ func newAPIHandler(server *Server) http.Handler { api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth) api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister) api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails) + api.mux.HandleFunc("POST /v1/account_list", api.handleAccountList) + api.mux.HandleFunc("POST /v1/status", api.handleStatus) return api } @@ -29,7 +34,6 @@ type ergoAPI struct { func (a *ergoAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer a.server.HandlePanic(nil) - defer a.server.logger.Debug("api", r.URL.Path) if a.checkBearerAuth(r.Header.Get("Authorization")) { @@ -117,8 +121,6 @@ func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) { // try passphrase if present if request.AccountName != "" && request.Passphrase != "" { - // TODO this only checks the internal database, not auth-script; - // it's a little weird to use both auth-script and the API but we should probably handle it account, err := a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase) switch err { case nil: @@ -133,7 +135,6 @@ func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) { response.Error = err.Error() } } - // try certfp if present if !response.Success && request.Certfp != "" { // TODO support cerftp @@ -175,8 +176,10 @@ func (a *ergoAPI) handleSaregister(w http.ResponseWriter, r *http.Request) { type apiAccountDetailsResponse struct { apiGenericResponse - AccountName string `json:"accountName,omitempty"` - Email string `json:"email,omitempty"` + AccountName string `json:"accountName,omitempty"` + Email string `json:"email,omitempty"` + RegisteredAt string `json:"registeredAt,omitempty"` + Channels []string `json:"channels,omitempty"` } type apiAccountDetailsRequest struct { @@ -191,8 +194,6 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) { var response apiAccountDetailsResponse - // TODO could probably use better error handling and more details - if request.AccountName != "" { accountData, err := a.server.accounts.LoadAccount(request.AccountName) if err == nil { @@ -207,6 +208,12 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) { case nil: response.AccountName = accountData.Name response.Email = accountData.Settings.Email + if !accountData.RegisteredAt.IsZero() { + response.RegisteredAt = accountData.RegisteredAt.Format(utils.IRCv3TimestampFormat) + } + + // Get channels the account is in + response.Channels = a.server.channels.ChannelsForAccount(accountData.NameCasefolded) response.Success = true case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended: response.Success = false @@ -222,3 +229,83 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) { a.writeJSONResponse(response, w, r) } + +type apiAccountListResponse struct { + apiGenericResponse + Accounts []apiAccountDetailsResponse `json:"accounts"` + TotalCount int `json:"totalCount"` +} + +func (a *ergoAPI) handleAccountList(w http.ResponseWriter, r *http.Request) { + var response apiAccountListResponse + + // Get all account names + accounts := a.server.accounts.AllNicks() + response.TotalCount = len(accounts) + + // Load account details + response.Accounts = make([]apiAccountDetailsResponse, len(accounts)) + for i, account := range accounts { + accountData, err := a.server.accounts.LoadAccount(account) + if err != nil { + response.Accounts[i] = apiAccountDetailsResponse{ + apiGenericResponse: apiGenericResponse{ + Success: false, + Error: err.Error(), + }, + } + continue + } + + response.Accounts[i] = apiAccountDetailsResponse{ + apiGenericResponse: apiGenericResponse{ + Success: true, + }, + AccountName: accountData.Name, + Email: accountData.Settings.Email, + } + } + + response.Success = true + a.writeJSONResponse(response, w, r) +} + +type apiStatusResponse struct { + apiGenericResponse + Version string `json:"version"` + GoVersion string `json:"go_version"` + Commit string `json:"commit,omitempty"` + StartTime string `json:"start_time"` + Users struct { + Total int `json:"total"` + Invisible int `json:"invisible"` + Operators int `json:"operators"` + Unknown int `json:"unknown"` + Max int `json:"max"` + } `json:"users"` + Channels int `json:"channels"` + Servers int `json:"servers"` +} + +func (a *ergoAPI) handleStatus(w http.ResponseWriter, r *http.Request) { + server := a.server + stats := server.stats.GetValues() + + response := apiStatusResponse{ + apiGenericResponse: apiGenericResponse{Success: true}, + Version: SemVer, + GoVersion: runtime.Version(), + Commit: Commit, + StartTime: server.ctime.Format(utils.IRCv3TimestampFormat), + } + + response.Users.Total = stats.Total + response.Users.Invisible = stats.Invisible + response.Users.Operators = stats.Operators + response.Users.Unknown = stats.Unknown + response.Users.Max = stats.Max + response.Channels = server.channels.Len() + response.Servers = 1 + + a.writeJSONResponse(response, w, r) +} From 7138e76151dfdb8f54b0119ea3bbba8971aa93a0 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 25 May 2025 01:59:55 -0400 Subject: [PATCH 08/35] fix #2147 use strings.CutPrefix when possible --- irc/bunt/bunt_datastore.go | 5 +++-- irc/handlers.go | 11 ++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/irc/bunt/bunt_datastore.go b/irc/bunt/bunt_datastore.go index 76c831e3..869c6461 100644 --- a/irc/bunt/bunt_datastore.go +++ b/irc/bunt/bunt_datastore.go @@ -44,10 +44,11 @@ func (b *buntdbDatastore) GetAll(table datastore.Table) (result []datastore.KV, tablePrefix := fmt.Sprintf("%x ", table) err = b.db.View(func(tx *buntdb.Tx) error { err := tx.AscendGreaterOrEqual("", tablePrefix, func(key, value string) bool { - if !strings.HasPrefix(key, tablePrefix) { + encUUID, ok := strings.CutPrefix(key, tablePrefix) + if !ok { return false } - uuid, err := utils.DecodeUUID(strings.TrimPrefix(key, tablePrefix)) + uuid, err := utils.DecodeUUID(encUUID) if err == nil { result = append(result, datastore.KV{UUID: uuid, Value: []byte(value)}) } else { diff --git a/irc/handlers.go b/irc/handlers.go index 538c2126..0883b3b4 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -149,11 +149,8 @@ func (server *Server) sendLoginSnomask(nickMask, accountName string) { // to indicate that it should be removed from the list func acceptHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { for _, tNick := range strings.Split(msg.Params[0], ",") { - add := true - if strings.HasPrefix(tNick, "-") { - add = false - tNick = strings.TrimPrefix(tNick, "-") - } + tNick, negPrefix := strings.CutPrefix(tNick, "-") + add := !negPrefix target := server.clients.Get(tNick) if target == nil { @@ -4118,9 +4115,9 @@ func zncHandler(server *Server, client *Client, msg ircmsg.Message, rb *Response // fake handler for unknown commands func unknownCommandHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { var message string - if strings.HasPrefix(msg.Command, "/") { + if trimmedCmd, initialSlash := strings.CutPrefix(msg.Command, "/"); initialSlash { message = fmt.Sprintf(client.t("Unknown command; if you are using /QUOTE, the correct syntax is /QUOTE %[1]s, not /QUOTE %[2]s"), - strings.TrimPrefix(msg.Command, "/"), msg.Command) + trimmedCmd, msg.Command) } else { message = client.t("Unknown command") } From 9a186f8e5455e26673e19e49b78f0d73dc62a4e2 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 8 Jun 2025 01:43:43 -0400 Subject: [PATCH 09/35] Fix invalid FAIL codes in REGISTER (#2269) * nickserv.go: Update FAIL codes to match spec * handlers.go: Fix FAIL code * use ACCOUNT_EXISTS for errNameReserved * bump irctest to development version --------- Co-authored-by: Valerie Liu <79415174+ValwareIRC@users.noreply.github.com> --- irc/handlers.go | 2 +- irc/nickserv.go | 4 ++-- irctest | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/irc/handlers.go b/irc/handlers.go index 0883b3b4..2b6c23c6 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2974,7 +2974,7 @@ func registerHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res announcePendingReg(client, rb, accountName) } case errAccountAlreadyRegistered, errAccountAlreadyUnregistered, errAccountMustHoldNick: - rb.Add(nil, server.name, "FAIL", "REGISTER", "USERNAME_EXISTS", accountName, client.t("Username is already registered or otherwise unavailable")) + rb.Add(nil, server.name, "FAIL", "REGISTER", "ACCOUNT_EXISTS", accountName, client.t("Username is already registered or otherwise unavailable")) case errAccountBadPassphrase: rb.Add(nil, server.name, "FAIL", "REGISTER", "INVALID_PASSWORD", accountName, client.t("Password was invalid")) default: diff --git a/irc/nickserv.go b/irc/nickserv.go index 51735be1..c973dc89 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -1055,10 +1055,10 @@ func nsSaregisterHandler(service *ircService, server *Server, client *Client, co var failCode string if err == errAccountAlreadyRegistered || err == errAccountAlreadyVerified { errMsg = client.t("Account already exists") - failCode = "USERNAME_EXISTS" + failCode = "ACCOUNT_EXISTS" } else if err == errNameReserved { errMsg = client.t(err.Error()) - failCode = "USERNAME_EXISTS" + failCode = "ACCOUNT_EXISTS" } else if err == errAccountBadPassphrase { errMsg = client.t("Passphrase contains forbidden characters or is otherwise invalid") failCode = "INVALID_PASSWORD" diff --git a/irctest b/irctest index e9e37f54..8fc57a55 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit e9e37f5438bd5f02656b89dab0cd40ef113edac6 +Subproject commit 8fc57a55caa717188129bb7c6399f8143442ed54 From 16568c5ab7b6e037aca229c2409396eb1964431c Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 8 Jun 2025 16:33:39 -0400 Subject: [PATCH 10/35] fix #2270 REGISTER should strip the guest format when applicable, same as NS REGISTER. --- default.yaml | 2 +- irc/handlers.go | 17 ++++++++++++++--- traditional.yaml | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/default.yaml b/default.yaml index 000cbd89..f0342204 100644 --- a/default.yaml +++ b/default.yaml @@ -522,7 +522,7 @@ accounts: # 1. these nicknames cannot be registered or reserved # 2. if a client is automatically renamed by the server, # this is the template that will be used (e.g., Guest-nccj6rgmt97cg) - # 3. if enforce-guest-format (see below) is enabled, clients without + # 3. if force-guest-format (see below) is enabled, clients without # a registered account will have this template applied to their # nicknames (e.g., 'katie' will become 'Guest-katie') guest-nickname-format: "Guest-*" diff --git a/irc/handlers.go b/irc/handlers.go index 2b6c23c6..c606637f 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2907,11 +2907,23 @@ func quitHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons // REGISTER < account | * > < email | * > func registerHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) { - accountName := client.Nick() - if accountName == "*" { + var accountName string + if client.registered { + accountName = client.Nick() + } else { accountName = client.preregNick } + config := server.Config() + if client.registered && config.Accounts.NickReservation.ForceGuestFormat { + matches := config.Accounts.NickReservation.guestRegexp.FindStringSubmatch(accountName) + if matches == nil || len(matches) < 2 { + rb.Add(nil, server.name, "FAIL", "REGISTER", "INVALID_USERNAME", utils.SafeErrorParam(accountName), client.t("Username invalid or not given")) + return + } + accountName = matches[1] + } + switch msg.Params[0] { case "*", accountName: // ok @@ -2928,7 +2940,6 @@ func registerHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res return } - config := server.Config() if !config.Accounts.Registration.Enabled { rb.Add(nil, server.name, "FAIL", "REGISTER", "DISALLOWED", accountName, client.t("Account registration is disabled")) return diff --git a/traditional.yaml b/traditional.yaml index c4c16e0e..df6ff052 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -494,7 +494,7 @@ accounts: # 1. these nicknames cannot be registered or reserved # 2. if a client is automatically renamed by the server, # this is the template that will be used (e.g., Guest-nccj6rgmt97cg) - # 3. if enforce-guest-format (see below) is enabled, clients without + # 3. if force-guest-format (see below) is enabled, clients without # a registered account will have this template applied to their # nicknames (e.g., 'katie' will become 'Guest-katie') guest-nickname-format: "Guest-*" From 0f5603eca2485d783742fa17c77226e56d8bc6eb Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 9 Jun 2025 02:20:49 -0400 Subject: [PATCH 11/35] bump irctest to upstream master --- irctest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest b/irctest index 8fc57a55..15c847eb 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit 8fc57a55caa717188129bb7c6399f8143442ed54 +Subproject commit 15c847ebb19b1c434347236296cbc4d34ba7c4b2 From 4dcbc48159e66fe5d426e62b9fd62d91403979a1 Mon Sep 17 00:00:00 2001 From: thatcher-gaming Date: Sun, 15 Jun 2025 09:06:45 +0100 Subject: [PATCH 12/35] metadata-2 (#2273) Initial implementation of draft/metadata-2 --- default.yaml | 10 +++ gencapdefs.py | 7 ++ irc/caps/defs.go | 7 +- irc/channel.go | 7 ++ irc/channelreg.go | 2 + irc/client.go | 4 + irc/commands.go | 4 + irc/config.go | 27 ++++++ irc/getters.go | 167 +++++++++++++++++++++++++++++++++++- irc/handlers.go | 197 +++++++++++++++++++++++++++++++++++++++++++ irc/help.go | 6 ++ irc/metadata.go | 164 +++++++++++++++++++++++++++++++++++ irc/metadata_test.go | 21 +++++ irc/numerics.go | 5 ++ irc/utils/chunks.go | 28 ++++++ traditional.yaml | 7 ++ 16 files changed, 660 insertions(+), 3 deletions(-) create mode 100644 irc/metadata.go create mode 100644 irc/metadata_test.go create mode 100644 irc/utils/chunks.go diff --git a/default.yaml b/default.yaml index f0342204..22fd27fd 100644 --- a/default.yaml +++ b/default.yaml @@ -1087,6 +1087,16 @@ history: # e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details. allow-environment-overrides: true +# experimental IRC metadata support for setting key/value data on channels and nicknames. +metadata: + # can clients store metadata? + enabled: true + # how many keys can a client subscribe to? + # set to 0 to disable subscriptions or -1 to allow unlimited subscriptions. + max-subs: 100 + # how many keys can a user store about themselves? set to -1 to allow unlimited keys. + max-keys: 1000 + # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications. # DO NOT enable if you are running a Tor or I2P hidden service (i.e. one diff --git a/gencapdefs.py b/gencapdefs.py index 859aaa3d..45453c2b 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -237,6 +237,13 @@ CAPDEFS = [ url="https://github.com/ircv3/ircv3-specifications/pull/471", standard="Soju/Goguma vendor", ), + CapDef( + identifier="Metadata", + name="draft/metadata-2", + url="https://ircv3.net/specs/extensions/metadata", + standard="draft IRCv3", + ), + ] def validate_defs(): diff --git a/irc/caps/defs.go b/irc/caps/defs.go index 5f747d49..a6377884 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -7,7 +7,7 @@ package caps const ( // number of recognized capabilities: - numCapabs = 37 + numCapabs = 38 // length of the uint32 array that represents the bitset: bitsetLen = 2 ) @@ -65,6 +65,10 @@ const ( // https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md MessageRedaction Capability = iota + // Metadata is the draft IRCv3 capability named "draft/metadata-2": + // https://ircv3.net/specs/extensions/metadata + Metadata Capability = iota + // Multiline is the proposed IRCv3 capability named "draft/multiline": // https://github.com/ircv3/ircv3-specifications/pull/398 Multiline Capability = iota @@ -178,6 +182,7 @@ var ( "draft/extended-isupport", "draft/languages", "draft/message-redaction", + "draft/metadata-2", "draft/multiline", "draft/no-implicit-names", "draft/persistence", diff --git a/irc/channel.go b/irc/channel.go index 1dc55f43..1b449a70 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -55,6 +55,7 @@ type Channel struct { dirtyBits uint settings ChannelSettings uuid utils.UUID + metadata map[string]string // these caches are paired to allow iteration over channel members without holding the lock membersCache []*Client memberDataCache []*memberData @@ -126,6 +127,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) { channel.userLimit = chanReg.UserLimit channel.settings = chanReg.Settings channel.forward = chanReg.Forward + channel.metadata = chanReg.Metadata for _, mode := range chanReg.Modes { channel.flags.SetMode(mode, true) @@ -163,6 +165,7 @@ func (channel *Channel) ExportRegistration() (info RegisteredChannel) { info.AccountToUMode = maps.Clone(channel.accountToUMode) info.Settings = channel.settings + info.Metadata = channel.metadata return } @@ -892,6 +895,10 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp rb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname)) } + if rb.session.capabilities.Has(caps.Metadata) { + syncChannelMetadata(client.server, rb, channel) + } + if rb.session.client == client { // don't send topic and names for a SAJOIN of a different client channel.SendTopic(client, rb, false) diff --git a/irc/channelreg.go b/irc/channelreg.go index 1978b4ef..3e61e460 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -63,6 +63,8 @@ type RegisteredChannel struct { Invites map[string]MaskInfo // Settings are the chanserv-modifiable settings Settings ChannelSettings + // Metadata set using the METADATA command + Metadata map[string]string } func (r *RegisteredChannel) Serialize() ([]byte, error) { diff --git a/irc/client.go b/irc/client.go index 2e814ce9..25aa72cf 100644 --- a/irc/client.go +++ b/irc/client.go @@ -131,6 +131,7 @@ type Client struct { clearablePushMessages map[string]time.Time pushSubscriptionsExist atomic.Uint32 // this is a cache on len(pushSubscriptions) != 0 pushQueue pushQueue + metadata map[string]string } type saslStatus struct { @@ -214,6 +215,8 @@ type Session struct { batch MultilineBatch webPushEndpoint string // goroutine-local: web push endpoint registered by the current session + + metadataSubscriptions utils.HashSet[string] } // MultilineBatch tracks the state of a client-to-server multiline batch. @@ -1129,6 +1132,7 @@ func (client *Client) SetNick(nick, nickCasefolded, skeleton string) (success bo client.nickCasefolded = nickCasefolded client.skeleton = skeleton client.updateNickMaskNoMutex() + return true } diff --git a/irc/commands.go b/irc/commands.go index 54016b96..d5387306 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -209,6 +209,10 @@ func init() { handler: markReadHandler, minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS }, + "METADATA": { + handler: metadataHandler, + minParams: 2, + }, "MODE": { handler: modeHandler, minParams: 1, diff --git a/irc/config.go b/irc/config.go index b10e08e6..67f2ec03 100644 --- a/irc/config.go +++ b/irc/config.go @@ -723,6 +723,14 @@ type Config struct { } `yaml:"tagmsg-storage"` } + Metadata struct { + // BeforeConnect int `yaml:"before-connect"` todo: this + Enabled bool + MaxSubs int `yaml:"max-subs"` + MaxKeys int `yaml:"max-keys"` + MaxValueBytes int `yaml:"max-value-length"` // todo: currently unenforced!! + } + WebPush struct { Enabled bool Timeout time.Duration @@ -1637,6 +1645,25 @@ func LoadConfig(filename string) (config *Config, err error) { } } + if !config.Metadata.Enabled { + config.Server.supportedCaps.Disable(caps.Metadata) + } else { + var metadataValues []string + if config.Metadata.MaxSubs >= 0 { + metadataValues = append(metadataValues, fmt.Sprintf("max-subs=%d", config.Metadata.MaxSubs)) + } + if config.Metadata.MaxKeys > 0 { + metadataValues = append(metadataValues, fmt.Sprintf("max-keys=%d", config.Metadata.MaxKeys)) + } + if config.Metadata.MaxValueBytes > 0 { + metadataValues = append(metadataValues, fmt.Sprintf("max-value-bytes=%d", config.Metadata.MaxValueBytes)) + } + if len(metadataValues) != 0 { + config.Server.capValues[caps.Metadata] = strings.Join(metadataValues, ",") + } + + } + err = config.processExtjwt() if err != nil { return nil, err diff --git a/irc/getters.go b/irc/getters.go index 29f97c75..5feadaa3 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -7,6 +7,7 @@ import ( "fmt" "maps" "net" + "slices" "time" "github.com/ergochat/ergo/irc/caps" @@ -797,10 +798,12 @@ func (channel *Channel) Settings() (result ChannelSettings) { } func (channel *Channel) SetSettings(settings ChannelSettings) { + defer channel.MarkDirty(IncludeSettings) + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + channel.settings = settings - channel.stateMutex.Unlock() - channel.MarkDirty(IncludeSettings) } func (channel *Channel) setForward(forward string) { @@ -827,3 +830,163 @@ func (channel *Channel) UUID() utils.UUID { defer channel.stateMutex.RUnlock() return channel.uuid } + +func (session *Session) isSubscribedTo(key string) bool { + session.client.stateMutex.RLock() + defer session.client.stateMutex.RUnlock() + + return session.metadataSubscriptions.Has(key) +} + +func (session *Session) SubscribeTo(keys ...string) ([]string, error) { + session.client.stateMutex.Lock() + defer session.client.stateMutex.Unlock() + + if session.metadataSubscriptions == nil { + session.metadataSubscriptions = make(utils.HashSet[string]) + } + + var added []string + + maxSubs := session.client.server.Config().Metadata.MaxSubs + + for _, k := range keys { + if !session.metadataSubscriptions.Has(k) { + if len(session.metadataSubscriptions) > maxSubs { + return added, errMetadataTooManySubs + } + added = append(added, k) + session.metadataSubscriptions.Add(k) + } + } + + return added, nil +} + +func (session *Session) UnsubscribeFrom(keys ...string) []string { + session.client.stateMutex.Lock() + defer session.client.stateMutex.Unlock() + + var removed []string + + for k := range session.metadataSubscriptions { + if slices.Contains(keys, k) { + removed = append(removed, k) + session.metadataSubscriptions.Remove(k) + } + } + + return removed +} + +func (session *Session) MetadataSubscriptions() utils.HashSet[string] { + session.client.stateMutex.Lock() + defer session.client.stateMutex.Unlock() + + return maps.Clone(session.metadataSubscriptions) +} + +func (channel *Channel) GetMetadata(key string) (string, bool) { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + + val, ok := channel.metadata[key] + return val, ok +} + +func (channel *Channel) SetMetadata(key string, value string) { + defer channel.MarkDirty(IncludeAllAttrs) + + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + + if channel.metadata == nil { + channel.metadata = make(map[string]string) + } + + channel.metadata[key] = value +} + +func (channel *Channel) ListMetadata() map[string]string { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + + return maps.Clone(channel.metadata) +} + +func (channel *Channel) DeleteMetadata(key string) { + defer channel.MarkDirty(IncludeAllAttrs) + + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + + delete(channel.metadata, key) +} + +func (channel *Channel) ClearMetadata() map[string]string { + defer channel.MarkDirty(IncludeAllAttrs) + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + + oldMap := channel.metadata + channel.metadata = nil + + return oldMap +} + +func (channel *Channel) CountMetadata() int { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + + return len(channel.metadata) +} + +func (client *Client) GetMetadata(key string) (string, bool) { + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + + val, ok := client.metadata[key] + return val, ok +} + +func (client *Client) SetMetadata(key string, value string) { + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + + if client.metadata == nil { + client.metadata = make(map[string]string) + } + + client.metadata[key] = value +} + +func (client *Client) ListMetadata() map[string]string { + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + + return maps.Clone(client.metadata) +} + +func (client *Client) DeleteMetadata(key string) { + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + + delete(client.metadata, key) +} + +func (client *Client) ClearMetadata() map[string]string { + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + + oldMap := client.metadata + client.metadata = nil + + return oldMap +} + +func (client *Client) CountMetadata() int { + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + + return len(client.metadata) +} diff --git a/irc/handlers.go b/irc/handlers.go index c606637f..6d727268 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -9,11 +9,13 @@ package irc import ( "bytes" "fmt" + "maps" "net" "os" "runtime" "runtime/debug" "runtime/pprof" + "slices" "sort" "strconv" "strings" @@ -3097,6 +3099,201 @@ func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res return } +// METADATA [...] +func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) { + originalTarget := msg.Params[0] + target := originalTarget + + if !server.Config().Metadata.Enabled { + rb.Add(nil, server.name, "FAIL", "METADATA", "FORBIDDEN", originalTarget, "Metadata is disabled on this server") + return + } + + subcommand := strings.ToLower(msg.Params[1]) + + invalidTarget := func() { + rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_TARGET", originalTarget, client.t("Invalid metadata target")) + } + noKeyPerms := func(key string) { + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NO_PERMISSION", originalTarget, key, client.t("You do not have permission to perform this action")) + } + + if target == "*" { + target = client.Nick() + } + + targetClient := server.clients.Get(target) + targetChannel := server.channels.Get(target) + if !metadataCanISeeThisTarget(client, target) { + invalidTarget() + return + } + + var t MetadataHaver + if targetClient != nil { + t = targetClient + } + if targetChannel != nil { + t = targetChannel + } + if t == nil { + invalidTarget() + return + } + + needsKey := subcommand == "set" || subcommand == "get" || subcommand == "sub" || subcommand == "unsub" + if needsKey && len(msg.Params) <= 2 { + rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), msg.Command, client.t("Not enough parameters")) + return + } + + switch subcommand { + case "set": + key := strings.ToLower(msg.Params[2]) + if metadataKeyIsEvil(key) { + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", key, client.t("Invalid key name")) + return + } + + if !metadataCanIEditThisKey(client, target, key) { + noKeyPerms(key) + return + } + + if len(msg.Params) > 3 { + value := msg.Params[3] + const maxCombinedLen = 350 + + if len(key)+len(value) > maxCombinedLen { + rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t("Value is too long")) + return + } + + maxKeys := server.Config().Metadata.MaxKeys + isSelf := targetClient != nil && client == targetClient + + if isSelf && maxKeys > 0 && t.CountMetadata() >= maxKeys { + rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", client.nick, client.t("You have too many keys set on yourself")) + return + } + + server.logger.Debug("metadata", "setting", key, value, "on", target) + + t.SetMetadata(key, value) + notifySubscribers(server, rb.session, target, key, value) + + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, "*", value) + } else { + server.logger.Debug("metadata", "deleting", key, "on", target) + t.DeleteMetadata(key) + notifySubscribers(server, rb.session, target, key, "") + + rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key deleted")) + } + + case "get": + batchId := rb.StartNestedBatch("metadata") + defer rb.EndNestedBatch(batchId) + + for _, key := range msg.Params[2:] { + if metadataKeyIsEvil(key) { + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", key, client.t("Invalid key name")) + continue + } + + val, ok := t.GetMetadata(key) + if !ok { + rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key is not set")) + continue + } + + visibility := "*" + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, visibility, val) + } + + case "list": + values := t.ListMetadata() + + batchId := rb.StartNestedBatch("metadata") + defer rb.EndNestedBatch(batchId) + + for key, val := range values { + visibility := "*" + + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, visibility, val) + } + + case "clear": + if !metadataCanIEditThisTarget(client, target) { + invalidTarget() + return + } + + values := t.ClearMetadata() + + batchId := rb.StartNestedBatch("metadata") + defer rb.EndNestedBatch(batchId) + + for key, val := range values { + visibility := "*" + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, visibility, val) + } + + case "sub": + keys := msg.Params[2:] + server.logger.Debug("metadata", client.nick, "has subscrumbled to", strings.Join(keys, ", ")) + added, err := rb.session.SubscribeTo(keys...) + if err == errMetadataTooManySubs { + bad := keys[len(added)] // get the key that broke the camel's back + rb.Add(nil, server.name, "FAIL", "METADATA", "TOO_MANY_SUBS", bad, client.t("Too many subscriptions")) + } + + lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBOK) - len(client.Nick()) - 10 + + chunked := utils.ChunkifyParams(slices.Values(added), lineLength) + for _, line := range chunked { + params := append([]string{client.Nick()}, line...) + rb.Add(nil, server.name, RPL_METADATASUBOK, params...) + } + + case "unsub": + keys := msg.Params[2:] + server.logger.Debug("metadata", client.nick, "has UNsubscrumbled to", strings.Join(keys, ", ")) + removed := rb.session.UnsubscribeFrom(keys...) + + lineLength := MaxLineLen - len(server.name) - len(RPL_METADATAUNSUBOK) - len(client.Nick()) - 10 + chunked := utils.ChunkifyParams(slices.Values(removed), lineLength) + for _, line := range chunked { + params := append([]string{client.Nick()}, line...) + rb.Add(nil, server.name, RPL_METADATAUNSUBOK, params...) + } + + case "subs": + lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBS) - len(client.Nick()) - 10 // for safety + + subs := rb.session.MetadataSubscriptions() + + chunked := utils.ChunkifyParams(maps.Keys(subs), lineLength) + for _, line := range chunked { + params := append([]string{client.Nick()}, line...) + rb.Add(nil, server.name, RPL_METADATASUBS, params...) + } + + case "sync": + if targetChannel != nil { + syncChannelMetadata(server, rb, targetChannel) + } + if targetClient != nil { + syncClientMetadata(server, rb, targetClient) + } + + default: + rb.Add(nil, server.name, "FAIL", "METADATA", "SUBCOMMAND_INVALID", msg.Params[1], client.t("Invalid subcommand")) + } + + return +} + // REHASH func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { nick := client.Nick() diff --git a/irc/help.go b/irc/help.go index 6a6824a0..d516c43e 100644 --- a/irc/help.go +++ b/irc/help.go @@ -339,6 +339,12 @@ command is processed by that server.`, MARKREAD updates an IRCv3 read message marker. It is not intended for use by end users. For more details, see the latest draft of the read-marker specification.`, + }, + "metadata": { + text: `METADATA [...] + +Retrieve and meddle with metadata for the given target. +Have a look at https://ircv3.net/specs/extensions/metadata for interesting technical information.`, }, "mode": { text: `MODE [ [...]] diff --git a/irc/metadata.go b/irc/metadata.go new file mode 100644 index 00000000..d7094065 --- /dev/null +++ b/irc/metadata.go @@ -0,0 +1,164 @@ +package irc + +import ( + "errors" + "regexp" + "strings" + + "github.com/ergochat/ergo/irc/caps" + "github.com/ergochat/ergo/irc/modes" + "github.com/ergochat/ergo/irc/utils" +) + +var ( + errMetadataTooManySubs = errors.New("too many subscriptions") + errMetadataNotFound = errors.New("key not found") +) + +type MetadataHaver = interface { + SetMetadata(key string, value string) + GetMetadata(key string) (string, bool) + DeleteMetadata(key string) + ListMetadata() map[string]string + ClearMetadata() map[string]string + CountMetadata() int +} + +func notifySubscribers(server *Server, session *Session, target string, key string, value string) { + var notify utils.HashSet[*Session] = make(utils.HashSet[*Session]) + targetChannel := server.channels.Get(target) + targetClient := server.clients.Get(target) + + if targetClient != nil { + notify = targetClient.FriendsMonitors(caps.Metadata) + // notify clients about changes regarding themselves + for _, s := range targetClient.Sessions() { + notify.Add(s) + } + } + if targetChannel != nil { + members := targetChannel.Members() + for _, m := range members { + for _, s := range m.Sessions() { + if s.capabilities.Has(caps.Metadata) { + notify.Add(s) + } + } + } + } + + // don't notify the session that made the change + notify.Remove(session) + + for s := range notify { + if !s.isSubscribedTo(key) { + continue + } + + if value != "" { + s.Send(nil, server.name, "METADATA", target, key, "*", value) + } else { + s.Send(nil, server.name, "METADATA", target, key, "*") + } + } +} + +func syncClientMetadata(server *Server, rb *ResponseBuffer, target *Client) { + if len(rb.session.MetadataSubscriptions()) == 0 { + return + } + + batchId := rb.StartNestedBatch("metadata") + defer rb.EndNestedBatch(batchId) + + values := target.ListMetadata() + for k, v := range values { + if rb.session.isSubscribedTo(k) { + visibility := "*" + rb.Add(nil, server.name, "METADATA", target.Nick(), k, visibility, v) + } + } +} + +func syncChannelMetadata(server *Server, rb *ResponseBuffer, target *Channel) { + if len(rb.session.MetadataSubscriptions()) == 0 { + return + } + + batchId := rb.StartNestedBatch("metadata") + defer rb.EndNestedBatch(batchId) + + values := target.ListMetadata() + for k, v := range values { + if rb.session.isSubscribedTo(k) { + visibility := "*" + rb.Add(nil, server.name, "METADATA", target.Name(), k, visibility, v) + } + } + + for _, client := range target.Members() { + values := client.ListMetadata() + for k, v := range values { + if rb.session.isSubscribedTo(k) { + visibility := "*" + rb.Add(nil, server.name, "METADATA", client.Nick(), k, visibility, v) + } + } + } +} + +var metadataEvilCharsRegexp = regexp.MustCompile("[^A-Za-z0-9_./:-]+") + +func metadataKeyIsEvil(key string) bool { + key = strings.TrimSpace(key) // just in case + + return len(key) == 0 || // key needs to contain stuff + key[0] == ':' || // key can't start with a colon + metadataEvilCharsRegexp.MatchString(key) // key can't contain the stuff it can't contain +} + +func metadataCanIEditThisKey(client *Client, target string, _ string) bool { + if !metadataCanIEditThisTarget(client, target) { // you can't edit keys on targets you can't edit. + return false + } + + // todo: we don't actually do anything regarding visibility yet so there's not much to do here + + return true +} + +func metadataCanIEditThisTarget(client *Client, target string) bool { + if !metadataCanISeeThisTarget(client, target) { // you can't edit what you can't see. a wise man told me this once + return false + } + + if client.HasRoleCapabs("sajoin") { // sajoin opers can do whatever they want + return true + } + + if target == client.Nick() { // your right to swing your fist ends where my nose begins + return true + } + + // if you're a channel operator, knock yourself out + channel := client.server.channels.Get(target) + if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) { + return true + } + + return false +} + +func metadataCanISeeThisTarget(client *Client, target string) bool { + if client.HasRoleCapabs("sajoin") { // sajoin opers can do whatever they want + return true + } + + // check if the user is in the channel + channel := client.server.channels.Get(target) + if channel != nil && !channel.hasClient(client) { + return false + } + + return true +} diff --git a/irc/metadata_test.go b/irc/metadata_test.go new file mode 100644 index 00000000..93942abb --- /dev/null +++ b/irc/metadata_test.go @@ -0,0 +1,21 @@ +package irc + +import "testing" + +func TestKeyCheck(t *testing.T) { + cases := []struct { + input string + isEvil bool + }{ + {"ImNormal", false}, + {":imevil", true}, + {"key£with$not%allowed^chars", true}, + {"key.that:s_completely/normal-and.fine", false}, + } + + for _, c := range cases { + if metadataKeyIsEvil(c.input) != c.isEvil { + t.Errorf("%s should have returned %v. but it didn't. so that's not great", c.input, c.isEvil) + } + } +} diff --git a/irc/numerics.go b/irc/numerics.go index 97d4604d..bf5a6d60 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -183,6 +183,11 @@ const ( RPL_MONLIST = "732" RPL_ENDOFMONLIST = "733" ERR_MONLISTFULL = "734" + RPL_KEYVALUE = "761" // metadata numerics + RPL_KEYNOTSET = "766" + RPL_METADATASUBOK = "770" + RPL_METADATAUNSUBOK = "771" + RPL_METADATASUBS = "772" RPL_LOGGEDIN = "900" RPL_LOGGEDOUT = "901" ERR_NICKLOCKED = "902" diff --git a/irc/utils/chunks.go b/irc/utils/chunks.go new file mode 100644 index 00000000..30376626 --- /dev/null +++ b/irc/utils/chunks.go @@ -0,0 +1,28 @@ +package utils + +import "iter" + +func ChunkifyParams(params iter.Seq[string], maxChars int) [][]string { + var chunked [][]string + + var acc []string + var length = 0 + + for p := range params { + length = length + len(p) + 1 // (accounting for the space) + + if length > maxChars { + chunked = append(chunked, acc) + acc = []string{} + length = 0 + } + + acc = append(acc, p) + } + + if len(acc) != 0 { + chunked = append(chunked, acc) + } + + return chunked +} diff --git a/traditional.yaml b/traditional.yaml index df6ff052..55f16ea7 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -1058,6 +1058,13 @@ history: # e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details. allow-environment-overrides: true +# experimental IRC metadata support for setting key/value data on channels and nicknames. +metadata: + # can clients use the metadata command? + enabled: true + # how many keys can a client subscribe to? + max-subs: 1000 + # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications. # DO NOT enable if you are running a Tor or I2P hidden service (i.e. one From 3b7db7fff757919dee3564f5e4adde46ce77d7be Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 18 Jun 2025 00:22:49 -0400 Subject: [PATCH 13/35] round 1 of follow-up for metadata (#2277) * refactoring * send an empty batch if necessary, as per spec * don't broadcast no-op updates * don't trim spaces before validating the key * bump irctest to cover metadata * replay existing metadata to reattaching always-on clients * use canonicalized name everywhere * use utils.SafeErrorParam in FAIL lines * validate key names for sub * fix error for METADATA CLEAR * max-keys is enforced for channels as well * remove unlimited configurations * maintain the limit exactly without off-by-one cases * add final channel registration check --- default.yaml | 10 +-- irc/channel.go | 15 +++++ irc/channelmanager.go | 4 ++ irc/config.go | 19 +++--- irc/errors.go | 1 + irc/getters.go | 40 +++++++++--- irc/handlers.go | 141 +++++++++++++++++++++++------------------- irc/help.go | 2 +- irc/metadata.go | 139 +++++++++++++++++------------------------ irc/server.go | 3 + irctest | 2 +- traditional.yaml | 9 ++- 12 files changed, 213 insertions(+), 172 deletions(-) diff --git a/default.yaml b/default.yaml index 22fd27fd..a8d12236 100644 --- a/default.yaml +++ b/default.yaml @@ -724,6 +724,7 @@ oper-classes: - "history" # modify or delete history messages - "defcon" # use the DEFCON command (restrict server capabilities) - "massmessage" # message all users on the server + - "metadata" # modify arbitrary metadata on channels and users # ircd operators opers: @@ -1087,15 +1088,14 @@ history: # e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details. allow-environment-overrides: true -# experimental IRC metadata support for setting key/value data on channels and nicknames. +# metadata support for setting key/value data on channels and nicknames. metadata: # can clients store metadata? enabled: true - # how many keys can a client subscribe to? - # set to 0 to disable subscriptions or -1 to allow unlimited subscriptions. + # how many keys can a client subscribe to? max-subs: 100 - # how many keys can a user store about themselves? set to -1 to allow unlimited keys. - max-keys: 1000 + # how many keys can be stored per entity? + max-keys: 100 # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications. diff --git a/irc/channel.go b/irc/channel.go index 1b449a70..ade4b5b4 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -7,6 +7,7 @@ package irc import ( "fmt" + "iter" "maps" "strconv" "strings" @@ -1676,6 +1677,20 @@ func (channel *Channel) auditoriumFriends(client *Client) (friends []*Client) { return } +func (channel *Channel) sessionsWithCaps(capabs ...caps.Capability) iter.Seq[*Session] { + return func(yield func(*Session) bool) { + for _, member := range channel.Members() { + for _, sess := range member.Sessions() { + if sess.capabilities.HasAll(capabs...) { + if !yield(sess) { + return + } + } + } + } + } +} + // returns whether the client is visible to unprivileged users in the channel // (i.e., respecting auditorium mode). note that this assumes that the client // is a member; if the client is not, it may return true anyway diff --git a/irc/channelmanager.go b/irc/channelmanager.go index 5934ab43..f5bbed39 100644 --- a/irc/channelmanager.go +++ b/irc/channelmanager.go @@ -206,6 +206,10 @@ func (cm *ChannelManager) Cleanup(channel *Channel) { } func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) { + if account == "" { + return errAuthRequired // this is already enforced by ChanServ, but do a final check + } + if cm.server.Defcon() <= 4 { return errFeatureDisabled } diff --git a/irc/config.go b/irc/config.go index 67f2ec03..2ef14681 100644 --- a/irc/config.go +++ b/irc/config.go @@ -728,7 +728,7 @@ type Config struct { Enabled bool MaxSubs int `yaml:"max-subs"` MaxKeys int `yaml:"max-keys"` - MaxValueBytes int `yaml:"max-value-length"` // todo: currently unenforced!! + MaxValueBytes int `yaml:"max-value-length"` } WebPush struct { @@ -1649,19 +1649,20 @@ func LoadConfig(filename string) (config *Config, err error) { config.Server.supportedCaps.Disable(caps.Metadata) } else { var metadataValues []string - if config.Metadata.MaxSubs >= 0 { - metadataValues = append(metadataValues, fmt.Sprintf("max-subs=%d", config.Metadata.MaxSubs)) + // these are required for normal operation, so set sane defaults: + if config.Metadata.MaxSubs == 0 { + config.Metadata.MaxSubs = 10 } - if config.Metadata.MaxKeys > 0 { - metadataValues = append(metadataValues, fmt.Sprintf("max-keys=%d", config.Metadata.MaxKeys)) + metadataValues = append(metadataValues, fmt.Sprintf("max-subs=%d", config.Metadata.MaxSubs)) + if config.Metadata.MaxKeys == 0 { + config.Metadata.MaxKeys = 10 } + metadataValues = append(metadataValues, fmt.Sprintf("max-keys=%d", config.Metadata.MaxKeys)) + // this is not required since we enforce a hardcoded upper bound on key+value if config.Metadata.MaxValueBytes > 0 { metadataValues = append(metadataValues, fmt.Sprintf("max-value-bytes=%d", config.Metadata.MaxValueBytes)) } - if len(metadataValues) != 0 { - config.Server.capValues[caps.Metadata] = strings.Join(metadataValues, ",") - } - + config.Server.capValues[caps.Metadata] = strings.Join(metadataValues, ",") } err = config.processExtjwt() diff --git a/irc/errors.go b/irc/errors.go index aed797eb..6c3a790a 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -33,6 +33,7 @@ var ( errAccountVerificationInvalidCode = errors.New("Invalid account verification code") errAccountUpdateFailed = errors.New(`Error while updating your account information`) errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`) + errAuthRequired = errors.New("You must be logged into an account to do this") errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`) errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`) errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account") diff --git a/irc/getters.go b/irc/getters.go index 5feadaa3..3bba6115 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -894,7 +894,7 @@ func (channel *Channel) GetMetadata(key string) (string, bool) { return val, ok } -func (channel *Channel) SetMetadata(key string, value string) { +func (channel *Channel) SetMetadata(key string, value string, limit int) (updated bool, err error) { defer channel.MarkDirty(IncludeAllAttrs) channel.stateMutex.Lock() @@ -904,7 +904,15 @@ func (channel *Channel) SetMetadata(key string, value string) { channel.metadata = make(map[string]string) } - channel.metadata[key] = value + existing, ok := channel.metadata[key] + if !ok && len(channel.metadata) >= limit { + return false, errLimitExceeded + } + updated = !ok || value != existing + if updated { + channel.metadata[key] = value + } + return updated, nil } func (channel *Channel) ListMetadata() map[string]string { @@ -914,13 +922,17 @@ func (channel *Channel) ListMetadata() map[string]string { return maps.Clone(channel.metadata) } -func (channel *Channel) DeleteMetadata(key string) { +func (channel *Channel) DeleteMetadata(key string) (updated bool) { defer channel.MarkDirty(IncludeAllAttrs) channel.stateMutex.Lock() defer channel.stateMutex.Unlock() - delete(channel.metadata, key) + _, updated = channel.metadata[key] + if updated { + delete(channel.metadata, key) + } + return updated } func (channel *Channel) ClearMetadata() map[string]string { @@ -949,7 +961,7 @@ func (client *Client) GetMetadata(key string) (string, bool) { return val, ok } -func (client *Client) SetMetadata(key string, value string) { +func (client *Client) SetMetadata(key string, value string, limit int) (updated bool, err error) { client.stateMutex.Lock() defer client.stateMutex.Unlock() @@ -957,7 +969,15 @@ func (client *Client) SetMetadata(key string, value string) { client.metadata = make(map[string]string) } - client.metadata[key] = value + existing, ok := client.metadata[key] + if !ok && len(client.metadata) >= limit { + return false, errLimitExceeded + } + updated = !ok || value != existing + if updated { + client.metadata[key] = value + } + return updated, nil } func (client *Client) ListMetadata() map[string]string { @@ -967,11 +987,15 @@ func (client *Client) ListMetadata() map[string]string { return maps.Clone(client.metadata) } -func (client *Client) DeleteMetadata(key string) { +func (client *Client) DeleteMetadata(key string) (updated bool) { client.stateMutex.Lock() defer client.stateMutex.Unlock() - delete(client.metadata, key) + _, updated = client.metadata[key] + if updated { + delete(client.metadata, key) + } + return updated } func (client *Client) ClearMetadata() map[string]string { diff --git a/irc/handlers.go b/irc/handlers.go index 6d727268..e2cdf94d 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -20,6 +20,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" "github.com/ergochat/irc-go/ircfmt" "github.com/ergochat/irc-go/ircmsg" @@ -3101,43 +3102,42 @@ func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res // METADATA [...] func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) { - originalTarget := msg.Params[0] - target := originalTarget + target := msg.Params[0] - if !server.Config().Metadata.Enabled { - rb.Add(nil, server.name, "FAIL", "METADATA", "FORBIDDEN", originalTarget, "Metadata is disabled on this server") + config := server.Config() + if !config.Metadata.Enabled { + rb.Add(nil, server.name, "FAIL", "METADATA", "FORBIDDEN", target, "Metadata is disabled on this server") return } subcommand := strings.ToLower(msg.Params[1]) - invalidTarget := func() { - rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_TARGET", originalTarget, client.t("Invalid metadata target")) - } noKeyPerms := func(key string) { - rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NO_PERMISSION", originalTarget, key, client.t("You do not have permission to perform this action")) + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NO_PERMISSION", target, key, client.t("You do not have permission to perform this action")) } if target == "*" { target = client.Nick() } - targetClient := server.clients.Get(target) - targetChannel := server.channels.Get(target) - if !metadataCanISeeThisTarget(client, target) { - invalidTarget() - return + var targetObj MetadataHaver + var targetClient *Client + var targetChannel *Channel + if strings.HasPrefix(target, "#") { + targetChannel = server.channels.Get(target) + if targetChannel != nil { + targetObj = targetChannel + target = targetChannel.Name() // canonicalize case + } + } else { + targetClient = server.clients.Get(target) + if targetClient != nil { + targetObj = targetClient + target = targetClient.Nick() // canonicalize case + } } - - var t MetadataHaver - if targetClient != nil { - t = targetClient - } - if targetChannel != nil { - t = targetChannel - } - if t == nil { - invalidTarget() + if targetObj == nil { + rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_TARGET", target, client.t("Invalid metadata target")) return } @@ -3151,101 +3151,105 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res case "set": key := strings.ToLower(msg.Params[2]) if metadataKeyIsEvil(key) { - rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", key, client.t("Invalid key name")) + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name")) return } - if !metadataCanIEditThisKey(client, target, key) { + if !metadataCanIEditThisKey(client, targetObj, key) { noKeyPerms(key) return } if len(msg.Params) > 3 { value := msg.Params[3] - const maxCombinedLen = 350 - if len(key)+len(value) > maxCombinedLen { + if !globalUtf8EnforcementSetting && !utf8.ValidString(value) { + rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t("METADATA values must be UTF-8")) + return + } + + if len(key)+len(value) > maxCombinedMetadataLenBytes || + (config.Metadata.MaxValueBytes > 0 && len(value) > config.Metadata.MaxValueBytes) { + rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t("Value is too long")) return } - maxKeys := server.Config().Metadata.MaxKeys - isSelf := targetClient != nil && client == targetClient - - if isSelf && maxKeys > 0 && t.CountMetadata() >= maxKeys { - rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", client.nick, client.t("You have too many keys set on yourself")) + updated, err := targetObj.SetMetadata(key, value, config.Metadata.MaxKeys) + if err != nil { + // errLimitExceeded is the only possible error + rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", client.t("Too many metadata keys")) return } - - server.logger.Debug("metadata", "setting", key, value, "on", target) - - t.SetMetadata(key, value) - notifySubscribers(server, rb.session, target, key, value) - - rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, "*", value) + // echo the value to the client whether or not there was a real update + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, "*", value) + if updated { + notifySubscribers(server, rb.session, targetObj, target, key, value) + } } else { - server.logger.Debug("metadata", "deleting", key, "on", target) - t.DeleteMetadata(key) - notifySubscribers(server, rb.session, target, key, "") - + if updated := targetObj.DeleteMetadata(key); updated { + notifySubscribers(server, rb.session, targetObj, target, key, "") + } + // acknowledge to the client whether or not there was a real update rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key deleted")) } case "get": + if !metadataCanISeeThisTarget(client, targetObj) { + noKeyPerms("*") + return + } + batchId := rb.StartNestedBatch("metadata") defer rb.EndNestedBatch(batchId) for _, key := range msg.Params[2:] { if metadataKeyIsEvil(key) { - rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", key, client.t("Invalid key name")) + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name")) continue } - val, ok := t.GetMetadata(key) + val, ok := targetObj.GetMetadata(key) if !ok { rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key is not set")) continue } visibility := "*" - rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, visibility, val) + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, visibility, val) } case "list": - values := t.ListMetadata() - - batchId := rb.StartNestedBatch("metadata") - defer rb.EndNestedBatch(batchId) - - for key, val := range values { - visibility := "*" - - rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, visibility, val) - } + playMetadataList(rb, client.Nick(), target, targetObj.ListMetadata()) case "clear": - if !metadataCanIEditThisTarget(client, target) { - invalidTarget() + if !metadataCanIEditThisTarget(client, targetObj) { + noKeyPerms("*") return } - values := t.ClearMetadata() + values := targetObj.ClearMetadata() batchId := rb.StartNestedBatch("metadata") defer rb.EndNestedBatch(batchId) for key, val := range values { visibility := "*" - rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, visibility, val) + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, visibility, val) } case "sub": keys := msg.Params[2:] - server.logger.Debug("metadata", client.nick, "has subscrumbled to", strings.Join(keys, ", ")) + for _, key := range keys { + if metadataKeyIsEvil(key) { + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name")) + return + } + } added, err := rb.session.SubscribeTo(keys...) if err == errMetadataTooManySubs { bad := keys[len(added)] // get the key that broke the camel's back - rb.Add(nil, server.name, "FAIL", "METADATA", "TOO_MANY_SUBS", bad, client.t("Too many subscriptions")) + rb.Add(nil, server.name, "FAIL", "METADATA", "TOO_MANY_SUBS", utils.SafeErrorParam(bad), client.t("Too many subscriptions")) } lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBOK) - len(client.Nick()) - 10 @@ -3258,7 +3262,6 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res case "unsub": keys := msg.Params[2:] - server.logger.Debug("metadata", client.nick, "has UNsubscrumbled to", strings.Join(keys, ", ")) removed := rb.session.UnsubscribeFrom(keys...) lineLength := MaxLineLen - len(server.name) - len(RPL_METADATAUNSUBOK) - len(client.Nick()) - 10 @@ -3288,12 +3291,22 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res } default: - rb.Add(nil, server.name, "FAIL", "METADATA", "SUBCOMMAND_INVALID", msg.Params[1], client.t("Invalid subcommand")) + rb.Add(nil, server.name, "FAIL", "METADATA", "SUBCOMMAND_INVALID", utils.SafeErrorParam(msg.Params[1]), client.t("Invalid subcommand")) } return } +func playMetadataList(rb *ResponseBuffer, nick, target string, values map[string]string) { + batchId := rb.StartNestedBatch("metadata") + defer rb.EndNestedBatch(batchId) + + for key, val := range values { + visibility := "*" + rb.Add(nil, rb.session.client.server.name, RPL_KEYVALUE, nick, target, key, visibility, val) + } +} + // REHASH func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { nick := client.Nick() diff --git a/irc/help.go b/irc/help.go index d516c43e..55aef6cf 100644 --- a/irc/help.go +++ b/irc/help.go @@ -342,7 +342,7 @@ specification.`, }, "metadata": { text: `METADATA [...] - + Retrieve and meddle with metadata for the given target. Have a look at https://ircv3.net/specs/extensions/metadata for interesting technical information.`, }, diff --git a/irc/metadata.go b/irc/metadata.go index d7094065..b19962c0 100644 --- a/irc/metadata.go +++ b/irc/metadata.go @@ -2,12 +2,17 @@ package irc import ( "errors" + "iter" + "maps" "regexp" - "strings" "github.com/ergochat/ergo/irc/caps" "github.com/ergochat/ergo/irc/modes" - "github.com/ergochat/ergo/irc/utils" +) + +const ( + // metadata key + value need to be relayable on a single IRC RPL_KEYVALUE line + maxCombinedMetadataLenBytes = 350 ) var ( @@ -16,42 +21,39 @@ var ( ) type MetadataHaver = interface { - SetMetadata(key string, value string) + SetMetadata(key string, value string, limit int) (updated bool, err error) GetMetadata(key string) (string, bool) - DeleteMetadata(key string) + DeleteMetadata(key string) (updated bool) ListMetadata() map[string]string ClearMetadata() map[string]string CountMetadata() int } -func notifySubscribers(server *Server, session *Session, target string, key string, value string) { - var notify utils.HashSet[*Session] = make(utils.HashSet[*Session]) - targetChannel := server.channels.Get(target) - targetClient := server.clients.Get(target) +func notifySubscribers(server *Server, session *Session, targetObj MetadataHaver, targetName, key, value string) { + var recipientSessions iter.Seq[*Session] - if targetClient != nil { - notify = targetClient.FriendsMonitors(caps.Metadata) - // notify clients about changes regarding themselves - for _, s := range targetClient.Sessions() { - notify.Add(s) - } - } - if targetChannel != nil { - members := targetChannel.Members() - for _, m := range members { - for _, s := range m.Sessions() { - if s.capabilities.Has(caps.Metadata) { - notify.Add(s) - } - } + switch target := targetObj.(type) { + case *Client: + // TODO this case is expensive and might warrant rate-limiting + friends := target.FriendsMonitors(caps.Metadata) + // broadcast metadata update to other connected sessions + for _, s := range target.Sessions() { + friends.Add(s) } + recipientSessions = maps.Keys(friends) + case *Channel: + recipientSessions = target.sessionsWithCaps(caps.Metadata) + default: + return // impossible } - // don't notify the session that made the change - notify.Remove(session) + broadcastMetadataUpdate(server, recipientSessions, session, targetName, key, value) +} - for s := range notify { - if !s.isSubscribedTo(key) { +func broadcastMetadataUpdate(server *Server, sessions iter.Seq[*Session], originator *Session, target, key, value string) { + for s := range sessions { + // don't notify the session that made the change + if s == originator || !s.isSubscribedTo(key) { continue } @@ -64,42 +66,38 @@ func notifySubscribers(server *Server, session *Session, target string, key stri } func syncClientMetadata(server *Server, rb *ResponseBuffer, target *Client) { - if len(rb.session.MetadataSubscriptions()) == 0 { - return - } - batchId := rb.StartNestedBatch("metadata") defer rb.EndNestedBatch(batchId) + subs := rb.session.MetadataSubscriptions() values := target.ListMetadata() for k, v := range values { - if rb.session.isSubscribedTo(k) { + if subs.Has(k) { visibility := "*" rb.Add(nil, server.name, "METADATA", target.Nick(), k, visibility, v) } } } -func syncChannelMetadata(server *Server, rb *ResponseBuffer, target *Channel) { - if len(rb.session.MetadataSubscriptions()) == 0 { - return - } - +func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) { batchId := rb.StartNestedBatch("metadata") defer rb.EndNestedBatch(batchId) - values := target.ListMetadata() + subs := rb.session.MetadataSubscriptions() + chname := channel.Name() + + values := channel.ListMetadata() for k, v := range values { - if rb.session.isSubscribedTo(k) { + if subs.Has(k) { visibility := "*" - rb.Add(nil, server.name, "METADATA", target.Name(), k, visibility, v) + rb.Add(nil, server.name, "METADATA", chname, k, visibility, v) } } - for _, client := range target.Members() { + for _, client := range channel.Members() { values := client.ListMetadata() for k, v := range values { - if rb.session.isSubscribedTo(k) { + if subs.Has(k) { visibility := "*" rb.Add(nil, server.name, "METADATA", client.Nick(), k, visibility, v) } @@ -110,55 +108,34 @@ func syncChannelMetadata(server *Server, rb *ResponseBuffer, target *Channel) { var metadataEvilCharsRegexp = regexp.MustCompile("[^A-Za-z0-9_./:-]+") func metadataKeyIsEvil(key string) bool { - key = strings.TrimSpace(key) // just in case - return len(key) == 0 || // key needs to contain stuff key[0] == ':' || // key can't start with a colon metadataEvilCharsRegexp.MatchString(key) // key can't contain the stuff it can't contain } -func metadataCanIEditThisKey(client *Client, target string, _ string) bool { - if !metadataCanIEditThisTarget(client, target) { // you can't edit keys on targets you can't edit. - return false - } - - // todo: we don't actually do anything regarding visibility yet so there's not much to do here - - return true +func metadataCanIEditThisKey(client *Client, targetObj MetadataHaver, key string) bool { + // no key-specific logic as yet + return metadataCanIEditThisTarget(client, targetObj) } -func metadataCanIEditThisTarget(client *Client, target string) bool { - if !metadataCanISeeThisTarget(client, target) { // you can't edit what you can't see. a wise man told me this once - return false +func metadataCanIEditThisTarget(client *Client, targetObj MetadataHaver) bool { + switch target := targetObj.(type) { + case *Client: + return client == target || client.HasRoleCapabs("metadata") + case *Channel: + return target.ClientIsAtLeast(client, modes.Operator) || client.HasRoleCapabs("metadata") + default: + return false // impossible } - - if client.HasRoleCapabs("sajoin") { // sajoin opers can do whatever they want - return true - } - - if target == client.Nick() { // your right to swing your fist ends where my nose begins - return true - } - - // if you're a channel operator, knock yourself out - channel := client.server.channels.Get(target) - if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) { - return true - } - - return false } -func metadataCanISeeThisTarget(client *Client, target string) bool { - if client.HasRoleCapabs("sajoin") { // sajoin opers can do whatever they want +func metadataCanISeeThisTarget(client *Client, targetObj MetadataHaver) bool { + switch target := targetObj.(type) { + case *Client: return true + case *Channel: + return target.hasClient(client) || client.HasRoleCapabs("metadata") + default: + return false // impossible } - - // check if the user is in the channel - channel := client.server.channels.Get(target) - if channel != nil && !channel.hasClient(client) { - return false - } - - return true } diff --git a/irc/server.go b/irc/server.go index 5974f633..4b23fe8a 100644 --- a/irc/server.go +++ b/irc/server.go @@ -496,6 +496,9 @@ func (server *Server) playRegistrationBurst(session *Session) { if !(rb.session.capabilities.Has(caps.ExtendedISupport) && rb.session.isupportSentPrereg) { server.RplISupport(c, rb) } + if session.capabilities.Has(caps.Metadata) && c.AlwaysOn() { + playMetadataList(rb, d.nick, d.nick, c.ListMetadata()) + } if d.account != "" && session.capabilities.Has(caps.Persistence) { reportPersistenceStatus(c, rb, false) } diff --git a/irctest b/irctest index 15c847eb..a3c2110b 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit 15c847ebb19b1c434347236296cbc4d34ba7c4b2 +Subproject commit a3c2110be24ad53ec4babe0facdc25aa2151d3e5 diff --git a/traditional.yaml b/traditional.yaml index 55f16ea7..4b29ec34 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -695,6 +695,7 @@ oper-classes: - "history" # modify or delete history messages - "defcon" # use the DEFCON command (restrict server capabilities) - "massmessage" # message all users on the server + - "metadata" # modify arbitrary metadata on channels and users # ircd operators opers: @@ -1058,12 +1059,14 @@ history: # e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details. allow-environment-overrides: true -# experimental IRC metadata support for setting key/value data on channels and nicknames. +# metadata support for setting key/value data on channels and nicknames. metadata: - # can clients use the metadata command? + # can clients store metadata? enabled: true # how many keys can a client subscribe to? - max-subs: 1000 + max-subs: 100 + # how many keys can be stored per entity? + max-keys: 100 # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications. From 23844d4103132de1eba539ea1fe8c67a2ce0fcc4 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 19 Jun 2025 13:22:07 -0400 Subject: [PATCH 14/35] update documentation for globalUtf8EnforcementSetting --- irc/strings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irc/strings.go b/irc/strings.go index fb5c3da0..03e13c41 100644 --- a/irc/strings.go +++ b/irc/strings.go @@ -73,7 +73,7 @@ var globalCasemappingSetting Casemapping = CasemappingPRECIS // XXX analogous unsynchronized global variable controlling utf8 validation // if this is off, you get the traditional IRC behavior (relaying any valid RFC1459 -// octets) and invalid utf8 messages are silently dropped for websocket clients only. +// octets), and websocket listeners are disabled. // if this is on, invalid utf8 inputs get a FAIL reply. var globalUtf8EnforcementSetting bool From 8f18454e8f977cd83b3d2213bf554859104a99ec Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 19 Jun 2025 13:25:34 -0400 Subject: [PATCH 15/35] fix help string for HISTORY --- irc/help.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/irc/help.go b/irc/help.go index 55aef6cf..ee1fb762 100644 --- a/irc/help.go +++ b/irc/help.go @@ -238,11 +238,10 @@ Get an explanation of , or "index" for a list of help topics.`, "history": { text: `HISTORY [limit] -Replay message history. can be a channel name, "me" to replay direct -message history, or a nickname to replay another client's direct message -history (they must be logged into the same account as you). [limit] can be -either an integer (the maximum number of messages to replay), or a time -duration like 10m or 1h (the time window within which to replay messages).`, +Replay message history. can be a channel name or a nickname you have +direct message history with. [limit] can be either an integer (the maximum +number of messages to replay), or a time duration like 10m or 1h (the time +window within which to replay messages).`, }, "info": { text: `INFO From a5e435a26b67191c8585e958052f1614005530f7 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sat, 21 Jun 2025 22:04:33 -0400 Subject: [PATCH 16/35] bump irctest --- irctest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest b/irctest index a3c2110b..b32cd08f 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit a3c2110be24ad53ec4babe0facdc25aa2151d3e5 +Subproject commit b32cd08f4ec18b846ea4d9fbad2fef932137d4c9 From 73e51333adb41062b0bf853599e464430fb1bff5 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 22 Jun 2025 13:57:46 -0400 Subject: [PATCH 17/35] implement metadata before-connect (#2281) * metadata spec update: disallow colon entirely * refactor key validation * implement metadata before-connect * play the metadata in reg burst to all clients with the cap * bump irctest * remove all case normalization for keys From spec discussion, we will most likely either require keys to be lowercase, or else treat them as case-opaque, similar to message tag keys. --- irc/client.go | 35 +++++++++- irc/commands.go | 5 +- irc/config.go | 4 +- irc/getters.go | 28 +++++++- irc/handlers.go | 155 +++++++++++++++++++++++++++++++------------ irc/metadata.go | 21 ++++-- irc/metadata_test.go | 3 +- irc/numerics.go | 1 + irc/server.go | 4 +- irctest | 2 +- 10 files changed, 199 insertions(+), 59 deletions(-) diff --git a/irc/client.go b/irc/client.go index 25aa72cf..16837dec 100644 --- a/irc/client.go +++ b/irc/client.go @@ -217,6 +217,7 @@ type Session struct { webPushEndpoint string // goroutine-local: web push endpoint registered by the current session metadataSubscriptions utils.HashSet[string] + metadataPreregVals map[string]string } // MultilineBatch tracks the state of a client-to-server multiline batch. @@ -677,7 +678,7 @@ func (client *Client) run(session *Session) { isReattach := client.Registered() if isReattach { client.Touch(session) - client.playReattachMessages(session) + client.performReattach(session) } firstLine := !isReattach @@ -777,7 +778,9 @@ func (client *Client) run(session *Session) { } } -func (client *Client) playReattachMessages(session *Session) { +func (client *Client) performReattach(session *Session) { + client.applyPreregMetadata(session) + client.server.playRegistrationBurst(session) hasHistoryCaps := session.HasHistoryCaps() for _, channel := range session.client.Channels() { @@ -801,6 +804,34 @@ func (client *Client) playReattachMessages(session *Session) { session.autoreplayMissedSince = time.Time{} } +func (client *Client) applyPreregMetadata(session *Session) { + if session.metadataPreregVals == nil { + return + } + + defer func() { + session.metadataPreregVals = nil + }() + + updates := client.UpdateMetadataFromPrereg(session.metadataPreregVals, client.server.Config().Metadata.MaxKeys) + if len(updates) == 0 { + return + } + + // TODO this is expensive + friends := client.FriendsMonitors(caps.Metadata) + for _, s := range client.Sessions() { + if s != session { + friends.Add(s) + } + } + + target := client.Nick() + for k, v := range updates { + broadcastMetadataUpdate(client.server, maps.Keys(friends), session, target, k, v) + } +} + // // idle, quit, timers and timeouts // diff --git a/irc/commands.go b/irc/commands.go index d5387306..b8be684d 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -210,8 +210,9 @@ func init() { minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS }, "METADATA": { - handler: metadataHandler, - minParams: 2, + handler: metadataHandler, + minParams: 2, + usablePreReg: true, }, "MODE": { handler: modeHandler, diff --git a/irc/config.go b/irc/config.go index 2ef14681..6939cf5c 100644 --- a/irc/config.go +++ b/irc/config.go @@ -724,7 +724,6 @@ type Config struct { } Metadata struct { - // BeforeConnect int `yaml:"before-connect"` todo: this Enabled bool MaxSubs int `yaml:"max-subs"` MaxKeys int `yaml:"max-keys"` @@ -1648,7 +1647,8 @@ func LoadConfig(filename string) (config *Config, err error) { if !config.Metadata.Enabled { config.Server.supportedCaps.Disable(caps.Metadata) } else { - var metadataValues []string + metadataValues := make([]string, 0, 4) + metadataValues = append(metadataValues, "before-connect") // these are required for normal operation, so set sane defaults: if config.Metadata.MaxSubs == 0 { config.Metadata.MaxSubs = 10 diff --git a/irc/getters.go b/irc/getters.go index 3bba6115..75896811 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -839,6 +839,8 @@ func (session *Session) isSubscribedTo(key string) bool { } func (session *Session) SubscribeTo(keys ...string) ([]string, error) { + maxSubs := session.client.server.Config().Metadata.MaxSubs + session.client.stateMutex.Lock() defer session.client.stateMutex.Unlock() @@ -848,8 +850,6 @@ func (session *Session) SubscribeTo(keys ...string) ([]string, error) { var added []string - maxSubs := session.client.server.Config().Metadata.MaxSubs - for _, k := range keys { if !session.metadataSubscriptions.Has(k) { if len(session.metadataSubscriptions) > maxSubs { @@ -980,6 +980,30 @@ func (client *Client) SetMetadata(key string, value string, limit int) (updated return updated, nil } +func (client *Client) UpdateMetadataFromPrereg(preregData map[string]string, limit int) (updates map[string]string) { + updates = make(map[string]string, len(preregData)) + + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + + if client.metadata == nil { + client.metadata = make(map[string]string) + } + + for k, v := range preregData { + // do not overwrite any existing keys + _, ok := client.metadata[k] + if ok { + continue + } + if len(client.metadata) >= limit { + return // we know this is a new key + } + client.metadata[k] = v + } + return +} + func (client *Client) ListMetadata() map[string]string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() diff --git a/irc/handlers.go b/irc/handlers.go index e2cdf94d..e7c6f614 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -20,7 +20,6 @@ import ( "strconv" "strings" "time" - "unicode/utf8" "github.com/ergochat/irc-go/ircfmt" "github.com/ergochat/irc-go/ircmsg" @@ -3102,15 +3101,39 @@ func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res // METADATA [...] func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) { - target := msg.Params[0] - config := server.Config() if !config.Metadata.Enabled { - rb.Add(nil, server.name, "FAIL", "METADATA", "FORBIDDEN", target, "Metadata is disabled on this server") + rb.Add(nil, server.name, "FAIL", "METADATA", "FORBIDDEN", utils.SafeErrorParam(msg.Params[0]), "Metadata is disabled on this server") return } subcommand := strings.ToLower(msg.Params[1]) + needsKey := subcommand == "set" || subcommand == "get" || subcommand == "sub" || subcommand == "unsub" + if needsKey && len(msg.Params) < 3 { + rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), msg.Command, client.t("Not enough parameters")) + return + } + + switch subcommand { + case "sub", "unsub", "subs": + // these are session-local and function the same whether or not the client is registered + return metadataSubsHandler(client, subcommand, msg.Params, rb) + case "get", "set", "list", "clear", "sync": + if client.registered { + return metadataRegisteredHandler(client, config, subcommand, msg.Params, rb) + } else { + return metadataUnregisteredHandler(client, config, subcommand, msg.Params, rb) + } + default: + rb.Add(nil, server.name, "FAIL", "METADATA", "SUBCOMMAND_INVALID", utils.SafeErrorParam(msg.Params[1]), client.t("Invalid subcommand")) + return + } +} + +// metadataRegisteredHandler handles metadata-modifying commands from registered clients +func metadataRegisteredHandler(client *Client, config *Config, subcommand string, params []string, rb *ResponseBuffer) (exiting bool) { + server := client.server + target := params[0] noKeyPerms := func(key string) { rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NO_PERMISSION", target, key, client.t("You do not have permission to perform this action")) @@ -3141,15 +3164,9 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res return } - needsKey := subcommand == "set" || subcommand == "get" || subcommand == "sub" || subcommand == "unsub" - if needsKey && len(msg.Params) <= 2 { - rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), msg.Command, client.t("Not enough parameters")) - return - } - switch subcommand { case "set": - key := strings.ToLower(msg.Params[2]) + key := params[2] if metadataKeyIsEvil(key) { rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name")) return @@ -3160,18 +3177,12 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res return } - if len(msg.Params) > 3 { - value := msg.Params[3] + if len(params) > 3 { + value := params[3] - if !globalUtf8EnforcementSetting && !utf8.ValidString(value) { - rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t("METADATA values must be UTF-8")) - return - } - - if len(key)+len(value) > maxCombinedMetadataLenBytes || - (config.Metadata.MaxValueBytes > 0 && len(value) > config.Metadata.MaxValueBytes) { - - rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t("Value is too long")) + config := client.server.Config() + if failMsg := metadataValueIsEvil(config, key, value); failMsg != "" { + rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t(failMsg)) return } @@ -3203,7 +3214,7 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res batchId := rb.StartNestedBatch("metadata") defer rb.EndNestedBatch(batchId) - for _, key := range msg.Params[2:] { + for _, key := range params[2:] { if metadataKeyIsEvil(key) { rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name")) continue @@ -3230,16 +3241,84 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res values := targetObj.ClearMetadata() - batchId := rb.StartNestedBatch("metadata") - defer rb.EndNestedBatch(batchId) + playMetadataList(rb, client.Nick(), target, values) - for key, val := range values { - visibility := "*" - rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, visibility, val) + case "sync": + if targetChannel != nil { + syncChannelMetadata(server, rb, targetChannel) } + if targetClient != nil { + syncClientMetadata(server, rb, targetClient) + } + } + return +} + +// metadataUnregisteredHandler handles metadata-modifying commands for pre-connection-registration +// clients. these operations act on a session-local buffer; if/when the client completes registration, +// they are applied to the final Client object (possibly a different client if there was a reattach) +// on a best-effort basis. +func metadataUnregisteredHandler(client *Client, config *Config, subcommand string, params []string, rb *ResponseBuffer) (exiting bool) { + server := client.server + if params[0] != "*" { + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NO_PERMISSION", utils.SafeErrorParam(params[0]), "*", client.t("You can only modify your own metadata before completing connection registration")) + return + } + + switch subcommand { + case "set": + if rb.session.metadataPreregVals == nil { + rb.session.metadataPreregVals = make(map[string]string) + } + key := params[2] + if metadataKeyIsEvil(key) { + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name")) + return + } + if len(params) >= 4 { + value := params[3] + // enforce a sane limit on prereg keys. we don't need to enforce the exact limit, + // that will be done when applying the buffer after registration + if len(rb.session.metadataPreregVals) > config.Metadata.MaxKeys { + rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", client.t("Too many metadata keys")) + return + } + if failMsg := metadataValueIsEvil(config, key, value); failMsg != "" { + rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t(failMsg)) + return + } + rb.session.metadataPreregVals[key] = value + rb.Add(nil, server.name, RPL_KEYVALUE, "*", "*", key, "*", value) + } else { + // unset + _, present := rb.session.metadataPreregVals[key] + if present { + delete(rb.session.metadataPreregVals, key) + rb.Add(nil, server.name, RPL_KEYNOTSET, "*", "*", key, client.t("Key deleted")) + } else { + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NOT_SET", utils.SafeErrorParam(key), client.t("Metadata key not set")) + } + } + case "list": + playMetadataList(rb, "*", "*", rb.session.metadataPreregVals) + case "clear": + oldMetadata := rb.session.metadataPreregVals + rb.session.metadataPreregVals = nil + playMetadataList(rb, "*", "*", oldMetadata) + case "sync": + rb.Add(nil, server.name, RPL_METADATASYNCLATER, "*", utils.SafeErrorParam(params[1]), "60") // lol + } + return false +} + +// metadataSubsHandler handles subscription-related commands; +// these are handled the same whether the client is registered or not +func metadataSubsHandler(client *Client, subcommand string, params []string, rb *ResponseBuffer) (exiting bool) { + server := client.server + switch subcommand { case "sub": - keys := msg.Params[2:] + keys := params[2:] for _, key := range keys { if metadataKeyIsEvil(key) { rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name")) @@ -3261,7 +3340,7 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res } case "unsub": - keys := msg.Params[2:] + keys := params[2:] removed := rb.session.UnsubscribeFrom(keys...) lineLength := MaxLineLen - len(server.name) - len(RPL_METADATAUNSUBOK) - len(client.Nick()) - 10 @@ -3272,7 +3351,7 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res } case "subs": - lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBS) - len(client.Nick()) - 10 // for safety + lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBS) - len(client.Nick()) - 10 subs := rb.session.MetadataSubscriptions() @@ -3281,20 +3360,8 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res params := append([]string{client.Nick()}, line...) rb.Add(nil, server.name, RPL_METADATASUBS, params...) } - - case "sync": - if targetChannel != nil { - syncChannelMetadata(server, rb, targetChannel) - } - if targetClient != nil { - syncClientMetadata(server, rb, targetClient) - } - - default: - rb.Add(nil, server.name, "FAIL", "METADATA", "SUBCOMMAND_INVALID", utils.SafeErrorParam(msg.Params[1]), client.t("Invalid subcommand")) } - - return + return false } func playMetadataList(rb *ResponseBuffer, nick, target string, values map[string]string) { diff --git a/irc/metadata.go b/irc/metadata.go index b19962c0..a254f280 100644 --- a/irc/metadata.go +++ b/irc/metadata.go @@ -5,6 +5,7 @@ import ( "iter" "maps" "regexp" + "unicode/utf8" "github.com/ergochat/ergo/irc/caps" "github.com/ergochat/ergo/irc/modes" @@ -105,12 +106,24 @@ func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) { } } -var metadataEvilCharsRegexp = regexp.MustCompile("[^A-Za-z0-9_./:-]+") +var validMetadataKeyRegexp = regexp.MustCompile("^[A-Za-z0-9_./-]+$") func metadataKeyIsEvil(key string) bool { - return len(key) == 0 || // key needs to contain stuff - key[0] == ':' || // key can't start with a colon - metadataEvilCharsRegexp.MatchString(key) // key can't contain the stuff it can't contain + return !validMetadataKeyRegexp.MatchString(key) +} + +func metadataValueIsEvil(config *Config, key, value string) (failMsg string) { + if !globalUtf8EnforcementSetting && !utf8.ValidString(value) { + return `METADATA values must be UTF-8` + } + + if len(key)+len(value) > maxCombinedMetadataLenBytes || + (config.Metadata.MaxValueBytes > 0 && len(value) > config.Metadata.MaxValueBytes) { + + return `Value is too long` + } + + return "" // success } func metadataCanIEditThisKey(client *Client, targetObj MetadataHaver, key string) bool { diff --git a/irc/metadata_test.go b/irc/metadata_test.go index 93942abb..91de213a 100644 --- a/irc/metadata_test.go +++ b/irc/metadata_test.go @@ -8,9 +8,10 @@ func TestKeyCheck(t *testing.T) { isEvil bool }{ {"ImNormal", false}, + {"", true}, {":imevil", true}, {"key£with$not%allowed^chars", true}, - {"key.that:s_completely/normal-and.fine", false}, + {"key.thats_completely/normal-and.fine", false}, } for _, c := range cases { diff --git a/irc/numerics.go b/irc/numerics.go index bf5a6d60..339080e1 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -188,6 +188,7 @@ const ( RPL_METADATASUBOK = "770" RPL_METADATAUNSUBOK = "771" RPL_METADATASUBS = "772" + RPL_METADATASYNCLATER = "774" RPL_LOGGEDIN = "900" RPL_LOGGEDOUT = "901" ERR_NICKLOCKED = "902" diff --git a/irc/server.go b/irc/server.go index 4b23fe8a..7003a367 100644 --- a/irc/server.go +++ b/irc/server.go @@ -428,6 +428,8 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) { c.SetMode(defaultMode, true) } + c.applyPreregMetadata(session) + // this is not a reattach, so if the client is always-on, this is the first time // the Client object was created during the current server uptime. mark dirty in // order to persist the realname and the user modes: @@ -496,7 +498,7 @@ func (server *Server) playRegistrationBurst(session *Session) { if !(rb.session.capabilities.Has(caps.ExtendedISupport) && rb.session.isupportSentPrereg) { server.RplISupport(c, rb) } - if session.capabilities.Has(caps.Metadata) && c.AlwaysOn() { + if session.capabilities.Has(caps.Metadata) { playMetadataList(rb, d.nick, d.nick, c.ListMetadata()) } if d.account != "" && session.capabilities.Has(caps.Persistence) { diff --git a/irctest b/irctest index b32cd08f..eff56655 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit b32cd08f4ec18b846ea4d9fbad2fef932137d4c9 +Subproject commit eff56655290b255f099c43d81f28f2d13e667e61 From cca400de736aa99ca6472be9c1fdc90724600daf Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 22 Jun 2025 13:59:36 -0400 Subject: [PATCH 18/35] fix: actually broadcast prereg updates to subscribers Missed in #2281, needs a test presumably :-) --- irc/getters.go | 1 + 1 file changed, 1 insertion(+) diff --git a/irc/getters.go b/irc/getters.go index 75896811..27b08d74 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -1000,6 +1000,7 @@ func (client *Client) UpdateMetadataFromPrereg(preregData map[string]string, lim return // we know this is a new key } client.metadata[k] = v + updates[k] = v } return } From 8798676ae926f979ce3ae691cbdfbb174b51d400 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 22 Jun 2025 18:59:42 -0400 Subject: [PATCH 19/35] update metadata corresponding to spec edits (#2282) * spec update: metadata keys are lowercase * add batch parameter to metadata batches * fix: connecting clients receive METADATA, not RPL_KEYVALUE * spec update: send RPL_METADATASUBS in a metadata-subs batch * move some helpers * bump irctest to forked hash This is https://github.com/progval/irctest/pull/314 but I don't want to couple the merges * fix: empty value is valid * fix: deleting a nonexistent key gets a FAIL --- irc/client.go | 2 +- irc/handlers.go | 24 +++++++++--------------- irc/metadata.go | 34 +++++++++++++++++++++++++++------- irc/metadata_test.go | 5 ++++- irc/server.go | 2 +- irctest | 2 +- 6 files changed, 43 insertions(+), 26 deletions(-) diff --git a/irc/client.go b/irc/client.go index 16837dec..d90789ea 100644 --- a/irc/client.go +++ b/irc/client.go @@ -828,7 +828,7 @@ func (client *Client) applyPreregMetadata(session *Session) { target := client.Nick() for k, v := range updates { - broadcastMetadataUpdate(client.server, maps.Keys(friends), session, target, k, v) + broadcastMetadataUpdate(client.server, maps.Keys(friends), session, target, k, v, true) } } diff --git a/irc/handlers.go b/irc/handlers.go index e7c6f614..b17d094b 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -3195,14 +3195,15 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string // echo the value to the client whether or not there was a real update rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, "*", value) if updated { - notifySubscribers(server, rb.session, targetObj, target, key, value) + notifySubscribers(server, rb.session, targetObj, target, key, value, true) } } else { if updated := targetObj.DeleteMetadata(key); updated { - notifySubscribers(server, rb.session, targetObj, target, key, "") + notifySubscribers(server, rb.session, targetObj, target, key, "", false) + rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key deleted")) + } else { + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NOT_SET", utils.SafeErrorParam(key), client.t("Metadata key not set")) } - // acknowledge to the client whether or not there was a real update - rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key deleted")) } case "get": @@ -3211,7 +3212,7 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string return } - batchId := rb.StartNestedBatch("metadata") + batchId := rb.StartNestedBatch("metadata", target) defer rb.EndNestedBatch(batchId) for _, key := range params[2:] { @@ -3355,6 +3356,9 @@ func metadataSubsHandler(client *Client, subcommand string, params []string, rb subs := rb.session.MetadataSubscriptions() + batchID := rb.StartNestedBatch("metadata-subs") + defer rb.EndNestedBatch(batchID) + chunked := utils.ChunkifyParams(maps.Keys(subs), lineLength) for _, line := range chunked { params := append([]string{client.Nick()}, line...) @@ -3364,16 +3368,6 @@ func metadataSubsHandler(client *Client, subcommand string, params []string, rb return false } -func playMetadataList(rb *ResponseBuffer, nick, target string, values map[string]string) { - batchId := rb.StartNestedBatch("metadata") - defer rb.EndNestedBatch(batchId) - - for key, val := range values { - visibility := "*" - rb.Add(nil, rb.session.client.server.name, RPL_KEYVALUE, nick, target, key, visibility, val) - } -} - // REHASH func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { nick := client.Nick() diff --git a/irc/metadata.go b/irc/metadata.go index a254f280..b5e9a5a2 100644 --- a/irc/metadata.go +++ b/irc/metadata.go @@ -30,7 +30,7 @@ type MetadataHaver = interface { CountMetadata() int } -func notifySubscribers(server *Server, session *Session, targetObj MetadataHaver, targetName, key, value string) { +func notifySubscribers(server *Server, session *Session, targetObj MetadataHaver, targetName, key, value string, set bool) { var recipientSessions iter.Seq[*Session] switch target := targetObj.(type) { @@ -48,17 +48,17 @@ func notifySubscribers(server *Server, session *Session, targetObj MetadataHaver return // impossible } - broadcastMetadataUpdate(server, recipientSessions, session, targetName, key, value) + broadcastMetadataUpdate(server, recipientSessions, session, targetName, key, value, set) } -func broadcastMetadataUpdate(server *Server, sessions iter.Seq[*Session], originator *Session, target, key, value string) { +func broadcastMetadataUpdate(server *Server, sessions iter.Seq[*Session], originator *Session, target, key, value string, set bool) { for s := range sessions { // don't notify the session that made the change if s == originator || !s.isSubscribedTo(key) { continue } - if value != "" { + if set { s.Send(nil, server.name, "METADATA", target, key, "*", value) } else { s.Send(nil, server.name, "METADATA", target, key, "*") @@ -67,7 +67,7 @@ func broadcastMetadataUpdate(server *Server, sessions iter.Seq[*Session], origin } func syncClientMetadata(server *Server, rb *ResponseBuffer, target *Client) { - batchId := rb.StartNestedBatch("metadata") + batchId := rb.StartNestedBatch("metadata", target.Nick()) defer rb.EndNestedBatch(batchId) subs := rb.session.MetadataSubscriptions() @@ -81,7 +81,7 @@ func syncClientMetadata(server *Server, rb *ResponseBuffer, target *Client) { } func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) { - batchId := rb.StartNestedBatch("metadata") + batchId := rb.StartNestedBatch("metadata", channel.Name()) defer rb.EndNestedBatch(batchId) subs := rb.session.MetadataSubscriptions() @@ -106,7 +106,27 @@ func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) { } } -var validMetadataKeyRegexp = regexp.MustCompile("^[A-Za-z0-9_./-]+$") +func playMetadataList(rb *ResponseBuffer, nick, target string, values map[string]string) { + batchId := rb.StartNestedBatch("metadata", target) + defer rb.EndNestedBatch(batchId) + + for key, val := range values { + visibility := "*" + rb.Add(nil, rb.session.client.server.name, RPL_KEYVALUE, nick, target, key, visibility, val) + } +} + +func playMetadataVerbBatch(rb *ResponseBuffer, target string, values map[string]string) { + batchId := rb.StartNestedBatch("metadata", target) + defer rb.EndNestedBatch(batchId) + + for key, val := range values { + visibility := "*" + rb.Add(nil, rb.session.client.server.name, "METADATA", target, key, visibility, val) + } +} + +var validMetadataKeyRegexp = regexp.MustCompile("^[a-z0-9_./-]+$") func metadataKeyIsEvil(key string) bool { return !validMetadataKeyRegexp.MatchString(key) diff --git a/irc/metadata_test.go b/irc/metadata_test.go index 91de213a..c90dbcec 100644 --- a/irc/metadata_test.go +++ b/irc/metadata_test.go @@ -7,9 +7,12 @@ func TestKeyCheck(t *testing.T) { input string isEvil bool }{ - {"ImNormal", false}, + {"ImNormalButIHaveCaps", true}, + {"imnormalandidonthavecaps", false}, + {"ergo.chat/vendor-extension", false}, {"", true}, {":imevil", true}, + {"im:evil", true}, {"key£with$not%allowed^chars", true}, {"key.thats_completely/normal-and.fine", false}, } diff --git a/irc/server.go b/irc/server.go index 7003a367..7aee6705 100644 --- a/irc/server.go +++ b/irc/server.go @@ -499,7 +499,7 @@ func (server *Server) playRegistrationBurst(session *Session) { server.RplISupport(c, rb) } if session.capabilities.Has(caps.Metadata) { - playMetadataList(rb, d.nick, d.nick, c.ListMetadata()) + playMetadataVerbBatch(rb, d.nick, c.ListMetadata()) } if d.account != "" && session.capabilities.Has(caps.Persistence) { reportPersistenceStatus(c, rb, false) diff --git a/irctest b/irctest index eff56655..713f81ed 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit eff56655290b255f099c43d81f28f2d13e667e61 +Subproject commit 713f81ed840ab2b733fafb23ec4e61e161e8e26d From f918e285137ba0e57ac10933a9d66bf0a5a635d3 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 26 Jun 2025 01:31:51 -0400 Subject: [PATCH 20/35] bump irctest --- irctest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest b/irctest index 713f81ed..ccdacd99 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit 713f81ed840ab2b733fafb23ec4e61e161e8e26d +Subproject commit ccdacd990a09ac3cd3591613b8d7fee4301e08c8 From f0b1f34da7f0481c5fa963570602e9dcbbc7b063 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 26 Jun 2025 22:14:19 -0400 Subject: [PATCH 21/35] refactor makefile to label individual targets phony --- Makefile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 804efeae..af4240d1 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,3 @@ -.PHONY: all install build release capdefs test smoke gofmt irctest - GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null) GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1) @@ -9,33 +7,42 @@ export CGO_ENABLED ?= 0 capdef_file = ./irc/caps/defs.go +.PHONY: all all: build +.PHONY: install install: go install -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)" +.PHONY: build build: go build -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)" +.PHONY: release release: goreleaser --skip=publish --clean +.PHONY: capdefs capdefs: python3 ./gencapdefs.py > ${capdef_file} +.PHONY: test test: python3 ./gencapdefs.py | diff - ${capdef_file} go test ./... go vet ./... ./.check-gofmt.sh +.PHONY: smoke smoke: install ergo mkcerts --conf ./default.yaml || true ergo run --conf ./default.yaml --smoke +.PHONY: gofmt gofmt: ./.check-gofmt.sh --fix +.PHONY: irctest irctest: install git submodule update --init cd irctest && make ergo From 064291e902c433268820449526fa11229ef926c0 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 12 Aug 2025 16:23:36 -0400 Subject: [PATCH 22/35] add an explicit note covering #2289 --- docs/MANUAL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 17cfcc94..59ef6cae 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -171,6 +171,7 @@ Rehashing also reloads TLS certificates and the MOTD. Some configuration setting Ergo can also be configured using environment variables, using the following technique: +1. Ensure that `allow-environment-variables` is set to `true` in the YAML config file itself (see `default.yaml` for an example) 1. Find the "path" of the config variable you want to override in the YAML file, e.g., `server.websockets.allowed-origins` 1. Convert each path component from "kebab case" to "screaming snake case", e.g., `SERVER`, `WEBSOCKETS`, and `ALLOWED_ORIGINS`. 1. Prepend `ERGO` to the components, then join them all together using `__` as the separator, e.g., `ERGO__SERVER__WEBSOCKETS__ALLOWED_ORIGINS`. From 8913bd7fa948f35c0b6e6054aac616abbd2aa40b Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 18 Aug 2025 00:07:11 -0400 Subject: [PATCH 23/35] upgrade to go 1.25 --- .github/workflows/build.yml | 2 +- Dockerfile | 2 +- go.mod | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 923fd90f..d8772310 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.24" + go-version: "1.25" - name: "install python3-pytest" run: "sudo apt install -y python3-pytest" - name: "make install" diff --git a/Dockerfile b/Dockerfile index ffffc7a6..4c3788b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ## build ergo binary -FROM docker.io/golang:1.24-alpine AS build-env +FROM docker.io/golang:1.25-alpine AS build-env RUN apk upgrade -U --force-refresh --no-cache && apk add --no-cache --purge --clean-protected -l -u make git diff --git a/go.mod b/go.mod index ab661164..a577c776 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ergochat/ergo -go 1.24 +go 1.25 require ( code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 From 6b8265fb17f61a560bbce4dd4ec8556f4a2a5682 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 26 Oct 2025 00:55:30 -0400 Subject: [PATCH 24/35] fix validation of web push URLs They are validated by test message, but it would have been possible to add an http url. If an http url was added, it's still possible to remove it via NS PUSH DELETE. --- irc/handlers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/irc/handlers.go b/irc/handlers.go index b17d094b..c56ce87f 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -3934,6 +3934,7 @@ func webpushHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp if err := webpush.SanityCheckWebPushEndpoint(endpoint); err != nil { rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", subcommand, client.t("Invalid web push URL")) + return false } switch subcommand { From 6fdac13ad4429047f232da4c84f085e9dfbed054 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 9 Nov 2025 19:32:25 -0500 Subject: [PATCH 25/35] changes to OPER command * Impose a throttle on OPER attempts regardless of whether they caused a password check. * Never disconnect the client on a failed attempt, even if there was a password check. * Change error numeric to ERR_NOOPERHOST * Explicit information about the failure in the server log (copying Insp) Fixes #2296. --- default.yaml | 4 ++++ docs/MANUAL.md | 2 +- irc/client.go | 2 ++ irc/config.go | 5 +++++ irc/handlers.go | 34 +++++++++++++++++++++++++++------- traditional.yaml | 4 ++++ 6 files changed, 43 insertions(+), 8 deletions(-) diff --git a/default.yaml b/default.yaml index a8d12236..34ef0f58 100644 --- a/default.yaml +++ b/default.yaml @@ -358,6 +358,10 @@ server: secure-nets: # - "10.0.0.0/8" + # allow attempts to OPER with a password at most this often. default to + # 10 seconds when unset. + oper-throttle: 10s + # Ergo will write files to disk under certain circumstances, e.g., # CPU profiling or data export. by default, these files will be written # to the working directory. set this to customize: diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 59ef6cae..cfa3ddc9 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -527,7 +527,7 @@ If your client or bot is failing to connect to Ergo, here are some things to che ## Why can't I oper? -If you try to oper unsuccessfully, Ergo will disconnect you from the network. If you're unable to oper, here are some things to double-check: +If your `OPER` command fails, check your server logs for more information. Here are some general issues to double-check: 1. Did you correctly generate the hashed password with `ergo genpasswd`? 1. Did you add the password hash to the correct config file, then save the file? diff --git a/irc/client.go b/irc/client.go index d90789ea..ed2f765c 100644 --- a/irc/client.go +++ b/irc/client.go @@ -189,6 +189,8 @@ type Session struct { fakelag Fakelag deferredFakelagCount int + lastOperAttempt time.Time + certfp string peerCerts []*x509.Certificate sasl saslStatus diff --git a/irc/config.go b/irc/config.go index 6939cf5c..592d4d7f 100644 --- a/irc/config.go +++ b/irc/config.go @@ -599,6 +599,7 @@ type Config struct { Cloaks cloaks.CloakConfig `yaml:"ip-cloaking"` SecureNetDefs []string `yaml:"secure-nets"` secureNets []net.IPNet + OperThrottle time.Duration `yaml:"oper-throttle"` supportedCaps *caps.Set supportedCapsWithoutSTS *caps.Set capValues caps.Values @@ -1480,6 +1481,10 @@ func LoadConfig(filename string) (config *Config, err error) { config.Server.supportedCaps.Disable(caps.SASL) } + if config.Server.OperThrottle <= 0 { + config.Server.OperThrottle = 10 * time.Second + } + if err := config.Accounts.OAuth2.Postprocess(); err != nil { return nil, err } diff --git a/irc/handlers.go b/irc/handlers.go index c56ce87f..7b455028 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2545,8 +2545,19 @@ func operHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons return false } + config := server.Config() + now := time.Now() + nextAllowableAttempt := rb.session.lastOperAttempt.Add(config.Server.OperThrottle) + if now.Before(nextAllowableAttempt) { + timeLeft := nextAllowableAttempt.Sub(now).Round(time.Millisecond) + rb.Add(nil, server.name, ERR_NOOPERHOST, client.Nick(), fmt.Sprintf(client.t("You must wait %v before issuing OPER again"), timeLeft)) + return false + } + + rb.session.lastOperAttempt = now + // must pass at least one check, and all enabled checks - var checkPassed, checkFailed, passwordFailed bool + var checkPassed, checkFailed, certFailed, passwordFailed bool oper := server.GetOperator(msg.Params[0]) if oper != nil { if oper.Certfp != "" { @@ -2554,11 +2565,13 @@ func operHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons checkPassed = true } else { checkFailed = true + certFailed = true } } if !checkFailed && oper.Pass != nil { if len(msg.Params) == 1 { checkFailed = true + passwordFailed = true } else if bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil { checkFailed = true passwordFailed = true @@ -2569,14 +2582,21 @@ func operHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons } if !checkPassed || checkFailed { - rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect")) - // #951: only disconnect them if we actually tried to check a password for them - if passwordFailed { - client.Quit(client.t("Password incorrect"), rb.session) - return true + rb.Add(nil, server.name, ERR_NOOPERHOST, client.Nick(), client.t("OPER failed; check the server logs for details.")) + + // hopefully not too spammy given the throttling: + if oper == nil { + server.logger.Info("opers", "OPER failed with invalid oper name", msg.Params[0]) + } else if certFailed { + server.logger.Info("opers", "OPER attempt for", msg.Params[0], "failed with invalid certfp") + } else if passwordFailed { + server.logger.Info("opers", "OPER attempt for", msg.Params[0], "failed with invalid password") } else { - return false + // should not be possible given config validation + server.logger.Info("opers", "OPER attempt for", msg.Params[0], "failed with invalid config") } + + return false } if oper != nil { diff --git a/traditional.yaml b/traditional.yaml index 4b29ec34..1e73a983 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -330,6 +330,10 @@ server: secure-nets: # - "10.0.0.0/8" + # allow attempts to OPER with a password at most this often. default to + # 10 seconds when unset. + oper-throttle: 10s + # Ergo will write files to disk under certain circumstances, e.g., # CPU profiling or data export. by default, these files will be written # to the working directory. set this to customize: From ed841ee62a21bbe96b9089983658ed8217051507 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 9 Nov 2025 21:11:04 -0500 Subject: [PATCH 26/35] configurable idle timeouts Fixes #2292 --- default.yaml | 11 +++++++++++ irc/client.go | 28 +++++++++++++--------------- irc/config.go | 30 ++++++++++++++++++++++++++++-- irc/server.go | 5 +++++ traditional.yaml | 11 +++++++++++ 5 files changed, 68 insertions(+), 17 deletions(-) diff --git a/default.yaml b/default.yaml index a8d12236..5d1dfee0 100644 --- a/default.yaml +++ b/default.yaml @@ -180,6 +180,17 @@ server: # if this is true, the motd is escaped using formatting codes like $c, $b, and $i motd-formatting: true + # idle timeouts for inactive clients + idle-timeouts: + # give the client this long to complete connection registration (i.e. the initial + # IRC handshake, including capability negotiation and SASL) + registration: 60s + # if the client hasn't sent anything for this long, send them a PING + ping: 1m30s + # if the client hasn't sent anything for this long (including the PONG to the + # above PING), disconnect them + disconnect: 2m30s + # relaying using the RELAYMSG command relaymsg: # is relaymsg enabled at all? diff --git a/irc/client.go b/irc/client.go index d90789ea..be98a5e9 100644 --- a/irc/client.go +++ b/irc/client.go @@ -53,18 +53,16 @@ const ( pushQueueLengthPerClient = 16 ) +var ( + // idle timeouts for client connections, set from the config + RegisterTimeout, PingTimeout, DisconnectTimeout time.Duration +) + const ( - // RegisterTimeout is how long clients have to register before we disconnect them - RegisterTimeout = time.Minute - // DefaultIdleTimeout is how long without traffic before we send the client a PING - DefaultIdleTimeout = time.Minute + 30*time.Second // For Tor clients, we send a PING at least every 30 seconds, as a workaround for this bug // (single-onion circuits will close unless the client sends data once every 60 seconds): // https://bugs.torproject.org/29665 - TorIdleTimeout = time.Second * 30 - // This is how long a client gets without sending any message, including the PONG to our - // PING, before we disconnect them: - DefaultTotalTimeout = 2*time.Minute + 30*time.Second + TorPingTimeout = time.Second * 30 // round off the ping interval by this much, see below: PingCoalesceThreshold = time.Second @@ -863,19 +861,19 @@ func (client *Client) updateIdleTimer(session *Session, now time.Time) { session.pingSent = false if session.idleTimer == nil { - pingTimeout := DefaultIdleTimeout - if session.isTor { - pingTimeout = TorIdleTimeout + pingTimeout := PingTimeout + if session.isTor && TorPingTimeout < pingTimeout { + pingTimeout = TorPingTimeout } session.idleTimer = time.AfterFunc(pingTimeout, session.handleIdleTimeout) } } func (session *Session) handleIdleTimeout() { - totalTimeout := DefaultTotalTimeout - pingTimeout := DefaultIdleTimeout - if session.isTor { - pingTimeout = TorIdleTimeout + totalTimeout := DisconnectTimeout + pingTimeout := PingTimeout + if session.isTor && TorPingTimeout < pingTimeout { + pingTimeout = TorPingTimeout } session.client.stateMutex.Lock() diff --git a/irc/config.go b/irc/config.go index 6939cf5c..22da3d97 100644 --- a/irc/config.go +++ b/irc/config.go @@ -45,6 +45,10 @@ import ( "github.com/ergochat/ergo/irc/webpush" ) +const ( + defaultProxyDeadline = time.Minute +) + // here's how this works: exported (capitalized) members of the config structs // are defined in the YAML file and deserialized directly from there. They may // be postprocessed and overwritten by LoadConfig. Unexported (lowercase) members @@ -577,7 +581,12 @@ type Config struct { MOTD string motdLines []string MOTDFormatting bool `yaml:"motd-formatting"` - Relaymsg struct { + IdleTimeouts struct { + Registration time.Duration + Ping time.Duration + Disconnect time.Duration + } `yaml:"idle-timeouts"` + Relaymsg struct { Enabled bool Separators string AvailableToChanops bool `yaml:"available-to-chanops"` @@ -986,7 +995,7 @@ func (conf *Config) prepareListeners() (err error) { conf.Server.trueListeners = make(map[string]utils.ListenerConfig) for addr, block := range conf.Server.Listeners { var lconf utils.ListenerConfig - lconf.ProxyDeadline = RegisterTimeout + lconf.ProxyDeadline = defaultProxyDeadline lconf.Tor = block.Tor lconf.STSOnly = block.STSOnly if lconf.STSOnly && !conf.Server.STS.Enabled { @@ -1236,6 +1245,23 @@ func LoadConfig(filename string) (config *Config, err error) { } } + if config.Server.IdleTimeouts.Registration <= 0 { + config.Server.IdleTimeouts.Registration = time.Minute + } + if config.Server.IdleTimeouts.Ping <= 0 { + config.Server.IdleTimeouts.Ping = time.Minute + 30*time.Second + } + if config.Server.IdleTimeouts.Disconnect <= 0 { + config.Server.IdleTimeouts.Disconnect = 2*time.Minute + 30*time.Second + } + + if !(config.Server.IdleTimeouts.Ping < config.Server.IdleTimeouts.Disconnect) { + return nil, fmt.Errorf( + "ping timeout %v must be strictly less than disconnect timeout %v, to give the client time to respond", + config.Server.IdleTimeouts.Ping, config.Server.IdleTimeouts.Disconnect, + ) + } + if config.Server.CoerceIdent != "" { if config.Server.CheckIdent { return nil, errors.New("Can't configure both check-ident and coerce-ident") diff --git a/irc/server.go b/irc/server.go index 7aee6705..c59ac571 100644 --- a/irc/server.go +++ b/irc/server.go @@ -690,6 +690,9 @@ func (server *Server) applyConfig(config *Config) (err error) { globalCasemappingSetting = config.Server.Casemapping globalUtf8EnforcementSetting = config.Server.EnforceUtf8 MaxLineLen = config.Server.MaxLineLen + RegisterTimeout = config.Server.IdleTimeouts.Registration + PingTimeout = config.Server.IdleTimeouts.Ping + DisconnectTimeout = config.Server.IdleTimeouts.Disconnect } else { // enforce configs that can't be changed after launch: if server.name != config.Server.Name { @@ -715,6 +718,8 @@ func (server *Server) applyConfig(config *Config) (err error) { return fmt.Errorf("Cannot enable MySQL after launching the server, rehash aborted") } else if oldConfig.Server.MaxLineLen != config.Server.MaxLineLen { return fmt.Errorf("Cannot change max-line-len after launching the server, rehash aborted") + } else if oldConfig.Server.IdleTimeouts != config.Server.IdleTimeouts { + return fmt.Errorf("Cannot change idle-timeouts after launching the server, rehash aborted") } } diff --git a/traditional.yaml b/traditional.yaml index 4b29ec34..70f31b81 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -154,6 +154,17 @@ server: # if this is true, the motd is escaped using formatting codes like $c, $b, and $i motd-formatting: true + # idle timeouts for inactive clients + idle-timeouts: + # give the client this long to complete connection registration (i.e. the initial + # IRC handshake, including capability negotiation and SASL) + registration: 60s + # if the client hasn't sent anything for this long, send them a PING + ping: 1m30s + # if the client hasn't sent anything for this long (including the PONG to the + # above PING), disconnect them + disconnect: 2m30s + # relaying using the RELAYMSG command relaymsg: # is relaymsg enabled at all? From 96aa0183527f73603104f0a84867d21bfa1fb42d Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 26 Nov 2025 03:27:13 -0500 Subject: [PATCH 27/35] bump irctest --- irctest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest b/irctest index ccdacd99..5031e160 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit ccdacd990a09ac3cd3591613b8d7fee4301e08c8 +Subproject commit 5031e1603df7f0d9017315c10a1a878b25ed80ae From 0119bbc36ff7d3a1f944a9636b49f4ba42c1dd68 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 23 Nov 2025 09:14:16 +0000 Subject: [PATCH 28/35] implement FAIL METADATA RATE_LIMITED --- default.yaml | 5 +++++ irc/client.go | 1 + irc/config.go | 9 +++++---- irc/getters.go | 20 ++++++++++++++++++++ irc/handlers.go | 12 ++++++++++++ irc/metadata.go | 2 +- irc/numerics.go | 5 +++-- traditional.yaml | 5 +++++ 8 files changed, 52 insertions(+), 7 deletions(-) diff --git a/default.yaml b/default.yaml index 242ab7bf..e6739f93 100644 --- a/default.yaml +++ b/default.yaml @@ -1111,6 +1111,11 @@ metadata: max-subs: 100 # how many keys can be stored per entity? max-keys: 100 + # rate limiting for client metadata updates, which are expensive to process + client-throttle: + enabled: true + duration: 2m + max-attempts: 10 # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications. diff --git a/irc/client.go b/irc/client.go index f5f79cb4..5ce22b44 100644 --- a/irc/client.go +++ b/irc/client.go @@ -130,6 +130,7 @@ type Client struct { pushSubscriptionsExist atomic.Uint32 // this is a cache on len(pushSubscriptions) != 0 pushQueue pushQueue metadata map[string]string + metadataThrottle connection_limits.ThrottleDetails } type saslStatus struct { diff --git a/irc/config.go b/irc/config.go index 714c8527..634ac5f5 100644 --- a/irc/config.go +++ b/irc/config.go @@ -734,10 +734,11 @@ type Config struct { } Metadata struct { - Enabled bool - MaxSubs int `yaml:"max-subs"` - MaxKeys int `yaml:"max-keys"` - MaxValueBytes int `yaml:"max-value-length"` + Enabled bool + MaxSubs int `yaml:"max-subs"` + MaxKeys int `yaml:"max-keys"` + MaxValueBytes int `yaml:"max-value-length"` + ClientThrottle ThrottleConfig `yaml:"client-throttle"` } WebPush struct { diff --git a/irc/getters.go b/irc/getters.go index 27b08d74..e5bd8e7d 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -11,6 +11,7 @@ import ( "time" "github.com/ergochat/ergo/irc/caps" + "github.com/ergochat/ergo/irc/connection_limits" "github.com/ergochat/ergo/irc/languages" "github.com/ergochat/ergo/irc/modes" "github.com/ergochat/ergo/irc/utils" @@ -1039,3 +1040,22 @@ func (client *Client) CountMetadata() int { return len(client.metadata) } + +func (client *Client) checkMetadataThrottle() (throttled bool, remainingTime time.Duration) { + config := client.server.Config() + if !config.Metadata.ClientThrottle.Enabled { + return false, 0 + } + + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + + // copy client.metadataThrottle locally and then back for processing + var throttle connection_limits.GenericThrottle + throttle.ThrottleDetails = client.metadataThrottle + throttle.Duration = config.Metadata.ClientThrottle.Duration + throttle.Limit = config.Metadata.ClientThrottle.MaxAttempts + throttled, remainingTime = throttle.Touch() + client.metadataThrottle = throttle.ThrottleDetails + return +} diff --git a/irc/handlers.go b/irc/handlers.go index 7b455028..3ffc505c 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -3197,6 +3197,18 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string return } + // only rate limit clients changing their own metadata: + // channel metadata updates are not any more costly than a PRIVMSG + if client == targetClient { + if throttled, remainingTime := client.checkMetadataThrottle(); throttled { + retryAfter := strconv.Itoa(int(remainingTime.Seconds()) + 1) + rb.Add(nil, server.name, "FAIL", "METADATA", "RATE_LIMITED", + target, utils.SafeErrorParam(key), retryAfter, + fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime.Round(time.Millisecond))) + return + } + } + if len(params) > 3 { value := params[3] diff --git a/irc/metadata.go b/irc/metadata.go index b5e9a5a2..80b0b063 100644 --- a/irc/metadata.go +++ b/irc/metadata.go @@ -21,7 +21,7 @@ var ( errMetadataNotFound = errors.New("key not found") ) -type MetadataHaver = interface { +type MetadataHaver interface { SetMetadata(key string, value string, limit int) (updated bool, err error) GetMetadata(key string) (string, bool) DeleteMetadata(key string) (updated bool) diff --git a/irc/numerics.go b/irc/numerics.go index 339080e1..fb2c1f07 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -183,12 +183,13 @@ const ( RPL_MONLIST = "732" RPL_ENDOFMONLIST = "733" ERR_MONLISTFULL = "734" - RPL_KEYVALUE = "761" // metadata numerics + RPL_WHOISKEYVALUE = "760" // metadata numerics + RPL_KEYVALUE = "761" RPL_KEYNOTSET = "766" RPL_METADATASUBOK = "770" RPL_METADATAUNSUBOK = "771" RPL_METADATASUBS = "772" - RPL_METADATASYNCLATER = "774" + RPL_METADATASYNCLATER = "774" // end metadata numerics RPL_LOGGEDIN = "900" RPL_LOGGEDOUT = "901" ERR_NICKLOCKED = "902" diff --git a/traditional.yaml b/traditional.yaml index 5955cdf6..bdb7f497 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -1082,6 +1082,11 @@ metadata: max-subs: 100 # how many keys can be stored per entity? max-keys: 100 + # rate limiting for client metadata updates, which are expensive to process + client-throttle: + enabled: true + duration: 2m + max-attempts: 10 # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications. From f91d1d94f6d68329d2b7c76c378de6b15fd4ebef Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 1 Dec 2025 06:29:56 +0000 Subject: [PATCH 29/35] add METADATA response when MONITOR triggered --- irc/client.go | 2 +- irc/monitor.go | 15 ++++++++++++++- irc/nickname.go | 8 +++++--- irc/server.go | 2 ++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/irc/client.go b/irc/client.go index 5ce22b44..aa0a6e52 100644 --- a/irc/client.go +++ b/irc/client.go @@ -1398,7 +1398,7 @@ func (client *Client) destroy(session *Session) { // alert monitors if registered { - client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false) + client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false, nil) } // clean up channels diff --git a/irc/monitor.go b/irc/monitor.go index 9131e81b..46ab46e6 100644 --- a/irc/monitor.go +++ b/irc/monitor.go @@ -38,7 +38,7 @@ func (manager *MonitorManager) AddMonitors(users utils.HashSet[*Session], cfnick } // AlertAbout alerts everyone monitoring `client`'s nick that `client` is now {on,off}line. -func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool) { +func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool, client *Client) { var watchers []*Session // safely copy the list of clients watching our nick manager.RLock() @@ -52,8 +52,21 @@ func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool) { command = RPL_MONONLINE } + var metadata map[string]string + if online && client != nil { + metadata = client.ListMetadata() + } + for _, session := range watchers { session.Send(nil, session.client.server.name, command, session.client.Nick(), nick) + + if metadata != nil && session.capabilities.Has(caps.Metadata) { + for key := range session.MetadataSubscriptions() { + if val, ok := metadata[key]; ok { + session.Send(nil, client.server.name, "METADATA", nick, key, "*", val) + } + } + } } } diff --git a/irc/nickname.go b/irc/nickname.go index 0f2f8232..28781fff 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -128,9 +128,11 @@ func performNickChange(server *Server, client *Client, target *Client, session * } newCfnick := target.NickCasefolded() - if newCfnick != details.nickCasefolded { - client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false) - client.server.monitorManager.AlertAbout(assignedNickname, newCfnick, true) + // send MONITOR updates only for nick changes, not for new connection registration; + // defer MONITOR for new connection registration until pre-registration metadata is applied + if hadNick && newCfnick != details.nickCasefolded { + client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false, nil) + client.server.monitorManager.AlertAbout(assignedNickname, newCfnick, true, target) } return nil } diff --git a/irc/server.go b/irc/server.go index c59ac571..d1cf736e 100644 --- a/irc/server.go +++ b/irc/server.go @@ -430,6 +430,8 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) { c.applyPreregMetadata(session) + c.server.monitorManager.AlertAbout(c.Nick(), c.NickCasefolded(), true, c) + // this is not a reattach, so if the client is always-on, this is the first time // the Client object was created during the current server uptime. mark dirty in // order to persist the realname and the user modes: From 0ce90160980b080e6e25e05cc26666561b17b20e Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 7 Dec 2025 03:04:16 -0500 Subject: [PATCH 30/35] add persistence for user metadata --- irc/accounts.go | 38 ++++++++++++++++++++++++++++++++++++++ irc/client.go | 10 +++++++++- irc/getters.go | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index c85a22c1..fd55aeff 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -52,6 +52,7 @@ const ( // (not to be confused with their amodes, which a non-always-on client can have): keyAccountChannelToModes = "account.channeltomodes %s" keyAccountPushSubscriptions = "account.pushsubscriptions %s" + keyAccountMetadata = "account.metadata %s" maxCertfpsPerAccount = 5 ) @@ -137,6 +138,7 @@ func (am *AccountManager) createAlwaysOnClients(config *Config) { am.loadModes(accountName), am.loadRealname(accountName), am.loadPushSubscriptions(accountName), + am.loadMetadata(accountName), ) } } @@ -751,6 +753,40 @@ func (am *AccountManager) loadPushSubscriptions(account string) (result []stored } } +func (am *AccountManager) saveMetadata(account string, metadata map[string]string) { + j, err := json.Marshal(metadata) + if err != nil { + am.server.logger.Error("internal", "error storing metadata", err.Error()) + return + } + val := string(j) + key := fmt.Sprintf(keyAccountMetadata, account) + am.server.store.Update(func(tx *buntdb.Tx) error { + tx.Set(key, val, nil) + return nil + }) + return +} + +func (am *AccountManager) loadMetadata(account string) (result map[string]string) { + key := fmt.Sprintf(keyAccountMetadata, account) + var val string + am.server.store.View(func(tx *buntdb.Tx) error { + val, _ = tx.Get(key) + return nil + }) + + if val == "" { + return nil + } + if err := json.Unmarshal([]byte(val), &result); err == nil { + return result + } else { + am.server.logger.Error("internal", "error loading metadata", err.Error()) + return nil + } +} + func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) { certfp, err = utils.NormalizeCertfp(certfp) if err != nil { @@ -1880,6 +1916,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error { pwResetKey := fmt.Sprintf(keyAccountPwReset, casefoldedAccount) emailChangeKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount) pushSubscriptionsKey := fmt.Sprintf(keyAccountPushSubscriptions, casefoldedAccount) + metadataKey := fmt.Sprintf(keyAccountMetadata, casefoldedAccount) var clients []*Client defer func() { @@ -1939,6 +1976,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error { tx.Delete(pwResetKey) tx.Delete(emailChangeKey) tx.Delete(pushSubscriptionsKey) + tx.Delete(metadataKey) return nil }) diff --git a/irc/client.go b/irc/client.go index aa0a6e52..42a6dc95 100644 --- a/irc/client.go +++ b/irc/client.go @@ -428,7 +428,7 @@ func (server *Server) RunClient(conn IRCConn) { client.run(session) } -func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus map[string]alwaysOnChannelStatus, lastSeen, readMarkers map[string]time.Time, uModes modes.Modes, realname string, pushSubscriptions []storedPushSubscription) { +func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus map[string]alwaysOnChannelStatus, lastSeen, readMarkers map[string]time.Time, uModes modes.Modes, realname string, pushSubscriptions []storedPushSubscription, metadata map[string]string) { now := time.Now().UTC() config := server.Config() if lastSeen == nil && account.Settings.AutoreplayMissed { @@ -513,6 +513,10 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus m } } client.rebuildPushSubscriptionCache() + + if len(metadata) != 0 { + client.metadata = metadata + } } func (client *Client) resizeHistory(config *Config) { @@ -1850,6 +1854,7 @@ const ( IncludeUserModes IncludeRealname IncludePushSubscriptions + IncludeMetadata ) func (client *Client) markDirty(dirtyBits uint) { @@ -1931,6 +1936,9 @@ func (client *Client) performWrite(additionalDirtyBits uint) { if (dirtyBits & IncludePushSubscriptions) != 0 { client.server.accounts.savePushSubscriptions(account, client.getPushSubscriptions(true)) } + if (dirtyBits & IncludeMetadata) != 0 { + client.server.accounts.saveMetadata(account, client.ListMetadata()) + } } // Blocking store; see Channel.Store and Socket.BlockingWrite diff --git a/irc/getters.go b/irc/getters.go index e5bd8e7d..fc85bce3 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -963,9 +963,18 @@ func (client *Client) GetMetadata(key string) (string, bool) { } func (client *Client) SetMetadata(key string, value string, limit int) (updated bool, err error) { + var alwaysOn bool + defer func() { + if alwaysOn && updated { + client.markDirty(IncludeMetadata) + } + }() + client.stateMutex.Lock() defer client.stateMutex.Unlock() + alwaysOn = client.registered && client.alwaysOn + if client.metadata == nil { client.metadata = make(map[string]string) } @@ -982,11 +991,20 @@ func (client *Client) SetMetadata(key string, value string, limit int) (updated } func (client *Client) UpdateMetadataFromPrereg(preregData map[string]string, limit int) (updates map[string]string) { + var alwaysOn bool + defer func() { + if alwaysOn && len(updates) > 0 { + client.markDirty(IncludeMetadata) + } + }() + updates = make(map[string]string, len(preregData)) client.stateMutex.Lock() defer client.stateMutex.Unlock() + alwaysOn = client.registered && client.alwaysOn + if client.metadata == nil { client.metadata = make(map[string]string) } @@ -1003,6 +1021,7 @@ func (client *Client) UpdateMetadataFromPrereg(preregData map[string]string, lim client.metadata[k] = v updates[k] = v } + return } @@ -1014,6 +1033,12 @@ func (client *Client) ListMetadata() map[string]string { } func (client *Client) DeleteMetadata(key string) (updated bool) { + defer func() { + if updated { + client.markDirty(IncludeMetadata) + } + }() + client.stateMutex.Lock() defer client.stateMutex.Unlock() @@ -1024,11 +1049,17 @@ func (client *Client) DeleteMetadata(key string) (updated bool) { return updated } -func (client *Client) ClearMetadata() map[string]string { +func (client *Client) ClearMetadata() (oldMap map[string]string) { + defer func() { + if len(oldMap) > 0 { + client.markDirty(IncludeMetadata) + } + }() + client.stateMutex.Lock() defer client.stateMutex.Unlock() - oldMap := client.metadata + oldMap = client.metadata client.metadata = nil return oldMap From fdd261a1e6d2d874fa183cd1d0ca4ca86a05f2ba Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 14 Dec 2025 02:13:15 -0500 Subject: [PATCH 31/35] fix #2303 (#2304) Fix inconsistent behavior when history.enabled is set but history.chathistory-maxmessages is not --- irc/config.go | 2 +- irc/handlers.go | 9 ++++++--- irc/server.go | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/irc/config.go b/irc/config.go index 634ac5f5..fd997512 100644 --- a/irc/config.go +++ b/irc/config.go @@ -1611,7 +1611,7 @@ func LoadConfig(filename string) (config *Config, err error) { // in the current implementation, we disable history by creating a history buffer // with zero capacity. but the `enabled` config option MUST be respected regardless // of this detail - if !config.History.Enabled { + if !config.History.Enabled || config.History.ChathistoryMax == 0 { config.History.ChannelLength = 0 config.History.ClientLength = 0 config.Server.supportedCaps.Disable(caps.Chathistory) diff --git a/irc/handlers.go b/irc/handlers.go index 3ffc505c..b74800f0 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -700,11 +700,13 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb * var channel *Channel var sequence history.Sequence var err error - var listTargets bool + var disabled, listTargets bool var targets []history.TargetListing defer func() { // errors are sent either without a batch, or in a draft/labeled-response batch as usual - if err == utils.ErrInvalidParams { + if disabled { + rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("That feature is disabled")) + } else if err == utils.ErrInvalidParams { rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMS", msg.Params[0], client.t("Invalid parameters")) } else if !listTargets && sequence == nil { rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_TARGET", msg.Params[0], utils.SafeErrorParam(target), client.t("Messages could not be retrieved")) @@ -730,7 +732,8 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb * config := server.Config() maxChathistoryLimit := config.History.ChathistoryMax - if maxChathistoryLimit == 0 { + if !config.History.Enabled || maxChathistoryLimit == 0 { + disabled = true return } preposition := strings.ToLower(msg.Params[0]) diff --git a/irc/server.go b/irc/server.go index d1cf736e..3c763593 100644 --- a/irc/server.go +++ b/irc/server.go @@ -908,6 +908,9 @@ func (server *Server) applyConfig(config *Config) (err error) { if config.Accounts.RequireSasl.Enabled && config.Accounts.Registration.Enabled { server.logger.Warning("server", "Warning: although require-sasl is enabled, users can still register accounts. If your server is not intended to be public, you must set accounts.registration.enabled to false.") } + if config.History.Enabled && config.History.ChathistoryMax == 0 { + server.logger.Warning("server", "Warning: for history to work correctly, you must set history.chathistory-maxmessages (see default.yaml for a recommendation).") + } return err } From 9ca936a777f139c2ee8dd76766ea3b7fd780579f Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 14 Dec 2025 02:13:33 -0500 Subject: [PATCH 32/35] bump irctest --- irctest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest b/irctest index 5031e160..13a76e45 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit 5031e1603df7f0d9017315c10a1a878b25ed80ae +Subproject commit 13a76e4501749dbc1a604e16978e128ff40edace From d26aa37f2cc34b428d217bf2ca769513cfebb898 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 14 Dec 2025 02:50:21 -0500 Subject: [PATCH 33/35] pin Alpine to 3.22 (#2306) * Upgrade the production image from 3.19 to 3.22 (3.19 went EOL 2025-11-01) * Downgrade the build image to 3.22 (3.23 is buggy, see #2305) --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4c3788b6..1d5bb800 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ## build ergo binary -FROM docker.io/golang:1.25-alpine AS build-env +FROM docker.io/golang:1.25-alpine3.22 AS build-env RUN apk upgrade -U --force-refresh --no-cache && apk add --no-cache --purge --clean-protected -l -u make git @@ -16,7 +16,7 @@ RUN sed -i 's/^\(\s*\)\"127.0.0.1:6667\":.*$/\1":6667":/' /go/src/github.com/erg RUN make install ## build ergo container -FROM docker.io/alpine:3.19 +FROM docker.io/alpine:3.22 # metadata LABEL maintainer="Daniel Oaks ,Daniel Thamdrup " \ From 53664694c4bbb647f01a708bd4d8fbad25f02bed Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 14 Dec 2025 04:32:02 -0500 Subject: [PATCH 34/35] add RPL_WHOISKEYVALUE output (#2302) --- irc/server.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/irc/server.go b/irc/server.go index 3c763593..641d63f2 100644 --- a/irc/server.go +++ b/irc/server.go @@ -637,7 +637,6 @@ func (client *Client) getWhoisOf(target *Client, hasPrivs bool, rb *ResponseBuff if target.HasMode(modes.Bot) { rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, fmt.Sprintf(ircfmt.Unescape(client.t("is a $bBot$b on %s")), client.server.Config().Network.Name)) } - if client == target || oper.HasRoleCapab("ban") { for _, session := range target.Sessions() { if session.certfp != "" { @@ -649,6 +648,11 @@ func (client *Client) getWhoisOf(target *Client, hasPrivs bool, rb *ResponseBuff if away, awayMessage := target.Away(); away { rb.Add(nil, client.server.name, RPL_AWAY, cnick, tnick, awayMessage) } + if rb.session.capabilities.Has(caps.Metadata) { + for key, value := range target.ListMetadata() { + rb.Add(nil, client.server.name, RPL_WHOISKEYVALUE, cnick, tnick, key, "*", value) + } + } } // rehash reloads the config and applies the changes from the config file. From d5fb189a555b96ef8a6a445b9e00fdb73ad81fcf Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 14 Dec 2025 04:43:30 -0500 Subject: [PATCH 35/35] changelog and version bump for v2.17.0-rc1 (#2308) --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ default.yaml | 2 +- irc/version.go | 2 +- traditional.yaml | 2 +- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51c7bd13..b24ce6b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,37 @@ # Changelog All notable changes to Ergo will be documented in this file. +## [2.17.0-rc1] - 2025-12-14 + +We're pleased to be publishing the release candidate for v2.17.0 (the official release should follow within a week or so). This release adds support for the [IRCv3 metadata specification](https://ircv3.net/specs/extensions/metadata), thanks to [@thatcher-gaming](https://github.com/thatcher-gaming), as well as bug fixes and minor updates. + +This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format. + +Many thanks to [@branchgrove](https://github.com/branchgrove), [@Brutus5000](https://github.com/Brutus5000), [@progval](https://github.com/progval), [@SarahRoseLives](https://github.com/SarahRoseLives), [@thatcher-gaming](https://github.com/thatcher-gaming), [@ValwareIRC](https://github.com/ValwareIRC), and Xogium for contributing patches, reporting issues, and helping test. + +### Config changes +* Added `accounts.metadata` block to configure the new metadata feature. If this block is absent, metadata is disabled. See `default.yaml` for an example. (#2273) +* Added `server.idle-timeouts` for configurable idle timeouts; when unset, the previous hardcoded defaults are used (#2292, thanks [@Brutus5000](https://github.com/Brutus5000)!) +* Added `server.oper-throttle` to configure throttling for failed `OPER` attempts; when unset, this defaults to 1 attempt every 10 seconds (#2296) + +### Added +* Implemented support for the [draft/metadata-2](https://ircv3.net/specs/extensions/metadata) specification, allowing clients to set and retrieve metadata on accounts and channels (#2273, #2277, #2281, #2282, #2301, thanks [@thatcher-gaming](https://github.com/thatcher-gaming)!) +* Added `/v1/status` and `/v1/account_list` HTTP API endpoints (#2261, thanks [@SarahRoseLives](https://github.com/SarahRoseLives)!) +* Enhanced `/v1/account_details` API response with additional fields (#2261, thanks [@SarahRoseLives](https://github.com/SarahRoseLives)!) + +### Fixed +* Fixed `REGISTER` command to strip guest format when applicable, matching `NS REGISTER` behavior (#2270, #2271, thanks [@ValwareIRC](https://github.com/ValwareIRC) and [@thatcher-gaming](https://github.com/thatcher-gaming)!) +* Fixed invalid `FAIL` codes in `REGISTER` command (#2269, thanks [@ValwareIRC](https://github.com/ValwareIRC)!) +* Fixed validation of web push URLs to reject non-HTTPS URLs (#2295) +* Fixed inconsistent behavior when `history.enabled` is set but `history.chathistory-maxmessages` is not (#2303, #2304, thanks [@branchgrove](https://github.com/branchgrove)!) + +### Changed +* The `OPER` command now imposes a throttle on all attempts, never disconnects the client on failure, and logs non-sensitive information about failed attempts (#2296, #2298, thanks Xogium!) + +### Internal +* Official release builds use Go 1.25 (#2290) +* Upgraded the Docker base image from Alpine 3.19 to 3.22 (#2306) + ## [2.16.0] - 2025-05-18 We're pleased to be publishing v2.16.0, a new stable release. This release contains bug fixes and some minor updates. diff --git a/default.yaml b/default.yaml index e6739f93..007d8e18 100644 --- a/default.yaml +++ b/default.yaml @@ -369,7 +369,7 @@ server: secure-nets: # - "10.0.0.0/8" - # allow attempts to OPER with a password at most this often. default to + # allow attempts to OPER with a password at most this often. defaults to # 10 seconds when unset. oper-throttle: 10s diff --git a/irc/version.go b/irc/version.go index 16dbbd3f..f7a311a6 100644 --- a/irc/version.go +++ b/irc/version.go @@ -7,7 +7,7 @@ import "fmt" const ( // SemVer is the semantic version of Ergo. - SemVer = "2.17.0-unreleased" + SemVer = "2.17.0-rc1" ) var ( diff --git a/traditional.yaml b/traditional.yaml index bdb7f497..0e36ec8c 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -341,7 +341,7 @@ server: secure-nets: # - "10.0.0.0/8" - # allow attempts to OPER with a password at most this often. default to + # allow attempts to OPER with a password at most this often. defaults to # 10 seconds when unset. oper-throttle: 10s