forked from External/ergo
Compare commits
1 commit
master
...
cgorelease
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f78cf1357 |
657 changed files with 27351 additions and 88526 deletions
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
|
@ -12,14 +12,14 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: "ubuntu-22.04"
|
runs-on: "ubuntu-20.04"
|
||||||
steps:
|
steps:
|
||||||
- name: "checkout repository"
|
- name: "checkout repository"
|
||||||
uses: "actions/checkout@v3"
|
uses: "actions/checkout@v2"
|
||||||
- name: "setup go"
|
- name: "setup go"
|
||||||
uses: "actions/setup-go@v3"
|
uses: "actions/setup-go@v2"
|
||||||
with:
|
with:
|
||||||
go-version: "1.23"
|
go-version: "1.19"
|
||||||
- name: "install python3-pytest"
|
- name: "install python3-pytest"
|
||||||
run: "sudo apt install -y python3-pytest"
|
run: "sudo apt install -y python3-pytest"
|
||||||
- name: "make install"
|
- name: "make install"
|
||||||
|
|
|
||||||
10
.github/workflows/docker-image.yml
vendored
10
.github/workflows/docker-image.yml
vendored
|
|
@ -18,10 +18,10 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Git repository
|
- name: Checkout Git repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Authenticate to container registry
|
- name: Authenticate to container registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v1
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
|
|
@ -30,16 +30,16 @@ jobs:
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v3
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
- name: Setup Docker buildx driver
|
- name: Setup Docker buildx driver
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
- name: Build and publish image
|
- name: Build and publish image
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
# .goreleaser.yml
|
# .goreleaser.yml
|
||||||
# Build customization
|
# Build customization
|
||||||
version: 2
|
|
||||||
project_name: ergo
|
project_name: ergo
|
||||||
builds:
|
builds:
|
||||||
- main: ergo.go
|
- main: ergo.go
|
||||||
|
|
@ -18,7 +17,6 @@ builds:
|
||||||
- amd64
|
- amd64
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
- riscv64
|
|
||||||
goarm:
|
goarm:
|
||||||
- 6
|
- 6
|
||||||
ignore:
|
ignore:
|
||||||
|
|
@ -26,41 +24,30 @@ builds:
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
- goos: windows
|
|
||||||
goarch: riscv64
|
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: darwin
|
|
||||||
goarch: riscv64
|
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
- goos: freebsd
|
|
||||||
goarch: riscv64
|
|
||||||
- goos: openbsd
|
- goos: openbsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: openbsd
|
- goos: openbsd
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
- goos: openbsd
|
|
||||||
goarch: riscv64
|
|
||||||
- goos: plan9
|
- goos: plan9
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: plan9
|
- goos: plan9
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
- goos: plan9
|
|
||||||
goarch: riscv64
|
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
-
|
-
|
||||||
name_template: >-
|
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||||
{{ .ProjectName }}-{{ .Version }}-
|
|
||||||
{{- if eq .Os "darwin" }}macos{{- else }}{{ .Os }}{{ end -}}-
|
|
||||||
{{- if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end -}}
|
|
||||||
{{ if .Arm }}v{{ .Arm }}{{ end -}}
|
|
||||||
format: tar.gz
|
format: tar.gz
|
||||||
|
replacements:
|
||||||
|
amd64: x86_64
|
||||||
|
darwin: macos
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
format: zip
|
||||||
|
|
|
||||||
136
CHANGELOG.md
136
CHANGELOG.md
|
|
@ -1,142 +1,6 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to Ergo will be documented in this file.
|
All notable changes to Ergo will be documented in this file.
|
||||||
|
|
||||||
## [2.14.0] - 2024-06-30
|
|
||||||
|
|
||||||
We're pleased to be publishing v2.14.0, a new stable release. This release contains primarily bug fixes, with the addition of some new authentication mechanisms for integrating with web clients.
|
|
||||||
|
|
||||||
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 [@al3xandros](https://github.com/al3xandros), donio, [@eeeeeta](https://github.com/eeeeeta), [@emersion](https://github.com/emersion), [@Eriner](https://github.com/Eriner), [@eskimo](https://github.com/eskimo), [@Herringway](https://github.com/Herringway), [@jwheare](https://github.com/jwheare), [@knolley](https://github.com/knolley), [@mengzhuo](https://github.com/mengzhuo), pathof, [@poVoq](https://github.com/poVoq), [@progval](https://github.com/progval), [@RNDpacman](https://github.com/RNDpacman), and [@xnaas](https://github.com/xnaas) for contributing patches, reporting issues, and helping test.
|
|
||||||
|
|
||||||
### Config changes
|
|
||||||
* Added `accounts.oauth2` and `accounts.jwt-auth` blocks for configuring OAuth2 and JWT authentication (#2004)
|
|
||||||
* Added `protocol` and `local-address` options to `accounts.registration.email-verification`, to force emails to be sent over IPv4 (or IPv6) or to force the use of a particular source address (#2142)
|
|
||||||
* Added `limits.realnamelen`, a configurable limit on the length of realnames. If unset, no limit is enforced beyond the IRC protocol line length limits (the previous behavior). (#2123, thanks [@eskimo](https://github.com/eskimo)!)
|
|
||||||
* Added the `accept-hostname` option to the webirc config block, allowing Ergo to accept hostnames passed from reverse proxies on the `WEBIRC` line. Note that this will have no effect under the default/recommended configuration, in which cloaks are used instead (#1686, #2146, thanks [@RNDpacman](https://github.com/RNDpacman)!)
|
|
||||||
* The default/recommended value of `limits.chan-list-modes` (the size limit for ban/except/invite lists) was raised to 100 (#2081, #2165, #2167)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
* Added support for the `OAUTHBEARER` SASL mechanism, allowing Ergo to interoperate with Gamja and an OAuth2 provider (#2004, #2122, thanks [@emersion](https://github.com/emersion)!)
|
|
||||||
* Added support for the [`IRCV3BEARER` SASL mechanism](https://github.com/ircv3/ircv3-specifications/pull/545), allowing Ergo to accept OAuth2 or JWT bearer tokens (#2158)
|
|
||||||
* Added support for the legacy `rfc1459` and `rfc1459-strict` casemappings (#2099, #2159, thanks [@xnaas](https://github.com/xnaas)!)
|
|
||||||
* The new `ergo defaultconfig` subcommand prints a copy of the default config file to standard output (#2157, #2160, thanks [@al3xandros](https://github.com/al3xandros)!)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
* Even with `allow-truncation: false` (the recommended default), some oversized messages were being accepted and relayed with truncation. These messages will now be rejected with `417 ERR_INPUTTOOLONG` as expected (#2170)
|
|
||||||
* NICK and QUIT from invisible members of auditorium channels are no longer recorded in history (#2133, #2137, thanks [@knolley](https://github.com/knolley) and [@poVoq](https://github.com/poVoq)!)
|
|
||||||
* If channel registration was disabled, registered channels could become inaccessible after rehash; this has been fixed (#2130, thanks [@eeeeeta](https://github.com/eeeeeta)!)
|
|
||||||
* Attempts to use unrecognized SASL mechanisms no longer count against the login throttle, improving compatibility with Pidgin (#2156, thanks donio and pathof!)
|
|
||||||
* Fixed database autoupgrade on Windows, which was previously broken due to the use of a colon in the backup filename (#2139, #2140, thanks [@Herringway](https://github.com/Herringway)!)
|
|
||||||
* Fixed handling of `NS CERT ADD <user> <fp>` when an unprivileged user invokes it on themself (#2128, #2098, thanks [@Eriner](https://github.com/Eriner)!)
|
|
||||||
* Fixed missing human-readable trailing parameters for two multiline `FAIL` messages (#2043, #2162, thanks [@jwheare](https://github.com/jwheare) and [@progval](https://github.com/progval)!)
|
|
||||||
* Fixed symbol sent by `353 RPL_NAMREPLY` for secret channels (#2144, #2145, thanks savoyard!)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
* Trying to claim a registered nickname that is also actually in use by another client now produces `433 ERR_NICKNAMEINUSE` as expected (#2135, #2136, thanks savoyard!)
|
|
||||||
* `SAMODE` now overrides the enforcement of `limits.chan-list-modes` (the size limit for ban/except/invite lists) (#2081, #2165)
|
|
||||||
* Certain unsuccessful `MODE` changes no longer send `324 RPL_CHANNELMODEIS` and `329 RPL_CREATIONTIME` (#2163)
|
|
||||||
* Debug logging for environment variable configuration overrides no longer prints the value, only the key (#2129, #2132, thanks [@eeeeeta](https://github.com/eeeeeta)!)
|
|
||||||
|
|
||||||
### Internal
|
|
||||||
|
|
||||||
* Official release builds use Go 1.22.4
|
|
||||||
* Added a linux/riscv64 release (#2172, #2173, thanks [@mengzhuo](https://github.com/mengzhuo)!)
|
|
||||||
|
|
||||||
## [2.13.1] - 2024-05-06
|
|
||||||
|
|
||||||
Ergo 2.13.1 is a bugfix release, fixing an exploitable deadlock that could lead to a denial of service. We regret the oversight.
|
|
||||||
|
|
||||||
This release includes no changes to the config file format or database format.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
* Fixed an exploitable deadlock that could lead to a denial of service (#2149)
|
|
||||||
|
|
||||||
### Internal
|
|
||||||
|
|
||||||
* Official release builds use Go 1.22.2
|
|
||||||
|
|
||||||
|
|
||||||
## [2.13.0] - 2024-01-14
|
|
||||||
|
|
||||||
We're pleased to be publishing v2.13.0, a new stable release. This is a bugfix release that fixes some issues, including a crash.
|
|
||||||
|
|
||||||
This release includes no changes to the config file format or database format.
|
|
||||||
|
|
||||||
Many thanks to [@dallemon](https://github.com/dallemon), [@jwheare](https://github.com/jwheare), [@Mikaela](https://github.com/Mikaela), [@nealey](https://github.com/nealey), and [@Sheikah45](https://github.com/Sheikah45) for contributing patches, reporting issues, and helping test.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
* Fixed a (hopefully rare) crash when persisting always-on client statuses (#2113, #2117, thanks [@Sheikah45](https://github.com/Sheikah45)!)
|
|
||||||
* Fixed not being able to message channels with `/` (or the configured `RELAYMSG` separator) in their names (#2114, thanks [@Mikaela](https://github.com/Mikaela)!)
|
|
||||||
* Verification emails now always include a `Message-ID` header, improving compatibility with Gmail (#2108, #2110)
|
|
||||||
* Improved human-readable description of `REDACT_FORBIDDEN` (#2101, thanks [@jwheare](https://github.com/jwheare)!)
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
* Removed numerics associated with the retired ACC spec (#2109, #2111, thanks [@jwheare](https://github.com/jwheare)!)
|
|
||||||
|
|
||||||
### Internal
|
|
||||||
|
|
||||||
* Upgraded the Docker base image from Alpine 3.13 to 3.19. The resulting images are incompatible with Docker 19.x and lower (all currently non-EOL Docker versions should be supported). (#2103)
|
|
||||||
* Official release builds use Go 1.21.6
|
|
||||||
|
|
||||||
|
|
||||||
## [2.12.0] - 2023-10-10
|
|
||||||
|
|
||||||
We're pleased to be publishing v2.12.0, a new stable release. This is another bugfix release aimed at improving client compatibility and keeping up with the IRCv3 specification process.
|
|
||||||
|
|
||||||
This release includes changes to the config file format, one of which is a compatibility break: if you were using `accounts.email-verification.blacklist-regexes`, you can restore the previous functionality by renaming `blacklist-regexes` to `address-blacklist` and setting the additional key `address-blacklist-syntax: regex`. See [default.yaml](https://github.com/ergochat/ergo/blob/e7597876d987a6fc061b768fcf878d0035d1c85a/default.yaml#L422-L424) for an example; for more details, see the "Changed" section below.
|
|
||||||
|
|
||||||
This release includes a database change. If you have `datastore.autoupgrade` set to `true` in your configuration, it will be automatically applied when you restart Ergo. Otherwise, you can update the database manually by running `ergo upgradedb` (see the manual for complete instructions).
|
|
||||||
|
|
||||||
Many thanks to [@adsr](https://github.com/adsr), [@avollmerhaus](https://github.com/avollmerhaus), [@csmith](https://github.com/csmith), [@EchedeyLR](https://github.com/EchedeyLR), [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo), [@julio-b](https://github.com/julio-b), knolle, [@KoxSosen](https://github.com/KoxSosen), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), and [@progval](https://github.com/progval) for contributing patches, reporting issues, and helping test.
|
|
||||||
|
|
||||||
### Config changes
|
|
||||||
* Removed `accounts.email-verification.blacklist-regexes` in favor of `address-blacklist`, `address-blacklist-syntax`, and `address-blacklist-file`. See the "Changed" section below for the semantics of these new keys. (#1997, #2088)
|
|
||||||
* Added `implicit-tls` (TLS from the first byte) support for MTAs (#2048, #2049, thanks [@EchedeyLR](https://github.com/EchedeyLR)!)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
* Fixed an edge case under `allow-truncation: true` (the recommended default is `false`) where Ergo could truncate a message in the middle of a UTF-8 codepoint (#2074)
|
|
||||||
* Fixed `CHATHISTORY TARGETS` being sent in a batch even without negotiation of the `batch` capability (#2066, thanks [@julio-b](https://github.com/julio-b)!)
|
|
||||||
* Errors from `/REHASH` are now properly sanitized before being sent to the user, fixing an edge case where they would be dropped (#2031, thanks [@eskimo](https://github.com/eskimo)!
|
|
||||||
* Fixed some edge cases in auto-away aggregation (#2044)
|
|
||||||
* Fixed a FAIL code sent by draft/account-registration (#2092, thanks [@progval](https://github.com/progval)!)
|
|
||||||
* Fixed a socket leak in the ident client (default/recommended configurations of Ergo disable ident and are not affected by this issue) (#2089)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
* Bouncer reattach from an "insecure" session is no longer disallowed. We continue to recommend that operators preemptively disable all insecure transports, such as plaintext listeners (#2013)
|
|
||||||
* Email addresses are now converted to lowercase before checking them against the blacklist (#1997, #2088)
|
|
||||||
* The default syntax for the email address blacklist is now "glob" (expressions with `*` and `?` as wildcard characters), as opposed to the full [Go regular expression syntax](https://github.com/google/re2/wiki/Syntax). To enable full regular expression syntax, set `address-blacklist-syntax: regex`.
|
|
||||||
* Due to line length limitations, some capabilities are now hidden from clients that only support version 301 CAP negotiation. To the best of our knowledge, all clients that support these capabilities also support version 302 CAP negotiation, rendering this moot (#2068)
|
|
||||||
* The default/recommended configuration now advertises the SCRAM-SHA-256 SASL method. We still do not recommend using this method in production. (#2032)
|
|
||||||
* Improved KILL messages (#2053, #2041, thanks [@mogad0n](https://github.com/mogad0n)!)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
* Added support for automatically joining new clients to a channel or channels (#2077, #2079, thanks [@adsr](https://github.com/adsr)!)
|
|
||||||
* Added implicit TLS (TLS from the first byte) support for MTAs (#2048, #2049, thanks [@EchedeyLR](https://github.com/EchedeyLR)!)
|
|
||||||
* Added support for [draft/message-redaction](https://github.com/ircv3/ircv3-specifications/pull/524) (#2065, thanks [@progval](https://github.com/progval)!)
|
|
||||||
* Added support for [draft/pre-away](https://github.com/ircv3/ircv3-specifications/pull/514) (#2044)
|
|
||||||
* Added support for [draft/no-implicit-names](https://github.com/ircv3/ircv3-specifications/pull/527) (#2083)
|
|
||||||
* Added support for the [MSGREFTYPES](https://ircv3.net/specs/extensions/chathistory#isupport-tokens) 005 token (#2042)
|
|
||||||
* Ergo now advertises the [standard-replies](https://ircv3.net/specs/extensions/standard-replies) capability. Requesting this capability does not change Ergo's behavior.
|
|
||||||
|
|
||||||
### Internal
|
|
||||||
* Release builds are now statically linked by default. This should not affect normal chat operations, but may disrupt attempts to connect to external services (e.g. MTAs) that are configured using a hostname that relies on libc's name resolution behavior. To restore the old behavior, build from source with `CGO_ENABLED=1`. (#2023)
|
|
||||||
* Upgraded to Go 1.21 (#2045, #2084); official release builds use Go 1.21.3, which includes a fix for CVE-2023-44487
|
|
||||||
* The default `make` target is now `build` (which builds an `ergo` binary in the working directory) instead of `install` (which builds and installs an `ergo` binary to `${GOPATH}/bin/ergo`). Take note if building from source, or testing Ergo in development! (#2047)
|
|
||||||
* `make irctest` now depends on `make install`, in an attempt to ensure that irctest runs against the intended development version of Ergo (#2047)
|
|
||||||
|
|
||||||
## [2.11.1] - 2022-01-22
|
|
||||||
|
|
||||||
Ergo 2.11.1 is a bugfix release, fixing a denial-of-service issue in our websocket implementation. We regret the oversight.
|
|
||||||
|
|
||||||
This release includes no changes to the config file format or database file format.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
* Fixed a denial-of-service issue affecting websocket clients (#2039)
|
|
||||||
|
|
||||||
## [2.11.0] - 2022-12-25
|
## [2.11.0] - 2022-12-25
|
||||||
|
|
||||||
We're pleased to be publishing v2.11.0, a new stable release. This is another bugfix release aimed at improving client compatibility and keeping up with the IRCv3 specification process.
|
We're pleased to be publishing v2.11.0, a new stable release. This is another bugfix release aimed at improving client compatibility and keeping up with the IRCv3 specification process.
|
||||||
|
|
|
||||||
22
Dockerfile
22
Dockerfile
|
|
@ -1,8 +1,7 @@
|
||||||
## build ergo binary
|
## build ergo binary
|
||||||
FROM docker.io/golang:1.23-alpine AS build-env
|
FROM golang:1.19-alpine AS build-env
|
||||||
|
|
||||||
RUN apk upgrade -U --force-refresh --no-cache
|
RUN apk add -U --force-refresh --no-cache --purge --clean-protected -l -u make git
|
||||||
RUN apk add --no-cache --purge --clean-protected -l -u make git
|
|
||||||
|
|
||||||
# copy ergo source
|
# copy ergo source
|
||||||
WORKDIR /go/src/github.com/ergochat/ergo
|
WORKDIR /go/src/github.com/ergochat/ergo
|
||||||
|
|
@ -14,10 +13,17 @@ RUN sed -i 's/^\(\s*\)\"127.0.0.1:6667\":.*$/\1":6667":/' /go/src/github.com/erg
|
||||||
sed -i 's/^\s*\"\[::1\]:6667\":.*$//' /go/src/github.com/ergochat/ergo/default.yaml
|
sed -i 's/^\s*\"\[::1\]:6667\":.*$//' /go/src/github.com/ergochat/ergo/default.yaml
|
||||||
|
|
||||||
# compile
|
# compile
|
||||||
RUN make install
|
RUN make
|
||||||
|
|
||||||
## build ergo container
|
## build ergo container
|
||||||
FROM docker.io/alpine:3.19
|
FROM alpine:3.13
|
||||||
|
|
||||||
|
# metadata
|
||||||
|
LABEL maintainer="Daniel Oaks <daniel@danieloaks.net>,Daniel Thamdrup <dallemon@protonmail.com>" \
|
||||||
|
description="Ergo is a modern, experimental IRC server written in Go"
|
||||||
|
|
||||||
|
# standard ports listened on
|
||||||
|
EXPOSE 6667/tcp 6697/tcp
|
||||||
|
|
||||||
# ergo itself
|
# ergo itself
|
||||||
COPY --from=build-env /go/bin/ergo \
|
COPY --from=build-env /go/bin/ergo \
|
||||||
|
|
@ -33,4 +39,10 @@ WORKDIR /ircd
|
||||||
# default motd
|
# default motd
|
||||||
COPY --from=build-env /go/src/github.com/ergochat/ergo/ergo.motd /ircd/ergo.motd
|
COPY --from=build-env /go/src/github.com/ergochat/ergo/ergo.motd /ircd/ergo.motd
|
||||||
|
|
||||||
|
# launch
|
||||||
ENTRYPOINT ["/ircd-bin/run.sh"]
|
ENTRYPOINT ["/ircd-bin/run.sh"]
|
||||||
|
|
||||||
|
# # uncomment to debug
|
||||||
|
# RUN apk add --no-cache bash
|
||||||
|
# RUN apk add --no-cache vim
|
||||||
|
# CMD /bin/bash
|
||||||
|
|
|
||||||
12
Makefile
12
Makefile
|
|
@ -9,16 +9,16 @@ export CGO_ENABLED ?= 0
|
||||||
|
|
||||||
capdef_file = ./irc/caps/defs.go
|
capdef_file = ./irc/caps/defs.go
|
||||||
|
|
||||||
all: build
|
all: install
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go install -mod=mod -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
go install -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -mod=mod -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
go build -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
goreleaser --skip=publish --clean
|
goreleaser --skip-publish --rm-dist
|
||||||
|
|
||||||
capdefs:
|
capdefs:
|
||||||
python3 ./gencapdefs.py > ${capdef_file}
|
python3 ./gencapdefs.py > ${capdef_file}
|
||||||
|
|
@ -29,13 +29,13 @@ test:
|
||||||
go vet ./...
|
go vet ./...
|
||||||
./.check-gofmt.sh
|
./.check-gofmt.sh
|
||||||
|
|
||||||
smoke: install
|
smoke:
|
||||||
ergo mkcerts --conf ./default.yaml || true
|
ergo mkcerts --conf ./default.yaml || true
|
||||||
ergo run --conf ./default.yaml --smoke
|
ergo run --conf ./default.yaml --smoke
|
||||||
|
|
||||||
gofmt:
|
gofmt:
|
||||||
./.check-gofmt.sh --fix
|
./.check-gofmt.sh --fix
|
||||||
|
|
||||||
irctest: install
|
irctest:
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
cd irctest && make ergo
|
cd irctest && make ergo
|
||||||
|
|
|
||||||
6
README
6
README
|
|
@ -33,15 +33,15 @@ Modify the config file as needed (the recommendations at the top may be helpful)
|
||||||
|
|
||||||
To generate passwords for opers and connect passwords, you can use this command:
|
To generate passwords for opers and connect passwords, you can use this command:
|
||||||
|
|
||||||
$ ./ergo genpasswd
|
$ ergo genpasswd
|
||||||
|
|
||||||
If you need to generate self-signed TLS certificates, use this command:
|
If you need to generate self-signed TLS certificates, use this command:
|
||||||
|
|
||||||
$ ./ergo mkcerts
|
$ ergo mkcerts
|
||||||
|
|
||||||
You are now ready to start Ergo!
|
You are now ready to start Ergo!
|
||||||
|
|
||||||
$ ./ergo run
|
$ ergo run
|
||||||
|
|
||||||
For further instructions, consult the manual. A copy of the manual should be
|
For further instructions, consult the manual. A copy of the manual should be
|
||||||
included in your release under `docs/MANUAL.md`. Or you can view it on the
|
included in your release under `docs/MANUAL.md`. Or you can view it on the
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ Extract it into a folder, then run the following commands:
|
||||||
```sh
|
```sh
|
||||||
cp default.yaml ircd.yaml
|
cp default.yaml ircd.yaml
|
||||||
vim ircd.yaml # modify the config file to your liking
|
vim ircd.yaml # modify the config file to your liking
|
||||||
./ergo mkcerts
|
ergo mkcerts
|
||||||
./ergo run # server should be ready to go!
|
ergo run # server should be ready to go!
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** See the [productionizing guide in our manual](https://github.com/ergochat/ergo/blob/stable/docs/MANUAL.md#productionizing-with-systemd) for recommendations on how to run a production network, including obtaining valid TLS certificates.
|
**Note:** See the [productionizing guide in our manual](https://github.com/ergochat/ergo/blob/stable/docs/MANUAL.md#productionizing-with-systemd) for recommendations on how to run a production network, including obtaining valid TLS certificates.
|
||||||
|
|
@ -84,7 +84,7 @@ For information on contributing to Ergo, see [DEVELOPING.md](https://github.com/
|
||||||
|
|
||||||
#### Building
|
#### Building
|
||||||
|
|
||||||
You'll need an [up-to-date distribution of the Go language for your OS and architecture](https://golang.org/dl/). Once that's installed (check the output of `go version`), just check out your desired branch or tag and run `make`. This will produce an executable binary named `ergo` in the base directory of the project. (Ergo vendors all its dependencies, so you will not need to fetch any dependencies remotely.)
|
You'll need an [up-to-date distribution of the Go language for your OS and architecture](https://golang.org/dl/). Once that's installed (check the output of `go version`), just check out your desired branch or tag and run `make build`. This will produce an executable binary named `ergo` in the base directory of the project. (Ergo vendors all its dependencies, so you will not need to fetch any dependencies remotely.)
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|
|
||||||
86
default.yaml
86
default.yaml
|
|
@ -134,10 +134,9 @@ server:
|
||||||
# the recommended default is 'ascii' (traditional ASCII-only identifiers).
|
# the recommended default is 'ascii' (traditional ASCII-only identifiers).
|
||||||
# the other options are 'precis', which allows UTF8 identifiers that are "sane"
|
# the other options are 'precis', which allows UTF8 identifiers that are "sane"
|
||||||
# (according to UFC 8265), with additional mitigations for homoglyph attacks,
|
# (according to UFC 8265), with additional mitigations for homoglyph attacks,
|
||||||
# 'permissive', which allows identifiers containing unusual characters like
|
# and 'permissive', which allows identifiers containing unusual characters like
|
||||||
# emoji, at the cost of increased vulnerability to homoglyph attacks and potential
|
# emoji, at the cost of increased vulnerability to homoglyph attacks and potential
|
||||||
# client compatibility problems, and the legacy mappings 'rfc1459' and
|
# client compatibility problems. we recommend leaving this value at its default;
|
||||||
# 'rfc1459-strict'. we recommend leaving this value at its default;
|
|
||||||
# however, note that changing it once the network is already up and running is
|
# however, note that changing it once the network is already up and running is
|
||||||
# problematic.
|
# problematic.
|
||||||
casemapping: "ascii"
|
casemapping: "ascii"
|
||||||
|
|
@ -219,10 +218,6 @@ server:
|
||||||
# - "192.168.1.1"
|
# - "192.168.1.1"
|
||||||
# - "192.168.10.1/24"
|
# - "192.168.10.1/24"
|
||||||
|
|
||||||
# whether to accept the hostname parameter on the WEBIRC line as the IRC hostname
|
|
||||||
# (the default/recommended Ergo configuration will use cloaks instead)
|
|
||||||
accept-hostname: false
|
|
||||||
|
|
||||||
# maximum length of clients' sendQ in bytes
|
# maximum length of clients' sendQ in bytes
|
||||||
# this should be big enough to hold bursts of channel/direct messages
|
# this should be big enough to hold bursts of channel/direct messages
|
||||||
max-sendq: 96k
|
max-sendq: 96k
|
||||||
|
|
@ -369,7 +364,7 @@ server:
|
||||||
# in a "closed-loop" system where you control the server and all the clients,
|
# in a "closed-loop" system where you control the server and all the clients,
|
||||||
# you may want to increase the maximum (non-tag) length of an IRC line from
|
# you may want to increase the maximum (non-tag) length of an IRC line from
|
||||||
# the default value of 512. DO NOT change this on a public server:
|
# the default value of 512. DO NOT change this on a public server:
|
||||||
max-line-len: 2048
|
# max-line-len: 512
|
||||||
|
|
||||||
# send all 0's as the LUSERS (user counts) output to non-operators; potentially useful
|
# send all 0's as the LUSERS (user counts) output to non-operators; potentially useful
|
||||||
# if you don't want to publicize how popular the server is
|
# if you don't want to publicize how popular the server is
|
||||||
|
|
@ -410,10 +405,6 @@ accounts:
|
||||||
sender: "admin@my.network"
|
sender: "admin@my.network"
|
||||||
require-tls: true
|
require-tls: true
|
||||||
helo-domain: "my.network" # defaults to server name if unset
|
helo-domain: "my.network" # defaults to server name if unset
|
||||||
# set to `tcp4` to force sending over IPv4, `tcp6` to force IPv6:
|
|
||||||
# protocol: "tcp4"
|
|
||||||
# set to force a specific source/local IPv4 or IPv6 address:
|
|
||||||
# local-address: "1.2.3.4"
|
|
||||||
# options to enable DKIM signing of outgoing emails (recommended, but
|
# options to enable DKIM signing of outgoing emails (recommended, but
|
||||||
# requires creating a DNS entry for the public key):
|
# requires creating a DNS entry for the public key):
|
||||||
# dkim:
|
# dkim:
|
||||||
|
|
@ -426,15 +417,8 @@ accounts:
|
||||||
# port: 25
|
# port: 25
|
||||||
# username: "admin"
|
# username: "admin"
|
||||||
# password: "hunter2"
|
# password: "hunter2"
|
||||||
# implicit-tls: false # TLS from the first byte, typically on port 465
|
blacklist-regexes:
|
||||||
# addresses that are not accepted for registration:
|
# - ".*@mailinator.com"
|
||||||
address-blacklist:
|
|
||||||
# - "*@mailinator.com"
|
|
||||||
address-blacklist-syntax: "glob" # change to "regex" for regular expressions
|
|
||||||
# file of newline-delimited address blacklist entries (no enclosing quotes)
|
|
||||||
# in the above syntax (i.e. either globs or regexes). supersedes
|
|
||||||
# address-blacklist if set:
|
|
||||||
# address-blacklist-file: "/path/to/address-blacklist-file"
|
|
||||||
timeout: 60s
|
timeout: 60s
|
||||||
# email-based password reset:
|
# email-based password reset:
|
||||||
password-reset:
|
password-reset:
|
||||||
|
|
@ -466,10 +450,6 @@ accounts:
|
||||||
# this is useful for compatibility with old clients that don't support SASL
|
# this is useful for compatibility with old clients that don't support SASL
|
||||||
login-via-pass-command: true
|
login-via-pass-command: true
|
||||||
|
|
||||||
# advertise the SCRAM-SHA-256 authentication method. set to false in case of
|
|
||||||
# compatibility issues with certain clients:
|
|
||||||
advertise-scram: true
|
|
||||||
|
|
||||||
# require-sasl controls whether clients are required to have accounts
|
# require-sasl controls whether clients are required to have accounts
|
||||||
# (and sign into them using SASL) to connect to the server
|
# (and sign into them using SASL) to connect to the server
|
||||||
require-sasl:
|
require-sasl:
|
||||||
|
|
@ -595,40 +575,6 @@ accounts:
|
||||||
# how many scripts are allowed to run at once? 0 for no limit:
|
# how many scripts are allowed to run at once? 0 for no limit:
|
||||||
max-concurrency: 64
|
max-concurrency: 64
|
||||||
|
|
||||||
# support for login via OAuth2 bearer tokens
|
|
||||||
oauth2:
|
|
||||||
enabled: false
|
|
||||||
# should we automatically create users on presentation of a valid token?
|
|
||||||
autocreate: true
|
|
||||||
# enable this to use auth-script for validation:
|
|
||||||
auth-script: false
|
|
||||||
introspection-url: "https://example.com/api/oidc/introspection"
|
|
||||||
introspection-timeout: 10s
|
|
||||||
# omit for auth method `none`; required for auth method `client_secret_basic`:
|
|
||||||
client-id: "ergo"
|
|
||||||
client-secret: "4TA0I7mJ3fUUcW05KJiODg"
|
|
||||||
|
|
||||||
# support for login via JWT bearer tokens
|
|
||||||
jwt-auth:
|
|
||||||
enabled: false
|
|
||||||
# should we automatically create users on presentation of a valid token?
|
|
||||||
autocreate: true
|
|
||||||
# any of these token definitions can be accepted, allowing for key rotation
|
|
||||||
tokens:
|
|
||||||
-
|
|
||||||
algorithm: "hmac" # either 'hmac', 'rsa', or 'eddsa' (ed25519)
|
|
||||||
# hmac takes a symmetric key, rsa and eddsa take PEM-encoded public keys;
|
|
||||||
# either way, the key can be specified either as a YAML string:
|
|
||||||
key: "nANiZ1De4v6WnltCHN2H7Q"
|
|
||||||
# or as a path to the file containing the key:
|
|
||||||
#key-file: "jwt_pubkey.pem"
|
|
||||||
# list of JWT claim names to search for the user's account name (make sure the format
|
|
||||||
# is what you expect, especially if using "sub"):
|
|
||||||
account-claims: ["preferred_username"]
|
|
||||||
# if a claim is formatted as an email address, require it to have the following domain,
|
|
||||||
# and then strip off the domain and use the local-part as the account name:
|
|
||||||
#strip-domain: "example.com"
|
|
||||||
|
|
||||||
# channel options
|
# channel options
|
||||||
channels:
|
channels:
|
||||||
# modes that are set when new channels are created
|
# modes that are set when new channels are created
|
||||||
|
|
@ -664,12 +610,6 @@ channels:
|
||||||
# (0 or omit for no expiration):
|
# (0 or omit for no expiration):
|
||||||
invite-expiration: 24h
|
invite-expiration: 24h
|
||||||
|
|
||||||
# channels that new clients will automatically join. this should be used with
|
|
||||||
# caution, since traditional IRC users will likely view it as an antifeature.
|
|
||||||
# it may be useful in small community networks that have a single "primary" channel:
|
|
||||||
#auto-join:
|
|
||||||
# - "#lounge"
|
|
||||||
|
|
||||||
# operator classes:
|
# operator classes:
|
||||||
# an operator has a single "class" (defining a privilege level), which can include
|
# an operator has a single "class" (defining a privilege level), which can include
|
||||||
# multiple "capabilities" (defining privileged actions they can take). all
|
# multiple "capabilities" (defining privileged actions they can take). all
|
||||||
|
|
@ -820,7 +760,7 @@ lock-file: "ircd.lock"
|
||||||
|
|
||||||
# datastore configuration
|
# datastore configuration
|
||||||
datastore:
|
datastore:
|
||||||
# path to the database file (used to store account and channel registrations):
|
# path to the datastore
|
||||||
path: ircd.db
|
path: ircd.db
|
||||||
|
|
||||||
# if the database schema requires an upgrade, `autoupgrade` will attempt to
|
# if the database schema requires an upgrade, `autoupgrade` will attempt to
|
||||||
|
|
@ -863,9 +803,6 @@ limits:
|
||||||
# identlen is the max ident length allowed
|
# identlen is the max ident length allowed
|
||||||
identlen: 20
|
identlen: 20
|
||||||
|
|
||||||
# realnamelen is the maximum realname length allowed
|
|
||||||
realnamelen: 150
|
|
||||||
|
|
||||||
# channellen is the max channel length allowed
|
# channellen is the max channel length allowed
|
||||||
channellen: 64
|
channellen: 64
|
||||||
|
|
||||||
|
|
@ -885,7 +822,7 @@ limits:
|
||||||
whowas-entries: 100
|
whowas-entries: 100
|
||||||
|
|
||||||
# maximum length of channel lists (beI modes)
|
# maximum length of channel lists (beI modes)
|
||||||
chan-list-modes: 100
|
chan-list-modes: 60
|
||||||
|
|
||||||
# maximum number of messages to accept during registration (prevents
|
# maximum number of messages to accept during registration (prevents
|
||||||
# DoS / resource exhaustion attacks):
|
# DoS / resource exhaustion attacks):
|
||||||
|
|
@ -1040,8 +977,7 @@ history:
|
||||||
|
|
||||||
# options to control how messages are stored and deleted:
|
# options to control how messages are stored and deleted:
|
||||||
retention:
|
retention:
|
||||||
# allow users to delete their own messages from history,
|
# allow users to delete their own messages from history?
|
||||||
# and channel operators to delete messages in their channel?
|
|
||||||
allow-individual-delete: false
|
allow-individual-delete: false
|
||||||
|
|
||||||
# if persistent history is enabled, create additional index tables,
|
# if persistent history is enabled, create additional index tables,
|
||||||
|
|
@ -1067,9 +1003,3 @@ history:
|
||||||
# whether to allow customization of the config at runtime using environment variables,
|
# whether to allow customization of the config at runtime using environment variables,
|
||||||
# e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
# e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
||||||
allow-environment-overrides: true
|
allow-environment-overrides: true
|
||||||
|
|
||||||
cef:
|
|
||||||
imagor:
|
|
||||||
url: "https://example.com/embed/"
|
|
||||||
secret: "secretgoeshere"
|
|
||||||
redis: "redis://user:password@localhost:6379/0?protocol=3"
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
include <tunables/global>
|
|
||||||
|
|
||||||
# Georg Pfuetzenreuter <georg+ergo@lysergic.dev>
|
|
||||||
# AppArmor confinement for ergo and ergo-ldap
|
|
||||||
|
|
||||||
profile ergo /usr/bin/ergo {
|
|
||||||
include <abstractions/base>
|
|
||||||
include <abstractions/consoles>
|
|
||||||
include <abstractions/nameservice>
|
|
||||||
|
|
||||||
/etc/ergo/ircd.{motd,yaml} r,
|
|
||||||
/etc/ssl/irc/{crt,key} r,
|
|
||||||
/etc/ssl/ergo/{crt,key} r,
|
|
||||||
/usr/bin/ergo mr,
|
|
||||||
/proc/sys/net/core/somaxconn r,
|
|
||||||
/sys/kernel/mm/transparent_hugepage/hpage_pmd_size r,
|
|
||||||
/usr/share/ergo/languages/{,*.lang.json,*.yaml} r,
|
|
||||||
owner /run/ergo/ircd.lock rwk,
|
|
||||||
owner /var/lib/ergo/ircd.db rw,
|
|
||||||
|
|
||||||
include if exists <local/ergo>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
profile ergo-ldap /usr/bin/ergo-ldap {
|
|
||||||
include <abstractions/openssl>
|
|
||||||
include <abstractions/ssl_certs>
|
|
||||||
|
|
||||||
/usr/bin/ergo-ldap rm,
|
|
||||||
/etc/ergo/ldap.yaml r,
|
|
||||||
|
|
||||||
include if exists <local/ergo-ldap>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
Ergo init script for bsd-rc
|
|
||||||
===
|
|
||||||
|
|
||||||
Written for and tested using FreeBSD.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Copy the `ergo` file from this folder to `/etc/rc.d/ergo`,
|
|
||||||
permissions should be `555`.
|
|
||||||
|
|
||||||
You should create a system user for Ergo.
|
|
||||||
This script defaults to running Ergo as a user named `ergo`,
|
|
||||||
but that can be changed using `/etc/rc.conf`.
|
|
||||||
|
|
||||||
Here are all `rc.conf` variables and their defaults:
|
|
||||||
- `ergo_enable`, defaults to `NO`. Whether to run `ergo` at system start.
|
|
||||||
- `ergo_user`, defaults to `ergo`. Run using this user.
|
|
||||||
- `ergo_group`, defaults to `ergo`. Run using this group.
|
|
||||||
- `ergo_chdir`, defaults to `/var/db/ergo`. Path to the working directory for the server. Should be writable for `ergo_user`.
|
|
||||||
- `ergo_conf`, defaults to `/usr/local/etc/ergo/ircd.yaml`. Config file path. Make sure `ergo_user` can read it.
|
|
||||||
|
|
||||||
This script assumes ergo to be installed at `/usr/local/bin/ergo`.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```shell
|
|
||||||
/etc/rc.d/ergo <command>
|
|
||||||
```
|
|
||||||
In addition to the obvious `start` and `stop` commands, this
|
|
||||||
script also has a `reload` command that sends `SIGHUP` to the Ergo process.
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# PROVIDE: ergo
|
|
||||||
# REQUIRE: DAEMON
|
|
||||||
# KEYWORD: shutdown
|
|
||||||
|
|
||||||
#
|
|
||||||
# Add the following lines to /etc/rc.conf to enable Ergo
|
|
||||||
#
|
|
||||||
# ergo_enable (bool): Set to YES to enable ergo.
|
|
||||||
# Default is "NO".
|
|
||||||
# ergo_user (user): Set user to run ergo.
|
|
||||||
# Default is "ergo".
|
|
||||||
# ergo_group (group): Set group to run ergo.
|
|
||||||
# Default is "ergo".
|
|
||||||
# ergo_config (file): Set ergo config file path.
|
|
||||||
# Default is "/usr/local/etc/ergo/config.yaml".
|
|
||||||
# ergo_chdir (dir): Set ergo working directory
|
|
||||||
# Default is "/var/db/ergo".
|
|
||||||
|
|
||||||
. /etc/rc.subr
|
|
||||||
|
|
||||||
name=ergo
|
|
||||||
rcvar=ergo_enable
|
|
||||||
desc="Ergo IRCv3 server"
|
|
||||||
|
|
||||||
load_rc_config "$name"
|
|
||||||
|
|
||||||
: ${ergo_enable:=NO}
|
|
||||||
: ${ergo_user:=ergo}
|
|
||||||
: ${ergo_group:=ergo}
|
|
||||||
: ${ergo_chdir:=/var/db/ergo}
|
|
||||||
: ${ergo_conf:=/usr/local/etc/ergo/ircd.yaml}
|
|
||||||
|
|
||||||
# If you don't define a custom reload function,
|
|
||||||
# rc automagically sends SIGHUP to the process on reload.
|
|
||||||
# But you have to list reload as an extra_command for that.
|
|
||||||
extra_commands="reload"
|
|
||||||
|
|
||||||
procname="/usr/local/bin/${name}"
|
|
||||||
command=/usr/sbin/daemon
|
|
||||||
command_args="-S -T ${name} ${procname} run --conf ${ergo_conf}"
|
|
||||||
|
|
||||||
run_rc_command "$1"
|
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ certificates. To get a working ircd, all you need to do is run the image and
|
||||||
expose the ports:
|
expose the ports:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run --init --name ergo -d -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable
|
docker run --name ergo -d -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start Ergo and listen on ports 6667 (plain text) and 6697 (TLS).
|
This will start Ergo and listen on ports 6667 (plain text) and 6697 (TLS).
|
||||||
|
|
@ -38,11 +38,6 @@ You should see a line similar to:
|
||||||
Oper username:password is admin:cnn2tm9TP3GeI4vLaEMS
|
Oper username:password is admin:cnn2tm9TP3GeI4vLaEMS
|
||||||
```
|
```
|
||||||
|
|
||||||
We recommend the use of `--init` (`init: true` in docker-compose) to solve an
|
|
||||||
edge case involving unreaped zombie processes when Ergo's script API is used
|
|
||||||
for authentication or IP validation. For more details, see
|
|
||||||
[krallin/tini#8](https://github.com/krallin/tini/issues/8).
|
|
||||||
|
|
||||||
## Persisting data
|
## Persisting data
|
||||||
|
|
||||||
Ergo has a persistent data store, used to keep account details, channel
|
Ergo has a persistent data store, used to keep account details, channel
|
||||||
|
|
@ -53,14 +48,14 @@ For example, to create a new docker volume and then mount it:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker volume create ergo-data
|
docker volume create ergo-data
|
||||||
docker run --init -d -v ergo-data:/ircd -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable
|
docker run -d -v ergo-data:/ircd -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable
|
||||||
```
|
```
|
||||||
|
|
||||||
Or to mount a folder from your host machine:
|
Or to mount a folder from your host machine:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
mkdir ergo-data
|
mkdir ergo-data
|
||||||
docker run --init -d -v $(PWD)/ergo-data:/ircd -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable
|
docker run -d -v $(PWD)/ergo-data:/ircd -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable
|
||||||
```
|
```
|
||||||
|
|
||||||
## Customising the config
|
## Customising the config
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ergo:
|
ergo:
|
||||||
init: true
|
|
||||||
image: ghcr.io/ergochat/ergo:stable
|
image: ghcr.io/ergochat/ergo:stable
|
||||||
ports:
|
ports:
|
||||||
- "6667:6667/tcp"
|
- "6667:6667/tcp"
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,6 @@ _Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn
|
||||||
- [Migrating from Anope or Atheme](#migrating-from-anope-or-atheme)
|
- [Migrating from Anope or Atheme](#migrating-from-anope-or-atheme)
|
||||||
- [HOPM](#hopm)
|
- [HOPM](#hopm)
|
||||||
- [Tor](#tor)
|
- [Tor](#tor)
|
||||||
- [I2P](#i2p)
|
|
||||||
- [ZNC](#znc)
|
- [ZNC](#znc)
|
||||||
- [External authentication systems](#external-authentication-systems)
|
- [External authentication systems](#external-authentication-systems)
|
||||||
- [DNSBLs and other IP checking systems](#dnsbls-and-other-ip-checking-systems)
|
- [DNSBLs and other IP checking systems](#dnsbls-and-other-ip-checking-systems)
|
||||||
|
|
@ -183,8 +182,8 @@ The recommended way to operate ergo as a service on Linux is via systemd. This p
|
||||||
|
|
||||||
The only major distribution that currently packages Ergo is Arch Linux; the aforementioned AUR package includes a systemd unit file. However, it should be fairly straightforward to set up a productionized Ergo on any Linux distribution. Here's a quickstart guide for Debian/Ubuntu:
|
The only major distribution that currently packages Ergo is Arch Linux; the aforementioned AUR package includes a systemd unit file. However, it should be fairly straightforward to set up a productionized Ergo on any Linux distribution. Here's a quickstart guide for Debian/Ubuntu:
|
||||||
|
|
||||||
1. Create a dedicated, unprivileged role user who will own the ergo process and all its associated files: `adduser --system --group --home=/home/ergo ergo`. This user now has a home directory at `/home/ergo`. To prevent other users from viewing Ergo's configuration file, database, and certificates, restrict the permissions on the home directory: `chmod 0700 /home/ergo`.
|
1. Create a dedicated, unprivileged role user who will own the ergo process and all its associated files: `adduser --system --group ergo`. This user now has a home directory at `/home/ergo`. To prevent other users from viewing Ergo's configuration file, database, and certificates, restrict the permissions on the home directory: `chmod 0700 /home/ergo`.
|
||||||
1. Copy the executable binary `ergo`, the config file `ircd.yaml`, the database `ircd.db`, and the self-signed TLS certificate (`fullchain.pem` and `privkey.pem`) to `/home/ergo`. (If you don't have an `ircd.db`, it will be auto-created as `/home/ergo/ircd.db` on first launch.) Ensure that they are all owned by the new ergo role user: `sudo chown -R ergo:ergo /home/ergo`. Ensure that the configuration file logs to stderr.
|
1. Copy the executable binary `ergo`, the config file `ircd.yaml`, the database `ircd.db`, and the self-signed TLS certificate (`fullchain.pem` and `privkey.pem`) to `/home/ergo`. (If you don't have an `ircd.db`, it will be auto-created as `/home/ergo/ircd.db` on first launch.) Ensure that they are all owned by the new ergo role user: `sudo chown ergo:ergo /home/ergo/*`. Ensure that the configuration file logs to stderr.
|
||||||
1. Install our example [ergo.service](https://github.com/ergochat/ergo/blob/stable/distrib/systemd/ergo.service) file to `/etc/systemd/system/ergo.service`.
|
1. Install our example [ergo.service](https://github.com/ergochat/ergo/blob/stable/distrib/systemd/ergo.service) file to `/etc/systemd/system/ergo.service`.
|
||||||
1. Enable and start the new service with the following commands:
|
1. Enable and start the new service with the following commands:
|
||||||
1. `systemctl daemon-reload`
|
1. `systemctl daemon-reload`
|
||||||
|
|
@ -624,8 +623,6 @@ Many clients do not have this support. However, you can designate port 6667 as a
|
||||||
|
|
||||||
Ergo supports the use of reverse proxies (such as nginx, or a Kubernetes [LoadBalancer](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer)) that sit between it and the client. In these deployments, the [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) is used to pass the end user's IP through to Ergo. These proxies can be used to terminate TLS externally to Ergo, e.g., if you need to support versions of the TLS protocol that are not implemented natively by Go, or if you want to consolidate your certificate management into a single nginx instance.
|
Ergo supports the use of reverse proxies (such as nginx, or a Kubernetes [LoadBalancer](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer)) that sit between it and the client. In these deployments, the [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) is used to pass the end user's IP through to Ergo. These proxies can be used to terminate TLS externally to Ergo, e.g., if you need to support versions of the TLS protocol that are not implemented natively by Go, or if you want to consolidate your certificate management into a single nginx instance.
|
||||||
|
|
||||||
### IRC Sockets
|
|
||||||
|
|
||||||
The first step is to add the reverse proxy's IP to `proxy-allowed-from` and `ip-limits.exempted`. (Use `localhost` to exempt all loopback IPs and Unix domain sockets.)
|
The first step is to add the reverse proxy's IP to `proxy-allowed-from` and `ip-limits.exempted`. (Use `localhost` to exempt all loopback IPs and Unix domain sockets.)
|
||||||
|
|
||||||
After that, there are two possibilities:
|
After that, there are two possibilities:
|
||||||
|
|
@ -641,10 +638,6 @@ After that, there are two possibilities:
|
||||||
proxy: true
|
proxy: true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Websockets through HTTP reverse proxies
|
|
||||||
|
|
||||||
Ergo will honor the `X-Forwarded-For` headers on incoming websocket connections, if the peer IP address appears in `proxy-allowed-from`. For these connections, set `proxy: false`, or omit the `proxy` option.
|
|
||||||
|
|
||||||
|
|
||||||
## Client certificates
|
## Client certificates
|
||||||
|
|
||||||
|
|
@ -1024,24 +1017,6 @@ or with Gamja, create a new `config.json` (in the base directory of the Gamja in
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
On Apache 2.4.47 or higher, websocket proxying can be configured with:
|
|
||||||
|
|
||||||
```
|
|
||||||
RequestHeader setifempty X-Forwarded-Proto https
|
|
||||||
|
|
||||||
ProxyPreserveHost On
|
|
||||||
ProxyPass /webirc http://127.0.0.1:8067 upgrade=websocket
|
|
||||||
ProxyPassReverse /webirc http://127.0.0.1:8067
|
|
||||||
```
|
|
||||||
|
|
||||||
On Caddy, websocket proxying can be configured with:
|
|
||||||
|
|
||||||
```
|
|
||||||
handle_path /webirc {
|
|
||||||
reverse_proxy 127.0.0.1:8067
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migrating from Anope or Atheme
|
## Migrating from Anope or Atheme
|
||||||
|
|
||||||
You can import user and channel registrations from an Anope or Atheme database into a new Ergo database (not all features are supported). Use the following steps:
|
You can import user and channel registrations from an Anope or Atheme database into a new Ergo database (not all features are supported). Use the following steps:
|
||||||
|
|
@ -1143,16 +1118,6 @@ Instructions on how client software should connect to an .onion address are outs
|
||||||
1. [Hexchat](https://hexchat.github.io/) is known to support .onion addresses, once it has been configured to use a local Tor daemon as a SOCKS proxy (Settings -> Preferences -> Network Setup -> Proxy Server).
|
1. [Hexchat](https://hexchat.github.io/) is known to support .onion addresses, once it has been configured to use a local Tor daemon as a SOCKS proxy (Settings -> Preferences -> Network Setup -> Proxy Server).
|
||||||
1. Pidgin should work with [torsocks](https://trac.torproject.org/projects/tor/wiki/doc/torsocks).
|
1. Pidgin should work with [torsocks](https://trac.torproject.org/projects/tor/wiki/doc/torsocks).
|
||||||
|
|
||||||
## I2P
|
|
||||||
|
|
||||||
I2P is an anonymizing overlay network similar to Tor. The recommended configuration for I2P is to treat it similarly to Tor: have the i2pd reverse proxy its connections to an Ergo listener configured with `tor: true`. See the [i2pd configuration guide](https://i2pd.readthedocs.io/en/latest/tutorials/irc/#running-anonymous-irc-server) for more details; note that the instructions to separate I2P traffic from other localhost traffic are unnecessary for a `tor: true` listener.
|
|
||||||
|
|
||||||
I2P can additionally expose an opaque client identifier (the user's "b32 address"). Exposing this identifier via Ergo is not recommended, but if you wish to do so, you can use the following procedure:
|
|
||||||
|
|
||||||
1. Enable WEBIRC support in the i2pd configuration by adding the `webircpassword` key to the [i2pd server block](https://i2pd.readthedocs.io/en/latest/tutorials/irc/#running-anonymous-irc-server)
|
|
||||||
1. Remove `tor: true` from the relevant Ergo listener config
|
|
||||||
1. Enable WEBIRC support in Ergo (starting from the default/recommended configuration, find the existing webirc block, delete the `certfp` configuration, change `password` to use the output of `ergo genpasswd` on the password you configured i2pd to send, and set `accept-hostname: true`)
|
|
||||||
1. To prevent Ergo from overwriting the hostname as passed from i2pd, set the following options: `server.ip-cloaking.enabled: false` and `server.lookup-hostnames: false`. (There is currently no support for applying cloaks to regular IP traffic but displaying the b32 address for I2P traffic).
|
|
||||||
|
|
||||||
## ZNC
|
## ZNC
|
||||||
|
|
||||||
|
|
|
||||||
36
ergo.go
36
ergo.go
|
|
@ -7,7 +7,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -15,7 +14,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/term"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
|
||||||
"github.com/docopt/docopt-go"
|
"github.com/docopt/docopt-go"
|
||||||
"github.com/ergochat/ergo/irc"
|
"github.com/ergochat/ergo/irc"
|
||||||
|
|
@ -27,17 +26,20 @@ import (
|
||||||
var commit = "" // git hash
|
var commit = "" // git hash
|
||||||
var version = "" // tagged version
|
var version = "" // tagged version
|
||||||
|
|
||||||
//go:embed default.yaml
|
|
||||||
var defaultConfig string
|
|
||||||
|
|
||||||
// get a password from stdin from the user
|
// get a password from stdin from the user
|
||||||
func getPasswordFromTerminal() string {
|
func getPassword() string {
|
||||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
fd := int(os.Stdin.Fd())
|
||||||
|
if terminal.IsTerminal(fd) {
|
||||||
|
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error reading password:", err.Error())
|
log.Fatal("Error reading password:", err.Error())
|
||||||
}
|
}
|
||||||
return string(bytePassword)
|
return string(bytePassword)
|
||||||
}
|
}
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
text, _ := reader.ReadString('\n')
|
||||||
|
return strings.TrimSpace(text)
|
||||||
|
}
|
||||||
|
|
||||||
func fileDoesNotExist(file string) bool {
|
func fileDoesNotExist(file string) bool {
|
||||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||||
|
|
@ -98,7 +100,6 @@ Usage:
|
||||||
ergo importdb <database.json> [--conf <filename>] [--quiet]
|
ergo importdb <database.json> [--conf <filename>] [--quiet]
|
||||||
ergo genpasswd [--conf <filename>] [--quiet]
|
ergo genpasswd [--conf <filename>] [--quiet]
|
||||||
ergo mkcerts [--conf <filename>] [--quiet]
|
ergo mkcerts [--conf <filename>] [--quiet]
|
||||||
ergo defaultconfig
|
|
||||||
ergo run [--conf <filename>] [--quiet] [--smoke]
|
ergo run [--conf <filename>] [--quiet] [--smoke]
|
||||||
ergo -h | --help
|
ergo -h | --help
|
||||||
ergo --version
|
ergo --version
|
||||||
|
|
@ -113,20 +114,19 @@ Options:
|
||||||
// don't require a config file for genpasswd
|
// don't require a config file for genpasswd
|
||||||
if arguments["genpasswd"].(bool) {
|
if arguments["genpasswd"].(bool) {
|
||||||
var password string
|
var password string
|
||||||
if term.IsTerminal(int(syscall.Stdin)) {
|
fd := int(os.Stdin.Fd())
|
||||||
|
if terminal.IsTerminal(fd) {
|
||||||
fmt.Print("Enter Password: ")
|
fmt.Print("Enter Password: ")
|
||||||
password = getPasswordFromTerminal()
|
password = getPassword()
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
fmt.Print("Reenter Password: ")
|
fmt.Print("Reenter Password: ")
|
||||||
confirm := getPasswordFromTerminal()
|
confirm := getPassword()
|
||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
if confirm != password {
|
if confirm != password {
|
||||||
log.Fatal("passwords do not match")
|
log.Fatal("passwords do not match")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
password = getPassword()
|
||||||
text, _ := reader.ReadString('\n')
|
|
||||||
password = strings.TrimSpace(text)
|
|
||||||
}
|
}
|
||||||
if err := irc.ValidatePassphrase(password); err != nil {
|
if err := irc.ValidatePassphrase(password); err != nil {
|
||||||
log.Printf("WARNING: this password contains characters that may cause problems with your IRC client software.\n")
|
log.Printf("WARNING: this password contains characters that may cause problems with your IRC client software.\n")
|
||||||
|
|
@ -136,10 +136,10 @@ Options:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("encoding error:", err.Error())
|
log.Fatal("encoding error:", err.Error())
|
||||||
}
|
}
|
||||||
fmt.Println(string(hash))
|
fmt.Print(string(hash))
|
||||||
return
|
if terminal.IsTerminal(fd) {
|
||||||
} else if arguments["defaultconfig"].(bool) {
|
fmt.Println()
|
||||||
fmt.Print(defaultConfig)
|
}
|
||||||
return
|
return
|
||||||
} else if arguments["mkcerts"].(bool) {
|
} else if arguments["mkcerts"].(bool) {
|
||||||
doMkcerts(arguments["--conf"].(string), arguments["--quiet"].(bool))
|
doMkcerts(arguments["--conf"].(string), arguments["--quiet"].(bool))
|
||||||
|
|
|
||||||
|
|
@ -87,12 +87,6 @@ CAPDEFS = [
|
||||||
url="https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6",
|
url="https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6",
|
||||||
standard="proposed IRCv3",
|
standard="proposed IRCv3",
|
||||||
),
|
),
|
||||||
CapDef(
|
|
||||||
identifier="MessageRedaction",
|
|
||||||
name="draft/message-redaction",
|
|
||||||
url="https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md",
|
|
||||||
standard="proposed IRCv3",
|
|
||||||
),
|
|
||||||
CapDef(
|
CapDef(
|
||||||
identifier="MessageTags",
|
identifier="MessageTags",
|
||||||
name="message-tags",
|
name="message-tags",
|
||||||
|
|
@ -201,30 +195,6 @@ CAPDEFS = [
|
||||||
url="https://github.com/ircv3/ircv3-specifications/pull/503",
|
url="https://github.com/ircv3/ircv3-specifications/pull/503",
|
||||||
standard="proposed IRCv3",
|
standard="proposed IRCv3",
|
||||||
),
|
),
|
||||||
CapDef(
|
|
||||||
identifier="Preaway",
|
|
||||||
name="draft/pre-away",
|
|
||||||
url="https://github.com/ircv3/ircv3-specifications/pull/514",
|
|
||||||
standard="proposed IRCv3",
|
|
||||||
),
|
|
||||||
CapDef(
|
|
||||||
identifier="StandardReplies",
|
|
||||||
name="standard-replies",
|
|
||||||
url="https://github.com/ircv3/ircv3-specifications/pull/506",
|
|
||||||
standard="IRCv3",
|
|
||||||
),
|
|
||||||
CapDef(
|
|
||||||
identifier="NoImplicitNames",
|
|
||||||
name="draft/no-implicit-names",
|
|
||||||
url="https://github.com/ircv3/ircv3-specifications/pull/527",
|
|
||||||
standard="proposed IRCv3",
|
|
||||||
),
|
|
||||||
CapDef(
|
|
||||||
identifier="ExtendedISupport",
|
|
||||||
name="draft/extended-isupport",
|
|
||||||
url="https://github.com/ircv3/ircv3-specifications/pull/543",
|
|
||||||
standard="proposed IRCv3",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_defs():
|
def validate_defs():
|
||||||
|
|
@ -260,7 +230,7 @@ package caps
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = %d
|
numCapabs = %d
|
||||||
// length of the uint32 array that represents the bitset:
|
// length of the uint64 array that represents the bitset:
|
||||||
bitsetLen = %d
|
bitsetLen = %d
|
||||||
)
|
)
|
||||||
""" % (numCapabs, bitsetLen), file=output)
|
""" % (numCapabs, bitsetLen), file=output)
|
||||||
|
|
|
||||||
33
go.mod
33
go.mod
|
|
@ -1,48 +1,43 @@
|
||||||
module github.com/ergochat/ergo
|
module github.com/ergochat/ergo
|
||||||
|
|
||||||
go 1.23
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
|
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
|
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
|
||||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
|
github.com/ergochat/go-ident v0.0.0-20200511222032-830550b1d775
|
||||||
github.com/ergochat/irc-go v0.5.0-rc2
|
github.com/ergochat/irc-go v0.1.0
|
||||||
github.com/go-sql-driver/mysql v1.7.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/go-test/deep v1.0.6 // indirect
|
github.com/go-test/deep v1.0.6 // indirect
|
||||||
github.com/gofrs/flock v0.8.1
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
|
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
|
||||||
github.com/onsi/ginkgo v1.12.0 // indirect
|
github.com/onsi/ginkgo v1.12.0 // indirect
|
||||||
github.com/onsi/gomega v1.9.0 // indirect
|
github.com/onsi/gomega v1.9.0 // indirect
|
||||||
github.com/tidwall/buntdb v1.3.1
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
|
github.com/tidwall/buntdb v1.2.9
|
||||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208
|
||||||
github.com/xdg-go/scram v1.0.2
|
github.com/xdg-go/scram v1.0.2
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
||||||
golang.org/x/term v0.22.0
|
golang.org/x/text v0.3.7
|
||||||
golang.org/x/text v0.16.0
|
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require github.com/gofrs/flock v0.8.1
|
||||||
github.com/cshum/imagor v1.4.13
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
|
||||||
github.com/redis/go-redis/v9 v9.6.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/tidwall/btree v1.1.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/tidwall/gjson v1.12.1 // indirect
|
||||||
github.com/tidwall/btree v1.4.2 // indirect
|
|
||||||
github.com/tidwall/gjson v1.14.3 // indirect
|
|
||||||
github.com/tidwall/grect v0.1.4 // indirect
|
github.com/tidwall/grect v0.1.4 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tidwall/rtred v0.1.2 // indirect
|
github.com/tidwall/rtred v0.1.2 // indirect
|
||||||
github.com/tidwall/tinyqueue v0.1.1 // indirect
|
github.com/tidwall/tinyqueue v0.1.1 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1
|
replace github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1
|
||||||
|
|
|
||||||
88
go.sum
88
go.sum
|
|
@ -2,41 +2,31 @@ code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9
|
||||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
|
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/cshum/imagor v1.4.13 h1:BFcSpsTUOJj+Wv5SzDeXa8bhsT/Ehw7EcrFD0UTdpmU=
|
|
||||||
github.com/cshum/imagor v1.4.13/go.mod h1:LHxXgks6Y06GzEHitnlO8vcD5gznxIHWPdvGsnlGpMo=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons=
|
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/confusables v0.0.0-20201108231250-4ab98ab61fb1/go.mod h1:mov+uh1DPWsltdQnOdzn08UO9GsJ3MEvhtu0Ci37fdk=
|
||||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5AnBVGzTXM/yHVytt48rXBGcJGzSbms=
|
github.com/ergochat/go-ident v0.0.0-20200511222032-830550b1d775 h1:QSJIdpr3HOzJDPwxT7hp7WbjoZcS+5GqVvsBscqChk0=
|
||||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881/go.mod h1:ASYJtQujNitna6cVHsNQTGrfWvMPJ5Sa2lZlmsH65uM=
|
github.com/ergochat/go-ident v0.0.0-20200511222032-830550b1d775/go.mod h1:d2qvgjD0TvGNSvUs+mZgX090RiJlrzUYW6vtANGOy3A=
|
||||||
github.com/ergochat/irc-go v0.5.0-rc1 h1:kFoIHExoNFQ2CV+iShAVna/H4xrXQB4t4jK5Sep2j9k=
|
github.com/ergochat/irc-go v0.0.0-20210617222258-256f1601d3ce h1:RfyjeynouKZjmnN8WGzCSrtuHGZ9dwfSYBq405FPoqs=
|
||||||
github.com/ergochat/irc-go v0.5.0-rc1/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
|
github.com/ergochat/irc-go v0.0.0-20210617222258-256f1601d3ce/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
|
||||||
github.com/ergochat/irc-go v0.5.0-rc2 h1:VuSQJF5K4hWvYSzGa4b8vgL6kzw8HF6LSOejE+RWpAo=
|
github.com/ergochat/irc-go v0.1.0 h1:jBHUayERH9SiPOWe4ePDWRztBjIQsU/jwLbbGUuiOWM=
|
||||||
github.com/ergochat/irc-go v0.5.0-rc2/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
|
github.com/ergochat/irc-go v0.1.0/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
|
||||||
github.com/ergochat/scram v1.0.2-ergo1 h1:2bYXiRFQH636pT0msOG39fmEYl4Eq+OuutcyDsCix/g=
|
github.com/ergochat/scram v1.0.2-ergo1 h1:2bYXiRFQH636pT0msOG39fmEYl4Eq+OuutcyDsCix/g=
|
||||||
github.com/ergochat/scram v1.0.2-ergo1/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
github.com/ergochat/scram v1.0.2-ergo1/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||||
github.com/ergochat/websocket v1.4.2-oragono1 h1:plMUunFBM6UoSCIYCKKclTdy/TkkHfUslhOfJQzfueM=
|
github.com/ergochat/websocket v1.4.2-oragono1 h1:plMUunFBM6UoSCIYCKKclTdy/TkkHfUslhOfJQzfueM=
|
||||||
github.com/ergochat/websocket v1.4.2-oragono1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/ergochat/websocket v1.4.2-oragono1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
||||||
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
|
@ -50,19 +40,25 @@ github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
|
||||||
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
|
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
|
||||||
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
|
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
|
||||||
github.com/tidwall/btree v1.4.2 h1:PpkaieETJMUxYNADsjgtNRcERX7mGc/GP2zp/r5FM3g=
|
github.com/tidwall/btree v0.6.1 h1:75VVgBeviiDO+3g4U+7+BaNBNhNINxB0ULPT3fs9pMY=
|
||||||
github.com/tidwall/btree v1.4.2/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE=
|
github.com/tidwall/btree v0.6.1/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
|
||||||
github.com/tidwall/buntdb v1.3.1 h1:HKoDF01/aBhl9RjYtbaLnvX9/OuenwvQiC3OP1CcL4o=
|
github.com/tidwall/btree v1.1.0 h1:5P+9WU8ui5uhmcg3SoPyTwoI0mVyZ1nps7YQzTZFkYM=
|
||||||
github.com/tidwall/buntdb v1.3.1/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
|
github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
|
||||||
|
github.com/tidwall/buntdb v1.2.7 h1:SIyObKAymzLyGhDeIhVk2Yc1/EwfCC75Uyu77CHlVoA=
|
||||||
|
github.com/tidwall/buntdb v1.2.7/go.mod h1:b6KvZM27x/8JLI5hgRhRu60pa3q0Tz9c50TyD46OHUM=
|
||||||
|
github.com/tidwall/buntdb v1.2.9 h1:XVz684P7X6HCTrdr385yDZWB1zt/n20ZNG3M1iGyFm4=
|
||||||
|
github.com/tidwall/buntdb v1.2.9/go.mod h1:IwyGSvvDg6hnKSIhtdZ0AqhCZGH8ukdtCAzaP8fI1X4=
|
||||||
|
github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
|
||||||
|
github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo=
|
||||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
github.com/tidwall/grect v0.1.3 h1:z9YwQAMUxVSBde3b7Sl8Da37rffgNfZ6Fq6h9t6KdXE=
|
||||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/grect v0.1.3/go.mod h1:8GMjwh3gPZVpLBI/jDz9uslCe0dpxRpWDdtN0lWAS/E=
|
||||||
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
|
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
|
||||||
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
|
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
|
||||||
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
|
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
|
||||||
|
|
@ -81,22 +77,27 @@ github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
|
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
|
||||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8 h1:5QRxNnVsaJP6NAse0UdkRgL3zHMvCRRkrDVLNdNpdy4=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||||
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|
@ -106,8 +107,7 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
|
||||||
100
irc/accounts.go
100
irc/accounts.go
|
|
@ -4,7 +4,6 @@
|
||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -24,7 +23,6 @@ import (
|
||||||
"github.com/ergochat/ergo/irc/email"
|
"github.com/ergochat/ergo/irc/email"
|
||||||
"github.com/ergochat/ergo/irc/migrations"
|
"github.com/ergochat/ergo/irc/migrations"
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/oauth2"
|
|
||||||
"github.com/ergochat/ergo/irc/passwd"
|
"github.com/ergochat/ergo/irc/passwd"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -41,6 +39,7 @@ const (
|
||||||
keyAccountSettings = "account.settings %s"
|
keyAccountSettings = "account.settings %s"
|
||||||
keyAccountVHost = "account.vhost %s"
|
keyAccountVHost = "account.vhost %s"
|
||||||
keyCertToAccount = "account.creds.certfp %s"
|
keyCertToAccount = "account.creds.certfp %s"
|
||||||
|
keyAccountChannels = "account.channels %s" // channels registered to the account
|
||||||
keyAccountLastSeen = "account.lastseen %s"
|
keyAccountLastSeen = "account.lastseen %s"
|
||||||
keyAccountReadMarkers = "account.readmarkers %s"
|
keyAccountReadMarkers = "account.readmarkers %s"
|
||||||
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
||||||
|
|
@ -1429,74 +1428,6 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) AuthenticateByBearerToken(client *Client, tokenType, token string) (err error) {
|
|
||||||
switch tokenType {
|
|
||||||
case "oauth2":
|
|
||||||
return am.AuthenticateByOAuthBearer(client, oauth2.OAuthBearerOptions{Token: token})
|
|
||||||
case "jwt":
|
|
||||||
return am.AuthenticateByJWT(client, token)
|
|
||||||
default:
|
|
||||||
return errInvalidBearerTokenType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *AccountManager) AuthenticateByOAuthBearer(client *Client, opts oauth2.OAuthBearerOptions) (err error) {
|
|
||||||
config := am.server.Config()
|
|
||||||
|
|
||||||
if !config.Accounts.OAuth2.Enabled {
|
|
||||||
return errFeatureDisabled
|
|
||||||
}
|
|
||||||
|
|
||||||
if throttled, remainingTime := client.checkLoginThrottle(); throttled {
|
|
||||||
return &ThrottleError{remainingTime}
|
|
||||||
}
|
|
||||||
|
|
||||||
var username string
|
|
||||||
if config.Accounts.AuthScript.Enabled && config.Accounts.OAuth2.AuthScript {
|
|
||||||
username, err = am.authenticateByOAuthBearerScript(client, config, opts)
|
|
||||||
} else {
|
|
||||||
username, err = config.Accounts.OAuth2.Introspect(context.Background(), opts.Token)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := am.loadWithAutocreation(username, config.Accounts.OAuth2.Autocreate)
|
|
||||||
if err == nil {
|
|
||||||
am.Login(client, account)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *AccountManager) AuthenticateByJWT(client *Client, token string) (err error) {
|
|
||||||
config := am.server.Config()
|
|
||||||
// enabled check is encapsulated here:
|
|
||||||
accountName, err := config.Accounts.JWTAuth.Validate(token)
|
|
||||||
if err != nil {
|
|
||||||
am.server.logger.Debug("accounts", "invalid JWT token", err.Error())
|
|
||||||
return errAccountInvalidCredentials
|
|
||||||
}
|
|
||||||
account, err := am.loadWithAutocreation(accountName, config.Accounts.JWTAuth.Autocreate)
|
|
||||||
if err == nil {
|
|
||||||
am.Login(client, account)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *AccountManager) authenticateByOAuthBearerScript(client *Client, config *Config, opts oauth2.OAuthBearerOptions) (username string, err error) {
|
|
||||||
output, err := CheckAuthScript(am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig,
|
|
||||||
AuthScriptInput{OAuthBearer: &opts, IP: client.IP().String()})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
|
||||||
return "", oauth2.ErrInvalidToken
|
|
||||||
} else if output.Success {
|
|
||||||
return output.AccountName, nil
|
|
||||||
} else {
|
|
||||||
return "", oauth2.ErrInvalidToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllNicks returns the uncasefolded nicknames for all accounts, including additional (grouped) nicks.
|
// AllNicks returns the uncasefolded nicknames for all accounts, including additional (grouped) nicks.
|
||||||
func (am *AccountManager) AllNicks() (result []string) {
|
func (am *AccountManager) AllNicks() (result []string) {
|
||||||
accountNamePrefix := fmt.Sprintf(keyAccountName, "")
|
accountNamePrefix := fmt.Sprintf(keyAccountName, "")
|
||||||
|
|
@ -1834,6 +1765,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||||
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
||||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||||
|
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
||||||
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
|
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
|
||||||
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
||||||
readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
|
readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
|
||||||
|
|
@ -1849,9 +1781,10 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||||
am.killClients(clients)
|
am.killClients(clients)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var registeredChannels []string
|
||||||
// on our way out, unregister all the account's channels and delete them from the db
|
// on our way out, unregister all the account's channels and delete them from the db
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, channelName := range am.server.channels.ChannelsForAccount(casefoldedAccount) {
|
for _, channelName := range registeredChannels {
|
||||||
err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
|
err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", "couldn't unregister channel", channelName, err.Error())
|
am.server.logger.Error("internal", "couldn't unregister channel", channelName, err.Error())
|
||||||
|
|
@ -1866,6 +1799,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||||
defer am.serialCacheUpdateMutex.Unlock()
|
defer am.serialCacheUpdateMutex.Unlock()
|
||||||
|
|
||||||
var accountName string
|
var accountName string
|
||||||
|
var channelsStr string
|
||||||
keepProtections := false
|
keepProtections := false
|
||||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
// get the unfolded account name; for an active account, this is
|
// get the unfolded account name; for an active account, this is
|
||||||
|
|
@ -1893,6 +1827,8 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||||
credText, err = tx.Get(credentialsKey)
|
credText, err = tx.Get(credentialsKey)
|
||||||
tx.Delete(credentialsKey)
|
tx.Delete(credentialsKey)
|
||||||
tx.Delete(vhostKey)
|
tx.Delete(vhostKey)
|
||||||
|
channelsStr, _ = tx.Get(channelsKey)
|
||||||
|
tx.Delete(channelsKey)
|
||||||
tx.Delete(joinedChannelsKey)
|
tx.Delete(joinedChannelsKey)
|
||||||
tx.Delete(lastSeenKey)
|
tx.Delete(lastSeenKey)
|
||||||
tx.Delete(readMarkersKey)
|
tx.Delete(readMarkersKey)
|
||||||
|
|
@ -1922,6 +1858,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||||
|
|
||||||
skeleton, _ := Skeleton(accountName)
|
skeleton, _ := Skeleton(accountName)
|
||||||
additionalNicks := unmarshalReservedNicks(rawNicks)
|
additionalNicks := unmarshalReservedNicks(rawNicks)
|
||||||
|
registeredChannels = unmarshalRegisteredChannels(channelsStr)
|
||||||
|
|
||||||
am.Lock()
|
am.Lock()
|
||||||
defer am.Unlock()
|
defer am.Unlock()
|
||||||
|
|
@ -1953,6 +1890,21 @@ func unmarshalRegisteredChannels(channelsStr string) (result []string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (am *AccountManager) ChannelsForAccount(account string) (channels []string) {
|
||||||
|
cfaccount, err := CasefoldName(account)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var channelStr string
|
||||||
|
key := fmt.Sprintf(keyAccountChannels, cfaccount)
|
||||||
|
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
channelStr, _ = tx.Get(key)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return unmarshalRegisteredChannels(channelStr)
|
||||||
|
}
|
||||||
|
|
||||||
func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
|
func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
|
||||||
if certfp == "" {
|
if certfp == "" {
|
||||||
return errAccountInvalidCredentials
|
return errAccountInvalidCredentials
|
||||||
|
|
@ -2009,11 +1961,9 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if authzid != "" {
|
if authzid != "" && authzid != account {
|
||||||
if cfAuthzid, err := CasefoldName(authzid); err != nil || cfAuthzid != account {
|
|
||||||
return errAuthzidAuthcidMismatch
|
return errAuthzidAuthcidMismatch
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ok, we found an account corresponding to their certificate
|
// ok, we found an account corresponding to their certificate
|
||||||
clientAccount, err = am.LoadAccount(account)
|
clientAccount, err = am.LoadAccount(account)
|
||||||
|
|
@ -2217,8 +2167,6 @@ var (
|
||||||
"PLAIN": authPlainHandler,
|
"PLAIN": authPlainHandler,
|
||||||
"EXTERNAL": authExternalHandler,
|
"EXTERNAL": authExternalHandler,
|
||||||
"SCRAM-SHA-256": authScramHandler,
|
"SCRAM-SHA-256": authScramHandler,
|
||||||
"OAUTHBEARER": authOauthBearerHandler,
|
|
||||||
"IRCV3BEARER": authIRCv3BearerHandler,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/oauth2"
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,7 +21,6 @@ type AuthScriptInput struct {
|
||||||
PeerCerts []string `json:"peerCerts,omitempty"`
|
PeerCerts []string `json:"peerCerts,omitempty"`
|
||||||
peerCerts []*x509.Certificate
|
peerCerts []*x509.Certificate
|
||||||
IP string `json:"ip,omitempty"`
|
IP string `json:"ip,omitempty"`
|
||||||
OAuthBearer *oauth2.OAuthBearerOptions `json:"oauth2,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthScriptOutput struct {
|
type AuthScriptOutput struct {
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
// Copyright (c) 2022 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package bunt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tidwall/buntdb"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/datastore"
|
|
||||||
"github.com/ergochat/ergo/irc/logger"
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BuntKey yields a string key corresponding to a (table, UUID) pair.
|
|
||||||
// Ideally this would not be public, but some of the migration code
|
|
||||||
// needs it.
|
|
||||||
func BuntKey(table datastore.Table, uuid utils.UUID) string {
|
|
||||||
return fmt.Sprintf("%x %s", table, uuid.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// buntdbDatastore implements datastore.Datastore using a buntdb.
|
|
||||||
type buntdbDatastore struct {
|
|
||||||
db *buntdb.DB
|
|
||||||
logger *logger.Manager
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBuntdbDatastore returns a datastore.Datastore backed by buntdb.
|
|
||||||
func NewBuntdbDatastore(db *buntdb.DB, logger *logger.Manager) datastore.Datastore {
|
|
||||||
return &buntdbDatastore{
|
|
||||||
db: db,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *buntdbDatastore) Backoff() time.Duration {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *buntdbDatastore) GetAll(table datastore.Table) (result []datastore.KV, err error) {
|
|
||||||
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) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
uuid, err := utils.DecodeUUID(strings.TrimPrefix(key, tablePrefix))
|
|
||||||
if err == nil {
|
|
||||||
result = append(result, datastore.KV{UUID: uuid, Value: []byte(value)})
|
|
||||||
} else {
|
|
||||||
b.logger.Error("datastore", "invalid uuid", key)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *buntdbDatastore) Get(table datastore.Table, uuid utils.UUID) (value []byte, err error) {
|
|
||||||
buntKey := BuntKey(table, uuid)
|
|
||||||
var result string
|
|
||||||
err = b.db.View(func(tx *buntdb.Tx) error {
|
|
||||||
result, err = tx.Get(buntKey)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
return []byte(result), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *buntdbDatastore) Set(table datastore.Table, uuid utils.UUID, value []byte, expiration time.Time) (err error) {
|
|
||||||
buntKey := BuntKey(table, uuid)
|
|
||||||
var setOptions *buntdb.SetOptions
|
|
||||||
if !expiration.IsZero() {
|
|
||||||
ttl := time.Until(expiration)
|
|
||||||
if ttl > 0 {
|
|
||||||
setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
|
|
||||||
} else {
|
|
||||||
return nil // it already expired, i guess?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strVal := string(value)
|
|
||||||
|
|
||||||
err = b.db.Update(func(tx *buntdb.Tx) error {
|
|
||||||
_, _, err := tx.Set(buntKey, strVal, setOptions)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *buntdbDatastore) Delete(table datastore.Table, key utils.UUID) (err error) {
|
|
||||||
buntKey := BuntKey(table, key)
|
|
||||||
err = b.db.Update(func(tx *buntdb.Tx) error {
|
|
||||||
_, err := tx.Delete(buntKey)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
// deleting a nonexistent key is not considered an error
|
|
||||||
switch err {
|
|
||||||
case buntdb.ErrNotFound:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -62,13 +62,10 @@ const (
|
||||||
RelaymsgTagName = "draft/relaymsg"
|
RelaymsgTagName = "draft/relaymsg"
|
||||||
// BOT mode: https://ircv3.net/specs/extensions/bot-mode
|
// BOT mode: https://ircv3.net/specs/extensions/bot-mode
|
||||||
BotTagName = "bot"
|
BotTagName = "bot"
|
||||||
// https://ircv3.net/specs/extensions/chathistory
|
|
||||||
ChathistoryTargetsBatchType = "draft/chathistory-targets"
|
|
||||||
ExtendedISupportBatchType = "draft/extended-isupport"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
nameToCapability = make(map[string]Capability, numCapabs)
|
nameToCapability = make(map[string]Capability)
|
||||||
for capab, name := range capabilityNames {
|
for capab, name := range capabilityNames {
|
||||||
nameToCapability[name] = Capability(capab)
|
nameToCapability[name] = Capability(capab)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ package caps
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 36
|
numCapabs = 30
|
||||||
// length of the uint32 array that represents the bitset:
|
// length of the uint64 array that represents the bitset:
|
||||||
bitsetLen = 2
|
bitsetLen = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -53,34 +53,18 @@ const (
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/362
|
// https://github.com/ircv3/ircv3-specifications/pull/362
|
||||||
EventPlayback Capability = iota
|
EventPlayback Capability = iota
|
||||||
|
|
||||||
// ExtendedISupport is the proposed IRCv3 capability named "draft/extended-isupport":
|
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/543
|
|
||||||
ExtendedISupport Capability = iota
|
|
||||||
|
|
||||||
// Languages is the proposed IRCv3 capability named "draft/languages":
|
// Languages is the proposed IRCv3 capability named "draft/languages":
|
||||||
// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
|
// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
|
||||||
Languages Capability = iota
|
Languages Capability = iota
|
||||||
|
|
||||||
// MessageRedaction is the proposed IRCv3 capability named "draft/message-redaction":
|
|
||||||
// https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md
|
|
||||||
MessageRedaction Capability = iota
|
|
||||||
|
|
||||||
// Multiline is the proposed IRCv3 capability named "draft/multiline":
|
// Multiline is the proposed IRCv3 capability named "draft/multiline":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/398
|
// https://github.com/ircv3/ircv3-specifications/pull/398
|
||||||
Multiline Capability = iota
|
Multiline Capability = iota
|
||||||
|
|
||||||
// NoImplicitNames is the proposed IRCv3 capability named "draft/no-implicit-names":
|
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/527
|
|
||||||
NoImplicitNames Capability = iota
|
|
||||||
|
|
||||||
// Persistence is the proposed IRCv3 capability named "draft/persistence":
|
// Persistence is the proposed IRCv3 capability named "draft/persistence":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/503
|
// https://github.com/ircv3/ircv3-specifications/pull/503
|
||||||
Persistence Capability = iota
|
Persistence Capability = iota
|
||||||
|
|
||||||
// Preaway is the proposed IRCv3 capability named "draft/pre-away":
|
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/514
|
|
||||||
Preaway Capability = iota
|
|
||||||
|
|
||||||
// ReadMarker is the draft IRCv3 capability named "draft/read-marker":
|
// ReadMarker is the draft IRCv3 capability named "draft/read-marker":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/489
|
// https://github.com/ircv3/ircv3-specifications/pull/489
|
||||||
ReadMarker Capability = iota
|
ReadMarker Capability = iota
|
||||||
|
|
@ -133,10 +117,6 @@ const (
|
||||||
// https://ircv3.net/specs/extensions/setname.html
|
// https://ircv3.net/specs/extensions/setname.html
|
||||||
SetName Capability = iota
|
SetName Capability = iota
|
||||||
|
|
||||||
// StandardReplies is the IRCv3 capability named "standard-replies":
|
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/506
|
|
||||||
StandardReplies Capability = iota
|
|
||||||
|
|
||||||
// STS is the IRCv3 capability named "sts":
|
// STS is the IRCv3 capability named "sts":
|
||||||
// https://ircv3.net/specs/extensions/sts.html
|
// https://ircv3.net/specs/extensions/sts.html
|
||||||
STS Capability = iota
|
STS Capability = iota
|
||||||
|
|
@ -152,8 +132,6 @@ const (
|
||||||
// ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message":
|
// ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message":
|
||||||
// https://wiki.znc.in/Query_buffers
|
// https://wiki.znc.in/Query_buffers
|
||||||
ZNCSelfMessage Capability = iota
|
ZNCSelfMessage Capability = iota
|
||||||
|
|
||||||
ExtendedNames Capability = iota
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// `capabilityNames[capab]` is the string name of the capability `capab`
|
// `capabilityNames[capab]` is the string name of the capability `capab`
|
||||||
|
|
@ -169,13 +147,9 @@ var (
|
||||||
"draft/channel-rename",
|
"draft/channel-rename",
|
||||||
"draft/chathistory",
|
"draft/chathistory",
|
||||||
"draft/event-playback",
|
"draft/event-playback",
|
||||||
"draft/extended-isupport",
|
|
||||||
"draft/languages",
|
"draft/languages",
|
||||||
"draft/message-redaction",
|
|
||||||
"draft/multiline",
|
"draft/multiline",
|
||||||
"draft/no-implicit-names",
|
|
||||||
"draft/persistence",
|
"draft/persistence",
|
||||||
"draft/pre-away",
|
|
||||||
"draft/read-marker",
|
"draft/read-marker",
|
||||||
"draft/relaymsg",
|
"draft/relaymsg",
|
||||||
"echo-message",
|
"echo-message",
|
||||||
|
|
@ -189,11 +163,9 @@ var (
|
||||||
"sasl",
|
"sasl",
|
||||||
"server-time",
|
"server-time",
|
||||||
"setname",
|
"setname",
|
||||||
"standard-replies",
|
|
||||||
"sts",
|
"sts",
|
||||||
"userhost-in-names",
|
"userhost-in-names",
|
||||||
"znc.in/playback",
|
"znc.in/playback",
|
||||||
"znc.in/self-message",
|
"znc.in/self-message",
|
||||||
"cef/extended-names",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -102,13 +102,6 @@ func (s *Set) Strings(version Version, values Values, maxLen int) (result []stri
|
||||||
var capab Capability
|
var capab Capability
|
||||||
asSlice := s[:]
|
asSlice := s[:]
|
||||||
for capab = 0; capab < numCapabs; capab++ {
|
for capab = 0; capab < numCapabs; capab++ {
|
||||||
// XXX clients that only support CAP LS 301 cannot handle multiline
|
|
||||||
// responses. omit some CAPs in this case, forcing the response to fit on
|
|
||||||
// a single line. this is technically buggy for CAP LIST (as opposed to LS)
|
|
||||||
// but it shouldn't matter
|
|
||||||
if version < Cap302 && !isAllowed301(capab) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// skip any capabilities that are not enabled
|
// skip any capabilities that are not enabled
|
||||||
if !utils.BitsetGet(asSlice, uint(capab)) {
|
if !utils.BitsetGet(asSlice, uint(capab)) {
|
||||||
continue
|
continue
|
||||||
|
|
@ -129,15 +122,3 @@ func (s *Set) Strings(version Version, values Values, maxLen int) (result []stri
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is a fixed whitelist of caps that are eligible for display in CAP LS 301
|
|
||||||
func isAllowed301(capab Capability) bool {
|
|
||||||
switch capab {
|
|
||||||
case AccountNotify, AccountTag, AwayNotify, Batch, ChgHost, Chathistory, EventPlayback,
|
|
||||||
Relaymsg, EchoMessage, Nope, ExtendedJoin, InviteNotify, LabeledResponse, MessageTags,
|
|
||||||
MultiPrefix, SASL, ServerTime, SetName, STS, UserhostInNames, ZNCSelfMessage, ZNCPlayback:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,8 @@
|
||||||
|
|
||||||
package caps
|
package caps
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"fmt"
|
import "reflect"
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSets(t *testing.T) {
|
func TestSets(t *testing.T) {
|
||||||
s1 := NewSet()
|
s1 := NewSet()
|
||||||
|
|
@ -63,19 +60,6 @@ func TestSets(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertEqual(found, expected interface{}) {
|
|
||||||
if !reflect.DeepEqual(found, expected) {
|
|
||||||
panic(fmt.Sprintf("found %#v, expected %#v", found, expected))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test301WhitelistNotRespectedFor302(t *testing.T) {
|
|
||||||
s1 := NewSet()
|
|
||||||
s1.Enable(AccountTag, EchoMessage, StandardReplies)
|
|
||||||
assertEqual(s1.Strings(Cap301, nil, 0), []string{"account-tag echo-message"})
|
|
||||||
assertEqual(s1.Strings(Cap302, nil, 0), []string{"account-tag echo-message standard-replies"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubtract(t *testing.T) {
|
func TestSubtract(t *testing.T) {
|
||||||
s1 := NewSet(AccountTag, EchoMessage, UserhostInNames, ServerTime)
|
s1 := NewSet(AccountTag, EchoMessage, UserhostInNames, ServerTime)
|
||||||
|
|
||||||
|
|
|
||||||
194
irc/cef.go
194
irc/cef.go
|
|
@ -1,194 +0,0 @@
|
||||||
package irc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/cshum/imagor/imagorpath"
|
|
||||||
"github.com/ergochat/ergo/irc/caps"
|
|
||||||
"github.com/ergochat/irc-go/ircmsg"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ctx = context.Background()
|
|
||||||
|
|
||||||
func (channel *Channel) RedisBroadcast(message ...string) {
|
|
||||||
if channel.server.redis == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := channel.server.redis.Publish(ctx, "channel."+channel.NameCasefolded(), strings.Join(message, " ")).Err()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Channel broadcast error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) RedisBroadcast(message ...string) {
|
|
||||||
err := client.server.redis.Publish(ctx, "user."+client.NickCasefolded(), strings.Join(message, " ")).Err()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("User broadcast error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) Broadcast(command string, params ...string) {
|
|
||||||
channel.BroadcastFrom(channel.server.name, command, params...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) BroadcastFrom(prefix string, command string, params ...string) {
|
|
||||||
for _, member := range channel.Members() {
|
|
||||||
for _, session := range member.Sessions() {
|
|
||||||
session.Send(nil, prefix, command, params...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChannelSub Actions and info centering around channels
|
|
||||||
func (server *Server) ChannelSub() {
|
|
||||||
pubsub := server.redis.PSubscribe(ctx, "channel.*")
|
|
||||||
defer pubsub.Close()
|
|
||||||
|
|
||||||
ch := pubsub.Channel()
|
|
||||||
|
|
||||||
for msg := range ch {
|
|
||||||
server.logger.Info("RedisMessage", msg.Channel, msg.Payload)
|
|
||||||
line := strings.Split(msg.Payload, " ")
|
|
||||||
channelName := strings.SplitN(msg.Channel, ".", 2)[1]
|
|
||||||
channel := server.channels.Get(channelName)
|
|
||||||
if len(line) == 0 {
|
|
||||||
println("Empty string dumped into ", msg.Channel, " channel")
|
|
||||||
}
|
|
||||||
if channel == nil {
|
|
||||||
server.logger.Warning("RedisMessage", "Unknown channel")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch line[0] {
|
|
||||||
case "VOICEPART":
|
|
||||||
channel.Broadcast("VOICEPART", channelName, line[1])
|
|
||||||
case "VOICESTATE":
|
|
||||||
channel.Broadcast("VOICESTATE", channelName, line[1], line[2], line[3])
|
|
||||||
case "BROADCASTTO":
|
|
||||||
for _, person := range channel.Members() {
|
|
||||||
person.Send(nil, server.name, line[1], line[2:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserSub Handles things pertaining to users
|
|
||||||
func (server *Server) UserSub() {
|
|
||||||
pubsub := server.redis.PSubscribe(ctx, "user.*")
|
|
||||||
defer pubsub.Close()
|
|
||||||
|
|
||||||
ch := pubsub.Channel()
|
|
||||||
|
|
||||||
for msg := range ch {
|
|
||||||
server.logger.Info("RedisMessage", msg.Channel, msg.Payload)
|
|
||||||
line := strings.Split(msg.Payload, " ")
|
|
||||||
userName := strings.SplitN(msg.Channel, ".", 2)[1]
|
|
||||||
user := server.clients.Get(userName)
|
|
||||||
if len(line) == 0 {
|
|
||||||
println("Empty string dumped into ", msg.Channel, " channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch line[0] {
|
|
||||||
case "FULLYREMOVE":
|
|
||||||
if user != nil {
|
|
||||||
user.destroy(nil)
|
|
||||||
err := server.accounts.Unregister(user.Account(), true)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "BROADCASTAS":
|
|
||||||
if user != nil {
|
|
||||||
// I'm not too sure what the capability bit is, I think it's just ones that match
|
|
||||||
for friend := range user.Friends(caps.ExtendedNames) {
|
|
||||||
friend.Send(nil, user.NickMaskString(), line[1], line[2:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startRedis(server *Server) {
|
|
||||||
go server.ChannelSub()
|
|
||||||
go server.UserSub()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) GenerateImagorSignaturesFromMessage(message *ircmsg.Message) string {
|
|
||||||
line, err := message.Line()
|
|
||||||
if err == nil {
|
|
||||||
return server.GenerateImagorSignatures(line)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) GetUrlMime(url string) string {
|
|
||||||
config := server.Config()
|
|
||||||
// hacky, should fix
|
|
||||||
if !strings.Contains(url, "?") {
|
|
||||||
url += "?"
|
|
||||||
}
|
|
||||||
params := imagorpath.Params{
|
|
||||||
Image: url,
|
|
||||||
Meta: true,
|
|
||||||
}
|
|
||||||
metaPath := imagorpath.Generate(params, imagorpath.NewHMACSigner(sha256.New, 0, config.Cef.Imagor.Secret))
|
|
||||||
client := http.Client{
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
resp, err := client.Get(config.Cef.Imagor.Url + metaPath)
|
|
||||||
if err != nil {
|
|
||||||
println("Failed on the initial get")
|
|
||||||
println(err.Error())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
var meta map[string]interface{}
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&meta)
|
|
||||||
if err != nil {
|
|
||||||
println("Failed on the JSON decode")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
contentType, valid := meta["format"].(string)
|
|
||||||
if !valid {
|
|
||||||
println("No content type")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return contentType
|
|
||||||
}
|
|
||||||
|
|
||||||
var urlRegex = regexp.MustCompile("https?:\\/\\/[\\w-]+(\\.[\\w-]+)+([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?")
|
|
||||||
|
|
||||||
// Process a message to add Imagor signatures
|
|
||||||
func (server *Server) GenerateImagorSignatures(str string) string {
|
|
||||||
urls := urlRegex.FindAllString(str, -1)
|
|
||||||
var sigs []string
|
|
||||||
for _, url := range urls {
|
|
||||||
params := imagorpath.Params{
|
|
||||||
Image: url,
|
|
||||||
FitIn: true,
|
|
||||||
Width: 600,
|
|
||||||
Height: 600,
|
|
||||||
}
|
|
||||||
path := imagorpath.Generate(params, imagorpath.NewHMACSigner(sha256.New, 0, server.Config().Cef.Imagor.Secret))
|
|
||||||
signature := path[:strings.IndexByte(path, '/')]
|
|
||||||
contentType := server.GetUrlMime(url)
|
|
||||||
if contentType != "" {
|
|
||||||
sigs = append(sigs, signature+"|"+strings.ReplaceAll(contentType, "/", "_"))
|
|
||||||
} else {
|
|
||||||
sigs = append(sigs, signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if len(sigs) > 0 {
|
|
||||||
return strings.Join(sigs, ",")
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
302
irc/channel.go
302
irc/channel.go
|
|
@ -7,17 +7,15 @@ package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ergochat/irc-go/ircmsg"
|
"github.com/ergochat/irc-go/ircutils"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/caps"
|
"github.com/ergochat/ergo/irc/caps"
|
||||||
"github.com/ergochat/ergo/irc/datastore"
|
|
||||||
"github.com/ergochat/ergo/irc/history"
|
"github.com/ergochat/ergo/irc/history"
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
|
|
@ -35,6 +33,7 @@ type Channel struct {
|
||||||
key string
|
key string
|
||||||
forward string
|
forward string
|
||||||
members MemberSet
|
members MemberSet
|
||||||
|
membersCache []*Client // allow iteration over channel members without holding the lock
|
||||||
name string
|
name string
|
||||||
nameCasefolded string
|
nameCasefolded string
|
||||||
server *Server
|
server *Server
|
||||||
|
|
@ -51,17 +50,14 @@ type Channel struct {
|
||||||
stateMutex sync.RWMutex // tier 1
|
stateMutex sync.RWMutex // tier 1
|
||||||
writebackLock sync.Mutex // tier 1.5
|
writebackLock sync.Mutex // tier 1.5
|
||||||
joinPartMutex sync.Mutex // tier 3
|
joinPartMutex sync.Mutex // tier 3
|
||||||
|
ensureLoaded utils.Once // manages loading stored registration info from the database
|
||||||
dirtyBits uint
|
dirtyBits uint
|
||||||
settings ChannelSettings
|
settings ChannelSettings
|
||||||
uuid utils.UUID
|
|
||||||
// these caches are paired to allow iteration over channel members without holding the lock
|
|
||||||
membersCache []*Client
|
|
||||||
memberDataCache []*memberData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannel creates a new channel from a `Server` and a `name`
|
// NewChannel creates a new channel from a `Server` and a `name`
|
||||||
// string, which must be unique on the server.
|
// string, which must be unique on the server.
|
||||||
func NewChannel(s *Server, name, casefoldedName string, registered bool, regInfo RegisteredChannel) *Channel {
|
func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channel {
|
||||||
config := s.Config()
|
config := s.Config()
|
||||||
|
|
||||||
channel := &Channel{
|
channel := &Channel{
|
||||||
|
|
@ -75,15 +71,14 @@ func NewChannel(s *Server, name, casefoldedName string, registered bool, regInfo
|
||||||
channel.initializeLists()
|
channel.initializeLists()
|
||||||
channel.history.Initialize(0, 0)
|
channel.history.Initialize(0, 0)
|
||||||
|
|
||||||
if registered {
|
if !registered {
|
||||||
channel.applyRegInfo(regInfo)
|
|
||||||
} else {
|
|
||||||
channel.resizeHistory(config)
|
channel.resizeHistory(config)
|
||||||
for _, mode := range config.Channels.defaultModes {
|
for _, mode := range config.Channels.defaultModes {
|
||||||
channel.flags.SetMode(mode, true)
|
channel.flags.SetMode(mode, true)
|
||||||
}
|
}
|
||||||
channel.uuid = utils.GenerateUUIDv4()
|
// no loading to do, so "mark" the load operation as "done":
|
||||||
}
|
channel.ensureLoaded.Do(func() {})
|
||||||
|
} // else: modes will be loaded before first join
|
||||||
|
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +92,24 @@ func (channel *Channel) initializeLists() {
|
||||||
channel.accountToUMode = make(map[string]modes.Mode)
|
channel.accountToUMode = make(map[string]modes.Mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureLoaded blocks until the channel's registration info has been loaded
|
||||||
|
// from the database.
|
||||||
|
func (channel *Channel) EnsureLoaded() {
|
||||||
|
channel.ensureLoaded.Do(func() {
|
||||||
|
nmc := channel.NameCasefolded()
|
||||||
|
info, err := channel.server.channelRegistry.LoadChannel(nmc)
|
||||||
|
if err == nil {
|
||||||
|
channel.applyRegInfo(info)
|
||||||
|
} else {
|
||||||
|
channel.server.logger.Error("internal", "couldn't load channel", nmc, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) IsLoaded() bool {
|
||||||
|
return channel.ensureLoaded.Done()
|
||||||
|
}
|
||||||
|
|
||||||
func (channel *Channel) resizeHistory(config *Config) {
|
func (channel *Channel) resizeHistory(config *Config) {
|
||||||
status, _, _ := channel.historyStatus(config)
|
status, _, _ := channel.historyStatus(config)
|
||||||
if status == HistoryEphemeral {
|
if status == HistoryEphemeral {
|
||||||
|
|
@ -113,7 +126,6 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
defer channel.stateMutex.Unlock()
|
defer channel.stateMutex.Unlock()
|
||||||
|
|
||||||
channel.uuid = chanReg.UUID
|
|
||||||
channel.registeredFounder = chanReg.Founder
|
channel.registeredFounder = chanReg.Founder
|
||||||
channel.registeredTime = chanReg.RegisteredAt
|
channel.registeredTime = chanReg.RegisteredAt
|
||||||
channel.topic = chanReg.Topic
|
channel.topic = chanReg.Topic
|
||||||
|
|
@ -138,41 +150,38 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// obtain a consistent snapshot of the channel state that can be persisted to the DB
|
// obtain a consistent snapshot of the channel state that can be persisted to the DB
|
||||||
func (channel *Channel) ExportRegistration() (info RegisteredChannel) {
|
func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredChannel) {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
|
|
||||||
info.Name = channel.name
|
info.Name = channel.name
|
||||||
info.UUID = channel.uuid
|
info.NameCasefolded = channel.nameCasefolded
|
||||||
info.Founder = channel.registeredFounder
|
info.Founder = channel.registeredFounder
|
||||||
info.RegisteredAt = channel.registeredTime
|
info.RegisteredAt = channel.registeredTime
|
||||||
|
|
||||||
|
if includeFlags&IncludeTopic != 0 {
|
||||||
info.Topic = channel.topic
|
info.Topic = channel.topic
|
||||||
info.TopicSetBy = channel.topicSetBy
|
info.TopicSetBy = channel.topicSetBy
|
||||||
info.TopicSetTime = channel.topicSetTime
|
info.TopicSetTime = channel.topicSetTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeFlags&IncludeModes != 0 {
|
||||||
info.Key = channel.key
|
info.Key = channel.key
|
||||||
info.Forward = channel.forward
|
info.Forward = channel.forward
|
||||||
info.Modes = channel.flags.AllModes()
|
info.Modes = channel.flags.AllModes()
|
||||||
info.UserLimit = channel.userLimit
|
info.UserLimit = channel.userLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeFlags&IncludeLists != 0 {
|
||||||
info.Bans = channel.lists[modes.BanMask].Masks()
|
info.Bans = channel.lists[modes.BanMask].Masks()
|
||||||
info.Invites = channel.lists[modes.InviteMask].Masks()
|
info.Invites = channel.lists[modes.InviteMask].Masks()
|
||||||
info.Excepts = channel.lists[modes.ExceptMask].Masks()
|
info.Excepts = channel.lists[modes.ExceptMask].Masks()
|
||||||
info.AccountToUMode = maps.Clone(channel.accountToUMode)
|
info.AccountToUMode = utils.CopyMap(channel.accountToUMode)
|
||||||
|
|
||||||
info.Settings = channel.settings
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) exportSummary() (info RegisteredChannel) {
|
if includeFlags&IncludeSettings != 0 {
|
||||||
channel.stateMutex.RLock()
|
info.Settings = channel.settings
|
||||||
defer channel.stateMutex.RUnlock()
|
}
|
||||||
|
|
||||||
info.Name = channel.name
|
|
||||||
info.Founder = channel.registeredFounder
|
|
||||||
info.RegisteredAt = channel.registeredTime
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -222,8 +231,6 @@ func (channel *Channel) wakeWriter() {
|
||||||
|
|
||||||
// equivalent of Socket.send()
|
// equivalent of Socket.send()
|
||||||
func (channel *Channel) writeLoop() {
|
func (channel *Channel) writeLoop() {
|
||||||
defer channel.server.HandlePanic()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// TODO(#357) check the error value of this and implement timed backoff
|
// TODO(#357) check the error value of this and implement timed backoff
|
||||||
channel.performWrite(0)
|
channel.performWrite(0)
|
||||||
|
|
@ -281,19 +288,9 @@ func (channel *Channel) performWrite(additionalDirtyBits uint) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var success bool
|
info := channel.ExportRegistration(dirtyBits)
|
||||||
info := channel.ExportRegistration()
|
err = channel.server.channelRegistry.StoreChannel(info, dirtyBits)
|
||||||
if b, err := info.Serialize(); err == nil {
|
if err != nil {
|
||||||
if err := channel.server.dstore.Set(datastore.TableChannels, info.UUID, b, time.Time{}); err == nil {
|
|
||||||
success = true
|
|
||||||
} else {
|
|
||||||
channel.server.logger.Error("internal", "couldn't persist channel", info.Name, err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.server.logger.Error("internal", "couldn't serialize channel", info.Name, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if !success {
|
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
channel.dirtyBits = channel.dirtyBits | dirtyBits
|
channel.dirtyBits = channel.dirtyBits | dirtyBits
|
||||||
channel.stateMutex.Unlock()
|
channel.stateMutex.Unlock()
|
||||||
|
|
@ -317,7 +314,6 @@ func (channel *Channel) SetRegistered(founder string) error {
|
||||||
|
|
||||||
// SetUnregistered deletes the channel's registration information.
|
// SetUnregistered deletes the channel's registration information.
|
||||||
func (channel *Channel) SetUnregistered(expectedFounder string) {
|
func (channel *Channel) SetUnregistered(expectedFounder string) {
|
||||||
uuid := utils.GenerateUUIDv4()
|
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
defer channel.stateMutex.Unlock()
|
defer channel.stateMutex.Unlock()
|
||||||
|
|
||||||
|
|
@ -328,9 +324,6 @@ func (channel *Channel) SetUnregistered(expectedFounder string) {
|
||||||
var zeroTime time.Time
|
var zeroTime time.Time
|
||||||
channel.registeredTime = zeroTime
|
channel.registeredTime = zeroTime
|
||||||
channel.accountToUMode = make(map[string]modes.Mode)
|
channel.accountToUMode = make(map[string]modes.Mode)
|
||||||
// reset the UUID so that any re-registration will persist under
|
|
||||||
// a separate key:
|
|
||||||
channel.uuid = uuid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes)
|
// implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes)
|
||||||
|
|
@ -426,19 +419,16 @@ func (channel *Channel) AcceptTransfer(client *Client) (err error) {
|
||||||
|
|
||||||
func (channel *Channel) regenerateMembersCache() {
|
func (channel *Channel) regenerateMembersCache() {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
membersCache := make([]*Client, len(channel.members))
|
result := make([]*Client, len(channel.members))
|
||||||
dataCache := make([]*memberData, len(channel.members))
|
|
||||||
i := 0
|
i := 0
|
||||||
for client, info := range channel.members {
|
for client := range channel.members {
|
||||||
membersCache[i] = client
|
result[i] = client
|
||||||
dataCache[i] = info
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
|
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
channel.membersCache = membersCache
|
channel.membersCache = result
|
||||||
channel.memberDataCache = dataCache
|
|
||||||
channel.stateMutex.Unlock()
|
channel.stateMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,51 +436,59 @@ func (channel *Channel) regenerateMembersCache() {
|
||||||
func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
|
func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
clientData, isJoined := channel.members[client]
|
clientData, isJoined := channel.members[client]
|
||||||
chname := channel.name
|
|
||||||
membersCache, memberDataCache := channel.membersCache, channel.memberDataCache
|
|
||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
symbol := "=" // https://modern.ircdocs.horse/#rplnamreply-353
|
|
||||||
if channel.flags.HasMode(modes.Secret) {
|
|
||||||
symbol = "@"
|
|
||||||
}
|
|
||||||
isOper := client.HasRoleCapabs("sajoin")
|
isOper := client.HasRoleCapabs("sajoin")
|
||||||
respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper &&
|
respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper &&
|
||||||
(!isJoined || clientData.modes.HighestChannelUserMode() == modes.Mode(0))
|
(!isJoined || clientData.modes.HighestChannelUserMode() == modes.Mode(0))
|
||||||
isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
|
isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
|
||||||
isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
|
isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
|
||||||
|
|
||||||
maxNamLen := 480 - len(client.server.name) - len(client.Nick()) - len(chname)
|
maxNamLen := 480 - len(client.server.name) - len(client.Nick())
|
||||||
var tl utils.TokenLineBuilder
|
var namesLines []string
|
||||||
tl.Initialize(maxNamLen, " ")
|
var buffer strings.Builder
|
||||||
if isJoined || !channel.flags.HasMode(modes.Secret) || isOper {
|
if isJoined || !channel.flags.HasMode(modes.Secret) || isOper {
|
||||||
for i, target := range membersCache {
|
for _, target := range channel.Members() {
|
||||||
if !isJoined && target.HasMode(modes.Invisible) && !isOper {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var nick string
|
var nick string
|
||||||
if isUserhostInNames {
|
if isUserhostInNames {
|
||||||
nick = target.NickMaskString()
|
nick = target.NickMaskString()
|
||||||
} else {
|
} else {
|
||||||
nick = target.Nick()
|
nick = target.Nick()
|
||||||
}
|
}
|
||||||
memberData := memberDataCache[i]
|
channel.stateMutex.RLock()
|
||||||
if respectAuditorium && memberData.modes.HighestChannelUserMode() == modes.Mode(0) {
|
memberData, _ := channel.members[target]
|
||||||
|
channel.stateMutex.RUnlock()
|
||||||
|
modeSet := memberData.modes
|
||||||
|
if modeSet == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if rb.session.capabilities.Has(caps.ExtendedNames) {
|
if !isJoined && target.HasMode(modes.Invisible) && !isOper {
|
||||||
away, _ := target.Away()
|
continue
|
||||||
if away {
|
|
||||||
nick = nick + "*"
|
|
||||||
}
|
}
|
||||||
|
if respectAuditorium && modeSet.HighestChannelUserMode() == modes.Mode(0) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
tl.AddParts(memberData.modes.Prefixes(isMultiPrefix), nick)
|
prefix := modeSet.Prefixes(isMultiPrefix)
|
||||||
|
if buffer.Len()+len(nick)+len(prefix)+1 > maxNamLen {
|
||||||
|
namesLines = append(namesLines, buffer.String())
|
||||||
|
buffer.Reset()
|
||||||
|
}
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
}
|
||||||
|
buffer.WriteString(prefix)
|
||||||
|
buffer.WriteString(nick)
|
||||||
|
}
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
namesLines = append(namesLines, buffer.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, line := range tl.Lines() {
|
for _, line := range namesLines {
|
||||||
rb.Add(nil, client.server.name, RPL_NAMREPLY, client.nick, symbol, chname, line)
|
if buffer.Len() > 0 {
|
||||||
|
rb.Add(nil, client.server.name, RPL_NAMREPLY, client.nick, "=", channel.name, line)
|
||||||
}
|
}
|
||||||
rb.Add(nil, client.server.name, RPL_ENDOFNAMES, client.nick, chname, client.t("End of NAMES list"))
|
}
|
||||||
|
rb.Add(nil, client.server.name, RPL_ENDOFNAMES, client.nick, channel.name, client.t("End of NAMES list"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// does `clientMode` give you privileges to grant/remove `targetMode` to/from people,
|
// does `clientMode` give you privileges to grant/remove `targetMode` to/from people,
|
||||||
|
|
@ -514,7 +512,7 @@ func channelUserModeHasPrivsOver(clientMode modes.Mode, targetMode modes.Mode) b
|
||||||
// ClientIsAtLeast returns whether the client has at least the given channel privilege.
|
// ClientIsAtLeast returns whether the client has at least the given channel privilege.
|
||||||
func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool {
|
func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
memberData, present := channel.members[client]
|
memberData := channel.members[client]
|
||||||
founder := channel.registeredFounder
|
founder := channel.registeredFounder
|
||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
|
@ -522,10 +520,6 @@ func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) b
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !present {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mode := range modes.ChannelUserModes {
|
for _, mode := range modes.ChannelUserModes {
|
||||||
if memberData.modes.HasMode(mode) {
|
if memberData.modes.HasMode(mode) {
|
||||||
return true
|
return true
|
||||||
|
|
@ -557,14 +551,11 @@ func (channel *Channel) ClientStatus(client *Client) (present bool, joinTimeSecs
|
||||||
|
|
||||||
// helper for persisting channel-user modes for always-on clients;
|
// helper for persisting channel-user modes for always-on clients;
|
||||||
// return the channel name and all channel-user modes for a client
|
// return the channel name and all channel-user modes for a client
|
||||||
func (channel *Channel) alwaysOnStatus(client *Client) (ok bool, chname string, status alwaysOnChannelStatus) {
|
func (channel *Channel) alwaysOnStatus(client *Client) (chname string, status alwaysOnChannelStatus) {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
chname = channel.name
|
chname = channel.name
|
||||||
data, ok := channel.members[client]
|
data := channel.members[client]
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
status.Modes = data.modes.String()
|
status.Modes = data.modes.String()
|
||||||
status.JoinTime = data.joinTime
|
status.JoinTime = data.joinTime
|
||||||
return
|
return
|
||||||
|
|
@ -578,20 +569,20 @@ func (channel *Channel) setMemberStatus(client *Client, status alwaysOnChannelSt
|
||||||
}
|
}
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
defer channel.stateMutex.Unlock()
|
defer channel.stateMutex.Unlock()
|
||||||
if mData, ok := channel.members[client]; ok {
|
if _, ok := channel.members[client]; !ok {
|
||||||
mData.modes.Clear()
|
return
|
||||||
for _, mode := range status.Modes {
|
|
||||||
mData.modes.SetMode(modes.Mode(mode), true)
|
|
||||||
}
|
|
||||||
mData.joinTime = status.JoinTime
|
|
||||||
}
|
}
|
||||||
|
memberData := channel.members[client]
|
||||||
|
memberData.modes = newModes
|
||||||
|
memberData.joinTime = status.JoinTime
|
||||||
|
channel.members[client] = memberData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
|
func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
founder := channel.registeredFounder
|
founder := channel.registeredFounder
|
||||||
clientData, clientOK := channel.members[client]
|
clientModes := channel.members[client].modes
|
||||||
targetData, targetOK := channel.members[target]
|
targetModes := channel.members[target].modes
|
||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
|
|
||||||
if founder != "" {
|
if founder != "" {
|
||||||
|
|
@ -602,11 +593,7 @@ func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return clientOK && targetOK &&
|
return channelUserModeHasPrivsOver(clientModes.HighestChannelUserMode(), targetModes.HighestChannelUserMode())
|
||||||
channelUserModeHasPrivsOver(
|
|
||||||
clientData.modes.HighestChannelUserMode(),
|
|
||||||
targetData.modes.HighestChannelUserMode(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) hasClient(client *Client) bool {
|
func (channel *Channel) hasClient(client *Client) bool {
|
||||||
|
|
@ -727,9 +714,6 @@ func (channel *Channel) AddHistoryItem(item history.Item, account string) (err e
|
||||||
if !itemIsStorable(&item, channel.server.Config()) {
|
if !itemIsStorable(&item, channel.server.Config()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if item.Target == "" {
|
|
||||||
item.Target = channel.nameCasefolded
|
|
||||||
}
|
|
||||||
|
|
||||||
status, target, _ := channel.historyStatus(channel.server.Config())
|
status, target, _ := channel.historyStatus(channel.server.Config())
|
||||||
if status == HistoryPersistent {
|
if status == HistoryPersistent {
|
||||||
|
|
@ -804,8 +788,6 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||||
}
|
}
|
||||||
|
|
||||||
client.server.logger.Debug("channels", fmt.Sprintf("%s joined channel %s", details.nick, chname))
|
client.server.logger.Debug("channels", fmt.Sprintf("%s joined channel %s", details.nick, chname))
|
||||||
// I think this is assured to always be a good join point
|
|
||||||
channel.RedisBroadcast("VOICEPOLL")
|
|
||||||
|
|
||||||
givenMode := func() (givenMode modes.Mode) {
|
givenMode := func() (givenMode modes.Mode) {
|
||||||
channel.joinPartMutex.Lock()
|
channel.joinPartMutex.Lock()
|
||||||
|
|
@ -841,9 +823,8 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||||
histItem := history.Item{
|
histItem := history.Item{
|
||||||
Type: history.Join,
|
Type: history.Join,
|
||||||
Nick: details.nickMask,
|
Nick: details.nickMask,
|
||||||
Account: details.account,
|
AccountName: details.accountName,
|
||||||
Message: message,
|
Message: message,
|
||||||
Target: channel.NameCasefolded(),
|
|
||||||
IsBot: isBot,
|
IsBot: isBot,
|
||||||
}
|
}
|
||||||
histItem.Params[0] = details.realname
|
histItem.Params[0] = details.realname
|
||||||
|
|
@ -906,9 +887,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||||
if rb.session.client == client {
|
if rb.session.client == client {
|
||||||
// don't send topic and names for a SAJOIN of a different client
|
// don't send topic and names for a SAJOIN of a different client
|
||||||
channel.SendTopic(client, rb, false)
|
channel.SendTopic(client, rb, false)
|
||||||
if !rb.session.capabilities.Has(caps.NoImplicitNames) {
|
|
||||||
channel.Names(client, rb)
|
channel.Names(client, rb)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// ensure that SAJOIN sends a MODE line to the originating client, if applicable
|
// ensure that SAJOIN sends a MODE line to the originating client, if applicable
|
||||||
if givenMode != 0 {
|
if givenMode != 0 {
|
||||||
|
|
@ -975,7 +954,7 @@ func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, sk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if 0 < numItems {
|
if 0 < numItems {
|
||||||
channel.replayHistoryItems(rb, items, false, "", "", numItems)
|
channel.replayHistoryItems(rb, items, false)
|
||||||
rb.Flush(true)
|
rb.Flush(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -999,11 +978,8 @@ func (channel *Channel) playJoinForSession(session *Session) {
|
||||||
sessionRb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
|
sessionRb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
|
||||||
}
|
}
|
||||||
channel.SendTopic(client, sessionRb, false)
|
channel.SendTopic(client, sessionRb, false)
|
||||||
if !session.capabilities.Has(caps.NoImplicitNames) {
|
|
||||||
channel.Names(client, sessionRb)
|
channel.Names(client, sessionRb)
|
||||||
}
|
|
||||||
sessionRb.Send(false)
|
sessionRb.Send(false)
|
||||||
channel.RedisBroadcast("VOICEPOLL")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part parts the given client from this channel, with the given message.
|
// Part parts the given client from this channel, with the given message.
|
||||||
|
|
@ -1057,9 +1033,8 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
||||||
channel.AddHistoryItem(history.Item{
|
channel.AddHistoryItem(history.Item{
|
||||||
Type: history.Part,
|
Type: history.Part,
|
||||||
Nick: details.nickMask,
|
Nick: details.nickMask,
|
||||||
Account: details.account,
|
AccountName: details.accountName,
|
||||||
Message: splitMessage,
|
Message: splitMessage,
|
||||||
Target: channel.NameCasefolded(),
|
|
||||||
IsBot: isBot,
|
IsBot: isBot,
|
||||||
}, details.account)
|
}, details.account)
|
||||||
}
|
}
|
||||||
|
|
@ -1067,7 +1042,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
||||||
client.server.logger.Debug("channels", fmt.Sprintf("%s left channel %s", details.nick, chname))
|
client.server.logger.Debug("channels", fmt.Sprintf("%s left channel %s", details.nick, chname))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, chathistoryCommand bool, identifier string, preposition string, limit int) {
|
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, chathistoryCommand bool) {
|
||||||
// send an empty batch if necessary, as per the CHATHISTORY spec
|
// send an empty batch if necessary, as per the CHATHISTORY spec
|
||||||
chname := channel.Name()
|
chname := channel.Name()
|
||||||
client := rb.target
|
client := rb.target
|
||||||
|
|
@ -1087,19 +1062,19 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
batchID := rb.StartNestedBatch("chathistory", chname, identifier, preposition, strconv.Itoa(limit))
|
batchID := rb.StartNestedHistoryBatch(chname)
|
||||||
defer rb.EndNestedBatch(batchID)
|
defer rb.EndNestedBatch(batchID)
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
nick := NUHToNick(item.Nick)
|
nick := NUHToNick(item.Nick)
|
||||||
switch item.Type {
|
switch item.Type {
|
||||||
case history.Privmsg:
|
case history.Privmsg:
|
||||||
rb.AddSplitMessageFromClientWithReactions(item.Nick, item.Account, item.IsBot, item.Tags, "PRIVMSG", chname, item.Message, item.Reactions)
|
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, item.Tags, "PRIVMSG", chname, item.Message)
|
||||||
case history.Notice:
|
case history.Notice:
|
||||||
rb.AddSplitMessageFromClientWithReactions(item.Nick, item.Account, item.IsBot, item.Tags, "NOTICE", chname, item.Message, item.Reactions)
|
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, item.Tags, "NOTICE", chname, item.Message)
|
||||||
case history.Tagmsg:
|
case history.Tagmsg:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddSplitMessageFromClient(item.Nick, item.Account, item.IsBot, item.Tags, "TAGMSG", chname, item.Message)
|
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, item.Tags, "TAGMSG", chname, item.Message)
|
||||||
} else if chathistoryCommand {
|
} else if chathistoryCommand {
|
||||||
// #1676, we have to send something here or else it breaks pagination
|
// #1676, we have to send something here or else it breaks pagination
|
||||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, fmt.Sprintf(client.t("%s sent a TAGMSG"), nick))
|
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, fmt.Sprintf(client.t("%s sent a TAGMSG"), nick))
|
||||||
|
|
@ -1107,25 +1082,25 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||||
case history.Join:
|
case history.Join:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
if extendedJoin {
|
if extendedJoin {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.Account, item.IsBot, nil, "JOIN", chname, item.Account, item.Params[0])
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "JOIN", chname, item.AccountName, item.Params[0])
|
||||||
} else {
|
} else {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.Account, item.IsBot, nil, "JOIN", chname)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "JOIN", chname)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !playJoinsAsPrivmsg {
|
if !playJoinsAsPrivmsg {
|
||||||
continue // #474
|
continue // #474
|
||||||
}
|
}
|
||||||
var message string
|
var message string
|
||||||
if item.Account == "*" {
|
if item.AccountName == "*" {
|
||||||
message = fmt.Sprintf(client.t("%s joined the channel"), nick)
|
message = fmt.Sprintf(client.t("%s joined the channel"), nick)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.Account)
|
message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName)
|
||||||
}
|
}
|
||||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
case history.Part:
|
case history.Part:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.Account, item.IsBot, nil, "PART", chname, item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "PART", chname, item.Message.Message)
|
||||||
} else {
|
} else {
|
||||||
if !playJoinsAsPrivmsg {
|
if !playJoinsAsPrivmsg {
|
||||||
continue // #474
|
continue // #474
|
||||||
|
|
@ -1135,14 +1110,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||||
}
|
}
|
||||||
case history.Kick:
|
case history.Kick:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.Account, item.IsBot, nil, "KICK", chname, item.Params[0], item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "KICK", chname, item.Params[0], item.Message.Message)
|
||||||
} else {
|
} else {
|
||||||
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
|
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
|
||||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
case history.Quit:
|
case history.Quit:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.Account, item.IsBot, nil, "QUIT", item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "QUIT", item.Message.Message)
|
||||||
} else {
|
} else {
|
||||||
if !playJoinsAsPrivmsg {
|
if !playJoinsAsPrivmsg {
|
||||||
continue // #474
|
continue // #474
|
||||||
|
|
@ -1152,14 +1127,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||||
}
|
}
|
||||||
case history.Nick:
|
case history.Nick:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.Account, item.IsBot, nil, "NICK", item.Params[0])
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "NICK", item.Params[0])
|
||||||
} else {
|
} else {
|
||||||
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
|
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
|
||||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
case history.Topic:
|
case history.Topic:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.Account, item.IsBot, nil, "TOPIC", chname, item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "TOPIC", chname, item.Message.Message)
|
||||||
} else {
|
} else {
|
||||||
message := fmt.Sprintf(client.t("%[1]s set the channel topic to: %[2]s"), nick, item.Message.Message)
|
message := fmt.Sprintf(client.t("%[1]s set the channel topic to: %[2]s"), nick, item.Message.Message)
|
||||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||||
|
|
@ -1171,7 +1146,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||||
params[i+1] = pair.Message
|
params[i+1] = pair.Message
|
||||||
}
|
}
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.Account, item.IsBot, nil, "MODE", params...)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "MODE", params...)
|
||||||
} else {
|
} else {
|
||||||
message := fmt.Sprintf(client.t("%[1]s set channel modes: %[2]s"), nick, strings.Join(params[1:], " "))
|
message := fmt.Sprintf(client.t("%[1]s set channel modes: %[2]s"), nick, strings.Join(params[1:], " "))
|
||||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||||
|
|
@ -1219,7 +1194,7 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
topic = ircmsg.TruncateUTF8Safe(topic, client.server.Config().Limits.TopicLen)
|
topic = ircutils.TruncateUTF8Safe(topic, client.server.Config().Limits.TopicLen)
|
||||||
|
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
chname := channel.name
|
chname := channel.name
|
||||||
|
|
@ -1243,10 +1218,9 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe
|
||||||
channel.AddHistoryItem(history.Item{
|
channel.AddHistoryItem(history.Item{
|
||||||
Type: history.Topic,
|
Type: history.Topic,
|
||||||
Nick: details.nickMask,
|
Nick: details.nickMask,
|
||||||
Account: details.account,
|
AccountName: details.accountName,
|
||||||
Message: message,
|
Message: message,
|
||||||
IsBot: isBot,
|
IsBot: isBot,
|
||||||
Target: channel.NameCasefolded(),
|
|
||||||
}, details.account)
|
}, details.account)
|
||||||
|
|
||||||
channel.MarkDirty(IncludeTopic)
|
channel.MarkDirty(IncludeTopic)
|
||||||
|
|
@ -1257,26 +1231,20 @@ func (channel *Channel) CanSpeak(client *Client) (bool, modes.Mode) {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
memberData, hasClient := channel.members[client]
|
memberData, hasClient := channel.members[client]
|
||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
|
clientModes := memberData.modes
|
||||||
highestMode := func() modes.Mode {
|
|
||||||
if !hasClient {
|
|
||||||
return modes.Mode(0)
|
|
||||||
}
|
|
||||||
return memberData.modes.HighestChannelUserMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasClient && channel.flags.HasMode(modes.NoOutside) {
|
if !hasClient && channel.flags.HasMode(modes.NoOutside) {
|
||||||
// TODO: enforce regular +b bans on -n channels?
|
// TODO: enforce regular +b bans on -n channels?
|
||||||
return false, modes.NoOutside
|
return false, modes.NoOutside
|
||||||
}
|
}
|
||||||
if channel.isMuted(client) && highestMode() == modes.Mode(0) {
|
if channel.isMuted(client) && clientModes.HighestChannelUserMode() == modes.Mode(0) {
|
||||||
return false, modes.BanMask
|
return false, modes.BanMask
|
||||||
}
|
}
|
||||||
if channel.flags.HasMode(modes.Moderated) && highestMode() == modes.Mode(0) {
|
if channel.flags.HasMode(modes.Moderated) && clientModes.HighestChannelUserMode() == modes.Mode(0) {
|
||||||
return false, modes.Moderated
|
return false, modes.Moderated
|
||||||
}
|
}
|
||||||
if channel.flags.HasMode(modes.RegisteredOnlySpeak) && client.Account() == "" &&
|
if channel.flags.HasMode(modes.RegisteredOnlySpeak) && client.Account() == "" &&
|
||||||
highestMode() == modes.Mode(0) {
|
clientModes.HighestChannelUserMode() == modes.Mode(0) {
|
||||||
return false, modes.RegisteredOnlySpeak
|
return false, modes.RegisteredOnlySpeak
|
||||||
}
|
}
|
||||||
return true, modes.Mode('?')
|
return true, modes.Mode('?')
|
||||||
|
|
@ -1335,11 +1303,6 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
||||||
isBot := client.HasMode(modes.Bot)
|
isBot := client.HasMode(modes.Bot)
|
||||||
chname := channel.Name()
|
chname := channel.Name()
|
||||||
|
|
||||||
// STATUSMSG targets are prefixed with the supplied min-prefix, e.g., @#channel
|
|
||||||
if minPrefixMode != modes.Mode(0) {
|
|
||||||
chname = fmt.Sprintf("%s%s", modes.ChannelModePrefixes[minPrefixMode], chname)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !client.server.Config().Server.Compatibility.allowTruncation {
|
if !client.server.Config().Server.Compatibility.allowTruncation {
|
||||||
if !validateSplitMessageLen(histType, details.nickMask, chname, message) {
|
if !validateSplitMessageLen(histType, details.nickMask, chname, message) {
|
||||||
rb.Add(nil, client.server.name, ERR_INPUTTOOLONG, details.nick, client.t("Line too long to be relayed without truncation"))
|
rb.Add(nil, client.server.name, ERR_INPUTTOOLONG, details.nick, client.t("Line too long to be relayed without truncation"))
|
||||||
|
|
@ -1347,11 +1310,16 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// STATUSMSG targets are prefixed with the supplied min-prefix, e.g., @#channel
|
||||||
|
if minPrefixMode != modes.Mode(0) {
|
||||||
|
chname = fmt.Sprintf("%s%s", modes.ChannelModePrefixes[minPrefixMode], chname)
|
||||||
|
}
|
||||||
|
|
||||||
if channel.flags.HasMode(modes.OpModerated) {
|
if channel.flags.HasMode(modes.OpModerated) {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
cuData, ok := channel.members[client]
|
cuData := channel.members[client]
|
||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
if !ok || cuData.modes.HighestChannelUserMode() == modes.Mode(0) {
|
if cuData.modes.HighestChannelUserMode() == modes.Mode(0) {
|
||||||
// max(statusmsg_minmode, halfop)
|
// max(statusmsg_minmode, halfop)
|
||||||
if minPrefixMode == modes.Mode(0) || minPrefixMode == modes.Voice {
|
if minPrefixMode == modes.Mode(0) || minPrefixMode == modes.Voice {
|
||||||
minPrefixMode = modes.Halfop
|
minPrefixMode = modes.Halfop
|
||||||
|
|
@ -1389,10 +1357,9 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
||||||
Type: histType,
|
Type: histType,
|
||||||
Message: message,
|
Message: message,
|
||||||
Nick: details.nickMask,
|
Nick: details.nickMask,
|
||||||
Account: details.accountName,
|
AccountName: details.accountName,
|
||||||
Tags: clientOnlyTags,
|
Tags: clientOnlyTags,
|
||||||
IsBot: isBot,
|
IsBot: isBot,
|
||||||
Target: channel.NameCasefolded(),
|
|
||||||
}, details.account)
|
}, details.account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1466,7 +1433,6 @@ func (channel *Channel) Quit(client *Client) {
|
||||||
client.server.channels.Cleanup(channel)
|
client.server.channels.Cleanup(channel)
|
||||||
}
|
}
|
||||||
client.removeChannel(channel)
|
client.removeChannel(channel)
|
||||||
channel.Broadcast("KICK", client.NickCasefolded())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer, hasPrivs bool) {
|
func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer, hasPrivs bool) {
|
||||||
|
|
@ -1481,7 +1447,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
comment = ircmsg.TruncateUTF8Safe(comment, channel.server.Config().Limits.KickLen)
|
comment = ircutils.TruncateUTF8Safe(comment, channel.server.Config().Limits.KickLen)
|
||||||
|
|
||||||
message := utils.MakeMessage(comment)
|
message := utils.MakeMessage(comment)
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
|
|
@ -1501,10 +1467,9 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
|
||||||
histItem := history.Item{
|
histItem := history.Item{
|
||||||
Type: history.Kick,
|
Type: history.Kick,
|
||||||
Nick: details.nickMask,
|
Nick: details.nickMask,
|
||||||
Account: details.account,
|
AccountName: details.accountName,
|
||||||
Message: message,
|
Message: message,
|
||||||
IsBot: isBot,
|
IsBot: isBot,
|
||||||
Target: channel.NameCasefolded(),
|
|
||||||
}
|
}
|
||||||
histItem.Params[0] = targetNick
|
histItem.Params[0] = targetNick
|
||||||
channel.AddHistoryItem(histItem, details.account)
|
channel.AddHistoryItem(histItem, details.account)
|
||||||
|
|
@ -1523,7 +1488,6 @@ func (channel *Channel) Purge(source string) {
|
||||||
chname := channel.name
|
chname := channel.name
|
||||||
members := channel.membersCache
|
members := channel.membersCache
|
||||||
channel.membersCache = nil
|
channel.membersCache = nil
|
||||||
channel.memberDataCache = nil
|
|
||||||
channel.members = make(MemberSet)
|
channel.members = make(MemberSet)
|
||||||
// TODO try to prevent Purge racing against (pending) Join?
|
// TODO try to prevent Purge racing against (pending) Join?
|
||||||
channel.stateMutex.Unlock()
|
channel.stateMutex.Unlock()
|
||||||
|
|
@ -1531,7 +1495,7 @@ func (channel *Channel) Purge(source string) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
for _, member := range members {
|
for _, member := range members {
|
||||||
tnick := member.Nick()
|
tnick := member.Nick()
|
||||||
msgid := utils.GenerateMessageIdStr()
|
msgid := utils.GenerateSecretToken()
|
||||||
for _, session := range member.Sessions() {
|
for _, session := range member.Sessions() {
|
||||||
session.sendFromClientInternal(false, now, msgid, source, "*", false, nil, "KICK", chname, tnick, member.t("This channel has been purged by the server administrators and cannot be used"))
|
session.sendFromClientInternal(false, now, msgid, source, "*", false, nil, "KICK", chname, tnick, member.t("This channel has been purged by the server administrators and cannot be used"))
|
||||||
}
|
}
|
||||||
|
|
@ -1581,8 +1545,6 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
|
||||||
item := history.Item{
|
item := history.Item{
|
||||||
Type: history.Invite,
|
Type: history.Invite,
|
||||||
Message: message,
|
Message: message,
|
||||||
Account: inviter.Account(),
|
|
||||||
Target: invitee.Account(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
|
|
@ -1646,26 +1608,6 @@ func (channel *Channel) auditoriumFriends(client *Client) (friends []*Client) {
|
||||||
return
|
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
|
|
||||||
func (channel *Channel) memberIsVisible(client *Client) bool {
|
|
||||||
// fast path, we assume they're a member so if this isn't an auditorium,
|
|
||||||
// they're visible:
|
|
||||||
if !channel.flags.HasMode(modes.Auditorium) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.stateMutex.RLock()
|
|
||||||
defer channel.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
clientData, found := channel.members[client]
|
|
||||||
if !found {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return clientData.modes.HighestChannelUserMode() != modes.Mode(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// data for RPL_LIST
|
// data for RPL_LIST
|
||||||
func (channel *Channel) listData() (memberCount int, name, topic string) {
|
func (channel *Channel) listData() (memberCount int, name, topic string) {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ package irc
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/datastore"
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -28,75 +26,85 @@ type ChannelManager struct {
|
||||||
sync.RWMutex // tier 2
|
sync.RWMutex // tier 2
|
||||||
// chans is the main data structure, mapping casefolded name -> *Channel
|
// chans is the main data structure, mapping casefolded name -> *Channel
|
||||||
chans map[string]*channelManagerEntry
|
chans map[string]*channelManagerEntry
|
||||||
chansSkeletons utils.HashSet[string]
|
chansSkeletons utils.HashSet[string] // skeletons of *unregistered* chans
|
||||||
purgedChannels map[string]ChannelPurgeRecord // casefolded name to purge record
|
registeredChannels utils.HashSet[string] // casefolds of registered chans
|
||||||
|
registeredSkeletons utils.HashSet[string] // skeletons of registered chans
|
||||||
|
purgedChannels utils.HashSet[string] // casefolds of purged chans
|
||||||
server *Server
|
server *Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannelManager returns a new ChannelManager.
|
// NewChannelManager returns a new ChannelManager.
|
||||||
func (cm *ChannelManager) Initialize(server *Server, config *Config) (err error) {
|
func (cm *ChannelManager) Initialize(server *Server) {
|
||||||
cm.chans = make(map[string]*channelManagerEntry)
|
cm.chans = make(map[string]*channelManagerEntry)
|
||||||
cm.chansSkeletons = make(utils.HashSet[string])
|
cm.chansSkeletons = make(utils.HashSet[string])
|
||||||
cm.server = server
|
cm.server = server
|
||||||
return cm.loadRegisteredChannels(config)
|
|
||||||
|
// purging should work even if registration is disabled
|
||||||
|
cm.purgedChannels = cm.server.channelRegistry.PurgedChannels()
|
||||||
|
cm.loadRegisteredChannels(server.Config())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *ChannelManager) loadRegisteredChannels(config *Config) (err error) {
|
func (cm *ChannelManager) loadRegisteredChannels(config *Config) {
|
||||||
allChannels, err := FetchAndDeserializeAll[RegisteredChannel](datastore.TableChannels, cm.server.dstore, cm.server.logger)
|
if !config.Channels.Registration.Enabled {
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
allPurgeRecords, err := FetchAndDeserializeAll[ChannelPurgeRecord](datastore.TableChannelPurges, cm.server.dstore, cm.server.logger)
|
|
||||||
if err != nil {
|
var newChannels []*Channel
|
||||||
return
|
var collisions []string
|
||||||
|
defer func() {
|
||||||
|
for _, ch := range newChannels {
|
||||||
|
ch.EnsureLoaded()
|
||||||
|
cm.server.logger.Debug("channels", "initialized registered channel", ch.Name())
|
||||||
}
|
}
|
||||||
|
for _, collision := range collisions {
|
||||||
|
cm.server.logger.Warning("channels", "registered channel collides with existing channel", collision)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
rawNames := cm.server.channelRegistry.AllChannels()
|
||||||
|
|
||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
cm.purgedChannels = make(map[string]ChannelPurgeRecord, len(allPurgeRecords))
|
cm.registeredChannels = make(utils.HashSet[string], len(rawNames))
|
||||||
for _, purge := range allPurgeRecords {
|
cm.registeredSkeletons = make(utils.HashSet[string], len(rawNames))
|
||||||
cm.purgedChannels[purge.NameCasefolded] = purge
|
for _, name := range rawNames {
|
||||||
}
|
cfname, err := CasefoldChannel(name)
|
||||||
|
|
||||||
for _, regInfo := range allChannels {
|
|
||||||
cfname, err := CasefoldChannel(regInfo.Name)
|
|
||||||
if err != nil {
|
|
||||||
cm.server.logger.Error("channels", "couldn't casefold registered channel, skipping", regInfo.Name, err.Error())
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
cm.server.logger.Debug("channels", "initializing registered channel", regInfo.Name)
|
|
||||||
}
|
|
||||||
skeleton, err := Skeleton(regInfo.Name)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cm.chansSkeletons.Add(skeleton)
|
cm.registeredChannels.Add(cfname)
|
||||||
|
}
|
||||||
|
skeleton, err := Skeleton(name)
|
||||||
|
if err == nil {
|
||||||
|
cm.registeredSkeletons.Add(skeleton)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := cm.purgedChannels[cfname]; !ok {
|
if !cm.purgedChannels.Has(cfname) {
|
||||||
ch := NewChannel(cm.server, regInfo.Name, cfname, true, regInfo)
|
if _, ok := cm.chans[cfname]; !ok {
|
||||||
|
ch := NewChannel(cm.server, name, cfname, true)
|
||||||
cm.chans[cfname] = &channelManagerEntry{
|
cm.chans[cfname] = &channelManagerEntry{
|
||||||
channel: ch,
|
channel: ch,
|
||||||
pendingJoins: 0,
|
pendingJoins: 0,
|
||||||
skeleton: skeleton,
|
}
|
||||||
|
newChannels = append(newChannels, ch)
|
||||||
|
} else {
|
||||||
|
collisions = append(collisions, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns an existing channel with name equivalent to `name`, or nil
|
// Get returns an existing channel with name equivalent to `name`, or nil
|
||||||
func (cm *ChannelManager) Get(name string) (channel *Channel) {
|
func (cm *ChannelManager) Get(name string) (channel *Channel) {
|
||||||
name, err := CasefoldChannel(name)
|
name, err := CasefoldChannel(name)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cm.RLock()
|
cm.RLock()
|
||||||
defer cm.RUnlock()
|
defer cm.RUnlock()
|
||||||
entry := cm.chans[name]
|
entry := cm.chans[name]
|
||||||
if entry != nil {
|
// if the channel is still loading, pretend we don't have it
|
||||||
|
if entry != nil && entry.channel.IsLoaded() {
|
||||||
return entry.channel
|
return entry.channel
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,26 +122,33 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
|
||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
// check purges first; a registered purged channel will still be present in `chans`
|
if cm.purgedChannels.Has(casefoldedName) {
|
||||||
if _, ok := cm.purgedChannels[casefoldedName]; ok {
|
|
||||||
return nil, errChannelPurged, false
|
return nil, errChannelPurged, false
|
||||||
}
|
}
|
||||||
entry := cm.chans[casefoldedName]
|
entry := cm.chans[casefoldedName]
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
if server.Config().Channels.OpOnlyCreation &&
|
registered := cm.registeredChannels.Has(casefoldedName)
|
||||||
|
// enforce OpOnlyCreation
|
||||||
|
if !registered && server.Config().Channels.OpOnlyCreation &&
|
||||||
!(isSajoin || client.HasRoleCapabs("chanreg")) {
|
!(isSajoin || client.HasRoleCapabs("chanreg")) {
|
||||||
return nil, errInsufficientPrivs, false
|
return nil, errInsufficientPrivs, false
|
||||||
}
|
}
|
||||||
// enforce confusables
|
// enforce confusables
|
||||||
if cm.chansSkeletons.Has(skeleton) {
|
if !registered && (cm.chansSkeletons.Has(skeleton) || cm.registeredSkeletons.Has(skeleton)) {
|
||||||
return nil, errConfusableIdentifier, false
|
return nil, errConfusableIdentifier, false
|
||||||
}
|
}
|
||||||
entry = &channelManagerEntry{
|
entry = &channelManagerEntry{
|
||||||
channel: NewChannel(server, name, casefoldedName, false, RegisteredChannel{}),
|
channel: NewChannel(server, name, casefoldedName, registered),
|
||||||
pendingJoins: 0,
|
pendingJoins: 0,
|
||||||
}
|
}
|
||||||
|
if !registered {
|
||||||
|
// for an unregistered channel, we already have the correct unfolded name
|
||||||
|
// and therefore the final skeleton. for a registered channel, we don't have
|
||||||
|
// the unfolded name yet (it needs to be loaded from the db), but we already
|
||||||
|
// have the final skeleton in `registeredSkeletons` so we don't need to track it
|
||||||
cm.chansSkeletons.Add(skeleton)
|
cm.chansSkeletons.Add(skeleton)
|
||||||
entry.skeleton = skeleton
|
entry.skeleton = skeleton
|
||||||
|
}
|
||||||
cm.chans[casefoldedName] = entry
|
cm.chans[casefoldedName] = entry
|
||||||
newChannel = true
|
newChannel = true
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +160,7 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
|
||||||
return err, ""
|
return err, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channel.EnsureLoaded()
|
||||||
err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
|
err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
|
||||||
|
|
||||||
cm.maybeCleanup(channel, true)
|
cm.maybeCleanup(channel, true)
|
||||||
|
|
@ -236,6 +252,13 @@ func (cm *ChannelManager) SetRegistered(channelName string, account string) (err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// transfer the skeleton from chansSkeletons to registeredSkeletons
|
||||||
|
skeleton := entry.skeleton
|
||||||
|
delete(cm.chansSkeletons, skeleton)
|
||||||
|
entry.skeleton = ""
|
||||||
|
cm.chans[cfname] = entry
|
||||||
|
cm.registeredChannels.Add(cfname)
|
||||||
|
cm.registeredSkeletons.Add(skeleton)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,13 +268,17 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var uuid utils.UUID
|
info, err := cm.server.channelRegistry.LoadChannel(cfname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.Founder != account {
|
||||||
|
return errChannelNotOwnedByAccount
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if delErr := cm.server.dstore.Delete(datastore.TableChannels, uuid); delErr != nil {
|
err = cm.server.channelRegistry.Delete(info)
|
||||||
cm.server.logger.Error("datastore", "couldn't delete channel registration", cfname, delErr.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -259,11 +286,15 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
entry := cm.chans[cfname]
|
entry := cm.chans[cfname]
|
||||||
if entry != nil {
|
if entry != nil {
|
||||||
if entry.channel.Founder() != account {
|
entry.channel.SetUnregistered(account)
|
||||||
return errChannelNotOwnedByAccount
|
delete(cm.registeredChannels, cfname)
|
||||||
|
// transfer the skeleton from registeredSkeletons to chansSkeletons
|
||||||
|
if skel, err := Skeleton(entry.channel.Name()); err == nil {
|
||||||
|
delete(cm.registeredSkeletons, skel)
|
||||||
|
cm.chansSkeletons.Add(skel)
|
||||||
|
entry.skeleton = skel
|
||||||
|
cm.chans[cfname] = entry
|
||||||
}
|
}
|
||||||
uuid = entry.channel.UUID()
|
|
||||||
entry.channel.SetUnregistered(account) // changes the UUID
|
|
||||||
// #1619: if the channel has 0 members and was only being retained
|
// #1619: if the channel has 0 members and was only being retained
|
||||||
// because it was registered, clean it up:
|
// because it was registered, clean it up:
|
||||||
cm.maybeCleanupInternal(cfname, entry, false)
|
cm.maybeCleanupInternal(cfname, entry, false)
|
||||||
|
|
@ -291,11 +322,12 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||||
var info RegisteredChannel
|
var info RegisteredChannel
|
||||||
defer func() {
|
defer func() {
|
||||||
if channel != nil && info.Founder != "" {
|
if channel != nil && info.Founder != "" {
|
||||||
channel.MarkDirty(IncludeAllAttrs)
|
channel.Store(IncludeAllAttrs)
|
||||||
|
if oldCfname != newCfname {
|
||||||
|
// we just flushed the channel under its new name, therefore this delete
|
||||||
|
// cannot be overwritten by a write to the old name:
|
||||||
|
cm.server.channelRegistry.Delete(info)
|
||||||
}
|
}
|
||||||
// always-on clients need to update their saved channel memberships
|
|
||||||
for _, member := range channel.Members() {
|
|
||||||
member.markDirty(IncludeChannels)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -303,11 +335,11 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
entry := cm.chans[oldCfname]
|
entry := cm.chans[oldCfname]
|
||||||
if entry == nil {
|
if entry == nil || !entry.channel.IsLoaded() {
|
||||||
return errNoSuchChannel
|
return errNoSuchChannel
|
||||||
}
|
}
|
||||||
channel = entry.channel
|
channel = entry.channel
|
||||||
info = channel.ExportRegistration()
|
info = channel.ExportRegistration(IncludeInitial)
|
||||||
registered := info.Founder != ""
|
registered := info.Founder != ""
|
||||||
|
|
||||||
oldSkeleton, err := Skeleton(info.Name)
|
oldSkeleton, err := Skeleton(info.Name)
|
||||||
|
|
@ -316,13 +348,13 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if newCfname != oldCfname {
|
if newCfname != oldCfname {
|
||||||
if cm.chans[newCfname] != nil {
|
if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) {
|
||||||
return errChannelNameInUse
|
return errChannelNameInUse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldSkeleton != newSkeleton {
|
if oldSkeleton != newSkeleton {
|
||||||
if cm.chansSkeletons.Has(newSkeleton) {
|
if cm.chansSkeletons.Has(newSkeleton) || cm.registeredSkeletons.Has(newSkeleton) {
|
||||||
return errConfusableIdentifier
|
return errConfusableIdentifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,8 +364,15 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||||
entry.skeleton = newSkeleton
|
entry.skeleton = newSkeleton
|
||||||
}
|
}
|
||||||
cm.chans[newCfname] = entry
|
cm.chans[newCfname] = entry
|
||||||
|
if registered {
|
||||||
|
delete(cm.registeredChannels, oldCfname)
|
||||||
|
cm.registeredChannels.Add(newCfname)
|
||||||
|
delete(cm.registeredSkeletons, oldSkeleton)
|
||||||
|
cm.registeredSkeletons.Add(newSkeleton)
|
||||||
|
} else {
|
||||||
delete(cm.chansSkeletons, oldSkeleton)
|
delete(cm.chansSkeletons, oldSkeleton)
|
||||||
cm.chansSkeletons.Add(newSkeleton)
|
cm.chansSkeletons.Add(newSkeleton)
|
||||||
|
}
|
||||||
entry.channel.Rename(newName, newCfname)
|
entry.channel.Rename(newName, newCfname)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -351,18 +390,7 @@ func (cm *ChannelManager) Channels() (result []*Channel) {
|
||||||
defer cm.RUnlock()
|
defer cm.RUnlock()
|
||||||
result = make([]*Channel, 0, len(cm.chans))
|
result = make([]*Channel, 0, len(cm.chans))
|
||||||
for _, entry := range cm.chans {
|
for _, entry := range cm.chans {
|
||||||
result = append(result, entry.channel)
|
if entry.channel.IsLoaded() {
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListableChannels returns a slice of all non-purged channels.
|
|
||||||
func (cm *ChannelManager) ListableChannels() (result []*Channel) {
|
|
||||||
cm.RLock()
|
|
||||||
defer cm.RUnlock()
|
|
||||||
result = make([]*Channel, 0, len(cm.chans))
|
|
||||||
for cfname, entry := range cm.chans {
|
|
||||||
if _, ok := cm.purgedChannels[cfname]; !ok {
|
|
||||||
result = append(result, entry.channel)
|
result = append(result, entry.channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -375,46 +403,29 @@ func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err e
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errInvalidChannelName
|
return errInvalidChannelName
|
||||||
}
|
}
|
||||||
|
skel, err := Skeleton(chname)
|
||||||
record.NameCasefolded = chname
|
|
||||||
record.UUID = utils.GenerateUUIDv4()
|
|
||||||
|
|
||||||
channel, err := func() (channel *Channel, err error) {
|
|
||||||
cm.Lock()
|
|
||||||
defer cm.Unlock()
|
|
||||||
|
|
||||||
if _, ok := cm.purgedChannels[chname]; ok {
|
|
||||||
return nil, errChannelPurgedAlready
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := cm.chans[chname]
|
|
||||||
// atomically prevent anyone from rejoining
|
|
||||||
cm.purgedChannels[chname] = record
|
|
||||||
if entry != nil {
|
|
||||||
channel = entry.channel
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errInvalidChannelName
|
||||||
}
|
}
|
||||||
|
|
||||||
if channel != nil {
|
cm.Lock()
|
||||||
// actually kick everyone off the channel
|
cm.purgedChannels.Add(chname)
|
||||||
channel.Purge("")
|
entry := cm.chans[chname]
|
||||||
|
if entry != nil {
|
||||||
|
delete(cm.chans, chname)
|
||||||
|
if entry.channel.Founder() != "" {
|
||||||
|
delete(cm.registeredSkeletons, skel)
|
||||||
|
} else {
|
||||||
|
delete(cm.chansSkeletons, skel)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
cm.Unlock()
|
||||||
|
|
||||||
var purgeBytes []byte
|
cm.server.channelRegistry.PurgeChannel(chname, record)
|
||||||
if purgeBytes, err = record.Serialize(); err != nil {
|
if entry != nil {
|
||||||
cm.server.logger.Error("internal", "couldn't serialize purge record", channel.Name(), err.Error())
|
entry.channel.Purge("")
|
||||||
}
|
}
|
||||||
// TODO we need a better story about error handling for later
|
return nil
|
||||||
if err = cm.server.dstore.Set(datastore.TableChannelPurges, record.UUID, purgeBytes, time.Time{}); err != nil {
|
|
||||||
cm.server.logger.Error("datastore", "couldn't store purge record", chname, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPurged queries whether a channel is purged.
|
// IsPurged queries whether a channel is purged.
|
||||||
|
|
@ -425,7 +436,7 @@ func (cm *ChannelManager) IsPurged(chname string) (result bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cm.RLock()
|
cm.RLock()
|
||||||
_, result = cm.purgedChannels[chname]
|
result = cm.purgedChannels.Has(chname)
|
||||||
cm.RUnlock()
|
cm.RUnlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -438,16 +449,14 @@ func (cm *ChannelManager) Unpurge(chname string) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cm.Lock()
|
cm.Lock()
|
||||||
record, found := cm.purgedChannels[chname]
|
found := cm.purgedChannels.Has(chname)
|
||||||
delete(cm.purgedChannels, chname)
|
delete(cm.purgedChannels, chname)
|
||||||
cm.Unlock()
|
cm.Unlock()
|
||||||
|
|
||||||
|
cm.server.channelRegistry.UnpurgeChannel(chname)
|
||||||
if !found {
|
if !found {
|
||||||
return errNoSuchChannel
|
return errNoSuchChannel
|
||||||
}
|
}
|
||||||
if err := cm.server.dstore.Delete(datastore.TableChannelPurges, record.UUID); err != nil {
|
|
||||||
cm.server.logger.Error("datastore", "couldn't delete purge record", chname, err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -466,46 +475,8 @@ func (cm *ChannelManager) UnfoldName(cfname string) (result string) {
|
||||||
cm.RLock()
|
cm.RLock()
|
||||||
entry := cm.chans[cfname]
|
entry := cm.chans[cfname]
|
||||||
cm.RUnlock()
|
cm.RUnlock()
|
||||||
if entry != nil {
|
if entry != nil && entry.channel.IsLoaded() {
|
||||||
return entry.channel.Name()
|
return entry.channel.Name()
|
||||||
}
|
}
|
||||||
return cfname
|
return cfname
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *ChannelManager) LoadPurgeRecord(cfchname string) (record ChannelPurgeRecord, err error) {
|
|
||||||
cm.RLock()
|
|
||||||
defer cm.RUnlock()
|
|
||||||
|
|
||||||
if record, ok := cm.purgedChannels[cfchname]; ok {
|
|
||||||
return record, nil
|
|
||||||
} else {
|
|
||||||
return record, errNoSuchChannel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *ChannelManager) ChannelsForAccount(account string) (channels []string) {
|
|
||||||
cm.RLock()
|
|
||||||
defer cm.RUnlock()
|
|
||||||
|
|
||||||
for cfname, entry := range cm.chans {
|
|
||||||
if entry.channel.Founder() == account {
|
|
||||||
channels = append(channels, cfname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllChannels returns the uncasefolded names of all registered channels.
|
|
||||||
func (cm *ChannelManager) AllRegisteredChannels() (result []string) {
|
|
||||||
cm.RLock()
|
|
||||||
defer cm.RUnlock()
|
|
||||||
|
|
||||||
for cfname, entry := range cm.chans {
|
|
||||||
if entry.channel.Founder() != "" {
|
|
||||||
result = append(result, cfname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,13 @@ package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/buntdb"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -14,6 +19,48 @@ import (
|
||||||
// this is exclusively the *persistence* layer for channel registration;
|
// this is exclusively the *persistence* layer for channel registration;
|
||||||
// channel creation/tracking/destruction is in channelmanager.go
|
// channel creation/tracking/destruction is in channelmanager.go
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyChannelExists = "channel.exists %s"
|
||||||
|
keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
|
||||||
|
keyChannelRegTime = "channel.registered.time %s"
|
||||||
|
keyChannelFounder = "channel.founder %s"
|
||||||
|
keyChannelTopic = "channel.topic %s"
|
||||||
|
keyChannelTopicSetBy = "channel.topic.setby %s"
|
||||||
|
keyChannelTopicSetTime = "channel.topic.settime %s"
|
||||||
|
keyChannelBanlist = "channel.banlist %s"
|
||||||
|
keyChannelExceptlist = "channel.exceptlist %s"
|
||||||
|
keyChannelInvitelist = "channel.invitelist %s"
|
||||||
|
keyChannelPassword = "channel.key %s"
|
||||||
|
keyChannelModes = "channel.modes %s"
|
||||||
|
keyChannelAccountToUMode = "channel.accounttoumode %s"
|
||||||
|
keyChannelUserLimit = "channel.userlimit %s"
|
||||||
|
keyChannelSettings = "channel.settings %s"
|
||||||
|
keyChannelForward = "channel.forward %s"
|
||||||
|
|
||||||
|
keyChannelPurged = "channel.purged %s"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
channelKeyStrings = []string{
|
||||||
|
keyChannelExists,
|
||||||
|
keyChannelName,
|
||||||
|
keyChannelRegTime,
|
||||||
|
keyChannelFounder,
|
||||||
|
keyChannelTopic,
|
||||||
|
keyChannelTopicSetBy,
|
||||||
|
keyChannelTopicSetTime,
|
||||||
|
keyChannelBanlist,
|
||||||
|
keyChannelExceptlist,
|
||||||
|
keyChannelInvitelist,
|
||||||
|
keyChannelPassword,
|
||||||
|
keyChannelModes,
|
||||||
|
keyChannelAccountToUMode,
|
||||||
|
keyChannelUserLimit,
|
||||||
|
keyChannelSettings,
|
||||||
|
keyChannelForward,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// these are bit flags indicating what part of the channel status is "dirty"
|
// these are bit flags indicating what part of the channel status is "dirty"
|
||||||
// and needs to be read from memory and written to the db
|
// and needs to be read from memory and written to the db
|
||||||
const (
|
const (
|
||||||
|
|
@ -33,8 +80,8 @@ const (
|
||||||
type RegisteredChannel struct {
|
type RegisteredChannel struct {
|
||||||
// Name of the channel.
|
// Name of the channel.
|
||||||
Name string
|
Name string
|
||||||
// UUID for the datastore.
|
// Casefolded name of the channel.
|
||||||
UUID utils.UUID
|
NameCasefolded string
|
||||||
// RegisteredAt represents the time that the channel was registered.
|
// RegisteredAt represents the time that the channel was registered.
|
||||||
RegisteredAt time.Time
|
RegisteredAt time.Time
|
||||||
// Founder indicates the founder of the channel.
|
// Founder indicates the founder of the channel.
|
||||||
|
|
@ -65,26 +112,322 @@ type RegisteredChannel struct {
|
||||||
Settings ChannelSettings
|
Settings ChannelSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RegisteredChannel) Serialize() ([]byte, error) {
|
|
||||||
return json.Marshal(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegisteredChannel) Deserialize(b []byte) (err error) {
|
|
||||||
return json.Unmarshal(b, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChannelPurgeRecord struct {
|
type ChannelPurgeRecord struct {
|
||||||
NameCasefolded string `json:"Name"`
|
|
||||||
UUID utils.UUID
|
|
||||||
Oper string
|
Oper string
|
||||||
PurgedAt time.Time
|
PurgedAt time.Time
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ChannelPurgeRecord) Serialize() ([]byte, error) {
|
// ChannelRegistry manages registered channels.
|
||||||
return json.Marshal(c)
|
type ChannelRegistry struct {
|
||||||
|
server *Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ChannelPurgeRecord) Deserialize(b []byte) error {
|
// NewChannelRegistry returns a new ChannelRegistry.
|
||||||
return json.Unmarshal(b, c)
|
func (reg *ChannelRegistry) Initialize(server *Server) {
|
||||||
|
reg.server = server
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllChannels returns the uncasefolded names of all registered channels.
|
||||||
|
func (reg *ChannelRegistry) AllChannels() (result []string) {
|
||||||
|
prefix := fmt.Sprintf(keyChannelName, "")
|
||||||
|
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
result = append(result, value)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurgedChannels returns the set of all casefolded channel names that have been purged
|
||||||
|
func (reg *ChannelRegistry) PurgedChannels() (result utils.HashSet[string]) {
|
||||||
|
result = make(utils.HashSet[string])
|
||||||
|
|
||||||
|
prefix := fmt.Sprintf(keyChannelPurged, "")
|
||||||
|
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
channel := strings.TrimPrefix(key, prefix)
|
||||||
|
result.Add(channel)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreChannel obtains a consistent view of a channel, then persists it to the store.
|
||||||
|
func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) {
|
||||||
|
if !reg.server.ChannelRegistrationEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Founder == "" {
|
||||||
|
// sanity check, don't try to store an unregistered channel
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
reg.saveChannel(tx, info, includeFlags)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadChannel loads a channel from the store.
|
||||||
|
func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredChannel, err error) {
|
||||||
|
if !reg.server.ChannelRegistrationEnabled() {
|
||||||
|
err = errFeatureDisabled
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channelKey := nameCasefolded
|
||||||
|
// nice to have: do all JSON (de)serialization outside of the buntdb transaction
|
||||||
|
err = reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
_, dberr := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
|
||||||
|
if dberr == buntdb.ErrNotFound {
|
||||||
|
// chan does not already exist, return
|
||||||
|
return errNoSuchChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel exists, load it
|
||||||
|
name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey))
|
||||||
|
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey))
|
||||||
|
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
|
||||||
|
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey))
|
||||||
|
topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey))
|
||||||
|
topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey))
|
||||||
|
var topicSetTime time.Time
|
||||||
|
topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
|
||||||
|
if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil {
|
||||||
|
topicSetTime = time.Unix(0, topicSetTimeInt).UTC()
|
||||||
|
}
|
||||||
|
password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
|
||||||
|
modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
|
||||||
|
userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey))
|
||||||
|
forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey))
|
||||||
|
banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey))
|
||||||
|
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
|
||||||
|
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
|
||||||
|
accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
|
||||||
|
settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
|
||||||
|
|
||||||
|
modeSlice := make([]modes.Mode, len(modeString))
|
||||||
|
for i, mode := range modeString {
|
||||||
|
modeSlice[i] = modes.Mode(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
userLimit, _ := strconv.Atoi(userLimitString)
|
||||||
|
|
||||||
|
var banlist map[string]MaskInfo
|
||||||
|
_ = json.Unmarshal([]byte(banlistString), &banlist)
|
||||||
|
var exceptlist map[string]MaskInfo
|
||||||
|
_ = json.Unmarshal([]byte(exceptlistString), &exceptlist)
|
||||||
|
var invitelist map[string]MaskInfo
|
||||||
|
_ = json.Unmarshal([]byte(invitelistString), &invitelist)
|
||||||
|
accountToUMode := make(map[string]modes.Mode)
|
||||||
|
_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
|
||||||
|
|
||||||
|
var settings ChannelSettings
|
||||||
|
_ = json.Unmarshal([]byte(settingsString), &settings)
|
||||||
|
|
||||||
|
info = RegisteredChannel{
|
||||||
|
Name: name,
|
||||||
|
NameCasefolded: nameCasefolded,
|
||||||
|
RegisteredAt: time.Unix(0, regTimeInt).UTC(),
|
||||||
|
Founder: founder,
|
||||||
|
Topic: topic,
|
||||||
|
TopicSetBy: topicSetBy,
|
||||||
|
TopicSetTime: topicSetTime,
|
||||||
|
Key: password,
|
||||||
|
Modes: modeSlice,
|
||||||
|
Bans: banlist,
|
||||||
|
Excepts: exceptlist,
|
||||||
|
Invites: invitelist,
|
||||||
|
AccountToUMode: accountToUMode,
|
||||||
|
UserLimit: int(userLimit),
|
||||||
|
Settings: settings,
|
||||||
|
Forward: forward,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a channel corresponding to `info`. If no such channel
|
||||||
|
// is present in the database, no error is returned.
|
||||||
|
func (reg *ChannelRegistry) Delete(info RegisteredChannel) (err error) {
|
||||||
|
if !reg.server.ChannelRegistrationEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
reg.deleteChannel(tx, info.NameCasefolded, info)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete a channel, unless it was overwritten by another registration of the same channel
|
||||||
|
func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info RegisteredChannel) {
|
||||||
|
_, err := tx.Get(fmt.Sprintf(keyChannelExists, key))
|
||||||
|
if err == nil {
|
||||||
|
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key))
|
||||||
|
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
|
||||||
|
registeredAt := time.Unix(0, regTimeInt).UTC()
|
||||||
|
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key))
|
||||||
|
|
||||||
|
// to see if we're deleting the right channel, confirm the founder and the registration time
|
||||||
|
if founder == info.Founder && registeredAt.Equal(info.RegisteredAt) {
|
||||||
|
for _, keyFmt := range channelKeyStrings {
|
||||||
|
tx.Delete(fmt.Sprintf(keyFmt, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove this channel from the client's list of registered channels
|
||||||
|
channelsKey := fmt.Sprintf(keyAccountChannels, info.Founder)
|
||||||
|
channelsStr, err := tx.Get(channelsKey)
|
||||||
|
if err == buntdb.ErrNotFound {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
registeredChannels := unmarshalRegisteredChannels(channelsStr)
|
||||||
|
var nowRegisteredChannels []string
|
||||||
|
for _, channel := range registeredChannels {
|
||||||
|
if channel != key {
|
||||||
|
nowRegisteredChannels = append(nowRegisteredChannels, channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.Set(channelsKey, strings.Join(nowRegisteredChannels, ","), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (reg *ChannelRegistry) updateAccountToChannelMapping(tx *buntdb.Tx, channelInfo RegisteredChannel) {
|
||||||
|
channelKey := channelInfo.NameCasefolded
|
||||||
|
chanFounderKey := fmt.Sprintf(keyChannelFounder, channelKey)
|
||||||
|
founder, existsErr := tx.Get(chanFounderKey)
|
||||||
|
if existsErr == buntdb.ErrNotFound || founder != channelInfo.Founder {
|
||||||
|
// add to new founder's list
|
||||||
|
accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder)
|
||||||
|
alreadyChannels, _ := tx.Get(accountChannelsKey)
|
||||||
|
newChannels := channelKey // this is the casefolded channel name
|
||||||
|
if alreadyChannels != "" {
|
||||||
|
newChannels = fmt.Sprintf("%s,%s", alreadyChannels, newChannels)
|
||||||
|
}
|
||||||
|
tx.Set(accountChannelsKey, newChannels, nil)
|
||||||
|
}
|
||||||
|
if existsErr == nil && founder != channelInfo.Founder {
|
||||||
|
// remove from old founder's list
|
||||||
|
accountChannelsKey := fmt.Sprintf(keyAccountChannels, founder)
|
||||||
|
alreadyChannelsRaw, _ := tx.Get(accountChannelsKey)
|
||||||
|
var newChannels []string
|
||||||
|
if alreadyChannelsRaw != "" {
|
||||||
|
for _, chname := range strings.Split(alreadyChannelsRaw, ",") {
|
||||||
|
if chname != channelInfo.NameCasefolded {
|
||||||
|
newChannels = append(newChannels, chname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.Set(accountChannelsKey, strings.Join(newChannels, ","), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveChannel saves a channel to the store.
|
||||||
|
func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) {
|
||||||
|
channelKey := channelInfo.NameCasefolded
|
||||||
|
// maintain the mapping of account -> registered channels
|
||||||
|
reg.updateAccountToChannelMapping(tx, channelInfo)
|
||||||
|
|
||||||
|
if includeFlags&IncludeInitial != 0 {
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.UnixNano(), 10), nil)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeFlags&IncludeTopic != 0 {
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
|
||||||
|
var topicSetTimeStr string
|
||||||
|
if !channelInfo.TopicSetTime.IsZero() {
|
||||||
|
topicSetTimeStr = strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10)
|
||||||
|
}
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), topicSetTimeStr, nil)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeFlags&IncludeModes != 0 {
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil)
|
||||||
|
modeString := modes.Modes(channelInfo.Modes).String()
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelForward, channelKey), channelInfo.Forward, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeFlags&IncludeLists != 0 {
|
||||||
|
banlistString, _ := json.Marshal(channelInfo.Bans)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil)
|
||||||
|
exceptlistString, _ := json.Marshal(channelInfo.Excepts)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil)
|
||||||
|
invitelistString, _ := json.Marshal(channelInfo.Invites)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil)
|
||||||
|
accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeFlags&IncludeSettings != 0 {
|
||||||
|
settingsString, _ := json.Marshal(channelInfo.Settings)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurgeChannel records a channel purge.
|
||||||
|
func (reg *ChannelRegistry) PurgeChannel(chname string, record ChannelPurgeRecord) (err error) {
|
||||||
|
serialized, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
serializedStr := string(serialized)
|
||||||
|
key := fmt.Sprintf(keyChannelPurged, chname)
|
||||||
|
|
||||||
|
return reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
tx.Set(key, serializedStr, nil)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPurgeRecord retrieves information about whether and how a channel was purged.
|
||||||
|
func (reg *ChannelRegistry) LoadPurgeRecord(chname string) (record ChannelPurgeRecord, err error) {
|
||||||
|
var rawRecord string
|
||||||
|
key := fmt.Sprintf(keyChannelPurged, chname)
|
||||||
|
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
rawRecord, _ = tx.Get(key)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if rawRecord == "" {
|
||||||
|
err = errNoSuchChannel
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(rawRecord), &record)
|
||||||
|
if err != nil {
|
||||||
|
reg.server.logger.Error("internal", "corrupt purge record", chname, err.Error())
|
||||||
|
err = errNoSuchChannel
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnpurgeChannel deletes the record of a channel purge.
|
||||||
|
func (reg *ChannelRegistry) UnpurgeChannel(chname string) (err error) {
|
||||||
|
key := fmt.Sprintf(keyChannelPurged, chname)
|
||||||
|
return reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
tx.Delete(key)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ package irc
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -219,7 +218,7 @@ func csAmodeHandler(service *ircService, server *Server, client *Client, command
|
||||||
// check for anything valid as a channel mode change that is not valid
|
// check for anything valid as a channel mode change that is not valid
|
||||||
// as an AMODE change
|
// as an AMODE change
|
||||||
for _, modeChange := range modeChanges {
|
for _, modeChange := range modeChanges {
|
||||||
if !slices.Contains(modes.ChannelUserModes, modeChange.Mode) {
|
if !utils.SliceContains(modes.ChannelUserModes, modeChange.Mode) {
|
||||||
invalid = true
|
invalid = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -460,7 +459,7 @@ func csRegisterHandler(service *ircService, server *Server, client *Client, comm
|
||||||
// check whether a client has already registered too many channels
|
// check whether a client has already registered too many channels
|
||||||
func checkChanLimit(service *ircService, client *Client, rb *ResponseBuffer) (ok bool) {
|
func checkChanLimit(service *ircService, client *Client, rb *ResponseBuffer) (ok bool) {
|
||||||
account := client.Account()
|
account := client.Account()
|
||||||
channelsAlreadyRegistered := client.server.channels.ChannelsForAccount(account)
|
channelsAlreadyRegistered := client.server.accounts.ChannelsForAccount(account)
|
||||||
ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
|
ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
|
||||||
if !ok {
|
if !ok {
|
||||||
service.Notice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
service.Notice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
||||||
|
|
@ -497,8 +496,8 @@ func csUnregisterHandler(service *ircService, server *Server, client *Client, co
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info := channel.exportSummary()
|
info := channel.ExportRegistration(0)
|
||||||
channelKey := channel.NameCasefolded()
|
channelKey := info.NameCasefolded
|
||||||
if !csPrivsCheck(service, info, client, rb) {
|
if !csPrivsCheck(service, info, client, rb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -520,7 +519,7 @@ func csClearHandler(service *ircService, server *Server, client *Client, command
|
||||||
service.Notice(rb, client.t("Channel does not exist"))
|
service.Notice(rb, client.t("Channel does not exist"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !csPrivsCheck(service, channel.exportSummary(), client, rb) {
|
if !csPrivsCheck(service, channel.ExportRegistration(0), client, rb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -551,7 +550,7 @@ func csTransferHandler(service *ircService, server *Server, client *Client, comm
|
||||||
service.Notice(rb, client.t("Channel does not exist"))
|
service.Notice(rb, client.t("Channel does not exist"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
regInfo := channel.exportSummary()
|
regInfo := channel.ExportRegistration(0)
|
||||||
chname = regInfo.Name
|
chname = regInfo.Name
|
||||||
account := client.Account()
|
account := client.Account()
|
||||||
isFounder := account != "" && account == regInfo.Founder
|
isFounder := account != "" && account == regInfo.Founder
|
||||||
|
|
@ -730,6 +729,11 @@ func csPurgeListHandler(service *ircService, client *Client, rb *ResponseBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func csListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func csListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
if !client.HasRoleCapabs("chanreg") {
|
||||||
|
service.Notice(rb, client.t("Insufficient privileges"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var searchRegex *regexp.Regexp
|
var searchRegex *regexp.Regexp
|
||||||
if len(params) > 0 {
|
if len(params) > 0 {
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -742,7 +746,7 @@ func csListHandler(service *ircService, server *Server, client *Client, command
|
||||||
|
|
||||||
service.Notice(rb, ircfmt.Unescape(client.t("*** $bChanServ LIST$b ***")))
|
service.Notice(rb, ircfmt.Unescape(client.t("*** $bChanServ LIST$b ***")))
|
||||||
|
|
||||||
channels := server.channels.AllRegisteredChannels()
|
channels := server.channelRegistry.AllChannels()
|
||||||
for _, channel := range channels {
|
for _, channel := range channels {
|
||||||
if searchRegex == nil || searchRegex.MatchString(channel) {
|
if searchRegex == nil || searchRegex.MatchString(channel) {
|
||||||
service.Notice(rb, fmt.Sprintf(" %s", channel))
|
service.Notice(rb, fmt.Sprintf(" %s", channel))
|
||||||
|
|
@ -753,7 +757,6 @@ func csListHandler(service *ircService, server *Server, client *Client, command
|
||||||
}
|
}
|
||||||
|
|
||||||
func csInfoHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func csInfoHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
// #765
|
// #765
|
||||||
listRegisteredChannels(service, client.Account(), rb)
|
listRegisteredChannels(service, client.Account(), rb)
|
||||||
|
|
@ -766,41 +769,43 @@ func csInfoHandler(service *ircService, server *Server, client *Client, command
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var chinfo RegisteredChannel
|
|
||||||
channel := server.channels.Get(params[0])
|
|
||||||
if channel != nil {
|
|
||||||
chinfo = channel.exportSummary()
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := map[string]string{
|
|
||||||
"target": chinfo.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
// purge status
|
// purge status
|
||||||
if client.HasRoleCapabs("chanreg") {
|
if client.HasRoleCapabs("chanreg") {
|
||||||
purgeRecord, err := server.channels.LoadPurgeRecord(chname)
|
purgeRecord, err := server.channelRegistry.LoadPurgeRecord(chname)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
service.TaggedNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname), tags)
|
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||||
service.TaggedNotice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper), tags)
|
service.Notice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
|
||||||
service.TaggedNotice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123)), tags)
|
service.Notice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123)))
|
||||||
if purgeRecord.Reason != "" {
|
if purgeRecord.Reason != "" {
|
||||||
service.TaggedNotice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason), tags)
|
service.Notice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if server.channels.IsPurged(chname) {
|
if server.channels.IsPurged(chname) {
|
||||||
service.TaggedNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname), tags)
|
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var chinfo RegisteredChannel
|
||||||
|
channel := server.channels.Get(params[0])
|
||||||
|
if channel != nil {
|
||||||
|
chinfo = channel.ExportRegistration(0)
|
||||||
|
} else {
|
||||||
|
chinfo, err = server.channelRegistry.LoadChannel(chname)
|
||||||
|
if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) {
|
||||||
|
service.Notice(rb, client.t("An error occurred"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// channel exists but is unregistered, or doesn't exist:
|
// channel exists but is unregistered, or doesn't exist:
|
||||||
if chinfo.Founder == "" {
|
if chinfo.Founder == "" {
|
||||||
service.TaggedNotice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname), tags)
|
service.Notice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
service.TaggedNotice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name), tags)
|
service.Notice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name))
|
||||||
service.TaggedNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder), tags)
|
service.Notice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
|
||||||
service.TaggedNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)), tags)
|
service.Notice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayChannelSetting(service *ircService, settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {
|
func displayChannelSetting(service *ircService, settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {
|
||||||
|
|
@ -830,12 +835,12 @@ func csGetHandler(service *ircService, server *Server, client *Client, command s
|
||||||
service.Notice(rb, client.t("No such channel"))
|
service.Notice(rb, client.t("No such channel"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info := channel.exportSummary()
|
info := channel.ExportRegistration(IncludeSettings)
|
||||||
if !csPrivsCheck(service, info, client, rb) {
|
if !csPrivsCheck(service, info, client, rb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
displayChannelSetting(service, setting, channel.Settings(), client, rb)
|
displayChannelSetting(service, setting, info.Settings, client, rb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func csSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func csSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
|
@ -845,12 +850,12 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
|
||||||
service.Notice(rb, client.t("No such channel"))
|
service.Notice(rb, client.t("No such channel"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info := channel.exportSummary()
|
info := channel.ExportRegistration(IncludeSettings)
|
||||||
|
settings := info.Settings
|
||||||
if !csPrivsCheck(service, info, client, rb) {
|
if !csPrivsCheck(service, info, client, rb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := channel.Settings()
|
|
||||||
var err error
|
var err error
|
||||||
switch strings.ToLower(setting) {
|
switch strings.ToLower(setting) {
|
||||||
case "history":
|
case "history":
|
||||||
|
|
|
||||||
164
irc/client.go
164
irc/client.go
|
|
@ -8,7 +8,6 @@ package irc
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net"
|
"net"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -21,7 +20,6 @@ import (
|
||||||
"github.com/ergochat/irc-go/ircfmt"
|
"github.com/ergochat/irc-go/ircfmt"
|
||||||
"github.com/ergochat/irc-go/ircmsg"
|
"github.com/ergochat/irc-go/ircmsg"
|
||||||
"github.com/ergochat/irc-go/ircreader"
|
"github.com/ergochat/irc-go/ircreader"
|
||||||
"github.com/ergochat/irc-go/ircutils"
|
|
||||||
"github.com/xdg-go/scram"
|
"github.com/xdg-go/scram"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/caps"
|
"github.com/ergochat/ergo/irc/caps"
|
||||||
|
|
@ -29,14 +27,13 @@ import (
|
||||||
"github.com/ergochat/ergo/irc/flatip"
|
"github.com/ergochat/ergo/irc/flatip"
|
||||||
"github.com/ergochat/ergo/irc/history"
|
"github.com/ergochat/ergo/irc/history"
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/oauth2"
|
|
||||||
"github.com/ergochat/ergo/irc/sno"
|
"github.com/ergochat/ergo/irc/sno"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Set to 4096 because CEF doesn't care about compatibility
|
// maximum IRC line length, not including tags
|
||||||
DefaultMaxLineLen = 4096
|
DefaultMaxLineLen = 512
|
||||||
|
|
||||||
// IdentTimeout is how long before our ident (username) check times out.
|
// IdentTimeout is how long before our ident (username) check times out.
|
||||||
IdentTimeout = time.Second + 500*time.Millisecond
|
IdentTimeout = time.Second + 500*time.Millisecond
|
||||||
|
|
@ -121,20 +118,12 @@ type Client struct {
|
||||||
|
|
||||||
type saslStatus struct {
|
type saslStatus struct {
|
||||||
mechanism string
|
mechanism string
|
||||||
value ircutils.SASLBuffer
|
value string
|
||||||
scramConv *scram.ServerConversation
|
scramConv *scram.ServerConversation
|
||||||
oauthConv *oauth2.OAuthBearerServer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *saslStatus) Initialize() {
|
|
||||||
s.value.Initialize(saslMaxResponseLength)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *saslStatus) Clear() {
|
func (s *saslStatus) Clear() {
|
||||||
s.mechanism = ""
|
*s = saslStatus{}
|
||||||
s.value.Clear()
|
|
||||||
s.scramConv = nil
|
|
||||||
s.oauthConv = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// what stage the client is at w.r.t. the PASS command:
|
// what stage the client is at w.r.t. the PASS command:
|
||||||
|
|
@ -165,7 +154,6 @@ type Session struct {
|
||||||
realIP net.IP
|
realIP net.IP
|
||||||
proxiedIP net.IP
|
proxiedIP net.IP
|
||||||
rawHostname string
|
rawHostname string
|
||||||
hostnameFinalized bool
|
|
||||||
isTor bool
|
isTor bool
|
||||||
hideSTS bool
|
hideSTS bool
|
||||||
|
|
||||||
|
|
@ -179,8 +167,6 @@ type Session struct {
|
||||||
|
|
||||||
batchCounter atomic.Uint32
|
batchCounter atomic.Uint32
|
||||||
|
|
||||||
isupportSentPrereg bool
|
|
||||||
|
|
||||||
quitMessage string
|
quitMessage string
|
||||||
|
|
||||||
awayMessage string
|
awayMessage string
|
||||||
|
|
@ -309,7 +295,7 @@ func (server *Server) RunClient(conn IRCConn) {
|
||||||
var banMsg string
|
var banMsg string
|
||||||
realIP := utils.AddrToIP(wConn.RemoteAddr())
|
realIP := utils.AddrToIP(wConn.RemoteAddr())
|
||||||
var proxiedIP net.IP
|
var proxiedIP net.IP
|
||||||
if wConn.Tor {
|
if wConn.Config.Tor {
|
||||||
// cover up details of the tor proxying infrastructure (not a user privacy concern,
|
// cover up details of the tor proxying infrastructure (not a user privacy concern,
|
||||||
// but a hardening measure):
|
// but a hardening measure):
|
||||||
proxiedIP = utils.IPv4LoopbackAddress
|
proxiedIP = utils.IPv4LoopbackAddress
|
||||||
|
|
@ -343,7 +329,7 @@ func (server *Server) RunClient(conn IRCConn) {
|
||||||
lastActive: now,
|
lastActive: now,
|
||||||
channels: make(ChannelSet),
|
channels: make(ChannelSet),
|
||||||
ctime: now,
|
ctime: now,
|
||||||
isSTSOnly: wConn.STSOnly,
|
isSTSOnly: wConn.Config.STSOnly,
|
||||||
languages: server.Languages().Default(),
|
languages: server.Languages().Default(),
|
||||||
loginThrottle: connection_limits.GenericThrottle{
|
loginThrottle: connection_limits.GenericThrottle{
|
||||||
Duration: config.Accounts.LoginThrottling.Duration,
|
Duration: config.Accounts.LoginThrottling.Duration,
|
||||||
|
|
@ -372,10 +358,9 @@ func (server *Server) RunClient(conn IRCConn) {
|
||||||
lastActive: now,
|
lastActive: now,
|
||||||
realIP: realIP,
|
realIP: realIP,
|
||||||
proxiedIP: proxiedIP,
|
proxiedIP: proxiedIP,
|
||||||
isTor: wConn.Tor,
|
isTor: wConn.Config.Tor,
|
||||||
hideSTS: wConn.Tor || wConn.HideSTS,
|
hideSTS: wConn.Config.Tor || wConn.Config.HideSTS,
|
||||||
}
|
}
|
||||||
session.sasl.Initialize()
|
|
||||||
client.sessions = []*Session{session}
|
client.sessions = []*Session{session}
|
||||||
|
|
||||||
session.resetFakelag()
|
session.resetFakelag()
|
||||||
|
|
@ -384,7 +369,7 @@ func (server *Server) RunClient(conn IRCConn) {
|
||||||
client.SetMode(modes.TLS, true)
|
client.SetMode(modes.TLS, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if wConn.TLS {
|
if wConn.Config.TLSConfig != nil {
|
||||||
// error is not useful to us here anyways so we can ignore it
|
// error is not useful to us here anyways so we can ignore it
|
||||||
session.certfp, session.peerCerts, _ = utils.GetCertFP(wConn.Conn, RegisterTimeout)
|
session.certfp, session.peerCerts, _ = utils.GetCertFP(wConn.Conn, RegisterTimeout)
|
||||||
}
|
}
|
||||||
|
|
@ -491,21 +476,12 @@ func (client *Client) resizeHistory(config *Config) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// once we have the final IP address (from the connection itself or from proxy data),
|
// resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
|
||||||
// compute the various possibilities for the hostname:
|
// and sending appropriate notices to the client
|
||||||
// * In the default/recommended configuration, via the cloak algorithm
|
func (client *Client) lookupHostname(session *Session, overwrite bool) {
|
||||||
// * If hostname lookup is enabled, via (forward-confirmed) reverse DNS
|
|
||||||
// * If WEBIRC was used, possibly via the hostname passed on the WEBIRC line
|
|
||||||
func (client *Client) finalizeHostname(session *Session) {
|
|
||||||
// only allow this once, since registration can fail (e.g. if the nickname is in use)
|
|
||||||
if session.hostnameFinalized {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
session.hostnameFinalized = true
|
|
||||||
|
|
||||||
if session.isTor {
|
if session.isTor {
|
||||||
return
|
return
|
||||||
}
|
} // else: even if cloaking is enabled, look up the real hostname to show to operators
|
||||||
|
|
||||||
config := client.server.Config()
|
config := client.server.Config()
|
||||||
ip := session.realIP
|
ip := session.realIP
|
||||||
|
|
@ -513,8 +489,6 @@ func (client *Client) finalizeHostname(session *Session) {
|
||||||
ip = session.proxiedIP
|
ip = session.proxiedIP
|
||||||
}
|
}
|
||||||
|
|
||||||
// even if cloaking is enabled, we may want to look up the real hostname to show to operators:
|
|
||||||
if session.rawHostname == "" {
|
|
||||||
var hostname string
|
var hostname string
|
||||||
lookupSuccessful := false
|
lookupSuccessful := false
|
||||||
if config.Server.lookupHostnames {
|
if config.Server.lookupHostnames {
|
||||||
|
|
@ -528,12 +502,17 @@ func (client *Client) finalizeHostname(session *Session) {
|
||||||
} else {
|
} else {
|
||||||
hostname = utils.IPStringToHostname(ip.String())
|
hostname = utils.IPStringToHostname(ip.String())
|
||||||
}
|
}
|
||||||
session.rawHostname = hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
// these will be discarded if this is actually a reattach:
|
session.rawHostname = hostname
|
||||||
client.rawHostname = session.rawHostname
|
cloakedHostname := config.Server.Cloaks.ComputeCloak(ip)
|
||||||
client.cloakedHostname = config.Server.Cloaks.ComputeCloak(ip)
|
client.stateMutex.Lock()
|
||||||
|
defer client.stateMutex.Unlock()
|
||||||
|
// update the hostname if this is a new connection, but not if it's a reattach
|
||||||
|
if overwrite || client.rawHostname == "" {
|
||||||
|
client.rawHostname = hostname
|
||||||
|
client.cloakedHostname = cloakedHostname
|
||||||
|
client.updateNickMaskNoMutex()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) doIdentLookup(conn net.Conn) {
|
func (client *Client) doIdentLookup(conn net.Conn) {
|
||||||
|
|
@ -651,17 +630,6 @@ func (client *Client) run(session *Session) {
|
||||||
|
|
||||||
firstLine := !isReattach
|
firstLine := !isReattach
|
||||||
|
|
||||||
correspondents, _ := client.server.historyDB.GetPMs(client.Account())
|
|
||||||
// For safety, let's keep this within the 4096 character barrier
|
|
||||||
var lineBuilder utils.TokenLineBuilder
|
|
||||||
lineBuilder.Initialize(MaxLineLen, ",")
|
|
||||||
for username, timestamp := range correspondents {
|
|
||||||
lineBuilder.Add(fmt.Sprintf("%s %d", client.server.getCurrentNick(username), timestamp))
|
|
||||||
}
|
|
||||||
for _, message := range lineBuilder.Lines() {
|
|
||||||
session.Send(nil, client.server.name, "PMS", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var invalidUtf8 bool
|
var invalidUtf8 bool
|
||||||
line, err := session.socket.Read()
|
line, err := session.socket.Read()
|
||||||
|
|
@ -875,14 +843,14 @@ func (session *Session) Ping() {
|
||||||
session.Send(nil, "", "PING", session.client.Nick())
|
session.Send(nil, "", "PING", session.client.Nick())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, chathistoryCommand bool, identifier string, preposition string, limit int) {
|
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, chathistoryCommand bool) {
|
||||||
var batchID string
|
var batchID string
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
nick := details.nick
|
nick := details.nick
|
||||||
if target == "" {
|
if target == "" {
|
||||||
target = nick
|
target = nick
|
||||||
}
|
}
|
||||||
batchID = rb.StartNestedBatch("chathistory", target, identifier, preposition, strconv.Itoa(limit))
|
batchID = rb.StartNestedHistoryBatch(target)
|
||||||
|
|
||||||
isSelfMessage := func(item *history.Item) bool {
|
isSelfMessage := func(item *history.Item) bool {
|
||||||
// XXX: Params[0] is the message target. if the source of this message is an in-memory
|
// XXX: Params[0] is the message target. if the source of this message is an in-memory
|
||||||
|
|
@ -903,7 +871,7 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if hasEventPlayback {
|
if hasEventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.Account, item.IsBot, nil, "INVITE", nick, item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "INVITE", nick, item.Message.Message)
|
||||||
} else {
|
} else {
|
||||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", fmt.Sprintf(client.t("%[1]s invited you to channel %[2]s"), NUHToNick(item.Nick), item.Message.Message))
|
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", fmt.Sprintf(client.t("%[1]s invited you to channel %[2]s"), NUHToNick(item.Nick), item.Message.Message))
|
||||||
}
|
}
|
||||||
|
|
@ -931,11 +899,11 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
|
||||||
tags = item.Tags
|
tags = item.Tags
|
||||||
}
|
}
|
||||||
if !isSelfMessage(&item) {
|
if !isSelfMessage(&item) {
|
||||||
rb.AddSplitMessageFromClientWithReactions(item.Nick, item.Account, item.IsBot, tags, command, nick, item.Message, item.Reactions)
|
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, tags, command, nick, item.Message)
|
||||||
} else {
|
} else {
|
||||||
// this message was sent *from* the client to another nick; the target is item.Params[0]
|
// this message was sent *from* the client to another nick; the target is item.Params[0]
|
||||||
// substitute client's current nickmask in case client changed nick
|
// substitute client's current nickmask in case client changed nick
|
||||||
rb.AddSplitMessageFromClientWithReactions(details.nickMask, item.Account, item.IsBot, tags, command, item.Params[0], item.Message, item.Reactions)
|
rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, item.IsBot, tags, command, item.Params[0], item.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1254,11 +1222,14 @@ func (client *Client) destroy(session *Session) {
|
||||||
client.destroyed = true
|
client.destroyed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
wasAway := client.awayMessage
|
becameAutoAway := false
|
||||||
if client.autoAwayEnabledNoMutex(config) {
|
var awayMessage string
|
||||||
|
if alwaysOn && persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) {
|
||||||
|
wasAway := client.awayMessage != ""
|
||||||
client.setAutoAwayNoMutex(config)
|
client.setAutoAwayNoMutex(config)
|
||||||
|
awayMessage = client.awayMessage
|
||||||
|
becameAutoAway = !wasAway && awayMessage != ""
|
||||||
}
|
}
|
||||||
nowAway := client.awayMessage
|
|
||||||
|
|
||||||
if client.registrationTimer != nil {
|
if client.registrationTimer != nil {
|
||||||
// unconditionally stop; if the client is still unregistered it must be destroyed
|
// unconditionally stop; if the client is still unregistered it must be destroyed
|
||||||
|
|
@ -1308,8 +1279,8 @@ func (client *Client) destroy(session *Session) {
|
||||||
client.server.stats.Remove(registered, invisible, operator)
|
client.server.stats.Remove(registered, invisible, operator)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !shouldDestroy && wasAway != nowAway {
|
if becameAutoAway {
|
||||||
dispatchAwayNotify(client, nowAway)
|
dispatchAwayNotify(client, true, awayMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !shouldDestroy {
|
if !shouldDestroy {
|
||||||
|
|
@ -1317,10 +1288,10 @@ func (client *Client) destroy(session *Session) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var quitItem history.Item
|
var quitItem history.Item
|
||||||
var quitHistoryChannels []*Channel
|
var channels []*Channel
|
||||||
// use a defer here to avoid writing to mysql while holding the destroy semaphore:
|
// use a defer here to avoid writing to mysql while holding the destroy semaphore:
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, channel := range quitHistoryChannels {
|
for _, channel := range channels {
|
||||||
channel.AddHistoryItem(quitItem, details.account)
|
channel.AddHistoryItem(quitItem, details.account)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
@ -1342,11 +1313,8 @@ func (client *Client) destroy(session *Session) {
|
||||||
// clean up channels
|
// clean up channels
|
||||||
// (note that if this is a reattach, client has no channels and therefore no friends)
|
// (note that if this is a reattach, client has no channels and therefore no friends)
|
||||||
friends := make(ClientSet)
|
friends := make(ClientSet)
|
||||||
channels := client.Channels()
|
channels = client.Channels()
|
||||||
for _, channel := range channels {
|
for _, channel := range channels {
|
||||||
if channel.memberIsVisible(client) {
|
|
||||||
quitHistoryChannels = append(quitHistoryChannels, channel)
|
|
||||||
}
|
|
||||||
for _, member := range channel.auditoriumFriends(client) {
|
for _, member := range channel.auditoriumFriends(client) {
|
||||||
friends.Add(member)
|
friends.Add(member)
|
||||||
}
|
}
|
||||||
|
|
@ -1367,7 +1335,7 @@ func (client *Client) destroy(session *Session) {
|
||||||
quitItem = history.Item{
|
quitItem = history.Item{
|
||||||
Type: history.Quit,
|
Type: history.Quit,
|
||||||
Nick: details.nickMask,
|
Nick: details.nickMask,
|
||||||
Account: details.accountName,
|
AccountName: details.accountName,
|
||||||
Message: splitQuitMessage,
|
Message: splitQuitMessage,
|
||||||
IsBot: isBot,
|
IsBot: isBot,
|
||||||
}
|
}
|
||||||
|
|
@ -1449,10 +1417,6 @@ func composeMultilineBatch(batchID, fromNickMask, fromAccount string, isBot bool
|
||||||
for _, msg := range message.Split {
|
for _, msg := range message.Split {
|
||||||
message := ircmsg.MakeMessage(nil, fromNickMask, command, target, msg.Message)
|
message := ircmsg.MakeMessage(nil, fromNickMask, command, target, msg.Message)
|
||||||
message.SetTag("batch", batchID)
|
message.SetTag("batch", batchID)
|
||||||
for k, v := range msg.Tags {
|
|
||||||
message.SetTag(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Concat {
|
if msg.Concat {
|
||||||
message.SetTag(caps.MultilineConcatTag, "")
|
message.SetTag(caps.MultilineConcatTag, "")
|
||||||
}
|
}
|
||||||
|
|
@ -1464,27 +1428,27 @@ func composeMultilineBatch(batchID, fromNickMask, fromAccount string, isBot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// in practice, many clients require that the final parameter be a trailing
|
// these are all the output commands that MUST have their last param be a trailing.
|
||||||
// (prefixed with `:`) even when this is not syntactically necessary.
|
// this is needed because dumb clients like to treat trailing params separately from the
|
||||||
// by default, force the following commands to use a trailing:
|
// other params in messages.
|
||||||
commandsThatMustUseTrailing = utils.SetLiteral(
|
commandsThatMustUseTrailing = map[string]bool{
|
||||||
"PRIVMSG",
|
"PRIVMSG": true,
|
||||||
"NOTICE",
|
"NOTICE": true,
|
||||||
RPL_WHOISCHANNELS,
|
|
||||||
RPL_USERHOST,
|
RPL_WHOISCHANNELS: true,
|
||||||
|
RPL_USERHOST: true,
|
||||||
|
|
||||||
// mirc's handling of RPL_NAMREPLY is broken:
|
// mirc's handling of RPL_NAMREPLY is broken:
|
||||||
// https://forums.mirc.com/ubbthreads.php/topics/266939/re-nick-list
|
// https://forums.mirc.com/ubbthreads.php/topics/266939/re-nick-list
|
||||||
RPL_NAMREPLY,
|
RPL_NAMREPLY: true,
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
func forceTrailing(config *Config, command string) bool {
|
|
||||||
return config.Server.Compatibility.forceTrailing && commandsThatMustUseTrailing.Has(command)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// SendRawMessage sends a raw message to the client.
|
// SendRawMessage sends a raw message to the client.
|
||||||
func (session *Session) SendRawMessage(message ircmsg.Message, blocking bool) error {
|
func (session *Session) SendRawMessage(message ircmsg.Message, blocking bool) error {
|
||||||
if forceTrailing(session.client.server.Config(), message.Command) {
|
// use dumb hack to force the last param to be a trailing param if required
|
||||||
|
config := session.client.server.Config()
|
||||||
|
if config.Server.Compatibility.forceTrailing && commandsThatMustUseTrailing[message.Command] {
|
||||||
message.ForceTrailing()
|
message.ForceTrailing()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1690,7 +1654,7 @@ func (client *Client) addHistoryItem(target *Client, item history.Item, details,
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Nick = details.nickMask
|
item.Nick = details.nickMask
|
||||||
item.Account = details.account
|
item.AccountName = details.accountName
|
||||||
targetedItem := item
|
targetedItem := item
|
||||||
targetedItem.Params[0] = tDetails.nick
|
targetedItem.Params[0] = tDetails.nick
|
||||||
|
|
||||||
|
|
@ -1698,15 +1662,15 @@ func (client *Client) addHistoryItem(target *Client, item history.Item, details,
|
||||||
tStatus, _ := target.historyStatus(config)
|
tStatus, _ := target.historyStatus(config)
|
||||||
// add to ephemeral history
|
// add to ephemeral history
|
||||||
if cStatus == HistoryEphemeral {
|
if cStatus == HistoryEphemeral {
|
||||||
targetedItem.Target = tDetails.account
|
targetedItem.CfCorrespondent = tDetails.nickCasefolded
|
||||||
client.history.Add(targetedItem)
|
client.history.Add(targetedItem)
|
||||||
}
|
}
|
||||||
if tStatus == HistoryEphemeral && client != target {
|
if tStatus == HistoryEphemeral && client != target {
|
||||||
item.Target = target.Account()
|
item.CfCorrespondent = details.nickCasefolded
|
||||||
target.history.Add(item)
|
target.history.Add(item)
|
||||||
}
|
}
|
||||||
if cStatus == HistoryPersistent || tStatus == HistoryPersistent {
|
if cStatus == HistoryPersistent || tStatus == HistoryPersistent {
|
||||||
targetedItem.Target = target.account
|
targetedItem.CfCorrespondent = ""
|
||||||
client.server.historyDB.AddDirectMessage(details.nickCasefolded, details.account, tDetails.nickCasefolded, tDetails.account, targetedItem)
|
client.server.historyDB.AddDirectMessage(details.nickCasefolded, details.account, tDetails.nickCasefolded, tDetails.account, targetedItem)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -1782,7 +1746,7 @@ func (client *Client) handleRegisterTimeout() {
|
||||||
func (client *Client) copyLastSeen() (result map[string]time.Time) {
|
func (client *Client) copyLastSeen() (result map[string]time.Time) {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
return maps.Clone(client.lastSeen)
|
return utils.CopyMap(client.lastSeen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// these are bit flags indicating what part of the client status is "dirty"
|
// these are bit flags indicating what part of the client status is "dirty"
|
||||||
|
|
@ -1811,8 +1775,6 @@ func (client *Client) wakeWriter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) writeLoop() {
|
func (client *Client) writeLoop() {
|
||||||
defer client.server.HandlePanic()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
client.performWrite(0)
|
client.performWrite(0)
|
||||||
client.writebackLock.Unlock()
|
client.writebackLock.Unlock()
|
||||||
|
|
@ -1843,11 +1805,7 @@ func (client *Client) performWrite(additionalDirtyBits uint) {
|
||||||
channels := client.Channels()
|
channels := client.Channels()
|
||||||
channelToModes := make(map[string]alwaysOnChannelStatus, len(channels))
|
channelToModes := make(map[string]alwaysOnChannelStatus, len(channels))
|
||||||
for _, channel := range channels {
|
for _, channel := range channels {
|
||||||
ok, chname, status := channel.alwaysOnStatus(client)
|
chname, status := channel.alwaysOnStatus(client)
|
||||||
if !ok {
|
|
||||||
client.server.logger.Error("internal", "client and channel membership out of sync", chname, client.Nick())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
channelToModes[chname] = status
|
channelToModes[chname] = status
|
||||||
}
|
}
|
||||||
client.server.accounts.saveChannels(account, channelToModes)
|
client.server.accounts.saveChannels(account, channelToModes)
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ func (clients *ClientManager) Remove(client *Client) error {
|
||||||
// SetNick sets a client's nickname, validating it against nicknames in use
|
// SetNick sets a client's nickname, validating it against nicknames in use
|
||||||
// XXX: dryRun validates a client's ability to claim a nick, without
|
// XXX: dryRun validates a client's ability to claim a nick, without
|
||||||
// actually claiming it
|
// actually claiming it
|
||||||
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, awayChanged bool) {
|
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, returnedFromAway bool) {
|
||||||
config := client.server.Config()
|
config := client.server.Config()
|
||||||
|
|
||||||
var newCfNick, newSkeleton string
|
var newCfNick, newSkeleton string
|
||||||
|
|
@ -116,8 +116,6 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||||
useAccountName = alwaysOn || config.Accounts.NickReservation.ForceNickEqualsAccount
|
useAccountName = alwaysOn || config.Accounts.NickReservation.ForceNickEqualsAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
nickIsReserved := false
|
|
||||||
|
|
||||||
if useAccountName {
|
if useAccountName {
|
||||||
if registered && newNick != accountName {
|
if registered && newNick != accountName {
|
||||||
return "", errNickAccountMismatch, false
|
return "", errNickAccountMismatch, false
|
||||||
|
|
@ -169,9 +167,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||||
|
|
||||||
reservedAccount, method := client.server.accounts.EnforcementStatus(newCfNick, newSkeleton)
|
reservedAccount, method := client.server.accounts.EnforcementStatus(newCfNick, newSkeleton)
|
||||||
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
|
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
|
||||||
// see #2135: we want to enter the critical section, see if the nick is actually in use,
|
return "", errNicknameReserved, false
|
||||||
// and return errNicknameInUse in that case
|
|
||||||
nickIsReserved = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,7 +195,16 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||||
dryRun || session == nil {
|
dryRun || session == nil {
|
||||||
return "", errNicknameInUse, false
|
return "", errNicknameInUse, false
|
||||||
}
|
}
|
||||||
reattachSuccessful, numSessions, lastSeen, wasAway, nowAway := currentClient.AddSession(session)
|
// check TLS modes
|
||||||
|
if client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
|
||||||
|
if useAccountName {
|
||||||
|
// #955: this is fatal because they can't fix it by trying a different nick
|
||||||
|
return "", errInsecureReattach, false
|
||||||
|
} else {
|
||||||
|
return "", errNicknameInUse, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reattachSuccessful, numSessions, lastSeen, back := currentClient.AddSession(session)
|
||||||
if !reattachSuccessful {
|
if !reattachSuccessful {
|
||||||
return "", errNicknameInUse, false
|
return "", errNicknameInUse, false
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +219,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||||
currentClient.SetRealname(realname)
|
currentClient.SetRealname(realname)
|
||||||
}
|
}
|
||||||
// successful reattach!
|
// successful reattach!
|
||||||
return newNick, nil, wasAway != nowAway
|
return newNick, nil, back
|
||||||
} else if currentClient == client && currentClient.Nick() == newNick {
|
} else if currentClient == client && currentClient.Nick() == newNick {
|
||||||
return "", errNoop, false
|
return "", errNoop, false
|
||||||
}
|
}
|
||||||
|
|
@ -223,9 +228,6 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||||
if skeletonHolder != nil && skeletonHolder != client {
|
if skeletonHolder != nil && skeletonHolder != client {
|
||||||
return "", errNicknameInUse, false
|
return "", errNicknameInUse, false
|
||||||
}
|
}
|
||||||
if nickIsReserved {
|
|
||||||
return "", errNicknameReserved, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
return "", nil, false
|
return "", nil, false
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@
|
||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/languages"
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,47 +30,6 @@ func BenchmarkGenerateBatchID(b *testing.B) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkNames(b *testing.B) {
|
|
||||||
channelSize := 1024
|
|
||||||
server := &Server{
|
|
||||||
name: "ergo.test",
|
|
||||||
}
|
|
||||||
lm, err := languages.NewManager(false, "", "")
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
server.config.Store(&Config{
|
|
||||||
languageManager: lm,
|
|
||||||
})
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
channel := &Channel{
|
|
||||||
name: "#test",
|
|
||||||
nameCasefolded: "#test",
|
|
||||||
server: server,
|
|
||||||
members: make(MemberSet),
|
|
||||||
}
|
|
||||||
for j := 0; j < channelSize; j++ {
|
|
||||||
nick := fmt.Sprintf("client_%d", j)
|
|
||||||
client := &Client{
|
|
||||||
server: server,
|
|
||||||
nick: nick,
|
|
||||||
nickCasefolded: nick,
|
|
||||||
}
|
|
||||||
channel.members.Add(client)
|
|
||||||
channel.regenerateMembersCache()
|
|
||||||
session := &Session{
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
rb := NewResponseBuffer(session)
|
|
||||||
channel.Names(client, rb)
|
|
||||||
if len(rb.messages) < 2 {
|
|
||||||
b.Fatalf("not enough messages: %d", len(rb.messages))
|
|
||||||
}
|
|
||||||
// to inspect the messages: line, _ := rb.messages[0].Line()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserMasks(t *testing.T) {
|
func TestUserMasks(t *testing.T) {
|
||||||
var um UserMaskSet
|
var um UserMaskSet
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ func init() {
|
||||||
},
|
},
|
||||||
"AWAY": {
|
"AWAY": {
|
||||||
handler: awayHandler,
|
handler: awayHandler,
|
||||||
usablePreReg: true,
|
|
||||||
minParams: 0,
|
minParams: 0,
|
||||||
},
|
},
|
||||||
"BATCH": {
|
"BATCH": {
|
||||||
|
|
@ -152,10 +151,6 @@ func init() {
|
||||||
handler: isonHandler,
|
handler: isonHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
"ISUPPORT": {
|
|
||||||
handler: isupportHandler,
|
|
||||||
usablePreReg: true,
|
|
||||||
},
|
|
||||||
"JOIN": {
|
"JOIN": {
|
||||||
handler: joinHandler,
|
handler: joinHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
|
|
@ -305,10 +300,6 @@ func init() {
|
||||||
usablePreReg: true,
|
usablePreReg: true,
|
||||||
minParams: 0,
|
minParams: 0,
|
||||||
},
|
},
|
||||||
"REDACT": {
|
|
||||||
handler: redactHandler,
|
|
||||||
minParams: 2,
|
|
||||||
},
|
|
||||||
"REHASH": {
|
"REHASH": {
|
||||||
handler: rehashHandler,
|
handler: rehashHandler,
|
||||||
minParams: 0,
|
minParams: 0,
|
||||||
|
|
@ -383,11 +374,6 @@ func init() {
|
||||||
handler: zncHandler,
|
handler: zncHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
// CEF custom commands
|
|
||||||
"REACT": {
|
|
||||||
handler: reactHandler,
|
|
||||||
minParams: 2,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeServices()
|
initializeServices()
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ import (
|
||||||
"github.com/ergochat/ergo/irc/logger"
|
"github.com/ergochat/ergo/irc/logger"
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/mysql"
|
"github.com/ergochat/ergo/irc/mysql"
|
||||||
"github.com/ergochat/ergo/irc/oauth2"
|
|
||||||
"github.com/ergochat/ergo/irc/passwd"
|
"github.com/ergochat/ergo/irc/passwd"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -304,7 +303,7 @@ func (t *ThrottleConfig) UnmarshalYAML(unmarshal func(interface{}) error) (err e
|
||||||
type AccountConfig struct {
|
type AccountConfig struct {
|
||||||
Registration AccountRegistrationConfig
|
Registration AccountRegistrationConfig
|
||||||
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
||||||
AdvertiseSCRAM bool `yaml:"advertise-scram"`
|
AdvertiseSCRAM bool `yaml:"advertise-scram"` // undocumented, see #1782
|
||||||
RequireSasl struct {
|
RequireSasl struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Exempted []string
|
Exempted []string
|
||||||
|
|
@ -333,8 +332,6 @@ type AccountConfig struct {
|
||||||
Bouncer *MulticlientConfig // # handle old name for 'multiclient'
|
Bouncer *MulticlientConfig // # handle old name for 'multiclient'
|
||||||
VHosts VHostConfig
|
VHosts VHostConfig
|
||||||
AuthScript AuthScriptConfig `yaml:"auth-script"`
|
AuthScript AuthScriptConfig `yaml:"auth-script"`
|
||||||
OAuth2 oauth2.OAuth2BearerConfig `yaml:"oauth2"`
|
|
||||||
JWTAuth jwt.JWTAuthConfig `yaml:"jwt-auth"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScriptConfig struct {
|
type ScriptConfig struct {
|
||||||
|
|
@ -453,10 +450,6 @@ func (cm *Casemapping) UnmarshalYAML(unmarshal func(interface{}) error) (err err
|
||||||
result = CasemappingPRECIS
|
result = CasemappingPRECIS
|
||||||
case "permissive", "fun":
|
case "permissive", "fun":
|
||||||
result = CasemappingPermissive
|
result = CasemappingPermissive
|
||||||
case "rfc1459":
|
|
||||||
result = CasemappingRFC1459
|
|
||||||
case "rfc1459-strict":
|
|
||||||
result = CasemappingRFC1459Strict
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid casemapping value: %s", orig)
|
return fmt.Errorf("invalid casemapping value: %s", orig)
|
||||||
}
|
}
|
||||||
|
|
@ -491,7 +484,6 @@ type Limits struct {
|
||||||
ChanListModes int `yaml:"chan-list-modes"`
|
ChanListModes int `yaml:"chan-list-modes"`
|
||||||
ChannelLen int `yaml:"channellen"`
|
ChannelLen int `yaml:"channellen"`
|
||||||
IdentLen int `yaml:"identlen"`
|
IdentLen int `yaml:"identlen"`
|
||||||
RealnameLen int `yaml:"realnamelen"`
|
|
||||||
KickLen int `yaml:"kicklen"`
|
KickLen int `yaml:"kicklen"`
|
||||||
MonitorEntries int `yaml:"monitor-entries"`
|
MonitorEntries int `yaml:"monitor-entries"`
|
||||||
NickLen int `yaml:"nicklen"`
|
NickLen int `yaml:"nicklen"`
|
||||||
|
|
@ -652,7 +644,6 @@ type Config struct {
|
||||||
}
|
}
|
||||||
ListDelay time.Duration `yaml:"list-delay"`
|
ListDelay time.Duration `yaml:"list-delay"`
|
||||||
InviteExpiration custime.Duration `yaml:"invite-expiration"`
|
InviteExpiration custime.Duration `yaml:"invite-expiration"`
|
||||||
AutoJoin []string `yaml:"auto-join"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OperClasses map[string]*OperClassConfig `yaml:"oper-classes"`
|
OperClasses map[string]*OperClassConfig `yaml:"oper-classes"`
|
||||||
|
|
@ -709,14 +700,6 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
Filename string
|
Filename string
|
||||||
|
|
||||||
Cef struct {
|
|
||||||
Imagor struct {
|
|
||||||
Url string
|
|
||||||
Secret string
|
|
||||||
}
|
|
||||||
Redis string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OperClass defines an assembled operator class.
|
// OperClass defines an assembled operator class.
|
||||||
|
|
@ -1055,7 +1038,7 @@ func (ce *configPathError) Error() string {
|
||||||
return fmt.Sprintf("Couldn't apply config override `%s`: %s", ce.name, ce.desc)
|
return fmt.Sprintf("Couldn't apply config override `%s`: %s", ce.name, ce.desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mungeFromEnvironment(config *Config, envPair string) (applied bool, name string, err *configPathError) {
|
func mungeFromEnvironment(config *Config, envPair string) (applied bool, err *configPathError) {
|
||||||
equalIdx := strings.IndexByte(envPair, '=')
|
equalIdx := strings.IndexByte(envPair, '=')
|
||||||
name, value := envPair[:equalIdx], envPair[equalIdx+1:]
|
name, value := envPair[:equalIdx], envPair[equalIdx+1:]
|
||||||
if strings.HasPrefix(name, "ERGO__") {
|
if strings.HasPrefix(name, "ERGO__") {
|
||||||
|
|
@ -1063,7 +1046,7 @@ func mungeFromEnvironment(config *Config, envPair string) (applied bool, name st
|
||||||
} else if strings.HasPrefix(name, "ORAGONO__") {
|
} else if strings.HasPrefix(name, "ORAGONO__") {
|
||||||
name = strings.TrimPrefix(name, "ORAGONO__")
|
name = strings.TrimPrefix(name, "ORAGONO__")
|
||||||
} else {
|
} else {
|
||||||
return false, "", nil
|
return false, nil
|
||||||
}
|
}
|
||||||
pathComponents := strings.Split(name, "__")
|
pathComponents := strings.Split(name, "__")
|
||||||
for i, pathComponent := range pathComponents {
|
for i, pathComponent := range pathComponents {
|
||||||
|
|
@ -1074,10 +1057,10 @@ func mungeFromEnvironment(config *Config, envPair string) (applied bool, name st
|
||||||
t := v.Type()
|
t := v.Type()
|
||||||
for _, component := range pathComponents {
|
for _, component := range pathComponents {
|
||||||
if component == "" {
|
if component == "" {
|
||||||
return false, "", &configPathError{name, "invalid", nil}
|
return false, &configPathError{name, "invalid", nil}
|
||||||
}
|
}
|
||||||
if v.Kind() != reflect.Struct {
|
if v.Kind() != reflect.Struct {
|
||||||
return false, "", &configPathError{name, "index into non-struct", nil}
|
return false, &configPathError{name, "index into non-struct", nil}
|
||||||
}
|
}
|
||||||
var nextField reflect.StructField
|
var nextField reflect.StructField
|
||||||
success := false
|
success := false
|
||||||
|
|
@ -1103,7 +1086,7 @@ func mungeFromEnvironment(config *Config, envPair string) (applied bool, name st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !success {
|
if !success {
|
||||||
return false, "", &configPathError{name, fmt.Sprintf("couldn't resolve path component: `%s`", component), nil}
|
return false, &configPathError{name, fmt.Sprintf("couldn't resolve path component: `%s`", component), nil}
|
||||||
}
|
}
|
||||||
v = v.FieldByName(nextField.Name)
|
v = v.FieldByName(nextField.Name)
|
||||||
// dereference pointer field if necessary, initialize new value if necessary
|
// dereference pointer field if necessary, initialize new value if necessary
|
||||||
|
|
@ -1117,9 +1100,9 @@ func mungeFromEnvironment(config *Config, envPair string) (applied bool, name st
|
||||||
}
|
}
|
||||||
yamlErr := yaml.Unmarshal([]byte(value), v.Addr().Interface())
|
yamlErr := yaml.Unmarshal([]byte(value), v.Addr().Interface())
|
||||||
if yamlErr != nil {
|
if yamlErr != nil {
|
||||||
return false, "", &configPathError{name, "couldn't deserialize YAML", yamlErr}
|
return false, &configPathError{name, "couldn't deserialize YAML", yamlErr}
|
||||||
}
|
}
|
||||||
return true, name, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig loads the given YAML configuration file.
|
// LoadConfig loads the given YAML configuration file.
|
||||||
|
|
@ -1131,7 +1114,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||||
|
|
||||||
if config.AllowEnvironmentOverrides {
|
if config.AllowEnvironmentOverrides {
|
||||||
for _, envPair := range os.Environ() {
|
for _, envPair := range os.Environ() {
|
||||||
applied, name, envErr := mungeFromEnvironment(config, envPair)
|
applied, envErr := mungeFromEnvironment(config, envPair)
|
||||||
if envErr != nil {
|
if envErr != nil {
|
||||||
if envErr.fatalErr != nil {
|
if envErr.fatalErr != nil {
|
||||||
return nil, envErr
|
return nil, envErr
|
||||||
|
|
@ -1139,7 +1122,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||||
log.Println(envErr.Error())
|
log.Println(envErr.Error())
|
||||||
}
|
}
|
||||||
} else if applied {
|
} else if applied {
|
||||||
log.Printf("applied environment override: %s\n", name)
|
log.Printf("applied environment override: %s\n", envPair)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1406,34 +1389,16 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||||
config.Accounts.VHosts.validRegexp = defaultValidVhostRegex
|
config.Accounts.VHosts.validRegexp = defaultValidVhostRegex
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Accounts.AuthenticationEnabled {
|
saslCapValue := "PLAIN,EXTERNAL,SCRAM-SHA-256"
|
||||||
saslCapValues := []string{"PLAIN", "EXTERNAL"}
|
// TODO(#1782) clean this up:
|
||||||
if config.Accounts.AdvertiseSCRAM {
|
if !config.Accounts.AdvertiseSCRAM {
|
||||||
saslCapValues = append(saslCapValues, "SCRAM-SHA-256")
|
saslCapValue = "PLAIN,EXTERNAL"
|
||||||
}
|
}
|
||||||
if config.Accounts.OAuth2.Enabled {
|
config.Server.capValues[caps.SASL] = saslCapValue
|
||||||
saslCapValues = append(saslCapValues, "OAUTHBEARER")
|
if !config.Accounts.AuthenticationEnabled {
|
||||||
}
|
|
||||||
if config.Accounts.OAuth2.Enabled || config.Accounts.JWTAuth.Enabled {
|
|
||||||
saslCapValues = append(saslCapValues, "IRCV3BEARER")
|
|
||||||
}
|
|
||||||
config.Server.capValues[caps.SASL] = strings.Join(saslCapValues, ",")
|
|
||||||
} else {
|
|
||||||
config.Server.supportedCaps.Disable(caps.SASL)
|
config.Server.supportedCaps.Disable(caps.SASL)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := config.Accounts.OAuth2.Postprocess(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := config.Accounts.JWTAuth.Postprocess(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Accounts.OAuth2.Enabled && config.Accounts.OAuth2.AuthScript && !config.Accounts.AuthScript.Enabled {
|
|
||||||
return nil, fmt.Errorf("oauth2 is enabled with auth-script, but no auth-script is enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.Accounts.Registration.Enabled {
|
if !config.Accounts.Registration.Enabled {
|
||||||
config.Server.supportedCaps.Disable(caps.AccountRegistration)
|
config.Server.supportedCaps.Disable(caps.AccountRegistration)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1603,10 +1568,6 @@ func (config *Config) isRelaymsgIdentifier(nick string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(nick, "#") {
|
|
||||||
return false // #2114
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, char := range config.Server.Relaymsg.Separators {
|
for _, char := range config.Server.Relaymsg.Separators {
|
||||||
if strings.ContainsRune(nick, char) {
|
if strings.ContainsRune(nick, char) {
|
||||||
return true
|
return true
|
||||||
|
|
@ -1624,16 +1585,7 @@ func (config *Config) generateISupport() (err error) {
|
||||||
isupport.Initialize()
|
isupport.Initialize()
|
||||||
isupport.Add("AWAYLEN", strconv.Itoa(config.Limits.AwayLen))
|
isupport.Add("AWAYLEN", strconv.Itoa(config.Limits.AwayLen))
|
||||||
isupport.Add("BOT", "B")
|
isupport.Add("BOT", "B")
|
||||||
var casemappingToken string
|
isupport.Add("CASEMAPPING", "ascii")
|
||||||
switch config.Server.Casemapping {
|
|
||||||
default:
|
|
||||||
casemappingToken = "ascii" // this is published for ascii, precis, or permissive
|
|
||||||
case CasemappingRFC1459:
|
|
||||||
casemappingToken = "rfc1459"
|
|
||||||
case CasemappingRFC1459Strict:
|
|
||||||
casemappingToken = "rfc1459-strict"
|
|
||||||
}
|
|
||||||
isupport.Add("CASEMAPPING", casemappingToken)
|
|
||||||
isupport.Add("CHANLIMIT", fmt.Sprintf("%s:%d", chanTypes, config.Channels.MaxChannelsPerClient))
|
isupport.Add("CHANLIMIT", fmt.Sprintf("%s:%d", chanTypes, config.Channels.MaxChannelsPerClient))
|
||||||
isupport.Add("CHANMODES", chanmodesToken)
|
isupport.Add("CHANMODES", chanmodesToken)
|
||||||
if config.History.Enabled && config.History.ChathistoryMax > 0 {
|
if config.History.Enabled && config.History.ChathistoryMax > 0 {
|
||||||
|
|
@ -1654,7 +1606,6 @@ func (config *Config) generateISupport() (err error) {
|
||||||
isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen))
|
isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen))
|
||||||
isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes)))
|
isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes)))
|
||||||
isupport.Add("MAXTARGETS", maxTargetsString)
|
isupport.Add("MAXTARGETS", maxTargetsString)
|
||||||
isupport.Add("MSGREFTYPES", "msgid,timestamp")
|
|
||||||
isupport.Add("MODES", "")
|
isupport.Add("MODES", "")
|
||||||
isupport.Add("MONITOR", strconv.Itoa(config.Limits.MonitorEntries))
|
isupport.Add("MONITOR", strconv.Itoa(config.Limits.MonitorEntries))
|
||||||
isupport.Add("NETWORK", config.Network.Name)
|
isupport.Add("NETWORK", config.Network.Name)
|
||||||
|
|
@ -1664,7 +1615,6 @@ func (config *Config) generateISupport() (err error) {
|
||||||
isupport.Add("RPCHAN", "E")
|
isupport.Add("RPCHAN", "E")
|
||||||
isupport.Add("RPUSER", "E")
|
isupport.Add("RPUSER", "E")
|
||||||
}
|
}
|
||||||
isupport.Add("SAFELIST", "")
|
|
||||||
isupport.Add("STATUSMSG", "~&@%+")
|
isupport.Add("STATUSMSG", "~&@%+")
|
||||||
isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d", maxTargetsString, maxTargetsString, maxTargetsString, config.Limits.MonitorEntries))
|
isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d", maxTargetsString, maxTargetsString, maxTargetsString, config.Limits.MonitorEntries))
|
||||||
isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen))
|
isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen))
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ func TestEnvironmentOverrides(t *testing.T) {
|
||||||
`ORAGONO__SERVER__IP_CLOAKING={"enabled": true, "enabled-for-always-on": true, "netname": "irc", "cidr-len-ipv4": 32, "cidr-len-ipv6": 64, "num-bits": 64}`,
|
`ORAGONO__SERVER__IP_CLOAKING={"enabled": true, "enabled-for-always-on": true, "netname": "irc", "cidr-len-ipv4": 32, "cidr-len-ipv6": 64, "num-bits": 64}`,
|
||||||
}
|
}
|
||||||
for _, envPair := range env {
|
for _, envPair := range env {
|
||||||
_, _, err := mungeFromEnvironment(&config, envPair)
|
_, err := mungeFromEnvironment(&config, envPair)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("couldn't apply override `%s`: %v", envPair, err)
|
t.Errorf("couldn't apply override `%s`: %v", envPair, err)
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +93,7 @@ func TestEnvironmentOverrideErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, env := range invalidEnvs {
|
for _, env := range invalidEnvs {
|
||||||
success, _, err := mungeFromEnvironment(&config, env)
|
success, err := mungeFromEnvironment(&config, env)
|
||||||
if err == nil || success {
|
if err == nil || success {
|
||||||
t.Errorf("accepted invalid env override `%s`", env)
|
t.Errorf("accepted invalid env override `%s`", env)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ package irc
|
||||||
const (
|
const (
|
||||||
// maxLastArgLength is used to simply cap off the final argument when creating general messages where we need to select a limit.
|
// maxLastArgLength is used to simply cap off the final argument when creating general messages where we need to select a limit.
|
||||||
// for instance, in MONITOR lists, RPL_ISUPPORT lists, etc.
|
// for instance, in MONITOR lists, RPL_ISUPPORT lists, etc.
|
||||||
maxLastArgLength = 1024
|
maxLastArgLength = 400
|
||||||
// maxTargets is the maximum number of targets for PRIVMSG and NOTICE.
|
// maxTargets is the maximum number of targets for PRIVMSG and NOTICE.
|
||||||
maxTargets = 4
|
maxTargets = 4
|
||||||
)
|
)
|
||||||
|
|
|
||||||
155
irc/database.go
155
irc/database.go
|
|
@ -14,8 +14,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/bunt"
|
|
||||||
"github.com/ergochat/ergo/irc/datastore"
|
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
|
|
||||||
|
|
@ -23,19 +21,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TODO migrate metadata keys as well
|
|
||||||
|
|
||||||
// 'version' of the database schema
|
// 'version' of the database schema
|
||||||
|
keySchemaVersion = "db.version"
|
||||||
// latest schema of the db
|
// latest schema of the db
|
||||||
latestDbSchema = 23
|
latestDbSchema = 22
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
keyCloakSecret = "crypto.cloak_secret"
|
||||||
schemaVersionUUID = utils.UUID{0, 255, 85, 13, 212, 10, 191, 121, 245, 152, 142, 89, 97, 141, 219, 87} // AP9VDdQKv3n1mI5ZYY3bVw
|
|
||||||
cloakSecretUUID = utils.UUID{170, 214, 184, 208, 116, 181, 67, 75, 161, 23, 233, 16, 113, 251, 94, 229} // qta40HS1Q0uhF-kQcfte5Q
|
|
||||||
|
|
||||||
keySchemaVersion = bunt.BuntKey(datastore.TableMetadata, schemaVersionUUID)
|
|
||||||
keyCloakSecret = bunt.BuntKey(datastore.TableMetadata, cloakSecretUUID)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SchemaChanger func(*Config, *buntdb.Tx) error
|
type SchemaChanger func(*Config, *buntdb.Tx) error
|
||||||
|
|
@ -108,7 +99,10 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
|
||||||
// read the current version string
|
// read the current version string
|
||||||
var version int
|
var version int
|
||||||
err = db.View(func(tx *buntdb.Tx) (err error) {
|
err = db.View(func(tx *buntdb.Tx) (err error) {
|
||||||
version, err = retrieveSchemaVersion(tx)
|
vStr, err := tx.Get(keySchemaVersion)
|
||||||
|
if err == nil {
|
||||||
|
version, err = strconv.Atoi(vStr)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -136,21 +130,10 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveSchemaVersion(tx *buntdb.Tx) (version int, err error) {
|
|
||||||
if val, err := tx.Get(keySchemaVersion); err == nil {
|
|
||||||
return strconv.Atoi(val)
|
|
||||||
}
|
|
||||||
// legacy key:
|
|
||||||
if val, err := tx.Get("db.version"); err == nil {
|
|
||||||
return strconv.Atoi(val)
|
|
||||||
}
|
|
||||||
return 0, buntdb.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func performAutoUpgrade(currentVersion int, config *Config) (err error) {
|
func performAutoUpgrade(currentVersion int, config *Config) (err error) {
|
||||||
path := config.Datastore.Path
|
path := config.Datastore.Path
|
||||||
log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
|
log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
|
||||||
timestamp := time.Now().UTC().Format("2006-01-02-15.04.05.000Z")
|
timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
|
||||||
backupPath := fmt.Sprintf("%s.v%d.%s.bak", path, currentVersion, timestamp)
|
backupPath := fmt.Sprintf("%s.v%d.%s.bak", path, currentVersion, timestamp)
|
||||||
log.Printf("making a backup of current database at %s\n", backupPath)
|
log.Printf("making a backup of current database at %s\n", backupPath)
|
||||||
err = utils.CopyFile(path, backupPath)
|
err = utils.CopyFile(path, backupPath)
|
||||||
|
|
@ -184,12 +167,8 @@ func UpgradeDB(config *Config) (err error) {
|
||||||
var version int
|
var version int
|
||||||
err = store.Update(func(tx *buntdb.Tx) error {
|
err = store.Update(func(tx *buntdb.Tx) error {
|
||||||
for {
|
for {
|
||||||
if version == 0 {
|
vStr, _ := tx.Get(keySchemaVersion)
|
||||||
version, err = retrieveSchemaVersion(tx)
|
version, _ = strconv.Atoi(vStr)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if version == latestDbSchema {
|
if version == latestDbSchema {
|
||||||
// success!
|
// success!
|
||||||
break
|
break
|
||||||
|
|
@ -204,12 +183,11 @@ func UpgradeDB(config *Config) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
version = change.TargetVersion
|
_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(change.TargetVersion), nil)
|
||||||
_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(version), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("successfully updated schema to version %d\n", version)
|
log.Printf("successfully updated schema to version %d\n", change.TargetVersion)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
@ -220,17 +198,19 @@ func UpgradeDB(config *Config) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadCloakSecret(dstore datastore.Datastore) (result string, err error) {
|
func LoadCloakSecret(db *buntdb.DB) (result string) {
|
||||||
val, err := dstore.Get(datastore.TableMetadata, cloakSecretUUID)
|
db.View(func(tx *buntdb.Tx) error {
|
||||||
if err != nil {
|
result, _ = tx.Get(keyCloakSecret)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return string(val), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func StoreCloakSecret(dstore datastore.Datastore, secret string) {
|
func StoreCloakSecret(db *buntdb.DB, secret string) {
|
||||||
// TODO error checking
|
db.Update(func(tx *buntdb.Tx) error {
|
||||||
dstore.Set(datastore.TableMetadata, cloakSecretUUID, []byte(secret), time.Time{})
|
tx.Set(keyCloakSecret, secret, nil)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|
||||||
|
|
@ -1132,92 +1112,6 @@ func schemaChangeV21To22(config *Config, tx *buntdb.Tx) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// first phase of document-oriented database refactor: channels
|
|
||||||
func schemaChangeV22ToV23(config *Config, tx *buntdb.Tx) error {
|
|
||||||
keyChannelExists := "channel.exists "
|
|
||||||
var channelNames []string
|
|
||||||
tx.AscendGreaterOrEqual("", keyChannelExists, func(key, value string) bool {
|
|
||||||
if !strings.HasPrefix(key, keyChannelExists) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
channelNames = append(channelNames, strings.TrimPrefix(key, keyChannelExists))
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
for _, channelName := range channelNames {
|
|
||||||
channel, err := loadLegacyChannel(tx, channelName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error loading legacy channel %s: %v", channelName, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
channel.UUID = utils.GenerateUUIDv4()
|
|
||||||
newKey := bunt.BuntKey(datastore.TableChannels, channel.UUID)
|
|
||||||
j, err := json.Marshal(channel)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error marshaling channel %s: %v", channelName, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tx.Set(newKey, string(j), nil)
|
|
||||||
deleteLegacyChannel(tx, channelName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// purges
|
|
||||||
keyChannelPurged := "channel.purged "
|
|
||||||
var purgeKeys []string
|
|
||||||
var channelPurges []ChannelPurgeRecord
|
|
||||||
tx.AscendGreaterOrEqual("", keyChannelPurged, func(key, value string) bool {
|
|
||||||
if !strings.HasPrefix(key, keyChannelPurged) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
purgeKeys = append(purgeKeys, key)
|
|
||||||
cfname := strings.TrimPrefix(key, keyChannelPurged)
|
|
||||||
var record ChannelPurgeRecord
|
|
||||||
err := json.Unmarshal([]byte(value), &record)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error unmarshaling channel purge for %s: %v", cfname, err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
record.NameCasefolded = cfname
|
|
||||||
record.UUID = utils.GenerateUUIDv4()
|
|
||||||
channelPurges = append(channelPurges, record)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
for _, record := range channelPurges {
|
|
||||||
newKey := bunt.BuntKey(datastore.TableChannelPurges, record.UUID)
|
|
||||||
j, err := json.Marshal(record)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error marshaling channel purge %s: %v", record.NameCasefolded, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tx.Set(newKey, string(j), nil)
|
|
||||||
}
|
|
||||||
for _, purgeKey := range purgeKeys {
|
|
||||||
tx.Delete(purgeKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up denormalized account-to-channels mapping
|
|
||||||
keyAccountChannels := "account.channels "
|
|
||||||
var accountToChannels []string
|
|
||||||
tx.AscendGreaterOrEqual("", keyAccountChannels, func(key, value string) bool {
|
|
||||||
if !strings.HasPrefix(key, keyAccountChannels) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
accountToChannels = append(accountToChannels, key)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
for _, key := range accountToChannels {
|
|
||||||
tx.Delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// migrate cloak secret
|
|
||||||
val, _ := tx.Get("crypto.cloak_secret")
|
|
||||||
tx.Set(keyCloakSecret, val, nil)
|
|
||||||
|
|
||||||
// bump the legacy version key to mark the database as downgrade-incompatible
|
|
||||||
tx.Set("db.version", "23", nil)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
|
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
|
||||||
for _, change := range allChanges {
|
for _, change := range allChanges {
|
||||||
if initialVersion == change.InitialVersion {
|
if initialVersion == change.InitialVersion {
|
||||||
|
|
@ -1333,9 +1227,4 @@ var allChanges = []SchemaChange{
|
||||||
TargetVersion: 22,
|
TargetVersion: 22,
|
||||||
Changer: schemaChangeV21To22,
|
Changer: schemaChangeV21To22,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
InitialVersion: 22,
|
|
||||||
TargetVersion: 23,
|
|
||||||
Changer: schemaChangeV22ToV23,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
// Copyright (c) 2022 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package datastore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Table uint16
|
|
||||||
|
|
||||||
// XXX these are persisted and must remain stable;
|
|
||||||
// do not reorder, when deleting use _ to ensure that the deleted value is skipped
|
|
||||||
const (
|
|
||||||
TableMetadata Table = iota
|
|
||||||
TableChannels
|
|
||||||
TableChannelPurges
|
|
||||||
)
|
|
||||||
|
|
||||||
type KV struct {
|
|
||||||
UUID utils.UUID
|
|
||||||
Value []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// A Datastore provides the following abstraction:
|
|
||||||
// 1. Tables, each keyed on a UUID (the implementation is free to merge
|
|
||||||
// the table name and the UUID into a single key as long as the rest of
|
|
||||||
// the contract can be satisfied). Table names are [a-z0-9_]+
|
|
||||||
// 2. The ability to efficiently enumerate all uuid-value pairs in a table
|
|
||||||
// 3. Gets, sets, and deletes for individual (table, uuid) keys
|
|
||||||
type Datastore interface {
|
|
||||||
Backoff() time.Duration
|
|
||||||
|
|
||||||
GetAll(table Table) ([]KV, error)
|
|
||||||
|
|
||||||
// This is rarely used because it would typically lead to TOCTOU races
|
|
||||||
Get(table Table, key utils.UUID) (value []byte, err error)
|
|
||||||
|
|
||||||
Set(table Table, key utils.UUID, value []byte, expiration time.Time) error
|
|
||||||
|
|
||||||
// Note that deleting a nonexistent key is not considered an error
|
|
||||||
Delete(table Table, key utils.UUID) error
|
|
||||||
}
|
|
||||||
|
|
@ -275,6 +275,6 @@ func (dm *DLineManager) loadFromDatastore() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) loadDLines() {
|
func (s *Server) loadDLines() {
|
||||||
server.dlines = NewDLineManager(server)
|
s.dlines = NewDLineManager(s)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,10 @@
|
||||||
package email
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -26,44 +23,11 @@ var (
|
||||||
ErrNoMXRecord = errors.New("Couldn't resolve MX record")
|
ErrNoMXRecord = errors.New("Couldn't resolve MX record")
|
||||||
)
|
)
|
||||||
|
|
||||||
type BlacklistSyntax uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
BlacklistSyntaxGlob BlacklistSyntax = iota
|
|
||||||
BlacklistSyntaxRegexp
|
|
||||||
)
|
|
||||||
|
|
||||||
func blacklistSyntaxFromString(status string) (BlacklistSyntax, error) {
|
|
||||||
switch strings.ToLower(status) {
|
|
||||||
case "glob", "":
|
|
||||||
return BlacklistSyntaxGlob, nil
|
|
||||||
case "re", "regex", "regexp":
|
|
||||||
return BlacklistSyntaxRegexp, nil
|
|
||||||
default:
|
|
||||||
return BlacklistSyntaxRegexp, fmt.Errorf("Unknown blacklist syntax type `%s`", status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *BlacklistSyntax) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
var orig string
|
|
||||||
var err error
|
|
||||||
if err = unmarshal(&orig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if result, err := blacklistSyntaxFromString(orig); err == nil {
|
|
||||||
*bs = result
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MTAConfig struct {
|
type MTAConfig struct {
|
||||||
Server string
|
Server string
|
||||||
Port int
|
Port int
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
ImplicitTLS bool `yaml:"implicit-tls"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MailtoConfig struct {
|
type MailtoConfig struct {
|
||||||
|
|
@ -75,15 +39,10 @@ type MailtoConfig struct {
|
||||||
Sender string
|
Sender string
|
||||||
HeloDomain string `yaml:"helo-domain"`
|
HeloDomain string `yaml:"helo-domain"`
|
||||||
RequireTLS bool `yaml:"require-tls"`
|
RequireTLS bool `yaml:"require-tls"`
|
||||||
Protocol string `yaml:"protocol"`
|
|
||||||
LocalAddress string `yaml:"local-address"`
|
|
||||||
localAddress net.Addr
|
|
||||||
VerifyMessageSubject string `yaml:"verify-message-subject"`
|
VerifyMessageSubject string `yaml:"verify-message-subject"`
|
||||||
DKIM DKIMConfig
|
DKIM DKIMConfig
|
||||||
MTAReal MTAConfig `yaml:"mta"`
|
MTAReal MTAConfig `yaml:"mta"`
|
||||||
AddressBlacklist []string `yaml:"address-blacklist"`
|
BlacklistRegexes []string `yaml:"blacklist-regexes"`
|
||||||
AddressBlacklistSyntax BlacklistSyntax `yaml:"address-blacklist-syntax"`
|
|
||||||
AddressBlacklistFile string `yaml:"address-blacklist-file"`
|
|
||||||
blacklistRegexes []*regexp.Regexp
|
blacklistRegexes []*regexp.Regexp
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
PasswordReset struct {
|
PasswordReset struct {
|
||||||
|
|
@ -93,44 +52,6 @@ type MailtoConfig struct {
|
||||||
} `yaml:"password-reset"`
|
} `yaml:"password-reset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *MailtoConfig) compileBlacklistEntry(source string) (re *regexp.Regexp, err error) {
|
|
||||||
if config.AddressBlacklistSyntax == BlacklistSyntaxGlob {
|
|
||||||
return utils.CompileGlob(source, false)
|
|
||||||
} else {
|
|
||||||
return regexp.Compile(fmt.Sprintf("^%s$", source))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *MailtoConfig) processBlacklistFile(filename string) (result []*regexp.Regexp, err error) {
|
|
||||||
f, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
reader := bufio.NewReader(f)
|
|
||||||
lineNo := 0
|
|
||||||
for {
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
lineNo++
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line != "" && line[0] != '#' {
|
|
||||||
if compiled, compileErr := config.compileBlacklistEntry(line); compileErr == nil {
|
|
||||||
result = append(result, compiled)
|
|
||||||
} else {
|
|
||||||
return result, fmt.Errorf("Failed to compile line %d of blacklist-regex-file `%s`: %w", lineNo, line, compileErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch err {
|
|
||||||
case io.EOF:
|
|
||||||
return result, nil
|
|
||||||
case nil:
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
|
func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
|
||||||
if config.Sender == "" {
|
if config.Sender == "" {
|
||||||
return errors.New("Invalid mailto sender address")
|
return errors.New("Invalid mailto sender address")
|
||||||
|
|
@ -146,40 +67,13 @@ func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
|
||||||
config.HeloDomain = heloDomain
|
config.HeloDomain = heloDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.AddressBlacklistFile != "" {
|
for _, reg := range config.BlacklistRegexes {
|
||||||
config.blacklistRegexes, err = config.processBlacklistFile(config.AddressBlacklistFile)
|
compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if len(config.AddressBlacklist) != 0 {
|
|
||||||
config.blacklistRegexes = make([]*regexp.Regexp, 0, len(config.AddressBlacklist))
|
|
||||||
for _, reg := range config.AddressBlacklist {
|
|
||||||
compiled, err := config.compileBlacklistEntry(reg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
config.blacklistRegexes = append(config.blacklistRegexes, compiled)
|
config.blacklistRegexes = append(config.blacklistRegexes, compiled)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
config.Protocol = strings.ToLower(config.Protocol)
|
|
||||||
if config.Protocol == "" {
|
|
||||||
config.Protocol = "tcp"
|
|
||||||
}
|
|
||||||
if !(config.Protocol == "tcp" || config.Protocol == "tcp4" || config.Protocol == "tcp6") {
|
|
||||||
return fmt.Errorf("Invalid protocol for email sending: `%s`", config.Protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.LocalAddress != "" {
|
|
||||||
ipAddr := net.ParseIP(config.LocalAddress)
|
|
||||||
if ipAddr == nil {
|
|
||||||
return fmt.Errorf("Could not parse local-address for email sending: `%s`", config.LocalAddress)
|
|
||||||
}
|
|
||||||
config.localAddress = &net.TCPAddr{
|
|
||||||
IP: ipAddr,
|
|
||||||
Port: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.MTAConfig.Server != "" {
|
if config.MTAConfig.Server != "" {
|
||||||
// smarthost, nothing more to validate
|
// smarthost, nothing more to validate
|
||||||
|
|
@ -215,9 +109,6 @@ func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.
|
||||||
dkimDomain := config.DKIM.Domain
|
dkimDomain := config.DKIM.Domain
|
||||||
if dkimDomain != "" {
|
if dkimDomain != "" {
|
||||||
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), dkimDomain)
|
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), dkimDomain)
|
||||||
} else {
|
|
||||||
// #2108: send Message-ID even if dkim is not enabled
|
|
||||||
fmt.Fprintf(&message, "Message-ID: <%s-%s>\r\n", utils.GenerateSecretKey(), config.Sender)
|
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
|
fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
|
||||||
fmt.Fprintf(&message, "Subject: %s\r\n", subject)
|
fmt.Fprintf(&message, "Subject: %s\r\n", subject)
|
||||||
|
|
@ -226,9 +117,8 @@ func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
||||||
recipientLower := strings.ToLower(recipient)
|
|
||||||
for _, reg := range config.blacklistRegexes {
|
for _, reg := range config.blacklistRegexes {
|
||||||
if reg.MatchString(recipientLower) {
|
if reg.MatchString(recipient) {
|
||||||
return ErrBlacklistedAddress
|
return ErrBlacklistedAddress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,13 +132,11 @@ func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
||||||
|
|
||||||
var addr string
|
var addr string
|
||||||
var auth smtp.Auth
|
var auth smtp.Auth
|
||||||
var implicitTLS bool
|
|
||||||
if !config.DirectSendingEnabled() {
|
if !config.DirectSendingEnabled() {
|
||||||
addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
|
addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
|
||||||
if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
|
if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
|
||||||
auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
|
auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
|
||||||
}
|
}
|
||||||
implicitTLS = config.MTAReal.ImplicitTLS
|
|
||||||
} else {
|
} else {
|
||||||
idx := strings.IndexByte(recipient, '@')
|
idx := strings.IndexByte(recipient, '@')
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
|
|
@ -261,8 +149,5 @@ func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
||||||
addr = fmt.Sprintf("%s:smtp", mx)
|
addr = fmt.Sprintf("%s:smtp", mx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return smtp.SendMail(
|
return smtp.SendMail(addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg, config.RequireTLS, config.Timeout)
|
||||||
addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg,
|
|
||||||
config.RequireTLS, implicitTLS, config.Protocol, config.localAddress, config.Timeout,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ var (
|
||||||
errNoExistingBan = errors.New("Ban does not exist")
|
errNoExistingBan = errors.New("Ban does not exist")
|
||||||
errNoSuchChannel = errors.New(`No such channel`)
|
errNoSuchChannel = errors.New(`No such channel`)
|
||||||
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
||||||
errChannelPurgedAlready = errors.New(`This channel was already purged and cannot be purged again`)
|
|
||||||
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use")
|
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use")
|
||||||
errInsufficientPrivs = errors.New("Insufficient privileges")
|
errInsufficientPrivs = errors.New("Insufficient privileges")
|
||||||
errInvalidUsername = errors.New("Invalid username")
|
errInvalidUsername = errors.New("Invalid username")
|
||||||
|
|
@ -76,7 +75,6 @@ var (
|
||||||
errValidEmailRequired = errors.New("A valid email address is required for account registration")
|
errValidEmailRequired = errors.New("A valid email address is required for account registration")
|
||||||
errInvalidAccountRename = errors.New("Account renames can only change the casefolding of the account name")
|
errInvalidAccountRename = errors.New("Account renames can only change the casefolding of the account name")
|
||||||
errNameReserved = errors.New(`Name reserved due to a prior registration`)
|
errNameReserved = errors.New(`Name reserved due to a prior registration`)
|
||||||
errInvalidBearerTokenType = errors.New("invalid bearer token type")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// String Errors
|
// String Errors
|
||||||
|
|
@ -99,5 +97,5 @@ type ThrottleError struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (te *ThrottleError) Error() string {
|
func (te *ThrottleError) Error() string {
|
||||||
return fmt.Sprintf(`Please wait at least %v and try again`, te.Duration.Round(time.Millisecond))
|
return fmt.Sprintf(`Please wait at least %v and try again`, te.Duration)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fakelag is a system for artificially delaying commands when a user issues
|
// fakelag is a system for artificially delaying commands when a user issues
|
||||||
|
|
@ -39,7 +40,7 @@ func (fl *Fakelag) Initialize(config FakelagConfig) {
|
||||||
fl.config = config
|
fl.config = config
|
||||||
// XXX don't share mutable member CommandBudgets:
|
// XXX don't share mutable member CommandBudgets:
|
||||||
if config.CommandBudgets != nil {
|
if config.CommandBudgets != nil {
|
||||||
fl.config.CommandBudgets = maps.Clone(config.CommandBudgets)
|
fl.config.CommandBudgets = utils.CopyMap(config.CommandBudgets)
|
||||||
}
|
}
|
||||||
fl.nowFunc = time.Now
|
fl.nowFunc = time.Now
|
||||||
fl.sleepFunc = time.Sleep
|
fl.sleepFunc = time.Sleep
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//go:build !(plan9 || solaris)
|
//go:build !plan9
|
||||||
|
|
||||||
package flock
|
package flock
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//go:build plan9 || solaris
|
//go:build plan9
|
||||||
|
|
||||||
package flock
|
package flock
|
||||||
|
|
||||||
|
|
@ -32,7 +32,6 @@ type webircConfig struct {
|
||||||
Fingerprint *string // legacy name for certfp, #1050
|
Fingerprint *string // legacy name for certfp, #1050
|
||||||
Certfp string
|
Certfp string
|
||||||
Hosts []string
|
Hosts []string
|
||||||
AcceptHostname bool `yaml:"accept-hostname"`
|
|
||||||
allowedNets []net.IPNet
|
allowedNets []net.IPNet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -19,6 +18,10 @@ func (server *Server) Config() (config *Config) {
|
||||||
return server.config.Load()
|
return server.config.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (server *Server) ChannelRegistrationEnabled() bool {
|
||||||
|
return server.Config().Channels.Registration.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
func (server *Server) GetOperator(name string) (oper *Oper) {
|
func (server *Server) GetOperator(name string) (oper *Oper) {
|
||||||
name, err := CasefoldName(name)
|
name, err := CasefoldName(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -89,7 +92,7 @@ func (client *Client) AllSessionData(currentSession *Session, hasPrivs bool) (da
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSeen time.Time, wasAway, nowAway string) {
|
func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSeen time.Time, back bool) {
|
||||||
config := client.server.Config()
|
config := client.server.Config()
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
|
|
@ -110,22 +113,14 @@ func (client *Client) AddSession(session *Session) (success bool, numSessions in
|
||||||
client.setLastSeen(time.Now().UTC(), session.deviceID)
|
client.setLastSeen(time.Now().UTC(), session.deviceID)
|
||||||
}
|
}
|
||||||
client.sessions = newSessions
|
client.sessions = newSessions
|
||||||
wasAway = client.awayMessage
|
// TODO(#1551) there should be a cap to opt out of this behavior on a session
|
||||||
if client.autoAwayEnabledNoMutex(config) {
|
if persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) {
|
||||||
client.setAutoAwayNoMutex(config)
|
|
||||||
} else {
|
|
||||||
if session.awayMessage != "" && session.awayMessage != "*" {
|
|
||||||
// set the away message
|
|
||||||
client.awayMessage = session.awayMessage
|
|
||||||
} else if session.awayMessage == "" && !session.awayAt.IsZero() {
|
|
||||||
// weird edge case: explicit `AWAY` or `AWAY :` during pre-registration makes the client back
|
|
||||||
client.awayMessage = ""
|
client.awayMessage = ""
|
||||||
|
if len(client.sessions) == 1 {
|
||||||
|
back = true
|
||||||
}
|
}
|
||||||
// else: the client sent no AWAY command at all, no-op
|
|
||||||
// or: the client sent `AWAY *`, which should not modify the publicly visible away state
|
|
||||||
}
|
}
|
||||||
nowAway = client.awayMessage
|
return true, len(client.sessions), lastSeen, back
|
||||||
return true, len(client.sessions), lastSeen, wasAway, nowAway
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) removeSession(session *Session) (success bool, length int) {
|
func (client *Client) removeSession(session *Session) (success bool, length int) {
|
||||||
|
|
@ -200,7 +195,7 @@ func (client *Client) Away() (result bool, message string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *Session) SetAway(awayMessage string) (wasAway, nowAway string) {
|
func (session *Session) SetAway(awayMessage string) {
|
||||||
client := session.client
|
client := session.client
|
||||||
config := client.server.Config()
|
config := client.server.Config()
|
||||||
|
|
||||||
|
|
@ -210,19 +205,13 @@ func (session *Session) SetAway(awayMessage string) (wasAway, nowAway string) {
|
||||||
session.awayMessage = awayMessage
|
session.awayMessage = awayMessage
|
||||||
session.awayAt = time.Now().UTC()
|
session.awayAt = time.Now().UTC()
|
||||||
|
|
||||||
wasAway = client.awayMessage
|
autoAway := client.registered && client.alwaysOn && persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway)
|
||||||
if client.autoAwayEnabledNoMutex(config) {
|
if autoAway {
|
||||||
client.setAutoAwayNoMutex(config)
|
client.setAutoAwayNoMutex(config)
|
||||||
} else if awayMessage != "*" {
|
} else {
|
||||||
client.awayMessage = awayMessage
|
client.awayMessage = awayMessage
|
||||||
} // else: `AWAY *`, should not modify publicly visible away state
|
|
||||||
nowAway = client.awayMessage
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
func (client *Client) autoAwayEnabledNoMutex(config *Config) bool {
|
|
||||||
return client.registered && client.alwaysOn &&
|
|
||||||
persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) setAutoAwayNoMutex(config *Config) {
|
func (client *Client) setAutoAwayNoMutex(config *Config) {
|
||||||
|
|
@ -234,8 +223,8 @@ func (client *Client) setAutoAwayNoMutex(config *Config) {
|
||||||
// a session is active, we are not auto-away
|
// a session is active, we are not auto-away
|
||||||
client.awayMessage = ""
|
client.awayMessage = ""
|
||||||
return
|
return
|
||||||
} else if cSession.awayAt.After(awaySetAt) && cSession.awayMessage != "*" {
|
} else if cSession.awayAt.After(awaySetAt) {
|
||||||
// choose the latest valid away message from any session
|
// choose the latest available away message from any session
|
||||||
globalAwayState = cSession.awayMessage
|
globalAwayState = cSession.awayMessage
|
||||||
awaySetAt = cSession.awayAt
|
awaySetAt = cSession.awayAt
|
||||||
}
|
}
|
||||||
|
|
@ -410,11 +399,7 @@ func (client *Client) SetMode(mode modes.Mode, on bool) bool {
|
||||||
|
|
||||||
func (client *Client) SetRealname(realname string) {
|
func (client *Client) SetRealname(realname string) {
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
// TODO: make this configurable
|
|
||||||
client.realname = realname
|
client.realname = realname
|
||||||
if len(realname) > 128 {
|
|
||||||
client.realname = client.realname[:128]
|
|
||||||
}
|
|
||||||
alwaysOn := client.registered && client.alwaysOn
|
alwaysOn := client.registered && client.alwaysOn
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
if alwaysOn {
|
if alwaysOn {
|
||||||
|
|
@ -516,7 +501,7 @@ func (client *Client) GetReadMarker(cfname string) (result string) {
|
||||||
func (client *Client) copyReadMarkers() (result map[string]time.Time) {
|
func (client *Client) copyReadMarkers() (result map[string]time.Time) {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
return maps.Clone(client.readMarkers)
|
return utils.CopyMap(client.readMarkers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) SetReadMarker(cfname string, now time.Time) (result time.Time) {
|
func (client *Client) SetReadMarker(cfname string, now time.Time) (result time.Time) {
|
||||||
|
|
@ -616,11 +601,9 @@ func (channel *Channel) Founder() string {
|
||||||
|
|
||||||
func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
|
func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
defer channel.stateMutex.RUnlock()
|
clientModes := channel.members[client].modes
|
||||||
if clientData, ok := channel.members[client]; ok {
|
channel.stateMutex.RUnlock()
|
||||||
return clientData.modes.HighestChannelUserMode()
|
return clientModes.HighestChannelUserMode()
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) Settings() (result ChannelSettings) {
|
func (channel *Channel) Settings() (result ChannelSettings) {
|
||||||
|
|
@ -655,9 +638,3 @@ func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) {
|
||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
return channel.accountToUMode[cfaccount]
|
return channel.accountToUMode[cfaccount]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) UUID() utils.UUID {
|
|
||||||
channel.stateMutex.RLock()
|
|
||||||
defer channel.stateMutex.RUnlock()
|
|
||||||
return channel.uuid
|
|
||||||
}
|
|
||||||
|
|
|
||||||
521
irc/handlers.go
521
irc/handlers.go
|
|
@ -8,6 +8,7 @@ package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -30,7 +31,6 @@ import (
|
||||||
"github.com/ergochat/ergo/irc/history"
|
"github.com/ergochat/ergo/irc/history"
|
||||||
"github.com/ergochat/ergo/irc/jwt"
|
"github.com/ergochat/ergo/irc/jwt"
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/oauth2"
|
|
||||||
"github.com/ergochat/ergo/irc/sno"
|
"github.com/ergochat/ergo/irc/sno"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -90,6 +90,8 @@ func sendSuccessfulRegResponse(service *ircService, client *Client, rb *Response
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
if service != nil {
|
if service != nil {
|
||||||
service.Notice(rb, client.t("Account created"))
|
service.Notice(rb, client.t("Account created"))
|
||||||
|
} else {
|
||||||
|
rb.Add(nil, client.server.name, RPL_REG_SUCCESS, details.nick, details.accountName, client.t("Account created"))
|
||||||
}
|
}
|
||||||
client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] registered account $c[grey][$r%s$c[grey]] from IP %s"), details.nickMask, details.accountName, rb.session.IP().String()))
|
client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] registered account $c[grey][$r%s$c[grey]] from IP %s"), details.nickMask, details.accountName, rb.session.IP().String()))
|
||||||
sendSuccessfulAccountAuth(service, client, rb, false)
|
sendSuccessfulAccountAuth(service, client, rb, false)
|
||||||
|
|
@ -178,10 +180,6 @@ func acceptHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
saslMaxResponseLength = 8192 // implementation-defined sanity check, long enough for bearer tokens
|
|
||||||
)
|
|
||||||
|
|
||||||
// AUTHENTICATE [<mechanism>|<data>|*]
|
// AUTHENTICATE [<mechanism>|<data>|*]
|
||||||
func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
session := rb.session
|
session := rb.session
|
||||||
|
|
@ -205,21 +203,17 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// start new sasl session: parameter is the authentication mechanism
|
// start new sasl session
|
||||||
if session.sasl.mechanism == "" {
|
if session.sasl.mechanism == "" {
|
||||||
mechanism := strings.ToUpper(msg.Params[0])
|
throttled, remainingTime := client.loginThrottle.Touch()
|
||||||
_, mechanismIsEnabled := EnabledSaslMechanisms[mechanism]
|
if throttled {
|
||||||
|
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
|
||||||
// The spec says: "The AUTHENTICATE command MUST be used before registration
|
|
||||||
// is complete and with the sasl capability enabled." Enforcing this universally
|
|
||||||
// would simplify the implementation somewhat, but we've never enforced it before
|
|
||||||
// and I don't want to break working clients that use PLAIN or EXTERNAL
|
|
||||||
// and violate this MUST (e.g. by sending CAP END too early).
|
|
||||||
if client.registered && !(mechanism == "PLAIN" || mechanism == "EXTERNAL") {
|
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL is only allowed before connection registration"))
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mechanism := strings.ToUpper(msg.Params[0])
|
||||||
|
_, mechanismIsEnabled := EnabledSaslMechanisms[mechanism]
|
||||||
|
|
||||||
if mechanismIsEnabled {
|
if mechanismIsEnabled {
|
||||||
session.sasl.mechanism = mechanism
|
session.sasl.mechanism = mechanism
|
||||||
if !config.Server.Compatibility.SendUnprefixedSasl {
|
if !config.Server.Compatibility.SendUnprefixedSasl {
|
||||||
|
|
@ -237,28 +231,46 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// continue existing sasl session: parameter is a message chunk
|
// continue existing sasl session
|
||||||
done, value, err := session.sasl.value.Add(msg.Params[0])
|
rawData := msg.Params[0]
|
||||||
if err == nil {
|
|
||||||
if done {
|
// https://ircv3.net/specs/extensions/sasl-3.1:
|
||||||
// call actual handler
|
// "The response is encoded in Base64 (RFC 4648), then split to 400-byte chunks,
|
||||||
handler := EnabledSaslMechanisms[session.sasl.mechanism]
|
// and each chunk is sent as a separate AUTHENTICATE command."
|
||||||
return handler(server, client, session, value, rb)
|
saslMaxArgLength := 400
|
||||||
} else {
|
if len(rawData) > saslMaxArgLength {
|
||||||
return false // wait for continuation line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// else: error handling
|
|
||||||
switch err {
|
|
||||||
case ircutils.ErrSASLTooLong:
|
|
||||||
rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long"))
|
rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long"))
|
||||||
case ircutils.ErrSASLLimitExceeded:
|
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
|
|
||||||
default:
|
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
|
|
||||||
}
|
|
||||||
session.sasl.Clear()
|
session.sasl.Clear()
|
||||||
return false
|
return false
|
||||||
|
} else if len(rawData) == saslMaxArgLength {
|
||||||
|
// allow 4 'continuation' lines before rejecting for length
|
||||||
|
if len(session.sasl.value) >= saslMaxArgLength*4 {
|
||||||
|
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
|
||||||
|
session.sasl.Clear()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
session.sasl.value += rawData
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if rawData != "+" {
|
||||||
|
session.sasl.value += rawData
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
if session.sasl.value != "+" {
|
||||||
|
data, err = base64.StdEncoding.DecodeString(session.sasl.value)
|
||||||
|
session.sasl.value = ""
|
||||||
|
if err != nil {
|
||||||
|
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
|
||||||
|
session.sasl.Clear()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call actual handler
|
||||||
|
handler := EnabledSaslMechanisms[session.sasl.mechanism]
|
||||||
|
return handler(server, client, session, data, rb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AUTHENTICATE PLAIN
|
// AUTHENTICATE PLAIN
|
||||||
|
|
@ -306,27 +318,6 @@ func authPlainHandler(server *Server, client *Client, session *Session, value []
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// AUTHENTICATE IRCV3BEARER
|
|
||||||
func authIRCv3BearerHandler(server *Server, client *Client, session *Session, value []byte, rb *ResponseBuffer) bool {
|
|
||||||
defer session.sasl.Clear()
|
|
||||||
|
|
||||||
// <authzid> \x00 <type> \x00 <token>
|
|
||||||
splitValue := bytes.SplitN(value, []byte{'\000'}, 3)
|
|
||||||
if len(splitValue) != 3 {
|
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), client.t("SASL authentication failed: Invalid auth blob"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
err := server.accounts.AuthenticateByBearerToken(client, string(splitValue[1]), string(splitValue[2]))
|
|
||||||
if err != nil {
|
|
||||||
sendAuthErrorResponse(client, rb, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
sendSuccessfulAccountAuth(nil, client, rb, true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendAuthErrorResponse(client *Client, rb *ResponseBuffer, err error) {
|
func sendAuthErrorResponse(client *Client, rb *ResponseBuffer, err error) {
|
||||||
msg := authErrorToMessage(client.server, err)
|
msg := authErrorToMessage(client.server, err)
|
||||||
rb.Add(nil, client.server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
|
rb.Add(nil, client.server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
|
||||||
|
|
@ -341,7 +332,7 @@ func authErrorToMessage(server *Server, err error) (msg string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch err {
|
switch err {
|
||||||
case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch, errNickAccountMismatch, errAccountSuspended, oauth2.ErrInvalidToken:
|
case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch, errNickAccountMismatch, errAccountSuspended:
|
||||||
return err.Error()
|
return err.Error()
|
||||||
default:
|
default:
|
||||||
// don't expose arbitrary error messages to the user
|
// don't expose arbitrary error messages to the user
|
||||||
|
|
@ -361,18 +352,28 @@ func authExternalHandler(server *Server, client *Client, session *Session, value
|
||||||
|
|
||||||
// EXTERNAL doesn't carry an authentication ID (this is determined from the
|
// EXTERNAL doesn't carry an authentication ID (this is determined from the
|
||||||
// certificate), but does carry an optional authorization ID.
|
// certificate), but does carry an optional authorization ID.
|
||||||
authzid := string(value)
|
var authzid string
|
||||||
var deviceID string
|
|
||||||
var err error
|
var err error
|
||||||
// see #843: strip the device ID for the benefit of clients that don't
|
if len(value) != 0 {
|
||||||
// distinguish user/ident from account name
|
authzid, err = CasefoldName(string(value))
|
||||||
if strudelIndex := strings.IndexByte(authzid, '@'); strudelIndex != -1 {
|
if err != nil {
|
||||||
authzid, deviceID = authzid[:strudelIndex], authzid[strudelIndex+1:]
|
err = errAuthzidAuthcidMismatch
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
// see #843: strip the device ID for the benefit of clients that don't
|
||||||
|
// distinguish user/ident from account name
|
||||||
|
if strudelIndex := strings.IndexByte(authzid, '@'); strudelIndex != -1 {
|
||||||
|
var deviceID string
|
||||||
|
authzid, deviceID = authzid[:strudelIndex], authzid[strudelIndex+1:]
|
||||||
|
if !client.registered {
|
||||||
|
rb.session.deviceID = deviceID
|
||||||
|
}
|
||||||
|
}
|
||||||
err = server.accounts.AuthenticateByCertificate(client, rb.session.certfp, rb.session.peerCerts, authzid)
|
err = server.accounts.AuthenticateByCertificate(client, rb.session.certfp, rb.session.peerCerts, authzid)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAuthErrorResponse(client, rb, err)
|
sendAuthErrorResponse(client, rb, err)
|
||||||
return false
|
return false
|
||||||
|
|
@ -381,9 +382,6 @@ func authExternalHandler(server *Server, client *Client, session *Session, value
|
||||||
}
|
}
|
||||||
|
|
||||||
sendSuccessfulAccountAuth(nil, client, rb, true)
|
sendSuccessfulAccountAuth(nil, client, rb, true)
|
||||||
if !client.registered && deviceID != "" {
|
|
||||||
rb.session.deviceID = deviceID
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -398,12 +396,6 @@ func authScramHandler(server *Server, client *Client, session *Session, value []
|
||||||
|
|
||||||
// first message? if so, initialize the SCRAM conversation
|
// first message? if so, initialize the SCRAM conversation
|
||||||
if session.sasl.scramConv == nil {
|
if session.sasl.scramConv == nil {
|
||||||
if throttled, remainingTime := client.checkLoginThrottle(); throttled {
|
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(),
|
|
||||||
fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime.Round(time.Millisecond)))
|
|
||||||
continueAuth = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
session.sasl.scramConv = server.accounts.NewScramConversation()
|
session.sasl.scramConv = server.accounts.NewScramConversation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,8 +419,9 @@ func authScramHandler(server *Server, client *Client, session *Session, value []
|
||||||
account, err := server.accounts.LoadAccount(authcid)
|
account, err := server.accounts.LoadAccount(authcid)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
server.accounts.Login(client, account)
|
server.accounts.Login(client, account)
|
||||||
// fixupNickEqualsAccount is not needed for unregistered clients
|
if fixupNickEqualsAccount(client, rb, server.Config(), "") {
|
||||||
sendSuccessfulAccountAuth(nil, client, rb, true)
|
sendSuccessfulAccountAuth(nil, client, rb, true)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
server.logger.Error("internal", "SCRAM succeeded but couldn't load account", authcid, err.Error())
|
server.logger.Error("internal", "SCRAM succeeded but couldn't load account", authcid, err.Error())
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed"))
|
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed"))
|
||||||
|
|
@ -441,7 +434,7 @@ func authScramHandler(server *Server, client *Client, session *Session, value []
|
||||||
|
|
||||||
response, err := session.sasl.scramConv.Step(string(value))
|
response, err := session.sasl.scramConv.Step(string(value))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
sendSASLChallenge(server, rb, []byte(response))
|
rb.Add(nil, server.name, "AUTHENTICATE", base64.StdEncoding.EncodeToString([]byte(response)))
|
||||||
} else {
|
} else {
|
||||||
continueAuth = false
|
continueAuth = false
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), err.Error())
|
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), err.Error())
|
||||||
|
|
@ -451,88 +444,34 @@ func authScramHandler(server *Server, client *Client, session *Session, value []
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// AUTHENTICATE OAUTHBEARER
|
|
||||||
func authOauthBearerHandler(server *Server, client *Client, session *Session, value []byte, rb *ResponseBuffer) bool {
|
|
||||||
if !server.Config().Accounts.OAuth2.Enabled {
|
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), "SASL authentication failed: mechanism not enabled")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if session.sasl.oauthConv == nil {
|
|
||||||
session.sasl.oauthConv = oauth2.NewOAuthBearerServer(
|
|
||||||
func(opts oauth2.OAuthBearerOptions) *oauth2.OAuthBearerError {
|
|
||||||
err := server.accounts.AuthenticateByOAuthBearer(client, opts)
|
|
||||||
switch err {
|
|
||||||
case nil:
|
|
||||||
return nil
|
|
||||||
case oauth2.ErrInvalidToken:
|
|
||||||
return &oauth2.OAuthBearerError{Status: "invalid_token", Schemes: "bearer"}
|
|
||||||
case errFeatureDisabled:
|
|
||||||
return &oauth2.OAuthBearerError{Status: "invalid_request", Schemes: "bearer"}
|
|
||||||
default:
|
|
||||||
// this is probably a misconfiguration or infrastructure error so we should log it
|
|
||||||
server.logger.Error("internal", "failed to validate OAUTHBEARER token", err.Error())
|
|
||||||
// tell the client it was their fault even though it probably wasn't:
|
|
||||||
return &oauth2.OAuthBearerError{Status: "invalid_request", Schemes: "bearer"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
challenge, done, err := session.sasl.oauthConv.Next(value)
|
|
||||||
if done {
|
|
||||||
if err == nil {
|
|
||||||
sendSuccessfulAccountAuth(nil, client, rb, true)
|
|
||||||
} else {
|
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), ircutils.SanitizeText(err.Error(), 350))
|
|
||||||
}
|
|
||||||
session.sasl.Clear()
|
|
||||||
} else {
|
|
||||||
// ignore `err`, we need to relay the challenge (which may contain a JSON-encoded error)
|
|
||||||
// to the client
|
|
||||||
sendSASLChallenge(server, rb, challenge)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper to b64 a sasl response and chunk it into 400-byte lines
|
|
||||||
// as per https://ircv3.net/specs/extensions/sasl-3.1
|
|
||||||
func sendSASLChallenge(server *Server, rb *ResponseBuffer, challenge []byte) {
|
|
||||||
for _, chunk := range ircutils.EncodeSASLResponse(challenge) {
|
|
||||||
rb.Add(nil, server.name, "AUTHENTICATE", chunk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AWAY [<message>]
|
// AWAY [<message>]
|
||||||
func awayHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func awayHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
// #1996: `AWAY :` is treated the same as `AWAY`
|
var isAway bool
|
||||||
var awayMessage string
|
var awayMessage string
|
||||||
if len(msg.Params) > 0 {
|
if len(msg.Params) > 0 {
|
||||||
awayMessage = msg.Params[0]
|
awayMessage = msg.Params[0]
|
||||||
awayMessage = ircmsg.TruncateUTF8Safe(awayMessage, server.Config().Limits.AwayLen)
|
awayMessage = ircutils.TruncateUTF8Safe(awayMessage, server.Config().Limits.AwayLen)
|
||||||
}
|
}
|
||||||
|
isAway = (awayMessage != "") // #1996
|
||||||
|
|
||||||
wasAway, nowAway := rb.session.SetAway(awayMessage)
|
rb.session.SetAway(awayMessage)
|
||||||
|
|
||||||
if nowAway != "" {
|
if isAway {
|
||||||
rb.Add(nil, server.name, RPL_NOWAWAY, client.nick, client.t("You have been marked as being away"))
|
rb.Add(nil, server.name, RPL_NOWAWAY, client.nick, client.t("You have been marked as being away"))
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, server.name, RPL_UNAWAY, client.nick, client.t("You are no longer marked as being away"))
|
rb.Add(nil, server.name, RPL_UNAWAY, client.nick, client.t("You are no longer marked as being away"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.registered && wasAway != nowAway {
|
dispatchAwayNotify(client, isAway, awayMessage)
|
||||||
dispatchAwayNotify(client, nowAway)
|
|
||||||
} // else: we'll send it (if applicable) after reattach
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchAwayNotify(client *Client, awayMessage string) {
|
func dispatchAwayNotify(client *Client, isAway bool, awayMessage string) {
|
||||||
// dispatch away-notify
|
// dispatch away-notify
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
isBot := client.HasMode(modes.Bot)
|
isBot := client.HasMode(modes.Bot)
|
||||||
for session := range client.FriendsMonitors(caps.AwayNotify) {
|
for session := range client.FriendsMonitors(caps.AwayNotify) {
|
||||||
if awayMessage != "" {
|
if isAway {
|
||||||
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY", awayMessage)
|
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY", awayMessage)
|
||||||
} else {
|
} else {
|
||||||
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY")
|
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY")
|
||||||
|
|
@ -570,16 +509,6 @@ func batchHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon
|
||||||
// XXX changing the label inside a handler is a bit dodgy, but it works here
|
// XXX changing the label inside a handler is a bit dodgy, but it works here
|
||||||
// because there's no way we could have triggered a flush up to this point
|
// because there's no way we could have triggered a flush up to this point
|
||||||
rb.Label = batch.responseLabel
|
rb.Label = batch.responseLabel
|
||||||
|
|
||||||
for _, msg := range batch.message.Split {
|
|
||||||
signatures := server.GenerateImagorSignatures(msg.Message)
|
|
||||||
if len(signatures) > 0 {
|
|
||||||
if msg.Tags == nil {
|
|
||||||
msg.Tags = make(map[string]string)
|
|
||||||
}
|
|
||||||
msg.Tags["signatures"] = signatures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dispatchMessageToTarget(client, batch.tags, histType, batch.command, batch.target, batch.message, rb)
|
dispatchMessageToTarget(client, batch.tags, histType, batch.command, batch.target, batch.message, rb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -712,14 +641,6 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
||||||
var err error
|
var err error
|
||||||
var listTargets bool
|
var listTargets bool
|
||||||
var targets []history.TargetListing
|
var targets []history.TargetListing
|
||||||
var _, batchIdentifier = msg.GetTag("identifier")
|
|
||||||
var assuredPreposition = "error"
|
|
||||||
var limit int
|
|
||||||
|
|
||||||
if len(batchIdentifier) == 0 {
|
|
||||||
batchIdentifier = "UNIDENTIFIED"
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
// errors are sent either without a batch, or in a draft/labeled-response batch as usual
|
// errors are sent either without a batch, or in a draft/labeled-response batch as usual
|
||||||
if err == utils.ErrInvalidParams {
|
if err == utils.ErrInvalidParams {
|
||||||
|
|
@ -731,7 +652,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
||||||
} else {
|
} else {
|
||||||
// successful responses are sent as a chathistory or history batch
|
// successful responses are sent as a chathistory or history batch
|
||||||
if listTargets {
|
if listTargets {
|
||||||
batchID := rb.StartNestedBatch(caps.ChathistoryTargetsBatchType)
|
batchID := rb.StartNestedBatch("draft/chathistory-targets")
|
||||||
defer rb.EndNestedBatch(batchID)
|
defer rb.EndNestedBatch(batchID)
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
name := server.UnfoldName(target.CfName)
|
name := server.UnfoldName(target.CfName)
|
||||||
|
|
@ -739,9 +660,9 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
||||||
target.Time.Format(IRCv3TimestampFormat))
|
target.Time.Format(IRCv3TimestampFormat))
|
||||||
}
|
}
|
||||||
} else if channel != nil {
|
} else if channel != nil {
|
||||||
channel.replayHistoryItems(rb, items, true, batchIdentifier, assuredPreposition, limit)
|
channel.replayHistoryItems(rb, items, true)
|
||||||
} else {
|
} else {
|
||||||
client.replayPrivmsgHistory(rb, items, target, true, batchIdentifier, assuredPreposition, limit)
|
client.replayPrivmsgHistory(rb, items, target, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
@ -794,6 +715,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
||||||
|
|
||||||
paramPos := 2
|
paramPos := 2
|
||||||
var start, end history.Selector
|
var start, end history.Selector
|
||||||
|
var limit int
|
||||||
switch preposition {
|
switch preposition {
|
||||||
case "targets":
|
case "targets":
|
||||||
// use the same selector parsing as BETWEEN,
|
// use the same selector parsing as BETWEEN,
|
||||||
|
|
@ -848,7 +770,6 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
||||||
err = utils.ErrInvalidParams
|
err = utils.ErrInvalidParams
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assuredPreposition = preposition
|
|
||||||
|
|
||||||
if listTargets {
|
if listTargets {
|
||||||
targets, err = client.listTargets(start, end, limit)
|
targets, err = client.listTargets(start, end, limit)
|
||||||
|
|
@ -873,6 +794,7 @@ func debugHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon
|
||||||
switch param {
|
switch param {
|
||||||
case "GCSTATS":
|
case "GCSTATS":
|
||||||
stats := debug.GCStats{
|
stats := debug.GCStats{
|
||||||
|
Pause: make([]time.Duration, 10),
|
||||||
PauseQuantiles: make([]time.Duration, 5),
|
PauseQuantiles: make([]time.Duration, 5),
|
||||||
}
|
}
|
||||||
debug.ReadGCStats(&stats)
|
debug.ReadGCStats(&stats)
|
||||||
|
|
@ -1145,12 +1067,6 @@ func extjwtHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
||||||
}
|
}
|
||||||
|
|
||||||
claims["channel"] = channel.Name()
|
claims["channel"] = channel.Name()
|
||||||
|
|
||||||
var channelModeStrings []string
|
|
||||||
for _, mode := range channel.flags.AllModes() {
|
|
||||||
channelModeStrings = append(channelModeStrings, mode.String())
|
|
||||||
}
|
|
||||||
claims["chanModes"] = channelModeStrings
|
|
||||||
claims["joined"] = 0
|
claims["joined"] = 0
|
||||||
claims["cmodes"] = []string{}
|
claims["cmodes"] = []string{}
|
||||||
if present, joinTimeSecs, cModes := channel.ClientStatus(client); present {
|
if present, joinTimeSecs, cModes := channel.ClientStatus(client); present {
|
||||||
|
|
@ -1234,8 +1150,6 @@ Get an explanation of <argument>, or "index" for a list of help topics.`), rb)
|
||||||
// HISTORY alice 15
|
// HISTORY alice 15
|
||||||
// HISTORY #darwin 1h
|
// HISTORY #darwin 1h
|
||||||
func historyHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func historyHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
rb.Notice(client.t("This command is currently disabled. Please use CHATHISTORY"))
|
|
||||||
/*
|
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
if !config.History.Enabled {
|
if !config.History.Enabled {
|
||||||
rb.Notice(client.t("This command has been disabled by the server administrators"))
|
rb.Notice(client.t("This command has been disabled by the server administrators"))
|
||||||
|
|
@ -1252,16 +1166,13 @@ func historyHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var _, batchIdentifier = msg.GetTag("identifier")
|
|
||||||
|
|
||||||
if len(items) != 0 {
|
if len(items) != 0 {
|
||||||
if channel != nil {
|
if channel != nil {
|
||||||
channel.replayHistoryItems(rb, items, true, batchIdentifier)
|
channel.replayHistoryItems(rb, items, true)
|
||||||
} else {
|
} else {
|
||||||
client.replayPrivmsgHistory(rb, items, "", true, batchIdentifier)
|
client.replayPrivmsgHistory(rb, items, "", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1349,15 +1260,6 @@ func isonHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ISUPPORT
|
|
||||||
func isupportHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
|
||||||
server.RplISupport(client, rb)
|
|
||||||
if !client.registered {
|
|
||||||
rb.session.isupportSentPrereg = true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// JOIN <channel>{,<channel>} [<key>{,<key>}]
|
// JOIN <channel>{,<channel>} [<key>{,<key>}]
|
||||||
func joinHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func joinHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
// #1417: allow `JOIN 0` with a confirmation code
|
// #1417: allow `JOIN 0` with a confirmation code
|
||||||
|
|
@ -1540,7 +1442,7 @@ func kickHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||||
// KILL <nickname> <comment>
|
// KILL <nickname> <comment>
|
||||||
func killHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func killHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
nickname := msg.Params[0]
|
nickname := msg.Params[0]
|
||||||
var comment string
|
comment := "<no reason supplied>"
|
||||||
if len(msg.Params) > 1 {
|
if len(msg.Params) > 1 {
|
||||||
comment = msg.Params[1]
|
comment = msg.Params[1]
|
||||||
}
|
}
|
||||||
|
|
@ -1550,21 +1452,12 @@ func killHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||||
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.Nick(), utils.SafeErrorParam(nickname), client.t("No such nick"))
|
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.Nick(), utils.SafeErrorParam(nickname), client.t("No such nick"))
|
||||||
return false
|
return false
|
||||||
} else if target.AlwaysOn() {
|
} else if target.AlwaysOn() {
|
||||||
rb.Add(nil, client.server.name, ERR_UNKNOWNERROR, client.Nick(), "KILL", fmt.Sprintf(client.t("Client %s is always-on and cannot be fully removed by /KILL; consider /UBAN ADD instead"), target.Nick()))
|
rb.Add(nil, client.server.name, ERR_UNKNOWNERROR, client.Nick(), "KILL", fmt.Sprintf(client.t("Client %s is always-on and cannot be fully removed by /KILL; consider /NS SUSPEND instead"), target.Nick()))
|
||||||
}
|
}
|
||||||
|
|
||||||
quitMsg := "Killed"
|
quitMsg := fmt.Sprintf("Killed (%s (%s))", client.nick, comment)
|
||||||
if comment != "" {
|
|
||||||
quitMsg = fmt.Sprintf("Killed by %s: %s", client.Nick(), comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
var snoLine string
|
server.snomasks.Send(sno.LocalKills, fmt.Sprintf(ircfmt.Unescape("%s$r was killed by %s $c[grey][$r%s$c[grey]]"), target.nick, client.nick, comment))
|
||||||
if comment == "" {
|
|
||||||
snoLine = fmt.Sprintf(ircfmt.Unescape("%s was killed by %s"), target.Nick(), client.Nick())
|
|
||||||
} else {
|
|
||||||
snoLine = fmt.Sprintf(ircfmt.Unescape("%s was killed by %s $c[grey][$r%s$c[grey]]"), target.Nick(), client.Nick(), comment)
|
|
||||||
}
|
|
||||||
server.snomasks.Send(sno.LocalKills, snoLine)
|
|
||||||
|
|
||||||
target.Quit(quitMsg, nil)
|
target.Quit(quitMsg, nil)
|
||||||
target.destroy(nil)
|
target.destroy(nil)
|
||||||
|
|
@ -1773,7 +1666,7 @@ func listHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
if time.Since(client.ctime) < config.Channels.ListDelay && client.Account() == "" && !client.HasMode(modes.Operator) {
|
if time.Since(client.ctime) < config.Channels.ListDelay && client.Account() == "" && !client.HasMode(modes.Operator) {
|
||||||
remaining := time.Until(client.ctime.Add(config.Channels.ListDelay))
|
remaining := time.Until(client.ctime.Add(config.Channels.ListDelay))
|
||||||
rb.Notice(fmt.Sprintf(client.t("This server requires that you wait %v after connecting before you can use /LIST. You have %v left."), config.Channels.ListDelay, remaining.Round(time.Millisecond)))
|
rb.Notice(fmt.Sprintf(client.t("This server requires that you wait %v after connecting before you can use /LIST. You have %v left."), config.Channels.ListDelay, remaining))
|
||||||
rb.Add(nil, server.name, RPL_LISTEND, client.Nick(), client.t("End of LIST"))
|
rb.Add(nil, server.name, RPL_LISTEND, client.Nick(), client.t("End of LIST"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -1825,7 +1718,7 @@ func listHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||||
|
|
||||||
clientIsOp := client.HasRoleCapabs("sajoin")
|
clientIsOp := client.HasRoleCapabs("sajoin")
|
||||||
if len(channels) == 0 {
|
if len(channels) == 0 {
|
||||||
for _, channel := range server.channels.ListableChannels() {
|
for _, channel := range server.channels.Channels() {
|
||||||
if !clientIsOp && channel.flags.HasMode(modes.Secret) && !channel.hasClient(client) {
|
if !clientIsOp && channel.flags.HasMode(modes.Secret) && !channel.hasClient(client) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1928,9 +1821,8 @@ func announceCmodeChanges(channel *Channel, applied modes.ModeChanges, source, a
|
||||||
channel.AddHistoryItem(history.Item{
|
channel.AddHistoryItem(history.Item{
|
||||||
Type: history.Mode,
|
Type: history.Mode,
|
||||||
Nick: source,
|
Nick: source,
|
||||||
Account: accountName,
|
AccountName: accountName,
|
||||||
Message: message,
|
Message: message,
|
||||||
Target: channel.NameCasefolded(),
|
|
||||||
IsBot: isBot,
|
IsBot: isBot,
|
||||||
}, account)
|
}, account)
|
||||||
}
|
}
|
||||||
|
|
@ -2156,6 +2048,8 @@ func namesHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon
|
||||||
channels = strings.Split(msg.Params[0], ",")
|
channels = strings.Split(msg.Params[0], ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: in a post-federation world, process `target` (server to forward request to)
|
||||||
|
|
||||||
// implement the modern behavior: https://modern.ircdocs.horse/#names-message
|
// implement the modern behavior: https://modern.ircdocs.horse/#names-message
|
||||||
// "Servers MAY only return information about the first <channel> and silently ignore the others."
|
// "Servers MAY only return information about the first <channel> and silently ignore the others."
|
||||||
// "If no parameter is given for this command, servers SHOULD return one RPL_ENDOFNAMES numeric
|
// "If no parameter is given for this command, servers SHOULD return one RPL_ENDOFNAMES numeric
|
||||||
|
|
@ -2222,7 +2116,6 @@ func validateLineLen(msgType history.ItemType, source, target, payload string) (
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
limit -= len(target)
|
|
||||||
limit -= len(payload)
|
limit -= len(payload)
|
||||||
return limit >= 0
|
return limit >= 0
|
||||||
}
|
}
|
||||||
|
|
@ -2243,50 +2136,39 @@ func validateSplitMessageLen(msgType history.ItemType, source, target string, me
|
||||||
|
|
||||||
// helper to store a batched PRIVMSG in the session object
|
// helper to store a batched PRIVMSG in the session object
|
||||||
func absorbBatchedMessage(server *Server, client *Client, msg ircmsg.Message, batchTag string, histType history.ItemType, rb *ResponseBuffer) {
|
func absorbBatchedMessage(server *Server, client *Client, msg ircmsg.Message, batchTag string, histType history.ItemType, rb *ResponseBuffer) {
|
||||||
var failParams []string
|
var errorCode, errorMessage string
|
||||||
defer func() {
|
defer func() {
|
||||||
if failParams != nil {
|
if errorCode != "" {
|
||||||
if histType != history.Notice {
|
if histType != history.Notice {
|
||||||
params := make([]string, 1+len(failParams))
|
rb.Add(nil, server.name, "FAIL", "BATCH", errorCode, errorMessage)
|
||||||
params[0] = "BATCH"
|
|
||||||
copy(params[1:], failParams)
|
|
||||||
rb.Add(nil, server.name, "FAIL", params...)
|
|
||||||
}
|
}
|
||||||
rb.session.EndMultilineBatch("")
|
rb.session.EndMultilineBatch("")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if batchTag != rb.session.batch.label {
|
if batchTag != rb.session.batch.label {
|
||||||
failParams = []string{"MULTILINE_INVALID", client.t("Incorrect batch tag sent")}
|
errorCode, errorMessage = "MULTILINE_INVALID", client.t("Incorrect batch tag sent")
|
||||||
return
|
return
|
||||||
} else if len(msg.Params) < 2 {
|
} else if len(msg.Params) < 2 {
|
||||||
failParams = []string{"MULTILINE_INVALID", client.t("Invalid multiline batch")}
|
errorCode, errorMessage = "MULTILINE_INVALID", client.t("Invalid multiline batch")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rb.session.batch.command = msg.Command
|
rb.session.batch.command = msg.Command
|
||||||
isConcat, _ := msg.GetTag(caps.MultilineConcatTag)
|
isConcat, _ := msg.GetTag(caps.MultilineConcatTag)
|
||||||
if isConcat && len(msg.Params[1]) == 0 {
|
if isConcat && len(msg.Params[1]) == 0 {
|
||||||
failParams = []string{"MULTILINE_INVALID", client.t("Cannot send a blank line with the multiline concat tag")}
|
errorCode, errorMessage = "MULTILINE_INVALID", client.t("Cannot send a blank line with the multiline concat tag")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !isConcat && len(rb.session.batch.message.Split) != 0 {
|
if !isConcat && len(rb.session.batch.message.Split) != 0 {
|
||||||
rb.session.batch.lenBytes++ // bill for the newline
|
rb.session.batch.lenBytes++ // bill for the newline
|
||||||
}
|
}
|
||||||
rb.session.batch.message.Append(msg.Params[1], isConcat, msg.ClientOnlyTags())
|
rb.session.batch.message.Append(msg.Params[1], isConcat)
|
||||||
rb.session.batch.lenBytes += len(msg.Params[1])
|
rb.session.batch.lenBytes += len(msg.Params[1])
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
if config.Limits.Multiline.MaxBytes < rb.session.batch.lenBytes {
|
if config.Limits.Multiline.MaxBytes < rb.session.batch.lenBytes {
|
||||||
failParams = []string{
|
errorCode, errorMessage = "MULTILINE_MAX_BYTES", strconv.Itoa(config.Limits.Multiline.MaxBytes)
|
||||||
"MULTILINE_MAX_BYTES",
|
|
||||||
strconv.Itoa(config.Limits.Multiline.MaxBytes),
|
|
||||||
fmt.Sprintf(client.t("Multiline batch byte limit %d exceeded"), config.Limits.Multiline.MaxBytes),
|
|
||||||
}
|
|
||||||
} else if config.Limits.Multiline.MaxLines != 0 && config.Limits.Multiline.MaxLines < rb.session.batch.message.LenLines() {
|
} else if config.Limits.Multiline.MaxLines != 0 && config.Limits.Multiline.MaxLines < rb.session.batch.message.LenLines() {
|
||||||
failParams = []string{
|
errorCode, errorMessage = "MULTILINE_MAX_LINES", strconv.Itoa(config.Limits.Multiline.MaxLines)
|
||||||
"MULTILINE_MAX_LINES",
|
|
||||||
strconv.Itoa(config.Limits.Multiline.MaxLines),
|
|
||||||
fmt.Sprintf(client.t("Multiline batch line limit %d exceeded"), config.Limits.Multiline.MaxLines),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2350,53 +2232,11 @@ func messageHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp
|
||||||
|
|
||||||
// each target gets distinct msgids
|
// each target gets distinct msgids
|
||||||
splitMsg := utils.MakeMessage(message)
|
splitMsg := utils.MakeMessage(message)
|
||||||
signatures := server.GenerateImagorSignaturesFromMessage(&msg)
|
|
||||||
if len(signatures) > 0 {
|
|
||||||
if clientOnlyTags == nil {
|
|
||||||
clientOnlyTags = make(map[string]string)
|
|
||||||
}
|
|
||||||
clientOnlyTags["signatures"] = signatures
|
|
||||||
}
|
|
||||||
dispatchMessageToTarget(client, clientOnlyTags, histType, msg.Command, targetString, splitMsg, rb)
|
dispatchMessageToTarget(client, clientOnlyTags, histType, msg.Command, targetString, splitMsg, rb)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not really sure how to do this in Go
|
|
||||||
var endChars = map[int32]bool{
|
|
||||||
' ': true,
|
|
||||||
'@': true,
|
|
||||||
':': true,
|
|
||||||
'!': true,
|
|
||||||
'?': true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectMentions(message string) (mentions []string) {
|
|
||||||
buf := ""
|
|
||||||
mentions = []string{}
|
|
||||||
working := false
|
|
||||||
for _, char := range message {
|
|
||||||
if char == '@' {
|
|
||||||
working = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !working {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, stop := endChars[char]; stop {
|
|
||||||
working = false
|
|
||||||
mentions = append(mentions, buf)
|
|
||||||
buf = ""
|
|
||||||
} else {
|
|
||||||
buf += string(char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(buf) != 0 {
|
|
||||||
mentions = append(mentions, buf)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func dispatchMessageToTarget(client *Client, tags map[string]string, histType history.ItemType, command, target string, message utils.SplitMessage, rb *ResponseBuffer) {
|
func dispatchMessageToTarget(client *Client, tags map[string]string, histType history.ItemType, command, target string, message utils.SplitMessage, rb *ResponseBuffer) {
|
||||||
server := client.server
|
server := client.server
|
||||||
|
|
||||||
|
|
@ -2413,15 +2253,7 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// This likely isn't that great for performance. Should figure out some way to deal with this at some point
|
|
||||||
mentions := detectMentions(message.Message)
|
|
||||||
channel.SendSplitMessage(command, lowestPrefix, tags, client, message, rb)
|
channel.SendSplitMessage(command, lowestPrefix, tags, client, message, rb)
|
||||||
for _, mention := range mentions {
|
|
||||||
user := client.server.clients.Get(mention)
|
|
||||||
if user != nil {
|
|
||||||
user.RedisBroadcast("MENTION", channel.Name(), message.Msgid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if target[0] == '$' && len(target) > 2 && client.Oper().HasRoleCapab("massmessage") {
|
} else if target[0] == '$' && len(target) > 2 && client.Oper().HasRoleCapab("massmessage") {
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
matcher, err := utils.CompileGlob(target[2:], false)
|
matcher, err := utils.CompileGlob(target[2:], false)
|
||||||
|
|
@ -2444,7 +2276,6 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// PMs
|
|
||||||
lowercaseTarget := strings.ToLower(target)
|
lowercaseTarget := strings.ToLower(target)
|
||||||
service, isService := ErgoServices[lowercaseTarget]
|
service, isService := ErgoServices[lowercaseTarget]
|
||||||
_, isZNC := zncHandlers[lowercaseTarget]
|
_, isZNC := zncHandlers[lowercaseTarget]
|
||||||
|
|
@ -2544,13 +2375,8 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||||
Type: histType,
|
Type: histType,
|
||||||
Message: message,
|
Message: message,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
Target: user.Account(),
|
|
||||||
Account: client.Account(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.addHistoryItem(user, item, &details, &tDetails, config)
|
client.addHistoryItem(user, item, &details, &tDetails, config)
|
||||||
user.RedisBroadcast("MENTION", user.NickCasefolded(), message.Msgid)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2827,97 +2653,6 @@ fail:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// REDACT <target> <targetmsgid> [:<reason>]
|
|
||||||
func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
|
||||||
target := msg.Params[0]
|
|
||||||
targetmsgid := msg.Params[1]
|
|
||||||
//clientOnlyTags := msg.ClientOnlyTags()
|
|
||||||
var reason string
|
|
||||||
if len(msg.Params) > 2 {
|
|
||||||
reason = msg.Params[2]
|
|
||||||
}
|
|
||||||
var members []*Client // members of a channel, or both parties of a PM
|
|
||||||
var canDelete CanDelete
|
|
||||||
|
|
||||||
msgid := utils.GenerateMessageIdStr()
|
|
||||||
time := time.Now().UTC().Round(0)
|
|
||||||
details := client.Details()
|
|
||||||
isBot := client.HasMode(modes.Bot)
|
|
||||||
|
|
||||||
if target[0] == '#' {
|
|
||||||
channel := server.channels.Get(target)
|
|
||||||
if channel == nil {
|
|
||||||
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
members = channel.Members()
|
|
||||||
canDelete = deletionPolicy(server, client, target)
|
|
||||||
} else {
|
|
||||||
targetClient := server.clients.Get(target)
|
|
||||||
if targetClient == nil {
|
|
||||||
rb.Add(nil, server.name, ERR_NOSUCHNICK, client.Nick(), target, "No such nick")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
members = []*Client{client, targetClient}
|
|
||||||
canDelete = canDeleteSelf
|
|
||||||
}
|
|
||||||
|
|
||||||
if canDelete == canDeleteNone {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("You are not authorized to delete messages"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
account := "*"
|
|
||||||
if canDelete == canDeleteSelf {
|
|
||||||
account = client.account
|
|
||||||
if account == "*" {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("You are not authorized to delete this message"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := server.DeleteMessage(target, targetmsgid, account)
|
|
||||||
if err == errNoop {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "UNKNOWN_MSGID", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("This message does not exist or is too old"))
|
|
||||||
return false
|
|
||||||
} else if err != nil {
|
|
||||||
isOper := client.HasRoleCapabs("history")
|
|
||||||
if isOper {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
|
||||||
} else {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("Could not delete message"))
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if target[0] != '#' {
|
|
||||||
// If this is a PM, we just removed the message from the buffer of the other party;
|
|
||||||
// now we have to remove it from the buffer of the client who sent the REDACT command
|
|
||||||
err := server.DeleteMessage(client.Nick(), targetmsgid, account)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
client.server.logger.Error("internal", fmt.Sprintf("Private message %s is not deletable by %s from their own buffer's even though we just deleted it from %s's. This is a bug, please report it in details.", targetmsgid, client.Nick(), target), client.Nick())
|
|
||||||
isOper := client.HasRoleCapabs("history")
|
|
||||||
if isOper {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
|
||||||
} else {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("Error deleting message"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, member := range members {
|
|
||||||
for _, session := range member.Sessions() {
|
|
||||||
if session.capabilities.Has(caps.MessageRedaction) {
|
|
||||||
session.sendFromClientInternal(false, time, msgid, details.nickMask, details.accountName, isBot, nil, "REDACT", target, targetmsgid, reason)
|
|
||||||
} else {
|
|
||||||
// If we wanted to send a fallback to clients which do not support
|
|
||||||
// draft/message-redaction, we would do it from here.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func reportPersistenceStatus(client *Client, rb *ResponseBuffer, broadcast bool) {
|
func reportPersistenceStatus(client *Client, rb *ResponseBuffer, broadcast bool) {
|
||||||
settings := client.AccountSettings()
|
settings := client.AccountSettings()
|
||||||
serverSetting := client.server.Config().Accounts.Multiclient.AlwaysOn
|
serverSetting := client.server.Config().Accounts.Multiclient.AlwaysOn
|
||||||
|
|
@ -2982,7 +2717,7 @@ func registerHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
|
||||||
case "*", accountName:
|
case "*", accountName:
|
||||||
// ok
|
// ok
|
||||||
default:
|
default:
|
||||||
rb.Add(nil, server.name, "FAIL", "REGISTER", "ACCOUNT_NAME_MUST_BE_NICK", utils.SafeErrorParam(msg.Params[0]), client.t("You may only register your nickname as your account name"))
|
rb.Add(nil, server.name, "FAIL", "REGISTER", "ACCOUNTNAME_MUST_BE_NICK", utils.SafeErrorParam(msg.Params[0]), client.t("You may only register your nickname as your account name"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3152,7 +2887,7 @@ func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
||||||
// TODO all operators should get a notice of some kind here
|
// TODO all operators should get a notice of some kind here
|
||||||
rb.Notice(client.t("Rehash complete"))
|
rb.Notice(client.t("Rehash complete"))
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, nick, "REHASH", ircutils.SanitizeText(err.Error(), 350))
|
rb.Add(nil, server.name, ERR_UNKNOWNERROR, nick, "REHASH", err.Error())
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -3218,8 +2953,6 @@ func relaymsgHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
|
||||||
Type: history.Privmsg,
|
Type: history.Privmsg,
|
||||||
Message: message,
|
Message: message,
|
||||||
Nick: nuh,
|
Nick: nuh,
|
||||||
Target: channel.NameCasefolded(),
|
|
||||||
Account: "$RELAYMSG",
|
|
||||||
}, "")
|
}, "")
|
||||||
|
|
||||||
// 3 possibilities for tags:
|
// 3 possibilities for tags:
|
||||||
|
|
@ -3335,10 +3068,8 @@ func renameHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
||||||
targetRb.Add(nil, targetPrefix, "JOIN", newName)
|
targetRb.Add(nil, targetPrefix, "JOIN", newName)
|
||||||
}
|
}
|
||||||
channel.SendTopic(mcl, targetRb, false)
|
channel.SendTopic(mcl, targetRb, false)
|
||||||
if !targetRb.session.capabilities.Has(caps.NoImplicitNames) {
|
|
||||||
channel.Names(mcl, targetRb)
|
channel.Names(mcl, targetRb)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if mcl != client {
|
if mcl != client {
|
||||||
targetRb.Send(false)
|
targetRb.Send(false)
|
||||||
}
|
}
|
||||||
|
|
@ -3502,10 +3233,6 @@ func userHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||||
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), "USER", client.t("Not enough parameters"))
|
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), "USER", client.t("Not enough parameters"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
config := server.Config()
|
|
||||||
if config.Limits.RealnameLen > 0 && len(realname) > config.Limits.RealnameLen {
|
|
||||||
realname = ircmsg.TruncateUTF8Safe(realname, config.Limits.RealnameLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
// #843: we accept either: `USER user:pass@clientid` or `USER user@clientid`
|
// #843: we accept either: `USER user:pass@clientid` or `USER user@clientid`
|
||||||
if strudelIndex := strings.IndexByte(username, '@'); strudelIndex != -1 {
|
if strudelIndex := strings.IndexByte(username, '@'); strudelIndex != -1 {
|
||||||
|
|
@ -3640,9 +3367,8 @@ func webircHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config := server.Config()
|
|
||||||
givenPassword := []byte(msg.Params[0])
|
givenPassword := []byte(msg.Params[0])
|
||||||
for _, info := range config.Server.WebIRC {
|
for _, info := range server.Config().Server.WebIRC {
|
||||||
if utils.IPInNets(client.realIP, info.allowedNets) {
|
if utils.IPInNets(client.realIP, info.allowedNets) {
|
||||||
// confirm password and/or fingerprint
|
// confirm password and/or fingerprint
|
||||||
if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
|
if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
|
||||||
|
|
@ -3652,23 +3378,11 @@ func webircHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
candidateIP := msg.Params[3]
|
err, quitMsg := client.ApplyProxiedIP(rb.session, net.ParseIP(msg.Params[3]), secure)
|
||||||
err, quitMsg := client.ApplyProxiedIP(rb.session, net.ParseIP(candidateIP), secure)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.Quit(quitMsg, rb.session)
|
client.Quit(quitMsg, rb.session)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
if info.AcceptHostname {
|
|
||||||
candidateHostname := msg.Params[2]
|
|
||||||
if candidateHostname != candidateIP {
|
|
||||||
if utils.IsHostname(candidateHostname) {
|
|
||||||
rb.session.rawHostname = candidateHostname
|
|
||||||
} else {
|
|
||||||
// log this at debug level since it may be spammy
|
|
||||||
server.logger.Debug("internal", "invalid hostname from WEBIRC", candidateHostname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4085,33 +3799,6 @@ func zncHandler(server *Server, client *Client, msg ircmsg.Message, rb *Response
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// REACT <msgid> :<reaction>
|
|
||||||
func reactHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
|
||||||
// This directly uses SQL stuff, since it's targeted at CEF, which requires a DB.
|
|
||||||
_, _, target, sender, _, pm, err := server.historyDB.GetMessage(msg.Params[0])
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var operation string
|
|
||||||
if server.historyDB.HasReactionFromUser(msg.Params[0], client.AccountName(), msg.Params[1]) {
|
|
||||||
server.historyDB.DeleteReaction(msg.Params[0], client.AccountName(), msg.Params[1])
|
|
||||||
operation = "DEL"
|
|
||||||
} else {
|
|
||||||
server.historyDB.AddReaction(msg.Params[0], client.AccountName(), msg.Params[1])
|
|
||||||
operation = "ADD"
|
|
||||||
}
|
|
||||||
|
|
||||||
if pm {
|
|
||||||
server.clients.Get(target).Send(nil, client.NickMaskString(), "REACT", operation, msg.Params[0], msg.Params[1])
|
|
||||||
server.clients.Get(sender).Send(nil, client.NickMaskString(), "REACT", operation, msg.Params[0], msg.Params[1])
|
|
||||||
} else {
|
|
||||||
server.channels.Get(target).BroadcastFrom(client.NickMaskString(), "REACT", operation, msg.Params[0], msg.Params[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// fake handler for unknown commands
|
// fake handler for unknown commands
|
||||||
func unknownCommandHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func unknownCommandHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
var message string
|
var message string
|
||||||
|
|
|
||||||
17
irc/help.go
17
irc/help.go
|
|
@ -259,11 +259,6 @@ appropriate channel privs.`,
|
||||||
text: `ISON <nickname>{ <nickname>}
|
text: `ISON <nickname>{ <nickname>}
|
||||||
|
|
||||||
Returns whether the given nicks exist on the network.`,
|
Returns whether the given nicks exist on the network.`,
|
||||||
},
|
|
||||||
"isupport": {
|
|
||||||
text: `ISUPPORT
|
|
||||||
|
|
||||||
Returns RPL_ISUPPORT lines describing the server's capabilities.`,
|
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
text: `JOIN <channel>{,<channel>} [<key>{,<key>}]
|
text: `JOIN <channel>{,<channel>} [<key>{,<key>}]
|
||||||
|
|
@ -440,12 +435,6 @@ Replies to a PING. Used to check link connectivity.`,
|
||||||
text: `PRIVMSG <target>{,<target>} <text to be sent>
|
text: `PRIVMSG <target>{,<target>} <text to be sent>
|
||||||
|
|
||||||
Sends the text to the given targets as a PRIVMSG.`,
|
Sends the text to the given targets as a PRIVMSG.`,
|
||||||
},
|
|
||||||
"redact": {
|
|
||||||
text: `REDACT <target> <targetmsgid> [<reason>]
|
|
||||||
|
|
||||||
Removes the message of the target msgid from the chat history of a channel
|
|
||||||
or target user.`,
|
|
||||||
},
|
},
|
||||||
"relaymsg": {
|
"relaymsg": {
|
||||||
text: `RELAYMSG <channel> <spoofed nick> :<message>
|
text: `RELAYMSG <channel> <spoofed nick> :<message>
|
||||||
|
|
@ -634,12 +623,6 @@ for direct use by end users.`,
|
||||||
duplicate: true,
|
duplicate: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
"react": {
|
|
||||||
text: `REACT <msgid> <reaction>
|
|
||||||
|
|
||||||
Toggles a reaction to a message. CEF-specific`,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Informational
|
// Informational
|
||||||
"modes": {
|
"modes": {
|
||||||
textGenerator: modesTextGenerator,
|
textGenerator: modesTextGenerator,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
package history
|
package history
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -32,20 +31,13 @@ const (
|
||||||
initialAutoSize = 32
|
initialAutoSize = 32
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reaction struct {
|
|
||||||
Name string
|
|
||||||
Total int
|
|
||||||
SampleUsers []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data
|
// Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Type ItemType
|
Type ItemType
|
||||||
|
|
||||||
Nick string
|
Nick string
|
||||||
// this is the uncasefolded account name, if there's no account it should be set to "*"
|
// this is the uncasefolded account name, if there's no account it should be set to "*"
|
||||||
// in cef, this is always set, at least in theory. cant wait for bugs toc rop up
|
AccountName string
|
||||||
Account string
|
|
||||||
// for non-privmsg items, we may stuff some other data in here
|
// for non-privmsg items, we may stuff some other data in here
|
||||||
Message utils.SplitMessage
|
Message utils.SplitMessage
|
||||||
Tags map[string]string
|
Tags map[string]string
|
||||||
|
|
@ -53,10 +45,8 @@ type Item struct {
|
||||||
// for a DM, this is the casefolded nickname of the other party (whether this is
|
// for a DM, this is the casefolded nickname of the other party (whether this is
|
||||||
// an incoming or outgoing message). this lets us emulate the "query buffer" functionality
|
// an incoming or outgoing message). this lets us emulate the "query buffer" functionality
|
||||||
// required by CHATHISTORY:
|
// required by CHATHISTORY:
|
||||||
Target string `json:"Target"`
|
CfCorrespondent string `json:"CfCorrespondent,omitempty"`
|
||||||
IsBot bool `json:"IsBot,omitempty"`
|
IsBot bool `json:"IsBot,omitempty"`
|
||||||
|
|
||||||
Reactions []Reaction
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasMsgid tests whether a message has the message id `msgid`.
|
// HasMsgid tests whether a message has the message id `msgid`.
|
||||||
|
|
@ -165,7 +155,7 @@ func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Pr
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if !ascending {
|
if !ascending {
|
||||||
slices.Reverse(results)
|
utils.ReverseSlice(results)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -222,10 +212,10 @@ func (list *Buffer) allCorrespondents() (results []TargetListing) {
|
||||||
stop := list.start
|
stop := list.start
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if !seen.Has(list.buffer[pos].Target) {
|
if !seen.Has(list.buffer[pos].CfCorrespondent) {
|
||||||
seen.Add(list.buffer[pos].Target)
|
seen.Add(list.buffer[pos].CfCorrespondent)
|
||||||
results = append(results, TargetListing{
|
results = append(results, TargetListing{
|
||||||
CfName: list.buffer[pos].Target,
|
CfName: list.buffer[pos].CfCorrespondent,
|
||||||
Time: list.buffer[pos].Message.Time,
|
Time: list.buffer[pos].Message.Time,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -272,7 +262,7 @@ func (list *Buffer) listCorrespondents(start, end Selector, cutoff time.Time, li
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ascending {
|
if !ascending {
|
||||||
slices.Reverse(results)
|
utils.ReverseSlice(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
@ -290,7 +280,7 @@ func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequenc
|
||||||
var pred Predicate
|
var pred Predicate
|
||||||
if correspondent != "" {
|
if correspondent != "" {
|
||||||
pred = func(item *Item) bool {
|
pred = func(item *Item) bool {
|
||||||
return item.Target == correspondent
|
return item.CfCorrespondent == correspondent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &bufferSequence{
|
return &bufferSequence{
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@
|
||||||
package history
|
package history
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TargetListing struct {
|
type TargetListing struct {
|
||||||
|
|
@ -34,8 +35,8 @@ func MergeTargets(base []TargetListing, extra []TargetListing, start, end time.T
|
||||||
results = make([]TargetListing, 0, prealloc)
|
results = make([]TargetListing, 0, prealloc)
|
||||||
|
|
||||||
if !ascending {
|
if !ascending {
|
||||||
slices.Reverse(base)
|
utils.ReverseSlice(base)
|
||||||
slices.Reverse(extra)
|
utils.ReverseSlice(extra)
|
||||||
}
|
}
|
||||||
|
|
||||||
for len(results) < limit {
|
for len(results) < limit {
|
||||||
|
|
@ -65,7 +66,7 @@ func MergeTargets(base []TargetListing, extra []TargetListing, start, end time.T
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ascending {
|
if !ascending {
|
||||||
slices.Reverse(results)
|
utils.ReverseSlice(results)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,6 @@ import (
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CanDelete uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
canDeleteAny CanDelete = iota // User is allowed to delete any message (for a given channel/PM)
|
|
||||||
canDeleteSelf // User is allowed to delete their own messages (ditto)
|
|
||||||
canDeleteNone // User is not allowed to delete any message (ditto)
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
histservHelp = `HistServ provides commands related to history.`
|
histservHelp = `HistServ provides commands related to history.`
|
||||||
)
|
)
|
||||||
|
|
@ -100,53 +92,33 @@ func histservForgetHandler(service *ircService, server *Server, client *Client,
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Enqueued account %s for message deletion"), accountName))
|
service.Notice(rb, fmt.Sprintf(client.t("Enqueued account %s for message deletion"), accountName))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns:
|
|
||||||
//
|
|
||||||
// 1. `canDeleteAny` if the client allowed to delete other users' messages from the target, ie.:
|
|
||||||
// - the client is a channel operator, or
|
|
||||||
// - the client is an operator with "history" capability
|
|
||||||
//
|
|
||||||
// 2. `canDeleteSelf` if the client is allowed to delete their own messages from the target
|
|
||||||
// 3. `canDeleteNone` otherwise
|
|
||||||
func deletionPolicy(server *Server, client *Client, target string) CanDelete {
|
|
||||||
isOper := client.HasRoleCapabs("history")
|
|
||||||
if isOper {
|
|
||||||
return canDeleteAny
|
|
||||||
} else {
|
|
||||||
if server.Config().History.Retention.AllowIndividualDelete {
|
|
||||||
channel := server.channels.Get(target)
|
|
||||||
if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) {
|
|
||||||
return canDeleteAny
|
|
||||||
} else {
|
|
||||||
return canDeleteSelf
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return canDeleteNone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func histservDeleteHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func histservDeleteHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
target, msgid := params[0], params[1] // Fix #1881 2 params are required
|
target, msgid := params[0], params[1] // Fix #1881 2 params are required
|
||||||
|
|
||||||
canDelete := deletionPolicy(server, client, target)
|
// operators can delete; if individual delete is allowed, a chanop or
|
||||||
|
// the message author can delete
|
||||||
accountName := "*"
|
accountName := "*"
|
||||||
if canDelete == canDeleteNone {
|
isChanop := false
|
||||||
service.Notice(rb, client.t("Insufficient privileges"))
|
isOper := client.HasRoleCapabs("history")
|
||||||
return
|
if !isOper {
|
||||||
} else if canDelete == canDeleteSelf {
|
if server.Config().History.Retention.AllowIndividualDelete {
|
||||||
|
channel := server.channels.Get(target)
|
||||||
|
if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) {
|
||||||
|
isChanop = true
|
||||||
|
} else {
|
||||||
accountName = client.AccountName()
|
accountName = client.AccountName()
|
||||||
if accountName == "*" {
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isOper && !isChanop && accountName == "*" {
|
||||||
service.Notice(rb, client.t("Insufficient privileges"))
|
service.Notice(rb, client.t("Insufficient privileges"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
err := server.DeleteMessage(target, msgid, accountName)
|
err := server.DeleteMessage(target, msgid, accountName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
service.Notice(rb, client.t("Successfully deleted message"))
|
service.Notice(rb, client.t("Successfully deleted message"))
|
||||||
} else {
|
} else {
|
||||||
isOper := client.HasRoleCapabs("history")
|
|
||||||
if isOper {
|
if isOper {
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
service.Notice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,6 @@ func hsSetCloakSecretHandler(service *ircService, server *Server, client *Client
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
|
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
StoreCloakSecret(server.dstore, secret)
|
StoreCloakSecret(server.store, secret)
|
||||||
service.Notice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
|
service.Notice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/bunt"
|
|
||||||
"github.com/ergochat/ergo/irc/datastore"
|
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -24,7 +20,7 @@ const (
|
||||||
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
|
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
|
||||||
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
|
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
|
||||||
// db of the hardcoded version)
|
// db of the hardcoded version)
|
||||||
importDBSchemaVersion = 23
|
importDBSchemaVersion = 22
|
||||||
)
|
)
|
||||||
|
|
||||||
type userImport struct {
|
type userImport struct {
|
||||||
|
|
@ -58,8 +54,8 @@ type databaseImport struct {
|
||||||
Channels map[string]channelImport
|
Channels map[string]channelImport
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result map[string]modes.Mode, err error) {
|
func serializeAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result []byte, err error) {
|
||||||
result = make(map[string]modes.Mode)
|
processed := make(map[string]int, len(raw))
|
||||||
for accountName, mode := range raw {
|
for accountName, mode := range raw {
|
||||||
if len(mode) != 1 {
|
if len(mode) != 1 {
|
||||||
return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
|
return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
|
||||||
|
|
@ -68,9 +64,10 @@ func convertAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]
|
||||||
if err != nil || !validCfUsernames.Has(cfname) {
|
if err != nil || !validCfUsernames.Has(cfname) {
|
||||||
log.Printf("skipping invalid amode recipient %s\n", accountName)
|
log.Printf("skipping invalid amode recipient %s\n", accountName)
|
||||||
} else {
|
} else {
|
||||||
result[cfname] = modes.Mode(mode[0])
|
processed[cfname] = int(mode[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
result, err = json.Marshal(processed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,9 +147,8 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
|
||||||
cfUsernames.Add(cfUsername)
|
cfUsernames.Add(cfUsername)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO fix this:
|
|
||||||
for chname, chInfo := range dbImport.Channels {
|
for chname, chInfo := range dbImport.Channels {
|
||||||
_, err := CasefoldChannel(chname)
|
cfchname, err := CasefoldChannel(chname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("invalid channel name %s: %v", chname, err)
|
log.Printf("invalid channel name %s: %v", chname, err)
|
||||||
continue
|
continue
|
||||||
|
|
@ -162,42 +158,43 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
|
||||||
log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
|
log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var regInfo RegisteredChannel
|
tx.Set(fmt.Sprintf(keyChannelExists, cfchname), "1", nil)
|
||||||
regInfo.Name = chname
|
tx.Set(fmt.Sprintf(keyChannelName, cfchname), chname, nil)
|
||||||
regInfo.UUID = utils.GenerateUUIDv4()
|
tx.Set(fmt.Sprintf(keyChannelRegTime, cfchname), strconv.FormatInt(chInfo.RegisteredAt, 10), nil)
|
||||||
regInfo.Founder = cffounder
|
tx.Set(fmt.Sprintf(keyChannelFounder, cfchname), cffounder, nil)
|
||||||
regInfo.RegisteredAt = time.Unix(0, chInfo.RegisteredAt).UTC()
|
accountChannelsKey := fmt.Sprintf(keyAccountChannels, cffounder)
|
||||||
if chInfo.Topic != "" {
|
founderChannels, fcErr := tx.Get(accountChannelsKey)
|
||||||
regInfo.Topic = chInfo.Topic
|
if fcErr != nil || founderChannels == "" {
|
||||||
regInfo.TopicSetBy = chInfo.TopicSetBy
|
founderChannels = cfchname
|
||||||
regInfo.TopicSetTime = time.Unix(0, chInfo.TopicSetAt).UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(chInfo.Amode) != 0 {
|
|
||||||
m, err := convertAmodes(chInfo.Amode, cfUsernames)
|
|
||||||
if err == nil {
|
|
||||||
regInfo.AccountToUMode = m
|
|
||||||
} else {
|
} else {
|
||||||
log.Printf("couldn't process amodes for %s: %v", chname, err)
|
founderChannels = fmt.Sprintf("%s,%s", founderChannels, cfchname)
|
||||||
|
}
|
||||||
|
tx.Set(accountChannelsKey, founderChannels, nil)
|
||||||
|
if chInfo.Topic != "" {
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelTopic, cfchname), chInfo.Topic, nil)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, cfchname), strconv.FormatInt(chInfo.TopicSetAt, 10), nil)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, cfchname), chInfo.TopicSetBy, nil)
|
||||||
|
}
|
||||||
|
if len(chInfo.Amode) != 0 {
|
||||||
|
m, err := serializeAmodes(chInfo.Amode, cfUsernames)
|
||||||
|
if err == nil {
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil)
|
||||||
|
} else {
|
||||||
|
log.Printf("couldn't serialize amodes for %s: %v", chname, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, mode := range chInfo.Modes {
|
tx.Set(fmt.Sprintf(keyChannelModes, cfchname), chInfo.Modes, nil)
|
||||||
regInfo.Modes = append(regInfo.Modes, modes.Mode(mode))
|
if chInfo.Key != "" {
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelPassword, cfchname), chInfo.Key, nil)
|
||||||
}
|
}
|
||||||
regInfo.Key = chInfo.Key
|
|
||||||
if chInfo.Limit > 0 {
|
if chInfo.Limit > 0 {
|
||||||
regInfo.UserLimit = chInfo.Limit
|
tx.Set(fmt.Sprintf(keyChannelUserLimit, cfchname), strconv.Itoa(chInfo.Limit), nil)
|
||||||
}
|
}
|
||||||
if chInfo.Forward != "" {
|
if chInfo.Forward != "" {
|
||||||
if _, err := CasefoldChannel(chInfo.Forward); err == nil {
|
if _, err := CasefoldChannel(chInfo.Forward); err == nil {
|
||||||
regInfo.Forward = chInfo.Forward
|
tx.Set(fmt.Sprintf(keyChannelForward, cfchname), chInfo.Forward, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if j, err := json.Marshal(regInfo); err == nil {
|
|
||||||
tx.Set(bunt.BuntKey(datastore.TableChannels, regInfo.UUID), string(j), nil)
|
|
||||||
} else {
|
|
||||||
log.Printf("couldn't serialize channel %s: %v", chname, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if warnSkeletons {
|
if warnSkeletons {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
|
@ -94,25 +93,21 @@ func (cc *IRCStreamConn) Close() (err error) {
|
||||||
// IRCWSConn is an IRCConn over a websocket.
|
// IRCWSConn is an IRCConn over a websocket.
|
||||||
type IRCWSConn struct {
|
type IRCWSConn struct {
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
buf []byte
|
|
||||||
binary bool
|
binary bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIRCWSConn(conn *websocket.Conn) *IRCWSConn {
|
func NewIRCWSConn(conn *websocket.Conn) IRCWSConn {
|
||||||
return &IRCWSConn{
|
binary := conn.Subprotocol() == "binary.ircv3.net"
|
||||||
conn: conn,
|
return IRCWSConn{conn: conn, binary: binary}
|
||||||
binary: conn.Subprotocol() == "binary.ircv3.net",
|
|
||||||
buf: make([]byte, initialBufferSize),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *IRCWSConn) UnderlyingConn() *utils.WrappedConn {
|
func (wc IRCWSConn) UnderlyingConn() *utils.WrappedConn {
|
||||||
// just assume that the type is OK
|
// just assume that the type is OK
|
||||||
wConn, _ := wc.conn.UnderlyingConn().(*utils.WrappedConn)
|
wConn, _ := wc.conn.UnderlyingConn().(*utils.WrappedConn)
|
||||||
return wConn
|
return wConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *IRCWSConn) WriteLine(buf []byte) (err error) {
|
func (wc IRCWSConn) WriteLine(buf []byte) (err error) {
|
||||||
buf = bytes.TrimSuffix(buf, crlf)
|
buf = bytes.TrimSuffix(buf, crlf)
|
||||||
// #1483: if we have websockets at all, then we're enforcing utf8
|
// #1483: if we have websockets at all, then we're enforcing utf8
|
||||||
messageType := websocket.TextMessage
|
messageType := websocket.TextMessage
|
||||||
|
|
@ -122,7 +117,7 @@ func (wc *IRCWSConn) WriteLine(buf []byte) (err error) {
|
||||||
return wc.conn.WriteMessage(messageType, buf)
|
return wc.conn.WriteMessage(messageType, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *IRCWSConn) WriteLines(buffers [][]byte) (err error) {
|
func (wc IRCWSConn) WriteLines(buffers [][]byte) (err error) {
|
||||||
for _, buf := range buffers {
|
for _, buf := range buffers {
|
||||||
err = wc.WriteLine(buf)
|
err = wc.WriteLine(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -132,47 +127,20 @@ func (wc *IRCWSConn) WriteLines(buffers [][]byte) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *IRCWSConn) ReadLine() (line []byte, err error) {
|
func (wc IRCWSConn) ReadLine() (line []byte, err error) {
|
||||||
_, reader, err := wc.conn.NextReader()
|
messageType, line, err := wc.conn.ReadMessage()
|
||||||
switch err {
|
if err == nil {
|
||||||
case nil:
|
if messageType == websocket.BinaryMessage && !utf8.Valid(line) {
|
||||||
// OK
|
|
||||||
case websocket.ErrReadLimit:
|
|
||||||
return line, ircreader.ErrReadQ
|
|
||||||
default:
|
|
||||||
return line, err
|
|
||||||
}
|
|
||||||
|
|
||||||
line, err = wc.readFull(reader)
|
|
||||||
switch err {
|
|
||||||
case io.ErrUnexpectedEOF, io.EOF:
|
|
||||||
// these are OK. io.ErrUnexpectedEOF is the good case:
|
|
||||||
// it means we read the full message and it consumed less than the full wc.buf
|
|
||||||
if !utf8.Valid(line) {
|
|
||||||
return line, errInvalidUtf8
|
return line, errInvalidUtf8
|
||||||
}
|
}
|
||||||
return line, nil
|
return line, nil
|
||||||
case nil, websocket.ErrReadLimit:
|
} else if err == websocket.ErrReadLimit {
|
||||||
// nil means we filled wc.buf without exhausting the reader:
|
|
||||||
return line, ircreader.ErrReadQ
|
return line, ircreader.ErrReadQ
|
||||||
default:
|
} else {
|
||||||
return line, err
|
return line, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wc *IRCWSConn) readFull(reader io.Reader) (line []byte, err error) {
|
func (wc IRCWSConn) Close() (err error) {
|
||||||
// XXX this is io.ReadFull with a single attempt to resize upwards
|
|
||||||
n, err := io.ReadFull(reader, wc.buf)
|
|
||||||
if err == nil && len(wc.buf) < maxReadQBytes() {
|
|
||||||
newBuf := make([]byte, maxReadQBytes())
|
|
||||||
copy(newBuf, wc.buf[:n])
|
|
||||||
wc.buf = newBuf
|
|
||||||
n2, err := io.ReadFull(reader, wc.buf[n:])
|
|
||||||
return wc.buf[:n+n2], err
|
|
||||||
}
|
|
||||||
return wc.buf[:n], err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wc *IRCWSConn) Close() (err error) {
|
|
||||||
return wc.conn.Close()
|
return wc.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,6 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxLastArgLength = 400
|
maxLastArgLength = 400
|
||||||
|
|
||||||
/* Modern: "As the maximum number of message parameters to any reply is 15,
|
|
||||||
the maximum number of RPL_ISUPPORT tokens that can be advertised is 13."
|
|
||||||
<nickname> [up to 13 parameters] <human-readable trailing>
|
|
||||||
*/
|
|
||||||
maxParameters = 13
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// List holds a list of ISUPPORT tokens
|
// List holds a list of ISUPPORT tokens
|
||||||
|
|
@ -101,7 +95,7 @@ func (il *List) GetDifference(newil *List) [][]string {
|
||||||
length += len(token)
|
length += len(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cache) == maxParameters || len(token)+length >= maxLastArgLength {
|
if len(cache) == 13 || len(token)+length >= maxLastArgLength {
|
||||||
replies = append(replies, cache)
|
replies = append(replies, cache)
|
||||||
cache = make([]string, 0)
|
cache = make([]string, 0)
|
||||||
length = 0
|
length = 0
|
||||||
|
|
@ -144,7 +138,7 @@ func (il *List) RegenerateCachedReply() (err error) {
|
||||||
length += len(token)
|
length += len(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cache) == maxParameters || len(token)+length >= maxLastArgLength {
|
if len(cache) == 13 || len(token)+length >= maxLastArgLength {
|
||||||
il.CachedReply = append(il.CachedReply, cache)
|
il.CachedReply = append(il.CachedReply, cache)
|
||||||
cache = make([]string, 0)
|
cache = make([]string, 0)
|
||||||
length = 0
|
length = 0
|
||||||
|
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
// Copyright (c) 2024 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package jwt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
jwt "github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrAuthDisabled = fmt.Errorf("JWT authentication is disabled")
|
|
||||||
ErrNoValidAccountClaim = fmt.Errorf("JWT token did not contain an acceptable account name claim")
|
|
||||||
)
|
|
||||||
|
|
||||||
// JWTAuthConfig is the config for Ergo to accept JWTs via draft/bearer
|
|
||||||
type JWTAuthConfig struct {
|
|
||||||
Enabled bool `yaml:"enabled"`
|
|
||||||
Autocreate bool `yaml:"autocreate"`
|
|
||||||
Tokens []JWTAuthTokenConfig `yaml:"tokens"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type JWTAuthTokenConfig struct {
|
|
||||||
Algorithm string `yaml:"algorithm"`
|
|
||||||
KeyString string `yaml:"key"`
|
|
||||||
KeyFile string `yaml:"key-file"`
|
|
||||||
key any
|
|
||||||
parser *jwt.Parser
|
|
||||||
AccountClaims []string `yaml:"account-claims"`
|
|
||||||
StripDomain string `yaml:"strip-domain"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JWTAuthConfig) Postprocess() error {
|
|
||||||
if !j.Enabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(j.Tokens) == 0 {
|
|
||||||
return fmt.Errorf("JWT authentication enabled, but no valid tokens defined")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range j.Tokens {
|
|
||||||
if err := j.Tokens[i].Postprocess(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JWTAuthTokenConfig) Postprocess() error {
|
|
||||||
keyBytes, err := j.keyBytes()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
j.Algorithm = strings.ToLower(j.Algorithm)
|
|
||||||
|
|
||||||
var methods []string
|
|
||||||
switch j.Algorithm {
|
|
||||||
case "hmac":
|
|
||||||
j.key = keyBytes
|
|
||||||
methods = []string{"HS256", "HS384", "HS512"}
|
|
||||||
case "rsa":
|
|
||||||
rsaKey, err := jwt.ParseRSAPublicKeyFromPEM(keyBytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
j.key = rsaKey
|
|
||||||
methods = []string{"RS256", "RS384", "RS512"}
|
|
||||||
case "eddsa":
|
|
||||||
eddsaKey, err := jwt.ParseEdPublicKeyFromPEM(keyBytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
j.key = eddsaKey
|
|
||||||
methods = []string{"EdDSA"}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid jwt algorithm: %s", j.Algorithm)
|
|
||||||
}
|
|
||||||
j.parser = jwt.NewParser(jwt.WithValidMethods(methods))
|
|
||||||
|
|
||||||
if len(j.AccountClaims) == 0 {
|
|
||||||
return fmt.Errorf("JWT auth enabled, but no account-claims specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
j.StripDomain = strings.ToLower(j.StripDomain)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JWTAuthConfig) Validate(t string) (accountName string, err error) {
|
|
||||||
if !j.Enabled || len(j.Tokens) == 0 {
|
|
||||||
return "", ErrAuthDisabled
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range j.Tokens {
|
|
||||||
accountName, err = j.Tokens[i].Validate(t)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JWTAuthTokenConfig) keyBytes() (result []byte, err error) {
|
|
||||||
if j.KeyFile != "" {
|
|
||||||
o, err := os.Open(j.KeyFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer o.Close()
|
|
||||||
return io.ReadAll(o)
|
|
||||||
}
|
|
||||||
if j.KeyString != "" {
|
|
||||||
return []byte(j.KeyString), nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("JWT auth enabled, but no JWT key specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
// implements jwt.Keyfunc
|
|
||||||
func (j *JWTAuthTokenConfig) keyFunc(_ *jwt.Token) (interface{}, error) {
|
|
||||||
return j.key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JWTAuthTokenConfig) Validate(t string) (accountName string, err error) {
|
|
||||||
token, err := j.parser.Parse(t, j.keyFunc)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
|
||||||
if !ok {
|
|
||||||
// impossible with Parse (as opposed to ParseWithClaims)
|
|
||||||
return "", fmt.Errorf("unexpected type from parsed token claims: %T", claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range j.AccountClaims {
|
|
||||||
if v, ok := claims[c]; ok {
|
|
||||||
if vstr, ok := v.(string); ok {
|
|
||||||
// validate and strip email addresses:
|
|
||||||
if idx := strings.IndexByte(vstr, '@'); idx != -1 {
|
|
||||||
suffix := vstr[idx+1:]
|
|
||||||
vstr = vstr[:idx]
|
|
||||||
if strings.ToLower(suffix) != j.StripDomain {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vstr, nil // success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", ErrNoValidAccountClaim
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
package jwt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
jwt "github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
rsaTestPubKey = `-----BEGIN PUBLIC KEY-----
|
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwhcCcXrfR/GmoPKxBi0H
|
|
||||||
cUl2pUl4acq2m3abFtMMoYTydJdEhgYWfsXuragyEIVkJU1ZnrgedW0QJUcANRGO
|
|
||||||
hP/B+MjBevDNsRXQECfhyjfzhz6KWZb4i7C2oImJuAjq/F4qGLdEGQDBpAzof8qv
|
|
||||||
9Zt5iN3GXY/EQtQVMFyR/7BPcbPLbHlOtzZ6tVEioXuUxQoai7x3Kc0jIcPWuyGa
|
|
||||||
Q04IvsgdaWO6oH4fhPfyVsmX37rYUn79zcqPHS4ieWM1KN9qc7W+/UJIeiwAStpJ
|
|
||||||
8gv+OSMrijRZGgQGCeOO5U59GGJC4mqUczB+JFvrlAIv0rggNpl+qalngosNxukB
|
|
||||||
uQIDAQAB
|
|
||||||
-----END PUBLIC KEY-----`
|
|
||||||
|
|
||||||
rsaTestPrivKey = `-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCFwJxet9H8aag
|
|
||||||
8rEGLQdxSXalSXhpyrabdpsW0wyhhPJ0l0SGBhZ+xe6tqDIQhWQlTVmeuB51bRAl
|
|
||||||
RwA1EY6E/8H4yMF68M2xFdAQJ+HKN/OHPopZlviLsLagiYm4COr8XioYt0QZAMGk
|
|
||||||
DOh/yq/1m3mI3cZdj8RC1BUwXJH/sE9xs8tseU63Nnq1USKhe5TFChqLvHcpzSMh
|
|
||||||
w9a7IZpDTgi+yB1pY7qgfh+E9/JWyZffuthSfv3Nyo8dLiJ5YzUo32pztb79Qkh6
|
|
||||||
LABK2knyC/45IyuKNFkaBAYJ447lTn0YYkLiapRzMH4kW+uUAi/SuCA2mX6pqWeC
|
|
||||||
iw3G6QG5AgMBAAECggEARaAnejoP2ykvE1G8e3Cv2M33x/eBQMI9m6uCmz9+qnqc
|
|
||||||
14JkTIfmjffHVXie7RpNAKys16lJE+rZ/eVoh6EStVdiaDLsZYP45evjRcho0Tgd
|
|
||||||
Hokq7FSiOMpd2V09kE1yrrHA/DjSLv38eTNAPIejc8IgaR7VyD6Is0iNiVnL7iLa
|
|
||||||
mj1zB6+dSeQ5ICYkrihb1gA+SvECsjLZ/5XESXEdHJvxhC0vLAdHmdQf3BPPlrGg
|
|
||||||
VHondxL5gt6MFykpOxTFA6f5JkSefhUR/2OcCDpMs6a5GUytjl3rA3aGT6v3CbnR
|
|
||||||
ykD6PzyC20EUADQYF2pmJfzbxyRqfNdbSJwQv5QQYQKBgQD4rFdvgZC97L7WhZ5T
|
|
||||||
axW8hRW2dH24GIqFT4ZnCg0suyMNshyGvDMuBfGvokN/yACmvsdE0/f57esar+ye
|
|
||||||
l9RC+CzGUch08Ke5WdqwACOCNDpx0kJcXKTuLIgkvthdla/oAQQ9T7OgEwDrvaR0
|
|
||||||
m8s/Z7Hb3hLD3xdOt6Xjrv/6xQKBgQDHzvbcIkhmWdvaPDT9NEu7psR/fxF5UjqU
|
|
||||||
Cca/bfHhySRQs3A1CF57pfwpUqAcSivNf7O+3NI62AKoyMDYv0ek2h6hGk6g5GJ1
|
|
||||||
SuXYfjcbkL6SWNV0InsgmzCjvxhyms83xZq7uMClEBvkiKVMdt6zFkwW9eRKtUuZ
|
|
||||||
pzVK5RfqZQKBgF5SME/xGw+O7su7ntQROAtrh1LPWKgtVs093sLSgzDGQoN9XWiV
|
|
||||||
lewNASEXMPcUy3pzvm2S4OoBnj1fISb+e9py+7i1aI1CgrvBIzvCsbU/TjPCBr21
|
|
||||||
vjFA3trhMHw+vJwJVqxSwNUkoCLKqcg5F5yTHllBIGj/A34uFlQIGrvpAoGAextm
|
|
||||||
d+1bhExbLBQqZdOh0cWHjjKBVqm2U93OKcYY4Q9oI5zbRqGYbUCwo9k3sxZz9JJ4
|
|
||||||
8eDmWsEaqlm+kA0SnFyTwJkP1wvAKhpykTf6xi4hbNP0+DACgu17Q3iLHJmLkQZc
|
|
||||||
Nss3TrwlI2KZzgnzXo4fZYotFWasZMhkCngqiw0CgYEAmz2D70RYEauUNE1+zLhS
|
|
||||||
6Ox5+PF/8Z0rZOlTghMTfqYcDJa+qQe9pJp7RPgilsgemqo0XtgLKz3ATE5FmMa4
|
|
||||||
HRRGXPkMNu6Hzz4Yk4eM/yJqckoEc8azV25myqQ+7QXTwZEvxVbtUWZtxfImGwq+
|
|
||||||
s/uzBKNwWf9UPTeIt+4JScg=
|
|
||||||
-----END PRIVATE KEY-----`
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestJWTBearerAuth(t *testing.T) {
|
|
||||||
j := JWTAuthConfig{
|
|
||||||
Enabled: true,
|
|
||||||
Tokens: []JWTAuthTokenConfig{
|
|
||||||
{
|
|
||||||
Algorithm: "rsa",
|
|
||||||
KeyString: rsaTestPubKey,
|
|
||||||
AccountClaims: []string{"preferred_username", "email"},
|
|
||||||
StripDomain: "example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := j.Postprocess(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fixed test vector signed with the RSA privkey:
|
|
||||||
token := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzbGluZ2FtbiJ9.caPZw2Dl4KZN-SErD5-WZB_lPPveHXaMCoUHxNebb94G9w3VaWDIRdngVU99JKx5nE_yRtpewkHHvXsQnNA_M63GBXGK7afXB8e-kV33QF3v9pXALMP5SzRwMgokyxas0RgHu4e4L0d7dn9o_nkdXp34GX3Pn1MVkUGBH6GdlbOdDHrs04pPQ0Qj-O2U0AIpnZq-X_GQs9ECJo4TlPKWR7Jlq5l9bS0dBnohea4FuqJr232je-dlRVkbCa7nrnFmsIsezsgA3Jb_j9Zu_iv460t_d2eaytbVp9P-DOVfzUfkBsKs-81URQEnTjW6ut445AJz2pxjX92X0GdmORpAkQ"
|
|
||||||
accountName, err := j.Validate(token)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not validate valid token: %v", err)
|
|
||||||
}
|
|
||||||
if accountName != "slingamn" {
|
|
||||||
t.Errorf("incorrect account name for token: `%s`", accountName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// programmatically sign a new token, validate it
|
|
||||||
privKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(rsaTestPrivKey))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
jTok := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(map[string]any{"preferred_username": "slingamn"}))
|
|
||||||
token, err = jTok.SignedString(privKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
accountName, err = j.Validate(token)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not validate valid token: %v", err)
|
|
||||||
}
|
|
||||||
if accountName != "slingamn" {
|
|
||||||
t.Errorf("incorrect account name for token: `%s`", accountName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test expiration
|
|
||||||
jTok = jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(map[string]any{"preferred_username": "slingamn", "exp": 1675740865}))
|
|
||||||
token, err = jTok.SignedString(privKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
accountName, err = j.Validate(token)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("validated expired token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// test for the infamous algorithm confusion bug
|
|
||||||
jTok = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(map[string]any{"preferred_username": "slingamn"}))
|
|
||||||
token, err = jTok.SignedString([]byte(rsaTestPubKey))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
accountName, err = j.Validate(token)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("validated HS256 token despite RSA being required")
|
|
||||||
}
|
|
||||||
|
|
||||||
// test no valid claims
|
|
||||||
jTok = jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(map[string]any{"sub": "slingamn"}))
|
|
||||||
token, err = jTok.SignedString(privKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
accountName, err = j.Validate(token)
|
|
||||||
if err != ErrNoValidAccountClaim {
|
|
||||||
t.Errorf("expected ErrNoValidAccountClaim, got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test email addresses
|
|
||||||
jTok = jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(map[string]any{"email": "Slingamn@example.com"}))
|
|
||||||
token, err = jTok.SignedString(privKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
accountName, err = j.Validate(token)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not validate valid token: %v", err)
|
|
||||||
}
|
|
||||||
if accountName != "Slingamn" {
|
|
||||||
t.Errorf("incorrect account name for token: `%s`", accountName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,15 +6,18 @@ package jwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jwt "github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNoKeys = errors.New("No EXTJWT signing keys are enabled")
|
ErrNoKeys = errors.New("No signing keys are enabled")
|
||||||
)
|
)
|
||||||
|
|
||||||
type MapClaims jwt.MapClaims
|
type MapClaims jwt.MapClaims
|
||||||
|
|
@ -35,10 +38,22 @@ func (t *JwtServiceConfig) Postprocess() (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.rsaPrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(keyBytes)
|
d, _ := pem.Decode(keyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
t.rsaPrivateKey, err = x509.ParsePKCS1PrivateKey(d.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
privateKey, err := x509.ParsePKCS8PrivateKey(d.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey); ok {
|
||||||
|
t.rsaPrivateKey = rsaPrivateKey
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Non-RSA key type for extjwt: %T", privateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +64,7 @@ func (t *JwtServiceConfig) Enabled() bool {
|
||||||
|
|
||||||
func (t *JwtServiceConfig) Sign(claims MapClaims) (result string, err error) {
|
func (t *JwtServiceConfig) Sign(claims MapClaims) (result string, err error) {
|
||||||
claims["exp"] = time.Now().Unix() + int64(t.Expiration/time.Second)
|
claims["exp"] = time.Now().Unix() + int64(t.Expiration/time.Second)
|
||||||
claims["now"] = time.Now().Unix()
|
|
||||||
if t.rsaPrivateKey != nil {
|
if t.rsaPrivateKey != nil {
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(claims))
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(claims))
|
||||||
return token.SignedString(t.rsaPrivateKey)
|
return token.SignedString(t.rsaPrivateKey)
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,6 @@ func (km *KLineManager) loadFromDatastore() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) loadKLines() {
|
func (s *Server) loadKLines() {
|
||||||
server.klines = NewKLineManager(server)
|
s.klines = NewKLineManager(s)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
121
irc/legacy.go
121
irc/legacy.go
|
|
@ -4,15 +4,7 @@ package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tidwall/buntdb"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -33,116 +25,3 @@ func decodeLegacyPasswordHash(hash string) ([]byte, error) {
|
||||||
return nil, errInvalidPasswordHash
|
return nil, errInvalidPasswordHash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// legacy channel registration code
|
|
||||||
|
|
||||||
const (
|
|
||||||
keyChannelExists = "channel.exists %s"
|
|
||||||
keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
|
|
||||||
keyChannelRegTime = "channel.registered.time %s"
|
|
||||||
keyChannelFounder = "channel.founder %s"
|
|
||||||
keyChannelTopic = "channel.topic %s"
|
|
||||||
keyChannelTopicSetBy = "channel.topic.setby %s"
|
|
||||||
keyChannelTopicSetTime = "channel.topic.settime %s"
|
|
||||||
keyChannelBanlist = "channel.banlist %s"
|
|
||||||
keyChannelExceptlist = "channel.exceptlist %s"
|
|
||||||
keyChannelInvitelist = "channel.invitelist %s"
|
|
||||||
keyChannelPassword = "channel.key %s"
|
|
||||||
keyChannelModes = "channel.modes %s"
|
|
||||||
keyChannelAccountToUMode = "channel.accounttoumode %s"
|
|
||||||
keyChannelUserLimit = "channel.userlimit %s"
|
|
||||||
keyChannelSettings = "channel.settings %s"
|
|
||||||
keyChannelForward = "channel.forward %s"
|
|
||||||
|
|
||||||
keyChannelPurged = "channel.purged %s"
|
|
||||||
)
|
|
||||||
|
|
||||||
func deleteLegacyChannel(tx *buntdb.Tx, nameCasefolded string) {
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelExists, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelName, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelRegTime, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelFounder, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelTopic, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelTopicSetBy, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelTopicSetTime, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelBanlist, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelExceptlist, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelInvitelist, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelPassword, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelModes, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelAccountToUMode, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelUserLimit, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelSettings, nameCasefolded))
|
|
||||||
tx.Delete(fmt.Sprintf(keyChannelForward, nameCasefolded))
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadLegacyChannel(tx *buntdb.Tx, nameCasefolded string) (info RegisteredChannel, err error) {
|
|
||||||
channelKey := nameCasefolded
|
|
||||||
// nice to have: do all JSON (de)serialization outside of the buntdb transaction
|
|
||||||
_, dberr := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
|
|
||||||
if dberr == buntdb.ErrNotFound {
|
|
||||||
// chan does not already exist, return
|
|
||||||
err = errNoSuchChannel
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel exists, load it
|
|
||||||
name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey))
|
|
||||||
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey))
|
|
||||||
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
|
|
||||||
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey))
|
|
||||||
topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey))
|
|
||||||
topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey))
|
|
||||||
var topicSetTime time.Time
|
|
||||||
topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
|
|
||||||
if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil {
|
|
||||||
topicSetTime = time.Unix(0, topicSetTimeInt).UTC()
|
|
||||||
}
|
|
||||||
password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
|
|
||||||
modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
|
|
||||||
userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey))
|
|
||||||
forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey))
|
|
||||||
banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey))
|
|
||||||
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
|
|
||||||
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
|
|
||||||
accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
|
|
||||||
settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
|
|
||||||
|
|
||||||
modeSlice := make([]modes.Mode, len(modeString))
|
|
||||||
for i, mode := range modeString {
|
|
||||||
modeSlice[i] = modes.Mode(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
userLimit, _ := strconv.Atoi(userLimitString)
|
|
||||||
|
|
||||||
var banlist map[string]MaskInfo
|
|
||||||
_ = json.Unmarshal([]byte(banlistString), &banlist)
|
|
||||||
var exceptlist map[string]MaskInfo
|
|
||||||
_ = json.Unmarshal([]byte(exceptlistString), &exceptlist)
|
|
||||||
var invitelist map[string]MaskInfo
|
|
||||||
_ = json.Unmarshal([]byte(invitelistString), &invitelist)
|
|
||||||
accountToUMode := make(map[string]modes.Mode)
|
|
||||||
_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
|
|
||||||
|
|
||||||
var settings ChannelSettings
|
|
||||||
_ = json.Unmarshal([]byte(settingsString), &settings)
|
|
||||||
|
|
||||||
info = RegisteredChannel{
|
|
||||||
Name: name,
|
|
||||||
RegisteredAt: time.Unix(0, regTimeInt).UTC(),
|
|
||||||
Founder: founder,
|
|
||||||
Topic: topic,
|
|
||||||
TopicSetBy: topicSetBy,
|
|
||||||
TopicSetTime: topicSetTime,
|
|
||||||
Key: password,
|
|
||||||
Modes: modeSlice,
|
|
||||||
Bans: banlist,
|
|
||||||
Excepts: exceptlist,
|
|
||||||
Invites: invitelist,
|
|
||||||
AccountToUMode: accountToUMode,
|
|
||||||
UserLimit: int(userLimit),
|
|
||||||
Settings: settings,
|
|
||||||
Forward: forward,
|
|
||||||
}
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -30,7 +29,7 @@ type IRCListener interface {
|
||||||
|
|
||||||
// NewListener creates a new listener according to the specifications in the config file
|
// NewListener creates a new listener according to the specifications in the config file
|
||||||
func NewListener(server *Server, addr string, config utils.ListenerConfig, bindMode os.FileMode) (result IRCListener, err error) {
|
func NewListener(server *Server, addr string, config utils.ListenerConfig, bindMode os.FileMode) (result IRCListener, err error) {
|
||||||
baseListener, err := createBaseListener(server, addr, bindMode)
|
baseListener, err := createBaseListener(addr, bindMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -44,14 +43,11 @@ func NewListener(server *Server, addr string, config utils.ListenerConfig, bindM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createBaseListener(server *Server, addr string, bindMode os.FileMode) (listener net.Listener, err error) {
|
func createBaseListener(addr string, bindMode os.FileMode) (listener net.Listener, err error) {
|
||||||
addr = strings.TrimPrefix(addr, "unix:")
|
addr = strings.TrimPrefix(addr, "unix:")
|
||||||
if strings.HasPrefix(addr, "/") {
|
if strings.HasPrefix(addr, "/") {
|
||||||
// https://stackoverflow.com/a/34881585
|
// https://stackoverflow.com/a/34881585
|
||||||
removeErr := os.Remove(addr)
|
os.Remove(addr)
|
||||||
if removeErr != nil && !errors.Is(removeErr, fs.ErrNotExist) {
|
|
||||||
server.logger.Warning("listeners", "could not delete unix domain listener", addr, removeErr.Error())
|
|
||||||
}
|
|
||||||
listener, err = net.Listen("unix", addr)
|
listener, err = net.Listen("unix", addr)
|
||||||
if err == nil && bindMode != 0 {
|
if err == nil && bindMode != 0 {
|
||||||
os.Chmod(addr, bindMode)
|
os.Chmod(addr, bindMode)
|
||||||
|
|
@ -208,10 +204,10 @@ func confirmProxyData(conn *utils.WrappedConn, remoteAddr, xForwardedFor, xForwa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn.TLS || conn.Tor {
|
if conn.Config.TLSConfig != nil || conn.Config.Tor {
|
||||||
// we terminated our own encryption:
|
// we terminated our own encryption:
|
||||||
conn.Secure = true
|
conn.Secure = true
|
||||||
} else if !conn.WebSocket {
|
} else if !conn.Config.WebSocket {
|
||||||
// plaintext normal connection: loopback and secureNets are secure
|
// plaintext normal connection: loopback and secureNets are secure
|
||||||
realIP := utils.AddrToIP(conn.RemoteAddr())
|
realIP := utils.AddrToIP(conn.RemoteAddr())
|
||||||
conn.Secure = realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets)
|
conn.Secure = realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets)
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,8 @@ func (m *MessageCache) Initialize(server *Server, serverTime time.Time, msgid st
|
||||||
m.params = params
|
m.params = params
|
||||||
|
|
||||||
var msg ircmsg.Message
|
var msg ircmsg.Message
|
||||||
if forceTrailing(server.Config(), command) {
|
config := server.Config()
|
||||||
|
if config.Server.Compatibility.forceTrailing && commandsThatMustUseTrailing[command] {
|
||||||
msg.ForceTrailing()
|
msg.ForceTrailing()
|
||||||
}
|
}
|
||||||
msg.Source = nickmask
|
msg.Source = nickmask
|
||||||
|
|
@ -110,7 +111,8 @@ func (m *MessageCache) InitializeSplitMessage(server *Server, nickmask, accountN
|
||||||
m.target = target
|
m.target = target
|
||||||
m.splitMessage = message
|
m.splitMessage = message
|
||||||
|
|
||||||
forceTrailing := forceTrailing(server.Config(), command)
|
config := server.Config()
|
||||||
|
forceTrailing := config.Server.Compatibility.forceTrailing && commandsThatMustUseTrailing[command]
|
||||||
|
|
||||||
if message.Is512() {
|
if message.Is512() {
|
||||||
isTagmsg := command == "TAGMSG"
|
isTagmsg := command == "TAGMSG"
|
||||||
|
|
|
||||||
15
irc/modes.go
15
irc/modes.go
|
|
@ -158,6 +158,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||||
|
|
||||||
var alreadySentPrivError bool
|
var alreadySentPrivError bool
|
||||||
|
|
||||||
|
maskOpCount := 0
|
||||||
chname := channel.Name()
|
chname := channel.Name()
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
|
|
||||||
|
|
@ -191,11 +192,6 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// should we send 324 RPL_CHANNELMODEIS? standard behavior is to send it for
|
|
||||||
// `MODE #channel`, i.e., an empty list of intended changes, but Ergo will
|
|
||||||
// also send it for no-op changes to zero-argument modes like +i
|
|
||||||
shouldSendModeIsLine := len(changes) == 0
|
|
||||||
|
|
||||||
for _, change := range changes {
|
for _, change := range changes {
|
||||||
if !hasPrivs(change) {
|
if !hasPrivs(change) {
|
||||||
if !alreadySentPrivError {
|
if !alreadySentPrivError {
|
||||||
|
|
@ -207,6 +203,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||||
|
|
||||||
switch change.Mode {
|
switch change.Mode {
|
||||||
case modes.BanMask, modes.ExceptMask, modes.InviteMask:
|
case modes.BanMask, modes.ExceptMask, modes.InviteMask:
|
||||||
|
maskOpCount += 1
|
||||||
if change.Op == modes.List {
|
if change.Op == modes.List {
|
||||||
channel.ShowMaskList(client, change.Mode, rb)
|
channel.ShowMaskList(client, change.Mode, rb)
|
||||||
continue
|
continue
|
||||||
|
|
@ -215,7 +212,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||||
mask := change.Arg
|
mask := change.Arg
|
||||||
switch change.Op {
|
switch change.Op {
|
||||||
case modes.Add:
|
case modes.Add:
|
||||||
if !isSamode && channel.lists[change.Mode].Length() >= client.server.Config().Limits.ChanListModes {
|
if channel.lists[change.Mode].Length() >= client.server.Config().Limits.ChanListModes {
|
||||||
if !listFullWarned[change.Mode] {
|
if !listFullWarned[change.Mode] {
|
||||||
rb.Add(nil, client.server.name, ERR_BANLISTFULL, details.nick, chname, change.Mode.String(), client.t("Channel list is full"))
|
rb.Add(nil, client.server.name, ERR_BANLISTFULL, details.nick, chname, change.Mode.String(), client.t("Channel list is full"))
|
||||||
listFullWarned[change.Mode] = true
|
listFullWarned[change.Mode] = true
|
||||||
|
|
@ -316,14 +313,11 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||||
default:
|
default:
|
||||||
// all channel modes with no args, e.g., InviteOnly, Secret
|
// all channel modes with no args, e.g., InviteOnly, Secret
|
||||||
if change.Op == modes.List {
|
if change.Op == modes.List {
|
||||||
shouldSendModeIsLine = true
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if channel.flags.SetMode(change.Mode, change.Op == modes.Add) {
|
if channel.flags.SetMode(change.Mode, change.Op == modes.Add) {
|
||||||
applied = append(applied, change)
|
applied = append(applied, change)
|
||||||
} else {
|
|
||||||
shouldSendModeIsLine = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -343,7 +337,8 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||||
channel.MarkDirty(includeFlags)
|
channel.MarkDirty(includeFlags)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(applied) == 0 && !alreadySentPrivError && shouldSendModeIsLine {
|
// #649: don't send 324 RPL_CHANNELMODEIS if we were only working with mask lists
|
||||||
|
if len(applied) == 0 && !alreadySentPrivError && (maskOpCount == 0 || maskOpCount < len(changes)) {
|
||||||
args := append([]string{details.nick, chname}, channel.modeStrings(client)...)
|
args := append([]string{details.nick, chname}, channel.modeStrings(client)...)
|
||||||
rb.Add(nil, client.server.name, RPL_CHANNELMODEIS, args...)
|
rb.Add(nil, client.server.name, RPL_CHANNELMODEIS, args...)
|
||||||
rb.Add(nil, client.server.name, RPL_CREATIONTIME, details.nick, chname, strconv.FormatInt(channel.createdTime.Unix(), 10))
|
rb.Add(nil, client.server.name, RPL_CREATIONTIME, details.nick, chname, strconv.FormatInt(channel.createdTime.Unix(), 10))
|
||||||
|
|
|
||||||
|
|
@ -345,10 +345,6 @@ func NewModeSet() *ModeSet {
|
||||||
return &set
|
return &set
|
||||||
}
|
}
|
||||||
|
|
||||||
func (set *ModeSet) Clear() {
|
|
||||||
utils.BitsetClear(set[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// test whether `mode` is set
|
// test whether `mode` is set
|
||||||
func (set *ModeSet) HasMode(mode Mode) bool {
|
func (set *ModeSet) HasMode(mode Mode) bool {
|
||||||
if set == nil {
|
if set == nil {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
@ -31,6 +30,14 @@ const (
|
||||||
// maximum length in bytes of any message target (nickname or channel name) in its
|
// maximum length in bytes of any message target (nickname or channel name) in its
|
||||||
// canonicalized (i.e., casefolded) state:
|
// canonicalized (i.e., casefolded) state:
|
||||||
MaxTargetLength = 64
|
MaxTargetLength = 64
|
||||||
|
|
||||||
|
// latest schema of the db
|
||||||
|
latestDbSchema = "2"
|
||||||
|
keySchemaVersion = "db.version"
|
||||||
|
// minor version indicates rollback-safe upgrades, i.e.,
|
||||||
|
// you can downgrade oragono and everything will work
|
||||||
|
latestDbMinorVersion = "2"
|
||||||
|
keySchemaMinorVersion = "db.minorversion"
|
||||||
cleanupRowLimit = 50
|
cleanupRowLimit = 50
|
||||||
cleanupPauseTime = 10 * time.Minute
|
cleanupPauseTime = 10 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
@ -42,16 +49,11 @@ type MySQL struct {
|
||||||
logger *logger.Manager
|
logger *logger.Manager
|
||||||
|
|
||||||
insertHistory *sql.Stmt
|
insertHistory *sql.Stmt
|
||||||
|
insertSequence *sql.Stmt
|
||||||
insertConversation *sql.Stmt
|
insertConversation *sql.Stmt
|
||||||
|
insertCorrespondent *sql.Stmt
|
||||||
insertAccountMessage *sql.Stmt
|
insertAccountMessage *sql.Stmt
|
||||||
|
|
||||||
getReactionsQuery *sql.Stmt
|
|
||||||
getSingleReaction *sql.Stmt
|
|
||||||
addReaction *sql.Stmt
|
|
||||||
deleteReaction *sql.Stmt
|
|
||||||
|
|
||||||
getMessageById *sql.Stmt
|
|
||||||
|
|
||||||
stateMutex sync.Mutex
|
stateMutex sync.Mutex
|
||||||
config Config
|
config Config
|
||||||
|
|
||||||
|
|
@ -86,55 +88,197 @@ func (mysql *MySQL) getExpireTime() (expireTime time.Duration) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) Open() (err error) {
|
func (m *MySQL) Open() (err error) {
|
||||||
var address string
|
var address string
|
||||||
if mysql.config.SocketPath != "" {
|
if m.config.SocketPath != "" {
|
||||||
address = fmt.Sprintf("unix(%s)", mysql.config.SocketPath)
|
address = fmt.Sprintf("unix(%s)", m.config.SocketPath)
|
||||||
} else if mysql.config.Port != 0 {
|
} else if m.config.Port != 0 {
|
||||||
address = fmt.Sprintf("tcp(%s:%d)", mysql.config.Host, mysql.config.Port)
|
address = fmt.Sprintf("tcp(%s:%d)", m.config.Host, m.config.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
mysql.db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@%s/%s", mysql.config.User, mysql.config.Password, address, mysql.config.HistoryDatabase))
|
m.db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@%s/%s", m.config.User, m.config.Password, address, m.config.HistoryDatabase))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if mysql.config.MaxConns != 0 {
|
if m.config.MaxConns != 0 {
|
||||||
mysql.db.SetMaxOpenConns(mysql.config.MaxConns)
|
m.db.SetMaxOpenConns(m.config.MaxConns)
|
||||||
mysql.db.SetMaxIdleConns(mysql.config.MaxConns)
|
m.db.SetMaxIdleConns(m.config.MaxConns)
|
||||||
}
|
}
|
||||||
if mysql.config.ConnMaxLifetime != 0 {
|
if m.config.ConnMaxLifetime != 0 {
|
||||||
mysql.db.SetConnMaxLifetime(mysql.config.ConnMaxLifetime)
|
m.db.SetConnMaxLifetime(m.config.ConnMaxLifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mysql.fixSchemas()
|
err = m.fixSchemas()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mysql.prepareStatements()
|
err = m.prepareStatements()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go mysql.cleanupLoop()
|
go m.cleanupLoop()
|
||||||
go mysql.forgetLoop()
|
go m.forgetLoop()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) fixSchemas() (err error) {
|
func (mysql *MySQL) fixSchemas() (err error) {
|
||||||
// 3M now handles this
|
_, err = mysql.db.Exec(`CREATE TABLE IF NOT EXISTS metadata (
|
||||||
|
key_name VARCHAR(32) primary key,
|
||||||
|
value VARCHAR(32) NOT NULL
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var schema string
|
||||||
|
err = mysql.db.QueryRow(`select value from metadata where key_name = ?;`, keySchemaVersion).Scan(&schema)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
err = mysql.createTables()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaVersion, latestDbSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaMinorVersion, latestDbMinorVersion)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if err == nil && schema != latestDbSchema {
|
||||||
|
// TODO figure out what to do about schema changes
|
||||||
|
return fmt.Errorf("incompatible schema: got %s, expected %s", schema, latestDbSchema)
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var minorVersion string
|
||||||
|
err = mysql.db.QueryRow(`select value from metadata where key_name = ?;`, keySchemaMinorVersion).Scan(&minorVersion)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
// XXX for now, the only minor version upgrade is the account tracking tables
|
||||||
|
err = mysql.createComplianceTables()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = mysql.createCorrespondentsTable()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaMinorVersion, latestDbMinorVersion)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if err == nil && minorVersion == "1" {
|
||||||
|
// upgrade from 2.1 to 2.2: create the correspondents table
|
||||||
|
err = mysql.createCorrespondentsTable()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = mysql.db.Exec(`update metadata set value = ? where key_name = ?;`, latestDbMinorVersion, keySchemaMinorVersion)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if err == nil && minorVersion != latestDbMinorVersion {
|
||||||
|
// TODO: if minorVersion < latestDbMinorVersion, upgrade,
|
||||||
|
// if latestDbMinorVersion < minorVersion, ignore because backwards compatible
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) createTables() (err error) {
|
func (mysql *MySQL) createTables() (err error) {
|
||||||
// 3M now handles this
|
_, err = mysql.db.Exec(`CREATE TABLE history (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
data BLOB NOT NULL,
|
||||||
|
msgid BINARY(16) NOT NULL,
|
||||||
|
KEY (msgid(4))
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE sequence (
|
||||||
|
history_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
|
||||||
|
target VARBINARY(%[1]d) NOT NULL,
|
||||||
|
nanotime BIGINT UNSIGNED NOT NULL,
|
||||||
|
KEY (target, nanotime)
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
/* XXX: this table used to be:
|
||||||
|
CREATE TABLE sequence (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
target VARBINARY(%[1]d) NOT NULL,
|
||||||
|
nanotime BIGINT UNSIGNED NOT NULL,
|
||||||
|
history_id BIGINT NOT NULL,
|
||||||
|
KEY (target, nanotime),
|
||||||
|
KEY (history_id)
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;
|
||||||
|
Some users may still be using the old schema.
|
||||||
|
*/
|
||||||
|
|
||||||
|
_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE conversations (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
target VARBINARY(%[1]d) NOT NULL,
|
||||||
|
correspondent VARBINARY(%[1]d) NOT NULL,
|
||||||
|
nanotime BIGINT UNSIGNED NOT NULL,
|
||||||
|
history_id BIGINT NOT NULL,
|
||||||
|
KEY (target, correspondent, nanotime),
|
||||||
|
KEY (history_id)
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mysql.createCorrespondentsTable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mysql.createComplianceTables()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) createCorrespondentsTable() (err error) {
|
||||||
|
_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE correspondents (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
target VARBINARY(%[1]d) NOT NULL,
|
||||||
|
correspondent VARBINARY(%[1]d) NOT NULL,
|
||||||
|
nanotime BIGINT UNSIGNED NOT NULL,
|
||||||
|
UNIQUE KEY (target, correspondent),
|
||||||
|
KEY (target, nanotime),
|
||||||
|
KEY (nanotime)
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) createComplianceTables() (err error) {
|
func (mysql *MySQL) createComplianceTables() (err error) {
|
||||||
// 3M now handles this
|
_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE account_messages (
|
||||||
|
history_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
|
||||||
|
account VARBINARY(%[1]d) NOT NULL,
|
||||||
|
KEY (account, history_id)
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE forget (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
account VARBINARY(%[1]d) NOT NULL
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,6 +326,10 @@ func (mysql *MySQL) doCleanup(age time.Duration) (count int, err error) {
|
||||||
|
|
||||||
mysql.logger.Debug("mysql", fmt.Sprintf("deleting %d history rows, max age %s", len(ids), utils.NanoToTimestamp(maxNanotime)))
|
mysql.logger.Debug("mysql", fmt.Sprintf("deleting %d history rows, max age %s", len(ids), utils.NanoToTimestamp(maxNanotime)))
|
||||||
|
|
||||||
|
if maxNanotime != 0 {
|
||||||
|
mysql.deleteCorrespondents(ctx, maxNanotime)
|
||||||
|
}
|
||||||
|
|
||||||
return len(ids), mysql.deleteHistoryIDs(ctx, ids)
|
return len(ids), mysql.deleteHistoryIDs(ctx, ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,14 +346,21 @@ func (mysql *MySQL) deleteHistoryIDs(ctx context.Context, ids []uint64) (err err
|
||||||
inBuf.WriteRune(')')
|
inBuf.WriteRune(')')
|
||||||
inClause := inBuf.String()
|
inClause := inBuf.String()
|
||||||
|
|
||||||
|
_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM conversations WHERE history_id in %s;`, inClause))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM sequence WHERE history_id in %s;`, inClause))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if mysql.isTrackingAccountMessages() {
|
if mysql.isTrackingAccountMessages() {
|
||||||
_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM account_messages WHERE history_id in %s;`, inClause))
|
_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM account_messages WHERE history_id in %s;`, inClause))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf(`DELETE FROM history WHERE msgid in %s;`, inClause)
|
_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM history WHERE id in %s;`, inClause))
|
||||||
_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM history WHERE msgid in %s;`, inClause))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -214,35 +369,57 @@ func (mysql *MySQL) deleteHistoryIDs(ctx context.Context, ids []uint64) (err err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) selectCleanupIDs(ctx context.Context, age time.Duration) (ids []uint64, maxNanotime int64, err error) {
|
func (mysql *MySQL) selectCleanupIDs(ctx context.Context, age time.Duration) (ids []uint64, maxNanotime int64, err error) {
|
||||||
before := timestampSnowflake(time.Now().Add(-age))
|
|
||||||
maxNanotime = time.Now().Add(-age).Unix() * 1000000000
|
|
||||||
|
|
||||||
rows, err := mysql.db.QueryContext(ctx, `
|
rows, err := mysql.db.QueryContext(ctx, `
|
||||||
SELECT history.msgid
|
SELECT history.id, sequence.nanotime, conversations.nanotime
|
||||||
FROM history
|
FROM history
|
||||||
WHERE msgid < ?
|
LEFT JOIN sequence ON history.id = sequence.history_id
|
||||||
ORDER BY history.msgid LIMIT ?;`, before, cleanupRowLimit)
|
LEFT JOIN conversations on history.id = conversations.history_id
|
||||||
|
ORDER BY history.id LIMIT ?;`, cleanupRowLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
ids = make([]uint64, cleanupRowLimit)
|
idset := make(map[uint64]struct{}, cleanupRowLimit)
|
||||||
|
threshold := time.Now().Add(-age).UnixNano()
|
||||||
i := 0
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id uint64
|
var id uint64
|
||||||
err = rows.Scan(&id)
|
var seqNano, convNano sql.NullInt64
|
||||||
|
err = rows.Scan(&id, &seqNano, &convNano)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
nanotime := extractNanotime(seqNano, convNano)
|
||||||
|
// returns 0 if not found; in that case the data is inconsistent
|
||||||
|
// and we should delete the entry
|
||||||
|
if nanotime < threshold {
|
||||||
|
idset[id] = struct{}{}
|
||||||
|
if nanotime > maxNanotime {
|
||||||
|
maxNanotime = nanotime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids = make([]uint64, len(idset))
|
||||||
|
i := 0
|
||||||
|
for id := range idset {
|
||||||
ids[i] = id
|
ids[i] = id
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
ids = ids[0:i]
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) deleteCorrespondents(ctx context.Context, threshold int64) {
|
||||||
|
result, err := mysql.db.ExecContext(ctx, `DELETE FROM correspondents WHERE nanotime <= (?);`, threshold)
|
||||||
|
if err != nil {
|
||||||
|
mysql.logError("error deleting correspondents", err)
|
||||||
|
} else {
|
||||||
|
count, err := result.RowsAffected()
|
||||||
|
if !mysql.logError("error deleting correspondents", err) {
|
||||||
|
mysql.logger.Debug(fmt.Sprintf("deleted %d correspondents entries", count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// wait for forget queue items and process them one by one
|
// wait for forget queue items and process them one by one
|
||||||
func (mysql *MySQL) forgetLoop() {
|
func (mysql *MySQL) forgetLoop() {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
@ -348,7 +525,23 @@ func (mysql *MySQL) doForgetIteration(account string) (count int, err error) {
|
||||||
|
|
||||||
func (mysql *MySQL) prepareStatements() (err error) {
|
func (mysql *MySQL) prepareStatements() (err error) {
|
||||||
mysql.insertHistory, err = mysql.db.Prepare(`INSERT INTO history
|
mysql.insertHistory, err = mysql.db.Prepare(`INSERT INTO history
|
||||||
(data, msgid, target, sender, nanotime) VALUES (?, ?, ?, ?, ?);`)
|
(data, msgid) VALUES (?, ?);`)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mysql.insertSequence, err = mysql.db.Prepare(`INSERT INTO sequence
|
||||||
|
(target, nanotime, history_id) VALUES (?, ?, ?);`)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mysql.insertConversation, err = mysql.db.Prepare(`INSERT INTO conversations
|
||||||
|
(target, correspondent, nanotime, history_id) VALUES (?, ?, ?, ?);`)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mysql.insertCorrespondent, err = mysql.db.Prepare(`INSERT INTO correspondents
|
||||||
|
(target, correspondent, nanotime) VALUES (?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE nanotime = GREATEST(nanotime, ?);`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -358,38 +551,6 @@ func (mysql *MySQL) prepareStatements() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mysql.getReactionsQuery, err = mysql.db.Prepare(`select react, count(*) as total, (select JSON_ARRAYAGG(user)
|
|
||||||
from reactions r
|
|
||||||
where r.msgid = main.msgid
|
|
||||||
and r.react = main.react
|
|
||||||
limit 3) as sample
|
|
||||||
from reactions as main
|
|
||||||
where main.msgid = ?
|
|
||||||
group by react, msgid;`)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mysql.getSingleReaction, err = mysql.db.Prepare(`SELECT COUNT(*) FROM reactions WHERE msgid = ? AND user = ? AND react = ?`)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mysql.deleteReaction, err = mysql.db.Prepare(`DELETE FROM reactions WHERE msgid = ? AND user = ? AND react = ?`)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mysql.addReaction, err = mysql.db.Prepare(`INSERT INTO reactions(msgid, user, react) VALUES (?, ?, ?)`)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mysql.getMessageById, err = mysql.db.Prepare(`SELECT msgid, data, target, sender, nanotime, pm FROM history WHERE msgid = ?`)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,6 +607,11 @@ func (mysql *MySQL) AddChannelItem(target string, item history.Item, account str
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = mysql.insertSequenceEntry(ctx, target, item.Message.Time.UnixNano(), id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = mysql.insertAccountMessageEntry(ctx, id, account)
|
err = mysql.insertAccountMessageEntry(ctx, id, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
@ -454,21 +620,39 @@ func (mysql *MySQL) AddChannelItem(target string, item history.Item, account str
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) insertBase(ctx context.Context, item history.Item) (id int64, err error) {
|
func (mysql *MySQL) insertSequenceEntry(ctx context.Context, target string, messageTime int64, id int64) (err error) {
|
||||||
var value []byte
|
_, err = mysql.insertSequence.ExecContext(ctx, target, messageTime, id)
|
||||||
value, err = marshalItem(&item)
|
mysql.logError("could not insert sequence entry", err)
|
||||||
if mysql.logError("could not marshal item", err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var account = item.Account
|
|
||||||
if account == "" {
|
|
||||||
account = "*"
|
|
||||||
}
|
|
||||||
result, err := mysql.insertHistory.ExecContext(ctx, value, item.Message.Msgid, item.Target, account, item.Message.Time.UnixNano())
|
|
||||||
if mysql.logError("could not insert item", err) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) insertConversationEntry(ctx context.Context, target, correspondent string, messageTime int64, id int64) (err error) {
|
||||||
|
_, err = mysql.insertConversation.ExecContext(ctx, target, correspondent, messageTime, id)
|
||||||
|
mysql.logError("could not insert conversations entry", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) insertCorrespondentsEntry(ctx context.Context, target, correspondent string, messageTime int64, historyId int64) (err error) {
|
||||||
|
_, err = mysql.insertCorrespondent.ExecContext(ctx, target, correspondent, messageTime, messageTime)
|
||||||
|
mysql.logError("could not insert conversations entry", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) insertBase(ctx context.Context, item history.Item) (id int64, err error) {
|
||||||
|
value, err := marshalItem(&item)
|
||||||
|
if mysql.logError("could not marshal item", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msgidBytes, err := decodeMsgid(item.Message.Msgid)
|
||||||
|
if mysql.logError("could not decode msgid", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := mysql.insertHistory.ExecContext(ctx, value, msgidBytes)
|
||||||
|
if mysql.logError("could not insert item", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
id, err = result.LastInsertId()
|
id, err = result.LastInsertId()
|
||||||
if mysql.logError("could not insert item", err) {
|
if mysql.logError("could not insert item", err) {
|
||||||
return
|
return
|
||||||
|
|
@ -502,7 +686,36 @@ func (mysql *MySQL) AddDirectMessage(sender, senderAccount, recipient, recipient
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
|
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err = mysql.insertBase(ctx, item)
|
id, err := mysql.insertBase(ctx, item)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nanotime := item.Message.Time.UnixNano()
|
||||||
|
|
||||||
|
if senderAccount != "" {
|
||||||
|
err = mysql.insertConversationEntry(ctx, senderAccount, recipient, nanotime, id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = mysql.insertCorrespondentsEntry(ctx, senderAccount, recipient, nanotime, id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipientAccount != "" && sender != recipient {
|
||||||
|
err = mysql.insertConversationEntry(ctx, recipientAccount, sender, nanotime, id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = mysql.insertCorrespondentsEntry(ctx, recipientAccount, sender, nanotime, id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mysql.insertAccountMessageEntry(ctx, id, senderAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -511,7 +724,7 @@ func (mysql *MySQL) AddDirectMessage(sender, senderAccount, recipient, recipient
|
||||||
}
|
}
|
||||||
|
|
||||||
// note that accountName is the unfolded name
|
// note that accountName is the unfolded name
|
||||||
func (mysql *MySQL) DeleteMsgid(msgid, account string) (err error) {
|
func (mysql *MySQL) DeleteMsgid(msgid, accountName string) (err error) {
|
||||||
if mysql.db == nil {
|
if mysql.db == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -524,11 +737,11 @@ func (mysql *MySQL) DeleteMsgid(msgid, account string) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if account != "*" {
|
if accountName != "*" {
|
||||||
var item history.Item
|
var item history.Item
|
||||||
err = unmarshalItem(data, &item)
|
err = unmarshalItem(data, &item)
|
||||||
// delete if the entry is corrupt
|
// delete if the entry is corrupt
|
||||||
if err == nil && item.Account != account {
|
if err == nil && item.AccountName != accountName {
|
||||||
return ErrDisallowed
|
return ErrDisallowed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -539,10 +752,7 @@ func (mysql *MySQL) DeleteMsgid(msgid, account string) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) Export(account string, writer io.Writer) {
|
func (mysql *MySQL) Export(account string, writer io.Writer) {
|
||||||
// no eu presence...
|
if mysql.db == nil {
|
||||||
// maybe fix this when i know the new schema works
|
|
||||||
return
|
|
||||||
/*if mysql.db == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -554,8 +764,10 @@ func (mysql *MySQL) Export(account string, writer io.Writer) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rows, rowsErr := mysql.db.QueryContext(ctx, `
|
rows, rowsErr := mysql.db.QueryContext(ctx, `
|
||||||
SELECT history.data, msgid, target FROM history
|
SELECT account_messages.history_id, history.data, sequence.target FROM account_messages
|
||||||
WHERE sender = ? AND account_messages.history_id > ?
|
INNER JOIN history ON history.id = account_messages.history_id
|
||||||
|
INNER JOIN sequence ON account_messages.history_id = sequence.history_id
|
||||||
|
WHERE account_messages.account = ? AND account_messages.history_id > ?
|
||||||
LIMIT ?`, account, lastSeen, cleanupRowLimit)
|
LIMIT ?`, account, lastSeen, cleanupRowLimit)
|
||||||
if rowsErr != nil {
|
if rowsErr != nil {
|
||||||
err = rowsErr
|
err = rowsErr
|
||||||
|
|
@ -567,7 +779,7 @@ func (mysql *MySQL) Export(account string, writer io.Writer) {
|
||||||
var blob, jsonBlob []byte
|
var blob, jsonBlob []byte
|
||||||
var target string
|
var target string
|
||||||
var item history.Item
|
var item history.Item
|
||||||
err = rows.Scan(&blob, &id, &target)
|
err = rows.Scan(&id, &blob, &target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -575,7 +787,7 @@ func (mysql *MySQL) Export(account string, writer io.Writer) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.Target = target
|
item.CfCorrespondent = target
|
||||||
jsonBlob, err = json.Marshal(item)
|
jsonBlob, err = json.Marshal(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
@ -595,66 +807,28 @@ func (mysql *MySQL) Export(account string, writer io.Writer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mysql.logError("could not export history", err)
|
mysql.logError("could not export history", err)
|
||||||
return*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kinda an intermediary function due to the CEF DB structure
|
|
||||||
func (mysql *MySQL) GetMessage(msgid string) (id uint64, item history.Item, target string, sender string, nanotime uint64, pm bool, err error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
|
|
||||||
defer cancel()
|
|
||||||
var data []byte
|
|
||||||
|
|
||||||
row := mysql.getMessageById.QueryRowContext(ctx, msgid)
|
|
||||||
err = row.Scan(&id, &data, &target, &sender, &nanotime, &pm)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = unmarshalItem(data, &item)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mysql *MySQL) HasReactionFromUser(msgid string, user string, reaction string) (exists bool) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
|
|
||||||
defer cancel()
|
|
||||||
row := mysql.getSingleReaction.QueryRowContext(ctx, msgid, user, reaction)
|
|
||||||
var count int
|
|
||||||
row.Scan(&count)
|
|
||||||
return count > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mysql *MySQL) AddReaction(msgid string, user string, reaction string) (err error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
|
|
||||||
defer cancel()
|
|
||||||
_, err = mysql.addReaction.ExecContext(ctx, msgid, user, reaction)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mysql *MySQL) DeleteReaction(msgid string, user string, reaction string) (err error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
|
|
||||||
defer cancel()
|
|
||||||
_, err = mysql.deleteReaction.ExecContext(ctx, msgid, user, reaction)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) lookupMsgid(ctx context.Context, msgid string, includeData bool) (result time.Time, id uint64, data []byte, err error) {
|
func (mysql *MySQL) lookupMsgid(ctx context.Context, msgid string, includeData bool) (result time.Time, id uint64, data []byte, err error) {
|
||||||
|
decoded, err := decodeMsgid(msgid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cols := `history.nanotime`
|
cols := `sequence.nanotime, conversations.nanotime`
|
||||||
if includeData {
|
if includeData {
|
||||||
cols = `history.nanotime, history.id, history.data`
|
cols = `sequence.nanotime, conversations.nanotime, history.id, history.data`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since CEF uses snowflakes and vanilla ergo uses blobs, we cast as int to make it function.
|
|
||||||
// May have to adjust it some day
|
|
||||||
row := mysql.db.QueryRowContext(ctx, fmt.Sprintf(`
|
row := mysql.db.QueryRowContext(ctx, fmt.Sprintf(`
|
||||||
SELECT %s FROM history
|
SELECT %s FROM history
|
||||||
WHERE history.msgid = CAST(? AS UNSIGNED) LIMIT 1;`, cols), msgid)
|
LEFT JOIN sequence ON history.id = sequence.history_id
|
||||||
var nanoSeq sql.NullInt64
|
LEFT JOIN conversations ON history.id = conversations.history_id
|
||||||
|
WHERE history.msgid = ? LIMIT 1;`, cols), decoded)
|
||||||
|
var nanoSeq, nanoConv sql.NullInt64
|
||||||
if !includeData {
|
if !includeData {
|
||||||
err = row.Scan(&nanoSeq)
|
err = row.Scan(&nanoSeq, &nanoConv)
|
||||||
} else {
|
} else {
|
||||||
err = row.Scan(&nanoSeq, &id, &data)
|
err = row.Scan(&nanoSeq, &nanoConv, &id, &data)
|
||||||
}
|
}
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
mysql.logError("could not resolve msgid to time", err)
|
mysql.logError("could not resolve msgid to time", err)
|
||||||
|
|
@ -662,7 +836,7 @@ func (mysql *MySQL) lookupMsgid(ctx context.Context, msgid string, includeData b
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nanotime := nanoSeq.Int64
|
nanotime := extractNanotime(nanoSeq, nanoConv)
|
||||||
if nanotime == 0 {
|
if nanotime == 0 {
|
||||||
err = sql.ErrNoRows
|
err = sql.ErrNoRows
|
||||||
return
|
return
|
||||||
|
|
@ -671,6 +845,15 @@ func (mysql *MySQL) lookupMsgid(ctx context.Context, msgid string, includeData b
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractNanotime(seq, conv sql.NullInt64) (result int64) {
|
||||||
|
if seq.Valid {
|
||||||
|
return seq.Int64
|
||||||
|
} else if conv.Valid {
|
||||||
|
return conv.Int64
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) selectItems(ctx context.Context, query string, args ...interface{}) (results []history.Item, err error) {
|
func (mysql *MySQL) selectItems(ctx context.Context, query string, args ...interface{}) (results []history.Item, err error) {
|
||||||
rows, err := mysql.db.QueryContext(ctx, query, args...)
|
rows, err := mysql.db.QueryContext(ctx, query, args...)
|
||||||
if mysql.logError("could not select history items", err) {
|
if mysql.logError("could not select history items", err) {
|
||||||
|
|
@ -681,10 +864,8 @@ func (mysql *MySQL) selectItems(ctx context.Context, query string, args ...inter
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var blob []byte
|
var blob []byte
|
||||||
var msgid uint64
|
|
||||||
var item history.Item
|
var item history.Item
|
||||||
|
err = rows.Scan(&blob)
|
||||||
err = rows.Scan(&blob, &msgid)
|
|
||||||
if mysql.logError("could not scan history item", err) {
|
if mysql.logError("could not scan history item", err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -692,36 +873,17 @@ func (mysql *MySQL) selectItems(ctx context.Context, query string, args ...inter
|
||||||
if mysql.logError("could not unmarshal history item", err) {
|
if mysql.logError("could not unmarshal history item", err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reactions, rErr := mysql.getReactionsQuery.Query(msgid)
|
|
||||||
if mysql.logError("could not get reactions", rErr) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var react string
|
|
||||||
var total int
|
|
||||||
var sample string
|
|
||||||
for reactions.Next() {
|
|
||||||
reactions.Scan(&react, &total, &sample)
|
|
||||||
var sampleDecoded []string
|
|
||||||
json.Unmarshal([]byte(sample), &sampleDecoded)
|
|
||||||
|
|
||||||
item.Reactions = append(item.Reactions, history.Reaction{
|
|
||||||
Name: react,
|
|
||||||
Total: total,
|
|
||||||
SampleUsers: sampleDecoded,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, item)
|
results = append(results, item)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func timestampSnowflake(t time.Time) uint64 {
|
|
||||||
var ts = t.Unix() & 0xffffffffffff
|
|
||||||
return uint64(ts << 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mysql *MySQL) betweenTimestamps(ctx context.Context, target, correspondent string, after, before, cutoff time.Time, limit int) (results []history.Item, err error) {
|
func (mysql *MySQL) betweenTimestamps(ctx context.Context, target, correspondent string, after, before, cutoff time.Time, limit int) (results []history.Item, err error) {
|
||||||
|
useSequence := correspondent == ""
|
||||||
|
table := "sequence"
|
||||||
|
if !useSequence {
|
||||||
|
table = "conversations"
|
||||||
|
}
|
||||||
|
|
||||||
after, before, ascending := history.MinMaxAsc(after, before, cutoff)
|
after, before, ascending := history.MinMaxAsc(after, before, cutoff)
|
||||||
direction := "ASC"
|
direction := "ASC"
|
||||||
|
|
@ -730,30 +892,32 @@ func (mysql *MySQL) betweenTimestamps(ctx context.Context, target, correspondent
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryBuf strings.Builder
|
var queryBuf strings.Builder
|
||||||
args := make([]interface{}, 0, 7)
|
|
||||||
if correspondent == "" {
|
args := make([]interface{}, 0, 6)
|
||||||
fmt.Fprintf(&queryBuf, "SELECT data, msgid FROM history WHERE target = ? ")
|
fmt.Fprintf(&queryBuf,
|
||||||
|
"SELECT history.data from history INNER JOIN %[1]s ON history.id = %[1]s.history_id WHERE", table)
|
||||||
|
if useSequence {
|
||||||
|
fmt.Fprintf(&queryBuf, " sequence.target = ?")
|
||||||
args = append(args, target)
|
args = append(args, target)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(&queryBuf, "SELECT data, msgid FROM history WHERE (target = ? and sender = ?) OR (target = ? and sender = ?)")
|
fmt.Fprintf(&queryBuf, " conversations.target = ? AND conversations.correspondent = ?")
|
||||||
args = append(args, target, correspondent, correspondent, target)
|
args = append(args, target)
|
||||||
|
args = append(args, correspondent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !after.IsZero() {
|
if !after.IsZero() {
|
||||||
fmt.Fprintf(&queryBuf, " AND nanotime > ?")
|
fmt.Fprintf(&queryBuf, " AND %s.nanotime > ?", table)
|
||||||
args = append(args, after.UnixNano())
|
args = append(args, after.UnixNano())
|
||||||
}
|
}
|
||||||
if !before.IsZero() {
|
if !before.IsZero() {
|
||||||
fmt.Fprintf(&queryBuf, " AND nanotime <= ?")
|
fmt.Fprintf(&queryBuf, " AND %s.nanotime < ?", table)
|
||||||
args = append(args, before.UnixNano())
|
args = append(args, before.UnixNano())
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(&queryBuf, " ORDER BY %[1]s.nanotime %[2]s LIMIT ?;", table, direction)
|
||||||
fmt.Fprintf(&queryBuf, " ORDER BY nanotime %[1]s LIMIT ?;", direction)
|
|
||||||
args = append(args, limit)
|
args = append(args, limit)
|
||||||
|
|
||||||
results, err = mysql.selectItems(ctx, queryBuf.String(), args...)
|
results, err = mysql.selectItems(ctx, queryBuf.String(), args...)
|
||||||
if err == nil && !ascending {
|
if err == nil && !ascending {
|
||||||
slices.Reverse(results)
|
utils.ReverseSlice(results)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -766,19 +930,19 @@ func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target strin
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryBuf strings.Builder
|
var queryBuf strings.Builder
|
||||||
args := make([]interface{}, 0, 5)
|
args := make([]interface{}, 0, 4)
|
||||||
queryBuf.WriteString(`SELECT target, sender, nanotime from history
|
queryBuf.WriteString(`SELECT correspondents.correspondent, correspondents.nanotime from correspondents
|
||||||
WHERE target = ? OR (sender = ? and pm = true)`)
|
WHERE target = ?`)
|
||||||
args = append(args, target, target)
|
args = append(args, target)
|
||||||
if !after.IsZero() {
|
if !after.IsZero() {
|
||||||
queryBuf.WriteString(" AND nanotime > ?")
|
queryBuf.WriteString(" AND correspondents.nanotime > ?")
|
||||||
args = append(args, after.UnixNano())
|
args = append(args, after.UnixNano())
|
||||||
}
|
}
|
||||||
if !before.IsZero() {
|
if !before.IsZero() {
|
||||||
queryBuf.WriteString(" AND nanotime < ?")
|
queryBuf.WriteString(" AND correspondents.nanotime < ?")
|
||||||
args = append(args, before.UnixNano())
|
args = append(args, before.UnixNano())
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&queryBuf, " ORDER BY nanotime %s LIMIT ?;", direction)
|
fmt.Fprintf(&queryBuf, " ORDER BY correspondents.nanotime %s LIMIT ?;", direction)
|
||||||
args = append(args, limit)
|
args = append(args, limit)
|
||||||
query := queryBuf.String()
|
query := queryBuf.String()
|
||||||
|
|
||||||
|
|
@ -787,30 +951,21 @@ func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target strin
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var msgTarget string
|
var correspondent string
|
||||||
var msgSender string
|
|
||||||
var nanotime int64
|
var nanotime int64
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = rows.Scan(&msgTarget, &msgSender, &nanotime)
|
err = rows.Scan(&correspondent, &nanotime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if msgTarget == target {
|
|
||||||
results = append(results, history.TargetListing{
|
results = append(results, history.TargetListing{
|
||||||
CfName: msgSender,
|
CfName: correspondent,
|
||||||
Time: time.Unix(0, nanotime),
|
Time: time.Unix(0, nanotime),
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
results = append(results, history.TargetListing{
|
|
||||||
CfName: msgTarget,
|
|
||||||
Time: time.Unix(0, nanotime),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ascending {
|
if !ascending {
|
||||||
slices.Reverse(results)
|
utils.ReverseSlice(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
@ -886,7 +1041,6 @@ func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
startTime := start.Time
|
startTime := start.Time
|
||||||
|
|
||||||
if start.Msgid != "" {
|
if start.Msgid != "" {
|
||||||
startTime, _, _, err = s.mysql.lookupMsgid(ctx, start.Msgid, false)
|
startTime, _, _, err = s.mysql.lookupMsgid(ctx, start.Msgid, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -900,7 +1054,6 @@ func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (
|
||||||
endTime := end.Time
|
endTime := end.Time
|
||||||
if end.Msgid != "" {
|
if end.Msgid != "" {
|
||||||
endTime, _, _, err = s.mysql.lookupMsgid(ctx, end.Msgid, false)
|
endTime, _, _, err = s.mysql.lookupMsgid(ctx, end.Msgid, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
@ -947,41 +1100,3 @@ func (mysql *MySQL) MakeSequence(target, correspondent string, cutoff time.Time)
|
||||||
cutoff: cutoff,
|
cutoff: cutoff,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) GetPMs(casefoldedUser string) (results map[string]int64, err error) {
|
|
||||||
if mysql.db == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
results = make(map[string]int64)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var queryBuf strings.Builder
|
|
||||||
args := make([]interface{}, 0)
|
|
||||||
|
|
||||||
queryBuf.WriteString(`SELECT max(nanotime), target, sender FROM history WHERE target = ? OR (sender = ? and pm = true) GROUP BY target, sender;`)
|
|
||||||
args = append(args, casefoldedUser, casefoldedUser)
|
|
||||||
|
|
||||||
rows, err := mysql.db.QueryContext(ctx, queryBuf.String(), args...)
|
|
||||||
if mysql.logError("could not get pms", err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var last int64
|
|
||||||
var target, sender string
|
|
||||||
for rows.Next() {
|
|
||||||
err = rows.Scan(&last, &target, &sender)
|
|
||||||
if mysql.logError("could not get pms", err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// We really don't need nanosecond precision
|
|
||||||
if target != casefoldedUser {
|
|
||||||
results[target] = last / 1000000
|
|
||||||
} else {
|
|
||||||
results[sender] = last / 1000000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/history"
|
"github.com/ergochat/ergo/irc/history"
|
||||||
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 123 / '{' is the magic number that means JSON;
|
// 123 / '{' is the magic number that means JSON;
|
||||||
|
|
@ -16,3 +17,7 @@ func marshalItem(item *history.Item) (result []byte, err error) {
|
||||||
func unmarshalItem(data []byte, result *history.Item) (err error) {
|
func unmarshalItem(data []byte, result *history.Item) (err error) {
|
||||||
return json.Unmarshal(data, result)
|
return json.Unmarshal(data, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeMsgid(msgid string) ([]byte, error) {
|
||||||
|
return utils.B32Encoder.DecodeString(msgid)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
||||||
origNickMask := details.nickMask
|
origNickMask := details.nickMask
|
||||||
isSanick := client != target
|
isSanick := client != target
|
||||||
|
|
||||||
assignedNickname, err, awayChanged := client.server.clients.SetNick(target, session, nickname, false)
|
assignedNickname, err, back := client.server.clients.SetNick(target, session, nickname, false)
|
||||||
if err == errNicknameInUse {
|
if err == errNicknameInUse {
|
||||||
if !isSanick {
|
if !isSanick {
|
||||||
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, details.nick, utils.SafeErrorParam(nickname), client.t("Nickname is already in use"))
|
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, details.nick, utils.SafeErrorParam(nickname), client.t("Nickname is already in use"))
|
||||||
|
|
@ -43,8 +43,6 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
||||||
}
|
}
|
||||||
} else if err == errNicknameReserved {
|
} else if err == errNicknameReserved {
|
||||||
if !isSanick {
|
if !isSanick {
|
||||||
// see #1594 for context: ERR_NICKNAMEINUSE can confuse clients if the nickname is not
|
|
||||||
// literally in use:
|
|
||||||
if !client.registered {
|
if !client.registered {
|
||||||
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, details.nick, utils.SafeErrorParam(nickname), client.t("Nickname is reserved by a different account"))
|
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, details.nick, utils.SafeErrorParam(nickname), client.t("Nickname is reserved by a different account"))
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +93,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
||||||
histItem := history.Item{
|
histItem := history.Item{
|
||||||
Type: history.Nick,
|
Type: history.Nick,
|
||||||
Nick: origNickMask,
|
Nick: origNickMask,
|
||||||
Account: details.account,
|
AccountName: details.accountName,
|
||||||
Message: message,
|
Message: message,
|
||||||
IsBot: isBot,
|
IsBot: isBot,
|
||||||
}
|
}
|
||||||
|
|
@ -117,17 +115,13 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if awayChanged {
|
if back {
|
||||||
dispatchAwayNotify(session.client, session.client.AwayMessage())
|
dispatchAwayNotify(session.client, false, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, channel := range target.Channels() {
|
for _, channel := range target.Channels() {
|
||||||
if channel.memberIsVisible(client) {
|
|
||||||
// I LOVE MUTATING STATE!
|
|
||||||
histItem.Target = channel.NameCasefolded()
|
|
||||||
channel.AddHistoryItem(histItem, details.account)
|
channel.AddHistoryItem(histItem, details.account)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
newCfnick := target.NickCasefolded()
|
newCfnick := target.NickCasefolded()
|
||||||
if newCfnick != details.nickCasefolded {
|
if newCfnick != details.nickCasefolded {
|
||||||
|
|
|
||||||
|
|
@ -811,7 +811,7 @@ func nsGroupHandler(service *ircService, server *Server, client *Client, command
|
||||||
func nsLoginThrottleCheck(service *ircService, client *Client, rb *ResponseBuffer) (success bool) {
|
func nsLoginThrottleCheck(service *ircService, client *Client, rb *ResponseBuffer) (success bool) {
|
||||||
throttled, remainingTime := client.checkLoginThrottle()
|
throttled, remainingTime := client.checkLoginThrottle()
|
||||||
if throttled {
|
if throttled {
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime.Round(time.Millisecond)))
|
service.Notice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
|
||||||
}
|
}
|
||||||
return !throttled
|
return !throttled
|
||||||
}
|
}
|
||||||
|
|
@ -954,9 +954,9 @@ func nsInfoHandler(service *ircService, server *Server, client *Client, command
|
||||||
|
|
||||||
func listRegisteredChannels(service *ircService, accountName string, rb *ResponseBuffer) {
|
func listRegisteredChannels(service *ircService, accountName string, rb *ResponseBuffer) {
|
||||||
client := rb.session.client
|
client := rb.session.client
|
||||||
channels := client.server.channels.ChannelsForAccount(accountName)
|
channels := client.server.accounts.ChannelsForAccount(accountName)
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Account %s has %d registered channel(s)."), accountName, len(channels)))
|
service.Notice(rb, fmt.Sprintf(client.t("Account %s has %d registered channel(s)."), accountName, len(channels)))
|
||||||
for _, channel := range channels {
|
for _, channel := range rb.session.client.server.accounts.ChannelsForAccount(accountName) {
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
|
service.Notice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1398,11 +1398,6 @@ func nsCertHandler(service *ircService, server *Server, client *Client, command
|
||||||
case "add", "del":
|
case "add", "del":
|
||||||
if 2 <= len(params) {
|
if 2 <= len(params) {
|
||||||
target, certfp = params[0], params[1]
|
target, certfp = params[0], params[1]
|
||||||
if cftarget, err := CasefoldName(target); err == nil && client.Account() == cftarget {
|
|
||||||
// If the target is equal to the account, then the user accidentally invoked operator
|
|
||||||
// syntax (cert add mynick <fp>) instead of self syntax (cert add <fp>).
|
|
||||||
target = ""
|
|
||||||
}
|
|
||||||
} else if len(params) == 1 {
|
} else if len(params) == 1 {
|
||||||
certfp = params[0]
|
certfp = params[0]
|
||||||
} else if len(params) == 0 && verb == "add" && rb.session.certfp != "" {
|
} else if len(params) == 0 && verb == "add" && rb.session.certfp != "" {
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,9 @@ const (
|
||||||
ERR_SASLABORTED = "906"
|
ERR_SASLABORTED = "906"
|
||||||
ERR_SASLALREADY = "907"
|
ERR_SASLALREADY = "907"
|
||||||
RPL_SASLMECHS = "908"
|
RPL_SASLMECHS = "908"
|
||||||
|
RPL_REG_SUCCESS = "920"
|
||||||
|
RPL_VERIFY_SUCCESS = "923"
|
||||||
|
RPL_REG_VERIFICATION_REQUIRED = "927"
|
||||||
ERR_TOOMANYLANGUAGES = "981"
|
ERR_TOOMANYLANGUAGES = "981"
|
||||||
ERR_NOLANGUAGE = "982"
|
ERR_NOLANGUAGE = "982"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
// Copyright 2022-2023 Simon Ser <contact@emersion.fr>
|
|
||||||
// Derived from https://git.sr.ht/~emersion/soju/tree/36d6cb19a4f90d217d55afb0b15318321baaad09/item/auth/oauth2.go
|
|
||||||
// Originally released under the AGPLv3, relicensed to the Ergo project under the MIT license
|
|
||||||
// Modifications copyright 2024 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
||||||
// Released under the MIT license
|
|
||||||
|
|
||||||
package oauth2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrAuthDisabled = fmt.Errorf("OAuth 2.0 authentication is disabled")
|
|
||||||
|
|
||||||
// all cases where the infrastructure is working correctly, but we determined
|
|
||||||
// that the user supplied an invalid token
|
|
||||||
ErrInvalidToken = fmt.Errorf("OAuth 2.0 bearer token invalid")
|
|
||||||
)
|
|
||||||
|
|
||||||
type OAuth2BearerConfig struct {
|
|
||||||
Enabled bool `yaml:"enabled"`
|
|
||||||
Autocreate bool `yaml:"autocreate"`
|
|
||||||
AuthScript bool `yaml:"auth-script"`
|
|
||||||
IntrospectionURL string `yaml:"introspection-url"`
|
|
||||||
IntrospectionTimeout time.Duration `yaml:"introspection-timeout"`
|
|
||||||
// omit for `none`, required for `client_secret_basic`
|
|
||||||
ClientID string `yaml:"client-id"`
|
|
||||||
ClientSecret string `yaml:"client-secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OAuth2BearerConfig) Postprocess() error {
|
|
||||||
if !o.Enabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.IntrospectionTimeout == 0 {
|
|
||||||
return fmt.Errorf("a nonzero oauthbearer introspection timeout is required (try 10s)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := url.Parse(o.IntrospectionURL); err != nil {
|
|
||||||
return fmt.Errorf("invalid introspection-url: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OAuth2BearerConfig) Introspect(ctx context.Context, token string) (username string, err error) {
|
|
||||||
if !o.Enabled {
|
|
||||||
return "", ErrAuthDisabled
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, o.IntrospectionTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
reqValues := make(url.Values)
|
|
||||||
reqValues.Set("token", token)
|
|
||||||
|
|
||||||
reqBody := strings.NewReader(reqValues.Encode())
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, o.IntrospectionURL, reqBody)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create OAuth 2.0 introspection request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
if o.ClientID != "" {
|
|
||||||
req.SetBasicAuth(url.QueryEscape(o.ClientID), url.QueryEscape(o.ClientSecret))
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to send OAuth 2.0 introspection request: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return "", fmt.Errorf("OAuth 2.0 introspection error: %v", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
var data oauth2Introspection
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to decode OAuth 2.0 introspection response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !data.Active {
|
|
||||||
return "", ErrInvalidToken
|
|
||||||
}
|
|
||||||
if data.Username == "" {
|
|
||||||
// We really need the username here, otherwise an OAuth 2.0 user can
|
|
||||||
// impersonate any other user.
|
|
||||||
return "", fmt.Errorf("missing username in OAuth 2.0 introspection response")
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.Username, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type oauth2Introspection struct {
|
|
||||||
Active bool `json:"active"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
package oauth2
|
|
||||||
|
|
||||||
/*
|
|
||||||
https://github.com/emersion/go-sasl/blob/e73c9f7bad438a9bf3f5b28e661b74d752ecafdd/oauthbearer.go
|
|
||||||
|
|
||||||
Copyright 2019-2022 Simon Ser, Frode Aannevik, Max Mazurov
|
|
||||||
Released under the MIT license
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrUnexpectedClientResponse = errors.New("unexpected client response")
|
|
||||||
)
|
|
||||||
|
|
||||||
// The OAUTHBEARER mechanism name.
|
|
||||||
const OAuthBearer = "OAUTHBEARER"
|
|
||||||
|
|
||||||
type OAuthBearerError struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Schemes string `json:"schemes"`
|
|
||||||
Scope string `json:"scope"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OAuthBearerOptions struct {
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
Token string `json:"token,omitempty"`
|
|
||||||
Host string `json:"host,omitempty"`
|
|
||||||
Port int `json:"port,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *OAuthBearerError) Error() string {
|
|
||||||
return fmt.Sprintf("OAUTHBEARER authentication error (%v)", err.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
type OAuthBearerAuthenticator func(opts OAuthBearerOptions) *OAuthBearerError
|
|
||||||
|
|
||||||
type OAuthBearerServer struct {
|
|
||||||
done bool
|
|
||||||
failErr error
|
|
||||||
authenticate OAuthBearerAuthenticator
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *OAuthBearerServer) fail(descr string) ([]byte, bool, error) {
|
|
||||||
blob, err := json.Marshal(OAuthBearerError{
|
|
||||||
Status: "invalid_request",
|
|
||||||
Schemes: "bearer",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // wtf
|
|
||||||
}
|
|
||||||
a.failErr = errors.New(descr)
|
|
||||||
return blob, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *OAuthBearerServer) Next(response []byte) (challenge []byte, done bool, err error) {
|
|
||||||
// Per RFC, we cannot just send an error, we need to return JSON-structured
|
|
||||||
// value as a challenge and then after getting dummy response from the
|
|
||||||
// client stop the exchange.
|
|
||||||
if a.failErr != nil {
|
|
||||||
// Server libraries (go-smtp, go-imap) will not call Next on
|
|
||||||
// protocol-specific SASL cancel response ('*'). However, GS2 (and
|
|
||||||
// indirectly OAUTHBEARER) defines a protocol-independent way to do so
|
|
||||||
// using 0x01.
|
|
||||||
if len(response) != 1 && response[0] != 0x01 {
|
|
||||||
return nil, true, errors.New("unexpected response")
|
|
||||||
}
|
|
||||||
return nil, true, a.failErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.done {
|
|
||||||
err = ErrUnexpectedClientResponse
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate empty challenge.
|
|
||||||
if response == nil {
|
|
||||||
return []byte{}, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
a.done = true
|
|
||||||
|
|
||||||
// Cut n,a=username,\x01host=...\x01auth=...
|
|
||||||
// into
|
|
||||||
// n
|
|
||||||
// a=username
|
|
||||||
// \x01host=...\x01auth=...\x01\x01
|
|
||||||
parts := bytes.SplitN(response, []byte{','}, 3)
|
|
||||||
if len(parts) != 3 {
|
|
||||||
return a.fail("Invalid response")
|
|
||||||
}
|
|
||||||
flag := parts[0]
|
|
||||||
authzid := parts[1]
|
|
||||||
if !bytes.Equal(flag, []byte{'n'}) {
|
|
||||||
return a.fail("Invalid response, missing 'n' in gs2-cb-flag")
|
|
||||||
}
|
|
||||||
opts := OAuthBearerOptions{}
|
|
||||||
if len(authzid) > 0 {
|
|
||||||
if !bytes.HasPrefix(authzid, []byte("a=")) {
|
|
||||||
return a.fail("Invalid response, missing 'a=' in gs2-authzid")
|
|
||||||
}
|
|
||||||
opts.Username = string(bytes.TrimPrefix(authzid, []byte("a=")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cut \x01host=...\x01auth=...\x01\x01
|
|
||||||
// into
|
|
||||||
// *empty*
|
|
||||||
// host=...
|
|
||||||
// auth=...
|
|
||||||
// *empty*
|
|
||||||
//
|
|
||||||
// Note that this code does not do a lot of checks to make sure the input
|
|
||||||
// follows the exact format specified by RFC.
|
|
||||||
params := bytes.Split(parts[2], []byte{0x01})
|
|
||||||
for _, p := range params {
|
|
||||||
// Skip empty fields (one at start and end).
|
|
||||||
if len(p) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pParts := bytes.SplitN(p, []byte{'='}, 2)
|
|
||||||
if len(pParts) != 2 {
|
|
||||||
return a.fail("Invalid response, missing '='")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch string(pParts[0]) {
|
|
||||||
case "host":
|
|
||||||
opts.Host = string(pParts[1])
|
|
||||||
case "port":
|
|
||||||
port, err := strconv.ParseUint(string(pParts[1]), 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return a.fail("Invalid response, malformed 'port' value")
|
|
||||||
}
|
|
||||||
opts.Port = int(port)
|
|
||||||
case "auth":
|
|
||||||
const prefix = "bearer "
|
|
||||||
strValue := string(pParts[1])
|
|
||||||
// Token type is case-insensitive.
|
|
||||||
if !strings.HasPrefix(strings.ToLower(strValue), prefix) {
|
|
||||||
return a.fail("Unsupported token type")
|
|
||||||
}
|
|
||||||
opts.Token = strValue[len(prefix):]
|
|
||||||
default:
|
|
||||||
return a.fail("Invalid response, unknown parameter: " + string(pParts[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
authzErr := a.authenticate(opts)
|
|
||||||
if authzErr != nil {
|
|
||||||
blob, err := json.Marshal(authzErr)
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // wtf
|
|
||||||
}
|
|
||||||
a.failErr = authzErr
|
|
||||||
return blob, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOAuthBearerServer(auth OAuthBearerAuthenticator) *OAuthBearerServer {
|
|
||||||
return &OAuthBearerServer{
|
|
||||||
authenticate: auth,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,16 +22,6 @@ func TestBasic(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVector(t *testing.T) {
|
|
||||||
// sanity check for persisted hashes
|
|
||||||
if CompareHashAndPassword(
|
|
||||||
[]byte("$2a$12$sJokyLJ5px3Nb51DEDhsQ.wh8nfwEYuMbVYrpqO5v9Ylyj0YyVWj."),
|
|
||||||
[]byte("this is my passphrase"),
|
|
||||||
) != nil {
|
|
||||||
t.Errorf("hash comparison failed unexpectedly")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLongPassphrases(t *testing.T) {
|
func TestLongPassphrases(t *testing.T) {
|
||||||
longPassphrase := make([]byte, 168)
|
longPassphrase := make([]byte, 168)
|
||||||
for i := range longPassphrase {
|
for i := range longPassphrase {
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,7 @@
|
||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ergochat/ergo/irc/history"
|
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/caps"
|
"github.com/ergochat/ergo/irc/caps"
|
||||||
|
|
@ -124,12 +121,8 @@ func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMa
|
||||||
rb.AddMessage(msg)
|
rb.AddMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, isBot bool, tags map[string]string, command string, target string, message utils.SplitMessage) {
|
|
||||||
rb.AddSplitMessageFromClientWithReactions(fromNickMask, fromAccount, isBot, tags, command, target, message, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddSplitMessageFromClient adds a new split message from a specific client to our queue.
|
// AddSplitMessageFromClient adds a new split message from a specific client to our queue.
|
||||||
func (rb *ResponseBuffer) AddSplitMessageFromClientWithReactions(fromNickMask string, fromAccount string, isBot bool, tags map[string]string, command string, target string, message utils.SplitMessage, reactions []history.Reaction) {
|
func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, isBot bool, tags map[string]string, command string, target string, message utils.SplitMessage) {
|
||||||
if message.Is512() {
|
if message.Is512() {
|
||||||
if message.Message == "" {
|
if message.Message == "" {
|
||||||
// XXX this is a TAGMSG
|
// XXX this is a TAGMSG
|
||||||
|
|
@ -149,23 +142,8 @@ func (rb *ResponseBuffer) AddSplitMessageFromClientWithReactions(fromNickMask st
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
msgid = message.Msgid
|
msgid = message.Msgid
|
||||||
}
|
}
|
||||||
mergedTags := make(map[string]string)
|
rb.AddFromClient(message.Time, msgid, fromNickMask, fromAccount, isBot, tags, command, target, messagePair.Message)
|
||||||
for k, v := range tags {
|
|
||||||
mergedTags[k] = v
|
|
||||||
}
|
}
|
||||||
for k, v := range messagePair.Tags {
|
|
||||||
mergedTags[k] = v
|
|
||||||
}
|
|
||||||
rb.AddFromClient(message.Time, msgid, fromNickMask, fromAccount, isBot, mergedTags, command, target, messagePair.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if reactions != nil && len(reactions) >= 1 {
|
|
||||||
var text string
|
|
||||||
for _, react := range reactions {
|
|
||||||
text = strings.Join([]string{message.Msgid, react.Name, strconv.Itoa(react.Total)}, " ")
|
|
||||||
text += " " + strings.Join(react.SampleUsers, " ")
|
|
||||||
rb.Add(nil, rb.target.server.name, "REACTIONS", text)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,9 +193,6 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
|
||||||
// Starts a nested batch (see the ResponseBuffer struct definition for a description of
|
// Starts a nested batch (see the ResponseBuffer struct definition for a description of
|
||||||
// how this works)
|
// how this works)
|
||||||
func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) {
|
func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) {
|
||||||
if !rb.session.capabilities.Has(caps.Batch) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
batchID = rb.session.generateBatchID()
|
batchID = rb.session.generateBatchID()
|
||||||
msgParams := make([]string, len(params)+2)
|
msgParams := make([]string, len(params)+2)
|
||||||
msgParams[0] = "+" + batchID
|
msgParams[0] = "+" + batchID
|
||||||
|
|
@ -244,6 +219,19 @@ func (rb *ResponseBuffer) EndNestedBatch(batchID string) {
|
||||||
rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+batchID))
|
rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+batchID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convenience to start a nested batch for history lines, at the highest level
|
||||||
|
// supported by the client (`history`, `chathistory`, or no batch, in descending order).
|
||||||
|
func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID string) {
|
||||||
|
var batchType string
|
||||||
|
if rb.session.capabilities.Has(caps.Batch) {
|
||||||
|
batchType = "chathistory"
|
||||||
|
}
|
||||||
|
if batchType != "" {
|
||||||
|
batchID = rb.StartNestedBatch(batchType, params...)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Send sends all messages in the buffer to the client.
|
// Send sends all messages in the buffer to the client.
|
||||||
// Afterwards, the buffer is in an undefined state and MUST NOT be used further.
|
// Afterwards, the buffer is in an undefined state and MUST NOT be used further.
|
||||||
// If `blocking` is true you MUST be sending to the client from its own goroutine.
|
// If `blocking` is true you MUST be sending to the client from its own goroutine.
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,6 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
|
||||||
Type: history.Privmsg,
|
Type: history.Privmsg,
|
||||||
Message: splitMessage,
|
Message: splitMessage,
|
||||||
Nick: sourceMask,
|
Nick: sourceMask,
|
||||||
Target: target,
|
|
||||||
// TODO: does this work?
|
|
||||||
Account: "$RP",
|
|
||||||
}, client.Account())
|
}, client.Account())
|
||||||
} else {
|
} else {
|
||||||
target, err := CasefoldName(targetString)
|
target, err := CasefoldName(targetString)
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,8 @@ package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -24,27 +21,7 @@ type scriptResponse struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunHttp(command string, args []string, input []byte, timeout time.Duration) (output []byte, err error) {
|
|
||||||
client := http.Client{
|
|
||||||
Timeout: timeout,
|
|
||||||
}
|
|
||||||
post, err := client.Post(command, "application/json", bytes.NewBuffer(input))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer post.Body.Close()
|
|
||||||
output, err = io.ReadAll(post.Body)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunScript(command string, args []string, input []byte, timeout, killTimeout time.Duration) (output []byte, err error) {
|
func RunScript(command string, args []string, input []byte, timeout, killTimeout time.Duration) (output []byte, err error) {
|
||||||
if strings.HasPrefix(command, "http") {
|
|
||||||
return RunHttp(command, args, input, timeout)
|
|
||||||
}
|
|
||||||
cmd := exec.Command(command, args...)
|
cmd := exec.Command(command, args...)
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
37
irc/serde.go
37
irc/serde.go
|
|
@ -1,37 +0,0 @@
|
||||||
// Copyright (c) 2022 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package irc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/datastore"
|
|
||||||
"github.com/ergochat/ergo/irc/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Serializable interface {
|
|
||||||
Serialize() ([]byte, error)
|
|
||||||
Deserialize([]byte) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchAndDeserializeAll[T any, C interface {
|
|
||||||
*T
|
|
||||||
Serializable
|
|
||||||
}](table datastore.Table, dstore datastore.Datastore, log *logger.Manager) (result []T, err error) {
|
|
||||||
rawRecords, err := dstore.GetAll(table)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result = make([]T, len(rawRecords))
|
|
||||||
pos := 0
|
|
||||||
for _, record := range rawRecords {
|
|
||||||
err := C(&result[pos]).Deserialize(record.Value)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("internal", "deserialization error", strconv.Itoa(int(table)), record.UUID.String(), err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
return result[:pos], nil
|
|
||||||
}
|
|
||||||
|
|
@ -22,12 +22,9 @@ import (
|
||||||
|
|
||||||
"github.com/ergochat/irc-go/ircfmt"
|
"github.com/ergochat/irc-go/ircfmt"
|
||||||
"github.com/okzk/sdnotify"
|
"github.com/okzk/sdnotify"
|
||||||
"github.com/tidwall/buntdb"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/bunt"
|
|
||||||
"github.com/ergochat/ergo/irc/caps"
|
"github.com/ergochat/ergo/irc/caps"
|
||||||
"github.com/ergochat/ergo/irc/connection_limits"
|
"github.com/ergochat/ergo/irc/connection_limits"
|
||||||
"github.com/ergochat/ergo/irc/datastore"
|
|
||||||
"github.com/ergochat/ergo/irc/flatip"
|
"github.com/ergochat/ergo/irc/flatip"
|
||||||
"github.com/ergochat/ergo/irc/flock"
|
"github.com/ergochat/ergo/irc/flock"
|
||||||
"github.com/ergochat/ergo/irc/history"
|
"github.com/ergochat/ergo/irc/history"
|
||||||
|
|
@ -36,8 +33,7 @@ import (
|
||||||
"github.com/ergochat/ergo/irc/mysql"
|
"github.com/ergochat/ergo/irc/mysql"
|
||||||
"github.com/ergochat/ergo/irc/sno"
|
"github.com/ergochat/ergo/irc/sno"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
|
"github.com/tidwall/buntdb"
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -70,6 +66,7 @@ type Server struct {
|
||||||
accepts AcceptManager
|
accepts AcceptManager
|
||||||
accounts AccountManager
|
accounts AccountManager
|
||||||
channels ChannelManager
|
channels ChannelManager
|
||||||
|
channelRegistry ChannelRegistry
|
||||||
clients ClientManager
|
clients ClientManager
|
||||||
config atomic.Pointer[Config]
|
config atomic.Pointer[Config]
|
||||||
configFilename string
|
configFilename string
|
||||||
|
|
@ -90,7 +87,6 @@ type Server struct {
|
||||||
tracebackSignal chan os.Signal
|
tracebackSignal chan os.Signal
|
||||||
snomasks SnoManager
|
snomasks SnoManager
|
||||||
store *buntdb.DB
|
store *buntdb.DB
|
||||||
dstore datastore.Datastore
|
|
||||||
historyDB mysql.MySQL
|
historyDB mysql.MySQL
|
||||||
torLimiter connection_limits.TorLimiter
|
torLimiter connection_limits.TorLimiter
|
||||||
whoWas WhoWasList
|
whoWas WhoWasList
|
||||||
|
|
@ -98,17 +94,10 @@ type Server struct {
|
||||||
semaphores ServerSemaphores
|
semaphores ServerSemaphores
|
||||||
flock flock.Flocker
|
flock flock.Flocker
|
||||||
defcon atomic.Uint32
|
defcon atomic.Uint32
|
||||||
|
|
||||||
// CEF
|
|
||||||
redis *redis.Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer returns a new Oragono server.
|
// NewServer returns a new Oragono server.
|
||||||
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
||||||
// sanity check that kernel randomness is available; on modern Linux,
|
|
||||||
// this will block until it is, on other platforms it may panic:
|
|
||||||
utils.GenerateUUIDv4()
|
|
||||||
|
|
||||||
// initialize data structures
|
// initialize data structures
|
||||||
server := &Server{
|
server := &Server{
|
||||||
ctime: time.Now().UTC(),
|
ctime: time.Now().UTC(),
|
||||||
|
|
@ -168,13 +157,6 @@ func (server *Server) Shutdown() {
|
||||||
func (server *Server) Run() {
|
func (server *Server) Run() {
|
||||||
defer server.Shutdown()
|
defer server.Shutdown()
|
||||||
|
|
||||||
redisOpts, err := redis.ParseURL(server.Config().Cef.Redis)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
server.redis = redis.NewClient(redisOpts)
|
|
||||||
startRedis(server)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-server.exitSignals:
|
case <-server.exitSignals:
|
||||||
|
|
@ -326,7 +308,9 @@ func (server *Server) checkBanScriptExemptSASL(config *Config, session *Session)
|
||||||
func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
||||||
// XXX PROXY or WEBIRC MUST be sent as the first line of the session;
|
// XXX PROXY or WEBIRC MUST be sent as the first line of the session;
|
||||||
// if we are here at all that means we have the final value of the IP
|
// if we are here at all that means we have the final value of the IP
|
||||||
c.finalizeHostname(session)
|
if session.rawHostname == "" {
|
||||||
|
session.client.lookupHostname(session, false)
|
||||||
|
}
|
||||||
|
|
||||||
// try to complete registration normally
|
// try to complete registration normally
|
||||||
// XXX(#1057) username can be filled in by an ident query without the client
|
// XXX(#1057) username can be filled in by an ident query without the client
|
||||||
|
|
@ -369,7 +353,10 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
||||||
rb := NewResponseBuffer(session)
|
rb := NewResponseBuffer(session)
|
||||||
nickError := performNickChange(server, c, c, session, c.preregNick, rb)
|
nickError := performNickChange(server, c, c, session, c.preregNick, rb)
|
||||||
rb.Send(true)
|
rb.Send(true)
|
||||||
if nickError != nil {
|
if nickError == errInsecureReattach {
|
||||||
|
c.Quit(c.t("You can't mix secure and insecure connections to this account"), nil)
|
||||||
|
return true
|
||||||
|
} else if nickError != nil {
|
||||||
c.preregNick = ""
|
c.preregNick = ""
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -404,12 +391,6 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
server.playRegistrationBurst(session)
|
server.playRegistrationBurst(session)
|
||||||
|
|
||||||
if len(config.Channels.AutoJoin) > 0 {
|
|
||||||
// only applicable to new clients, not reattaches:
|
|
||||||
server.handleAutojoins(session, config.Channels.AutoJoin)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,9 +427,7 @@ func (server *Server) playRegistrationBurst(session *Session) {
|
||||||
session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, rplMyInfo1, rplMyInfo2, rplMyInfo3)
|
session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, rplMyInfo1, rplMyInfo2, rplMyInfo3)
|
||||||
|
|
||||||
rb := NewResponseBuffer(session)
|
rb := NewResponseBuffer(session)
|
||||||
if !(rb.session.capabilities.Has(caps.ExtendedISupport) && rb.session.isupportSentPrereg) {
|
|
||||||
server.RplISupport(c, rb)
|
server.RplISupport(c, rb)
|
||||||
}
|
|
||||||
if d.account != "" && session.capabilities.Has(caps.Persistence) {
|
if d.account != "" && session.capabilities.Has(caps.Persistence) {
|
||||||
reportPersistenceStatus(c, rb, false)
|
reportPersistenceStatus(c, rb, false)
|
||||||
}
|
}
|
||||||
|
|
@ -470,17 +449,10 @@ func (server *Server) playRegistrationBurst(session *Session) {
|
||||||
|
|
||||||
// RplISupport outputs our ISUPPORT lines to the client. This is used on connection and in VERSION responses.
|
// RplISupport outputs our ISUPPORT lines to the client. This is used on connection and in VERSION responses.
|
||||||
func (server *Server) RplISupport(client *Client, rb *ResponseBuffer) {
|
func (server *Server) RplISupport(client *Client, rb *ResponseBuffer) {
|
||||||
server.sendRplISupportLines(client, rb, server.Config().Server.isupport.CachedReply)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) sendRplISupportLines(client *Client, rb *ResponseBuffer, lines [][]string) {
|
|
||||||
if rb.session.capabilities.Has(caps.ExtendedISupport) {
|
|
||||||
batchID := rb.StartNestedBatch(caps.ExtendedISupportBatchType)
|
|
||||||
defer rb.EndNestedBatch(batchID)
|
|
||||||
}
|
|
||||||
translatedISupport := client.t("are supported by this server")
|
translatedISupport := client.t("are supported by this server")
|
||||||
nick := client.Nick()
|
nick := client.Nick()
|
||||||
for _, cachedTokenLine := range lines {
|
config := server.Config()
|
||||||
|
for _, cachedTokenLine := range config.Server.isupport.CachedReply {
|
||||||
length := len(cachedTokenLine) + 2
|
length := len(cachedTokenLine) + 2
|
||||||
tokenline := make([]string, length)
|
tokenline := make([]string, length)
|
||||||
tokenline[0] = nick
|
tokenline[0] = nick
|
||||||
|
|
@ -527,14 +499,6 @@ func (server *Server) MOTD(client *Client, rb *ResponseBuffer) {
|
||||||
rb.Add(nil, server.name, RPL_ENDOFMOTD, client.nick, client.t("End of MOTD command"))
|
rb.Add(nil, server.name, RPL_ENDOFMOTD, client.nick, client.t("End of MOTD command"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) handleAutojoins(session *Session, channelNames []string) {
|
|
||||||
rb := NewResponseBuffer(session)
|
|
||||||
for _, chname := range channelNames {
|
|
||||||
server.channels.Join(session.client, chname, "", false, rb)
|
|
||||||
}
|
|
||||||
rb.Send(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) whoisChannelsNames(target *Client, multiPrefix bool, hasPrivs bool) []string {
|
func (client *Client) whoisChannelsNames(target *Client, multiPrefix bool, hasPrivs bool) []string {
|
||||||
var chstrs []string
|
var chstrs []string
|
||||||
targetInvis := target.HasMode(modes.Invisible)
|
targetInvis := target.HasMode(modes.Invisible)
|
||||||
|
|
@ -721,6 +685,9 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
||||||
if !oldConfig.Accounts.NickReservation.Enabled {
|
if !oldConfig.Accounts.NickReservation.Enabled {
|
||||||
server.accounts.buildNickToAccountIndex(config)
|
server.accounts.buildNickToAccountIndex(config)
|
||||||
}
|
}
|
||||||
|
if !oldConfig.Channels.Registration.Enabled {
|
||||||
|
server.channels.loadRegisteredChannels(config)
|
||||||
|
}
|
||||||
// resize history buffers as needed
|
// resize history buffers as needed
|
||||||
if config.historyChangedFrom(oldConfig) {
|
if config.historyChangedFrom(oldConfig) {
|
||||||
for _, channel := range server.channels.Channels() {
|
for _, channel := range server.channels.Channels() {
|
||||||
|
|
@ -749,11 +716,7 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
||||||
// now that the datastore is initialized, we can load the cloak secret from it
|
// now that the datastore is initialized, we can load the cloak secret from it
|
||||||
// XXX this modifies config after the initial load, which is naughty,
|
// XXX this modifies config after the initial load, which is naughty,
|
||||||
// but there's no data race because we haven't done SetConfig yet
|
// but there's no data race because we haven't done SetConfig yet
|
||||||
cloakSecret, err := LoadCloakSecret(server.dstore)
|
config.Server.Cloaks.SetSecret(LoadCloakSecret(server.store))
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Could not load cloak secret: %w", err)
|
|
||||||
}
|
|
||||||
config.Server.Cloaks.SetSecret(cloakSecret)
|
|
||||||
|
|
||||||
// activate the new config
|
// activate the new config
|
||||||
server.config.Store(config)
|
server.config.Store(config)
|
||||||
|
|
@ -815,19 +778,13 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !initial {
|
if !initial {
|
||||||
// send 005 updates (somewhat rare)
|
// push new info to all of our clients
|
||||||
if len(newISupportReplies) != 0 {
|
|
||||||
for _, sClient := range server.clients.AllClients() {
|
for _, sClient := range server.clients.AllClients() {
|
||||||
for _, session := range sClient.Sessions() {
|
for _, tokenline := range newISupportReplies {
|
||||||
rb := NewResponseBuffer(session)
|
sClient.Send(nil, server.name, RPL_ISUPPORT, append([]string{sClient.nick}, tokenline...)...)
|
||||||
server.sendRplISupportLines(sClient, rb, newISupportReplies)
|
|
||||||
rb.Send(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if sendRawOutputNotice {
|
if sendRawOutputNotice {
|
||||||
for _, sClient := range server.clients.AllClients() {
|
|
||||||
sClient.Notice(sClient.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
|
sClient.Notice(sClient.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -880,7 +837,6 @@ func (server *Server) loadDatastore(config *Config) error {
|
||||||
db, err := OpenDatabase(config)
|
db, err := OpenDatabase(config)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
server.store = db
|
server.store = db
|
||||||
server.dstore = bunt.NewBuntdbDatastore(db, server.logger)
|
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("Failed to open datastore: %s", err.Error())
|
return fmt.Errorf("Failed to open datastore: %s", err.Error())
|
||||||
|
|
@ -893,7 +849,8 @@ func (server *Server) loadFromDatastore(config *Config) (err error) {
|
||||||
server.loadDLines()
|
server.loadDLines()
|
||||||
server.loadKLines()
|
server.loadKLines()
|
||||||
|
|
||||||
server.channels.Initialize(server, config)
|
server.channelRegistry.Initialize(server)
|
||||||
|
server.channels.Initialize(server)
|
||||||
server.accounts.Initialize(server)
|
server.accounts.Initialize(server)
|
||||||
|
|
||||||
if config.Datastore.MySQL.Enabled {
|
if config.Datastore.MySQL.Enabled {
|
||||||
|
|
@ -1079,7 +1036,7 @@ func (server *Server) ForgetHistory(accountName string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
predicate := func(item *history.Item) bool { return item.Account == accountName }
|
predicate := func(item *history.Item) bool { return item.AccountName == accountName }
|
||||||
|
|
||||||
for _, channel := range server.channels.Channels() {
|
for _, channel := range server.channels.Channels() {
|
||||||
channel.history.Delete(predicate)
|
channel.history.Delete(predicate)
|
||||||
|
|
@ -1093,7 +1050,7 @@ func (server *Server) ForgetHistory(accountName string) {
|
||||||
// deletes a message. target is a hint about what buffer it's in (not required for
|
// deletes a message. target is a hint about what buffer it's in (not required for
|
||||||
// persistent history, where all the msgids are indexed together). if accountName
|
// persistent history, where all the msgids are indexed together). if accountName
|
||||||
// is anything other than "*", it must match the recorded AccountName of the message
|
// is anything other than "*", it must match the recorded AccountName of the message
|
||||||
func (server *Server) DeleteMessage(target, msgid, account string) (err error) {
|
func (server *Server) DeleteMessage(target, msgid, accountName string) (err error) {
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
var hist *history.Buffer
|
var hist *history.Buffer
|
||||||
|
|
||||||
|
|
@ -1116,10 +1073,10 @@ func (server *Server) DeleteMessage(target, msgid, account string) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if hist == nil {
|
if hist == nil {
|
||||||
err = server.historyDB.DeleteMsgid(msgid, account)
|
err = server.historyDB.DeleteMsgid(msgid, accountName)
|
||||||
} else {
|
} else {
|
||||||
count := hist.Delete(func(item *history.Item) bool {
|
count := hist.Delete(func(item *history.Item) bool {
|
||||||
return item.Message.Msgid == msgid && (account == "*" || item.Account == account)
|
return item.Message.Msgid == msgid && (accountName == "*" || item.AccountName == accountName)
|
||||||
})
|
})
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
err = errNoop
|
err = errNoop
|
||||||
|
|
|
||||||
|
|
@ -107,10 +107,6 @@ func (service *ircService) Notice(rb *ResponseBuffer, text string) {
|
||||||
rb.Add(nil, service.prefix, "NOTICE", rb.target.Nick(), text)
|
rb.Add(nil, service.prefix, "NOTICE", rb.target.Nick(), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *ircService) TaggedNotice(rb *ResponseBuffer, text string, tags map[string]string) {
|
|
||||||
rb.Add(tags, service.prefix, "NOTICE", rb.target.Nick(), text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// all service commands at the protocol level, by uppercase command name
|
// all service commands at the protocol level, by uppercase command name
|
||||||
// e.g., NICKSERV, NS
|
// e.g., NICKSERV, NS
|
||||||
var ergoServicesByCommandAlias map[string]*ircService
|
var ergoServicesByCommandAlias map[string]*ircService
|
||||||
|
|
|
||||||
|
|
@ -55,18 +55,14 @@ type Client struct {
|
||||||
|
|
||||||
// Dial returns a new Client connected to an SMTP server at addr.
|
// Dial returns a new Client connected to an SMTP server at addr.
|
||||||
// The addr must include a port, as in "mail.example.com:smtp".
|
// The addr must include a port, as in "mail.example.com:smtp".
|
||||||
func Dial(protocol, addr string, localAddress net.Addr, timeout time.Duration, implicitTLS bool) (*Client, error) {
|
func Dial(addr string, timeout time.Duration) (*Client, error) {
|
||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
var err error
|
var err error
|
||||||
dialer := net.Dialer{
|
|
||||||
Timeout: timeout,
|
|
||||||
LocalAddr: localAddress,
|
|
||||||
}
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
if !implicitTLS {
|
if timeout == 0 {
|
||||||
conn, err = dialer.Dial(protocol, addr)
|
conn, err = net.Dial("tcp", addr)
|
||||||
} else {
|
} else {
|
||||||
conn, err = tls.DialWithDialer(&dialer, protocol, addr, nil)
|
conn, err = net.DialTimeout("tcp", addr, timeout)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -342,7 +338,7 @@ var testHookStartTLS func(*tls.Config) // nil, except for tests
|
||||||
// functionality. Higher-level packages exist outside of the standard
|
// functionality. Higher-level packages exist outside of the standard
|
||||||
// library.
|
// library.
|
||||||
// XXX: modified in Ergo to add `requireTLS`, `heloDomain`, and `timeout` arguments
|
// XXX: modified in Ergo to add `requireTLS`, `heloDomain`, and `timeout` arguments
|
||||||
func SendMail(addr string, a Auth, heloDomain string, from string, to []string, msg []byte, requireTLS, implicitTLS bool, protocol string, localAddress net.Addr, timeout time.Duration) error {
|
func SendMail(addr string, a Auth, heloDomain string, from string, to []string, msg []byte, requireTLS bool, timeout time.Duration) error {
|
||||||
if err := validateLine(from); err != nil {
|
if err := validateLine(from); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -351,7 +347,7 @@ func SendMail(addr string, a Auth, heloDomain string, from string, to []string,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c, err := Dial(protocol, addr, localAddress, timeout, implicitTLS)
|
c, err := Dial(addr, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -359,7 +355,6 @@ func SendMail(addr string, a Auth, heloDomain string, from string, to []string,
|
||||||
if err = c.Hello(heloDomain); err != nil {
|
if err = c.Hello(heloDomain); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !implicitTLS {
|
|
||||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||||
var config *tls.Config
|
var config *tls.Config
|
||||||
if requireTLS {
|
if requireTLS {
|
||||||
|
|
@ -378,7 +373,6 @@ func SendMail(addr string, a Auth, heloDomain string, from string, to []string,
|
||||||
} else if requireTLS {
|
} else if requireTLS {
|
||||||
return errors.New("TLS required, but not negotiated")
|
return errors.New("TLS required, but not negotiated")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if a != nil && c.ext != nil {
|
if a != nil && c.ext != nil {
|
||||||
if _, ok := c.ext["AUTH"]; !ok {
|
if _, ok := c.ext["AUTH"]; !ok {
|
||||||
return errors.New("smtp: server doesn't support AUTH")
|
return errors.New("smtp: server doesn't support AUTH")
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,6 @@ const (
|
||||||
// confusables detection: standard skeleton algorithm (which may be ineffective
|
// confusables detection: standard skeleton algorithm (which may be ineffective
|
||||||
// over the larger set of permitted identifiers)
|
// over the larger set of permitted identifiers)
|
||||||
CasemappingPermissive
|
CasemappingPermissive
|
||||||
// rfc1459 is a legacy mapping as defined here: https://modern.ircdocs.horse/#casemapping-parameter
|
|
||||||
CasemappingRFC1459
|
|
||||||
// rfc1459-strict is a legacy mapping as defined here: https://modern.ircdocs.horse/#casemapping-parameter
|
|
||||||
CasemappingRFC1459Strict
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// XXX this is a global variable without explicit synchronization.
|
// XXX this is a global variable without explicit synchronization.
|
||||||
|
|
@ -114,10 +110,6 @@ func casefoldWithSetting(str string, setting Casemapping) (string, error) {
|
||||||
return foldASCII(str)
|
return foldASCII(str)
|
||||||
case CasemappingPermissive:
|
case CasemappingPermissive:
|
||||||
return foldPermissive(str)
|
return foldPermissive(str)
|
||||||
case CasemappingRFC1459:
|
|
||||||
return foldRFC1459(str, false)
|
|
||||||
case CasemappingRFC1459Strict:
|
|
||||||
return foldRFC1459(str, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,7 +214,7 @@ func Skeleton(name string) (string, error) {
|
||||||
switch globalCasemappingSetting {
|
switch globalCasemappingSetting {
|
||||||
default:
|
default:
|
||||||
return realSkeleton(name)
|
return realSkeleton(name)
|
||||||
case CasemappingASCII, CasemappingRFC1459, CasemappingRFC1459Strict:
|
case CasemappingASCII:
|
||||||
// identity function is fine because we independently case-normalize in Casefold
|
// identity function is fine because we independently case-normalize in Casefold
|
||||||
return name, nil
|
return name, nil
|
||||||
}
|
}
|
||||||
|
|
@ -310,23 +302,6 @@ func foldASCII(str string) (result string, err error) {
|
||||||
return strings.ToLower(str), nil
|
return strings.ToLower(str), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
rfc1459Replacer = strings.NewReplacer("[", "{", "]", "}", "\\", "|", "~", "^")
|
|
||||||
rfc1459StrictReplacer = strings.NewReplacer("[", "{", "]", "}", "\\", "|")
|
|
||||||
)
|
|
||||||
|
|
||||||
func foldRFC1459(str string, strict bool) (result string, err error) {
|
|
||||||
asciiFold, err := foldASCII(str)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
replacer := rfc1459Replacer
|
|
||||||
if strict {
|
|
||||||
replacer = rfc1459StrictReplacer
|
|
||||||
}
|
|
||||||
return replacer.Replace(asciiFold), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsPrintableASCII(str string) bool {
|
func IsPrintableASCII(str string) bool {
|
||||||
for i := 0; i < len(str); i++ {
|
for i := 0; i < len(str); i++ {
|
||||||
// allow space here because it's technically printable;
|
// allow space here because it's technically printable;
|
||||||
|
|
|
||||||
|
|
@ -279,31 +279,3 @@ func TestFoldASCIIInvalid(t *testing.T) {
|
||||||
t.Errorf("control characters should be invalid in identifiers")
|
t.Errorf("control characters should be invalid in identifiers")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFoldRFC1459(t *testing.T) {
|
|
||||||
folder := func(str string) (string, error) {
|
|
||||||
return foldRFC1459(str, false)
|
|
||||||
}
|
|
||||||
tester := func(first, second string, equal bool) {
|
|
||||||
validFoldTester(first, second, equal, folder, t)
|
|
||||||
}
|
|
||||||
tester("shivaram", "SHIVARAM", true)
|
|
||||||
tester("shivaram[a]", "shivaram{a}", true)
|
|
||||||
tester("shivaram\\a]", "shivaram{a}", false)
|
|
||||||
tester("shivaram\\a]", "shivaram|a}", true)
|
|
||||||
tester("shivaram~a]", "shivaram^a}", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFoldRFC1459Strict(t *testing.T) {
|
|
||||||
folder := func(str string) (string, error) {
|
|
||||||
return foldRFC1459(str, true)
|
|
||||||
}
|
|
||||||
tester := func(first, second string, equal bool) {
|
|
||||||
validFoldTester(first, second, equal, folder, t)
|
|
||||||
}
|
|
||||||
tester("shivaram", "SHIVARAM", true)
|
|
||||||
tester("shivaram[a]", "shivaram{a}", true)
|
|
||||||
tester("shivaram\\a]", "shivaram{a}", false)
|
|
||||||
tester("shivaram\\a]", "shivaram|a}", true)
|
|
||||||
tester("shivaram~a]", "shivaram^a}", false)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,17 @@ import (
|
||||||
type ClientSet = utils.HashSet[*Client]
|
type ClientSet = utils.HashSet[*Client]
|
||||||
|
|
||||||
type memberData struct {
|
type memberData struct {
|
||||||
modes modes.ModeSet
|
modes *modes.ModeSet
|
||||||
joinTime int64
|
joinTime int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemberSet is a set of members with modes.
|
// MemberSet is a set of members with modes.
|
||||||
type MemberSet map[*Client]*memberData
|
type MemberSet map[*Client]memberData
|
||||||
|
|
||||||
// Add adds the given client to this set.
|
// Add adds the given client to this set.
|
||||||
func (members MemberSet) Add(member *Client) {
|
func (members MemberSet) Add(member *Client) {
|
||||||
members[member] = &memberData{
|
members[member] = memberData{
|
||||||
|
modes: modes.NewModeSet(),
|
||||||
joinTime: time.Now().UnixNano(),
|
joinTime: time.Now().UnixNano(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,6 @@ func BitsetSet(set []uint32, position uint, on bool) (changed bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BitsetClear clears the bitset in-place.
|
|
||||||
func BitsetClear(set []uint32) {
|
|
||||||
for i := 0; i < len(set); i++ {
|
|
||||||
atomic.StoreUint32(&set[i], 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BitsetEmpty returns whether the bitset is empty.
|
// BitsetEmpty returns whether the bitset is empty.
|
||||||
// This has false positives under concurrent modification (i.e., it can return true
|
// This has false positives under concurrent modification (i.e., it can return true
|
||||||
// even though w.r.t. the sequence of atomic modifications, there was no point at
|
// even though w.r.t. the sequence of atomic modifications, there was no point at
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -32,26 +31,8 @@ var (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SecretTokenLength = 26
|
SecretTokenLength = 26
|
||||||
MachineId = 1 // Since there's no scaling Ergo, id is fixed at 1. Other things can have 2-127
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var inc uint64 = 0
|
|
||||||
|
|
||||||
// slingamn, if you ever see this, i'm sorry - I just didn't want to attach what i think is redundant data to every
|
|
||||||
// message.
|
|
||||||
func GenerateMessageId() uint64 {
|
|
||||||
inc++
|
|
||||||
var ts = time.Now().Unix() & 0xffffffffffff
|
|
||||||
var flake = uint64(ts << 16)
|
|
||||||
flake |= MachineId << 10
|
|
||||||
flake |= inc % 0x3ff
|
|
||||||
return flake
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateMessageIdStr() string {
|
|
||||||
return strconv.FormatUint(GenerateMessageId(), 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate a secret token that cannot be brute-forced via online attacks
|
// generate a secret token that cannot be brute-forced via online attacks
|
||||||
func GenerateSecretToken() string {
|
func GenerateSecretToken() string {
|
||||||
// 128 bits of entropy are enough to resist any online attack:
|
// 128 bits of entropy are enough to resist any online attack:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -209,11 +209,7 @@ func parseProxyLineV2(line []byte) (ip net.IP, err error) {
|
||||||
type WrappedConn struct {
|
type WrappedConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
ProxiedIP net.IP
|
ProxiedIP net.IP
|
||||||
TLS bool
|
Config ListenerConfig
|
||||||
Tor bool
|
|
||||||
STSOnly bool
|
|
||||||
WebSocket bool
|
|
||||||
HideSTS bool
|
|
||||||
// Secure indicates whether we believe the connection between us and the client
|
// Secure indicates whether we believe the connection between us and the client
|
||||||
// was secure against interception and modification (including all proxies):
|
// was secure against interception and modification (including all proxies):
|
||||||
Secure bool
|
Secure bool
|
||||||
|
|
@ -222,30 +218,35 @@ type WrappedConn struct {
|
||||||
// ReloadableListener is a wrapper for net.Listener that allows reloading
|
// ReloadableListener is a wrapper for net.Listener that allows reloading
|
||||||
// of config data for postprocessing connections (TLS, PROXY protocol, etc.)
|
// of config data for postprocessing connections (TLS, PROXY protocol, etc.)
|
||||||
type ReloadableListener struct {
|
type ReloadableListener struct {
|
||||||
|
// TODO: make this lock-free
|
||||||
|
sync.Mutex
|
||||||
realListener net.Listener
|
realListener net.Listener
|
||||||
// nil means the listener is closed:
|
config ListenerConfig
|
||||||
config atomic.Pointer[ListenerConfig]
|
isClosed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReloadableListener(realListener net.Listener, config ListenerConfig) *ReloadableListener {
|
func NewReloadableListener(realListener net.Listener, config ListenerConfig) *ReloadableListener {
|
||||||
result := &ReloadableListener{
|
return &ReloadableListener{
|
||||||
realListener: realListener,
|
realListener: realListener,
|
||||||
|
config: config,
|
||||||
}
|
}
|
||||||
result.config.Store(&config) // heap escape
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *ReloadableListener) Reload(config ListenerConfig) {
|
func (rl *ReloadableListener) Reload(config ListenerConfig) {
|
||||||
rl.config.Store(&config)
|
rl.Lock()
|
||||||
|
rl.config = config
|
||||||
|
rl.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
|
func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
|
||||||
conn, err = rl.realListener.Accept()
|
conn, err = rl.realListener.Accept()
|
||||||
|
|
||||||
config := rl.config.Load()
|
rl.Lock()
|
||||||
|
config := rl.config
|
||||||
|
isClosed := rl.isClosed
|
||||||
|
rl.Unlock()
|
||||||
|
|
||||||
if config == nil {
|
if isClosed {
|
||||||
// Close() was called
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
|
|
@ -278,17 +279,14 @@ func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
|
||||||
return &WrappedConn{
|
return &WrappedConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
ProxiedIP: proxiedIP,
|
ProxiedIP: proxiedIP,
|
||||||
TLS: config.TLSConfig != nil,
|
Config: config,
|
||||||
Tor: config.Tor,
|
|
||||||
STSOnly: config.STSOnly,
|
|
||||||
WebSocket: config.WebSocket,
|
|
||||||
HideSTS: config.HideSTS,
|
|
||||||
// Secure will be set later by client code
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *ReloadableListener) Close() error {
|
func (rl *ReloadableListener) Close() error {
|
||||||
rl.config.Store(nil)
|
rl.Lock()
|
||||||
|
rl.isClosed = true
|
||||||
|
rl.Unlock()
|
||||||
|
|
||||||
return rl.realListener.Close()
|
return rl.realListener.Close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ func IsRestrictedCTCPMessage(message string) bool {
|
||||||
|
|
||||||
type MessagePair struct {
|
type MessagePair struct {
|
||||||
Message string
|
Message string
|
||||||
Tags map[string]string
|
|
||||||
Concat bool // should be relayed with the multiline-concat tag
|
Concat bool // should be relayed with the multiline-concat tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,20 +37,19 @@ type SplitMessage struct {
|
||||||
|
|
||||||
func MakeMessage(original string) (result SplitMessage) {
|
func MakeMessage(original string) (result SplitMessage) {
|
||||||
result.Message = original
|
result.Message = original
|
||||||
result.Msgid = GenerateMessageIdStr()
|
result.Msgid = GenerateSecretToken()
|
||||||
result.SetTime()
|
result.SetTime()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SplitMessage) Append(message string, concat bool, tags map[string]string) {
|
func (sm *SplitMessage) Append(message string, concat bool) {
|
||||||
if sm.Msgid == "" {
|
if sm.Msgid == "" {
|
||||||
sm.Msgid = GenerateMessageIdStr()
|
sm.Msgid = GenerateSecretToken()
|
||||||
}
|
}
|
||||||
sm.Split = append(sm.Split, MessagePair{
|
sm.Split = append(sm.Split, MessagePair{
|
||||||
Message: message,
|
Message: message,
|
||||||
Concat: concat,
|
Concat: concat,
|
||||||
Tags: tags,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,28 +125,6 @@ func (t *TokenLineBuilder) Add(token string) {
|
||||||
t.buf.WriteString(token)
|
t.buf.WriteString(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddParts concatenates `parts` into a token and adds it to the line,
|
|
||||||
// creating a new line if necessary.
|
|
||||||
func (t *TokenLineBuilder) AddParts(parts ...string) {
|
|
||||||
var tokenLen int
|
|
||||||
for _, part := range parts {
|
|
||||||
tokenLen += len(part)
|
|
||||||
}
|
|
||||||
if t.buf.Len() != 0 {
|
|
||||||
tokenLen += len(t.delim)
|
|
||||||
}
|
|
||||||
if t.lineLen < t.buf.Len()+tokenLen {
|
|
||||||
t.result = append(t.result, t.buf.String())
|
|
||||||
t.buf.Reset()
|
|
||||||
}
|
|
||||||
if t.buf.Len() != 0 {
|
|
||||||
t.buf.WriteString(t.delim)
|
|
||||||
}
|
|
||||||
for _, part := range parts {
|
|
||||||
t.buf.WriteString(part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lines terminates the line-building and returns all the lines.
|
// Lines terminates the line-building and returns all the lines.
|
||||||
func (t *TokenLineBuilder) Lines() (result []string) {
|
func (t *TokenLineBuilder) Lines() (result []string) {
|
||||||
result = t.result
|
result = t.result
|
||||||
|
|
|
||||||
|
|
@ -43,26 +43,3 @@ func TestBuildTokenLines(t *testing.T) {
|
||||||
val = BuildTokenLines(10, []string{"abcd", "efgh", "ijkl"}, ",")
|
val = BuildTokenLines(10, []string{"abcd", "efgh", "ijkl"}, ",")
|
||||||
assertEqual(val, []string{"abcd,efgh", "ijkl"}, t)
|
assertEqual(val, []string{"abcd,efgh", "ijkl"}, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLBuilderAddParts(t *testing.T) {
|
|
||||||
var tl TokenLineBuilder
|
|
||||||
tl.Initialize(20, " ")
|
|
||||||
tl.Add("bob")
|
|
||||||
tl.AddParts("@", "alice")
|
|
||||||
tl.AddParts("@", "ErgoBot__")
|
|
||||||
assertEqual(tl.Lines(), []string{"bob @alice", "@ErgoBot__"}, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkTokenLines(b *testing.B) {
|
|
||||||
tokens := strings.Fields(monteCristo)
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
var tl TokenLineBuilder
|
|
||||||
tl.Initialize(400, " ")
|
|
||||||
for _, tok := range tokens {
|
|
||||||
tl.Add(tok)
|
|
||||||
}
|
|
||||||
tl.Lines()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,26 @@ func (s HashSet[T]) Remove(elem T) {
|
||||||
delete(s, elem)
|
delete(s, elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetLiteral[T comparable](elems ...T) HashSet[T] {
|
func CopyMap[K comparable, V any](input map[K]V) (result map[K]V) {
|
||||||
result := make(HashSet[T], len(elems))
|
result = make(map[K]V, len(input))
|
||||||
for _, elem := range elems {
|
for key, value := range input {
|
||||||
result.Add(elem)
|
result[key] = value
|
||||||
}
|
}
|
||||||
return result
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverse the order of a slice in place
|
||||||
|
func ReverseSlice[T any](results []T) {
|
||||||
|
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
results[i], results[j] = results[j], results[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SliceContains[T comparable](slice []T, elem T) (result bool) {
|
||||||
|
for _, t := range slice {
|
||||||
|
if elem == t {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
// Copyright (c) 2022 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidUUID = errors.New("Invalid uuid")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Technically a UUIDv4 has version bits set, but this doesn't matter in practice
|
|
||||||
type UUID [16]byte
|
|
||||||
|
|
||||||
func (u UUID) MarshalJSON() (b []byte, err error) {
|
|
||||||
b = make([]byte, 24)
|
|
||||||
b[0] = '"'
|
|
||||||
base64.RawURLEncoding.Encode(b[1:], u[:])
|
|
||||||
b[23] = '"'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UUID) UnmarshalJSON(b []byte) (err error) {
|
|
||||||
if len(b) != 24 {
|
|
||||||
return ErrInvalidUUID
|
|
||||||
}
|
|
||||||
readLen, err := base64.RawURLEncoding.Decode(u[:], b[1:23])
|
|
||||||
if err != nil || readLen != 16 {
|
|
||||||
return ErrInvalidUUID
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UUID) String() string {
|
|
||||||
return base64.RawURLEncoding.EncodeToString(u[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateUUIDv4() (result UUID) {
|
|
||||||
_, err := rand.Read(result[:])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeUUID(ustr string) (result UUID, err error) {
|
|
||||||
length, err := base64.RawURLEncoding.Decode(result[:], []byte(ustr))
|
|
||||||
if err == nil && length != 16 {
|
|
||||||
err = ErrInvalidUUID
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,7 @@ import "fmt"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// SemVer is the semantic version of Ergo.
|
// SemVer is the semantic version of Ergo.
|
||||||
SemVer = "2.15.0-unreleased"
|
SemVer = "2.12.0-unreleased"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ func zncPlayPrivmsgsFrom(client *Client, rb *ResponseBuffer, target string, star
|
||||||
zncMax := client.server.Config().History.ZNCMax
|
zncMax := client.server.Config().History.ZNCMax
|
||||||
items, err := sequence.Between(history.Selector{Time: start}, history.Selector{Time: end}, zncMax)
|
items, err := sequence.Between(history.Selector{Time: start}, history.Selector{Time: end}, zncMax)
|
||||||
if err == nil && len(items) != 0 {
|
if err == nil && len(items) != 0 {
|
||||||
client.replayPrivmsgHistory(rb, items, target, false, "", "", 0)
|
client.replayPrivmsgHistory(rb, items, target, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +211,7 @@ func zncPlayPrivmsgsFromAll(client *Client, rb *ResponseBuffer, start, end time.
|
||||||
zncMax := client.server.Config().History.ZNCMax
|
zncMax := client.server.Config().History.ZNCMax
|
||||||
items, err := client.privmsgsBetween(start, end, maxDMTargetsForAutoplay, zncMax)
|
items, err := client.privmsgsBetween(start, end, maxDMTargetsForAutoplay, zncMax)
|
||||||
if err == nil && len(items) != 0 {
|
if err == nil && len(items) != 0 {
|
||||||
client.replayPrivmsgHistory(rb, items, "", false, "", "", 0)
|
client.replayPrivmsgHistory(rb, items, "", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
irctest
2
irctest
|
|
@ -1 +1 @@
|
||||||
Subproject commit a1324407893b603fe6b55ce7c4ee385938291ae1
|
Subproject commit 35d342a478f8ddc7d6b9ba7b2e55f769c60478d1
|
||||||
|
|
@ -108,10 +108,9 @@ server:
|
||||||
# the recommended default is 'ascii' (traditional ASCII-only identifiers).
|
# the recommended default is 'ascii' (traditional ASCII-only identifiers).
|
||||||
# the other options are 'precis', which allows UTF8 identifiers that are "sane"
|
# the other options are 'precis', which allows UTF8 identifiers that are "sane"
|
||||||
# (according to UFC 8265), with additional mitigations for homoglyph attacks,
|
# (according to UFC 8265), with additional mitigations for homoglyph attacks,
|
||||||
# 'permissive', which allows identifiers containing unusual characters like
|
# and 'permissive', which allows identifiers containing unusual characters like
|
||||||
# emoji, at the cost of increased vulnerability to homoglyph attacks and potential
|
# emoji, at the cost of increased vulnerability to homoglyph attacks and potential
|
||||||
# client compatibility problems, and the legacy mappings 'rfc1459' and
|
# client compatibility problems. we recommend leaving this value at its default;
|
||||||
# 'rfc1459-strict'. we recommend leaving this value at its default;
|
|
||||||
# however, note that changing it once the network is already up and running is
|
# however, note that changing it once the network is already up and running is
|
||||||
# problematic.
|
# problematic.
|
||||||
casemapping: "ascii"
|
casemapping: "ascii"
|
||||||
|
|
@ -193,9 +192,6 @@ server:
|
||||||
# - "192.168.1.1"
|
# - "192.168.1.1"
|
||||||
# - "192.168.10.1/24"
|
# - "192.168.10.1/24"
|
||||||
|
|
||||||
# whether to accept the hostname parameter on the WEBIRC line as the IRC hostname
|
|
||||||
accept-hostname: true
|
|
||||||
|
|
||||||
# maximum length of clients' sendQ in bytes
|
# maximum length of clients' sendQ in bytes
|
||||||
# this should be big enough to hold bursts of channel/direct messages
|
# this should be big enough to hold bursts of channel/direct messages
|
||||||
max-sendq: 96k
|
max-sendq: 96k
|
||||||
|
|
@ -382,10 +378,6 @@ accounts:
|
||||||
sender: "admin@my.network"
|
sender: "admin@my.network"
|
||||||
require-tls: true
|
require-tls: true
|
||||||
helo-domain: "my.network" # defaults to server name if unset
|
helo-domain: "my.network" # defaults to server name if unset
|
||||||
# set to `tcp4` to force sending over IPv4, `tcp6` to force IPv6:
|
|
||||||
# protocol: "tcp4"
|
|
||||||
# set to force a specific source/local IPv4 or IPv6 address:
|
|
||||||
# local-address: "1.2.3.4"
|
|
||||||
# options to enable DKIM signing of outgoing emails (recommended, but
|
# options to enable DKIM signing of outgoing emails (recommended, but
|
||||||
# requires creating a DNS entry for the public key):
|
# requires creating a DNS entry for the public key):
|
||||||
# dkim:
|
# dkim:
|
||||||
|
|
@ -398,15 +390,8 @@ accounts:
|
||||||
# port: 25
|
# port: 25
|
||||||
# username: "admin"
|
# username: "admin"
|
||||||
# password: "hunter2"
|
# password: "hunter2"
|
||||||
# implicit-tls: false # TLS from the first byte, typically on port 465
|
blacklist-regexes:
|
||||||
# addresses that are not accepted for registration:
|
# - ".*@mailinator.com"
|
||||||
address-blacklist:
|
|
||||||
# - "*@mailinator.com"
|
|
||||||
address-blacklist-syntax: "glob" # change to "regex" for regular expressions
|
|
||||||
# file of newline-delimited address blacklist entries (no enclosing quotes)
|
|
||||||
# in the above syntax (i.e. either globs or regexes). supersedes
|
|
||||||
# address-blacklist if set:
|
|
||||||
# address-blacklist-file: "/path/to/address-blacklist-file"
|
|
||||||
timeout: 60s
|
timeout: 60s
|
||||||
# email-based password reset:
|
# email-based password reset:
|
||||||
password-reset:
|
password-reset:
|
||||||
|
|
@ -438,10 +423,6 @@ accounts:
|
||||||
# this is useful for compatibility with old clients that don't support SASL
|
# this is useful for compatibility with old clients that don't support SASL
|
||||||
login-via-pass-command: false
|
login-via-pass-command: false
|
||||||
|
|
||||||
# advertise the SCRAM-SHA-256 authentication method. set to false in case of
|
|
||||||
# compatibility issues with certain clients:
|
|
||||||
advertise-scram: true
|
|
||||||
|
|
||||||
# require-sasl controls whether clients are required to have accounts
|
# require-sasl controls whether clients are required to have accounts
|
||||||
# (and sign into them using SASL) to connect to the server
|
# (and sign into them using SASL) to connect to the server
|
||||||
require-sasl:
|
require-sasl:
|
||||||
|
|
@ -567,40 +548,6 @@ accounts:
|
||||||
# how many scripts are allowed to run at once? 0 for no limit:
|
# how many scripts are allowed to run at once? 0 for no limit:
|
||||||
max-concurrency: 64
|
max-concurrency: 64
|
||||||
|
|
||||||
# support for login via OAuth2 bearer tokens
|
|
||||||
oauth2:
|
|
||||||
enabled: false
|
|
||||||
# should we automatically create users on presentation of a valid token?
|
|
||||||
autocreate: true
|
|
||||||
# enable this to use auth-script for validation:
|
|
||||||
auth-script: false
|
|
||||||
introspection-url: "https://example.com/api/oidc/introspection"
|
|
||||||
introspection-timeout: 10s
|
|
||||||
# omit for auth method `none`; required for auth method `client_secret_basic`:
|
|
||||||
client-id: "ergo"
|
|
||||||
client-secret: "4TA0I7mJ3fUUcW05KJiODg"
|
|
||||||
|
|
||||||
# support for login via JWT bearer tokens
|
|
||||||
jwt-auth:
|
|
||||||
enabled: false
|
|
||||||
# should we automatically create users on presentation of a valid token?
|
|
||||||
autocreate: true
|
|
||||||
# any of these token definitions can be accepted, allowing for key rotation
|
|
||||||
tokens:
|
|
||||||
-
|
|
||||||
algorithm: "hmac" # either 'hmac', 'rsa', or 'eddsa' (ed25519)
|
|
||||||
# hmac takes a symmetric key, rsa and eddsa take PEM-encoded public keys;
|
|
||||||
# either way, the key can be specified either as a YAML string:
|
|
||||||
key: "nANiZ1De4v6WnltCHN2H7Q"
|
|
||||||
# or as a path to the file containing the key:
|
|
||||||
#key-file: "jwt_pubkey.pem"
|
|
||||||
# list of JWT claim names to search for the user's account name (make sure the format
|
|
||||||
# is what you expect, especially if using "sub"):
|
|
||||||
account-claims: ["preferred_username"]
|
|
||||||
# if a claim is formatted as an email address, require it to have the following domain,
|
|
||||||
# and then strip off the domain and use the local-part as the account name:
|
|
||||||
#strip-domain: "example.com"
|
|
||||||
|
|
||||||
# channel options
|
# channel options
|
||||||
channels:
|
channels:
|
||||||
# modes that are set when new channels are created
|
# modes that are set when new channels are created
|
||||||
|
|
@ -635,12 +582,6 @@ channels:
|
||||||
# (0 or omit for no expiration):
|
# (0 or omit for no expiration):
|
||||||
invite-expiration: 24h
|
invite-expiration: 24h
|
||||||
|
|
||||||
# channels that new clients will automatically join. this should be used with
|
|
||||||
# caution, since traditional IRC users will likely view it as an antifeature.
|
|
||||||
# it may be useful in small community networks that have a single "primary" channel:
|
|
||||||
#auto-join:
|
|
||||||
# - "#lounge"
|
|
||||||
|
|
||||||
# operator classes:
|
# operator classes:
|
||||||
# an operator has a single "class" (defining a privilege level), which can include
|
# an operator has a single "class" (defining a privilege level), which can include
|
||||||
# multiple "capabilities" (defining privileged actions they can take). all
|
# multiple "capabilities" (defining privileged actions they can take). all
|
||||||
|
|
@ -791,7 +732,7 @@ lock-file: "ircd.lock"
|
||||||
|
|
||||||
# datastore configuration
|
# datastore configuration
|
||||||
datastore:
|
datastore:
|
||||||
# path to the database file (used to store account and channel registrations):
|
# path to the datastore
|
||||||
path: ircd.db
|
path: ircd.db
|
||||||
|
|
||||||
# if the database schema requires an upgrade, `autoupgrade` will attempt to
|
# if the database schema requires an upgrade, `autoupgrade` will attempt to
|
||||||
|
|
@ -834,9 +775,6 @@ limits:
|
||||||
# identlen is the max ident length allowed
|
# identlen is the max ident length allowed
|
||||||
identlen: 20
|
identlen: 20
|
||||||
|
|
||||||
# realnamelen is the maximum realname length allowed
|
|
||||||
realnamelen: 150
|
|
||||||
|
|
||||||
# channellen is the max channel length allowed
|
# channellen is the max channel length allowed
|
||||||
channellen: 64
|
channellen: 64
|
||||||
|
|
||||||
|
|
@ -856,7 +794,7 @@ limits:
|
||||||
whowas-entries: 100
|
whowas-entries: 100
|
||||||
|
|
||||||
# maximum length of channel lists (beI modes)
|
# maximum length of channel lists (beI modes)
|
||||||
chan-list-modes: 100
|
chan-list-modes: 60
|
||||||
|
|
||||||
# maximum number of messages to accept during registration (prevents
|
# maximum number of messages to accept during registration (prevents
|
||||||
# DoS / resource exhaustion attacks):
|
# DoS / resource exhaustion attacks):
|
||||||
|
|
@ -1011,8 +949,7 @@ history:
|
||||||
|
|
||||||
# options to control how messages are stored and deleted:
|
# options to control how messages are stored and deleted:
|
||||||
retention:
|
retention:
|
||||||
# allow users to delete their own messages from history,
|
# allow users to delete their own messages from history?
|
||||||
# and channel operators to delete messages in their channel?
|
|
||||||
allow-individual-delete: false
|
allow-individual-delete: false
|
||||||
|
|
||||||
# if persistent history is enabled, create additional index tables,
|
# if persistent history is enabled, create additional index tables,
|
||||||
|
|
|
||||||
15
vendor/github.com/ergochat/go-ident/client.go
generated
vendored
15
vendor/github.com/ergochat/go-ident/client.go
generated
vendored
|
|
@ -38,12 +38,6 @@ func (e ProtocolError) Error() string {
|
||||||
|
|
||||||
// Query makes an Ident query, if timeout is >0 the query is timed out after that many seconds.
|
// Query makes an Ident query, if timeout is >0 the query is timed out after that many seconds.
|
||||||
func Query(ip string, portOnServer, portOnClient int, timeout time.Duration) (response Response, err error) {
|
func Query(ip string, portOnServer, portOnClient int, timeout time.Duration) (response Response, err error) {
|
||||||
// if a timeout is set, respect it from the beginning of the query, including the dial time
|
|
||||||
var deadline time.Time
|
|
||||||
if timeout > 0 {
|
|
||||||
deadline = time.Now().Add(timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
if timeout > 0 {
|
if timeout > 0 {
|
||||||
conn, err = net.DialTimeout("tcp", net.JoinHostPort(ip, "113"), timeout)
|
conn, err = net.DialTimeout("tcp", net.JoinHostPort(ip, "113"), timeout)
|
||||||
|
|
@ -53,12 +47,13 @@ func Query(ip string, portOnServer, portOnClient int, timeout time.Duration) (re
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// if timeout is 0, `deadline` is the empty time.Time{} which means no deadline:
|
// stop the ident read after <timeout> seconds
|
||||||
conn.SetDeadline(deadline)
|
if timeout > 0 {
|
||||||
|
conn.SetDeadline(time.Now().Add(timeout))
|
||||||
|
}
|
||||||
|
|
||||||
_, err = conn.Write([]byte(fmt.Sprintf("%d, %d\r\n", portOnClient, portOnServer)))
|
_, err = conn.Write([]byte(fmt.Sprintf("%d, %d", portOnClient, portOnServer) + "\r\n"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
194
vendor/github.com/ergochat/irc-go/ircfmt/ircfmt.go
generated
vendored
194
vendor/github.com/ergochat/irc-go/ircfmt/ircfmt.go
generated
vendored
|
|
@ -5,7 +5,6 @@ package ircfmt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -20,126 +19,24 @@ const (
|
||||||
underline string = "\x1f"
|
underline string = "\x1f"
|
||||||
reset string = "\x0f"
|
reset string = "\x0f"
|
||||||
|
|
||||||
metacharacters = (bold + colour + monospace + reverseColour + italic + strikethrough + underline + reset)
|
runecolour rune = '\x03'
|
||||||
|
runebold rune = '\x02'
|
||||||
|
runemonospace rune = '\x11'
|
||||||
|
runereverseColour rune = '\x16'
|
||||||
|
runeitalic rune = '\x1d'
|
||||||
|
runestrikethrough rune = '\x1e'
|
||||||
|
runereset rune = '\x0f'
|
||||||
|
runeunderline rune = '\x1f'
|
||||||
|
|
||||||
|
// valid characters in a colour code character, for speed
|
||||||
|
colours1 string = "0123456789"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ColorCode is a normalized representation of an IRC color code,
|
|
||||||
// as per this de facto specification: https://modern.ircdocs.horse/formatting.html#color
|
|
||||||
// The zero value of the type represents a default or unset color,
|
|
||||||
// whereas ColorCode{true, 0} represents the color white.
|
|
||||||
type ColorCode struct {
|
|
||||||
IsSet bool
|
|
||||||
Value uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseColor converts a string representation of an IRC color code, e.g. "04",
|
|
||||||
// into a normalized ColorCode, e.g. ColorCode{true, 4}.
|
|
||||||
func ParseColor(str string) (color ColorCode) {
|
|
||||||
// "99 - Default Foreground/Background - Not universally supported."
|
|
||||||
// normalize 99 to ColorCode{} meaning "unset":
|
|
||||||
if code, err := strconv.ParseUint(str, 10, 8); err == nil && code < 99 {
|
|
||||||
color.IsSet = true
|
|
||||||
color.Value = uint8(code)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormattedSubstring represents a section of an IRC message with associated
|
|
||||||
// formatting data.
|
|
||||||
type FormattedSubstring struct {
|
|
||||||
Content string
|
|
||||||
ForegroundColor ColorCode
|
|
||||||
BackgroundColor ColorCode
|
|
||||||
Bold bool
|
|
||||||
Monospace bool
|
|
||||||
Strikethrough bool
|
|
||||||
Underline bool
|
|
||||||
Italic bool
|
|
||||||
ReverseColor bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsFormatted returns whether the section has any formatting flags switched on.
|
|
||||||
func (f *FormattedSubstring) IsFormatted() bool {
|
|
||||||
// could rely on value receiver but if this is to be a public API,
|
|
||||||
// let's make it a pointer receiver
|
|
||||||
g := *f
|
|
||||||
g.Content = ""
|
|
||||||
return g != FormattedSubstring{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// "If there are two ASCII digits available where a <COLOR> is allowed,
|
|
||||||
// then two characters MUST always be read for it and displayed as described below."
|
|
||||||
// we rely on greedy matching to implement this for both forms:
|
|
||||||
// (\x03)00,01
|
|
||||||
colorForeBackRe = regexp.MustCompile(`^([0-9]{1,2}),([0-9]{1,2})`)
|
|
||||||
// (\x03)00
|
|
||||||
colorForeRe = regexp.MustCompile(`^([0-9]{1,2})`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Split takes an IRC message (typically a PRIVMSG or NOTICE final parameter)
|
|
||||||
// containing IRC formatting control codes, and splits it into substrings with
|
|
||||||
// associated formatting information.
|
|
||||||
func Split(raw string) (result []FormattedSubstring) {
|
|
||||||
var chunk FormattedSubstring
|
|
||||||
for {
|
|
||||||
// skip to the next metacharacter, or the end of the string
|
|
||||||
if idx := strings.IndexAny(raw, metacharacters); idx != 0 {
|
|
||||||
if idx == -1 {
|
|
||||||
idx = len(raw)
|
|
||||||
}
|
|
||||||
chunk.Content = raw[:idx]
|
|
||||||
if len(chunk.Content) != 0 {
|
|
||||||
result = append(result, chunk)
|
|
||||||
}
|
|
||||||
raw = raw[idx:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(raw) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// we're at a metacharacter. by default, all previous formatting carries over
|
|
||||||
metacharacter := raw[0]
|
|
||||||
raw = raw[1:]
|
|
||||||
switch metacharacter {
|
|
||||||
case bold[0]:
|
|
||||||
chunk.Bold = !chunk.Bold
|
|
||||||
case monospace[0]:
|
|
||||||
chunk.Monospace = !chunk.Monospace
|
|
||||||
case strikethrough[0]:
|
|
||||||
chunk.Strikethrough = !chunk.Strikethrough
|
|
||||||
case underline[0]:
|
|
||||||
chunk.Underline = !chunk.Underline
|
|
||||||
case italic[0]:
|
|
||||||
chunk.Italic = !chunk.Italic
|
|
||||||
case reverseColour[0]:
|
|
||||||
chunk.ReverseColor = !chunk.ReverseColor
|
|
||||||
case reset[0]:
|
|
||||||
chunk = FormattedSubstring{}
|
|
||||||
case colour[0]:
|
|
||||||
// preferentially match the "\x0399,01" form, then "\x0399";
|
|
||||||
// if neither of those matches, then it's a reset
|
|
||||||
if matches := colorForeBackRe.FindStringSubmatch(raw); len(matches) != 0 {
|
|
||||||
chunk.ForegroundColor = ParseColor(matches[1])
|
|
||||||
chunk.BackgroundColor = ParseColor(matches[2])
|
|
||||||
raw = raw[len(matches[0]):]
|
|
||||||
} else if matches := colorForeRe.FindStringSubmatch(raw); len(matches) != 0 {
|
|
||||||
chunk.ForegroundColor = ParseColor(matches[1])
|
|
||||||
raw = raw[len(matches[0]):]
|
|
||||||
} else {
|
|
||||||
chunk.ForegroundColor = ColorCode{}
|
|
||||||
chunk.BackgroundColor = ColorCode{}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// should be impossible, but just ignore it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// valtoescape replaces most of IRC characters with our escapes.
|
// valtoescape replaces most of IRC characters with our escapes.
|
||||||
valtoescape = strings.NewReplacer("$", "$$", colour, "$c", reverseColour, "$v", bold, "$b", italic, "$i", strikethrough, "$s", underline, "$u", monospace, "$m", reset, "$r")
|
valtoescape = strings.NewReplacer("$", "$$", colour, "$c", reverseColour, "$v", bold, "$b", italic, "$i", strikethrough, "$s", underline, "$u", monospace, "$m", reset, "$r")
|
||||||
|
// valToStrip replaces most of the IRC characters with nothing
|
||||||
|
valToStrip = strings.NewReplacer(colour, "$c", reverseColour, "", bold, "", italic, "", strikethrough, "", underline, "", monospace, "", reset, "")
|
||||||
|
|
||||||
// escapetoval contains most of our escapes and how they map to real IRC characters.
|
// escapetoval contains most of our escapes and how they map to real IRC characters.
|
||||||
// intentionally skips colour, since that's handled elsewhere.
|
// intentionally skips colour, since that's handled elsewhere.
|
||||||
|
|
@ -201,9 +98,7 @@ var (
|
||||||
"light blue": "12",
|
"light blue": "12",
|
||||||
"pink": "13",
|
"pink": "13",
|
||||||
"grey": "14",
|
"grey": "14",
|
||||||
"gray": "14",
|
|
||||||
"light grey": "15",
|
"light grey": "15",
|
||||||
"light gray": "15",
|
|
||||||
"default": "99",
|
"default": "99",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,7 +123,7 @@ func Escape(in string) string {
|
||||||
out.WriteString("$c")
|
out.WriteString("$c")
|
||||||
inRunes = inRunes[2:] // strip colour code chars
|
inRunes = inRunes[2:] // strip colour code chars
|
||||||
|
|
||||||
if len(inRunes) < 1 || !isDigit(inRunes[0]) {
|
if len(inRunes) < 1 || !strings.Contains(colours1, string(inRunes[0])) {
|
||||||
out.WriteString("[]")
|
out.WriteString("[]")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -236,14 +131,14 @@ func Escape(in string) string {
|
||||||
var foreBuffer, backBuffer string
|
var foreBuffer, backBuffer string
|
||||||
foreBuffer += string(inRunes[0])
|
foreBuffer += string(inRunes[0])
|
||||||
inRunes = inRunes[1:]
|
inRunes = inRunes[1:]
|
||||||
if 0 < len(inRunes) && isDigit(inRunes[0]) {
|
if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
|
||||||
foreBuffer += string(inRunes[0])
|
foreBuffer += string(inRunes[0])
|
||||||
inRunes = inRunes[1:]
|
inRunes = inRunes[1:]
|
||||||
}
|
}
|
||||||
if 1 < len(inRunes) && inRunes[0] == ',' && isDigit(inRunes[1]) {
|
if 1 < len(inRunes) && inRunes[0] == ',' && strings.Contains(colours1, string(inRunes[1])) {
|
||||||
backBuffer += string(inRunes[1])
|
backBuffer += string(inRunes[1])
|
||||||
inRunes = inRunes[2:]
|
inRunes = inRunes[2:]
|
||||||
if 0 < len(inRunes) && isDigit(inRunes[1]) {
|
if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
|
||||||
backBuffer += string(inRunes[0])
|
backBuffer += string(inRunes[0])
|
||||||
inRunes = inRunes[1:]
|
inRunes = inRunes[1:]
|
||||||
}
|
}
|
||||||
|
|
@ -283,27 +178,52 @@ func Escape(in string) string {
|
||||||
return out.String()
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDigit(r rune) bool {
|
|
||||||
return '0' <= r && r <= '9' // don't use unicode.IsDigit, it includes non-ASCII numerals
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip takes a raw IRC string and removes it with all formatting codes removed
|
// Strip takes a raw IRC string and removes it with all formatting codes removed
|
||||||
// IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
|
// IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
|
||||||
// into: "This is a cool, red message!"
|
// into: "This is a cool, red message!"
|
||||||
func Strip(in string) string {
|
func Strip(in string) string {
|
||||||
splitChunks := Split(in)
|
out := strings.Builder{}
|
||||||
if len(splitChunks) == 0 {
|
runes := []rune(in)
|
||||||
return ""
|
if out.Len() < len(runes) { // Reduce allocations where needed
|
||||||
} else if len(splitChunks) == 1 {
|
out.Grow(len(in) - out.Len())
|
||||||
return splitChunks[0].Content
|
}
|
||||||
|
for len(runes) > 0 {
|
||||||
|
switch runes[0] {
|
||||||
|
case runebold, runemonospace, runereverseColour, runeitalic, runestrikethrough, runeunderline, runereset:
|
||||||
|
runes = runes[1:]
|
||||||
|
case runecolour:
|
||||||
|
runes = removeColour(runes)
|
||||||
|
default:
|
||||||
|
out.WriteRune(runes[0])
|
||||||
|
runes = runes[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeNumber(runes []rune) []rune {
|
||||||
|
if len(runes) > 0 && runes[0] >= '0' && runes[0] <= '9' {
|
||||||
|
runes = runes[1:]
|
||||||
|
}
|
||||||
|
return runes
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeColour(runes []rune) []rune {
|
||||||
|
if runes[0] != runecolour {
|
||||||
|
return runes
|
||||||
|
}
|
||||||
|
|
||||||
|
runes = runes[1:]
|
||||||
|
runes = removeNumber(runes)
|
||||||
|
runes = removeNumber(runes)
|
||||||
|
|
||||||
|
if len(runes) > 1 && runes[0] == ',' && runes[1] >= '0' && runes[1] <= '9' {
|
||||||
|
runes = runes[2:]
|
||||||
} else {
|
} else {
|
||||||
var buf strings.Builder
|
return runes // Nothing else because we dont have a comma
|
||||||
buf.Grow(len(in))
|
|
||||||
for _, chunk := range splitChunks {
|
|
||||||
buf.WriteString(chunk.Content)
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
}
|
||||||
|
runes = removeNumber(runes)
|
||||||
|
return runes
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve "light blue" to "12", "12" to "12", "asdf" to "", etc.
|
// resolve "light blue" to "12", "12" to "12", "asdf" to "", etc.
|
||||||
|
|
|
||||||
22
vendor/github.com/ergochat/irc-go/ircmsg/message.go
generated
vendored
22
vendor/github.com/ergochat/irc-go/ircmsg/message.go
generated
vendored
|
|
@ -196,15 +196,6 @@ func trimInitialSpaces(str string) string {
|
||||||
return str[i:]
|
return str[i:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func isASCII(str string) bool {
|
|
||||||
for i := 0; i < len(str); i++ {
|
|
||||||
if str[i] > 127 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Message, err error) {
|
func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Message, err error) {
|
||||||
// remove either \n or \r\n from the end of the line:
|
// remove either \n or \r\n from the end of the line:
|
||||||
line = strings.TrimSuffix(line, "\n")
|
line = strings.TrimSuffix(line, "\n")
|
||||||
|
|
@ -247,7 +238,7 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Messa
|
||||||
// truncate if desired
|
// truncate if desired
|
||||||
if truncateLen != 0 && truncateLen < len(line) {
|
if truncateLen != 0 && truncateLen < len(line) {
|
||||||
err = ErrorBodyTooLong
|
err = ErrorBodyTooLong
|
||||||
line = TruncateUTF8Safe(line, truncateLen)
|
line = line[:truncateLen]
|
||||||
}
|
}
|
||||||
|
|
||||||
// modern: "These message parts, and parameters themselves, are separated
|
// modern: "These message parts, and parameters themselves, are separated
|
||||||
|
|
@ -274,16 +265,11 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Messa
|
||||||
commandEnd = len(line)
|
commandEnd = len(line)
|
||||||
paramStart = len(line)
|
paramStart = len(line)
|
||||||
}
|
}
|
||||||
baseCommand := line[:commandEnd]
|
// normalize command to uppercase:
|
||||||
if len(baseCommand) == 0 {
|
ircmsg.Command = strings.ToUpper(line[:commandEnd])
|
||||||
|
if len(ircmsg.Command) == 0 {
|
||||||
return ircmsg, ErrorLineIsEmpty
|
return ircmsg, ErrorLineIsEmpty
|
||||||
}
|
}
|
||||||
// technically this must be either letters or a 3-digit numeric:
|
|
||||||
if !isASCII(baseCommand) {
|
|
||||||
return ircmsg, ErrorLineContainsBadChar
|
|
||||||
}
|
|
||||||
// normalize command to uppercase:
|
|
||||||
ircmsg.Command = strings.ToUpper(baseCommand)
|
|
||||||
line = line[paramStart:]
|
line = line[paramStart:]
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
|
|
||||||
29
vendor/github.com/ergochat/irc-go/ircmsg/unicode.go
generated
vendored
29
vendor/github.com/ergochat/irc-go/ircmsg/unicode.go
generated
vendored
|
|
@ -1,29 +0,0 @@
|
||||||
// Copyright (c) 2021 Shivaram Lingamneni
|
|
||||||
// Released under the MIT License
|
|
||||||
|
|
||||||
package ircmsg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TruncateUTF8Safe truncates a message, respecting UTF8 boundaries. If a message
|
|
||||||
// was originally valid UTF8, TruncateUTF8Safe will not make it invalid; instead
|
|
||||||
// it will truncate additional bytes as needed, back to the last valid
|
|
||||||
// UTF8-encoded codepoint. If a message is not UTF8, TruncateUTF8Safe will truncate
|
|
||||||
// at most 3 additional bytes before giving up.
|
|
||||||
func TruncateUTF8Safe(message string, byteLimit int) (result string) {
|
|
||||||
if len(message) <= byteLimit {
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
message = message[:byteLimit]
|
|
||||||
for i := 0; i < (utf8.UTFMax - 1); i++ {
|
|
||||||
r, n := utf8.DecodeLastRuneInString(message)
|
|
||||||
if r == utf8.RuneError && n <= 1 {
|
|
||||||
message = message[:len(message)-1]
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
111
vendor/github.com/ergochat/irc-go/ircutils/sasl.go
generated
vendored
111
vendor/github.com/ergochat/irc-go/ircutils/sasl.go
generated
vendored
|
|
@ -1,111 +0,0 @@
|
||||||
package ircutils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrSASLLimitExceeded = errors.New("SASL total response size exceeded configured limit")
|
|
||||||
ErrSASLTooLong = errors.New("SASL response chunk exceeded 400-byte limit")
|
|
||||||
)
|
|
||||||
|
|
||||||
// EncodeSASLResponse encodes a raw SASL response as parameters to successive
|
|
||||||
// AUTHENTICATE commands, as described in the IRCv3 SASL specification.
|
|
||||||
func EncodeSASLResponse(raw []byte) (result []string) {
|
|
||||||
// https://ircv3.net/specs/extensions/sasl-3.1#the-authenticate-command
|
|
||||||
// "The response is encoded in Base64 (RFC 4648), then split to 400-byte chunks,
|
|
||||||
// and each chunk is sent as a separate AUTHENTICATE command. Empty (zero-length)
|
|
||||||
// responses are sent as AUTHENTICATE +. If the last chunk was exactly 400 bytes
|
|
||||||
// long, it must also be followed by AUTHENTICATE + to signal end of response."
|
|
||||||
|
|
||||||
if len(raw) == 0 {
|
|
||||||
return []string{"+"}
|
|
||||||
}
|
|
||||||
|
|
||||||
response := base64.StdEncoding.EncodeToString(raw)
|
|
||||||
result = make([]string, 0, (len(response)/400)+1)
|
|
||||||
lastLen := 0
|
|
||||||
for len(response) > 0 {
|
|
||||||
// TODO once we require go 1.21, this can be: lastLen = min(len(response), 400)
|
|
||||||
lastLen = len(response)
|
|
||||||
if lastLen > 400 {
|
|
||||||
lastLen = 400
|
|
||||||
}
|
|
||||||
result = append(result, response[:lastLen])
|
|
||||||
response = response[lastLen:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastLen == 400 {
|
|
||||||
result = append(result, "+")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// SASLBuffer handles buffering and decoding SASL responses sent as parameters
|
|
||||||
// to AUTHENTICATE commands, as described in the IRCv3 SASL specification.
|
|
||||||
// Do not copy a SASLBuffer after first use.
|
|
||||||
type SASLBuffer struct {
|
|
||||||
maxLength int
|
|
||||||
buf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSASLBuffer returns a new SASLBuffer. maxLength is the maximum amount of
|
|
||||||
// data to buffer (0 for no limit).
|
|
||||||
func NewSASLBuffer(maxLength int) *SASLBuffer {
|
|
||||||
result := new(SASLBuffer)
|
|
||||||
result.Initialize(maxLength)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes a SASLBuffer in place.
|
|
||||||
func (b *SASLBuffer) Initialize(maxLength int) {
|
|
||||||
b.maxLength = maxLength
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add processes an additional SASL response chunk sent via AUTHENTICATE.
|
|
||||||
// If the response is complete, it resets the buffer and returns the decoded
|
|
||||||
// response along with any decoding or protocol errors detected.
|
|
||||||
func (b *SASLBuffer) Add(value string) (done bool, output []byte, err error) {
|
|
||||||
if value == "+" {
|
|
||||||
// total size is a multiple of 400 (possibly 0)
|
|
||||||
output = b.buf
|
|
||||||
b.Clear()
|
|
||||||
return true, output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(value) > 400 {
|
|
||||||
b.Clear()
|
|
||||||
return true, nil, ErrSASLTooLong
|
|
||||||
}
|
|
||||||
|
|
||||||
curLen := len(b.buf)
|
|
||||||
chunkDecodedLen := base64.StdEncoding.DecodedLen(len(value))
|
|
||||||
if b.maxLength != 0 && (curLen+chunkDecodedLen) > b.maxLength {
|
|
||||||
b.Clear()
|
|
||||||
return true, nil, ErrSASLLimitExceeded
|
|
||||||
}
|
|
||||||
|
|
||||||
// "append-make pattern" as in the bytes.Buffer implementation:
|
|
||||||
b.buf = append(b.buf, make([]byte, chunkDecodedLen)...)
|
|
||||||
n, err := base64.StdEncoding.Decode(b.buf[curLen:], []byte(value))
|
|
||||||
b.buf = b.buf[0 : curLen+n]
|
|
||||||
if err != nil {
|
|
||||||
b.Clear()
|
|
||||||
return true, nil, err
|
|
||||||
}
|
|
||||||
if len(value) < 400 {
|
|
||||||
output = b.buf
|
|
||||||
b.Clear()
|
|
||||||
return true, output, nil
|
|
||||||
} else {
|
|
||||||
return false, nil, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear resets the buffer state.
|
|
||||||
func (b *SASLBuffer) Clear() {
|
|
||||||
// we can't reuse this buffer in general since we may have returned it
|
|
||||||
b.buf = nil
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue