3
0
mirror of https://github.com/ergochat/ergo.git synced 2025-08-03 11:17:28 +02:00

Compare commits

..

No commits in common. "master" and "v2.3.0-rc1" have entirely different histories.

1077 changed files with 69571 additions and 159590 deletions

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# exclude vendor/ # exclude vendor/
SOURCES="./ergo.go ./irc" SOURCES="./oragono.go ./irc"
if [ "$1" = "--fix" ]; then if [ "$1" = "--fix" ]; then
exec gofmt -s -w $SOURCES exec gofmt -s -w $SOURCES

View File

@ -1,32 +0,0 @@
name: "build"
on:
pull_request:
branches:
- "master"
- "stable"
push:
branches:
- "master"
- "stable"
jobs:
build:
runs-on: "ubuntu-24.04"
steps:
- name: "checkout repository"
uses: "actions/checkout@v3"
- name: "setup go"
uses: "actions/setup-go@v3"
with:
go-version: "1.24"
- name: "install python3-pytest"
run: "sudo apt install -y python3-pytest"
- name: "make install"
run: "make install"
- name: "make test"
run: "make test"
- name: "make smoke"
run: "make smoke"
- name: "make irctest"
run: "make irctest"

View File

@ -1,48 +0,0 @@
name: 'ghcr'
on:
push:
branches:
- "master"
- "stable"
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout Git repository
uses: actions/checkout@v3
- name: Authenticate to container registry
uses: docker/login-action@v2
if: github.event_name != 'pull_request'
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Setup Docker buildx driver
id: buildx
uses: docker/setup-buildx-action@v2
- name: Build and publish image
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

9
.gitignore vendored
View File

@ -95,7 +95,7 @@ _testmain.go
*.out *.out
### custom ### ### Oragono ###
/_site/ /_site/
/.vscode/* /.vscode/*
/ircd* /ircd*
@ -103,11 +103,10 @@ _testmain.go
/web.* /web.*
/ssl.* /ssl.*
/tls.* /tls.*
/ergo /oragono
/build/* /build/*
_test _test
ergo.prof oragono.prof
ergo.mprof oragono.mprof
/dist /dist
*.pem *.pem
.dccache

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "irctest"]
path = irctest
url = https://github.com/ergochat/irctest

View File

@ -1,79 +1,53 @@
# .goreleaser.yml # .goreleaser.yml
# Build customization # Build customization
version: 2 project_name: oragono
project_name: ergo
builds: builds:
- main: ergo.go - main: oragono.go
env: binary: oragono
- CGO_ENABLED=0
binary: ergo
goos: goos:
- linux - freebsd
- windows - windows
- darwin - darwin
- freebsd - linux
- openbsd
- plan9
goarch: goarch:
- "386"
- amd64 - amd64
- arm - arm
- arm64 - arm64
- riscv64
goarm: goarm:
- 6 - 6
- 7
ignore: ignore:
- goos: windows - goos: windows
goarch: arm goarch: arm
- goos: windows
goarch: arm64
- goos: windows
goarch: riscv64
- goos: darwin - goos: darwin
goarch: arm goarch: arm
- goos: darwin - goos: darwin
goarch: riscv64 goarch: 386
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm
- goos: freebsd - goos: freebsd
goarch: arm64 goarch: arm64
- goos: freebsd
goarch: riscv64
- goos: openbsd
goarch: arm
- goos: openbsd
goarch: arm64
- goos: openbsd
goarch: riscv64
- goos: plan9
goarch: arm
- goos: plan9
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
files: files:
- README - README
- CHANGELOG.md - CHANGELOG.md
- LICENSE - oragono.motd
- ergo.motd
- default.yaml - default.yaml
- traditional.yaml - conventional.yaml
- docs/API.md - docs/*
- docs/MANUAL.md
- docs/USERGUIDE.md
- languages/*.yaml - languages/*.yaml
- languages/*.json - languages/*.json
- languages/*.md - languages/*.md

13
.travis.yml Normal file
View File

@ -0,0 +1,13 @@
language: go
go:
- "1.15.x"
before_install:
# https://github.com/travis-ci/travis-ci/issues/8361
- sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'
script:
- make
- make test
- make smoke

View File

@ -1,717 +1,13 @@
# Changelog # Changelog
All notable changes to Ergo will be documented in this file. All notable changes to Oragono will be documented in this file.
## [2.16.0] - 2025-05-18 ## [2.3.0-rc1] - 2020-08-23
We're pleased to be publishing v2.16.0, a new stable release. This release contains bug fixes and some minor updates.
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format. We're pleased to be publishing the release candidate for 2.3.0 (the official release should follow in a week or so).
Many thanks to [@csmith](https://github.com/csmith), [@delthas](https://github.com/delthas), donio, [@emersion](https://github.com/emersion), [@KlaasT](https://github.com/KlaasT), [@knolley](https://github.com/knolley), [@Mailaender](https://github.com/Mailaender), and [@prdes](https://github.com/prdes) for reporting issues and helping test.
### Config changes
* Added `api` block for configuring the new HTTP API. If this block is absent, the API is disabled (#2231)
* Added `server.additional-isupport` for publishing arbitrary ISUPPORT tokens (#2220, #2240)
* Added `server.command-aliases` to configure aliases for server commands (#2229, #2236)
* Added options to `roleplay` to customize the NUH's sent for `NPC` and `SCENE`. Roleplay remains deprecated and disabled by default. (#2237)
### Security
* Mitigated HTTP DoS attacks by rejecting IRC sessions that begin with an HTTP verb, such as `POST`. If you were relying on this to create IRC sessions via an HTTP client, please open an issue. (#2239)
### Added
* Added an HTTP API, providing programmatic access to Ergo functionality (#2231, thanks [@KlaasT](https://github.com/KlaasT)!)
* Added SAFERATE to 005 ISUPPORT tokens (#2223, thanks [@delthas](https://github.com/delthas)!)
* Added support for ed25519-sha256 for DKIM. However, enabling this algorithm is not recommended since mainstream email providers still do not support it. (#1041, #2242)
### Fixed
* Fixed `CHATHISTORY TARGETS` from MySQL backend reporting incorrect timestamps when the server timezone is not UTC (#2224)
* Fixed batch name parameter in `draft/isupport` responses (#2253)
* Fixed `NS UNREGISTER` not deleting the stored push subscriptions (#2254)
* Fixed cases where `NS SAREGISTER` could create clients without applying the default user modes (#2252, #2254, thanks donio!)
* Improved validation of `CHATHISTORY` parameters (#2248, #2249, thanks [@prdes](https://github.com/prdes)!)
* Added validation to ensure the MOTD is UTF-8 when `enforce-utf8` is enabled (the recommended default) (#2228, #2233, thanks [@KlaasT](https://github.com/KlaasT)!)
* The client's own `QUIT` line now respects the `server-time` capability (#2218, #2219)
* Fixed sending unnecessary replies to certain invalid `MODE` changes (#2213)
* Improved safety of ISUPPORT length limits (#2241)
### Changed
* The `draft/message-redaction` capability is no longer advertised when `allow-individual-delete` is disabled (#2215, #2216, thanks [@delthas](https://github.com/delthas)!)
* Receiving the UTF-8 BOM (byte-order mark) at the start of an IRC connection now produces an explicit error (#2244, #2247, thanks [@csmith](https://github.com/csmith), [@Mailaender](https://github.com/Mailaender)!)
### Internal
* Release builds use Go 1.24.3 (#2217)
## [2.15.0] - 2025-01-26
We're pleased to be publishing v2.15.0, a new stable release. This release adds support for mobile push notifications, via the [draft/webpush](https://github.com/ircv3/ircv3-specifications/pull/471) specification. More information on this is available in the [manual](https://github.com/ergochat/ergo/blob/ab2d842b270d9df217c779df9c7a5c594d85fdd5/docs/MANUAL.md#push-notifications) and [user guide](https://github.com/ergochat/ergo/blob/ab2d842b270d9df217c779df9c7a5c594d85fdd5/docs/USERGUIDE.md#push-notifications). This feature is still considered to be in an experimental state; `default.yaml` ships with it disabled, and its configuration may have backwards-incompatible changes in the future.
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.
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 [@delthas](https://github.com/delthas), [@donatj](https://github.com/donatj), donio, [@emersion](https://github.com/emersion), and [@eskimo](https://github.com/eskimo) for contributing patches and helping test.
### Config changes
* Added `webpush` block to the config file to configure push notifications. See `default.yaml` for an example. Note that at this time, `default.yaml` ships with support for push notifications disabled; operators can enable them by setting `webpush.enabled: true`. In the absence of such a block, push notifications are disabled.
* We recommend the addition of `"WEBPUSH": 1` to `fakelag.command-budgets`, to speed up mobile reattach when web push is enabled. See `default.yaml` for an example.
### Added
* Added support for the [draft/webpush](https://github.com/ircv3/ircv3-specifications/pull/471) specification (#2205, thanks [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo)!)
* Added support for the [draft/extended-isupport](https://github.com/ircv3/ircv3-specifications/pull/543) specification (#2184, thanks [@emersion](https://github.com/emersion)!)
* `UBAN ADD` now accepts `REQUIRE-SASL` with NUH masks, i.e. k-lines (#2198, #2199)
* Ergo now publishes the `SAFELIST` ISUPPORT parameter (#2196, thanks [@delthas](https://github.com/delthas)!)
### Fixed
* Fixed incorrect parameters when pushing `005` (ISUPPORT) updates to clients on rehash (#2177, #2184)
### Internal
* Official release builds use Go 1.23.5
* Added a unique identifier to identify connections in debug logs. This has no privacy implications in a standard, non-debug configuration of Ergo. (#2206, thanks donio!)
* Added support for Solaris on amd64 CPUs (#2183)
## [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
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.
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 dedekro, [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo), [@FiskFan1999](https://github.com/FiskFan1999), hauser, [@jwheare](https://github.com/jwheare), [@kingter-sutjiadi](https://github.com/kingter-sutjiadi), knolle, [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@PeGaSuS-Coder](https://github.com/PeGaSuS-Coder), and [@progval](https://github.com/progval) for contributing patches, reporting issues, and helping test.
### Config changes
* Added `fakelag.command-budgets`, which allows each client session a limited number of specific commands that are exempt from fakelag. This improves compatibility with Goguma in particular. For the current recommended default, see `default.yaml` (#1978, thanks [@emersion](https://github.com/emersion)!)
* The recommended value of `server.casemapping` is now `ascii` instead of `precis`. PRECIS remains fully supported; if you are already running an Ergo instance, we do not recommend changing the value unless you are confident that your existing users are not relying on non-ASCII nicknames and channel names. (#1718)
### Changed
* Network services like `NickServ` now appear in `WHO` responses where applicable (#1850, thanks [@emersion](https://github.com/emersion)!)
* The `extended-monitor` capability now appears under its ratified name (#2006, thanks [@progval](https://github.com/progval)!)
* `TAGMSG` no longer receives automatic `RPL_AWAY` responses (#1983, thanks [@eskimo](https://github.com/eskimo)!)
* `UBAN` now states explicitly that bans without a time limit have "indefinite" duration (#1988, thanks [@mogad0n](https://github.com/mogad0n)!)
### Fixed
* `WHO` with a bare nickname as an argument now shows invisible users, comparable to `WHOIS` (#1991, thanks [@emersion](https://github.com/emersion)!)
* MySQL did not work on 32-bit architectures; this has been fixed (#1969, thanks hauser!)
* Fixed the name of the `CHATHISTORY` 005 token (#2008, #2009, thanks [@emersion](https://github.com/emersion)!)
* Fixed handling of the address `::1` in WHOX output (#1980, thanks knolle!)
* Fixed handling of `AWAY` with an empty parameter (the de facto standard is to treat as a synonym for no parameter, which means "back") (#1996, thanks [@emersion](https://github.com/emersion), [@jwheare](https://github.com/jwheare)!)
* Fixed incorrect handling of some invalid modes in `CS AMODE` (#2002, thanks [@eskimo](https://github.com/eskimo)!)
* Fixed incorrect help text for `NS SAVERIFY` (#2021, thanks [@FiskFan1999](https://github.com/FiskFan1999)!)
### Added
* Added the `draft/persistence` capability and associated `PERSISTENCE` command. This is a first attempt to standardize Ergo's "always-on" functionality so that clients can interact with it programmatically. (#1982)
* Sending `SIGUSR1` to the Ergo process now prints a full goroutine stack dump to stderr, allowing debugging even when the HTTP pprof listener is disabled (#1975)
### Internal
* Upgraded to Go 1.19; this makes further architecture-specific bugs like #1969 much less likely (#1987, #1989)
* The test suite is now parallelized (#1976, thanks [@progval](https://github.com/progval)!)
## [2.10.0] - 2022-05-29
We're pleased to be publishing v2.10.0, a new stable release.
This release contains no changes to the config file format or database file format.
Many thanks to [@csmith](https://github.com/csmith), [@FiskFan1999](https://github.com/FiskFan1999), [@Mikaela](https://github.com/Mikaela), [@progval](https://github.com/progval), and [@thesamesam](https://github.com/thesamesam) for contributing patches, and to [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo), [@FiskFan1999](https://github.com/FiskFan1999), [@jigsy1](https://github.com/jigsy1), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@progval](https://github.com/progval), and [@xnaas](https://github.com/xnaas) for reporting issues and helping test.
### Config changes
* For better interoperability with [Goguma](https://sr.ht/~emersion/goguma/), the recommended value of `history.chathistory-maxmessages` has been increased to `1000` (previously `100`) (#1919)
### Changed
* Persistent voice (`AMODE +v`) in a channel is now treated as a permanent invite (i.e. overriding `+i` on the channel) (#1901, thanks [@eskimo](https://github.com/eskimo)!)
* If you are `+R`, sending a direct message to an anonymous user allows them to send you replies (#1687, #1688, thanks [@Mikaela](https://github.com/Mikaela) and [@progval](https://github.com/progval)!)
* `0` is no longer valid as a nickname or account name, with a grandfather exception if it was registered on a previous version of Ergo (#1896)
* Implemented the [ratified version of the bot mode spec](https://ircv3.net/specs/extensions/bot-mode); the tag name is now `bot` instead of `draft/bot` (#1938)
* Privileged WHOX on a user with multiclient shows an arbitrarily chosen client IP address, comparable to WHO (#1897)
* `SAREGISTER` is allowed even under `DEFCON` levels 4 and lower (#1922)
* Operators with the `history` capability are now exempted from time cutoff restrictions on history retrieval (#1593, #1955)
### Added
* Added `draft/read-marker` capability, allowing server-side tracking of read messages for synchronization across multiple clients. (#1926, thanks [@emersion](https://github.com/emersion)!)
* `INFO` now includes the server start time (#1895, thanks [@xnaas](https://github.com/xnaas)!)
* Added `ACCEPT` command modeled on Charybdis/Solanum, allowing `+R` users to whitelist users who can DM them (#1688, thanks [@Mikaela](https://github.com/Mikaela)!)
* Added `NS SAVERIFY` for operators to manually complete an account verification (#1924, #1952, thanks [@tacerus](https://github.com/tacerus)!)
### Fixed
* Having the `samode` operator capability made all uses of the `KICK` command privileged (i.e. overriding normal channel privilege checks); this has been fixed (#1906, thanks [@pcho](https://github.com/pcho)!)
* Fixed `LIST <n` always returning no results (#1934, thanks [@progval](https://github.com/progval) and [@mitchr](https://github.com/mitchr)!)
* NickServ commands are now more clear about when a nickname is unavailable because it was previously registered and unregistered (#1886, thanks [@Mikaela](https://github.com/Mikaela)!)
* Fixed KLINE'd clients producing a `QUIT` snotice without a corresponding `CONNECT` snotice (#1941, thanks [@tacerus](https://github.com/tacerus), [@xnaas](https://github.com/xnaas)!)
* Fixed incorrect handling of long/multiline `319 RPL_WHOISCHANNELS` responses (#1935, thanks [@Mikaela](https://github.com/Mikaela)!)
* Fixed `LIST` returning `403 ERR_NOSUCHCHANNEL` for a nonexistent channel; the correct response is an empty list (#1928, thanks [@emersion](https://github.com/emersion)!)
* Fixed `+s` ("secret") channels not appearing in `LIST` even when the client is already a member (#1911, #1923, thanks [@jigsy1](https://github.com/jigsy1) and [@FiskFan1999](https://github.com/FiskFan1999)!)
* Fixed a spurious success message in `HISTSERV DELETE` by always requiring a consistent number of parameters (#1881, #1927, thanks [@FiskFan1999](https://github.com/FiskFan1999)!)
* Sending the empty string as a nickname would not always produce the expected error numeric `431 ERR_NONICKNAMEGIVEN`; this has been fixed (#1933, #1936, thanks [@kylef](https://github.com/kylef)!)
* `znc.in/playback` timestamps are now parsed as pairs of exact integers, not as floats (#1918)
### Internal
* Upgraded to Go 1.18 (#1925)
* Upgraded Alpine version in official Docker image
* Fixed some issues in the example OpenRC init scripts (#1914, #1920, thanks [@thesamesam](https://github.com/thesamesam)!)
## [2.9.1] - 2022-01-10
Ergo 2.9.1 is a bugfix release, fixing a regression introduced in 2.9.0. We regret the oversight.
This release includes no changes to the config file format or database format relative to 2.9.0.
Many thanks to [@FiskFan1999](https://github.com/FiskFan1999) for reporting the issue.
### Fixed
* Every use of NS SAREGISTER would fail; this has been fixed (#1898, thanks [@FiskFan1999](https://github.com/FiskFan1999)!)
## [2.9.0] - 2022-01-09
We're pleased to be publishing 2.9.0, a new stable release. This release contains mostly bug fixes, with some enhancements to moderation tools.
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 [@erincerys](https://github.com/erincerys), [@FiskFan1999](https://github.com/FiskFan1999), [@mogad0n](https://github.com/mogad0n), and [@tacerus](https://github.com/tacerus) for contributing patches, and to [@ajaspers](https://github.com/ajaspers), [@emersion](https://github.com/emersion), [@FiskFan1999](https://github.com/FiskFan1999), [@Jobe1986](https://github.com/Jobe1986), [@kylef](https://github.com/kylef), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@pcho](https://github.com/pcho), and [@progval](https://github.com/progval) for reporting issues and helping test.
### Config changes
* Added `lock-file`, which helps protect against accidentally starting multiple instances of Ergo. This is a no-op if unset. The recommended default value is `ircd.lock`, which (like the default datastore path `ircd.db`) is relative to the working directory of the Ergo process. If your `datastore.path` is absolute, this path (if set) should be absolute as well. (#1823)
* `+C` (no channel-wide CTCP messages other than ACTION) is now a recommended default channel mode (#1851)
* Added `exempt-sasl` boolean to `server.ip-check-script`; if enabled, IP check scripts are run only for connections without SASL, improving performance for registered users (#1888)
* `hidden: true` is now the recommended default for operator definitions (#1730)
### Changed
* The semantics of `+R` have been changed. `+R` now only prevents unauthenticated users from joining, so unregistered users who have already joined can still speak. The old semantics are still available via `+RM` (i.e. `+R` together with the `+M` "moderated-registered" mode). (#1858, thanks [@ajaspers](https://github.com/ajaspers)!)
* Unauthenticated users matching a `+I` invite exception mask can now join `+R` channels (#1871)
* INVITE now exempts the user from `+b` bans (#1876, thanks [@progval](https://github.com/progval)!)
* NS SUSPEND now only requires only the `ban` operator capability, as opposed to `accreg` (#1828, #1839, thanks [@mogad0n](https://github.com/mogad0n)!)
### Added
* SHA-256 certificate fingerprints can now be imported from Anope and Atheme (#1864, #1869, thanks [@tacerus](https://github.com/tacerus)!)
* IP check scripts can now be run only for users that have not authenticated with SASL by the end of the handshake, improving performance for registered users (#1888)
* Logging into an unverified account with SASL sends the new `NOTE AUTHENTICATE VERIFICATION_REQUIRED` [standard reply code](https://ircv3.net/specs/extensions/standard-replies) (#1852, #1853, thanks [@emersion](https://github.com/emersion)!)
* CS PURGE now sends a snotice (#1826, thanks [@tacerus](https://github.com/tacerus)!)
* The `v` snomask is now used to send notifications about vhost changes initiated by operators (#1844, thanks [@pcho](https://github.com/pcho)!)
### Fixed
* CAP LS and LIST responses after connection registration could be truncated in some cases; this has been fixed (#1872)
* Unprivileged users with both a password and a certfp could not remove their password with `NS PASSWD <password> * *` as expected; this has been fixed (#1883, #1884, thanks [@FiskFan1999](https://github.com/FiskFan1999)!)
* RELAYMSG identifiers that were not already in their case-normalized form could not be muted with `+b m:`; this has been fixed (#1838, thanks [@mogad0n](https://github.com/mogad0n)!)
* CS AMODE changes did not take immediate effect if `force-nick-equals-account` was disabled and the nick did not coincide with the account; this has been fixed (#1860, thanks [@eskimo](https://github.com/eskimo)!)
* `315 RPL_ENDOFWHO` now sends the exact, un-normalized mask argument provided by the client (#1831, thanks [@progval](https://github.com/progval)!)
* A leading `$` character is now disallowed in new nicknames and account names, to avoid collision with the massmessage syntax (#1857, thanks [@emersion](https://github.com/emersion)!)
* The [deprecated](https://github.com/ircdocs/modern-irc/pull/138) `o` parameter of `WHO` now returns an empty list of results, instead of being ignored (#1730, thanks [@kylef](https://github.com/kylef), [@emersion](https://github.com/emersion), [@progval](https://github.com/progval)!)
* WHOX queries for channel oplevel now receive `*` instead of `0` (#1866, thanks [@Jobe1986](https://github.com/Jobe1986)!)
### Internal
* Updated list of official release binaries: added Apple M1, OpenBSD x86-64, and Plan 9 x86-64, removed Linux armv7, FreeBSD x86-32, and Windows x86-32. (The removed platforms are still fully supported by Ergo; you can build them from source or ask us for help.) (#1833)
* Added an official Linux arm64 Docker image (#1855, thanks [@erincerys](https://github.com/erincerys)!)
* Added service management files for OpenSolaris/Illumos (#1846, thanks [@tacerus](https://github.com/tacerus)!)
## [2.8.0] - 2021-11-14
We're pleased to be publishing Ergo 2.8.0. This release contains many fixes and enhancements, plus one major user-facing feature: user-initiated password resets via e-mail (#734).
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.
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).
As part of this release, our official Docker images have moved from Docker Hub to the GitHub Container Registry, at `ghcr.io/ergochat/ergo`. The `stable` and `master` tags correspond to the respective branches. Tagged releases (e.g. `v2.8.0`) are available under the corresponding named tags.
Many thanks to [@ajaspers](https://github.com/ajaspers), [@delthas](https://github.com/delthas), [@mogad0n](https://github.com/mogad0n), [@majiru](https://github.com/majiru), [@ProgVal](https://github.com/ProgVal), and [@tacerus](https://github.com/tacerus) for contributing patches, to [@ajaspers](https://github.com/ajaspers) for contributing code review, to [@ajaspers](https://github.com/ajaspers), [@cxxboy](https://github.com/cxxboy), [@dallemon](https://github.com/dallemon), [@emersion](https://github.com/emersion), [@erikh](https://github.com/erikh), [@eskimo](https://github.com/eskimo), [@jwheare](https://github.com/jwheare), [@kylef](https://github.com/kylef), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@MystaraTheGreat](https://github.com/MystaraTheGreat), [@ProgVal](https://github.com/ProgVal), [@tacerus](https://github.com/tacerus), [@tamiko](https://github.com/tamiko), and [@xnaas](https://github.com/xnaas) for reporting issues and helping test, and to our translators for contributing translations.
### Config changes
* Added `accounts.registration.email-verification.password-reset` block to configure e-mail-based password reset (#734, #1779)
* Added `accounts.registration.email-verification.timeout` to impose a timeout on e-mail sending; the recommended default value is `60s` (60 seconds) (#1741)
* Added `server.suppress-lusers` to allow hiding the LUSERS counts (#1802, thanks [@eskimo](https://github.com/eskimo)!)
### Security
* Added `accounts.registration.email-verification.timeout` to impose a timeout on e-mail sending; the recommended default value is `60s` (60 seconds) (#1741)
### Added
* Added user-initiated password resets via email (#734). This requires e-mail verification of accounts, and must additionally be enabled explicitly: see the `email-verification` block in `default.yaml` for more information.
* Added the `draft/extended-monitor` capability (#1761, thanks [@delthas](https://github.com/delthas)!)
* When doing direct sending of verification emails, make email delivery failures directly visible to the end user (#1659, #1741, thanks [@tacerus](https://github.com/tacerus)!)
* For operators, `NS INFO` now shows the user's email address (you can also view your own address) (#1677, thanks [@ajaspers](https://github.com/ajaspers)!)
* Operators with the appropriate permissions will now see IPs in `/WHOWAS` output (#1702, thanks [@ajaspers](https://github.com/ajaspers)!)
* Added the `+s d` snomask, for operators to receive information about session disconnections that do not result in a full QUIT (#1709, #1728, thanks [@mogad0n](https://github.com/mogad0n)!)
* Added support for the `SCRAM-SHA-256` SASL authentication mechanism (#175). This mechanism is not currently advertised in `CAP LS` output because IRCCloud handles it incorrectly. We also [recommend against using SCRAM because of its lack of genuine security benefits](https://gist.github.com/slingamn/3f2fed196df5ef14d1316a1ffa9d59f8).
* `/UBAN LIST` output now includes the time the ban was created (#1725, #1755, thanks [@Mikaela](https://github.com/Mikaela) and [@mogad0n](https://github.com/mogad0n)!)
* Added support for running as a `Type=notify` systemd service (#1733)
* Added a warning to help users detect incorrect uses of `/QUOTE` (#1530)
### Fixed
* The `+M` (only registered users can speak) channel mode did not work; this has been fixed (#1696, thanks [@Mikaela](https://github.com/Mikaela)!)
* A channel `/RENAME` that only changed the case of the channel would delete the channel registration; this has been fixed (#1751, thanks [@Mikaela](https://github.com/Mikaela)!)
* Fixed `allow-truncation: true` not actually allowing truncation of overlong lines (#1766, thanks [@tacerus](https://github.com/tacerus)!)
* Fixed several pagination bugs in `CHATHISTORY` (#1676, thanks [@emersion](https://github.com/emersion)!)
* Fixed support for kicking multiple users from a channel on the same line, the `TARGMAX` 005 parameter that advertises this, and the default kick message (#1748, #1777, #1776), thanks [@ProgVal](https://github.com/ProgVal)!)
* Fixed `/SAMODE` on a channel not producing a snomask (#1787, thanks [@mogad0n](https://github.com/mogad0n), [@ajaspers](https://github.com/ajaspers)!)
* Adding `+f` to a channel with `SAMODE` used to require channel operator privileges on the receiving channel; this has been fixed (#1825, thanks [@Mikaela](https://github.com/Mikaela)!)
* Fixed parameters sent with `697 ERR_LISTMODEALREADYSET` and `698 ERR_LISTMODENOTSET` (#1727, thanks [@kylef](https://github.com/kylef)!)
* Fixed parameter sent with `696 ERR_INVALIDMODEPARAM` (#1773, thanks [@kylef](https://github.com/kylef)!)
* Fixed handling of channel mode `+k` with an empty parameter (#1774, #1775, thanks [@ProgVal](https://github.com/ProgVal)!)
* `WHOWAS` with an empty string as the parameter now produces an appropriate error response (#1703, thanks [@kylef](https://github.com/kylef)!)
* Fixed error response to an empty realname on the `USER` line (#1778, thanks [@ProgVal](https://github.com/ProgVal)!)
* Fixed `/UBAN ADD` of a NUH mask (i.e. a k-line) not killing affected clients (#1736, thanks [@mogad0n](https://github.com/mogad0n)!)
* Fixed buggy behavior when `+i` is configured as a default mode for channels (#1756, thanks [@Mikaela](https://github.com/Mikaela)!)
* Fixed issues with `channels.operator-only-creation` not respecting `/SAJOIN` or always-on clients (#1757)
* Protocol-breaking operator vhosts are now disallowed during config validation (#1722)
* Fixed error message associated with `/NS PASSWD` on a nonexistent account (#1738, thanks [@Mikaela](https://github.com/Mikaela)!)
* Fixed an incorrect `CHATHISTORY` fail message (#1731, thanks [@ProgVal](https://github.com/ProgVal)!)
* Fixed a panic on an invalid configuration case (#1714, thanks [@erikh](https://github.com/erikh)!)
### Changed
* Upgraded the `draft/register` capability to the latest [`draft/account-registration`](https://github.com/ircv3/ircv3-specifications/pull/435) iteration (#1740)
* Unregistered users with `+v` or higher can now speak in `+R` (registered-only) channels (#1715, thanks [@Mikaela](https://github.com/Mikaela) and [@ajaspers](https://github.com/ajaspers)!)
* For always-on clients with at least one active connection, `338 RPL_WHOISACTUALLY` now displays an arbitrarily chosen client IP address (#1650, thanks [@MystaraTheGreat](https://github.com/MystaraTheGreat)!)
* `#` can no longer be used in new account names and nicknames, or as the RELAYMSG separator (#1679)
* The `oragono.io/nope` capability was renamed to `ergo.chat/nope` (#1793)
### Removed
* `never` is no longer accepted as a value of the `replay-joins` NickServ setting (`/NS SET replay-joins`); user accounts which enabled this setting have been reverted to the default value of `commands-only` (#1676)
### Internal
* We have a cool new logo!
* Official builds now use Go 1.17 (#1781)
* Official Docker containers are now at [ghcr.io/ergochat/ergo](https://ghcr.io/ergochat/ergo) (#1808)
* Added a traditional SysV init script (#1691, thanks [@tacerus](https://github.com/tacerus)!)
* Added an s6 init script (#1786, thanks [@majiru](https://github.com/majiru)!)
## [2.7.0] - 2021-06-07
We're pleased to be publishing Ergo 2.7.0, our first official release under our new name of Ergo. This release contains bug fixes and minor enhancements.
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. This release includes no changes to the database format.
Because the name of the executable has changed from `oragono` to `ergo` (`ergo.exe` on Windows), you may need to update your system configuration (e.g., scripts or systemd unit files that reference the executable).
Many thanks to [@ajaspers](https://github.com/ajaspers) and [@jesopo](https://github.com/jesopo) for contributing patches, to [@ajaspers](https://github.com/ajaspers), [@ChrisTX](https://github.com/ChrisTX), [@emersion](https://github.com/emersion), [@jwheare](https://github.com/jwheare), [@kylef](https://github.com/kylef), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), and [@ProgVal](https://github.com/ProgVal) for reporting issues and helping test, and to our translators for contributing translations.
### Changed
* The project was renamed from "Oragono" to "Ergo" (#897, thanks to everyone who contributed feedback or voted in the poll)
### Config changes
* Entries in `server.listeners` now take a new key, `min-tls-version`, that can be used to set the minimum required TLS version; the recommended default value is `1.2` (#1611, thanks [@ChrisTX](https://github.com/ChrisTX)!)
* Added `max-conns` (maximum connection count) and `max-conn-lifetime` (maximum lifetime of a connection before it is cycled) to `datastore.mysql` (#1622)
* Added `massmessage` operator capability to allow sending NOTICEs to all connected users (#1153, #1629, thanks [@jesopo](https://github.com/jesopo)!)
### Security
* If `require-sasl.enabled` is set to `true`, `tor-listeners.require-sasl` will be automatically set to `true` as well (#1636)
* It is now possible to set the minimum required TLS version, using the `min-tls-version` key in listener configuration
* Configurations that require SASL but allow user registration now produce a warning (#1637)
### Added:
* Operators with the correct permissions can now send "mass messages", e.g. `/NOTICE $$*` will send a `NOTICE` to all users (#1153, #1629, thanks [@jesopo](https://github.com/jesopo)!)
* Operators can now extend the maximum (non-tags) length of the IRC line using the `server.max-line-len` configuration key. This is not recommended for use outside of "closed-circuit" deployments where IRC operators have full control of all client software. (#1651)
### Fixed
* `RELAYMSG` now sends a full NUH ("nick-user-host"), instead of only the relay nickname, as the message source (#1647, thanks [@ProgVal](https://github.com/ProgVal), [@jwheare](https://github.com/jwheare), and [@Mikaela](https://github.com/Mikaela)!)
* Fixed a case where channels would remain visible in `/LIST` after unregistration (#1619, thanks [@ajaspers](https://github.com/ajaspers)!)
* Fixed incorrect tags on `JOIN` lines in `+u` ("auditorium") channels (#1642)
* Fixed an issue where LUSERS counts could get out of sync (#1617)
* It was impossible to add a restricted set of snomasks to an operator's permissions; this has been fixed (#1618)
* Fixed incorrect language in `NS INFO` responses (#1627, thanks [@ajaspers](https://github.com/ajaspers)!)
* Fixed a case where the `REGISTER` command would emit an invalid error message (#1633, thanks [@ajaspers](https://github.com/ajaspers)!)
* Fixed snomasks displaying in a nondeterministic order (#1669, thanks [@Mikaela](https://github.com/Mikaela)!)
### Removed
* Removed the `draft/resume-0.5` capability, and the associated `RESUME` and `BRB` commands (#1624)
### Internal
* Optimized MySQL storage of direct messages (#1615)
## [2.6.1] - 2021-04-26
Oragono 2.6.1 is a bugfix release, fixing a security issue that is critical for some private server configurations. We regret the oversight.
The issue affects two classes of server configuration:
1. Private servers that use `server.password` (i.e., the `PASS` command) for protection. If `accounts.registration.allow-before-connect` is enabled, the `REGISTER` command can be used to bypass authentication. Affected operators should set this field to `false`, or upgrade to 2.6.1, which disallows the insecure configuration. (If the field does not appear in the configuration file, the configuration is secure since the value defaults to false when unset.)
2. Private servers that use `accounts.require-sasl` for protection. If these servers do not additionally set `accounts.registration.enabled` to `false`, the `REGISTER` command can potentially be used to bypass authentication. Affected operators should set `accounts.registration.enabled` to false; this recommendation appeared in the operator manual but was not emphasized sufficiently. (Configurations that require SASL but allow open registration are potentially valid, e.g., in the case of public servers that require everyone to use a registered account; accordingly, Oragono 2.6.1 continues to permit such configurations.)
This release includes no changes to the config file format or the database.
Many thanks to [@ajaspers](https://github.com/ajaspers) for reporting the issue.
### Security
* Fixed and documented potential authentication bypasses via the `REGISTER` command (#1634, thanks [@ajaspers](https://github.com/ajaspers)!)
## [2.6.0] - 2021-04-18
We're pleased to announce Oragono 2.6.0, a new stable release.
This release has some user-facing enhancements, but is primarily focused on fixing bugs and advancing the state of IRCv3 standardization (by publishing a release that implements the latest drafts). Some highlights:
* A new CHATHISTORY API for listing direct message conversations (#1592)
* The latest proposal for IRC-over-websockets, which should be backwards-compatible with existing clients (#1558)
* The latest specification for the bot usermode (`+B` in our implementation) (#1562)
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.
This release includes no changes to the embedded database format. If you are using MySQL for history storage, it adds a new table; this change is backwards and forwards-compatible and does not require any manual intervention.
If you are using nginx as a reverse proxy for IRC-over-websockets, previous documentation did not recommend increasing `proxy_read_timeout`; the default value of `60s` is too low and can lead to user disconnections. The current recommended value is `proxy_read_timeout 600s;`; see the manual for an example configuration.
Many thanks to [@ajaspers](https://github.com/ajaspers) and [@Mikaela](https://github.com/Mikaela) for contributing patches, to [@aster1sk](https://github.com/aster1sk), [@emersion](https://github.com/emersion), [@eskimo](https://github.com/eskimo), [@hhirtz](https://github.com/hhirtz), [@jlu5](https://github.com/jlu5), [@jwheare](https://github.com/jwheare), [@KoraggKnightWolf](https://github.com/KoraggKnightWolf), [@kylef](https://github.com/kylef), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@ProgVal](https://github.com/ProgVal), and [@szlend](https://github.com/szlend) for reporting issues and helping test, and to our translators for contributing translations.
### Config changes
* Listeners now support multiple TLS certificates for use with SNI; see the manual for details (#875, thanks [@Mikaela](https://github.com/Mikaela)!)
* Added `server.compatibility.allow-truncation`, controlling whether the server accepts messages that are too long to be relayed intact; this value defaults to `true` when unset (#1577, #1586, thanks [@kylef](https://github.com/kylef)!)
* Added new `snomasks` operator capability; operators must have either the `ban` or `snomasks` capability to subscribe to additional snomasks (#1176)
### Security
* Fixed several edge cases where Oragono might relay invalid UTF8 despite the `UTF8ONLY` guarantee, or to a text-mode websocket client (#1575, #1596, thanks [@ProgVal](https://github.com/ProgVal)!)
* All operator privilege checks now use the capabilities system, making it easier to define operators with restricted powers (#1176)
* Adding and removing bans with `UBAN` now produces snomasks and audit loglines (#1518, thanks [@mogad0n](https://github.com/mogad0n)!)
### Fixed
* Fixed an edge case in line buffering that could result in client disconnections (#1572, thanks [@ProgVal](https://github.com/ProgVal)!)
* Upgraded buntdb, our embedded database library, fixing an edge case that could cause data corruption (#1603, thanks [@Mikaela](https://github.com/Mikaela), [@tidwall](https://github.com/tidwall)!)
* Improved compatibility with the published `draft/register` specification (#1568, thanks [@ProgVal](https://github.com/ProgVal)!)
* `433 ERR_NICKNAMEINUSE` is no longer sent when a fully connected ("registered") client fails to claim a reserved nickname, fixing a bad interaction with some client software (#1594, thanks [@ProgVal](https://github.com/ProgVal)!)
* Fixed `znc.in/playback` commands causing client disconnections when history is disabled (#1552, thanks [@szlend](https://github.com/szlend)!)
* Fixed syntactically invalid `696 ERR_INVALIDMODEPARAM` response for invalid channel keys (#1563, thanks [@ProgVal](https://github.com/ProgVal)!)
* User-set nickserv settings now display as "enabled" instead of "mandatory" (#1544, thanks [@Mikaela](https://github.com/Mikaela)!)
* Improved error messages for some invalid configuration cases (#1559, thanks [@aster1sk](https://github.com/aster1sk)!)
* Improved `CS TRANSFER` error messages (#1534, thanks burning!)
* Handle panics caused when rehashing with SIGHUP (#1570)
### Changed
* Registered channels will always appear in `/LIST` output, even with no members (#1507)
* In the new recommended default configuration, Oragono will preemptively reject messages that are too long to be relayed to clients without truncation. This is controlled by the config variable `server.compatibility.allow-truncation`; this field defaults to `true` when unset, preserving the legacy behavior for older config files (#1577, #1586, thanks [@kylef](https://github.com/kylef)!)
* Auto-away behavior now respects individual clients; the user is not considered away unless all clients are away or disconnected (#1531, thanks [@kylef](https://github.com/kylef)!)
* Direct messages rejected due to the `+R` registered-only usermode now produce an error message (#1064, thanks [@KoraggKnightWolf](https://github.com/KoraggKnightWolf), [@ajaspers](https://github.com/ajaspers)!)
* RELAYMSG identifiers now respect bans and mutes (#1502)
* If end user message deletion is enabled, channel operators can now delete channel messages (#1565, thanks [@Mikaela](https://github.com/Mikaela)!)
* Halfops can change the channel topic (#1523)
* Snomask add/remove syntax now matches other ircds more closely (#1074)
* `CS OP` will regrant your channel `AMODE`, in case you removed it (#1516, #1307, thanks [@jlu5](https://github.com/jlu5)!)
* User passwords may no longer begin with `:` (#1571)
* Improved documentation of `CS AMODE` and `NS UNREGISTER` (#1524, #1545, thanks [@Mikaela](https://github.com/Mikaela)!)
* Disabling history disables history-related CAPs (#1549)
### Added
* Implemented the new [CHATHISTORY TARGETS](https://github.com/ircv3/ircv3-specifications/pull/450) API for listing direct message conversations (#1592, thanks [@emersion](https://github.com/emersion), [@hhirtz](https://github.com/hhirtz), [@jwheare](https://github.com/jwheare), [@kylef](https://github.com/kylef)!)
* Implemented the new [IRC-over-websockets draft](https://github.com/ircv3/ircv3-specifications/pull/342), adding support for binary websockets and subprotocol negotiation (#1558, thanks [@jwheare](https://github.com/jwheare)!)
* Implemented the new [bot mode spec](https://github.com/ircv3/ircv3-specifications/pull/439) (#1562)
* Implemented the new [forward mode spec](https://github.com/ircv3/ircv3-specifications/pull/440) (#1612, thanks [@ProgVal](https://github.com/ProgVal)!)
* `WARN NICK ACCOUNT_REQUIRED` is sent on failed attempts to claim a reserved nickname (#1594)
* `NS CLIENTS LIST` displays enabled client capabilities (#1576)
* `CS INFO` with no arguments lists your registered channels (#765)
* `NS PASSWORD` is now accepted as an alias for `NS PASSWD` (#1547)
### Internal
* Upgraded to Go 1.16 (#1510)
## [2.5.1] - 2021-02-02
Oragono 2.5.1 is a bugfix release that fixes a significant security issue. We apologize for the oversight.
This release includes no changes to the config file format or the database.
Many thanks to [@xnaas](https://github.com/xnaas) for reporting the issue.
### Security
* Fix an incorrect permissions check in NickServ (#1520, thanks [@xnaas](https://github.com/xnaas)!)
## [2.5.0] - 2021-01-31
We're pleased to announce Oragono 2.5.0, a new stable release.
This release includes enhancements based on the needs of real-world operators, as well as bug fixes. Highlights include:
* `UBAN`, a new "unified ban" system for server operators, with a corresponding `CHANSERV HOWTOBAN` command for channel operators (#1447)
* A new forwarding/overflow channel mode `+f` (#1260)
* Support for PROXY protocol v2 (#1389)
This release includes changes to the config file format, including two breaking changes. One is fairly significant: enabling a websocket listener now requires the use of `server.enforce-utf8`, as has been the recommended default since 2.2.0 (so continuing to accept legacy non-UTF-8 content will require disabling websockets). The other is that the "unban" operator capability has been removed (it is now included in the "ban" capability). Other config changes are backwards compatible and do not require updating the file before upgrading.
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 Oragono. Otherwise, you can update the database manually by running `oragono upgradedb` (see the manual for complete instructions).
Many thanks to [@jlu5](https://github.com/jlu5), [@kylef](https://github.com/kylef) and [@Mikaela](https://github.com/Mikaela) for contributing patches, to [@bogdomania](https://github.com/bogdomania), [@eskimo](https://github.com/eskimo), [@happyhater](https://github.com/happyhater), [@jlu5](https://github.com/jlu5), [@kylef](https://github.com/kylef), [@LukeHoersten](https://github.com/LukeHoersten), [@Mikaela](https://github.com/Mikaela), [@mogad0n](https://github.com/mogad0n), [@robinlemon](https://github.com/robinlemon), and [@vertisan](https://github.com/vertisan) for reporting issues and helping test, and to our translators for contributing translations.
### Config changes
* Enabling websockets now requires `server.enforce-utf8 = true` (#1483)
* `proxy` is now a top-level field of the listener config block; in particular, the PROXY protocol (v1 or v2) can now be required ahead of a plaintext connection. The field is still accepted in its legacy position (inside the `tls` block). (#1389, thanks [@robinlemon](https://github.com/robinlemon)!)
* Added `accounts.multiclient.always-on-expiration`, allowing always-on clients to be timed out for inactivity (#810, thanks [@bogdomania](https://github.com/bogdomania)!)
* `local_` prefixes have been stripped from operator capability names, so that, e.g., `local_ban` is now just `ban`. The old names are still accepted. (#1442)
* The `local_unban` operator capability has been removed (unbanning is now contained in the `ban` permission). (#1442)
* The recommended value of `accounts.bcrypt-cost` is now `4`, the minimum acceptable value (#1497)
* `server.ip-limits.custom-limits` now accepts networks that contain multiple CIDRs; the old syntax is still accepted (#1421, thanks [@Mikaela](https://github.com/Mikaela)!
* A new field, `history.restrictions.query-cutoff`, generalizes the old `history.restrictions.enforce-registration-date` (the old field is still accepted) (#1490, thanks [@Mikaela](https://github.com/Mikaela)!)
* Added `server.override-services-hostname`, allowing the hostname of NickServ, ChanServ, etc. to be overridden (#1407, thanks [@Mikaela](https://github.com/Mikaela)!)
* Added a boolean `hide-sts` key to the listener block; this can be used to hide the STS CAP when the listener is secured at layer 3 or 4 (e.g., by a VPN or an E2E mixnet). It will still be necessary to add the relevant IPs to `secure-nets`. (#1428, thanks [@Mikaela](https://github.com/Mikaela)!)
### Security
* Improved validation of names and encodings for client-only tags (#1385)
* Improved auditability of sensitive operator actions (#1443, thanks [@mogad0n](https://github.com/mogad0n)!)
* `DEFCON 4` and lower now require Tor users to authenticate with SASL (#1450)
### Fixed
* Fixed `NS UNSUSPEND` requiring the casefolded / lowercase version of the account name (#1382, thanks [@mogad0n](https://github.com/mogad0n)!)
* Fixed client-only tags in direct (user-to-user) `PRIVMSG` not being replayed (#1411)
* Fixed many bugs in import of Anope and Atheme databases (#1403, #1423, #1424, #1431, #1435, #1439, #1444, thanks [@jlu5](https://github.com/jlu5), [@kylef](https://github.com/kylef), and [@Mikaela](https://github.com/Mikaela)!)
* Fixed case-handling bugs in `RENAME` (i.e., channel rename) (#1456, thanks [@mogad0n](https://github.com/mogad0n)!)
* Fixed incorrect processing of color code escapes in MOTD files (#1467, thanks [@mogad0n](https://github.com/mogad0n)!)
* STS is no longer advertised to Tor clients (#1428, thanks [@Mikaela](https://github.com/Mikaela)!)
* Fixed HELP/HELPOP numerics not including the nick as an argument (#1472, thanks [@kylef](https://github.com/kylef)!)
* Made connection registration snomasks less confusing (#1396, thanks [@eskimo](https://github.com/eskimo)!)
* Fixed duplicated nicks in `KLINE` response (#1379, thanks [@mogad0n](https://github.com/mogad0n)!)
* The `RELAYMSG` tag name is now `draft/relaymsg`, conforming to the amended draft specification (#1468, thanks [@jlu5](https://github.com/jlu5)!)
* Fixed `SAJOIN` not sending a `MODE` line to the originating client (#1383, thanks [@mogad0n](https://github.com/mogad0n)!)
* Improved consistency of message sources sent by `CS AMODE` (#1383, thanks [@mogad0n](https://github.com/mogad0n)!)
* Fixed duplicated `JOIN` line sent to some clients using the `draft/resume-0.5` extension (#1397, thanks [@kylef](https://github.com/kylef)!)
* Added a warning that MySQL cannot be enabled by rehash (#1452, thanks [@Mikaela](https://github.com/Mikaela)!)
### Changed
* Channel-user modes (e.g., `+o`, `+v`) of always-on clients are now persisted in the database (#1345)
* `/CHANSERV PURGE` now takes `ADD`, `DEL`, and `LIST` subcommands; the separate `UNPURGE` command has been removed; `PURGE ADD` now requires a confirmation code (#1294, thanks [@mogad0n](https://github.com/mogad0n)!)
* The characters `<`, `>`, `'`, `"`, and `;` are no longer allowed in nicknames (previously registered account names containing these characters are still accepted) (#1436, thanks [@happyhater](https://github.com/happyhater)!)
* Authenticated clients from Tor now receive their (account-unique) always-on cloaked hostname; this allows channel operators to ban unauthenticated Tor users by banning `*!*@tor-network.onion` (#1479, thanks [@mogad0n](https://github.com/mogad0n)!)
* Included the network name in the human-readable final parameter of `001 RPL_WELCOME` (#1410)
* `RELAYMSG` can now take client-only tags (#1470)
* WebSocket listeners will attempt to negotiate the `text.ircv3.net` [subprotocol](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#subprotocols); negotiating this is optional for clients (#1483)
### Added
* Added `UBAN`, a new command giving server operators a unified interface to D-LINEs (IP bans), K-LINEs (NUH mask bans, which are now deprecated), and account suspensions (`NS SUSPEND`) (#1447)
* Added `CHANSERV HOWTOBAN`, a ChanServ subcommand that helps channel operators choose an appropriate ban (#1447)
* Added a new channel mode `+f`; users who cannot join the channel due to `+i` or `+l` will be forwarded to the channel specified by `+f`. (#1260)
* Added support for the PROXY protocol v2 (#1389, thanks [@robinlemon](https://github.com/robinlemon)!)
* Added support for `/JOIN 0` (part all channels), requiring a confirmation code (#1417, thanks [@Mikaela](https://github.com/Mikaela)!)
* Added support for grouped nicknames as SASL usernames (#1476, thanks [@eskimo](https://github.com/eskimo)!)
* Added history support for `INVITE` (#1409, thanks [@Mikaela](https://github.com/Mikaela)!)
* Added a new channel setting accessible via `/CS SET`: `history-cutoff`, allowing the channel owner more fine-grained control over who can see history (#1490, thanks [@Mikaela](https://github.com/Mikaela)!)
* Added the `UTF8ONLY` ISUPPORT token, allowing the server to advertise to clients that only UTF-8 content is accepted (#1483)
* Added `/NICKSERV RENAME`, an operator-only command that can change the case of an account name (#1380, thanks [@LukeHoersten](https://github.com/LukeHoersten)!)
### Internal
* Added caching for serialized messages (#1387)
* Improved memory efficiency of line reading (#1231)
## [2.4.0] - 2020-11-08
We're pleased to announce Oragono 2.4.0, a new stable release.
This release includes a number of exciting enhancements and fixes. Here are some highlights:
* Support for migrating an Anope or Atheme database to Oragono (#1042)
* A pluggable system for validating external IPs, e.g., via DNSBLs (#68, thanks [@moortens](https://github.com/moortens)!)
* [draft/relaymsg](https://github.com/ircv3/ircv3-specifications/pull/417), a new draft extension simplifying bridging with other chat systems (thanks [@jlu5](https://github.com/jlu5)!)
* New moderation tools: `+u` ("auditorium", #1300), `+U` ("op-moderated", #1178), `+M` ("moderated-registered", #1182, thanks [@ajaspers](https://github.com/ajaspers)!), and `+b m:` (an extban for muting users, #307)
This release includes changes to the config file format, including one breaking change: `roleplay.enabled` now defaults to false (the new recommended default) instead of true when unset. Other config changes are backwards compatible and do not require updating the file before upgrading.
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 Oragono. Otherwise, you can update the database manually by running `oragono upgradedb` (see the manual for complete instructions).
Many thanks to [@ajaspers](https://github.com/ajaspers), [@jesopo](https://github.com/jesopo), [@moortens](https://github.com/moortens), and [@RunBarryRun](https://github.com/RunBarryRun) for contributing patches, to [@csmith](https://github.com/csmith) for contributing code reviews, to [@ajaspers](https://github.com/ajaspers), [@Amiga60077](https://github.com/Amiga60077), [@bogdomania](https://github.com/bogdomania), [@csmith](https://github.com/csmith), [@edk0](https://github.com/edk0), [@eskimo](https://github.com/eskimo), [@jlu5](https://github.com/jlu5), [@jwheare](https://github.com/jwheare), [@KoraggKnightWolf](https://github.com/KoraggKnightWolf), [@Mitaka8](https://github.com/Mitaka8), [@mogad0n](https://github.com/mogad0n), [@RyanSquared](https://github.com/RyanSquared), and [@vertisan](https://github.com/vertisan) for reporting issues and helping test, and to our translators for contributing translations.
### Config changes
* Added `server.ip-cloaking.enabled-for-always-on`, which generates a unique hostname for each always-on client. The recommended default value of this field is `true` (#1312)
* Added `server.coerce-ident`; if this is set to a string value, all user/ident fields supplied by clients are ignored and replaced with this value. The recommended default value of this field is `~u`. This simplifies bans. (#1340)
* Simplified the config file format for email verification into a new `accounts.nick-reservation.email-verification` section. The old format (`callbacks`) is still accepted (#1075)
* The recommended value of `roleplay.enabled` is now `false`; this field now defaults to false when unset (#1240, #1271)
* Added `server.relaymsg` section for configuring the new `draft/relaymsg` capability; added the new `relaymsg` operator capability for exercising it (#1119)
* Added `allow-environment-overrides` config variable, allowing config options to be overridden by environment variables. See the manual for more details. (#1049, thanks [@csmith](https://github.com/csmith)!)
* Added `server.ip-check-script` for configuring IP check plugins (#68, #1267, thanks [@moortens](https://github.com/moortens)!)
* Added `max-concurrency` restriction to `accounts.auth-script` section. The recommended default value is `64` (`0` or unset disable the restriction) (#1267)
* Added `accounts.registration.allow-before-connect`; this allows the use of the new `REGISTER` command before connecting to the server (#1075)
* Added `hidden` option in operator blocks: if set to `true`, operator status is hidden from commands like `WHOIS` that would otherwise display it (#1194)
* Added `accounts.nick-reservation.forbid-anonymous-nick-changes`, which forbids anonymous users from changing their nicknames after initially connecting (#1337, thanks [@Amiga60077](https://github.com/Amiga60077)!)
* Added `channels.invite-expiration`, allowing invites to `+i` channels to expire after a given amount of time (#1171)
### Security
* Added `/NICKSERV CLIENTS LOGOUT` command for disconnecting clients connected to a user account (#1072, #1272, thanks [@ajaspers](https://github.com/ajaspers)!)
* Disallowed the use of service nicknames during roleplaying (#1240, thanks [@Mitaka8](https://github.com/Mitaka8)!)
* Improved security properties of `INVITE` for invite-only channels, including an `UNINVITE` command (#1171)
### Removed
* Removed the request queue system for HostServ, i.e., the `REQUEST`, `APPROVE`, and `REJECT` subcommands of `HOSTSERV` (#1346)
### Fixed
* `PONG` is now sent with the server name as the first parameter, matching the behavior of other ircds (#1249, thanks [@jesopo](https://github.com/jesopo)!)
* It was not possible to set or unset the `+T` no-CTCP user mode; this has been fixed (#1299, thanks [@mogad0n](https://github.com/mogad0n)!)
* Fixed edge cases with `/NICKSERV SAREGISTER` of confusable nicknames (#1322, thanks [@mogad0n](https://github.com/mogad0n)!)
* Fixed websocket listeners with proxy-before-TLS enabled closing on invalid PROXY lines (#1269, thanks [@RyanSquared](https://github.com/RyanSquared)!)
* Fixed error responses and history for SANICK (#1277, #1278, thanks [@eskimo](https://github.com/eskimo)!)
* Ensured that stored realnames of always-on clients are deleted during account unregistration (#1330)
* Whitespace is now stripped from KLINEs (#1327, thanks [@mogad0n](https://github.com/mogad0n)!)
* Fixed incorrect `LUSERS` counts caused by KLINE (#1303, thanks [@mogad0n](https://github.com/mogad0n)!)
* `CHATHISTORY` queries for invalid channels now get an empty batch instead of a `FAIL` (#1322)
* `fakelag.messages-per-window = 0` no longer causes a panic (#861, thanks [@vertisan](https://github.com/vertisan)!)
### Added
* Added `oragono importdb` command for importing a converted Anope or Atheme database; see the manual for details (#1042)
* Added support for the new [draft/relaymsg](https://github.com/ircv3/ircv3-specifications/pull/417) extension, which simplifies bridging IRC with other protocols relaymsg (#1119, thanks [@jlu5](https://github.com/jlu5)!)
* Added `ip-check-script`, a scripting API for restricting access by client IP. We provide [oragono-dnsbl](https://github.com/oragono/oragono-dnsbl), an external script that can query DNSBLs for this purpose (#68, #1267, thanks [@moortens](https://github.com/moortens)!)
* Added channel mode `+u`. This is an "auditorium" mode that prevents unprivileged users from seeing each other's `JOIN` and `PART` lines. It's useful for large public-announcement channels, possibly in conjunction with `+m` (#1300)
* Added channel mode `+U`. This is an "op-moderated" mode; messages from unprivileged users are sent only to channel operators, who can then choose to grant them `+v`. (#1178)
* Added a mute extban `+b m:`: users matching the ban expression (e.g., `+b m:*!*@j6dwi4vacx47y.irc`) will be able to join the channel, but will be unable to speak. (#307)
* Added support for the new [draft/register](https://gist.github.com/edk0/bf3b50fc219fd1bed1aa15d98bfb6495) extension, which exposes a cleaner account registration API to clients (#1075, thanks [@edk0](https://github.com/edk0)!)
* Added a `379 RPL_WHOISMODES` line to the `WHOIS` response, making it easier for operators to see other users' modes (#769, thanks [@Amiga60077](https://github.com/Amiga60077) and [@KoraggKnightWolf](https://github.com/KoraggKnightWolf)!)
* Added `/CHANSERV DEOP` command for removing channel operator privileges (#361, thanks [@RunBarryRun](https://github.com/RunBarryRun)!)
* Added `r` flag to `/WHO` responses for registered nicknames (#1366, thanks [@Amiga60077](https://github.com/Amiga60077)!)
### Changed
* Always-on clients now receive a user/ident of `~u` by default, instead of `~user`; this can be changed by setting the `coerce-ident` field (#1340)
* `/NICKSERV SUSPEND` has been modified to take subcommands (`ADD`, `DEL`, and `LIST`); the `ADD` subcommand now accepts time duration and reason arguments. See `/msg NickServ HELP SUSPEND` for details. (#1274, thanks [@mogad0n](https://github.com/mogad0n)!)
* Only the channel founder can kick the channel founder, regardless of either party's modes (#1262)
* `/NICKSERV SESSIONS` is now `/NICKSERV CLIENTS LIST`, but the old command is still accepted (#1272, thanks [@ajaspers](https://github.com/ajaspers)!)
* Improved `SETNAME` behavior for legacy clients (#1358, thanks [@KoraggKnightWolf](https://github.com/KoraggKnightWolf)!)
* Halfops can set the channel topic (#1306)
* Full client certificates are now passed to auth scripts. This allows for more flexible checks on certificates, including verification against an internal CA (#414)
### Internal
* Added a logline for debugging client disconnections (#1293)
* Renamed `conventional.yaml` to `traditional.yaml` (#1350)
* Integration tests are now run during CI (#1279)
## [2.3.0] - 2020-09-06
We're pleased to announce Oragono 2.3.0, a new stable release.
This release contains primarily bug fixes, but includes one notable feature enhancement: a change contributed by [@hhirtz](https://github.com/hhirtz) that updates the `draft/rename` specification to correspond to the new (soon-to-be) published draft. This release contains primarily bug fixes, but includes one notable feature enhancement: a change contributed by [@hhirtz](https://github.com/hhirtz) that updates the `draft/rename` specification to correspond to the new (soon-to-be) published draft.
Many thanks to [@hhirtz](https://github.com/hhirtz) for contributing patches, to [@bogdomania](https://github.com/bogdomania), [@digitalcircuit](https://github.com/digitalcircuit), [@ivan-avalos](https://github.com/ivan-avalos), [@jesopo](https://github.com/jesopo), [@kylef](https://github.com/kylef), [@Mitaka8](https://github.com/Mitaka8), [@mogad0n](https://github.com/mogad0n), and [@ProgVal](https://github.com/ProgVal) for reporting issues and helping test, and to our translators for contributing translations. Many thanks to [@hhirtz](https://github.com/hhirtz) for contributing patches, to [@bogdomania](https://github.com/bogdomania), [@jesopo](https://github.com/jesopo), [@kylef](https://github.com/kylef), [@Mitaka8](https://github.com/Mitaka8),and [@mogad0n](https://github.com/mogad0n) for reporting issues and helping test, and to our translators for contributing translations.
This release includes no changes to the config file format or database changes. This release includes no changes to the config file format or database changes.
@ -728,8 +24,6 @@ This release includes no changes to the config file format or database changes.
* Fixed an edge case in handling no-op nick changes (#1242) * Fixed an edge case in handling no-op nick changes (#1242)
* Fixed edge cases with users transitioning in and out of always-on status (#1218, #1219, thanks [@bogdomania](https://github.com/bogdomania)!) * Fixed edge cases with users transitioning in and out of always-on status (#1218, #1219, thanks [@bogdomania](https://github.com/bogdomania)!)
* Fixed a race condition related to the registration timeout (#1225, thanks [@hhirtz](https://github.com/hhirtz)!) * Fixed a race condition related to the registration timeout (#1225, thanks [@hhirtz](https://github.com/hhirtz)!)
* Fixed incorrectly formatted account tags on some messages (#1254, thanks [@digitalcircuit](https://github.com/digitalcircuit)!)
* Improved checks for invalid config files (#1244, thanks [@ivan-avalos](https://github.com/ivan-avalos)!)
* Fixed messages to services and `*playback` not receiving echo-message when applicable (#1204, thanks [@kylef](https://github.com/kylef)!) * Fixed messages to services and `*playback` not receiving echo-message when applicable (#1204, thanks [@kylef](https://github.com/kylef)!)
* Fixed a help string (#1237, thanks [@Mitaka8](https://github.com/Mitaka8)!) * Fixed a help string (#1237, thanks [@Mitaka8](https://github.com/Mitaka8)!)
@ -1333,7 +627,7 @@ Thanks to [slingamn](https://github.com/slingamn) for a lot of heavy lifting thi
## [0.11.0] - 2018-04-15 ## [0.11.0] - 2018-04-15
And v0.11.0 finally comes along! This release has been in the works for almost four months now, with an alpha and beta helping square away the issues. And v0.11.0 finally comes along! This release has been in the works for almost four months now, with an alpha and beta helping square away the issues.
We're adding a lot of features to improve debugging, better support international users, and make things better for network administrators. Among the new features, you can use the `LANGUAGE` command to set a custom server language (see our [CrowdIn](https://crowdin.com/project/ergochat) to contribute), expose a debugging `pprof` endpoint, reserve nicknames with `NickServ`, and force email verification for new user accounts. On the improvements side we have a `CAP REQ` fix, and we now have a manual that contains a nice overview of Oragono's documentation. We're adding a lot of features to improve debugging, better support international users, and make things better for network administrators. Among the new features, you can use the `LANGUAGE` command to set a custom server language (see our [CrowdIn](https://crowdin.com/project/oragono) to contribute), expose a debugging `pprof` endpoint, reserve nicknames with `NickServ`, and force email verification for new user accounts. On the improvements side we have a `CAP REQ` fix, and we now have a manual that contains a nice overview of Oragono's documentation.
If you have any trouble with this release, please let us know with an issue on our tracker, or by talking to us in `#oragono` on Freenode. If you have any trouble with this release, please let us know with an issue on our tracker, or by talking to us in `#oragono` on Freenode.

View File

@ -1,76 +1,49 @@
# Developing Ergo # Developing Oragono
This is a guide to modifying Ergo's code. If you're just trying to run your own Ergo, or use one, you shouldn't need to worry about these issues. This is just a bunch of tips and tricks we keep in mind while developing Oragono. If you wanna help develop as well, they might also be worth keeping in mind!
## Golang issues ## Golang issues
You should use the [latest distribution of the Go language for your OS and architecture](https://golang.org/dl/). (If `uname -m` on your Raspberry Pi reports `armv7l`, use the `armv6l` distribution of Go; if it reports v8, you may be able to use the `arm64` distribution.) You should use the [latest distribution of the Go language for your OS and architecture](https://golang.org/dl/). (If `uname -m` on your Raspberry Pi reports `armv7l`, use the `armv6l` distribution of Go; if it reports v8, you may be able to use the `arm64` distribution.)
Ergo vendors all its dependencies. Because of this, Ergo is self-contained and you should not need to fetch any dependencies with `go get`. Doing so is not recommended, since it may fetch incompatible versions of the dependencies. Oragono vendors all its dependencies. Because of this, Oragono is self-contained and you should not need to fetch any dependencies with `go get`. Doing so is not recommended, since it may fetch incompatible versions of the dependencies.
If you're upgrading the Go version used by Ergo, there are several places where it's hard-coded and must be changed: If you're upgrading the Go version used by Oragono, there are several places where it's hard-coded and must be changed:
1. `.github/workflows/build.yml`, which controls the version that our CI test suite uses to build and test the code (e.g., for a PR) 1. `.travis.yml`, which controls the version that our CI test suite uses to build and test the code (e.g., for a PR)
2. `Dockerfile`, which controls the version that the Ergo binaries in our Docker images are built with 2. `Dockerfile`, which controls the version that the Oragono binaries in our Docker images are built with
3. `go.mod`: this should be updated automatically by Go when you do module-related operations 3. `go.mod`: this should be updated automatically by Go when you do module-related operations
## Branches ## Branches
The recommended workflow for development is to create a new branch starting from the current `master`. Even though `master` is not recommended for production use, we strive to keep it in a usable state. Starting from `master` increases the likelihood that your patches will be accepted. The `master` branch should be kept relatively runnable. It might be a bit broken or contain some bad commits now and then, but the pre-release checks should weed those out before users see them.
Long-running feature branches that aren't ready for merge into `master` may be maintained under a `devel+` prefix, e.g. `devel+metadata` for a feature branch implementing the IRCv3 METADATA extension. For either particularly broken or particularly WiP changes, we work on them in a `develop` branch. The normal branch naming is `develop+feature[.version]`. For example, when first developing 'cloaking', you may use the branch `develop+cloaks`. If you need to create a new branch to work on it (a second version of the implementation, for example), you could use `develop+cloaks.2`, and so on.
Develop branches are either used to work out implementation details in preperation for a cleaned-up version, for half-written ideas we want to continue persuing, or for stuff that we just don't want on `master` yet for whatever reason.
## Workflow
We have two test suites:
1. `make test`, which runs some relatively shallow unit tests, checks `go vet`, and does some other internal consistency checks
1. `make irctest`, which runs the [irctest](https://github.com/ProgVal/irctest) integration test suite
Barring special circumstances, both must pass for a PR to be accepted. irctest will test the `ergo` binary visible on `$PATH`; make sure your development version is the one being tested. (If you have `~/go/bin` on your `$PATH`, a successful `make install` will accomplish this.)
The project style is [gofmt](https://go.dev/blog/gofmt); it is enforced by `make test`. You can fix any style issues automatically by running `make gofmt`.
## Updating dependencies
Ergo vendors all dependencies using `go mod vendor`. To update a dependency, or add a new one:
1. `go get -v bazbat.com/path/to/dependency` ; this downloads the new dependency
2. `go mod vendor` ; this writes the dependency's source files to the `vendor/` directory
3. `git add go.mod go.sum vendor/` ; this stages all relevant changes to the vendor directory, including file deletions. Take care that spurious changes (such as editor swapfiles) aren't added.
4. `git commit`
## Releasing a new version ## Releasing a new version
1. Ensure the tests pass, locally on travis (`make test`, `make smoke`, and `make irctest`)
1. Test backwards compatibility guarantees. Get an example config file and an example database from the previous stable release. Make sure the current build still works with them (modulo anything explicitly called out in the changelog as a breaking change). 1. Test backwards compatibility guarantees. Get an example config file and an example database from the previous stable release. Make sure the current build still works with them (modulo anything explicitly called out in the changelog as a breaking change).
1. Run `irctest` over it to make sure nothing's severely broken. Talk to the maintainers to find out which version of irctest to run.
1. Run the `ircstress` chanflood benchmark to look for data races (enable race detection) and performance regressions (disable it). 1. Run the `ircstress` chanflood benchmark to look for data races (enable race detection) and performance regressions (disable it).
1. Update the changelog with new changes and write release notes. 1. Update the changelog with new changes and write release notes.
1. Update the version number `irc/version.go` (either change `-unreleased` to `-rc1`, or remove `-rc1`, as appropriate). 1. Update the version number `irc/version.go` (either change `-unreleased` to `-rc1`, or remove `-rc1`, as appropriate).
1. Commit the new changelog and constants change. 1. Commit the new changelog and constants change.
1. Tag the release with `git tag --sign v0.0.0 -m "Release v0.0.0"` (`0.0.0` replaced with the real ver number). 1. Tag the release with `git tag --sign v0.0.0 -m "Release v0.0.0"` (`0.0.0` replaced with the real ver number).
1. Build binaries using `make release` 1. Build binaries using `make release`, upload release to Github including the changelog and binaries.
1. Sign the checksums file with `gpg --sign --detach-sig --local-user <fingerprint>` 1. If it's a proper release (i.e. not an alpha/beta), merge the updates into the `stable` branch.
1. Smoke-test a built binary locally
1. Point of no return: `git push origin master --tags` (this publishes the tag; any fixes after this will require a new point release)
1. Publish the release on GitHub (Releases -> "Draft a new release"); use the new tag, post the changelog entries, upload the binaries, the checksums file, and the signature of the checksums file
1. Update the `irctest_stable` branch with the new changes (this may be a force push).
1. If it's a production release (as opposed to a release candidate), update the `stable` branch with the new changes. (This may be a force push in the event that stable contained a backport. This is fine because all stable releases and release candidates are tagged.)
1. Similarly, for a production release, update the `irctest_stable` branch (this is the branch used by upstream irctest to integration-test against Ergo).
1. Make the appropriate announcements: 1. Make the appropriate announcements:
* For a release candidate: * For a release candidate:
1. the channel topic 1. the channel topic
1. any operators who may be interested 1. any operators who may be interested
1. update the testnet
* For a production release: * For a production release:
1. everything applicable to a release candidate 1. everything applicable to a release candidate
1. Twitter 1. Twitter
1. ergo.chat/news 1. oragono.io/news
1. ircv3.net support tables, if applicable 1. ircv3.net support tables, if applicable
1. other social media? 1. other social media?
@ -84,7 +57,7 @@ Once it's built and released, you need to setup the new development version. To
```md ```md
## Unreleased ## Unreleased
New release of Ergo! New release of Oragono!
### Config Changes ### Config Changes
@ -101,6 +74,17 @@ New release of Ergo!
## Fuzzing and Testing
Fuzzing can be useful. We don't have testing done inside the IRCd itself, but this fuzzer I've written works alright and has helped shake out various bugs: [irc_fuzz.py](https://gist.github.com/DanielOaks/63ae611039cdf591dfa4).
In addition, I've got the beginnings of a stress-tester here which is useful:
https://github.com/DanielOaks/irc-stress-test
As well, there's a decent set of 'tests' here, which I like to run Oragono through now and then:
https://github.com/DanielOaks/irctest
## Debugging ## Debugging
It's helpful to enable all loglines while developing. Here's how to configure this: It's helpful to enable all loglines while developing. Here's how to configure this:
@ -117,12 +101,12 @@ To debug a hang, the best thing to do is to get a stack trace. The easiest way t
$ kill -ABRT <procid> $ kill -ABRT <procid>
This will kill Ergo and print out a stack trace for you to take a look at. This will kill Oragono and print out a stack trace for you to take a look at.
## Concurrency design ## Concurrency design
Ergo involves a fair amount of shared state. Here are some of the main points: Oragono involves a fair amount of shared state. Here are some of the main points:
1. Each client has a separate goroutine that listens for incoming messages and synchronously processes them. 1. Each client has a separate goroutine that listens for incoming messages and synchronously processes them.
1. All sends to clients are asynchronous; `client.Send` appends the message to a queue, which is then processed on a separate goroutine. It is always safe to call `client.Send`. 1. All sends to clients are asynchronous; `client.Send` appends the message to a queue, which is then processed on a separate goroutine. It is always safe to call `client.Send`.
@ -170,7 +154,7 @@ In addition, throughout most of the codebase, if a string is created using the b
## Updating Translations ## Updating Translations
We support translating server strings using [CrowdIn](https://crowdin.com/project/ergochat)! To send updated source strings to CrowdIn, you should: We support translating server strings using [CrowdIn](https://crowdin.com/project/oragono)! To send updated source strings to CrowdIn, you should:
1. `cd` to the base directory (the one this `DEVELOPING` file is in). 1. `cd` to the base directory (the one this `DEVELOPING` file is in).
2. Install the `pyyaml` and `docopt` deps using `pip3 install pyyamp docopt`. 2. Install the `pyyaml` and `docopt` deps using `pip3 install pyyamp docopt`.
@ -200,14 +184,3 @@ We also support grabbing translations directly from CrowdIn. To do this:
4. Run `crowdin download` 4. Run `crowdin download`
This will download a bunch of updated files and put them in the right place This will download a bunch of updated files and put them in the right place
## Adding a mode
When adding a mode, keep in mind the following places it may need to be referenced:
1. The mode needs to be defined in the `irc/modes` subpackage
1. It may need to be special-cased in `modes.RplMyInfo()`
1. It may need to be added to the `CHANMODES` ISUPPORT token
1. It may need special handling in `ApplyUserModeChanges` or `ApplyChannelModeChanges`
1. It may need special persistence handling code

View File

@ -1,43 +1,56 @@
## build ergo binary ## build Oragono
FROM docker.io/golang:1.24-alpine AS build-env FROM golang:1.15-alpine AS build-env
RUN apk upgrade -U --force-refresh --no-cache && apk add --no-cache --purge --clean-protected -l -u make git RUN apk add --no-cache git make curl sed
# copy ergo source # copy oragono
WORKDIR /go/src/github.com/ergochat/ergo RUN mkdir -p /go/src/github.com/oragono/oragono
COPY . . WORKDIR /go/src/github.com/oragono/oragono
ADD . /go/src/github.com/oragono/oragono/
# modify default config file so that it doesn't die on IPv6 # modify default config file so that it doesn't die on IPv6
# and so it can be exposed via 6667 by default # and so it can be exposed via 6667 by default
RUN sed -i 's/^\(\s*\)\"127.0.0.1:6667\":.*$/\1":6667":/' /go/src/github.com/ergochat/ergo/default.yaml && \ run sed -i 's/^\(\s*\)\"127.0.0.1:6667\":.*$/\1":6667":/' /go/src/github.com/oragono/oragono/default.yaml
sed -i 's/^\s*\"\[::1\]:6667\":.*$//' /go/src/github.com/ergochat/ergo/default.yaml run sed -i 's/^\s*\"\[::1\]:6667\":.*$//' /go/src/github.com/oragono/oragono/default.yaml
# make sure submodules are up-to-date
RUN git submodule update --init
# compile # compile
RUN make install RUN make
## build ergo container
FROM docker.io/alpine:3.19
## run Oragono
FROM alpine:3.9
# metadata # metadata
LABEL maintainer="Daniel Oaks <daniel@danieloaks.net>,Daniel Thamdrup <dallemon@protonmail.com>" \ LABEL maintainer="daniel@danieloaks.net"
description="Ergo is a modern, experimental IRC server written in Go" LABEL description="Oragono is a modern, experimental IRC server written in Go"
# install latest updates and configure alpine
RUN apk update
RUN apk upgrade
RUN mkdir /lib/modules
# standard ports listened on # standard ports listened on
EXPOSE 6667/tcp 6697/tcp EXPOSE 6667/tcp 6697/tcp
# ergo itself # oragono itself
COPY --from=build-env /go/bin/ergo \ RUN mkdir -p /ircd-bin
/go/src/github.com/ergochat/ergo/default.yaml \ COPY --from=build-env /go/bin/oragono /ircd-bin
/go/src/github.com/ergochat/ergo/distrib/docker/run.sh \ COPY --from=build-env /go/src/github.com/oragono/oragono/languages /ircd-bin/languages/
/ircd-bin/ COPY --from=build-env /go/src/github.com/oragono/oragono/default.yaml /ircd-bin/default.yaml
COPY --from=build-env /go/src/github.com/ergochat/ergo/languages /ircd-bin/languages/
COPY distrib/docker/run.sh /ircd-bin/run.sh
RUN chmod +x /ircd-bin/run.sh
# running volume holding config file, db, certs # running volume holding config file, db, certs
VOLUME /ircd VOLUME /ircd
WORKDIR /ircd 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/oragono/oragono/oragono.motd /ircd/oragono.motd
# launch # launch
ENTRYPOINT ["/ircd-bin/run.sh"] ENTRYPOINT ["/ircd-bin/run.sh"]

View File

@ -1,48 +1,41 @@
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null) .PHONY: all install build release capdefs test smoke gofmt
GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1)
# disable linking against native libc / libpthread by default; GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null)
# this can be overridden by passing CGO_ENABLED=1 to make
export CGO_ENABLED ?= 0
capdef_file = ./irc/caps/defs.go capdef_file = ./irc/caps/defs.go
.PHONY: all all: install
all: build
.PHONY: install
install: install:
go install -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)" go install -v -ldflags "-X main.commit=$(GIT_COMMIT)"
.PHONY: build
build: build:
go build -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)" go build -v -ldflags "-X main.commit=$(GIT_COMMIT)"
.PHONY: release
release: release:
goreleaser --skip=publish --clean goreleaser --skip-publish --rm-dist
.PHONY: capdefs
capdefs: capdefs:
python3 ./gencapdefs.py > ${capdef_file} python3 ./gencapdefs.py > ${capdef_file}
.PHONY: test
test: test:
python3 ./gencapdefs.py | diff - ${capdef_file} python3 ./gencapdefs.py | diff - ${capdef_file}
go test ./... cd irc && go test . && go vet .
go vet ./... cd irc/caps && go test . && go vet .
cd irc/cloaks && go test . && go vet .
cd irc/connection_limits && go test . && go vet .
cd irc/email && go test . && go vet .
cd irc/history && go test . && go vet .
cd irc/isupport && go test . && go vet .
cd irc/modes && go test . && go vet .
cd irc/mysql && go test . && go vet .
cd irc/passwd && go test . && go vet .
cd irc/utils && go test . && go vet .
./.check-gofmt.sh ./.check-gofmt.sh
.PHONY: smoke smoke:
smoke: install oragono mkcerts --conf ./default.yaml || true
ergo mkcerts --conf ./default.yaml || true oragono run --conf ./default.yaml --smoke
ergo run --conf ./default.yaml --smoke
.PHONY: gofmt
gofmt: gofmt:
./.check-gofmt.sh --fix ./.check-gofmt.sh --fix
.PHONY: irctest
irctest: install
git submodule update --init
cd irctest && make ergo

55
README
View File

@ -1,24 +1,20 @@
___ _ __ __ _ ___
/ _ \ '__/ _` |/ _ \ ▄▄▄ ▄▄▄· ▄▄ • ▐ ▄
| __/ | | (_| | (_) | ▪ ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪ •█▌▐█▪
\___|_| \__, |\___/ ▄█▀▄ ▐▀▀▄ ▄█▀▀█ ▄█ ▀█▄ ▄█▀▄▪▐█▐▐▌ ▄█▀▄
__/ | ▐█▌.▐▌▐█•█▌▐█ ▪▐▌▐█▄▪▐█▐█▌ ▐▌██▐█▌▐█▌.▐▌
|___/ ▀█▄▀▪.▀ ▀ ▀ ▀ ·▀▀▀▀ ▀█▄▀ ▀▀ █▪ ▀█▄▀▪
----------------------------------------------------------------------------------------------- -----------------------------------------------------------------------------------------------
Ergo is a modern IRC server written in Go. Its core design principles are: Oragono is a modern IRC server written in Go. It's designed to be simple to setup
and use, and to provide the majority of features that IRC users expect today.
* Being simple to set up and use It includes features such as UTF-8 nicks and channel names, client accounts and SASL, and other
* Combining the features of an ircd, a services framework, and a bouncer: assorted IRCv3 support.
* Integrated account management
* History storage
* Bouncer functionality
* Bleeding-edge IRCv3 support
* High customizability via a rehashable (i.e., reloadable at runtime) YAML config
https://ergo.chat/ https://oragono.io/
https://github.com/ergochat/ergo https://github.com/oragono/oragono
#ergo on irc.ergo.chat or irc.libera.chat
----------------------------------------------------------------------------------------------- -----------------------------------------------------------------------------------------------
@ -29,30 +25,33 @@ Copy the example config file to ircd.yaml with a command like:
$ cp default.yaml ircd.yaml $ cp default.yaml ircd.yaml
Modify the config file as needed (the recommendations at the top may be helpful). Modify the config file as you like.
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 $ oragono genpasswd
If you need to generate self-signed TLS certificates, use this command: Run these commands in order -- these will setup each section of the server:
$ ./ergo mkcerts $ oragono mkcerts
$ oragono run
You are now ready to start Ergo! And you should now be running Oragono!
$ ./ergo run
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
Web: https://ergo.chat/manual.html
=== Updating === === Updating ===
If you're updating from a previous version of Ergo, check out the CHANGELOG for a list If you're updating from a previous version of Oragono, checkout the CHANGELOG for a shortlist
of important changes you'll want to take a look at. The change log details config changes, of important changes you'll want to take a look at. The change log details config changes,
fixes, new features and anything else you'll want to be aware of! fixes, new features and anything else you'll want to be aware of!
If there's been a database update, you can run this command to upgrade it manually:
$ oragono upgradedb
Otherwise, just starting the server will run an automagic backup and upgrade.
=== Credits === === Credits ===
* Jeremy Latt (2012-2014) * Jeremy Latt (2012-2014)

View File

@ -1,22 +1,23 @@
![Ergo logo](docs/logo.png) ![Oragono logo](docs/logo.png)
Ergo (formerly known as Oragono) is a modern IRC server written in Go. Its core design principles are: Oragono is a modern IRC server written in Go. Its core design principles are:
* Being simple to set up and use * Being simple to set up and use
* Combining the features of an ircd, a services framework, and a bouncer (integrated account management, history storage, and bouncer functionality) * Combining the features of an ircd, a services framework, and a bouncer (integrated account management, history storage, and bouncer functionality)
* Bleeding-edge [IRCv3 support](https://ircv3.net/software/servers.html), suitable for use as an IRCv3 reference implementation * Bleeding-edge [IRCv3 support](https://ircv3.net/software/servers.html), suitable for use as an IRCv3 reference implementation
* High customizability via a rehashable (i.e., reloadable at runtime) YAML config * Highly customizable via a rehashable (i.e., reloadable at runtime) YAML config
Ergo is a fork of the [Ergonomadic](https://github.com/jlatt/ergonomadic) IRC daemon <3 Oragono is a fork of the [Ergonomadic](https://github.com/jlatt/ergonomadic) IRC daemon <3
--- ---
[![Go Report Card](https://goreportcard.com/badge/github.com/ergochat/ergo)](https://goreportcard.com/report/github.com/ergochat/ergo) [![Go Report Card](https://goreportcard.com/badge/github.com/oragono/oragono)](https://goreportcard.com/report/github.com/oragono/oragono)
[![build](https://github.com/ergochat/ergo/actions/workflows/build.yml/badge.svg)](https://github.com/ergochat/ergo/actions/workflows/build.yml) [![Build Status](https://travis-ci.com/oragono/oragono.svg?branch=master)](https://travis-ci.com/oragono/oragono)
[![Download Latest Release](https://img.shields.io/badge/downloads-latest%20release-green.svg)](https://github.com/ergochat/ergo/releases/latest) [![Download Latest Release](https://img.shields.io/badge/downloads-latest%20release-green.svg)](https://github.com/oragono/oragono/releases/latest)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/ergochat/localized.svg)](https://crowdin.com/project/ergochat) [![Freenode #oragono](https://img.shields.io/badge/Freenode-%23oragono-1e72ff.svg?style=flat)](https://www.irccloud.com/invite?channel=%23oragono&hostname=irc.freenode.net&port=6697&ssl=1)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/oragono/localized.svg)](https://crowdin.com/project/oragono)
If you want to take a look at a running Ergo instance or test some client code, feel free to play with [testnet.ergo.chat](https://testnet.ergo.chat/) (TLS on port 6697 or plaintext on port 6667). If you want to take a look at a running Oragono instance or test some client code, feel free to play with [testnet.oragono.io](https://testnet.oragono.io/) (TLS on port 6697 or plaintext on port 6667).
--- ---
@ -25,72 +26,68 @@ If you want to take a look at a running Ergo instance or test some client code,
* integrated services: NickServ for user accounts, ChanServ for channel registration, and HostServ for vanity hosts * integrated services: NickServ for user accounts, ChanServ for channel registration, and HostServ for vanity hosts
* bouncer-like features: storing and replaying history, allowing multiple clients to use the same nickname * bouncer-like features: storing and replaying history, allowing multiple clients to use the same nickname
* UTF-8 nick and channel names with rfc7613 (PRECIS)
* native TLS/SSL support, including support for client certificates * native TLS/SSL support, including support for client certificates
* [IRCv3 support](https://ircv3.net/software/servers.html)
* [yaml](https://yaml.org/) configuration * [yaml](https://yaml.org/) configuration
* updating server config and TLS certificates on-the-fly (rehashing) * updating server config and TLS certificates on-the-fly (rehashing)
* SASL authentication * SASL authentication
* [LDAP support](https://github.com/ergochat/ergo-ldap) * LDAP support
* supports [multiple languages](https://crowdin.com/project/ergochat) (you can also set a default language for your network) * supports [multiple languages](https://crowdin.com/project/oragono) (you can also set a default language for your network)
* optional support for UTF-8 nick and channel names with RFC 8265 (PRECIS)
* advanced security and privacy features (support for requiring SASL for all logins, cloaking IPs, and running as a Tor hidden service) * advanced security and privacy features (support for requiring SASL for all logins, cloaking IPs, and running as a Tor hidden service)
* an extensible privilege system for IRC operators * an extensible privilege system for IRC operators
* ident lookups for usernames * ident lookups for usernames
* automated client connection limits * automated client connection limits
* passwords stored with [bcrypt](https://godoc.org/golang.org/x/crypto) * passwords stored with [bcrypt](https://godoc.org/golang.org/x/crypto)
* `UBAN`, a unified ban system that can target IPs, networks, masks, and registered accounts (`KLINE` and `DLINE` are also supported) * banning ips/nets and masks with `KLINE` and `DLINE`
* a focus on developing with [specifications](https://ergo.chat/specs.html) * [IRCv3 support](https://ircv3.net/software/servers.html)
* a heavy focus on developing with [specifications](https://oragono.io/specs.html)
For more detailed information on Ergo's functionality, see:
* [MANUAL.md, the operator manual](https://github.com/ergochat/ergo/blob/stable/docs/MANUAL.md)
* [USERGUIDE.md, the guide for end users](https://github.com/ergochat/ergo/blob/stable/docs/USERGUIDE.md)
## Quick start guide ## Quick start guide
Download the latest release from this page: https://github.com/ergochat/ergo/releases/latest Download the latest release from this page: https://github.com/oragono/oragono/releases/latest
Extract it into a folder, then run the following commands: 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 oragono mkcerts
./ergo run # server should be ready to go! oragono 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/oragono/oragono/blob/master/docs/MANUAL.md#productionizing) for recommendations on how to run a production network, including obtaining valid TLS certificates.
### Platform Packages ### Platform Packages
Some platforms/distros also have Ergo packages maintained for them: Some platforms/distros also have Oragono packages maintained for them:
* Arch Linux [AUR](https://aur.archlinux.org/packages/ergochat/) - Maintained by [Jason Papakostas (@vith)](https://github.com/vith). * Arch Linux [AUR](https://aur.archlinux.org/packages/oragono/) - Maintained by [Sean Enck (@enckse)](https://github.com/enckse).
* [Gentoo Linux](https://packages.gentoo.org/packages/net-irc/ergo) - Maintained by [Sam James (@thesamesam)](https://github.com/thesamesam).
### Using Docker ### Using Docker
A Dockerfile and example docker-compose recipe are available in the `distrib/docker` directory. Ergo is automatically published A Dockerfile and example docker-compose recipe are available in the `distrib/docker` directory. Oragono is automatically published
to the GitHub Container Registry at [ghcr.io/ergochat/ergo](https://ghcr.io/ergochat/ergo). For more information, see the distrib/docker to Docker Hub at [oragono/oragono](https://hub.docker.com/r/oragono/oragono). For more information, see the distrib/docker
[README file](https://github.com/ergochat/ergo/blob/master/distrib/docker/README.md). [README file](https://github.com/oragono/oragono/blob/master/distrib/docker/README.md).
### From Source ### From Source
You can also clone this repository and build from source. Typical deployments should use the `stable` branch, which points to the latest stable release. In general, `stable` should coincide with the latest published tag that is not designated as a beta or release candidate (for example, `v2.7.0-rc1` was an unstable release candidate and `v2.7.0` was the corresponding stable release), so you can also identify the latest stable release tag on the [releases page](https://github.com/ergochat/ergo/releases) and build that. You can also install this repo and use that instead! However, keep some things in mind if you go that way:
The `master` branch is not recommended for production use since it may contain bugs, and because the forwards compatibility guarantees for the config file and the database that apply to releases do not apply to master. That is to say, running master may result in changes to your database that end up being incompatible with future versions of Ergo. `devel` branches are intentionally unstable, containing fixes that may not work, and they may be rebased or reworked extensively.
For information on contributing to Ergo, see [DEVELOPING.md](https://github.com/ergochat/ergo/blob/master/DEVELOPING.md). The `master` branch _should_ usually be stable, but may contain database changes that either have not been finalised or not had database upgrade code written yet. Don't run `master` on a live production network.
The `stable` branch contains the latest release. You can run this for a production version without any trouble.
#### 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 you have that, just clone the repository and run `make build`. If everything goes well, you should now have an executable named `oragono` in the base directory of the project.
## Configuration ## Configuration
The default config file [`default.yaml`](default.yaml) helps walk you through what each option means and changes. The default config file [`default.yaml`](default.yaml) helps walk you through what each option means and changes.
You can use the `--conf` parameter when launching Ergo to control where it looks for the config file. For instance: `ergo run --conf /path/to/ircd.yaml`. The configuration file also stores where the log, database, certificate, and other files are opened. Normally, all these files use relative paths, but you can change them to be absolute (such as `/var/log/ircd.log`) when running Ergo as a service. You can use the `--conf` parameter when launching Oragono to control where it looks for the config file. For instance: `oragono run --conf /path/to/ircd.yaml`. The configuration file also stores where the log, database, certificate, and other files are opened. Normally, all these files use relative paths, but you can change them to be absolute (such as `/var/log/ircd.log`) when running Oragono as a service.
### Logs ### Logs
@ -101,22 +98,21 @@ By default, logs go to stderr only. They can be configured to go to a file, or y
Passwords (for both `PASS` and oper logins) are stored using bcrypt. To generate encrypted strings for use in the config, use the `genpasswd` subcommand as such: Passwords (for both `PASS` and oper logins) are stored using bcrypt. To generate encrypted strings for use in the config, use the `genpasswd` subcommand as such:
```sh ```sh
ergo genpasswd oragono genpasswd
``` ```
With this, you receive a blob of text which you can plug into your configuration file. With this, you receive a blob of text which you can plug into your configuration file.
### Nickname and channel registration ### How to register a channel
Ergo relies heavily on user accounts to enable its distinctive features (such as allowing multiple clients per nickname). As a user, you can register your current nickname as an account using `/msg NickServ register <password>`. Once you have done so, you should [enable SASL in your clients](https://libera.chat/guides/sasl), ensuring that you will be automatically logged into your account on each connection. This will prevent [problems claiming your registered nickname](https://github.com/ergochat/ergo/blob/master/docs/MANUAL.md#nick-equals-account). 1. Register your account with `/NS REGISTER <password>`
2. Join the channel with `/join #channel`
Once you have registered your nickname, you can use it to register channels: 3. Register the channel with `/CS REGISTER #channel`
1. Join the channel with `/join #channel`
2. Register the channel with `/CS REGISTER #channel`
After this, your channel will remember the fact that you're the owner, the topic, and any modes set on it! After this, your channel will remember the fact that you're the owner, the topic, and any modes set on it!
Make sure to setup [SASL](https://freenode.net/kb/answer/sasl) in your client to automatically login to your account when you next join the server.
# Credits # Credits
@ -124,4 +120,4 @@ After this, your channel will remember the fact that you're the owner, the topic
* Edmund Huber (2014-2015) * Edmund Huber (2014-2015)
* Daniel Oaks (2016-present) * Daniel Oaks (2016-present)
* Shivaram Lingamneni (2017-present) * Shivaram Lingamneni (2017-present)
* [Many other contributors and friends of the project <3](https://github.com/ergochat/ergo/blob/master/CHANGELOG.md) * [Many other contributors and friends of the project <3](https://github.com/oragono/oragono/blob/master/CHANGELOG.md)

View File

@ -1,17 +1,17 @@
# This is the "traditional" or "mainstream" config file for Ergo. # This is the "conventional" or "mainstream" config file for Oragono.
# It tries to replicate the behavior of other ircds, at the cost of not # It tries to replicate the behavior of other ircds, at the cost of not
# taking full advantage of Ergo's features. This config is suitable for use # taking full advantage of Oragono's features. This config is suitable for use
# in IRCv3 conformance testing. # in IRCv3 conformance testing.
# network configuration # network configuration
network: network:
# name of the network # name of the network
name: ErgoTest name: OragonoTest
# server configuration # server configuration
server: server:
# server name # server name
name: ergo.test name: oragono.test
# addresses to listen on # addresses to listen on
listeners: listeners:
@ -23,25 +23,21 @@ server:
# The standard SSL/TLS port for IRC is 6697. This will listen on all interfaces: # The standard SSL/TLS port for IRC is 6697. This will listen on all interfaces:
":6697": ":6697":
# this is a standard TLS configuration with a single certificate;
# see the manual for instructions on how to configure SNI
tls: tls:
cert: fullchain.pem cert: fullchain.pem
key: privkey.pem key: privkey.pem
# 'proxy' should typically be false. It's for cloud load balancers that # 'proxy' should typically be false. It's only for Kubernetes-style load
# always send a PROXY protocol header ahead of the connection. See the # balancing that does not terminate TLS, but sends an initial PROXY line
# manual ("Reverse proxies") for more details. # in plaintext.
proxy: false proxy: false
# optionally set the minimum TLS version (defaults to 1.0):
# min-tls-version: 1.2
# Example of a Unix domain socket for proxying: # Example of a Unix domain socket for proxying:
# "/tmp/ergo_sock": # "/tmp/oragono_sock":
# Example of a Tor listener: any connection that comes in on this listener will # Example of a Tor listener: any connection that comes in on this listener will
# be considered a Tor connection. It is strongly recommended that this listener # be considered a Tor connection. It is strongly recommended that this listener
# *not* be on a public interface --- it should be on 127.0.0.0/8 or unix domain: # *not* be on a public interface --- it should be on 127.0.0.0/8 or unix domain:
# "/hidden_service_sockets/ergo_tor_sock": # "/hidden_service_sockets/oragono_tor_sock":
# tor: true # tor: true
# Example of a WebSocket listener: # Example of a WebSocket listener:
@ -74,7 +70,6 @@ server:
max-connections-per-duration: 64 max-connections-per-duration: 64
# strict transport security, to get clients to automagically use TLS # strict transport security, to get clients to automagically use TLS
# (irrelevant in the recommended configuration, with no public plaintext listener)
sts: sts:
# whether to advertise STS # whether to advertise STS
# #
@ -95,37 +90,34 @@ server:
websockets: websockets:
# Restrict the origin of WebSocket connections by matching the "Origin" HTTP # Restrict the origin of WebSocket connections by matching the "Origin" HTTP
# header. This setting causes ergo to reject websocket connections unless # header. This settings makes oragono reject every WebSocket connection,
# they originate from a page on one of the whitelisted websites in this list. # except when it originates from one of the hosts in this list. Use this to
# This prevents malicious websites from making their visitors connect to your # prevent malicious websites from making their visitors connect to oragono
# ergo instance without their knowledge. An empty list means there are no # without their knowledge. An empty list means that there are no restrictions.
# restrictions.
allowed-origins: allowed-origins:
# - "https://ergo.chat" # - "https://oragono.io"
# - "https://*.ergo.chat" # - "https://*.oragono.io"
# casemapping controls what kinds of strings are permitted as identifiers (nicknames, # casemapping controls what kinds of strings are permitted as identifiers (nicknames,
# channel names, account names, etc.), and how they are normalized for case. # channel names, account names, etc.), and how they are normalized for case.
# the recommended default is 'ascii' (traditional ASCII-only identifiers). # with the recommended default of 'precis', UTF8 identifiers that are "sane"
# the other options are 'precis', which allows UTF8 identifiers that are "sane" # (according to RFC 8265) are allowed, and the server additionally tries to protect
# (according to UFC 8265), with additional mitigations for homoglyph attacks, # against confusable characters ("homoglyph attacks").
# 'permissive', which allows identifiers containing unusual characters like # the other options are 'ascii' (traditional ASCII-only identifiers), and 'permissive',
# emoji, at the cost of increased vulnerability to homoglyph attacks and potential # which allows identifiers to contain unusual characters like emoji, but makes users
# client compatibility problems, and the legacy mappings 'rfc1459' and # vulnerable to homoglyph attacks. unless you're really confident in your decision,
# 'rfc1459-strict'. we recommend leaving this value at its default; # we recommend leaving this value at its default (changing it once the network is
# however, note that changing it once the network is already up and running is # already up and running is problematic).
# problematic. casemapping: "precis"
casemapping: "ascii"
# enforce-utf8 controls whether the server will preemptively discard non-UTF8 # enforce-utf8 controls whether the server allows non-UTF8 bytes in messages
# messages (since they cannot be relayed to websocket clients), or will allow # (as in traditional IRC) or preemptively discards non-UTF8 messages (since
# them and relay them to non-websocket clients (as in traditional IRC). # they cannot be relayed to websocket clients).
enforce-utf8: true enforce-utf8: true
# whether to look up user hostnames with reverse DNS. there are 3 possibilities: # whether to look up user hostnames with reverse DNS.
# 1. [enabled here] lookup-hostnames enabled, IP cloaking disabled; users will see each other's hostnames # (disabling this will expose user IPs instead of hostnames;
# 2. lookup-hostnames disabled, IP cloaking disabled; users will see each other's numeric IPs # to make IP/hostname information private, see the ip-cloaking section)
# 3. IP cloaking enabled; users will see cloaked hostnames
lookup-hostnames: true lookup-hostnames: true
# whether to confirm hostname lookups using "forward-confirmed reverse DNS", i.e., for # whether to confirm hostname lookups using "forward-confirmed reverse DNS", i.e., for
# any hostname returned from reverse DNS, resolve it back to an IP address and reject it # any hostname returned from reverse DNS, resolve it back to an IP address and reject it
@ -135,41 +127,21 @@ server:
# use ident protocol to get usernames # use ident protocol to get usernames
check-ident: true check-ident: true
# ignore the supplied user/ident string from the USER command, always setting user/ident # password to login to the server
# to the following literal value; this can potentially reduce confusion and simplify bans. # generated using "oragono genpasswd"
# the value must begin with a '~' character. comment out / omit to disable: #password: ""
#coerce-ident: '~u'
# 'password' allows you to require a global, shared password (the IRC `PASS` command)
# to connect to the server. for operator passwords, see the `opers` section of the
# config. for a more secure way to create a private server, see the `require-sasl`
# section. you must hash the password with `ergo genpasswd`, then enter the hash here:
#password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
# motd filename # motd filename
# if you change the motd, you should move it to ircd.motd # if you change the motd, you should move it to ircd.motd
motd: ergo.motd motd: oragono.motd
# motd formatting codes # motd formatting codes
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i # if this is true, the motd is escaped using formatting codes like $c, $b, and $i
motd-formatting: true motd-formatting: true
# relaying using the RELAYMSG command # addresses/CIDRs the PROXY command can be used from
relaymsg: # this should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets),
# is relaymsg enabled at all? # unless you have a good reason. you should also add these addresses to the
enabled: true
# which character(s) are reserved for relayed nicks?
separators: "/"
# can channel operators use RELAYMSG in their channels?
# our implementation of RELAYMSG makes it safe for chanops to use without the
# possibility of real users being silently spoofed
available-to-chanops: true
# IPs/CIDRs the PROXY command can be used from
# This should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets).
# Unless you have a good reason. you should also add these addresses to the
# connection limits and throttling exemption lists. # connection limits and throttling exemption lists.
proxy-allowed-from: proxy-allowed-from:
- localhost - localhost
@ -184,18 +156,19 @@ server:
# (comment this out to use passwords only) # (comment this out to use passwords only)
certfp: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" certfp: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
# password the gateway uses to connect, made with `ergo genpasswd` # password the gateway uses to connect, made with oragono genpasswd
password: "$2a$04$abcdef0123456789abcdef0123456789abcdef0123456789abcde" password: "$2a$04$abcdef0123456789abcdef0123456789abcdef0123456789abcde"
# IPs/CIDRs that can use this webirc command # addresses/CIDRs that can use this webirc command
# you should also add these addresses to the connection limits and throttling exemption lists # you should also add these addresses to the connection limits and throttling exemption lists
hosts: hosts:
- localhost - localhost
# - "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 # allow use of the RESUME extension over plaintext connections:
accept-hostname: true # do not enable this unless the ircd is only accessible over internal networks
allow-plaintext-resume: 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
@ -205,7 +178,7 @@ server:
compatibility: compatibility:
# many clients require that the final parameter of certain messages be an # many clients require that the final parameter of certain messages be an
# RFC1459 trailing parameter, i.e., prefixed with :, whether or not this is # RFC1459 trailing parameter, i.e., prefixed with :, whether or not this is
# actually required. this forces Ergo to send those parameters # actually required. this forces Oragono to send those parameters
# as trailings. this is recommended unless you're testing clients for conformance; # as trailings. this is recommended unless you're testing clients for conformance;
# defaults to true when unset for that reason. # defaults to true when unset for that reason.
force-trailing: true force-trailing: true
@ -216,13 +189,6 @@ server:
# this works around that bug, allowing them to use SASL. # this works around that bug, allowing them to use SASL.
send-unprefixed-sasl: true send-unprefixed-sasl: true
# traditionally, IRC servers will truncate and send messages that are
# too long to be relayed intact. this behavior can be disabled by setting
# allow-truncation to false, in which case Ergo will reject the message
# and return an error to the client. (note that this option defaults to true
# when unset.)
allow-truncation: true
# IP-based DoS protection # IP-based DoS protection
ip-limits: ip-limits:
# whether to limit the total number of concurrent connections per IP/CIDR # whether to limit the total number of concurrent connections per IP/CIDR
@ -236,6 +202,9 @@ server:
window: 10m window: 10m
# maximum number of new connections per IP/CIDR within the given duration # maximum number of new connections per IP/CIDR within the given duration
max-connections-per-window: 32 max-connections-per-window: 32
# how long to ban offenders for. after banning them, the number of connections is
# reset, which lets you use /UNDLINE to unban people
throttle-ban-duration: 10m
# how wide the CIDR should be for IPv4 (a /32 is a fully specified IPv4 address) # how wide the CIDR should be for IPv4 (a /32 is a fully specified IPv4 address)
cidr-len-ipv4: 32 cidr-len-ipv4: 32
@ -249,59 +218,24 @@ server:
# - "192.168.1.1" # - "192.168.1.1"
# - "2001:0db8::/32" # - "2001:0db8::/32"
# custom connection limits for certain IPs/networks. # custom connection limits for certain IPs/networks. note that CIDR
# widths defined here override the default CIDR width --- the limit
# will apply to the entire CIDR no matter how large or small it is
custom-limits: custom-limits:
#"irccloud": # "8.8.0.0/16":
# nets: # max-concurrent-connections: 128
# - "192.184.9.108" # highgate.irccloud.com # max-connections-per-window: 1024
# - "192.184.9.110" # ealing.irccloud.com
# - "192.184.9.112" # charlton.irccloud.com
# - "192.184.10.118" # brockwell.irccloud.com
# - "192.184.10.9" # tooting.irccloud.com
# - "192.184.8.73" # hathersage.irccloud.com
# - "192.184.8.103" # stonehaven.irccloud.com
# - "5.254.36.57" # tinside.irccloud.com
# - "5.254.36.56/29" # additional ipv4 net
# - "2001:67c:2f08::/48"
# - "2a03:5180:f::/64"
# max-concurrent-connections: 2048
# max-connections-per-window: 2048
# pluggable IP ban mechanism, via subprocess invocation
# this can be used to check new connections against a DNSBL, for example
# see the manual for details on how to write an IP ban checking script
ip-check-script:
enabled: false
command: "/usr/local/bin/check-ip-ban"
# constant list of args to pass to the command; the actual query
# and result are transmitted over stdin/stdout:
args: []
# timeout for process execution, after which we send a SIGTERM:
timeout: 9s
# how long after the SIGTERM before we follow up with a SIGKILL:
kill-timeout: 1s
# how many scripts are allowed to run at once? 0 for no limit:
max-concurrency: 64
# if true, only check anonymous connections (not logged into an account)
# at the very end of the handshake:
exempt-sasl: false
# IP cloaking hides users' IP addresses from other users and from channel admins # IP cloaking hides users' IP addresses from other users and from channel admins
# (but not from server admins), while still allowing channel admins to ban # (but not from server admins), while still allowing channel admins to ban
# offending IP addresses or networks. In place of hostnames derived from reverse # offending IP addresses or networks. In place of hostnames derived from reverse
# DNS, users see fake domain names like pwbs2ui4377257x8.irc. These names are # DNS, users see fake domain names like pwbs2ui4377257x8.oragono. These names are
# generated deterministically from the underlying IP address, but if the underlying # generated deterministically from the underlying IP address, but if the underlying
# IP is not already known, it is infeasible to recover it from the cloaked name. # IP is not already known, it is infeasible to recover it from the cloaked name.
ip-cloaking: ip-cloaking:
# whether to enable IP cloaking # whether to enable IP cloaking
enabled: false enabled: false
# whether to use these cloak settings (specifically, `netname` and `num-bits`)
# to produce unique hostnames for always-on clients. you can enable this even if
# you disabled IP cloaking for normal clients above. if this is disabled,
# always-on clients will all have an identical hostname (the server name).
enabled-for-always-on: true
# fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.irc # fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.irc
# you may want to use your network name here # you may want to use your network name here
netname: "irc" netname: "irc"
@ -330,34 +264,10 @@ server:
secure-nets: secure-nets:
# - "10.0.0.0/8" # - "10.0.0.0/8"
# Ergo will write files to disk under certain circumstances, e.g., # oragono will write files to disk under certain circumstances, e.g.,
# CPU profiling or data export. by default, these files will be written # CPU profiling or data export. by default, these files will be written
# to the working directory. set this to customize: # to the working directory. set this to customize:
#output-path: "/home/ergo/out" # output-path: "/home/oragono/out"
# the hostname used by "services", e.g., NickServ, defaults to "localhost",
# e.g., `NickServ!NickServ@localhost`. uncomment this to override:
#override-services-hostname: "example.network"
# 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
# the default value of 512. DO NOT change this on a public server:
#max-line-len: 512
# 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
suppress-lusers: false
# publish additional key-value pairs in ISUPPORT (the 005 numeric).
# keys that collide with a key published by Ergo will be silently ignored.
additional-isupport:
#"draft/FILEHOST": "https://example.com/filehost"
#"draft/bazbat": "" # empty string means no value
# optionally map command alias names to existing ergo commands. most deployments
# should ignore this.
#command-aliases:
#"UMGEBUNG": "AMBIANCE"
# account options # account options
accounts: accounts:
@ -370,9 +280,6 @@ accounts:
# the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER` # the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER`
enabled: true enabled: true
# can users use the REGISTER command to register before fully connecting?
allow-before-connect: true
# global throttle on new account creation # global throttle on new account creation
throttling: throttling:
enabled: true enabled: true
@ -382,51 +289,33 @@ accounts:
max-attempts: 30 max-attempts: 30
# this is the bcrypt cost we'll use for account passwords # this is the bcrypt cost we'll use for account passwords
# (note that 4 is the lowest value allowed by the bcrypt library) bcrypt-cost: 9
bcrypt-cost: 4
# length of time a user has to verify their account before it can be re-registered # length of time a user has to verify their account before it can be re-registered
verify-timeout: "32h" verify-timeout: "32h"
# options for email verification of account registrations # callbacks to allow
email-verification: enabled-callbacks:
enabled: false - none # no verification needed, will instantly register successfully
sender: "admin@my.network"
require-tls: true # example configuration for sending verification emails
helo-domain: "my.network" # defaults to server name if unset # callbacks:
# set to `tcp4` to force sending over IPv4, `tcp6` to force IPv6: # mailto:
# protocol: "tcp4" # sender: "admin@my.network"
# set to force a specific source/local IPv4 or IPv6 address: # require-tls: true
# local-address: "1.2.3.4" # helo-domain: "my.network" # defaults to server name if unset
# options to enable DKIM signing of outgoing emails (recommended, but # dkim:
# requires creating a DNS entry for the public key): # domain: "my.network"
# dkim: # selector: "20200229"
# domain: "my.network" # key-file: "dkim.pem"
# selector: "20200229" # # to use an MTA/smarthost instead of sending email directly:
# key-file: "dkim.pem" # # mta:
# to use an MTA/smarthost instead of sending email directly: # # server: localhost
# mta: # # port: 25
# server: localhost # # username: "admin"
# port: 25 # # password: "hunter2"
# username: "admin" # blacklist-regexes:
# password: "hunter2" # # - ".*@mailinator.com"
# implicit-tls: false # TLS from the first byte, typically on port 465
# addresses that are not accepted for registration:
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
# email-based password reset:
password-reset:
enabled: false
# time before we allow resending the email
cooldown: 1h
# time for which a password reset code is valid
timeout: 1d
# throttle account login attempts (to prevent either password guessing, or DoS # throttle account login attempts (to prevent either password guessing, or DoS
# attacks on the server aimed at forcing repeated expensive bcrypt computations) # attacks on the server aimed at forcing repeated expensive bcrypt computations)
@ -450,17 +339,10 @@ 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:
# if this is enabled, all clients must authenticate with SASL while connecting. # if this is enabled, all clients must authenticate with SASL while connecting
# WARNING: for a private server, you MUST set accounts.registration.enabled
# to false as well, in order to prevent non-administrators from registering
# accounts.
enabled: false enabled: false
# IPs/CIDRs which are exempted from the account requirement # IPs/CIDRs which are exempted from the account requirement
@ -474,27 +356,32 @@ accounts:
enabled: true enabled: true
# how many nicknames, in addition to the account name, can be reserved? # how many nicknames, in addition to the account name, can be reserved?
# (note that additional nicks are unusable under force-nick-equals-account
# or if the client is always-on)
additional-nick-limit: 2 additional-nick-limit: 2
# method describes how nickname reservation is handled # method describes how nickname reservation is handled
# strict: users must already be logged in to their account (via # timeout: let the user change to the registered nickname, give them X seconds
# SASL, PASS account:password, or /NickServ IDENTIFY) # to login and then rename them if they haven't done so
# in order to use their reserved nickname(s) # strict: don't let the user change to the registered nickname unless they're
# already logged-in using SASL or NickServ
# optional: no enforcement by default, but allow users to opt in to # optional: no enforcement by default, but allow users to opt in to
# the enforcement level of their choice # the enforcement level of their choice
#
# 'optional' matches the behavior of other NickServs, but 'strict' is
# preferable if all your users can enable SASL.
method: optional method: optional
# allow users to set their own nickname enforcement status, e.g., # allow users to set their own nickname enforcement status, e.g.,
# to opt out of strict enforcement # to opt out of strict enforcement
allow-custom-enforcement: true allow-custom-enforcement: true
# rename-timeout - this is how long users have 'til they're renamed
rename-timeout: 30s
# format for guest nicknames: # format for guest nicknames:
# 1. these nicknames cannot be registered or reserved # 1. these nicknames cannot be registered or reserved
# 2. if a client is automatically renamed by the server, # 2. if a client is automatically renamed by the server,
# this is the template that will be used (e.g., Guest-nccj6rgmt97cg) # this is the template that will be used (e.g., Guest-nccj6rgmt97cg)
# 3. if force-guest-format (see below) is enabled, clients without # 3. if enforce-guest-format (see below) is enabled, clients without
# a registered account will have this template applied to their # a registered account will have this template applied to their
# nicknames (e.g., 'katie' will become 'Guest-katie') # nicknames (e.g., 'katie' will become 'Guest-katie')
guest-nickname-format: "Guest-*" guest-nickname-format: "Guest-*"
@ -511,12 +398,7 @@ accounts:
# as equivalent for the purpose of ban/invite/exception lists. # as equivalent for the purpose of ban/invite/exception lists.
force-nick-equals-account: false force-nick-equals-account: false
# parallel setting to force-nick-equals-account: if true, this forbids # multiclient controls whether oragono allows multiple connections to
# anonymous users (i.e., users not logged into an account) to change their
# nickname after the initial connection is complete
forbid-anonymous-nick-changes: false
# multiclient controls whether Ergo allows multiple connections to
# attach to the same client/nickname identity; this is part of the # attach to the same client/nickname identity; this is part of the
# functionality traditionally provided by a bouncer like ZNC # functionality traditionally provided by a bouncer like ZNC
multiclient: multiclient:
@ -539,10 +421,6 @@ accounts:
# whether to mark always-on clients away when they have no active connections: # whether to mark always-on clients away when they have no active connections:
auto-away: "opt-in" auto-away: "opt-in"
# QUIT always-on clients from the server if they go this long without connecting
# (use 0 or omit for no expiration):
#always-on-expiration: 90d
# vhosts controls the assignment of vhosts (strings displayed in place of the user's # vhosts controls the assignment of vhosts (strings displayed in place of the user's
# hostname/IP) by the HostServ service # hostname/IP) by the HostServ service
vhosts: vhosts:
@ -556,6 +434,23 @@ accounts:
# (make sure any changes you make here are RFC-compliant) # (make sure any changes you make here are RFC-compliant)
valid-regexp: '^[0-9A-Za-z.\-_/]+$' valid-regexp: '^[0-9A-Za-z.\-_/]+$'
# options controlling users requesting vhosts:
user-requests:
# can users request vhosts at all? if this is false, operators with the
# 'vhosts' capability can still assign vhosts manually
enabled: false
# if uncommented, all new vhost requests will be dumped into the given
# channel, so opers can review them as they are sent in. ensure that you
# have registered and restricted the channel appropriately before you
# uncomment this.
#channel: "#vhosts"
# after a user's vhost has been approved or rejected, they need to wait
# this long (starting from the time of their original request)
# before they can request a new one.
cooldown: 168h
# modes that are set by default when a user connects # modes that are set by default when a user connects
# if unset, no user modes will be set by default # if unset, no user modes will be set by default
# +i is invisible (a user's channels are hidden from whois replies) # +i is invisible (a user's channels are hidden from whois replies)
@ -576,42 +471,6 @@ accounts:
timeout: 9s timeout: 9s
# how long after the SIGTERM before we follow up with a SIGKILL: # how long after the SIGTERM before we follow up with a SIGKILL:
kill-timeout: 1s kill-timeout: 1s
# how many scripts are allowed to run at once? 0 for no limit:
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:
@ -643,86 +502,75 @@ channels:
# than this value will get an empty response to /LIST (a time period of 0 disables) # than this value will get an empty response to /LIST (a time period of 0 disables)
list-delay: 0s list-delay: 0s
# INVITE to an invite-only channel expires after this amount of time # operator classes
# (0 or omit for no expiration):
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:
# an operator has a single "class" (defining a privilege level), which can include
# multiple "capabilities" (defining privileged actions they can take). all
# currently available operator capabilities are associated with either the
# 'chat-moderator' class (less privileged) or the 'server-admin' class (full
# privileges) below: you can mix and match to create new classes.
oper-classes: oper-classes:
# chat moderator: can ban/unban users from the server, join channels, # local operator
# fix mode issues and sort out vhosts. "local-oper":
"chat-moderator":
# title shown in WHOIS # title shown in WHOIS
title: Chat Moderator title: Local Operator
# capability names # capability names
capabilities: capabilities:
- "kill" # disconnect user sessions - "local_kill"
- "ban" # ban IPs, CIDRs, NUH masks, and suspend accounts (UBAN / DLINE / KLINE) - "local_ban"
- "nofakelag" # exempted from "fakelag" restrictions on rate of message sending - "local_unban"
- "relaymsg" # use RELAYMSG in any channel (see the `relaymsg` config block) - "nofakelag"
- "vhosts" # add and remove vhosts from users - "roleplay"
- "sajoin" # join arbitrary channels, including private channels
- "samode" # modify arbitrary channel and user modes
- "snomasks" # subscribe to arbitrary server notice masks
- "roleplay" # use the (deprecated) roleplay commands in any channel
# server admin: has full control of the ircd, including nickname and # network operator
# channel registrations "network-oper":
# title shown in WHOIS
title: Network Operator
# oper class this extends from
extends: "local-oper"
# capability names
capabilities:
- "remote_kill"
- "remote_ban"
- "remote_unban"
# server admin
"server-admin": "server-admin":
# title shown in WHOIS # title shown in WHOIS
title: Server Admin title: Server Admin
# oper class this extends from # oper class this extends from
extends: "chat-moderator" extends: "local-oper"
# capability names # capability names
capabilities: capabilities:
- "rehash" # rehash the server, i.e. reload the config at runtime - "rehash"
- "accreg" # modify arbitrary account registrations - "die"
- "chanreg" # modify arbitrary channel registrations - "accreg"
- "history" # modify or delete history messages - "sajoin"
- "defcon" # use the DEFCON command (restrict server capabilities) - "samode"
- "massmessage" # message all users on the server - "vhosts"
- "metadata" # modify arbitrary metadata on channels and users - "chanreg"
- "history"
- "defcon"
# ircd operators # ircd operators
opers: opers:
# default operator named 'admin'; log in with /OPER admin <password> # operator named 'admin'; log in with /OPER admin [password]
admin: admin:
# which capabilities this oper has access to # which capabilities this oper has access to
class: "server-admin" class: "server-admin"
# traditionally, operator status is visible to unprivileged users in # custom whois line
# WHO and WHOIS responses. this can be disabled with 'hidden'. whois-line: is a cool dude
hidden: false
# custom whois line (if `hidden` is enabled, visible only to other operators) # custom hostname
whois-line: is the server administrator vhost: "n"
# custom hostname (ignored if `hidden` is enabled) # modes are the modes to auto-set upon opering-up
vhost: "staff" modes: +is acjknoqtuxv
# modes are modes to auto-set upon opering-up. uncomment this to automatically
# enable snomasks ("server notification masks" that alert you to server events;
# see `/quote help snomasks` while opered-up for more information):
#modes: +is acdjknoqtuxv
# operators can be authenticated either by password (with the /OPER command), # operators can be authenticated either by password (with the /OPER command),
# or by certificate fingerprint, or both. if a password hash is set, then a # or by certificate fingerprint, or both. if a password hash is set, then a
# password is required to oper up (e.g., /OPER dan mypassword). to generate # password is required to oper up (e.g., /OPER dan mypassword). to generate
# the hash, use `ergo genpasswd`. # the hash, use `oragono genpasswd`.
password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234" password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
# if a SHA-256 certificate fingerprint is configured here, then it will be # if a SHA-256 certificate fingerprint is configured here, then it will be
@ -733,13 +581,6 @@ opers:
# granted automatically as soon as you connect with the right fingerprint. # granted automatically as soon as you connect with the right fingerprint.
#auto: true #auto: true
# example of a moderator named 'alice'
# (log in with /OPER alice <password>):
#alice:
# class: "chat-moderator"
# whois-line: "can help with moderation issues!"
# password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
# logging, takes inspiration from Insp # logging, takes inspiration from Insp
logging: logging:
- -
@ -760,10 +601,11 @@ logging:
# be logged, even if you explicitly include it # be logged, even if you explicitly include it
# #
# useful types include: # useful types include:
# * everything (usually used with excluding some types below) # * everything (usually used with exclusing some types below)
# server server startup, rehash, and shutdown events # server server startup, rehash, and shutdown events
# accounts account registration and authentication # accounts account registration and authentication
# channels channel creation and operations # channels channel creation and operations
# commands command calling and operations
# opers oper actions, authentication, etc # opers oper actions, authentication, etc
# services actions related to NickServ, ChanServ, etc. # services actions related to NickServ, ChanServ, etc.
# internal unexpected runtime behavior, including potential bugs # internal unexpected runtime behavior, including potential bugs
@ -782,7 +624,7 @@ logging:
# debug options # debug options
debug: debug:
# when enabled, Ergo will attempt to recover from certain kinds of # when enabled, oragono will attempt to recover from certain kinds of
# client-triggered runtime errors that would normally crash the server. # client-triggered runtime errors that would normally crash the server.
# this makes the server more resilient to DoS, but could result in incorrect # this makes the server more resilient to DoS, but could result in incorrect
# behavior. deployments that would prefer to "start from scratch", e.g., by # behavior. deployments that would prefer to "start from scratch", e.g., by
@ -796,15 +638,9 @@ debug:
# set to `null`, "", leave blank, or omit to disable # set to `null`, "", leave blank, or omit to disable
# pprof-listener: "localhost:6060" # pprof-listener: "localhost:6060"
# lock file preventing multiple instances of Ergo from accidentally being
# started at once. comment out or set to the empty string ("") to disable.
# this path is relative to the working directory; if your datastore.path
# is absolute, you should use an absolute path here as well.
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
@ -819,13 +655,10 @@ datastore:
port: 3306 port: 3306
# if socket-path is set, it will be used instead of host:port # if socket-path is set, it will be used instead of host:port
#socket-path: "/var/run/mysqld/mysqld.sock" #socket-path: "/var/run/mysqld/mysqld.sock"
user: "ergo" user: "oragono"
password: "hunter2" password: "hunter2"
history-database: "ergo_history" history-database: "oragono_history"
timeout: 3s timeout: 3s
max-conns: 4
# this may be necessary to prevent middleware from closing your connections:
#conn-max-lifetime: 180s
# languages config # languages config
languages: languages:
@ -847,9 +680,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
@ -869,7 +699,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):
@ -899,22 +729,13 @@ fakelag:
# sending any commands: # sending any commands:
cooldown: 2s cooldown: 2s
# exempt a certain number of command invocations per session from fakelag;
# this is to speed up "resynchronization" of client state during reattach
command-budgets:
"CHATHISTORY": 16
"MARKREAD": 16
"MONITOR": 1
"WHO": 4
"WEBPUSH": 1
# the roleplay commands are semi-standardized extensions to IRC that allow # the roleplay commands are semi-standardized extensions to IRC that allow
# sending and receiving messages from pseudo-nicknames. this can be used either # sending and receiving messages from pseudo-nicknames. this can be used either
# for actual roleplaying, or for bridging IRC with other protocols. # for actual roleplaying, or for bridging IRC with other protocols.
roleplay: roleplay:
# are roleplay commands enabled at all? (channels and clients still have to # are roleplay commands enabled at all? (channels and clients still have to
# opt in individually with the +E mode) # opt in individually with the +E mode)
enabled: false enabled: true
# require the "roleplay" oper capability to send roleplay messages? # require the "roleplay" oper capability to send roleplay messages?
require-oper: false require-oper: false
@ -925,12 +746,6 @@ roleplay:
# add the real nickname, in parentheses, to the end of every roleplay message? # add the real nickname, in parentheses, to the end of every roleplay message?
add-suffix: true add-suffix: true
# allow customizing the NUH's sent for NPC and SCENE commands
# NPC: the first %s is the NPC name, the second is the user's real nick
#npc-nick-mask: "*%s*!%s@npc.fakeuser.invalid"
# SCENE: the %s is the client's real nick
#scene-nick-mask: "=Scene=!%s@npc.fakeuser.invalid"
# external services can integrate with the ircd using JSON Web Tokens (https://jwt.io). # external services can integrate with the ircd using JSON Web Tokens (https://jwt.io).
# in effect, the server can sign a token attesting that the client is present on # in effect, the server can sign a token attesting that the client is present on
# the server, is a member of a particular channel, etc. # the server, is a member of a particular channel, etc.
@ -978,7 +793,7 @@ history:
# maximum number of CHATHISTORY messages that can be # maximum number of CHATHISTORY messages that can be
# requested at once (0 disables support for CHATHISTORY) # requested at once (0 disables support for CHATHISTORY)
chathistory-maxmessages: 1000 chathistory-maxmessages: 100
# maximum number of messages that can be replayed at once during znc emulation # maximum number of messages that can be replayed at once during znc emulation
# (znc.in/playback, or automatic replay on initial reattach to a persistent client): # (znc.in/playback, or automatic replay on initial reattach to a persistent client):
@ -990,26 +805,17 @@ history:
# (and will eventually be deleted from persistent storage, if that's enabled) # (and will eventually be deleted from persistent storage, if that's enabled)
expire-time: 1w expire-time: 1w
# this restricts access to channel history (it can be overridden by channel # if this is set, logged-in users cannot retrieve messages older than their
# owners). options are: 'none' (no restrictions), 'registration-time' # account registration date, and logged-out users cannot retrieve messages
# (logged-in users cannot retrieve messages older than their account # older than their sign-on time (modulo grace-period, see below):
# registration date, and anonymous users cannot retrieve messages older than enforce-registration-date: false
# their sign-on time, modulo the grace-period described below), and
# 'join-time' (users cannot retrieve messages older than the time they
# joined the channel, so only always-on clients can view history).
query-cutoff: 'none'
# if query-cutoff is set to 'registration-time', this allows retrieval # but if this is set, you can retrieve messages that are up to `grace-period`
# of messages that are up to 'grace-period' older than the above cutoff. # older than the above cutoff time. this is recommended to allow logged-out
# if you use 'registration-time', this is recommended to allow logged-out # users to do session resumption / query history after disconnections.
# users to query history after disconnections.
grace-period: 1h grace-period: 1h
# options to store history messages in a persistent database (currently only MySQL). # options to store history messages in a persistent database (currently only MySQL):
# in order to enable any of this functionality, you must configure a MySQL server
# in the `datastore.mysql` section. enabling persistence overrides the history
# size limits above (`channel-length`, `client-length`, etc.); persistent
# history has no limits other than those imposed by expire-time.
persistent: persistent:
enabled: false enabled: false
@ -1031,8 +837,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,
@ -1048,61 +853,9 @@ history:
# if `default` is false, store TAGMSG containing any of these tags: # if `default` is false, store TAGMSG containing any of these tags:
whitelist: whitelist:
- "+draft/react" - "+draft/react"
- "+react" - "react"
# if `default` is true, don't store TAGMSG containing any of these tags: # if `default` is true, don't store TAGMSG containing any of these tags:
#blacklist: #blacklist:
# - "+draft/typing" # - "+draft/typing"
# - "typing" # - "typing"
# 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.
allow-environment-overrides: true
# metadata support for setting key/value data on channels and nicknames.
metadata:
# can clients store metadata?
enabled: true
# how many keys can a client subscribe to?
max-subs: 100
# how many keys can be stored per entity?
max-keys: 100
# experimental support for mobile push notifications
# see the manual for potential security, privacy, and performance implications.
# DO NOT enable if you are running a Tor or I2P hidden service (i.e. one
# with no public IP listeners, only Tor/I2P listeners).
webpush:
# are push notifications enabled at all?
enabled: false
# request timeout for POST'ing the http notification
timeout: 10s
# delay sending the notification for this amount of time, then suppress it
# if the client sent MARKREAD to indicate that it was read on another device
delay: 0s
# subscriber field for the VAPID JWT authorization:
#subscriber: "https://your-website.com/"
# maximum number of push subscriptions per user
max-subscriptions: 4
# expiration time for a push subscription; it must be renewed within this time
# by the client reconnecting to IRC. we also detect whether the client is no longer
# successfully receiving push messages.
expiration: 14d
# HTTP API. we strongly recommend leaving this disabled unless you have a specific
# need for it.
api:
# is the API enabled at all?
enabled: false
# listen address:
listener: "127.0.0.1:8089"
# serve over TLS (strongly recommended if the listener is public):
#tls:
#cert: fullchain.pem
#key: privkey.pem
# one or more static bearer tokens accepted for HTTP bearer authentication.
# these must be strong, unique, high-entropy printable ASCII strings.
# to generate a new token, use `ergo gentoken` or:
# python3 -c "import secrets; print(secrets.token_urlsafe(32))"
bearer-tokens:
- "example"

View File

@ -1,10 +1,10 @@
# This is the default config file for Ergo. # This is the default config file for Oragono.
# It contains recommended defaults for all settings, including some behaviors # It contains recommended defaults for all settings, including some behaviors
# that differ from conventional ircd+services setups. See traditional.yaml # that differ from conventional ircds. See conventional.yaml for a config
# for a config with more "mainstream" behavior. # with more "mainstream" behavior.
# #
# If you are setting up a new Ergo server, you should copy this file # If you are setting up a new oragono server, you should copy this file
# to a new one named 'ircd.yaml', then look through the file to see which # to a new one named 'ircd.yaml', then read the whole file to see which
# settings you want to customize. If you don't understand a setting, or # settings you want to customize. If you don't understand a setting, or
# aren't sure what behavior you want, most of the defaults are fine # aren't sure what behavior you want, most of the defaults are fine
# to start with (you can change them later, even on a running server). # to start with (you can change them later, even on a running server).
@ -25,12 +25,12 @@
# network configuration # network configuration
network: network:
# name of the network # name of the network
name: ErgoTest name: OragonoTest
# server configuration # server configuration
server: server:
# server name # server name
name: ergo.test name: oragono.test
# addresses to listen on # addresses to listen on
listeners: listeners:
@ -49,25 +49,21 @@ server:
# The standard SSL/TLS port for IRC is 6697. This will listen on all interfaces: # The standard SSL/TLS port for IRC is 6697. This will listen on all interfaces:
":6697": ":6697":
# this is a standard TLS configuration with a single certificate;
# see the manual for instructions on how to configure SNI
tls: tls:
cert: fullchain.pem cert: fullchain.pem
key: privkey.pem key: privkey.pem
# 'proxy' should typically be false. It's for cloud load balancers that # 'proxy' should typically be false. It's only for Kubernetes-style load
# always send a PROXY protocol header ahead of the connection. See the # balancing that does not terminate TLS, but sends an initial PROXY line
# manual ("Reverse proxies") for more details. # in plaintext.
proxy: false proxy: false
# set the minimum TLS version:
min-tls-version: 1.2
# Example of a Unix domain socket for proxying: # Example of a Unix domain socket for proxying:
# "/tmp/ergo_sock": # "/tmp/oragono_sock":
# Example of a Tor listener: any connection that comes in on this listener will # Example of a Tor listener: any connection that comes in on this listener will
# be considered a Tor connection. It is strongly recommended that this listener # be considered a Tor connection. It is strongly recommended that this listener
# *not* be on a public interface --- it should be on 127.0.0.0/8 or unix domain: # *not* be on a public interface --- it should be on 127.0.0.0/8 or unix domain:
# "/hidden_service_sockets/ergo_tor_sock": # "/hidden_service_sockets/oragono_tor_sock":
# tor: true # tor: true
# Example of a WebSocket listener: # Example of a WebSocket listener:
@ -100,7 +96,6 @@ server:
max-connections-per-duration: 64 max-connections-per-duration: 64
# strict transport security, to get clients to automagically use TLS # strict transport security, to get clients to automagically use TLS
# (irrelevant in the recommended configuration, with no public plaintext listener)
sts: sts:
# whether to advertise STS # whether to advertise STS
# #
@ -121,31 +116,29 @@ server:
websockets: websockets:
# Restrict the origin of WebSocket connections by matching the "Origin" HTTP # Restrict the origin of WebSocket connections by matching the "Origin" HTTP
# header. This setting causes ergo to reject websocket connections unless # header. This settings makes oragono reject every WebSocket connection,
# they originate from a page on one of the whitelisted websites in this list. # except when it originates from one of the hosts in this list. Use this to
# This prevents malicious websites from making their visitors connect to your # prevent malicious websites from making their visitors connect to oragono
# ergo instance without their knowledge. An empty list means there are no # without their knowledge. An empty list means that there are no restrictions.
# restrictions.
allowed-origins: allowed-origins:
# - "https://ergo.chat" # - "https://oragono.io"
# - "https://*.ergo.chat" # - "https://*.oragono.io"
# casemapping controls what kinds of strings are permitted as identifiers (nicknames, # casemapping controls what kinds of strings are permitted as identifiers (nicknames,
# channel names, account names, etc.), and how they are normalized for case. # channel names, account names, etc.), and how they are normalized for case.
# the recommended default is 'ascii' (traditional ASCII-only identifiers). # with the recommended default of 'precis', UTF8 identifiers that are "sane"
# the other options are 'precis', which allows UTF8 identifiers that are "sane" # (according to RFC 8265) are allowed, and the server additionally tries to protect
# (according to UFC 8265), with additional mitigations for homoglyph attacks, # against confusable characters ("homoglyph attacks").
# 'permissive', which allows identifiers containing unusual characters like # the other options are 'ascii' (traditional ASCII-only identifiers), and 'permissive',
# emoji, at the cost of increased vulnerability to homoglyph attacks and potential # which allows identifiers to contain unusual characters like emoji, but makes users
# client compatibility problems, and the legacy mappings 'rfc1459' and # vulnerable to homoglyph attacks. unless you're really confident in your decision,
# 'rfc1459-strict'. we recommend leaving this value at its default; # we recommend leaving this value at its default (changing it once the network is
# however, note that changing it once the network is already up and running is # already up and running is problematic).
# problematic. casemapping: "precis"
casemapping: "ascii"
# enforce-utf8 controls whether the server will preemptively discard non-UTF8 # enforce-utf8 controls whether the server allows non-UTF8 bytes in messages
# messages (since they cannot be relayed to websocket clients), or will allow # (as in traditional IRC) or preemptively discards non-UTF8 messages (since
# them and relay them to non-websocket clients (as in traditional IRC). # they cannot be relayed to websocket clients).
enforce-utf8: true enforce-utf8: true
# whether to look up user hostnames with reverse DNS. there are 3 possibilities: # whether to look up user hostnames with reverse DNS. there are 3 possibilities:
@ -161,41 +154,21 @@ server:
# use ident protocol to get usernames # use ident protocol to get usernames
check-ident: false check-ident: false
# ignore the supplied user/ident string from the USER command, always setting user/ident # password to login to the server
# to the following literal value; this can potentially reduce confusion and simplify bans. # generated using "oragono genpasswd"
# the value must begin with a '~' character. comment out / omit to disable: #password: ""
coerce-ident: '~u'
# 'password' allows you to require a global, shared password (the IRC `PASS` command)
# to connect to the server. for operator passwords, see the `opers` section of the
# config. for a more secure way to create a private server, see the `require-sasl`
# section. you must hash the password with `ergo genpasswd`, then enter the hash here:
#password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
# motd filename # motd filename
# if you change the motd, you should move it to ircd.motd # if you change the motd, you should move it to ircd.motd
motd: ergo.motd motd: oragono.motd
# motd formatting codes # motd formatting codes
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i # if this is true, the motd is escaped using formatting codes like $c, $b, and $i
motd-formatting: true motd-formatting: true
# relaying using the RELAYMSG command # addresses/CIDRs the PROXY command can be used from
relaymsg: # this should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets),
# is relaymsg enabled at all? # unless you have a good reason. you should also add these addresses to the
enabled: true
# which character(s) are reserved for relayed nicks?
separators: "/"
# can channel operators use RELAYMSG in their channels?
# our implementation of RELAYMSG makes it safe for chanops to use without the
# possibility of real users being silently spoofed
available-to-chanops: true
# IPs/CIDRs the PROXY command can be used from
# This should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets).
# Unless you have a good reason. you should also add these addresses to the
# connection limits and throttling exemption lists. # connection limits and throttling exemption lists.
proxy-allowed-from: proxy-allowed-from:
- localhost - localhost
@ -210,19 +183,19 @@ server:
# (comment this out to use passwords only) # (comment this out to use passwords only)
certfp: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" certfp: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
# password the gateway uses to connect, made with `ergo genpasswd` # password the gateway uses to connect, made with oragono genpasswd
password: "$2a$04$abcdef0123456789abcdef0123456789abcdef0123456789abcde" password: "$2a$04$abcdef0123456789abcdef0123456789abcdef0123456789abcde"
# IPs/CIDRs that can use this webirc command # addresses/CIDRs that can use this webirc command
# you should also add these addresses to the connection limits and throttling exemption lists # you should also add these addresses to the connection limits and throttling exemption lists
hosts: hosts:
- localhost - localhost
# - "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 # allow use of the RESUME extension over plaintext connections:
# (the default/recommended Ergo configuration will use cloaks instead) # do not enable this unless the ircd is only accessible over internal networks
accept-hostname: false allow-plaintext-resume: 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
@ -232,7 +205,7 @@ server:
compatibility: compatibility:
# many clients require that the final parameter of certain messages be an # many clients require that the final parameter of certain messages be an
# RFC1459 trailing parameter, i.e., prefixed with :, whether or not this is # RFC1459 trailing parameter, i.e., prefixed with :, whether or not this is
# actually required. this forces Ergo to send those parameters # actually required. this forces Oragono to send those parameters
# as trailings. this is recommended unless you're testing clients for conformance; # as trailings. this is recommended unless you're testing clients for conformance;
# defaults to true when unset for that reason. # defaults to true when unset for that reason.
force-trailing: true force-trailing: true
@ -243,13 +216,6 @@ server:
# this works around that bug, allowing them to use SASL. # this works around that bug, allowing them to use SASL.
send-unprefixed-sasl: true send-unprefixed-sasl: true
# traditionally, IRC servers will truncate and send messages that are
# too long to be relayed intact. this behavior can be disabled by setting
# allow-truncation to false, in which case Ergo will reject the message
# and return an error to the client. (note that this option defaults to true
# when unset.)
allow-truncation: false
# IP-based DoS protection # IP-based DoS protection
ip-limits: ip-limits:
# whether to limit the total number of concurrent connections per IP/CIDR # whether to limit the total number of concurrent connections per IP/CIDR
@ -263,6 +229,9 @@ server:
window: 10m window: 10m
# maximum number of new connections per IP/CIDR within the given duration # maximum number of new connections per IP/CIDR within the given duration
max-connections-per-window: 32 max-connections-per-window: 32
# how long to ban offenders for. after banning them, the number of connections is
# reset, which lets you use /UNDLINE to unban people
throttle-ban-duration: 10m
# how wide the CIDR should be for IPv4 (a /32 is a fully specified IPv4 address) # how wide the CIDR should be for IPv4 (a /32 is a fully specified IPv4 address)
cidr-len-ipv4: 32 cidr-len-ipv4: 32
@ -276,47 +245,18 @@ server:
# - "192.168.1.1" # - "192.168.1.1"
# - "2001:0db8::/32" # - "2001:0db8::/32"
# custom connection limits for certain IPs/networks. # custom connection limits for certain IPs/networks. note that CIDR
# widths defined here override the default CIDR width --- the limit
# will apply to the entire CIDR no matter how large or small it is
custom-limits: custom-limits:
#"irccloud": # "8.8.0.0/16":
# nets: # max-concurrent-connections: 128
# - "192.184.9.108" # highgate.irccloud.com # max-connections-per-window: 1024
# - "192.184.9.110" # ealing.irccloud.com
# - "192.184.9.112" # charlton.irccloud.com
# - "192.184.10.118" # brockwell.irccloud.com
# - "192.184.10.9" # tooting.irccloud.com
# - "192.184.8.73" # hathersage.irccloud.com
# - "192.184.8.103" # stonehaven.irccloud.com
# - "5.254.36.57" # tinside.irccloud.com
# - "5.254.36.56/29" # additional ipv4 net
# - "2001:67c:2f08::/48"
# - "2a03:5180:f::/64"
# max-concurrent-connections: 2048
# max-connections-per-window: 2048
# pluggable IP ban mechanism, via subprocess invocation
# this can be used to check new connections against a DNSBL, for example
# see the manual for details on how to write an IP ban checking script
ip-check-script:
enabled: false
command: "/usr/local/bin/check-ip-ban"
# constant list of args to pass to the command; the actual query
# and result are transmitted over stdin/stdout:
args: []
# timeout for process execution, after which we send a SIGTERM:
timeout: 9s
# how long after the SIGTERM before we follow up with a SIGKILL:
kill-timeout: 1s
# how many scripts are allowed to run at once? 0 for no limit:
max-concurrency: 64
# if true, only check anonymous connections (not logged into an account)
# at the very end of the handshake:
exempt-sasl: false
# IP cloaking hides users' IP addresses from other users and from channel admins # IP cloaking hides users' IP addresses from other users and from channel admins
# (but not from server admins), while still allowing channel admins to ban # (but not from server admins), while still allowing channel admins to ban
# offending IP addresses or networks. In place of hostnames derived from reverse # offending IP addresses or networks. In place of hostnames derived from reverse
# DNS, users see fake domain names like pwbs2ui4377257x8.irc. These names are # DNS, users see fake domain names like pwbs2ui4377257x8.oragono. These names are
# generated deterministically from the underlying IP address, but if the underlying # generated deterministically from the underlying IP address, but if the underlying
# IP is not already known, it is infeasible to recover it from the cloaked name. # IP is not already known, it is infeasible to recover it from the cloaked name.
# If you disable this, you should probably enable lookup-hostnames in its place. # If you disable this, you should probably enable lookup-hostnames in its place.
@ -324,12 +264,6 @@ server:
# whether to enable IP cloaking # whether to enable IP cloaking
enabled: true enabled: true
# whether to use these cloak settings (specifically, `netname` and `num-bits`)
# to produce unique hostnames for always-on clients. you can enable this even if
# you disabled IP cloaking for normal clients above. if this is disabled,
# always-on clients will all have an identical hostname (the server name).
enabled-for-always-on: true
# fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.irc # fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.irc
# you may want to use your network name here # you may want to use your network name here
netname: "irc" netname: "irc"
@ -358,34 +292,10 @@ server:
secure-nets: secure-nets:
# - "10.0.0.0/8" # - "10.0.0.0/8"
# Ergo will write files to disk under certain circumstances, e.g., # oragono will write files to disk under certain circumstances, e.g.,
# CPU profiling or data export. by default, these files will be written # CPU profiling or data export. by default, these files will be written
# to the working directory. set this to customize: # to the working directory. set this to customize:
#output-path: "/home/ergo/out" # output-path: "/home/oragono/out"
# the hostname used by "services", e.g., NickServ, defaults to "localhost",
# e.g., `NickServ!NickServ@localhost`. uncomment this to override:
#override-services-hostname: "example.network"
# 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
# the default value of 512. DO NOT change this on a public server:
#max-line-len: 512
# 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
suppress-lusers: false
# publish additional key-value pairs in ISUPPORT (the 005 numeric).
# keys that collide with a key published by Ergo will be silently ignored.
additional-isupport:
#"draft/FILEHOST": "https://example.com/filehost"
#"draft/bazbat": "" # empty string means no value
# optionally map command alias names to existing ergo commands. most deployments
# should ignore this.
#command-aliases:
#"UMGEBUNG": "AMBIANCE"
# account options # account options
accounts: accounts:
@ -398,9 +308,6 @@ accounts:
# the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER` # the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER`
enabled: true enabled: true
# can users use the REGISTER command to register before fully connecting?
allow-before-connect: true
# global throttle on new account creation # global throttle on new account creation
throttling: throttling:
enabled: true enabled: true
@ -410,51 +317,33 @@ accounts:
max-attempts: 30 max-attempts: 30
# this is the bcrypt cost we'll use for account passwords # this is the bcrypt cost we'll use for account passwords
# (note that 4 is the lowest value allowed by the bcrypt library) bcrypt-cost: 9
bcrypt-cost: 4
# length of time a user has to verify their account before it can be re-registered # length of time a user has to verify their account before it can be re-registered
verify-timeout: "32h" verify-timeout: "32h"
# options for email verification of account registrations # callbacks to allow
email-verification: enabled-callbacks:
enabled: false - none # no verification needed, will instantly register successfully
sender: "admin@my.network"
require-tls: true # example configuration for sending verification emails
helo-domain: "my.network" # defaults to server name if unset # callbacks:
# set to `tcp4` to force sending over IPv4, `tcp6` to force IPv6: # mailto:
# protocol: "tcp4" # sender: "admin@my.network"
# set to force a specific source/local IPv4 or IPv6 address: # require-tls: true
# local-address: "1.2.3.4" # helo-domain: "my.network" # defaults to server name if unset
# options to enable DKIM signing of outgoing emails (recommended, but # dkim:
# requires creating a DNS entry for the public key): # domain: "my.network"
# dkim: # selector: "20200229"
# domain: "my.network" # key-file: "dkim.pem"
# selector: "20200229" # # to use an MTA/smarthost instead of sending email directly:
# key-file: "dkim.pem" # # mta:
# to use an MTA/smarthost instead of sending email directly: # # server: localhost
# mta: # # port: 25
# server: localhost # # username: "admin"
# port: 25 # # password: "hunter2"
# username: "admin" # blacklist-regexes:
# password: "hunter2" # # - ".*@mailinator.com"
# implicit-tls: false # TLS from the first byte, typically on port 465
# addresses that are not accepted for registration:
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
# email-based password reset:
password-reset:
enabled: false
# time before we allow resending the email
cooldown: 1h
# time for which a password reset code is valid
timeout: 1d
# throttle account login attempts (to prevent either password guessing, or DoS # throttle account login attempts (to prevent either password guessing, or DoS
# attacks on the server aimed at forcing repeated expensive bcrypt computations) # attacks on the server aimed at forcing repeated expensive bcrypt computations)
@ -478,17 +367,10 @@ 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:
# if this is enabled, all clients must authenticate with SASL while connecting. # if this is enabled, all clients must authenticate with SASL while connecting
# WARNING: for a private server, you MUST set accounts.registration.enabled
# to false as well, in order to prevent non-administrators from registering
# accounts.
enabled: false enabled: false
# IPs/CIDRs which are exempted from the account requirement # IPs/CIDRs which are exempted from the account requirement
@ -502,27 +384,32 @@ accounts:
enabled: true enabled: true
# how many nicknames, in addition to the account name, can be reserved? # how many nicknames, in addition to the account name, can be reserved?
# (note that additional nicks are unusable under force-nick-equals-account additional-nick-limit: 2
# or if the client is always-on)
additional-nick-limit: 0
# method describes how nickname reservation is handled # method describes how nickname reservation is handled
# strict: users must already be logged in to their account (via # timeout: let the user change to the registered nickname, give them X seconds
# SASL, PASS account:password, or /NickServ IDENTIFY) # to login and then rename them if they haven't done so
# in order to use their reserved nickname(s) # strict: don't let the user change to the registered nickname unless they're
# already logged-in using SASL or NickServ
# optional: no enforcement by default, but allow users to opt in to # optional: no enforcement by default, but allow users to opt in to
# the enforcement level of their choice # the enforcement level of their choice
#
# 'optional' matches the behavior of other NickServs, but 'strict' is
# preferable if all your users can enable SASL.
method: strict method: strict
# allow users to set their own nickname enforcement status, e.g., # allow users to set their own nickname enforcement status, e.g.,
# to opt out of strict enforcement # to opt out of strict enforcement
allow-custom-enforcement: false allow-custom-enforcement: false
# rename-timeout - this is how long users have 'til they're renamed
rename-timeout: 30s
# format for guest nicknames: # format for guest nicknames:
# 1. these nicknames cannot be registered or reserved # 1. these nicknames cannot be registered or reserved
# 2. if a client is automatically renamed by the server, # 2. if a client is automatically renamed by the server,
# this is the template that will be used (e.g., Guest-nccj6rgmt97cg) # this is the template that will be used (e.g., Guest-nccj6rgmt97cg)
# 3. if force-guest-format (see below) is enabled, clients without # 3. if enforce-guest-format (see below) is enabled, clients without
# a registered account will have this template applied to their # a registered account will have this template applied to their
# nicknames (e.g., 'katie' will become 'Guest-katie') # nicknames (e.g., 'katie' will become 'Guest-katie')
guest-nickname-format: "Guest-*" guest-nickname-format: "Guest-*"
@ -539,12 +426,7 @@ accounts:
# as equivalent for the purpose of ban/invite/exception lists. # as equivalent for the purpose of ban/invite/exception lists.
force-nick-equals-account: true force-nick-equals-account: true
# parallel setting to force-nick-equals-account: if true, this forbids # multiclient controls whether oragono allows multiple connections to
# anonymous users (i.e., users not logged into an account) to change their
# nickname after the initial connection is complete
forbid-anonymous-nick-changes: false
# multiclient controls whether Ergo allows multiple connections to
# attach to the same client/nickname identity; this is part of the # attach to the same client/nickname identity; this is part of the
# functionality traditionally provided by a bouncer like ZNC # functionality traditionally provided by a bouncer like ZNC
multiclient: multiclient:
@ -567,10 +449,6 @@ accounts:
# whether to mark always-on clients away when they have no active connections: # whether to mark always-on clients away when they have no active connections:
auto-away: "opt-in" auto-away: "opt-in"
# QUIT always-on clients from the server if they go this long without connecting
# (use 0 or omit for no expiration):
#always-on-expiration: 90d
# vhosts controls the assignment of vhosts (strings displayed in place of the user's # vhosts controls the assignment of vhosts (strings displayed in place of the user's
# hostname/IP) by the HostServ service # hostname/IP) by the HostServ service
vhosts: vhosts:
@ -584,6 +462,23 @@ accounts:
# (make sure any changes you make here are RFC-compliant) # (make sure any changes you make here are RFC-compliant)
valid-regexp: '^[0-9A-Za-z.\-_/]+$' valid-regexp: '^[0-9A-Za-z.\-_/]+$'
# options controlling users requesting vhosts:
user-requests:
# can users request vhosts at all? if this is false, operators with the
# 'vhosts' capability can still assign vhosts manually
enabled: false
# if uncommented, all new vhost requests will be dumped into the given
# channel, so opers can review them as they are sent in. ensure that you
# have registered and restricted the channel appropriately before you
# uncomment this.
#channel: "#vhosts"
# after a user's vhost has been approved or rejected, they need to wait
# this long (starting from the time of their original request)
# before they can request a new one.
cooldown: 168h
# modes that are set by default when a user connects # modes that are set by default when a user connects
# if unset, no user modes will be set by default # if unset, no user modes will be set by default
# +i is invisible (a user's channels are hidden from whois replies) # +i is invisible (a user's channels are hidden from whois replies)
@ -604,50 +499,13 @@ accounts:
timeout: 9s timeout: 9s
# how long after the SIGTERM before we follow up with a SIGKILL: # how long after the SIGTERM before we follow up with a SIGKILL:
kill-timeout: 1s kill-timeout: 1s
# how many scripts are allowed to run at once? 0 for no limit:
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
# +n is no-external-messages, +t is op-only-topic, # +n is no-external-messages and +t is op-only-topic
# +C is no CTCPs (besides ACTION)
# see /QUOTE HELP cmodes for more channel modes # see /QUOTE HELP cmodes for more channel modes
default-modes: +ntC default-modes: +nt
# how many channels can a client be in at once? # how many channels can a client be in at once?
max-channels-per-client: 100 max-channels-per-client: 100
@ -672,86 +530,75 @@ channels:
# than this value will get an empty response to /LIST (a time period of 0 disables) # than this value will get an empty response to /LIST (a time period of 0 disables)
list-delay: 0s list-delay: 0s
# INVITE to an invite-only channel expires after this amount of time # operator classes
# (0 or omit for no expiration):
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:
# an operator has a single "class" (defining a privilege level), which can include
# multiple "capabilities" (defining privileged actions they can take). all
# currently available operator capabilities are associated with either the
# 'chat-moderator' class (less privileged) or the 'server-admin' class (full
# privileges) below: you can mix and match to create new classes.
oper-classes: oper-classes:
# chat moderator: can ban/unban users from the server, join channels, # local operator
# fix mode issues and sort out vhosts. "local-oper":
"chat-moderator":
# title shown in WHOIS # title shown in WHOIS
title: Chat Moderator title: Local Operator
# capability names # capability names
capabilities: capabilities:
- "kill" # disconnect user sessions - "local_kill"
- "ban" # ban IPs, CIDRs, NUH masks, and suspend accounts (UBAN / DLINE / KLINE) - "local_ban"
- "nofakelag" # exempted from "fakelag" restrictions on rate of message sending - "local_unban"
- "relaymsg" # use RELAYMSG in any channel (see the `relaymsg` config block) - "nofakelag"
- "vhosts" # add and remove vhosts from users - "roleplay"
- "sajoin" # join arbitrary channels, including private channels
- "samode" # modify arbitrary channel and user modes
- "snomasks" # subscribe to arbitrary server notice masks
- "roleplay" # use the (deprecated) roleplay commands in any channel
# server admin: has full control of the ircd, including nickname and # network operator
# channel registrations "network-oper":
# title shown in WHOIS
title: Network Operator
# oper class this extends from
extends: "local-oper"
# capability names
capabilities:
- "remote_kill"
- "remote_ban"
- "remote_unban"
# server admin
"server-admin": "server-admin":
# title shown in WHOIS # title shown in WHOIS
title: Server Admin title: Server Admin
# oper class this extends from # oper class this extends from
extends: "chat-moderator" extends: "local-oper"
# capability names # capability names
capabilities: capabilities:
- "rehash" # rehash the server, i.e. reload the config at runtime - "rehash"
- "accreg" # modify arbitrary account registrations - "die"
- "chanreg" # modify arbitrary channel registrations - "accreg"
- "history" # modify or delete history messages - "sajoin"
- "defcon" # use the DEFCON command (restrict server capabilities) - "samode"
- "massmessage" # message all users on the server - "vhosts"
- "metadata" # modify arbitrary metadata on channels and users - "chanreg"
- "history"
- "defcon"
# ircd operators # ircd operators
opers: opers:
# default operator named 'admin'; log in with /OPER admin <password> # operator named 'admin'; log in with /OPER admin [password]
admin: admin:
# which capabilities this oper has access to # which capabilities this oper has access to
class: "server-admin" class: "server-admin"
# traditionally, operator status is visible to unprivileged users in # custom whois line
# WHO and WHOIS responses. this can be disabled with 'hidden'. whois-line: is a cool dude
hidden: true
# custom whois line (if `hidden` is enabled, visible only to other operators) # custom hostname
whois-line: is the server administrator vhost: "n"
# custom hostname (ignored if `hidden` is enabled) # modes are the modes to auto-set upon opering-up
#vhost: "staff" modes: +is acjknoqtuxv
# modes are modes to auto-set upon opering-up. uncomment this to automatically
# enable snomasks ("server notification masks" that alert you to server events;
# see `/quote help snomasks` while opered-up for more information):
#modes: +is acdjknoqtuxv
# operators can be authenticated either by password (with the /OPER command), # operators can be authenticated either by password (with the /OPER command),
# or by certificate fingerprint, or both. if a password hash is set, then a # or by certificate fingerprint, or both. if a password hash is set, then a
# password is required to oper up (e.g., /OPER dan mypassword). to generate # password is required to oper up (e.g., /OPER dan mypassword). to generate
# the hash, use `ergo genpasswd`. # the hash, use `oragono genpasswd`.
password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234" password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
# if a SHA-256 certificate fingerprint is configured here, then it will be # if a SHA-256 certificate fingerprint is configured here, then it will be
@ -762,13 +609,6 @@ opers:
# granted automatically as soon as you connect with the right fingerprint. # granted automatically as soon as you connect with the right fingerprint.
#auto: true #auto: true
# example of a moderator named 'alice'
# (log in with /OPER alice <password>):
#alice:
# class: "chat-moderator"
# whois-line: "can help with moderation issues!"
# password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
# logging, takes inspiration from Insp # logging, takes inspiration from Insp
logging: logging:
- -
@ -789,10 +629,11 @@ logging:
# be logged, even if you explicitly include it # be logged, even if you explicitly include it
# #
# useful types include: # useful types include:
# * everything (usually used with excluding some types below) # * everything (usually used with exclusing some types below)
# server server startup, rehash, and shutdown events # server server startup, rehash, and shutdown events
# accounts account registration and authentication # accounts account registration and authentication
# channels channel creation and operations # channels channel creation and operations
# commands command calling and operations
# opers oper actions, authentication, etc # opers oper actions, authentication, etc
# services actions related to NickServ, ChanServ, etc. # services actions related to NickServ, ChanServ, etc.
# internal unexpected runtime behavior, including potential bugs # internal unexpected runtime behavior, including potential bugs
@ -811,7 +652,7 @@ logging:
# debug options # debug options
debug: debug:
# when enabled, Ergo will attempt to recover from certain kinds of # when enabled, oragono will attempt to recover from certain kinds of
# client-triggered runtime errors that would normally crash the server. # client-triggered runtime errors that would normally crash the server.
# this makes the server more resilient to DoS, but could result in incorrect # this makes the server more resilient to DoS, but could result in incorrect
# behavior. deployments that would prefer to "start from scratch", e.g., by # behavior. deployments that would prefer to "start from scratch", e.g., by
@ -825,15 +666,9 @@ debug:
# set to `null`, "", leave blank, or omit to disable # set to `null`, "", leave blank, or omit to disable
# pprof-listener: "localhost:6060" # pprof-listener: "localhost:6060"
# lock file preventing multiple instances of Ergo from accidentally being
# started at once. comment out or set to the empty string ("") to disable.
# this path is relative to the working directory; if your datastore.path
# is absolute, you should use an absolute path here as well.
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
@ -848,13 +683,10 @@ datastore:
port: 3306 port: 3306
# if socket-path is set, it will be used instead of host:port # if socket-path is set, it will be used instead of host:port
#socket-path: "/var/run/mysqld/mysqld.sock" #socket-path: "/var/run/mysqld/mysqld.sock"
user: "ergo" user: "oragono"
password: "hunter2" password: "hunter2"
history-database: "ergo_history" history-database: "oragono_history"
timeout: 3s timeout: 3s
max-conns: 4
# this may be necessary to prevent middleware from closing your connections:
#conn-max-lifetime: 180s
# languages config # languages config
languages: languages:
@ -876,9 +708,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
@ -898,7 +727,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):
@ -928,22 +757,13 @@ fakelag:
# sending any commands: # sending any commands:
cooldown: 2s cooldown: 2s
# exempt a certain number of command invocations per session from fakelag;
# this is to speed up "resynchronization" of client state during reattach
command-budgets:
"CHATHISTORY": 16
"MARKREAD": 16
"MONITOR": 1
"WHO": 4
"WEBPUSH": 1
# the roleplay commands are semi-standardized extensions to IRC that allow # the roleplay commands are semi-standardized extensions to IRC that allow
# sending and receiving messages from pseudo-nicknames. this can be used either # sending and receiving messages from pseudo-nicknames. this can be used either
# for actual roleplaying, or for bridging IRC with other protocols. # for actual roleplaying, or for bridging IRC with other protocols.
roleplay: roleplay:
# are roleplay commands enabled at all? (channels and clients still have to # are roleplay commands enabled at all? (channels and clients still have to
# opt in individually with the +E mode) # opt in individually with the +E mode)
enabled: false enabled: true
# require the "roleplay" oper capability to send roleplay messages? # require the "roleplay" oper capability to send roleplay messages?
require-oper: false require-oper: false
@ -954,12 +774,6 @@ roleplay:
# add the real nickname, in parentheses, to the end of every roleplay message? # add the real nickname, in parentheses, to the end of every roleplay message?
add-suffix: true add-suffix: true
# allow customizing the NUH's sent for NPC and SCENE commands
# NPC: the first %s is the NPC name, the second is the user's real nick
#npc-nick-mask: "*%s*!%s@npc.fakeuser.invalid"
# SCENE: the %s is the client's real nick
#scene-nick-mask: "=Scene=!%s@npc.fakeuser.invalid"
# external services can integrate with the ircd using JSON Web Tokens (https://jwt.io). # external services can integrate with the ircd using JSON Web Tokens (https://jwt.io).
# in effect, the server can sign a token attesting that the client is present on # in effect, the server can sign a token attesting that the client is present on
# the server, is a member of a particular channel, etc. # the server, is a member of a particular channel, etc.
@ -1007,7 +821,7 @@ history:
# maximum number of CHATHISTORY messages that can be # maximum number of CHATHISTORY messages that can be
# requested at once (0 disables support for CHATHISTORY) # requested at once (0 disables support for CHATHISTORY)
chathistory-maxmessages: 1000 chathistory-maxmessages: 100
# maximum number of messages that can be replayed at once during znc emulation # maximum number of messages that can be replayed at once during znc emulation
# (znc.in/playback, or automatic replay on initial reattach to a persistent client): # (znc.in/playback, or automatic replay on initial reattach to a persistent client):
@ -1019,26 +833,17 @@ history:
# (and will eventually be deleted from persistent storage, if that's enabled) # (and will eventually be deleted from persistent storage, if that's enabled)
expire-time: 1w expire-time: 1w
# this restricts access to channel history (it can be overridden by channel # if this is set, logged-in users cannot retrieve messages older than their
# owners). options are: 'none' (no restrictions), 'registration-time' # account registration date, and logged-out users cannot retrieve messages
# (logged-in users cannot retrieve messages older than their account # older than their sign-on time (modulo grace-period, see below):
# registration date, and anonymous users cannot retrieve messages older than enforce-registration-date: false
# their sign-on time, modulo the grace-period described below), and
# 'join-time' (users cannot retrieve messages older than the time they
# joined the channel, so only always-on clients can view history).
query-cutoff: 'none'
# if query-cutoff is set to 'registration-time', this allows retrieval # but if this is set, you can retrieve messages that are up to `grace-period`
# of messages that are up to 'grace-period' older than the above cutoff. # older than the above cutoff time. this is recommended to allow logged-out
# if you use 'registration-time', this is recommended to allow logged-out # users to do session resumption / query history after disconnections.
# users to query history after disconnections.
grace-period: 1h grace-period: 1h
# options to store history messages in a persistent database (currently only MySQL). # options to store history messages in a persistent database (currently only MySQL):
# in order to enable any of this functionality, you must configure a MySQL server
# in the `datastore.mysql` section. enabling persistence overrides the history
# size limits above (`channel-length`, `client-length`, etc.); persistent
# history has no limits other than those imposed by expire-time.
persistent: persistent:
enabled: false enabled: false
@ -1060,8 +865,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,
@ -1077,61 +881,9 @@ history:
# if `default` is false, store TAGMSG containing any of these tags: # if `default` is false, store TAGMSG containing any of these tags:
whitelist: whitelist:
- "+draft/react" - "+draft/react"
- "+react" - "react"
# if `default` is true, don't store TAGMSG containing any of these tags: # if `default` is true, don't store TAGMSG containing any of these tags:
#blacklist: #blacklist:
# - "+draft/typing" # - "+draft/typing"
# - "typing" # - "typing"
# 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.
allow-environment-overrides: true
# metadata support for setting key/value data on channels and nicknames.
metadata:
# can clients store metadata?
enabled: true
# how many keys can a client subscribe to?
max-subs: 100
# how many keys can be stored per entity?
max-keys: 100
# experimental support for mobile push notifications
# see the manual for potential security, privacy, and performance implications.
# DO NOT enable if you are running a Tor or I2P hidden service (i.e. one
# with no public IP listeners, only Tor/I2P listeners).
webpush:
# are push notifications enabled at all?
enabled: false
# request timeout for POST'ing the http notification
timeout: 10s
# delay sending the notification for this amount of time, then suppress it
# if the client sent MARKREAD to indicate that it was read on another device
delay: 0s
# subscriber field for the VAPID JWT authorization:
#subscriber: "https://your-website.com/"
# maximum number of push subscriptions per user
max-subscriptions: 4
# expiration time for a push subscription; it must be renewed within this time
# by the client reconnecting to IRC. we also detect whether the client is no longer
# successfully receiving push messages.
expiration: 14d
# HTTP API. we strongly recommend leaving this disabled unless you have a specific
# need for it.
api:
# is the API enabled at all?
enabled: false
# listen address:
listener: "127.0.0.1:8089"
# serve over TLS (strongly recommended if the listener is public):
#tls:
#cert: fullchain.pem
#key: privkey.pem
# one or more static bearer tokens accepted for HTTP bearer authentication.
# these must be strong, unique, high-entropy printable ASCII strings.
# to generate a new token, use `ergo gentoken` or:
# python3 -c "import secrets; print(secrets.token_urlsafe(32))"
bearer-tokens:
- "example"

View File

@ -1,26 +0,0 @@
Created 22/11/2021 by georg@lysergic.dev.
This directory contains Service Management Facility service files for ergo.
These files should be compatible with current OpenSolaris / Illumos based operating systems. Tested on OpenIndiana.
Prerequesites:
- ergo binary located at /opt/ergo/ergo
- ergo configuration located at /opt/ergo/ircd.yaml (hardcoded)
- ergo languages located at /opt/ergo/languages (to be compatible with default.yaml - you may adjust this path or disable languages in your custom ircd.yaml)
- ergo certificate and key located at /opt/ergo/fullchain.pem /opt/ergo/privkey.pem (to be compatible with default.yaml - you may adjust these paths in your custom ircd.yaml)
- `ergo` role user and `ergo` role group owning all of the above
Installation:
- cp ergo.xml /lib/svc/manifest/network/
- cp ergo /lib/svc/method/
- svcadm restart manifest-import
Usage:
- svcadm enable ergo (Start)
- tail /var/svc/log/network-ergo:default.log (Check ergo log and SMF output)
- svcs ergo (Check status)
- svcadm refresh ergo (Reload manifest and ergo configuration)
- svcadm disable ergo (Stop)
Notes:
- Does not support multiple instances - spawns instance :default

View File

@ -1,26 +0,0 @@
#!/sbin/sh
#
# SMF method script for ergo - used by manifest file ergo.xml
# Created 22/11/2021 by georg@lysergic.dev
. /lib/svc/share/smf_include.sh
case $1 in
'start')
exec /opt/ergo/ergo run --conf /opt/ergo/ircd.yaml
;;
'refresh' )
exec pkill -1 -U ergo -x ergo
;;
'stop' )
exec pkill -U ergo -x ergo
;;
*)
echo "Usage: $0 { start | refresh | stop }"
exit 1
;;
esac
exit $?

View File

@ -1,48 +0,0 @@
<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='ergo'>
<service name='network/ergo' type='service' version='0'>
<create_default_instance enabled="true"/>
<single_instance/>
<dependency name='fs-local' grouping='require_all' restart_on='none' type='service'>
<service_fmri value='svc:/system/filesystem/local'/>
</dependency>
<dependency name='fs-autofs' grouping='optional_all' restart_on='none' type='service'>
<service_fmri value='svc:/system/filesystem/autofs'/>
</dependency>
<dependency name='net-loopback' grouping='require_all' restart_on='none' type='service'>
<service_fmri value='svc:/network/loopback'/>
</dependency>
<dependency name='net-physical' grouping='require_all' restart_on='none' type='service'>
<service_fmri value='svc:/network/physical'/>
</dependency>
<dependency name='config_data' grouping='require_all' restart_on='restart' type='path'>
<service_fmri value='file://localhost/opt/ergo/ircd.yaml'/>
</dependency>
<method_context working_directory="/opt/ergo">
<method_credential user='ergo' group='ergo' />
</method_context>
<exec_method name='start' type='method' exec='/lib/svc/method/ergo start' timeout_seconds='20'>
<method_context security_flags='aslr'/>
</exec_method>
<exec_method name='stop' type='method' exec='/lib/svc/method/ergo stop' timeout_seconds='20'/>
<exec_method name='refresh' type='method' exec='/lib/svc/method/ergo refresh' timeout_seconds='20'/>
<property_group name='general' type='framework'>
<propval name='action_authorization' type='astring' value='solaris.smf.manage.ergo'/>
</property_group>
<property_group name='startd' type='framework'>
<propval name='ignore_error' type='astring' value='core,signal'/>
<propval name='duration' type='astring' value='child'/>
</property_group>
<stability value='Unstable'/>
<template>
<common_name>
<loctext xml:lang='C'>IRC server</loctext>
</common_name>
<documentation>
<doc_link name='ergo-manual' uri='https://github.com/ergochat/ergo/blob/master/docs/MANUAL.md'/>
<doc_link name='ergo-userguide' uri='https://github.com/ergochat/ergo/blob/master/docs/USERGUIDE.md'/>
</documentation>
</template>
</service>
</service_bundle>

View File

@ -1,206 +0,0 @@
#!/usr/bin/python3
import binascii
import json
import logging
import re
import sys
from collections import defaultdict, namedtuple
AnopeObject = namedtuple('AnopeObject', ('type', 'kv'))
MASK_MAGIC_REGEX = re.compile(r'[*?!@]')
def access_level_to_amode(level):
# https://wiki.anope.org/index.php/2.0/Modules/cs_xop
if level == 'QOP':
return 'q'
elif level == 'SOP':
return 'a'
elif level == 'AOP':
return 'o'
elif level == 'HOP':
return 'h'
elif level == 'VOP':
return 'v'
try:
level = int(level)
except:
return None
if level >= 10000:
return 'q'
elif level >= 9999:
return 'a'
elif level >= 5:
return 'o'
elif level >= 4:
return 'h'
elif level >= 3:
return 'v'
else:
return None
def to_unixnano(timestamp):
return int(timestamp) * (10**9)
def file_to_objects(infile):
result = []
obj = None
while True:
line = infile.readline()
if not line:
break
line = line.rstrip(b'\r\n')
try:
line = line.decode('utf-8')
except UnicodeDecodeError:
line = line.decode('utf-8', 'replace')
logging.warning("line contained invalid utf8 data " + line)
pieces = line.split(' ', maxsplit=2)
if len(pieces) == 0:
logging.warning("skipping blank line in db")
continue
if pieces[0] == 'END':
result.append(obj)
obj = None
elif pieces[0] == 'OBJECT':
obj = AnopeObject(pieces[1], {})
elif pieces[0] == 'DATA':
obj.kv[pieces[1]] = pieces[2]
elif pieces[0] == 'ID':
# not sure what these do?
continue
else:
raise ValueError("unknown command found in anope db", pieces[0])
return result
ANOPE_MODENAME_TO_MODE = {
'NOEXTERNAL': 'n',
'TOPIC': 't',
'INVITE': 'i',
'NOCTCP': 'C',
'AUDITORIUM': 'u',
'SECRET': 's',
}
# verify that a certfp appears to be a hex-encoded SHA-256 fingerprint;
# if it's anything else, silently ignore it
def validate_certfps(certobj):
certfps = []
for fingerprint in certobj.split():
try:
dec = binascii.unhexlify(fingerprint)
except:
continue
if len(dec) == 32:
certfps.append(fingerprint)
return certfps
def convert(infile):
out = {
'version': 1,
'source': 'anope',
'users': defaultdict(dict),
'channels': defaultdict(dict),
}
objects = file_to_objects(infile)
lastmode_channels = set()
for obj in objects:
if obj.type == 'NickCore':
username = obj.kv['display']
userdata = {'name': username, 'hash': obj.kv['pass'], 'email': obj.kv['email']}
certobj = obj.kv.get('cert')
if certobj:
userdata['certfps'] = validate_certfps(certobj)
out['users'][username] = userdata
elif obj.type == 'NickAlias':
username = obj.kv['nc']
nick = obj.kv['nick']
userdata = out['users'][username]
if username.lower() == nick.lower():
userdata['registeredAt'] = to_unixnano(obj.kv['time_registered'])
else:
if 'additionalNicks' not in userdata:
userdata['additionalNicks'] = []
userdata['additionalNicks'].append(nick)
elif obj.type == 'ChannelInfo':
chname = obj.kv['name']
founder = obj.kv['founder']
chdata = {
'name': chname,
'founder': founder,
'registeredAt': to_unixnano(obj.kv['time_registered']),
'topic': obj.kv['last_topic'],
'topicSetBy': obj.kv['last_topic_setter'],
'topicSetAt': to_unixnano(obj.kv['last_topic_time']),
'amode': {founder: 'q',}
}
# DATA last_modes INVITE KEY,hunter2 NOEXTERNAL REGISTERED TOPIC
last_modes = obj.kv.get('last_modes')
if last_modes:
modes = []
for mode_desc in last_modes.split():
if ',' in mode_desc:
mode_name, mode_value = mode_desc.split(',', maxsplit=1)
else:
mode_name, mode_value = mode_desc, None
if mode_name == 'KEY':
chdata['key'] = mode_value
else:
modes.append(ANOPE_MODENAME_TO_MODE.get(mode_name, ''))
chdata['modes'] = ''.join(modes)
# prevent subsequent ModeLock objects from modifying the mode list further:
lastmode_channels.add(chname)
out['channels'][chname] = chdata
elif obj.type == 'ModeLock':
if obj.kv.get('set') != '1':
continue
chname = obj.kv['ci']
if chname in lastmode_channels:
continue
chdata = out['channels'][chname]
modename = obj.kv['name']
if modename == 'KEY':
chdata['key'] = obj.kv['param']
else:
oragono_mode = ANOPE_MODENAME_TO_MODE.get(modename)
if oragono_mode is not None:
stored_modes = chdata.get('modes', '')
stored_modes += oragono_mode
chdata['modes'] = stored_modes
elif obj.type == 'ChanAccess':
chname = obj.kv['ci']
target = obj.kv['mask']
mode = access_level_to_amode(obj.kv['data'])
if mode is None:
continue
if MASK_MAGIC_REGEX.search(target):
continue
chdata = out['channels'][chname]
amode = chdata.setdefault('amode', {})
amode[target] = mode
chdata['amode'] = amode
# do some basic integrity checks
for chname, chdata in out['channels'].items():
founder = chdata.get('founder')
if founder not in out['users']:
raise ValueError("no user corresponding to channel founder", chname, chdata.get('founder'))
return out
def main():
if len(sys.argv) != 3:
raise Exception("Usage: anope2json.py anope.db output.json")
with open(sys.argv[1], 'rb') as infile:
output = convert(infile)
with open(sys.argv[2], 'w') as outfile:
json.dump(output, outfile)
if __name__ == '__main__':
logging.basicConfig()
sys.exit(main())

View File

@ -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>
}

View File

@ -1,209 +0,0 @@
#!/usr/bin/python3
import binascii
import json
import logging
import re
import sys
from collections import defaultdict
MASK_MAGIC_REGEX = re.compile(r'[*?!@$]')
def to_unixnano(timestamp):
return int(timestamp) * (10**9)
# include/atheme/channels.h
CMODE_FLAG_TO_MODE = {
0x001: 'i', # CMODE_INVITE
0x010: 'n', # CMODE_NOEXT
0x080: 's', # CMODE_SEC
0x100: 't', # CMODE_TOPIC
}
# attempt to interpret certfp as a hex-encoded SHA-256 fingerprint
def validate_certfp(certfp):
try:
dec = binascii.unhexlify(certfp)
except:
return False
return len(dec) == 32
def convert(infile):
out = {
'version': 1,
'source': 'atheme',
'users': defaultdict(dict),
'channels': defaultdict(dict),
}
group_to_founders = defaultdict(list)
channel_to_founder = defaultdict(lambda: (None, None))
while True:
line = infile.readline()
if not line:
break
line = line.rstrip(b'\r\n')
try:
line = line.decode('utf-8')
except UnicodeDecodeError:
line = line.decode('utf-8', 'replace')
logging.warning("line contained invalid utf8 data " + line)
parts = line.split(' ')
category = parts[0]
if category == 'GACL':
# Note: all group definitions precede channel access entries (token CA) by design, so it
# should be safe to read this in using one pass.
groupname = parts[1]
user = parts[2]
flags = parts[3]
if 'F' in flags:
group_to_founders[groupname].append(user)
elif category == 'MU':
# user account
# MU AAAAAAAAB shivaram $1$hcspif$nCm4r3S14Me9ifsOPGuJT. user@example.com 1600134392 1600467343 +sC default
name = parts[2]
user = {'name': name, 'hash': parts[3], 'email': parts[4], 'registeredAt': to_unixnano(parts[5])}
out['users'][name].update(user)
pass
elif category == 'MN':
# grouped nick
# MN shivaram slingamn 1600218831 1600467343
username, groupednick = parts[1], parts[2]
if username != groupednick:
user = out['users'][username]
user.setdefault('additionalnicks', []).append(groupednick)
elif category == 'MDU':
if parts[2] == 'private:usercloak':
username = parts[1]
out['users'][username]['vhost'] = parts[3]
elif category == 'MCFP':
username, certfp = parts[1], parts[2]
if validate_certfp(certfp):
user = out['users'][username]
user.setdefault('certfps', []).append(certfp.lower())
elif category == 'MC':
# channel registration
# MC #mychannel 1600134478 1600467343 +v 272 0 0
# MC #NEWCHANNELTEST 1602270889 1602270974 +vg 1 0 0 jaeger4
chname = parts[1]
chdata = out['channels'][chname]
# XXX just give everyone +nt, regardless of lock status; they can fix it later
chdata.update({'name': chname, 'registeredAt': to_unixnano(parts[2])})
if parts[8] != '':
chdata['key'] = parts[8]
modes = {'n', 't'}
mlock_on, mlock_off = int(parts[5]), int(parts[6])
for flag, mode in CMODE_FLAG_TO_MODE.items():
if flag & mlock_on != 0:
modes.add(mode)
elif flag & mlock_off != 0 and mode in modes:
modes.remove(mode)
chdata['modes'] = ''.join(sorted(modes))
chdata['limit'] = int(parts[7])
elif category == 'MDC':
# auxiliary data for a channel registration
# MDC #mychannel private:topic:setter s
# MDC #mychannel private:topic:text hi again
# MDC #mychannel private:topic:ts 1600135864
chname = parts[1]
category = parts[2]
if category == 'private:topic:text':
out['channels'][chname]['topic'] = line.split(maxsplit=3)[3]
elif category == 'private:topic:setter':
out['channels'][chname]['topicSetBy'] = parts[3]
elif category == 'private:topic:ts':
out['channels'][chname]['topicSetAt'] = to_unixnano(parts[3])
elif category == 'private:mlockext':
# the channel forward mode is +L on insp/unreal, +f on charybdis
# charybdis has a +L ("large banlist") taking no argument
# and unreal has a +f ("flood limit") taking two colon-delimited numbers,
# so check for an argument that starts with a #
if parts[3].startswith('L#') or parts[3].startswith('f#'):
out['channels'][chname]['forward'] = parts[3][1:]
elif category == 'CA':
# channel access lists
# CA #mychannel shivaram +AFORafhioqrstv 1600134478 shivaram
chname, username, flags, set_at = parts[1], parts[2], parts[3], int(parts[4])
chname = parts[1]
chdata = out['channels'][chname]
flags = parts[3]
set_at = int(parts[4])
if 'amode' not in chdata:
chdata['amode'] = {}
# see libathemecore/flags.c: +o is op, +O is autoop, etc.
if 'F' in flags:
# If the username starts with "!", it's actually a GroupServ group.
if username.startswith('!'):
group_founders = group_to_founders.get(username)
if not group_founders:
# skip this and warn about it later
continue
# attempt to promote the first group founder to channel founder
username = group_founders[0]
# but everyone gets the +q flag
for founder in group_founders:
chdata['amode'][founder] = 'q'
# there can only be one founder
preexisting_founder, preexisting_set_at = channel_to_founder[chname]
if preexisting_founder is None or set_at < preexisting_set_at:
chdata['founder'] = username
channel_to_founder[chname] = (username, set_at)
# but multiple people can receive the 'q' amode
chdata['amode'][username] = 'q'
continue
if MASK_MAGIC_REGEX.search(username):
# ignore groups, masks, etc. for any field other than founder
continue
# record the first appearing successor, if necessary
if 'S' in flags:
if not chdata.get('successor'):
chdata['successor'] = username
# finally, handle amodes
if 'q' in flags:
chdata['amode'][username] = 'q'
elif 'a' in flags:
chdata['amode'][username] = 'a'
elif 'o' in flags or 'O' in flags:
chdata['amode'][username] = 'o'
elif 'h' in flags or 'H' in flags:
chdata['amode'][username] = 'h'
elif 'v' in flags or 'V' in flags:
chdata['amode'][username] = 'v'
else:
pass
# do some basic integrity checks
def validate_user(name):
if not name:
return False
return bool(out['users'].get(name))
invalid_channels = []
for chname, chdata in out['channels'].items():
if not validate_user(chdata.get('founder')):
if validate_user(chdata.get('successor')):
chdata['founder'] = chdata['successor']
else:
invalid_channels.append(chname)
for chname in invalid_channels:
logging.warning("Unable to find a valid founder for channel %s, discarding it", chname)
del out['channels'][chname]
return out
def main():
if len(sys.argv) != 3:
raise Exception("Usage: atheme2json.py atheme_db output.json")
with open(sys.argv[1], 'rb') as infile:
output = convert(infile)
with open(sys.argv[2], 'w') as outfile:
json.dump(output, outfile)
if __name__ == '__main__':
logging.basicConfig()
sys.exit(main())

View File

@ -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.

View File

@ -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"

View File

@ -1,35 +1,33 @@
# Ergo Docker # Oragono Docker
This folder holds Ergo's Docker compose file. The Dockerfile is in the root This folder holds Oragono's Dockerfile and related materials. Oragono
directory. Ergo is published automatically to the GitHub Container Registry at is published automatically to Docker Hub at
[ghcr.io/ergochat/ergo](https://ghcr.io/ergochat/ergo). [oragono/oragono](https://hub.docker.com/r/oragono/oragono).
Most users should use either the `stable` tag (corresponding to the The `latest` tag tracks the `stable` branch of Oragono, which contains
`stable` branch in git, which tracks the latest stable release), or the latest stable release. The `dev` tag tracks the master branch, which
a tag corresponding to a tagged version (e.g. `v2.8.0`). The `master` may by unstable and is not recommended for production.
tag corresponds to the `master` branch, which is not recommended for
production use. The `latest` tag is not recommended.
## Quick start ## Quick start
The Ergo docker image is designed to work out of the box - it comes with a The Oragono docker image is designed to work out of the box - it comes with a
usable default config and will automatically generate self-signed TLS usable default config and will automatically generate self-signed TLS
certificates. To get a working ircd, all you need to do is run the image and 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 oragono -d -p 6667:6667 -p 6697:6697 oragono/oragono:tag
``` ```
This will start Ergo and listen on ports 6667 (plain text) and 6697 (TLS). This will start Oragono and listen on ports 6667 (plain text) and 6697 (TLS).
The first time Ergo runs it will create a config file with a randomised The first time Oragono runs it will create a config file with a randomised
oper password. This is output to stdout, and you can view it with the docker oper password. This is output to stdout, and you can view it with the docker
logs command: logs command:
```shell ```shell
# Assuming your container is named `ergo`; use `docker container ls` to # Assuming your container is named `oragono`; use `docker container ls` to
# find the name if you're not sure. # find the name if you're not sure.
docker logs ergo docker logs oragono
``` ```
You should see a line similar to: You should see a line similar to:
@ -38,74 +36,69 @@ 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 Oragono has a persistent data store, used to keep account details, channel
registrations, and so on. To persist this data across restarts, you can mount registrations, and so on. To persist this data across restarts, you can mount
a volume at /ircd. a volume at /ircd.
For example, to create a new docker volume and then mount it: For example, to create a new docker volume and then mount it:
```shell ```shell
docker volume create ergo-data docker volume create oragono-data
docker run --init --name ergo -d -v ergo-data:/ircd -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable docker run -d -v oragono-data:/ircd -p 6667:6667 -p 6697:6697 oragono/oragono:tag
``` ```
Or to mount a folder from your host machine: Or to mount a folder from your host machine:
```shell ```shell
mkdir ergo-data mkdir oragono-data
docker run --init --name ergo -d -v $(pwd)/ergo-data:/ircd -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable docker run -d -v $(PWD)/oragono-data:/ircd -p 6667:6667 -p 6697:6697 oragono/oragono:tag
``` ```
## Customising the config ## Customising the config
Ergo's config file is stored at /ircd/ircd.yaml. If the file does not Oragono's config file is stored at /ircd/ircd.yaml. If the file does not
exist, the default config will be written out. You can copy the config from exist, the default config will be written out. You can copy the config from
the container, edit it, and then copy it back: the container, edit it, and then copy it back:
```shell ```shell
# Assuming that your container is named `ergo`, as above. # Assuming that your container is named `oragono`, as above.
docker cp ergo:/ircd/ircd.yaml . docker cp oragono:/ircd/ircd.yaml .
vim ircd.yaml # edit the config to your liking vim ircd.yaml # edit the config to your liking
docker cp ircd.yaml ergo:/ircd/ircd.yaml docker cp ircd.yaml oragono:/ircd/ircd.yaml
``` ```
You can use the `/rehash` command to make Ergo reload its config, or You can use the `/rehash` command to make Oragono reload its config, or
send it the HUP signal: send it the HUP signal:
```shell ```shell
docker kill -s SIGHUP ergo docker kill -HUP oragono
``` ```
## Using custom TLS certificates ## Using custom TLS certificates
TLS certs will by default be read from /ircd/fullchain.pem, with a private key TLS certs will by default be read from /ircd/tls.crt, with a private key
in /ircd/privkey.pem. You can customise this path in the ircd.yaml file if in /ircd/tls.key. You can customise this path in the ircd.yaml file if
you wish to mount the certificates from another volume. For information you wish to mount the certificates from another volume. For information
on using Let's Encrypt certificates, see on using Let's Encrypt certificates, see
[this manual entry](https://github.com/ergochat/ergo/blob/master/docs/MANUAL.md#using-valid-tls-certificates). [this manual entry](https://github.com/oragono/oragono/blob/master/docs/MANUAL.md#how-do-i-use-lets-encrypt-certificates).
## Using docker-compose ## Using docker-compose
This folder contains a sample docker-compose file which can be used This folder contains a sample docker-compose file which can be used
to start an Ergo instance with ports exposed and data persisted in to start an Oragono instance with ports exposed and data persisted in
a docker volume. Simply download the file and then bring it up: a docker volume. Simply download the file and then bring it up:
```shell ```shell
curl -O https://raw.githubusercontent.com/ergochat/ergo/master/distrib/docker/docker-compose.yml curl -O https://raw.githubusercontent.com/oragono/oragono/master/distrib/docker/docker-compose.yml
docker-compose up -d docker-compose up -d
``` ```
## Building ## Building
If you wish to manually build the docker image, you need to do so from If you wish to manually build the docker image, you need to do so from
the root of the Ergo repository (not the `distrib/docker` directory): the root of the Oragono repository (not the `distrib/docker` directory):
```shell ```shell
docker build . docker build .

View File

@ -1,9 +1,8 @@
version: "3.8" version: "3.2"
services: services:
ergo: oragono:
init: true image: oragono/oragono:latest
image: ghcr.io/ergochat/ergo:stable
ports: ports:
- "6667:6667/tcp" - "6667:6667/tcp"
- "6697:6697/tcp" - "6697:6697/tcp"

9
distrib/docker/run.sh Executable file → Normal file
View File

@ -1,5 +1,8 @@
#!/bin/sh #!/bin/sh
# start in right dir
cd /ircd
# make config file # make config file
if [ ! -f "/ircd/ircd.yaml" ]; then if [ ! -f "/ircd/ircd.yaml" ]; then
awk '{gsub(/path: languages/,"path: /ircd-bin/languages")}1' /ircd-bin/default.yaml > /tmp/ircd.yaml awk '{gsub(/path: languages/,"path: /ircd-bin/languages")}1' /ircd-bin/default.yaml > /tmp/ircd.yaml
@ -7,7 +10,7 @@ if [ ! -f "/ircd/ircd.yaml" ]; then
# change default oper passwd # change default oper passwd
OPERPASS=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c20) OPERPASS=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c20)
echo "Oper username:password is admin:$OPERPASS" echo "Oper username:password is admin:$OPERPASS"
ENCRYPTEDPASS=$(echo "$OPERPASS" | /ircd-bin/ergo genpasswd) ENCRYPTEDPASS=$(echo "$OPERPASS" | /ircd-bin/oragono genpasswd)
ORIGINALPASS='\$2a\$04\$0123456789abcdef0123456789abcdef0123456789abcdef01234' ORIGINALPASS='\$2a\$04\$0123456789abcdef0123456789abcdef0123456789abcdef01234'
awk "{gsub(/password: \\\"$ORIGINALPASS\\\"/,\"password: \\\"$ENCRYPTEDPASS\\\"\")}1" /tmp/ircd.yaml > /tmp/ircd2.yaml awk "{gsub(/password: \\\"$ORIGINALPASS\\\"/,\"password: \\\"$ENCRYPTEDPASS\\\"\")}1" /tmp/ircd.yaml > /tmp/ircd2.yaml
@ -20,7 +23,7 @@ if [ ! -f "/ircd/ircd.yaml" ]; then
fi fi
# make self-signed certs if they don't already exist # make self-signed certs if they don't already exist
/ircd-bin/ergo mkcerts /ircd-bin/oragono mkcerts
# run! # run!
exec /ircd-bin/ergo run exec /ircd-bin/oragono run

View File

@ -1,57 +0,0 @@
#!/bin/sh
# Init script for the ergo IRCd
# Created 14/06/2021 by georg@lysergic.dev
# Desgigned for and tested on Slackware -current
# Depends on `daemon` (installable using slackpkg)
# In its stock configuration ergo will be jailed to /opt/ergo - all paths are relative from there. Consider this in your ergo configuration file (i.e. certificate, database and log locations)
NAME=ergo
DIR=/opt/ergo
ERGO=/ergo
DAEMONIZER=/usr/bin/daemon
CONFIG=ircd.yaml
USER=ergo
GROUP=ergo
daemon_start() {
$DAEMONIZER -n $NAME -v -- chroot --userspec=$USER --groups=$USER -- $DIR $ERGO run --conf $CONFIG
}
daemon_stop() {
$DAEMONIZER --stop -n $NAME -v
}
daemon_restart() {
$DAEMONIZER --restart -n $NAME -v
}
daemon_reload() {
$DAEMONIZER --signal=SIGHUP -n $NAME -v
}
daemon_status() {
$DAEMONIZER --running -n $NAME -v
}
case "$1" in
start)
daemon_start
;;
stop)
daemon_stop
;;
restart)
daemon_restart
;;
reload)
daemon_reload
;;
status)
daemon_status
;;
*)
echo "Source: https://github.com/ergochat/ergo"
echo "Usage: $0 {start|stop|restart|reload|status}"
exit 1
esac

View File

@ -1,3 +0,0 @@
# /etc/conf.d/ergo: config file for /etc/init.d/ergo
ERGO_CONFIGFILE="/etc/ergo/ircd.yaml"
ERGO_USERNAME="ergo"

View File

@ -1,32 +0,0 @@
#!/sbin/openrc-run
name=${RC_SVCNAME}
description="ergo IRC daemon"
command=/usr/bin/ergo
command_args="run --conf ${ERGO_CONFIGFILE:-'/etc/ergo/ircd.yaml'}"
command_user=${ERGO_USERNAME:-ergo}
command_background=true
pidfile=/var/run/${RC_SVCNAME}.pid
output_log="/var/log/${RC_SVCNAME}.out"
error_log="/var/log/${RC_SVCNAME}.err"
# --wait: to wait 1 second after launching to see if it survived startup
start_stop_daemon_args="--wait 1000"
extra_started_commands="reload"
depend() {
use dns
provide ircd
}
start_pre() {
checkpath --owner ${command_user}:${command_user} --mode 0640 --file /var/log/${RC_SVCNAME}.out /var/log/${RC_SVCNAME}.err
}
reload() {
ebegin "Reloading ${RC_SVCNAME}"
start-stop-daemon --signal HUP --pidfile "${pidfile}"
eend $?
}

View File

@ -1,8 +0,0 @@
This directory contains s6 srv and log services for ergo.
These services expect that ergo is installed to /opt/ergo,
and an ergo system user that owns /opt/ergo.
To install:
cp -r ergo-srv ergo-log /etc/s6/sv/
cp ergo.conf /etc/s6/config/

View File

@ -1 +0,0 @@
ergo-srv

View File

@ -1 +0,0 @@
3

View File

@ -1 +0,0 @@
ergo

View File

@ -1,9 +0,0 @@
#!/usr/bin/execlineb -P
envfile /etc/s6/config/ergo.conf
importas -sCiu DIRECTIVES DIRECTIVES
ifelse { test -w /var/log } {
foreground { install -d -o s6log -g s6log /var/log/ergo }
s6-setuidgid s6log exec -c s6-log -d3 -b -- ${DIRECTIVES} /var/log/ergo
}
foreground { install -d -o s6log -g s6log /run/log/ergo }
s6-setuidgid s6log exec -c s6-log -d3 -b -- ${DIRECTIVES} /run/log/ergo

View File

@ -1 +0,0 @@
longrun

View File

@ -1 +0,0 @@
ergo-log

View File

@ -1,4 +0,0 @@
#!/usr/bin/execlineb -P
fdmove -c 2 1
execline-cd /opt/ergo
s6-setuidgid ergo ./ergo run

View File

@ -1 +0,0 @@
longrun

View File

@ -1,2 +0,0 @@
# This configures the directives used for s6-log in the log service.
DIRECTIVES="n3 s2000000"

View File

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=ergo Description=oragono
After=network.target After=network.target
# If you are using MySQL for history storage, comment out the above line # If you are using MySQL for history storage, comment out the above line
# and uncomment these two instead (you must independently install and configure # and uncomment these two instead (you must independently install and configure
@ -8,16 +8,13 @@ After=network.target
# After=network.target mysql.service # After=network.target mysql.service
[Service] [Service]
Type=notify Type=simple
User=ergo User=oragono
WorkingDirectory=/home/ergo WorkingDirectory=/home/oragono
ExecStart=/home/ergo/ergo run --conf /home/ergo/ircd.yaml ExecStart=/home/oragono/oragono run --conf /home/oragono/ircd.yaml
ExecReload=/bin/kill -HUP $MAINPID ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure Restart=on-failure
LimitNOFILE=1048576 LimitNOFILE=1048576
NotifyAccess=main
# Uncomment this for a hidden service:
# PrivateNetwork=true
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -1,124 +0,0 @@
__ __ ______ ___ ______ ___
__/ // /_/ ____/ __ \/ ____/ __ \
/_ // __/ __/ / /_/ / / __/ / / /
/_ // __/ /___/ _, _/ /_/ / /_/ /
/_//_/ /_____/_/ |_|\____/\____/
Ergo IRCd API Documentation
https://ergo.chat/
_Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn@cs.stanford.edu>_
--------------------------------------------------------------------------------------------
Ergo has an experimental HTTP API. Some general information about the API:
1. All requests to the API are via POST.
1. All requests to the API are authenticated via bearer authentication. This is a header named `Authorization` with the value `Bearer <token>`. A list of valid tokens is hardcoded in the Ergo config. Future versions of Ergo may allow additional validation schemes for tokens.
1. The request parameters are sent as JSON in the POST body.
1. Any status code other than 200 is an error response; the response body is undefined in this case (likely human-readable text for debugging).
1. A 200 status code indicates successful execution of the request. The response body will be JSON and may indicate application-level success or failure (typically via the `success` field, which takes a boolean value).
API endpoints are versioned (currently all endpoints have a `/v1/` path prefix). Backwards-incompatible updates will most likely take the form of endpoints with new names, or an increased version prefix. Any exceptions to this will be specifically documented in the changelog.
All API endpoints should be considered highly privileged. Bearer tokens should be kept secret. Access to the API should be either over a trusted link (like loopback) or secured via verified TLS. See the `api` section of `default.yaml` for examples of how to configure this.
Here's an example of how to test an API configured to run over loopback TCP in plaintext:
```bash
curl -d '{"accountName": "invalidaccountname", "passphrase": "invalidpassphrase"}' -H 'Authorization: Bearer EYBbXVilnumTtfn4A9HE8_TiKLGWEGylre7FG6gEww0' -v http://127.0.0.1:8089/v1/check_auth
```
This returns:
```json
{"success":false}
```
Endpoints
=========
`/v1/account_details`
----------------
This endpoint fetches account details and returns them as JSON. The request is a JSON object with fields:
* `accountName`: string, name of the account
The response is a JSON object with fields:
* `success`: whether the account exists or not
* `accountName`: canonical, case-unfolded version of the account name
* `email`: email address of the account provided
* `registeredAt`: string, registration date/time of the account (in ISO8601 format)
* `channels`: array of strings, list of channels the account is registered on or associated with
`/v1/check_auth`
----------------
This endpoint verifies the credentials of a NickServ account; this allows Ergo to be used as the source of truth for authentication by another system. The request is a JSON object with fields:
* `accountName`: string, name of the account
* `passphrase`: string, alleged passphrase of the account
The response is a JSON object with fields:
* `success`: whether the credentials provided were valid
* `accountName`: canonical, case-unfolded version of the account name
`/v1/rehash`
------------
This endpoint rehashes the server (i.e. reloads the configuration file, TLS certificates, and other associated data). The body is ignored. The response is a JSON object with fields:
* `success`: boolean, indicates whether the rehash was successful
* `error`: string, optional, human-readable description of the failure
`/v1/saregister`
----------------
This endpoint registers an account in NickServ, with the same semantics as `NS SAREGISTER`. The request is a JSON object with fields:
* `accountName`: string, name of the account
* `passphrase`: string, passphrase of the account
The response is a JSON object with fields:
* `success`: whether the account creation succeeded
* `errorCode`: string, optional, machine-readable description of the error. Possible values include: `ACCOUNT_EXISTS`, `INVALID_PASSPHRASE`, `UNKNOWN_ERROR`.
* `error`: string, optional, human-readable description of the failure.
`/v1/account_list`
-------------------
This endpoint fetches a list of all accounts. The request body is ignored and can be empty.
The response is a JSON object with fields:
* `success`: whether the request succeeded
* `accounts`: array of objects, each with fields:
* `success`: boolean, whether this individual account query succeeded
* `accountName`: string, canonical, case-unfolded version of the account name
* `totalCount`: integer, total number of accounts returned
`/v1/status`
-------------
This endpoint returns status information about the running Ergo server. The request body is ignored and can be empty.
The response is a JSON object with fields:
* `success`: whether the request succeeded
* `version`: string, Ergo server version string
* `go_version`: string, version of Go runtime used
* `start_time`: string, server start time in ISO8601 format
* `users`: object with fields:
* `total`: total number of users connected
* `invisible`: number of invisible users
* `operators`: number of operators connected
* `unknown`: number of users with unknown status
* `max`: maximum number of users seen connected at once
* `channels`: integer, number of channels currently active
* `servers`: integer, number of servers connected in the network

92
docs/INFO.md Normal file
View File

@ -0,0 +1,92 @@
# Oragono Information
Here's a bunch of misc info about the Oragono server! This can include questions, plans on
how I'm going forward, how to properly use features, or why Oragono does/doesn't do
something.
Essentially, this document acts as a braindump about Oragono while we figure out a better
place to put all this information.
## Accounts and Channels
Most IRC servers out there offer IRC account and channel registration through external
services such as NickServ and ChanServ. In Oragono, we bundle accounts and channel ownership
in as a native server feature instead!
Because there's a lot of aspects of accounts/channels that haven't been specified as native
commands and all yet, Oragono includes the pseudo-clients NickServ and ChanServ to roughly
mimic the functionality that other IRCds get from services packages, in a user-facing set
of commands that's familiar to everyone.
The plan is to move more features and functionality (such as channel registration, channel
permissions and all) over to native commands first and to use the NickServ/ChanServ as
legacy interfaces to access these functions. However, it's gonna be a while before all of
this is specified by someone like the IRCv3 WG.
## PROXY
The PROXY command, specified by [HAProxy's PROXY v1 specifications](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt),
allows someone to setup HAProxy in front of Oragono. This allows them to use HAProxy for
TLS negotiation (allowing older versions of SSL/TLS than Go's inbuilt TLS support does).
However, it also allows them to update TLS certificates by updating them with HAProxy,
rather than relying on our `REHASH` command (which is less-well-tested than I'd like
right now).
This is a toss-up of course  allowing older versions of TLS might be seen as undesired,
and I wouldn't use the feature myself, but it's useful for real-world installations which
is why it exists. The command is only allowed from specific hosts which should restrict it
appropriately.
## Server-to-Server Linking (or Federation)
Right now Oragono doesn't support linking multiple servers together. It's certainly planned,
but it's a fair while away.
When I do add S2S linking to Oragono, I want to use it as a testbed for a new sort of
linking protocol. Mostly, I want a meshy protocol that minimises the effects of netsplits
while still ensuring that messages get delivered, and preserves the AP nature of IRC
reliability (in terms of the CAP theorem), which is something that traditional solutions
based on the Raft protocol don't do.
Basically, I'm going to continue working on my [DCMI](https://github.com/DanielOaks/dcmi)
protocol, get that to a point where I'm happy with it and _then_ start looking at S2S
linking properly. If anyone is interested in server protocols and wants to look at this with
me, please feel free to reach out!
## Rehashing
Rehashing is reloading the config files and TLS certificates. Of course, you can rehash the
server by connect, opering-up and using the `/REHASH` command. However, similar to other
IRCds, you can also make the server rehash by sending an appropriate signal to it!
To make the server rehash from the command line, send it a `SIGHUP` signal. In *nix and OSX,
you can do this by performing the following command:
killall -HUP oragono
This will make the server rehash its configuration files and TLS certificates, and so can be
useful if you're automatically updating your TLS certs!
## Rejected Features
'Rejected' sounds harsh, but basically these are features I've decided I'm not gonna
implement in Oragono (at least, not until someone convinces me they're worth doing).
### Force/Auto-Join Channels on Connect
When a user connects, some IRC servers let you force-join them to a given channel. For
instance, this could be a channel like `#coolnet` for a network named CoolNet, a lobby
channel, or something similar.
My main objection to having this feature is just that I don't like it that much. It doesn't
seem nice to forcibly join clients to a channel, and I know I'm always annoyed when networks
do it to me.
To network operators that want to do this, I'd suggest instead mentioning the channel(s) in
your MOTD so that your users know the channels exist! If they want to join in, they can do
it from there :)

File diff suppressed because it is too large Load Diff

View File

@ -1,128 +0,0 @@
__ __ ______ ___ ______ ___
__/ // /_/ ____/ __ \/ ____/ __ \
/_ // __/ __/ / /_/ / / __/ / / /
/_ // __/ /___/ _, _/ /_/ / /_/ /
/_//_/ /_____/_/ |_|\____/\____/
Ergo IRCd User Guide
https://ergo.chat/
_Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn@cs.stanford.edu>_
--------------------------------------------------------------------------------------------
Table of Contents
- [Introduction](#introduction)
- [About IRC](#about-irc)
- [How Ergo is different](#how-ergo-is-different)
- [Account registration](#account-registration)
- [Channel registration](#channel-registration)
- [Always-on](#always-on)
- [Multiclient](#multiclient)
- [History](#history)
- [Push notifications](#push-notifications)
--------------------------------------------------------------------------------------------
# Introduction
Welcome to Ergo, a modern IRC server!
This guide is for end users of Ergo (people using Ergo to chat). If you're installing your own Ergo instance, you should consult the official manual instead (a copy should be bundled with your release, in the `docs/` directory).
This guide assumes that Ergo is in its default or recommended configuration; Ergo server administrators can change settings to make the server behave differently. If something isn't working as expected, ask your server administrator for help.
# About IRC
Before continuing, you should be familiar with basic features of the IRC platform. If you're comfortable with IRC, you can skip this section.
[IRC](https://en.wikipedia.org/wiki/Internet_Relay_Chat) is a chat platform invented in 1988, which makes it older than the World Wide Web! At its most basic level, IRC is a chat system composed of chat rooms; these are called "channels" and their names begin with a `#` character (this is actually the origin of the [hashtag](https://www.cmu.edu/homepage/computing/2014/summer/originstory.shtml)!). As a user, you "join" the channels you're interested in, enabling you to participate in those discussions.
Here are some guides covering the basics of IRC:
* [Fedora Magazine: Beginner's Guide to IRC](https://fedoramagazine.org/beginners-guide-irc/)
* [IRCHelp's IRC Tutorial](https://www.irchelp.org/faq/irctutorial.html) (in particular, section 3, "Beyond the Basics")
# How Ergo is different
Ergo differs in many ways from conventional IRC servers. If you're *not* familiar with other IRC servers, you may want to skip this section. Here are some of the most salient differences:
* Ergo integrates a "bouncer" into the server. In particular:
* Ergo stores message history for later retrieval.
* You can be "present" on the server (joined to channels, able to receive DMs) without having an active client connection to the server.
* Conversely, you can use multiple clients to view / control the same presence (nickname) on the server, as long as you authenticate with SASL when connecting.
* Ergo integrates "services" into the server. In particular:
* Nicknames are strictly reserved: once you've registered your nickname, you must log in in order to use it. Consequently, SASL is more important when using Ergo than in other systems.
* All properties of registered channels are protected without the need for `ChanServ` to be joined to the channel.
* Ergo "cloaks", i.e., cryptographically scrambles, end user IPs so that they are not displayed publicly.
* By default, the user/ident field is inoperative in Ergo: it is always set to `~u`, regardless of the `USER` command or the client's support for identd. This is because it is not in general a reliable or trustworthy way to distinguish users coming from the same IP. Ergo's integrated bouncer features should reduce the need for shared shell hosts and hosted bouncers (one of the main remaining use cases for identd).
* By default, Ergo is only accessible via TLS.
# Account registration
Although (as in other IRC systems) basic chat functionality is available without creating an account, most of Ergo's features require an account. You can create an account by sending a direct message to `NickServ`. (In IRC jargon, `NickServ` is a "network service", but if you're not familiar with the concept you can just think of it as a bot or a text user interface.) In a typical client, this will be:
```
/msg NickServ register mySecretPassword validEmailAddress@example.com
```
This registers your current nickname as your account name, with the password `mySecretPassword` (replace this with your own secret password!)
Once you have registered your account, you must configure SASL in your client, so that you will be logged in automatically on each connection. [libera.chat's SASL guide](https://libera.chat/guides/sasl) covers most popular clients.
If your client doesn't support SASL, you can typically use the "server password" (`PASS`) field in your client to log into your account automatically when connecting. Set the server password to `accountname:accountpassword`, where `accountname` is your account name and `accountpassword` is your account password.
For information on how to use a client certificate for authentication, see the [operator manual](https://github.com/ergochat/ergo/blob/stable/docs/MANUAL.md#client-certificates).
# Channel registration
Once you've registered your nickname, you can use it to register channels. By default, channels are ephemeral; they go away when there are no longer any users in the channel, or when the server is restarted. Registering a channel gives you permanent control over it, and ensures that its settings will persist. To register a channel, send a message to `ChanServ`:
```
/msg ChanServ register #myChannel
```
The channel must exist (if it doesn't, you can create it with `/join #myChannel`) and you must already be an operator (have the `+o` channel mode --- your client may display this as an `@` next to your nickname). If you're not a channel operator in the channel you want to register, ask your server administrator for help.
# Always-on
By default, if you lose your connection to the IRC server, you are no longer present on the server; other users will see that you have "quit", you will no longer appear in channel lists, and you will not be able to receive direct messages. Ergo supports "always-on clients", where you remain on the server even when you are disconnected. To enable this, you can send a message to `NickServ`:
```
/msg NickServ set always-on true
```
# Multiclient
Ergo natively supports attaching multiple clients to the same nickname (this normally requires the use of an external bouncer, like ZNC or WeeChat's "relay" functionality). To use this feature, simply authenticate with SASL (or the PASS workaround, if necessary) when connecting. In the recommended configuration of Ergo, you will receive the nickname associated with your account, even if you have other clients already using it.
# History
Ergo stores message history on the server side (typically not an unlimited amount --- consult your server's FAQ, or your server administrator, to find out how much is being stored and how long it's being retained).
1. The [IRCv3 chathistory specification](https://ircv3.net/specs/extensions/chathistory) offers the most fine-grained control over history replay. It is supported by [Gamja](https://git.sr.ht/~emersion/gamja), [Goguma](https://sr.ht/~emersion/goguma/), and [Kiwi IRC](https://github.com/kiwiirc/kiwiirc), and hopefully other clients soon.
1. We emulate the [ZNC playback module](https://wiki.znc.in/Playback) for clients that support it. You may need to enable support for it explicitly in your client. For example, in [Textual](https://www.codeux.com/textual/), go to "Server properties", select "Vendor specific", uncheck "Do not automatically join channels on connect", and check "Only play back messages you missed". ZNC's wiki page covers other common clients (although if the feature is only supported via a script or third-party extension, the following option may be easier).
1. If you set your client to always-on (see the previous section for details), you can set a "device ID" for each device you use. Ergo will then remember the last time your device was present on the server, and each time you sign on, it will attempt to replay exactly those messages you missed. There are a few ways to set your device ID when connecting:
- You can add it to your SASL username with an `@`, e.g., if your SASL username is `alice` you can send `alice@phone`
- You can add it in a similar way to your IRC protocol username ("ident"), e.g., `alice@phone`
- If login to user accounts via the `PASS` command is enabled on the server, you can provide it there, e.g., by sending `alice@phone:hunter2` as the server password
1. If you only have one device, you can set your client to be always-on and furthermore `/msg NickServ set autoreplay-missed true`. This will replay missed messages, with the caveat that you must be connecting with at most one client at a time.
1. You can manually request history using `/history #channel 1h` (the parameter is either a message count or a time duration). (Depending on your client, you may need to use `/QUOTE history` instead.)
1. You can autoreplay a fixed number of lines (e.g., 25) each time you join a channel using `/msg NickServ set autoreplay-lines 25`.
# Private channels
If you have registered a channel, you can make it private. The best way to do this is with the `+i` ("invite-only") mode:
1. Set your channel to be invite-only (`/mode #example +i`)
1. Identify the users you want to be able to access the channel. Ensure that they have registered their accounts (you should be able to see their registration status if you `/WHOIS` their nicknames).
1. Add the desired nick/account names to the invite exception list (`/mode #example +I alice`) or give them persistent voice (`/msg ChanServ AMODE #example +v alice`)
1. If you want to grant a persistent channel privilege to a user, you can do it with `CS AMODE` (`/msg ChanServ AMODE #example +o bob`)
# Push notifications
Ergo has experimental support for mobile push notifications. The server operator must enable this functionality; to check whether this is the case, you can send `/msg NickServ push list`. You must additionally be using a client (e.g. Goguma) that supports the functionality, and your account must be set to always-on (`/msg NickServ set always-on true`, as described above).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -1,2 +1 @@
<?xml version="1.0" encoding="UTF-8"?> <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 209"><defs><style>.cls-1{fill:#0f0f0f;}.cls-2{fill:#6a83c1;}.cls-3{fill:#9eb6de;}</style></defs><title>logo</title><path class="cls-1" d="M96.63,94v21.95H73.8V137H62.39v39.89H73.8v21.95H96.63V220H51V198.87H28.13V178.52H16.72V135.44H28.13V115.91H51V94H96.63Z" transform="translate(-12 -11)"/><path class="cls-1" d="M621.85,94v21.95H599V137H587.6v39.89H599v21.95h22.84V220H576.18V198.87H553.34V178.52H541.93V135.44h11.41V115.91h22.84V94h45.67Z" transform="translate(-12 -11)"/><path class="cls-1" d="M713.19,11V52.48h11.43V94H736v41.48h11.43V54.07H736V32.95h22.84V52.48h22.84V95.56H770.29v83H758.86V220H736V178.52H724.62v-83H713.19V198.87H667.52V135.44h11.43V94h11.41V52.48h11.43V11h11.41Z" transform="translate(-12 -11)"/><path class="cls-1" d="M873,94v21.95H850.2V137H838.79v39.89H850.2v21.95H873V220H827.37V198.87H804.53V178.52H793.12V135.44h11.41V115.91h22.84V94H873Z" transform="translate(-12 -11)"/><path class="cls-2" d="M154.07,204.11a2.45,2.45,0,0,1,1.78.73,2.51,2.51,0,0,1,0,3.57,2.47,2.47,0,0,1-1.77.72H153.4a2.45,2.45,0,0,1-1.78-.73,2.51,2.51,0,0,1,0-3.57,2.48,2.48,0,0,1,1.78-.72h0.67Z" transform="translate(-12 -11)"/><path class="cls-3" d="M775,192.06v9.41h-9.42v-9.41H775Z" transform="translate(-12 -11)"/><path class="cls-3" d="M135.6,192.06v9.41h-9.42v-9.41h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M912,192.06v9.41h-9.42v-9.41H912Z" transform="translate(-12 -11)"/><path class="cls-1" d="M895.87,115.91v19.54H907.3v43.07H895.87v20.35H873V176.93h11.43V137H873V115.91h22.84Z" transform="translate(-12 -11)"/><path class="cls-1" d="M644.68,115.91v19.54h11.43v43.07H644.68v20.35H621.85V176.93h11.43V137H621.85V115.91h22.84Z" transform="translate(-12 -11)"/><path class="cls-1" d="M462,32.95V52.48h22.84V74.43H462V54.07H439.16V157.39H462v19.54h34.26V137H484.84V115.91H462V94h45.67v21.95h22.84v62.61H507.67v20.35H416.33V178.52H404.92V137H393.49V115.91h22.84V95.56H404.92V52.48h11.41V32.95H462Z" transform="translate(-12 -11)"/><path class="cls-1" d="M210.81,32.95V52.48h22.84V95.56H210.81v20.35H165.14v19.54H188v63.43H165.14V178.52H153.73V94h11.41V74.43H188V94h22.84V54.07H165.14V74.43H142.31V32.95h68.51Z" transform="translate(-12 -11)"/><path class="cls-1" d="M119.47,115.91v19.54H130.9v43.07H119.47v20.35H96.63V176.93h11.43V137H96.63V115.91h22.84Z" transform="translate(-12 -11)"/><path class="cls-1" d="M233.65,115.91v19.54h11.43v41.48h11.41v21.95H233.65V178.52H210.81V115.91h22.84Z" transform="translate(-12 -11)"/><path class="cls-1" d="M347.82,32.95V52.48h22.84v83h11.43v43.07H370.66v20.35H347.82V176.93h11.43V137H347.82V115.91H302.15v83H279.32V178.52H267.91V137H256.48V115.91h22.84V95.56H267.91V52.48h11.41V32.95h68.51ZM325,54.07H302.15V94h45.67V74.43H325V54.07Z" transform="translate(-12 -11)"/><path class="cls-2" d="M408.56,194a1.9,1.9,0,0,1,0,3.81,1.85,1.85,0,0,1-1.36-.56,1.9,1.9,0,0,1,0-2.69A1.84,1.84,0,0,1,408.56,194Z" transform="translate(-12 -11)"/><path class="cls-2" d="M85.56,162.63a2.45,2.45,0,0,1,1.78.73,2.51,2.51,0,0,1,0,3.57,2.47,2.47,0,0,1-1.77.72H84.89a2.45,2.45,0,0,1-1.78-.73,2.51,2.51,0,0,1,0-3.57,2.47,2.47,0,0,1,1.77-.73h0.67Z" transform="translate(-12 -11)"/><path class="cls-2" d="M862,162.63a2.45,2.45,0,0,1,1.78.73,2.51,2.51,0,0,1,0,3.57,2.47,2.47,0,0,1-1.77.72h-0.67a2.45,2.45,0,0,1-1.78-.73,2.51,2.51,0,0,1,0-3.57,2.47,2.47,0,0,1,1.78-.73H862Z" transform="translate(-12 -11)"/><path class="cls-3" d="M341.12,150.58V160H331.7v-9.41h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M478.13,150.58V160h-9.42v-9.41h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M199.38,150.69a4.6,4.6,0,1,1-3.24,1.35A4.45,4.45,0,0,1,199.38,150.69Z" transform="translate(-12 -11)"/><path class="cls-3" d="M660.81,109.09v9.41h-9.42v-9.41h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M523.8,67.61V77h-9.42V67.61h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M21.42,67.61V77H12V67.61h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M546.63,67.61V77h-9.42V67.61h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M797.82,67.61V77H788.4V67.61h9.42Z" transform="translate(-12 -11)"/><path class="cls-3" d="M678.93,67.72a4.6,4.6,0,1,1-3.24,1.35,4.45,4.45,0,0,1,3.24-1.35h0Z" transform="translate(-12 -11)"/><path class="cls-2" d="M248.72,69.54l0.72,0.14,0.61,0.42a1.84,1.84,0,0,1,.56,1.36,1.9,1.9,0,0,1-1.9,1.89,1.84,1.84,0,0,1-1.36-.56,1.82,1.82,0,0,1-.56-1.34A1.9,1.9,0,0,1,248.72,69.54Z" transform="translate(-12 -11)"/><path class="cls-3" d="M496.24,26.24a4.6,4.6,0,1,1,0,9.19A4.59,4.59,0,0,1,493,27.59,4.42,4.42,0,0,1,496.24,26.24Z" transform="translate(-12 -11)"/><path class="cls-2" d="M362.89,28.06a1.82,1.82,0,0,1,1.34.56,1.85,1.85,0,0,1,.56,1.36,1.9,1.9,0,0,1-1.9,1.89,1.85,1.85,0,0,1-1.36-.56A1.82,1.82,0,0,1,361,30,1.9,1.9,0,0,1,362.89,28.06Z" transform="translate(-12 -11)"/></svg>
<svg width="552.48" height="226.39" version="1.1" viewBox="0 0 146.18 59.901" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(3.0169 0 0 3.0169 -99.412 -462.64)"><g stroke-width=".40656" aria-label="#ERGO"><path d="m34.33 165.07h1.9027l2.0003-11.351h-1.9027l-0.55292 3.1549h-2.0328v1.7888h1.7075l-0.24394 1.4636h-2.0816v1.7888h1.7563zm3.1549 0h1.9027l0.55292-3.1549h2.0328v-1.7888h-1.7075l0.24393-1.4636h2.0816v-1.7888h-1.7563l0.55292-3.1549h-1.9027z" fill="#5f901d"/><g fill="#161616"><path d="m51.898 165.07v-2.0003h-4.8136v-2.7483h4.651v-2.0003h-4.651v-2.602h4.8136v-2.0003h-7.253v11.351z"/><path d="m56.001 160.86h1.1221l1.9027 4.2119h2.667l-2.2279-4.5372c1.2685-0.35777 2.0003-1.61 2.0003-3.2037 0-2.1954-1.2522-3.6102-3.4801-3.6102h-4.3908v11.351h2.4068zm0-1.8864v-3.285h1.3986c1.1384 0 1.5287 0.40656 1.5287 1.3986v0.48787c0 0.992-0.3903 1.3986-1.5287 1.3986z"/><path d="m68.823 165.07h2.1791v-6.0658h-4.1144v1.7238h1.9352v0.82938c0 1.0083-0.55292 1.7401-1.6425 1.7401-1.4148 0-1.8864-1.2034-1.8864-3.041v-1.8539c0-1.8214 0.45534-2.911 1.6913-2.911 1.1221 0 1.4961 0.89443 1.7401 1.8539l2.2767-0.55291c-0.45534-1.9515-1.6262-3.2687-3.968-3.2687-2.9435 0-4.3258 2.1141-4.3258 5.9683 0 3.6102 1.2034 5.7731 3.4313 5.7731 1.3986 0 2.1629-0.8619 2.5369-1.8051h0.14636z"/><path d="m76.791 165.27c3.0411 0 4.4396-2.1629 4.4396-5.8707 0-3.7078-1.3986-5.8707-4.4396-5.8707-3.041 0-4.4396 2.1629-4.4396 5.8707 0 3.7078 1.3986 5.8707 4.4396 5.8707zm0-1.9677c-1.3823 0-1.8376-1.0896-1.8376-2.911v-1.984c0-1.8214 0.45534-2.911 1.8376-2.911 1.3823 0 1.8376 1.0896 1.8376 2.911v1.9677c0 1.8376-0.45534 2.9272-1.8376 2.9272z"/></g></g><g fill="#4a7411" stroke-width=".17823" aria-label="irc server"><path d="m42.203 168.4c0.24239 0 0.34932-0.12833 0.34932-0.32081v-0.0927c0-0.19249-0.10694-0.32081-0.34932-0.32081s-0.34933 0.12832-0.34933 0.32081v0.0927c0 0.19248 0.10694 0.32081 0.34933 0.32081zm-0.28516 4.5412h0.57033v-3.6786h-0.57033z"/><path d="m44.271 172.94v-2.4952c0-0.34933 0.37071-0.61311 0.98382-0.61311h0.33507v-0.57032h-0.221c-0.59884 0-0.93391 0.32793-1.0622 0.67726h-0.03565v-0.67726h-0.57033v3.6786z"/><path d="m47.65 173.03c0.69865 0 1.1763-0.3422 1.4116-0.86975l-0.41349-0.27804c-0.19961 0.42062-0.53468 0.64162-0.99807 0.64162-0.67726 0-1.0266-0.46339-1.0266-1.105v-0.62736c0-0.64162 0.34932-1.105 1.0266-1.105 0.44913 0 0.76281 0.221 0.89826 0.59885l0.47765-0.24239c-0.21387-0.50617-0.64875-0.86262-1.3759-0.86262-1.0337 0-1.6397 0.74855-1.6397 1.9248s0.60597 1.9249 1.6397 1.9249z"/><path d="m52.655 173.03c0.84123 0 1.3617-0.43488 1.3617-1.1478 0-0.55607-0.31368-0.91252-1.1264-1.0337l-0.28516-0.0428c-0.45626-0.0713-0.70578-0.21387-0.70578-0.57033 0-0.34932 0.24952-0.57032 0.72004-0.57032 0.47052 0 0.7842 0.221 0.94817 0.44913l0.37784-0.3422c-0.29942-0.37071-0.69152-0.59171-1.2832-0.59171-0.74855 0-1.3118 0.35645-1.3118 1.0836 0 0.68439 0.50616 0.96243 1.1834 1.0622l0.29229 0.0428c0.48478 0.0713 0.64162 0.29229 0.64162 0.57745 0 0.37785-0.28516 0.59885-0.76994 0.59885-0.46339 0-0.80559-0.20675-1.0908-0.5632l-0.40636 0.32794c0.32794 0.43487 0.77707 0.72004 1.4543 0.72004z"/><path d="m56.405 173.03c0.69152 0 1.2191-0.3422 1.4543-0.84124l-0.40636-0.29229c-0.19248 0.40636-0.54894 0.63449-1.0123 0.63449-0.68439 0-1.0908-0.47765-1.0908-1.1121v-0.1711h2.6449v-0.2709c0-1.0408-0.60597-1.7965-1.5898-1.7965-0.99807 0-1.6539 0.75568-1.6539 1.9248s0.65588 1.9249 1.6539 1.9249zm0-3.3721c0.58459 0 0.97669 0.43487 0.97669 1.0836v0.0784h-2.0318v-0.0499c0-0.64161 0.43487-1.1121 1.0551-1.1121z"/><path d="m59.506 172.94v-2.4952c0-0.34933 0.37071-0.61311 0.98381-0.61311h0.33507v-0.57032h-0.221c-0.59884 0-0.93391 0.32793-1.0622 0.67726h-0.03565v-0.67726h-0.57033v3.6786z"/><path d="m63.099 172.94 1.2975-3.6786h-0.54894l-0.65588 1.825-0.39923 1.2547h-0.03564l-0.39923-1.2547-0.64162-1.825h-0.57033l1.2904 3.6786z"/><path d="m66.457 173.03c0.69152 0 1.2191-0.3422 1.4543-0.84124l-0.40636-0.29229c-0.19249 0.40636-0.54894 0.63449-1.0123 0.63449-0.68439 0-1.0908-0.47765-1.0908-1.1121v-0.1711h2.6449v-0.2709c0-1.0408-0.60597-1.7965-1.5898-1.7965-0.99807 0-1.6539 0.75568-1.6539 1.9248s0.65588 1.9249 1.6539 1.9249zm0-3.3721c0.58458 0 0.97668 0.43487 0.97668 1.0836v0.0784h-2.0318v-0.0499c0-0.64161 0.43488-1.1121 1.0551-1.1121z"/><path d="m69.558 172.94v-2.4952c0-0.34933 0.37071-0.61311 0.98382-0.61311h0.33507v-0.57032h-0.221c-0.59884 0-0.93391 0.32793-1.0622 0.67726h-0.03565v-0.67726h-0.57033v3.6786z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -63,12 +63,6 @@ CAPDEFS = [
url="https://ircv3.net/specs/extensions/extended-join-3.1.html", url="https://ircv3.net/specs/extensions/extended-join-3.1.html",
standard="IRCv3", standard="IRCv3",
), ),
CapDef(
identifier="ExtendedMonitor",
name="extended-monitor",
url="https://ircv3.net/specs/extensions/extended-monitor.html",
standard="IRCv3",
),
CapDef( CapDef(
identifier="InviteNotify", identifier="InviteNotify",
name="invite-notify", name="invite-notify",
@ -87,12 +81,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",
@ -106,16 +94,16 @@ CAPDEFS = [
standard="IRCv3", standard="IRCv3",
), ),
CapDef( CapDef(
identifier="Relaymsg", identifier="Rename",
name="draft/relaymsg", name="draft/rename",
url="https://github.com/ircv3/ircv3-specifications/pull/417", url="https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md",
standard="proposed IRCv3", standard="proposed IRCv3",
), ),
CapDef( CapDef(
identifier="ChannelRename", identifier="Resume",
name="draft/channel-rename", name="draft/resume-0.5",
url="https://ircv3.net/specs/extensions/channel-rename", url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md",
standard="draft IRCv3", standard="proposed IRCv3",
), ),
CapDef( CapDef(
identifier="SASL", identifier="SASL",
@ -167,9 +155,9 @@ CAPDEFS = [
), ),
CapDef( CapDef(
identifier="Nope", identifier="Nope",
name="ergo.chat/nope", name="oragono.io/nope",
url="https://ergo.chat/nope", url="https://oragono.io/nope",
standard="Ergo vendor", standard="Oragono vendor",
), ),
CapDef( CapDef(
identifier="Multiline", identifier="Multiline",
@ -183,67 +171,6 @@ CAPDEFS = [
url="https://github.com/ircv3/ircv3-specifications/pull/393", url="https://github.com/ircv3/ircv3-specifications/pull/393",
standard="proposed IRCv3", standard="proposed IRCv3",
), ),
CapDef(
identifier="AccountRegistration",
name="draft/account-registration",
url="https://github.com/ircv3/ircv3-specifications/pull/435",
standard="draft IRCv3",
),
CapDef(
identifier="ReadMarker",
name="draft/read-marker",
url="https://github.com/ircv3/ircv3-specifications/pull/489",
standard="draft IRCv3",
),
CapDef(
identifier="Persistence",
name="draft/persistence",
url="https://github.com/ircv3/ircv3-specifications/pull/503",
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",
),
CapDef(
identifier="WebPush",
name="draft/webpush",
url="https://github.com/ircv3/ircv3-specifications/pull/471",
standard="proposed IRCv3",
),
CapDef(
identifier="SojuWebPush",
name="soju.im/webpush",
url="https://github.com/ircv3/ircv3-specifications/pull/471",
standard="Soju/Goguma vendor",
),
CapDef(
identifier="Metadata",
name="draft/metadata-2",
url="https://ircv3.net/specs/extensions/metadata",
standard="draft IRCv3",
),
] ]
def validate_defs(): def validate_defs():
@ -279,7 +206,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)

51
go.mod
View File

@ -1,47 +1,24 @@
module github.com/ergochat/ergo module github.com/oragono/oragono
go 1.24 go 1.15
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/dgrijalva/jwt-go v3.2.0+incompatible
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/go-sql-driver/mysql v1.5.0
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 github.com/go-test/deep v1.0.6 // indirect
github.com/ergochat/irc-go v0.5.0-rc2
github.com/go-sql-driver/mysql v1.7.0
github.com/gofrs/flock v0.8.1
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/goshuirc/irc-go v0.0.0-20200311142257-57fd157327ac
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/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
github.com/oragono/go-ident v0.0.0-20200511222032-830550b1d775
github.com/stretchr/testify v1.4.0 // indirect github.com/stretchr/testify v1.4.0 // indirect
github.com/tidwall/buntdb v1.3.2 github.com/tidwall/buntdb v1.1.2
github.com/xdg-go/scram v1.0.2 github.com/toorop/go-dkim v0.0.0-20200526084421-76378ae5207e
golang.org/x/crypto v0.38.0 golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/term v0.32.0 golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c // indirect
golang.org/x/text v0.25.0 golang.org/x/text v0.3.3
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.3.0
) )
require (
github.com/emersion/go-msgauth v0.7.0
github.com/ergochat/webpush-go/v2 v2.0.0
github.com/golang-jwt/jwt/v5 v5.2.2
)
require (
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/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
golang.org/x/sys v0.33.0 // indirect
)
replace github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1
replace github.com/xdg-go/scram => github.com/ergochat/scram v1.0.2-ergo1

130
go.sum
View File

@ -1,99 +1,85 @@
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9xClkGV/t0rmHrdhX4+trQUcBqjwc9xE= code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9xClkGV/t0rmHrdhX4+trQUcBqjwc9xE=
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/DanielOaks/go-idn v0.0.0-20160120021903-76db0e10dc65/go.mod h1:GYIaL2hleNQvfMUBTes1Zd/lDTyI/p2hv3kYB4jssyU=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
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/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/emersion/go-msgauth v0.6.8/go.mod h1:YDwuyTCUHu9xxmAeVj0eW4INnwB6NNZoPdLerpSxRrc=
github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc=
github.com/emersion/go-msgauth v0.7.0/go.mod h1:mmS9I6HkSovrNgq0HNXTeu8l3sRAAuQ9RMvbM4KU7Ck=
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons=
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1/go.mod h1:mov+uh1DPWsltdQnOdzn08UO9GsJ3MEvhtu0Ci37fdk=
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5AnBVGzTXM/yHVytt48rXBGcJGzSbms=
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881/go.mod h1:ASYJtQujNitna6cVHsNQTGrfWvMPJ5Sa2lZlmsH65uM=
github.com/ergochat/irc-go v0.5.0-rc2 h1:VuSQJF5K4hWvYSzGa4b8vgL6kzw8HF6LSOejE+RWpAo=
github.com/ergochat/irc-go v0.5.0-rc2/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
github.com/ergochat/scram v1.0.2-ergo1 h1:2bYXiRFQH636pT0msOG39fmEYl4Eq+OuutcyDsCix/g=
github.com/ergochat/scram v1.0.2-ergo1/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/ergochat/webpush-go/v2 v2.0.0 h1:n6eoJk8RpzJFeBJ6gxvqo/dngnVEmJbzJwzKtCZbByo=
github.com/ergochat/webpush-go/v2 v2.0.0/go.mod h1:OQlhnq8JeHDzRzAy6bdDObr19uqbHliOV+z7mHbYr4c=
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/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.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 h1:KmRLPRstEJiE/9OjumKqI8Rccip8Qmyw2FwyTFxtVqs=
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940/go.mod h1:VOmrX6cmj7zwUeexC9HzznUdTIObHqIXUrWNYS+Ik7w=
github.com/goshuirc/irc-go v0.0.0-20200311142257-57fd157327ac h1:0JSojWrghcpK9/wx1RpV9Bv2d+3TbBWtHWubKjU2tao=
github.com/goshuirc/irc-go v0.0.0-20200311142257-57fd157327ac/go.mod h1:BRnLblzpqH2T5ANCODHBZLytz0NZN2KaMJ+di8oh3EM=
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=
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd h1:+iAPaTbi1gZpcpDwe/BW1fx7Xoesv69hLNGPheoyhBs=
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd/go.mod h1:4soZNh0zW0LtYGdQ416i0jO0EIqMGcbtaspRS4BDvRQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= 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/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0 h1:4qw57EiWD2MhnmXoQus2ClSyPpGRd8/UxcAmmNe2FCg=
github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0/go.mod h1:+uesPRay9e5tW6zhw4CJkRV3QOEbbZIJcsuo9ZnC+hE=
github.com/oragono/go-ident v0.0.0-20200511222032-830550b1d775 h1:AMAsAn/i4AgsmWQYdMoze9omwtHpbxrKuT+AT1LmhtI=
github.com/oragono/go-ident v0.0.0-20200511222032-830550b1d775/go.mod h1:r5Fk840a4eu3ii1kxGDNSJupQu9Z1UC1nfJOZZXC24c=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/btree v1.4.2 h1:PpkaieETJMUxYNADsjgtNRcERX7mGc/GP2zp/r5FM3g= github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo=
github.com/tidwall/btree v1.4.2/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE= github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI=
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg= github.com/tidwall/gjson v1.3.4 h1:On5waDnyKKk3SWE4EthbjjirAWXp43xx5cKCUZY1eZw=
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/toorop/go-dkim v0.0.0-20200526084421-76378ae5207e h1:uZTp+hhFm+PCH0t0Px5oE+QYlVTwVJ+XKNQr7ct4Q7w=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= github.com/toorop/go-dkim v0.0.0-20200526084421-76378ae5207e/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA=
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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/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=
@ -105,5 +91,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
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.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.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,76 +0,0 @@
package irc
import (
"sync"
"github.com/ergochat/ergo/irc/utils"
)
// tracks ACCEPT relationships, i.e., `accepter` is willing to receive DMs from
// `accepted` despite some restriction (currently the only relevant restriction
// is that `accepter` is +R and `accepted` is not logged in)
type AcceptManager struct {
sync.RWMutex
// maps recipient -> whitelist of permitted senders:
// this is what we actually check
clientToAccepted map[*Client]utils.HashSet[*Client]
// this is the reverse mapping, it's needed so we can
// clean up the forward mapping during (*Client).destroy():
clientToAccepters map[*Client]utils.HashSet[*Client]
}
func (am *AcceptManager) Initialize() {
am.clientToAccepted = make(map[*Client]utils.HashSet[*Client])
am.clientToAccepters = make(map[*Client]utils.HashSet[*Client])
}
func (am *AcceptManager) MaySendTo(sender, recipient *Client) (result bool) {
am.RLock()
defer am.RUnlock()
return am.clientToAccepted[recipient].Has(sender)
}
func (am *AcceptManager) Accept(accepter, accepted *Client) {
am.Lock()
defer am.Unlock()
var m utils.HashSet[*Client]
m = am.clientToAccepted[accepter]
if m == nil {
m = make(utils.HashSet[*Client])
am.clientToAccepted[accepter] = m
}
m.Add(accepted)
m = am.clientToAccepters[accepted]
if m == nil {
m = make(utils.HashSet[*Client])
am.clientToAccepters[accepted] = m
}
m.Add(accepter)
}
func (am *AcceptManager) Unaccept(accepter, accepted *Client) {
am.Lock()
defer am.Unlock()
delete(am.clientToAccepted[accepter], accepted)
delete(am.clientToAccepters[accepted], accepter)
}
func (am *AcceptManager) Remove(client *Client) {
am.Lock()
defer am.Unlock()
for accepter := range am.clientToAccepters[client] {
delete(am.clientToAccepted[accepter], client)
}
for accepted := range am.clientToAccepted[client] {
delete(am.clientToAccepters[accepted], client)
}
delete(am.clientToAccepters, client)
delete(am.clientToAccepted, client)
}

View File

@ -1,108 +0,0 @@
package irc
import (
"testing"
)
func TestAccept(t *testing.T) {
var am AcceptManager
am.Initialize()
alice := new(Client)
bob := new(Client)
eve := new(Client)
// must not panic:
am.Unaccept(eve, bob)
assertEqual(am.MaySendTo(alice, bob), false)
assertEqual(am.MaySendTo(bob, alice), false)
assertEqual(am.MaySendTo(alice, eve), false)
assertEqual(am.MaySendTo(eve, alice), false)
assertEqual(am.MaySendTo(bob, eve), false)
assertEqual(am.MaySendTo(eve, bob), false)
am.Accept(alice, bob)
assertEqual(am.MaySendTo(alice, bob), false)
assertEqual(am.MaySendTo(bob, alice), true)
assertEqual(am.MaySendTo(alice, eve), false)
assertEqual(am.MaySendTo(eve, alice), false)
assertEqual(am.MaySendTo(bob, eve), false)
assertEqual(am.MaySendTo(eve, bob), false)
am.Accept(bob, alice)
assertEqual(am.MaySendTo(alice, bob), true)
assertEqual(am.MaySendTo(bob, alice), true)
assertEqual(am.MaySendTo(alice, eve), false)
assertEqual(am.MaySendTo(eve, alice), false)
assertEqual(am.MaySendTo(bob, eve), false)
assertEqual(am.MaySendTo(eve, bob), false)
am.Accept(bob, eve)
assertEqual(am.MaySendTo(alice, bob), true)
assertEqual(am.MaySendTo(bob, alice), true)
assertEqual(am.MaySendTo(alice, eve), false)
assertEqual(am.MaySendTo(eve, alice), false)
assertEqual(am.MaySendTo(bob, eve), false)
assertEqual(am.MaySendTo(eve, bob), true)
am.Accept(eve, bob)
assertEqual(am.MaySendTo(alice, bob), true)
assertEqual(am.MaySendTo(bob, alice), true)
assertEqual(am.MaySendTo(alice, eve), false)
assertEqual(am.MaySendTo(eve, alice), false)
assertEqual(am.MaySendTo(bob, eve), true)
assertEqual(am.MaySendTo(eve, bob), true)
am.Unaccept(eve, bob)
assertEqual(am.MaySendTo(alice, bob), true)
assertEqual(am.MaySendTo(bob, alice), true)
assertEqual(am.MaySendTo(alice, eve), false)
assertEqual(am.MaySendTo(eve, alice), false)
assertEqual(am.MaySendTo(bob, eve), false)
assertEqual(am.MaySendTo(eve, bob), true)
am.Remove(alice)
assertEqual(am.MaySendTo(alice, bob), false)
assertEqual(am.MaySendTo(bob, alice), false)
assertEqual(am.MaySendTo(alice, eve), false)
assertEqual(am.MaySendTo(eve, alice), false)
assertEqual(am.MaySendTo(bob, eve), false)
assertEqual(am.MaySendTo(eve, bob), true)
am.Remove(bob)
assertEqual(am.MaySendTo(alice, bob), false)
assertEqual(am.MaySendTo(bob, alice), false)
assertEqual(am.MaySendTo(alice, eve), false)
assertEqual(am.MaySendTo(eve, alice), false)
assertEqual(am.MaySendTo(bob, eve), false)
assertEqual(am.MaySendTo(eve, bob), false)
}
func TestAcceptInternal(t *testing.T) {
var am AcceptManager
am.Initialize()
alice := new(Client)
bob := new(Client)
eve := new(Client)
am.Accept(alice, bob)
am.Accept(bob, alice)
am.Accept(bob, eve)
am.Remove(alice)
am.Remove(bob)
// assert that there is no memory leak
for _, client := range []*Client{alice, bob, eve} {
assertEqual(len(am.clientToAccepted[client]), 0)
assertEqual(len(am.clientToAccepters[client]), 0)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,311 +0,0 @@
package irc
import (
"crypto/subtle"
"encoding/json"
"fmt"
"net/http"
"runtime"
"strings"
"github.com/ergochat/ergo/irc/utils"
)
func newAPIHandler(server *Server) http.Handler {
api := &ergoAPI{
server: server,
mux: http.NewServeMux(),
}
api.mux.HandleFunc("POST /v1/rehash", api.handleRehash)
api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth)
api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister)
api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails)
api.mux.HandleFunc("POST /v1/account_list", api.handleAccountList)
api.mux.HandleFunc("POST /v1/status", api.handleStatus)
return api
}
type ergoAPI struct {
server *Server
mux *http.ServeMux
}
func (a *ergoAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer a.server.HandlePanic(nil)
defer a.server.logger.Debug("api", r.URL.Path)
if a.checkBearerAuth(r.Header.Get("Authorization")) {
a.mux.ServeHTTP(w, r)
} else {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
}
func (a *ergoAPI) checkBearerAuth(authHeader string) (authorized bool) {
if authHeader == "" {
return false
}
c := a.server.Config()
if !c.API.Enabled {
return false
}
spaceIdx := strings.IndexByte(authHeader, ' ')
if spaceIdx < 0 {
return false
}
if !strings.EqualFold("Bearer", authHeader[:spaceIdx]) {
return false
}
providedTokenBytes := []byte(authHeader[spaceIdx+1:])
for _, tokenBytes := range c.API.bearerTokenBytes {
if subtle.ConstantTimeCompare(tokenBytes, providedTokenBytes) == 1 {
return true
}
}
return false
}
func (a *ergoAPI) decodeJSONRequest(request any, w http.ResponseWriter, r *http.Request) (err error) {
err = json.NewDecoder(r.Body).Decode(request)
if err != nil {
http.Error(w, fmt.Sprintf("failed to deserialize json request: %v", err), http.StatusBadRequest)
}
return err
}
func (a *ergoAPI) writeJSONResponse(response any, w http.ResponseWriter, r *http.Request) {
j, err := json.Marshal(response)
if err == nil {
j = append(j, '\n') // less annoying in curl output
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(j)
} else {
a.server.logger.Error("internal", "failed to serialize API response", r.URL.Path, err.Error())
http.Error(w, fmt.Sprintf("failed to serialize json response: %v", err), http.StatusInternalServerError)
}
}
type apiGenericResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
}
func (a *ergoAPI) handleRehash(w http.ResponseWriter, r *http.Request) {
var response apiGenericResponse
err := a.server.rehash()
if err == nil {
response.Success = true
} else {
response.Success = false
response.Error = err.Error()
}
a.writeJSONResponse(response, w, r)
}
type apiCheckAuthResponse struct {
apiGenericResponse
AccountName string `json:"accountName,omitempty"`
}
func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) {
var request AuthScriptInput
if err := a.decodeJSONRequest(&request, w, r); err != nil {
return
}
var response apiCheckAuthResponse
// try passphrase if present
if request.AccountName != "" && request.Passphrase != "" {
account, err := a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase)
switch err {
case nil:
// success, no error
response.Success = true
response.AccountName = account.Name
case errAccountDoesNotExist, errAccountInvalidCredentials, errAccountUnverified, errAccountSuspended:
// fail, no error
response.Success = false
default:
response.Success = false
response.Error = err.Error()
}
}
// try certfp if present
if !response.Success && request.Certfp != "" {
// TODO support cerftp
}
a.writeJSONResponse(response, w, r)
}
type apiSaregisterRequest struct {
AccountName string `json:"accountName"`
Passphrase string `json:"passphrase"`
}
func (a *ergoAPI) handleSaregister(w http.ResponseWriter, r *http.Request) {
var request apiSaregisterRequest
if err := a.decodeJSONRequest(&request, w, r); err != nil {
return
}
var response apiGenericResponse
err := a.server.accounts.SARegister(request.AccountName, request.Passphrase)
if err == nil {
response.Success = true
} else {
response.Success = false
response.Error = err.Error()
switch err {
case errAccountAlreadyRegistered, errAccountAlreadyVerified, errNameReserved:
response.ErrorCode = "ACCOUNT_EXISTS"
case errAccountBadPassphrase:
response.ErrorCode = "INVALID_PASSPHRASE"
default:
response.ErrorCode = "UNKNOWN_ERROR"
}
}
a.writeJSONResponse(response, w, r)
}
type apiAccountDetailsResponse struct {
apiGenericResponse
AccountName string `json:"accountName,omitempty"`
Email string `json:"email,omitempty"`
RegisteredAt string `json:"registeredAt,omitempty"`
Channels []string `json:"channels,omitempty"`
}
type apiAccountDetailsRequest struct {
AccountName string `json:"accountName"`
}
func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) {
var request apiAccountDetailsRequest
if err := a.decodeJSONRequest(&request, w, r); err != nil {
return
}
var response apiAccountDetailsResponse
if request.AccountName != "" {
accountData, err := a.server.accounts.LoadAccount(request.AccountName)
if err == nil {
if !accountData.Verified {
err = errAccountUnverified
} else if accountData.Suspended != nil {
err = errAccountSuspended
}
}
switch err {
case nil:
response.AccountName = accountData.Name
response.Email = accountData.Settings.Email
if !accountData.RegisteredAt.IsZero() {
response.RegisteredAt = accountData.RegisteredAt.Format(utils.IRCv3TimestampFormat)
}
// Get channels the account is in
response.Channels = a.server.channels.ChannelsForAccount(accountData.NameCasefolded)
response.Success = true
case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
response.Success = false
default:
response.Success = false
response.ErrorCode = "UNKNOWN_ERROR"
response.Error = err.Error()
}
} else {
response.Success = false
response.ErrorCode = "INVALID_REQUEST"
}
a.writeJSONResponse(response, w, r)
}
type apiAccountListResponse struct {
apiGenericResponse
Accounts []apiAccountDetailsResponse `json:"accounts"`
TotalCount int `json:"totalCount"`
}
func (a *ergoAPI) handleAccountList(w http.ResponseWriter, r *http.Request) {
var response apiAccountListResponse
// Get all account names
accounts := a.server.accounts.AllNicks()
response.TotalCount = len(accounts)
// Load account details
response.Accounts = make([]apiAccountDetailsResponse, len(accounts))
for i, account := range accounts {
accountData, err := a.server.accounts.LoadAccount(account)
if err != nil {
response.Accounts[i] = apiAccountDetailsResponse{
apiGenericResponse: apiGenericResponse{
Success: false,
Error: err.Error(),
},
}
continue
}
response.Accounts[i] = apiAccountDetailsResponse{
apiGenericResponse: apiGenericResponse{
Success: true,
},
AccountName: accountData.Name,
Email: accountData.Settings.Email,
}
}
response.Success = true
a.writeJSONResponse(response, w, r)
}
type apiStatusResponse struct {
apiGenericResponse
Version string `json:"version"`
GoVersion string `json:"go_version"`
Commit string `json:"commit,omitempty"`
StartTime string `json:"start_time"`
Users struct {
Total int `json:"total"`
Invisible int `json:"invisible"`
Operators int `json:"operators"`
Unknown int `json:"unknown"`
Max int `json:"max"`
} `json:"users"`
Channels int `json:"channels"`
Servers int `json:"servers"`
}
func (a *ergoAPI) handleStatus(w http.ResponseWriter, r *http.Request) {
server := a.server
stats := server.stats.GetValues()
response := apiStatusResponse{
apiGenericResponse: apiGenericResponse{Success: true},
Version: SemVer,
GoVersion: runtime.Version(),
Commit: Commit,
StartTime: server.ctime.Format(utils.IRCv3TimestampFormat),
}
response.Users.Total = stats.Total
response.Users.Invisible = stats.Invisible
response.Users.Operators = stats.Operators
response.Users.Unknown = stats.Unknown
response.Users.Max = stats.Max
response.Channels = server.channels.Len()
response.Servers = 1
a.writeJSONResponse(response, w, r)
}

View File

@ -4,25 +4,21 @@
package irc package irc
import ( import (
"crypto/x509" "bufio"
"encoding/json" "encoding/json"
"encoding/pem"
"fmt" "fmt"
"net" "io"
"os/exec"
"github.com/ergochat/ergo/irc/oauth2" "syscall"
"github.com/ergochat/ergo/irc/utils" "time"
) )
// JSON-serializable input and output types for the script // JSON-serializable input and output types for the script
type AuthScriptInput struct { type AuthScriptInput struct {
AccountName string `json:"accountName,omitempty"` AccountName string `json:"accountName,omitempty"`
Passphrase string `json:"passphrase,omitempty"` Passphrase string `json:"passphrase,omitempty"`
Certfp string `json:"certfp,omitempty"` Certfp string `json:"certfp,omitempty"`
PeerCerts []string `json:"peerCerts,omitempty"` IP string `json:"ip,omitempty"`
peerCerts []*x509.Certificate
IP string `json:"ip,omitempty"`
OAuthBearer *oauth2.OAuthBearerOptions `json:"oauth2,omitempty"`
} }
type AuthScriptOutput struct { type AuthScriptOutput struct {
@ -31,85 +27,84 @@ type AuthScriptOutput struct {
Error string `json:"error"` Error string `json:"error"`
} }
func CheckAuthScript(sem utils.Semaphore, config ScriptConfig, input AuthScriptInput) (output AuthScriptOutput, err error) { // internal tupling of output and error for passing over a channel
if sem != nil { type authScriptResponse struct {
sem.Acquire() output AuthScriptOutput
defer sem.Release() err error
} }
// PEM-encode the peer certificates before applying JSON
if len(input.peerCerts) != 0 {
input.PeerCerts = make([]string, len(input.peerCerts))
for i, cert := range input.peerCerts {
input.PeerCerts[i] = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))
}
}
func CheckAuthScript(config AuthScriptConfig, input AuthScriptInput) (output AuthScriptOutput, err error) {
inputBytes, err := json.Marshal(input) inputBytes, err := json.Marshal(input)
if err != nil { if err != nil {
return return
} }
outBytes, err := RunScript(config.Command, config.Args, inputBytes, config.Timeout, config.KillTimeout) cmd := exec.Command(config.Command, config.Args...)
stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return return
} }
err = json.Unmarshal(outBytes, &output) stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return return
} }
if output.Error != "" { channel := make(chan authScriptResponse, 1)
err = fmt.Errorf("Authentication process reported error: %s", output.Error) err = cmd.Start()
if err != nil {
return
} }
stdin.Write(inputBytes)
stdin.Write([]byte{'\n'})
// lots of potential race conditions here. we want to ensure that Wait()
// will be called, and will return, on the other goroutine, no matter
// where it is blocked. If it's blocked on ReadBytes(), we will kill it
// (first with SIGTERM, then with SIGKILL) and ReadBytes will return
// with EOF. If it's blocked on Wait(), then one of the kill signals
// will succeed and unblock it.
go processAuthScriptOutput(cmd, stdout, channel)
outputTimer := time.NewTimer(config.Timeout)
select {
case response := <-channel:
return response.output, response.err
case <-outputTimer.C:
}
err = errTimedOut
cmd.Process.Signal(syscall.SIGTERM)
termTimer := time.NewTimer(config.Timeout)
select {
case <-channel:
return
case <-termTimer.C:
}
cmd.Process.Kill()
return return
} }
type IPScriptResult uint func processAuthScriptOutput(cmd *exec.Cmd, stdout io.Reader, channel chan authScriptResponse) {
var response authScriptResponse
var out AuthScriptOutput
const ( reader := bufio.NewReader(stdout)
IPNotChecked IPScriptResult = 0 outBytes, err := reader.ReadBytes('\n')
IPAccepted IPScriptResult = 1 if err == nil {
IPBanned IPScriptResult = 2 err = json.Unmarshal(outBytes, &out)
IPRequireSASL IPScriptResult = 3 if err == nil {
) response.output = out
if out.Error != "" {
type IPScriptInput struct { err = fmt.Errorf("Authentication process reported error: %s", out.Error)
IP string `json:"ip"` }
} }
type IPScriptOutput struct {
Result IPScriptResult `json:"result"`
BanMessage string `json:"banMessage"`
// for caching: the network to which this result is applicable, and a TTL in seconds:
CacheNet string `json:"cacheNet"`
CacheSeconds int `json:"cacheSeconds"`
Error string `json:"error"`
}
func CheckIPBan(sem utils.Semaphore, config IPCheckScriptConfig, addr net.IP) (output IPScriptOutput, err error) {
if sem != nil {
sem.Acquire()
defer sem.Release()
} }
response.err = err
inputBytes, err := json.Marshal(IPScriptInput{IP: addr.String()}) // always call Wait() to ensure resource cleanup
err = cmd.Wait()
if err != nil { if err != nil {
return response.err = err
}
outBytes, err := RunScript(config.Command, config.Args, inputBytes, config.Timeout, config.KillTimeout)
if err != nil {
return
}
err = json.Unmarshal(outBytes, &output)
if err != nil {
return
} }
if output.Error != "" { channel <- response
err = fmt.Errorf("IP ban process reported error: %s", output.Error)
} else if !(IPAccepted <= output.Result && output.Result <= IPRequireSASL) {
err = fmt.Errorf("Invalid result from IP checking script: %d", output.Result)
}
return
} }

View File

@ -1,107 +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 {
encUUID, ok := strings.CutPrefix(key, tablePrefix)
if !ok {
return false
}
uuid, err := utils.DecodeUUID(encUUID)
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
}
}

View File

@ -58,17 +58,10 @@ const (
// More draft names associated with draft/multiline: // More draft names associated with draft/multiline:
MultilineBatchType = "draft/multiline" MultilineBatchType = "draft/multiline"
MultilineConcatTag = "draft/multiline-concat" MultilineConcatTag = "draft/multiline-concat"
// draft/relaymsg:
RelaymsgTagName = "draft/relaymsg"
// BOT mode: https://ircv3.net/specs/extensions/bot-mode
BotTagName = "bot"
// https://ircv3.net/specs/extensions/chathistory
ChathistoryTargetsBatchType = "draft/chathistory-targets"
ExtendedISupportBatchType = "draft/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)
} }

View File

@ -7,9 +7,9 @@ package caps
const ( const (
// number of recognized capabilities: // number of recognized capabilities:
numCapabs = 38 numCapabs = 26
// length of the uint32 array that represents the bitset: // length of the uint64 array that represents the bitset:
bitsetLen = 2 bitsetLen = 1
) )
const ( const (
@ -37,14 +37,6 @@ const (
// https://ircv3.net/specs/extensions/chghost-3.2.html // https://ircv3.net/specs/extensions/chghost-3.2.html
ChgHost Capability = iota ChgHost Capability = iota
// AccountRegistration is the draft IRCv3 capability named "draft/account-registration":
// https://github.com/ircv3/ircv3-specifications/pull/435
AccountRegistration Capability = iota
// ChannelRename is the draft IRCv3 capability named "draft/channel-rename":
// https://ircv3.net/specs/extensions/channel-rename
ChannelRename Capability = iota
// Chathistory is the proposed IRCv3 capability named "draft/chathistory": // Chathistory is the proposed IRCv3 capability named "draft/chathistory":
// https://github.com/ircv3/ircv3-specifications/pull/393 // https://github.com/ircv3/ircv3-specifications/pull/393
Chathistory Capability = iota Chathistory Capability = iota
@ -53,66 +45,30 @@ 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
// Metadata is the draft IRCv3 capability named "draft/metadata-2":
// https://ircv3.net/specs/extensions/metadata
Metadata Capability = iota
// Multiline is the proposed IRCv3 capability named "draft/multiline": // 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": // Rename is the proposed IRCv3 capability named "draft/rename":
// https://github.com/ircv3/ircv3-specifications/pull/527 // https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
NoImplicitNames Capability = iota Rename Capability = iota
// Persistence is the proposed IRCv3 capability named "draft/persistence": // Resume is the proposed IRCv3 capability named "draft/resume-0.5":
// https://github.com/ircv3/ircv3-specifications/pull/503 // https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
Persistence Capability = iota Resume 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":
// https://github.com/ircv3/ircv3-specifications/pull/489
ReadMarker Capability = iota
// Relaymsg is the proposed IRCv3 capability named "draft/relaymsg":
// https://github.com/ircv3/ircv3-specifications/pull/417
Relaymsg Capability = iota
// WebPush is the proposed IRCv3 capability named "draft/webpush":
// https://github.com/ircv3/ircv3-specifications/pull/471
WebPush Capability = iota
// EchoMessage is the IRCv3 capability named "echo-message": // EchoMessage is the IRCv3 capability named "echo-message":
// https://ircv3.net/specs/extensions/echo-message-3.2.html // https://ircv3.net/specs/extensions/echo-message-3.2.html
EchoMessage Capability = iota EchoMessage Capability = iota
// Nope is the Ergo vendor capability named "ergo.chat/nope":
// https://ergo.chat/nope
Nope Capability = iota
// ExtendedJoin is the IRCv3 capability named "extended-join": // ExtendedJoin is the IRCv3 capability named "extended-join":
// https://ircv3.net/specs/extensions/extended-join-3.1.html // https://ircv3.net/specs/extensions/extended-join-3.1.html
ExtendedJoin Capability = iota ExtendedJoin Capability = iota
// ExtendedMonitor is the IRCv3 capability named "extended-monitor":
// https://ircv3.net/specs/extensions/extended-monitor.html
ExtendedMonitor Capability = iota
// InviteNotify is the IRCv3 capability named "invite-notify": // InviteNotify is the IRCv3 capability named "invite-notify":
// https://ircv3.net/specs/extensions/invite-notify-3.2.html // https://ircv3.net/specs/extensions/invite-notify-3.2.html
InviteNotify Capability = iota InviteNotify Capability = iota
@ -129,6 +85,10 @@ const (
// https://ircv3.net/specs/extensions/multi-prefix-3.1.html // https://ircv3.net/specs/extensions/multi-prefix-3.1.html
MultiPrefix Capability = iota MultiPrefix Capability = iota
// Nope is the Oragono vendor capability named "oragono.io/nope":
// https://oragono.io/nope
Nope Capability = iota
// SASL is the IRCv3 capability named "sasl": // SASL is the IRCv3 capability named "sasl":
// https://ircv3.net/specs/extensions/sasl-3.2.html // https://ircv3.net/specs/extensions/sasl-3.2.html
SASL Capability = iota SASL Capability = iota
@ -141,14 +101,6 @@ const (
// https://ircv3.net/specs/extensions/setname.html // https://ircv3.net/specs/extensions/setname.html
SetName Capability = iota SetName Capability = iota
// SojuWebPush is the Soju/Goguma vendor capability named "soju.im/webpush":
// https://github.com/ircv3/ircv3-specifications/pull/471
SojuWebPush 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
@ -175,34 +127,22 @@ var (
"batch", "batch",
"cap-notify", "cap-notify",
"chghost", "chghost",
"draft/account-registration",
"draft/channel-rename",
"draft/chathistory", "draft/chathistory",
"draft/event-playback", "draft/event-playback",
"draft/extended-isupport",
"draft/languages", "draft/languages",
"draft/message-redaction",
"draft/metadata-2",
"draft/multiline", "draft/multiline",
"draft/no-implicit-names", "draft/rename",
"draft/persistence", "draft/resume-0.5",
"draft/pre-away",
"draft/read-marker",
"draft/relaymsg",
"draft/webpush",
"echo-message", "echo-message",
"ergo.chat/nope",
"extended-join", "extended-join",
"extended-monitor",
"invite-notify", "invite-notify",
"labeled-response", "labeled-response",
"message-tags", "message-tags",
"multi-prefix", "multi-prefix",
"oragono.io/nope",
"sasl", "sasl",
"server-time", "server-time",
"setname", "setname",
"soju.im/webpush",
"standard-replies",
"sts", "sts",
"userhost-in-names", "userhost-in-names",
"znc.in/playback", "znc.in/playback",

View File

@ -5,7 +5,7 @@ package caps
import ( import (
"fmt" "fmt"
"github.com/ergochat/ergo/irc/utils" "github.com/oragono/oragono/irc/utils"
) )
// Set holds a set of enabled capabilities. // Set holds a set of enabled capabilities.
@ -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
}
}

View File

@ -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)
@ -94,14 +78,14 @@ func BenchmarkSetReads(b *testing.B) {
set.Has(UserhostInNames) set.Has(UserhostInNames)
set.Has(LabeledResponse) set.Has(LabeledResponse)
set.Has(EchoMessage) set.Has(EchoMessage)
set.Has(Nope) set.Has(Rename)
} }
} }
func BenchmarkSetWrites(b *testing.B) { func BenchmarkSetWrites(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
set := NewSet(UserhostInNames, EchoMessage) set := NewSet(UserhostInNames, EchoMessage)
set.Add(Nope) set.Add(Rename)
set.Add(ExtendedJoin) set.Add(ExtendedJoin)
set.Remove(UserhostInNames) set.Remove(UserhostInNames)
set.Remove(LabeledResponse) set.Remove(LabeledResponse)

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,9 @@
package irc package irc
import ( import (
"sort"
"sync" "sync"
"time"
"github.com/ergochat/ergo/irc/datastore" "github.com/oragono/oragono/irc/utils"
"github.com/ergochat/ergo/irc/utils"
) )
type channelManagerEntry struct { type channelManagerEntry struct {
@ -27,129 +24,119 @@ type channelManagerEntry struct {
type ChannelManager struct { 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.StringSet // skeletons of *unregistered* chans
purgedChannels map[string]ChannelPurgeRecord // casefolded name to purge record registeredChannels utils.StringSet // casefolds of registered chans
server *Server registeredSkeletons utils.StringSet // skeletons of registered chans
purgedChannels utils.StringSet // casefolds of purged chans
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.StringSet)
cm.server = server cm.server = server
return cm.loadRegisteredChannels(config)
cm.loadRegisteredChannels(server.Config())
// purging should work even if registration is disabled
cm.purgedChannels = cm.server.channelRegistry.PurgedChannels()
} }
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
}
allPurgeRecords, err := FetchAndDeserializeAll[ChannelPurgeRecord](datastore.TableChannelPurges, cm.server.dstore, cm.server.logger)
if err != nil {
return return
} }
rawNames := cm.server.channelRegistry.AllChannels()
registeredChannels := make(utils.StringSet, len(rawNames))
registeredSkeletons := make(utils.StringSet, len(rawNames))
for _, name := range rawNames {
cfname, err := CasefoldChannel(name)
if err == nil {
registeredChannels.Add(cfname)
}
skeleton, err := Skeleton(name)
if err == nil {
registeredSkeletons.Add(skeleton)
}
}
cm.Lock() cm.Lock()
defer cm.Unlock() defer cm.Unlock()
cm.registeredChannels = registeredChannels
cm.purgedChannels = make(map[string]ChannelPurgeRecord, len(allPurgeRecords)) cm.registeredSkeletons = registeredSkeletons
for _, purge := range allPurgeRecords {
cm.purgedChannels[purge.NameCasefolded] = purge
}
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 {
cm.chansSkeletons.Add(skeleton)
}
if _, ok := cm.purgedChannels[cfname]; !ok {
ch := NewChannel(cm.server, regInfo.Name, cfname, true, regInfo)
cm.chans[cfname] = &channelManagerEntry{
channel: ch,
pendingJoins: 0,
skeleton: skeleton,
}
}
}
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()
} defer cm.RUnlock()
cm.RLock() entry := cm.chans[name]
defer cm.RUnlock() // if the channel is still loading, pretend we don't have it
entry := cm.chans[name] if entry != nil && entry.channel.IsLoaded() {
if entry != nil { return entry.channel
return entry.channel }
} }
return nil return nil
} }
// Join causes `client` to join the channel named `name`, creating it if necessary. // Join causes `client` to join the channel named `name`, creating it if necessary.
func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) (err error, forward string) { func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) error {
server := client.server server := client.server
casefoldedName, err := CasefoldChannel(name) casefoldedName, err := CasefoldChannel(name)
skeleton, skerr := Skeleton(name) skeleton, skerr := Skeleton(name)
if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen { if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen {
return errNoSuchChannel, "" return errNoSuchChannel
} }
channel, err, newChannel := func() (*Channel, error, bool) { channel, err := func() (*Channel, error) {
var newChannel bool
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
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)
!(isSajoin || client.HasRoleCapabs("chanreg")) { // enforce OpOnlyCreation
return nil, errInsufficientPrivs, false if !registered && server.Config().Channels.OpOnlyCreation && !client.HasRoleCapabs("chanreg") {
return nil, errInsufficientPrivs
} }
// enforce confusables // enforce confusables
if cm.chansSkeletons.Has(skeleton) { if cm.chansSkeletons.Has(skeleton) || (!registered && cm.registeredSkeletons.Has(skeleton)) {
return nil, errConfusableIdentifier, false return nil, errConfusableIdentifier
} }
entry = &channelManagerEntry{ entry = &channelManagerEntry{
channel: NewChannel(server, name, casefoldedName, false, RegisteredChannel{}), channel: NewChannel(server, name, casefoldedName, registered),
pendingJoins: 0, pendingJoins: 0,
} }
cm.chansSkeletons.Add(skeleton) if !registered {
entry.skeleton = skeleton // 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)
entry.skeleton = skeleton
}
cm.chans[casefoldedName] = entry cm.chans[casefoldedName] = entry
newChannel = true
} }
entry.pendingJoins += 1 entry.pendingJoins += 1
return entry.channel, nil, newChannel return entry.channel, nil
}() }()
if err != nil { if err != nil {
return err, "" return err
} }
err, forward = channel.Join(client, key, isSajoin || newChannel, rb) channel.EnsureLoaded()
err = channel.Join(client, key, isSajoin, rb)
cm.maybeCleanup(channel, true) cm.maybeCleanup(channel, true)
return return err
} }
func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) { func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {
@ -163,10 +150,6 @@ func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {
return return
} }
cm.maybeCleanupInternal(cfname, entry, afterJoin)
}
func (cm *ChannelManager) maybeCleanupInternal(cfname string, entry *channelManagerEntry, afterJoin bool) {
if afterJoin { if afterJoin {
entry.pendingJoins -= 1 entry.pendingJoins -= 1
} }
@ -206,10 +189,6 @@ func (cm *ChannelManager) Cleanup(channel *Channel) {
} }
func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) { func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) {
if account == "" {
return errAuthRequired // this is already enforced by ChanServ, but do a final check
}
if cm.server.Defcon() <= 4 { if cm.server.Defcon() <= 4 {
return errFeatureDisabled return errFeatureDisabled
} }
@ -240,6 +219,10 @@ func (cm *ChannelManager) SetRegistered(channelName string, account string) (err
if err != nil { if err != nil {
return err return err
} }
cm.registeredChannels.Add(cfname)
if skel, err := Skeleton(channel.Name()); err == nil {
cm.registeredSkeletons.Add(skel)
}
return nil return nil
} }
@ -249,13 +232,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())
}
} }
}() }()
@ -263,21 +250,18 @@ 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)
if skel, err := Skeleton(entry.channel.Name()); err == nil {
delete(cm.registeredSkeletons, skel)
} }
uuid = entry.channel.UUID()
entry.channel.SetUnregistered(account) // changes the UUID
// #1619: if the channel has 0 members and was only being retained
// because it was registered, clean it up:
cm.maybeCleanupInternal(cfname, entry, false)
} }
return nil return nil
} }
// Rename renames a channel (but does not notify the members) // Rename renames a channel (but does not notify the members)
func (cm *ChannelManager) Rename(name string, newName string) (err error) { func (cm *ChannelManager) Rename(name string, newName string) (err error) {
oldCfname, err := CasefoldChannel(name) cfname, err := CasefoldChannel(name)
if err != nil { if err != nil {
return errNoSuchChannel return errNoSuchChannel
} }
@ -295,49 +279,52 @@ 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)
} // we just flushed the channel under its new name, therefore this delete
// always-on clients need to update their saved channel memberships // cannot be overwritten by a write to the old name:
for _, member := range channel.Members() { cm.server.channelRegistry.Delete(info)
member.markDirty(IncludeChannels)
} }
}() }()
cm.Lock() cm.Lock()
defer cm.Unlock() defer cm.Unlock()
entry := cm.chans[oldCfname] if newCfname == cfname {
if entry == nil { entry := cm.chans[cfname]
if entry == nil || !entry.channel.IsLoaded() {
return errNoSuchChannel
}
entry.channel.Rename(newName, cfname)
return nil
}
if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) {
return errChannelNameInUse
}
if cm.chansSkeletons.Has(newSkeleton) || cm.registeredSkeletons.Has(newSkeleton) {
return errChannelNameInUse
}
entry := cm.chans[cfname]
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 != ""
delete(cm.chans, cfname)
oldSkeleton, err := Skeleton(info.Name)
if err != nil {
return errNoSuchChannel // ugh
}
if newCfname != oldCfname {
if cm.chans[newCfname] != nil {
return errChannelNameInUse
}
}
if oldSkeleton != newSkeleton {
if cm.chansSkeletons.Has(newSkeleton) {
return errConfusableIdentifier
}
}
delete(cm.chans, oldCfname)
if !registered {
entry.skeleton = newSkeleton
}
cm.chans[newCfname] = entry cm.chans[newCfname] = entry
delete(cm.chansSkeletons, oldSkeleton) if registered {
cm.chansSkeletons.Add(newSkeleton) delete(cm.registeredChannels, cfname)
if oldSkeleton, err := Skeleton(info.Name); err == nil {
delete(cm.registeredSkeletons, oldSkeleton)
}
cm.registeredChannels.Add(newCfname)
cm.registeredSkeletons.Add(newSkeleton)
} else {
delete(cm.chansSkeletons, entry.skeleton)
cm.chansSkeletons.Add(newSkeleton)
entry.skeleton = newSkeleton
cm.chans[cfname] = entry
}
entry.channel.Rename(newName, newCfname) entry.channel.Rename(newName, newCfname)
return nil return nil
} }
@ -355,18 +342,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)
} }
} }
@ -380,45 +356,12 @@ func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err e
return errInvalidChannelName return errInvalidChannelName
} }
record.NameCasefolded = chname cm.Lock()
record.UUID = utils.GenerateUUIDv4() cm.purgedChannels.Add(chname)
cm.Unlock()
channel, err := func() (channel *Channel, err error) { cm.server.channelRegistry.PurgeChannel(chname, record)
cm.Lock() return nil
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 {
return err
}
if channel != nil {
// actually kick everyone off the channel
channel.Purge("")
}
var purgeBytes []byte
if purgeBytes, err = record.Serialize(); err != nil {
cm.server.logger.Error("internal", "couldn't serialize purge record", channel.Name(), err.Error())
}
// TODO we need a better story about error handling for later
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.
@ -429,7 +372,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
} }
@ -442,74 +385,13 @@ 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
} }
func (cm *ChannelManager) ListPurged() (result []string) {
cm.RLock()
result = make([]string, 0, len(cm.purgedChannels))
for c := range cm.purgedChannels {
result = append(result, c)
}
cm.RUnlock()
sort.Strings(result)
return
}
func (cm *ChannelManager) UnfoldName(cfname string) (result string) {
cm.RLock()
entry := cm.chans[cfname]
cm.RUnlock()
if entry != nil {
return entry.channel.Name()
}
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
}

View File

@ -5,15 +5,60 @@ package irc
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strconv"
"strings"
"time" "time"
"github.com/ergochat/ergo/irc/modes" "github.com/tidwall/buntdb"
"github.com/ergochat/ergo/irc/utils"
"github.com/oragono/oragono/irc/modes"
"github.com/oragono/oragono/irc/utils"
) )
// 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"
keyChannelPurged = "channel.purged %s"
)
var (
channelKeyStrings = []string{
keyChannelExists,
keyChannelName,
keyChannelRegTime,
keyChannelFounder,
keyChannelTopic,
keyChannelTopicSetBy,
keyChannelTopicSetTime,
keyChannelBanlist,
keyChannelExceptlist,
keyChannelInvitelist,
keyChannelPassword,
keyChannelModes,
keyChannelAccountToUMode,
keyChannelUserLimit,
keyChannelSettings,
}
)
// 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 +78,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.
@ -49,8 +94,6 @@ type RegisteredChannel struct {
Modes []modes.Mode Modes []modes.Mode
// Key represents the channel key / password // Key represents the channel key / password
Key string Key string
// Forward is the forwarding/overflow (+f) channel
Forward string
// UserLimit is the user limit (0 for no limit) // UserLimit is the user limit (0 for no limit)
UserLimit int UserLimit int
// AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h) // AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h)
@ -63,30 +106,317 @@ type RegisteredChannel struct {
Invites map[string]MaskInfo Invites map[string]MaskInfo
// Settings are the chanserv-modifiable settings // Settings are the chanserv-modifiable settings
Settings ChannelSettings Settings ChannelSettings
// Metadata set using the METADATA command
Metadata map[string]string
}
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"` Oper string
UUID utils.UUID PurgedAt time.Time
Oper string Reason string
PurgedAt time.Time
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.StringSet) {
result = make(utils.StringSet)
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))
topicSetTime, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
topicSetTimeInt, _ := strconv.ParseInt(topicSetTime, 10, 64)
password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, 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(regTimeInt, 0).UTC(),
Founder: founder,
Topic: topic,
TopicSetBy: topicSetBy,
TopicSetTime: time.Unix(topicSetTimeInt, 0).UTC(),
Key: password,
Modes: modeSlice,
Bans: banlist,
Excepts: exceptlist,
Invites: invitelist,
AccountToUMode: accountToUMode,
UserLimit: int(userLimit),
Settings: settings,
}
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(regTimeInt, 0).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.Unix() == info.RegisteredAt.Unix() {
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.Unix(), 10), nil)
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
}
if includeFlags&IncludeTopic != 0 {
tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.Unix(), 10), nil)
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
}
if includeFlags&IncludeModes != 0 {
tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil)
modeStrings := make([]string, len(channelInfo.Modes))
for i, mode := range channelInfo.Modes {
modeStrings[i] = string(mode)
}
tx.Set(fmt.Sprintf(keyChannelModes, channelKey), strings.Join(modeStrings, ""), nil)
tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), 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
})
} }

View File

@ -6,18 +6,18 @@ package irc
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"slices"
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/ergochat/ergo/irc/modes" "github.com/goshuirc/irc-go/ircfmt"
"github.com/ergochat/ergo/irc/sno" "github.com/oragono/oragono/irc/modes"
"github.com/ergochat/ergo/irc/utils" "github.com/oragono/oragono/irc/sno"
"github.com/ergochat/irc-go/ircfmt" "github.com/oragono/oragono/irc/utils"
) )
const chanservHelp = `ChanServ lets you register and manage channels.` const chanservHelp = `ChanServ lets you register and manage channels.`
const chanservMask = "ChanServ!ChanServ@localhost"
func chanregEnabled(config *Config) bool { func chanregEnabled(config *Config) bool {
return config.Channels.Registration.Enabled return config.Channels.Registration.Enabled
@ -30,22 +30,12 @@ var (
help: `Syntax: $bOP #channel [nickname]$b help: `Syntax: $bOP #channel [nickname]$b
OP makes the given nickname, or yourself, a channel admin. You can only use OP makes the given nickname, or yourself, a channel admin. You can only use
this command if you're a founder or in the AMODEs of the channel.`, this command if you're the founder of the channel.`,
helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`, helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`,
authRequired: true, authRequired: true,
enabled: chanregEnabled, enabled: chanregEnabled,
minParams: 1, minParams: 1,
}, },
"deop": {
handler: csDeopHandler,
help: `Syntax: $bDEOP #channel [nickname]$b
DEOP removes the given nickname, or yourself, the channel admin. You can only use
this command if you're the founder of the channel.`,
helpShort: `$bDEOP$b removes the given user (or yourself) from a channel admin.`,
enabled: chanregEnabled,
minParams: 1,
},
"register": { "register": {
handler: csRegisterHandler, handler: csRegisterHandler,
help: `Syntax: $bREGISTER #channel$b help: `Syntax: $bREGISTER #channel$b
@ -77,12 +67,10 @@ invoking the command without a code will display the necessary code.`,
help: `Syntax: $bAMODE #channel [mode change] [account]$b help: `Syntax: $bAMODE #channel [mode change] [account]$b
AMODE lists or modifies persistent mode settings that affect channel members. AMODE lists or modifies persistent mode settings that affect channel members.
For example, $bAMODE #channel +o dan$b grants the holder of the "dan" For example, $bAMODE #channel +o dan$b grants the the holder of the "dan"
account the +o operator mode every time they join #channel. To list current account the +o operator mode every time they join #channel. To list current
accounts and modes, use $bAMODE #channel$b. Note that users are always accounts and modes, use $bAMODE #channel$b. Note that users are always
referenced by their registered account names, not their nicknames. referenced by their registered account names, not their nicknames.`,
The permissions hierarchy for adding and removing modes is the same as in
the ordinary /MODE command.`,
helpShort: `$bAMODE$b modifies persistent mode settings for channel members.`, helpShort: `$bAMODE$b modifies persistent mode settings for channel members.`,
enabled: chanregEnabled, enabled: chanregEnabled,
minParams: 1, minParams: 1,
@ -117,19 +105,28 @@ To cancel a pending transfer, transfer the channel to yourself.`,
}, },
"purge": { "purge": {
handler: csPurgeHandler, handler: csPurgeHandler,
help: `Syntax: $bPURGE <ADD | DEL | LIST> #channel [code] [reason]$b help: `Syntax: $bPURGE #channel [reason]$b
PURGE ADD blacklists a channel from the server, making it impossible to join PURGE blacklists a channel from the server, making it impossible to join
or otherwise interact with the channel. If the channel currently has members, or otherwise interact with the channel. If the channel currently has members,
they will be kicked from it. PURGE may also be applied preemptively to they will be kicked from it. PURGE may also be applied preemptively to
channels that do not currently have members. A purge can be undone with channels that do not currently have members.`,
PURGE DEL. To list purged channels, use PURGE LIST.`,
helpShort: `$bPURGE$b blacklists a channel from the server.`, helpShort: `$bPURGE$b blacklists a channel from the server.`,
capabs: []string{"chanreg"}, capabs: []string{"chanreg"},
minParams: 1, minParams: 1,
maxParams: 3, maxParams: 2,
unsplitFinalParam: true, unsplitFinalParam: true,
}, },
"unpurge": {
handler: csUnpurgeHandler,
help: `Syntax: $bUNPURGE #channel$b
UNPURGE removes any blacklisting of a channel that was previously
set using PURGE.`,
helpShort: `$bUNPURGE$b undoes a previous PURGE command.`,
capabs: []string{"chanreg"},
minParams: 1,
},
"list": { "list": {
handler: csListHandler, handler: csListHandler,
help: `Syntax: $bLIST [regex]$b help: `Syntax: $bLIST [regex]$b
@ -147,6 +144,7 @@ If no regex is provided, all registered channels are returned.`,
INFO displays info about a registered channel.`, INFO displays info about a registered channel.`,
helpShort: `$bINFO$b displays info about a registered channel.`, helpShort: `$bINFO$b displays info about a registered channel.`,
enabled: chanregEnabled, enabled: chanregEnabled,
minParams: 1,
}, },
"get": { "get": {
handler: csGetHandler, handler: csGetHandler,
@ -173,59 +171,34 @@ SET modifies a channel's settings. The following settings are available:`,
2. 'ephemeral' [a limited amount of temporary history, not stored on disk] 2. 'ephemeral' [a limited amount of temporary history, not stored on disk]
3. 'on' [history stored in a permanent database, if available] 3. 'on' [history stored in a permanent database, if available]
4. 'default' [use the server default]`, 4. 'default' [use the server default]`,
`$bQUERY-CUTOFF$b
'query-cutoff' lets you restrict how much channel history can be retrieved
by unprivileged users. Your options are:
1. 'none' [no restrictions]
2. 'registration-time' [users can view history from after their account was
registered, plus a grace period]
3. 'join-time' [users can view history from after they joined the
channel; note that history will be effectively
unavailable to clients that are not always-on]
4. 'default' [use the server default]`,
}, },
enabled: chanregEnabled, enabled: chanregEnabled,
minParams: 3, minParams: 3,
}, },
"howtoban": {
handler: csHowToBanHandler,
helpShort: `$bHOWTOBAN$b suggests the best available way of banning a user`,
help: `Syntax: $bHOWTOBAN #channel <nick>
The best way to ban a user from a channel will depend on how they are
connected to the server. $bHOWTOBAN$b suggests a ban command that will
(ideally) prevent the user from returning to the channel.`,
enabled: chanregEnabled,
minParams: 2,
},
} }
) )
func csAmodeHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { // csNotice sends the client a notice from ChanServ
func csNotice(rb *ResponseBuffer, text string) {
rb.Add(nil, chanservMask, "NOTICE", rb.target.Nick(), text)
}
func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
channelName := params[0] channelName := params[0]
channel := server.channels.Get(channelName) channel := server.channels.Get(channelName)
if channel == nil { if channel == nil {
service.Notice(rb, client.t("Channel does not exist")) csNotice(rb, client.t("Channel does not exist"))
return return
} else if channel.Founder() == "" { } else if channel.Founder() == "" {
service.Notice(rb, client.t("Channel is not registered")) csNotice(rb, client.t("Channel is not registered"))
return return
} }
modeChanges, unknown := modes.ParseChannelModeChanges(params[1:]...) modeChanges, unknown := modes.ParseChannelModeChanges(params[1:]...)
invalid := len(unknown) != 0
// #2002: +f takes an argument but is not a channel-user mode,
// check for anything valid as a channel mode change that is not valid
// as an AMODE change
for _, modeChange := range modeChanges {
if !slices.Contains(modes.ChannelUserModes, modeChange.Mode) {
invalid = true
}
}
var change modes.ModeChange var change modes.ModeChange
if len(modeChanges) > 1 || invalid { if len(modeChanges) > 1 || len(unknown) > 0 {
service.Notice(rb, client.t("Invalid mode change")) csNotice(rb, client.t("Invalid mode change"))
return return
} else if len(modeChanges) == 1 { } else if len(modeChanges) == 1 {
change = modeChanges[0] change = modeChanges[0]
@ -250,17 +223,17 @@ func csAmodeHandler(service *ircService, server *Server, client *Client, command
accountIsValid = (change.Arg != "") accountIsValid = (change.Arg != "")
} }
if !accountIsValid { if !accountIsValid {
service.Notice(rb, client.t("Account does not exist")) csNotice(rb, client.t("Account does not exist"))
return return
} }
affectedModes, err := channel.ProcessAccountToUmodeChange(client, change) affectedModes, err := channel.ProcessAccountToUmodeChange(client, change)
if err == errInsufficientPrivs { if err == errInsufficientPrivs {
service.Notice(rb, client.t("Insufficient privileges")) csNotice(rb, client.t("Insufficient privileges"))
return return
} else if err != nil { } else if err != nil {
service.Notice(rb, client.t("Internal error")) csNotice(rb, client.t("Internal error"))
return return
} }
@ -270,44 +243,39 @@ func csAmodeHandler(service *ircService, server *Server, client *Client, command
sort.Slice(affectedModes, func(i, j int) bool { sort.Slice(affectedModes, func(i, j int) bool {
return umodeGreaterThan(affectedModes[i].Mode, affectedModes[j].Mode) return umodeGreaterThan(affectedModes[i].Mode, affectedModes[j].Mode)
}) })
service.Notice(rb, fmt.Sprintf(client.t("Channel %[1]s has %[2]d persistent modes set"), channelName, len(affectedModes))) csNotice(rb, fmt.Sprintf(client.t("Channel %[1]s has %[2]d persistent modes set"), channelName, len(affectedModes)))
for _, modeChange := range affectedModes { for _, modeChange := range affectedModes {
service.Notice(rb, fmt.Sprintf(client.t("Account %[1]s receives mode +%[2]s"), modeChange.Arg, string(modeChange.Mode))) csNotice(rb, fmt.Sprintf(client.t("Account %[1]s receives mode +%[2]s"), modeChange.Arg, string(modeChange.Mode)))
} }
case modes.Add, modes.Remove: case modes.Add, modes.Remove:
if len(affectedModes) > 0 { if len(affectedModes) > 0 {
service.Notice(rb, fmt.Sprintf(client.t("Successfully set persistent mode %[1]s on %[2]s"), strings.Join([]string{string(change.Op), string(change.Mode)}, ""), change.Arg)) csNotice(rb, fmt.Sprintf(client.t("Successfully set persistent mode %[1]s on %[2]s"), strings.Join([]string{string(change.Op), string(change.Mode)}, ""), change.Arg))
// #729: apply change to current membership // #729: apply change to current membership
for _, member := range channel.Members() { for _, member := range channel.Members() {
if member.Account() == change.Arg { if member.Account() == change.Arg {
// applyModeToMember takes the nickname, not the account name, applied, change := channel.applyModeToMember(client, change, rb)
// so translate:
modeChange := change
modeChange.Arg = member.Nick()
applied, modeChange := channel.applyModeToMember(client, modeChange, rb)
if applied { if applied {
announceCmodeChanges(channel, modes.ModeChanges{modeChange}, server.name, "*", "", false, rb) announceCmodeChanges(channel, modes.ModeChanges{change}, chanservMask, "*", "", rb)
} }
} }
} }
} else { } else {
service.Notice(rb, client.t("No changes were made")) csNotice(rb, client.t("No changes were made"))
} }
} }
} }
func csOpHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func csOpHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
channelInfo := server.channels.Get(params[0]) channelInfo := server.channels.Get(params[0])
if channelInfo == nil { if channelInfo == nil {
service.Notice(rb, client.t("Channel does not exist")) csNotice(rb, client.t("Channel does not exist"))
return return
} }
channelName := channelInfo.Name() channelName := channelInfo.Name()
founder := channelInfo.Founder()
clientAccount := client.Account() clientAccount := client.Account()
if clientAccount == "" { if clientAccount == "" || clientAccount != channelInfo.Founder() {
service.Notice(rb, client.t("You're not logged into an account")) csNotice(rb, client.t("Only the channel founder can do this"))
return return
} }
@ -315,33 +283,18 @@ func csOpHandler(service *ircService, server *Server, client *Client, command st
if len(params) > 1 { if len(params) > 1 {
target = server.clients.Get(params[1]) target = server.clients.Get(params[1])
if target == nil { if target == nil {
service.Notice(rb, client.t("Could not find given client")) csNotice(rb, client.t("Could not find given client"))
return return
} }
} else { } else {
target = client target = client
} }
var givenMode modes.Mode // give them privs
if target == client { givenMode := modes.ChannelOperator
if clientAccount == founder { if clientAccount == target.Account() {
givenMode = modes.ChannelFounder givenMode = modes.ChannelFounder
} else {
givenMode = channelInfo.getAmode(clientAccount)
if givenMode == modes.Mode(0) {
service.Notice(rb, client.t("You don't have any stored privileges on that channel"))
return
}
}
} else {
if clientAccount == founder {
givenMode = modes.ChannelOperator
} else {
service.Notice(rb, client.t("Only the channel founder can do this"))
return
}
} }
applied, change := channelInfo.applyModeToMember(client, applied, change := channelInfo.applyModeToMember(client,
modes.ModeChange{Mode: givenMode, modes.ModeChange{Mode: givenMode,
Op: modes.Add, Op: modes.Add,
@ -349,97 +302,45 @@ func csOpHandler(service *ircService, server *Server, client *Client, command st
}, },
rb) rb)
if applied { if applied {
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, server.name, "*", "", false, rb) announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, "*", "", rb)
} }
service.Notice(rb, client.t("Successfully granted operator privileges")) csNotice(rb, client.t("Successfully granted operator privileges"))
tnick := target.Nick() tnick := target.Nick()
server.logger.Info("services", fmt.Sprintf("Client %s op'd [%s] in channel %s", client.Nick(), tnick, channelName)) server.logger.Info("services", fmt.Sprintf("Client %s op'd [%s] in channel %s", client.Nick(), tnick, channelName))
server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] CS OP'd $c[grey][$r%s$c[grey]] in channel $c[grey][$r%s$c[grey]]"), client.NickMaskString(), tnick, channelName)) server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] CS OP'd $c[grey][$r%s$c[grey]] in channel $c[grey][$r%s$c[grey]]"), client.NickMaskString(), tnick, channelName))
} }
func csDeopHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func csRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
channel := server.channels.Get(params[0])
if channel == nil {
service.Notice(rb, client.t("Channel does not exist"))
return
}
if !channel.hasClient(client) {
service.Notice(rb, client.t("You're not on that channel"))
return
}
var target *Client
if len(params) > 1 {
target = server.clients.Get(params[1])
if target == nil {
service.Notice(rb, client.t("Could not find given client"))
return
}
} else {
target = client
}
present, _, cumodes := channel.ClientStatus(target)
if !present || len(cumodes) == 0 {
service.Notice(rb, client.t("Target has no privileges to remove"))
return
}
tnick := target.Nick()
modeChanges := make(modes.ModeChanges, len(cumodes))
for i, mode := range cumodes {
modeChanges[i] = modes.ModeChange{
Mode: mode,
Op: modes.Remove,
Arg: tnick,
}
}
// use the user's own permissions for the check, then announce
// the changes as coming from chanserv
applied := channel.ApplyChannelModeChanges(client, false, modeChanges, rb)
details := client.Details()
isBot := client.HasMode(modes.Bot)
announceCmodeChanges(channel, applied, details.nickMask, details.accountName, details.account, isBot, rb)
if len(applied) == 0 {
return
}
service.Notice(rb, client.t("Successfully removed operator privileges"))
}
func csRegisterHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if server.Config().Channels.Registration.OperatorOnly && !client.HasRoleCapabs("chanreg") { if server.Config().Channels.Registration.OperatorOnly && !client.HasRoleCapabs("chanreg") {
service.Notice(rb, client.t("Channel registration is restricted to server operators")) csNotice(rb, client.t("Channel registration is restricted to server operators"))
return return
} }
channelName := params[0] channelName := params[0]
channelInfo := server.channels.Get(channelName) channelInfo := server.channels.Get(channelName)
if channelInfo == nil { if channelInfo == nil {
service.Notice(rb, client.t("No such channel")) csNotice(rb, client.t("No such channel"))
return return
} }
if !channelInfo.ClientIsAtLeast(client, modes.ChannelOperator) { if !channelInfo.ClientIsAtLeast(client, modes.ChannelOperator) {
service.Notice(rb, client.t("You must be an oper on the channel to register it")) csNotice(rb, client.t("You must be an oper on the channel to register it"))
return return
} }
account := client.Account() account := client.Account()
if !checkChanLimit(service, client, rb) { if !checkChanLimit(client, rb) {
return return
} }
// this provides the synchronization that allows exactly one registration of the channel: // this provides the synchronization that allows exactly one registration of the channel:
err := server.channels.SetRegistered(channelName, account) err := server.channels.SetRegistered(channelName, account)
if err != nil { if err != nil {
service.Notice(rb, err.Error()) csNotice(rb, err.Error())
return return
} }
service.Notice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName)) csNotice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName))
server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.Nick(), channelName)) server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.Nick(), channelName))
server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString)) server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString))
@ -453,38 +354,38 @@ func csRegisterHandler(service *ircService, server *Server, client *Client, comm
}, },
rb) rb)
if applied { if applied {
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, service.prefix, "*", "", false, rb) announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, "*", "", rb)
} }
} }
// 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(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")) csNotice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
} }
return return
} }
func csPrivsCheck(service *ircService, channel RegisteredChannel, client *Client, rb *ResponseBuffer) (success bool) { func csPrivsCheck(channel RegisteredChannel, client *Client, rb *ResponseBuffer) (success bool) {
founder := channel.Founder founder := channel.Founder
if founder == "" { if founder == "" {
service.Notice(rb, client.t("That channel is not registered")) csNotice(rb, client.t("That channel is not registered"))
return false return false
} }
if client.HasRoleCapabs("chanreg") { if client.HasRoleCapabs("chanreg") {
return true return true
} }
if founder != client.Account() { if founder != client.Account() {
service.Notice(rb, client.t("Insufficient privileges")) csNotice(rb, client.t("Insufficient privileges"))
return false return false
} }
return true return true
} }
func csUnregisterHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
channelName := params[0] channelName := params[0]
var verificationCode string var verificationCode string
if len(params) > 1 { if len(params) > 1 {
@ -493,41 +394,41 @@ func csUnregisterHandler(service *ircService, server *Server, client *Client, co
channel := server.channels.Get(channelName) channel := server.channels.Get(channelName)
if channel == nil { if channel == nil {
service.Notice(rb, client.t("No such channel")) csNotice(rb, client.t("No such channel"))
return return
} }
info := channel.exportSummary() info := channel.ExportRegistration(0)
channelKey := channel.NameCasefolded() channelKey := info.NameCasefolded
if !csPrivsCheck(service, info, client, rb) { if !csPrivsCheck(info, client, rb) {
return return
} }
expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt) expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt)
if expectedCode != verificationCode { if expectedCode != verificationCode {
service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b"))) csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/CS UNREGISTER %s %s", channelKey, expectedCode))) csNotice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/CS UNREGISTER %s %s", channelKey, expectedCode)))
return return
} }
server.channels.SetUnregistered(channelKey, info.Founder) server.channels.SetUnregistered(channelKey, info.Founder)
service.Notice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey)) csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
} }
func csClearHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func csClearHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
channel := server.channels.Get(params[0]) channel := server.channels.Get(params[0])
if channel == nil { if channel == nil {
service.Notice(rb, client.t("Channel does not exist")) csNotice(rb, client.t("Channel does not exist"))
return return
} }
if !csPrivsCheck(service, channel.exportSummary(), client, rb) { if !csPrivsCheck(channel.ExportRegistration(0), client, rb) {
return return
} }
switch strings.ToLower(params[1]) { switch strings.ToLower(params[1]) {
case "access": case "access":
channel.resetAccess() channel.resetAccess()
service.Notice(rb, client.t("Successfully reset channel access")) csNotice(rb, client.t("Successfully reset channel access"))
case "users": case "users":
for _, target := range channel.Members() { for _, target := range channel.Members() {
if target != client { if target != client {
@ -535,74 +436,63 @@ func csClearHandler(service *ircService, server *Server, client *Client, command
} }
} }
default: default:
service.Notice(rb, client.t("Invalid parameters")) csNotice(rb, client.t("Invalid parameters"))
} }
} }
func csTransferHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func csTransferHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if strings.ToLower(params[0]) == "accept" { if strings.ToLower(params[0]) == "accept" {
processTransferAccept(service, client, params[1], rb) processTransferAccept(client, params[1], rb)
return return
} }
chname := params[0] chname := params[0]
channel := server.channels.Get(chname) channel := server.channels.Get(chname)
if channel == nil { if channel == nil {
service.Notice(rb, client.t("Channel does not exist")) csNotice(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
oper := client.Oper() hasPrivs := client.HasRoleCapabs("chanreg")
hasPrivs := oper.HasRoleCapab("chanreg") if !(isFounder || hasPrivs) {
if !isFounder && !hasPrivs { csNotice(rb, client.t("Insufficient privileges"))
service.Notice(rb, client.t("Insufficient privileges"))
return return
} }
target := params[1] target := params[1]
targetAccount, err := server.accounts.LoadAccount(params[1]) targetAccount, err := server.accounts.LoadAccount(params[1])
if err != nil { if err != nil {
service.Notice(rb, client.t("Account does not exist")) csNotice(rb, client.t("Account does not exist"))
return return
} }
if targetAccount.NameCasefolded != account { if targetAccount.NameCasefolded != account {
expectedCode := utils.ConfirmationCode(regInfo.Name, regInfo.RegisteredAt) expectedCode := utils.ConfirmationCode(regInfo.Name, regInfo.RegisteredAt)
codeValidated := 2 < len(params) && params[2] == expectedCode codeValidated := 2 < len(params) && params[2] == expectedCode
if !codeValidated { if !codeValidated {
service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to transfer control of your channel to another user.$b"))) csNotice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to transfer control of your channel to another user.$b")))
service.Notice(rb, fmt.Sprintf(client.t("To confirm your channel transfer, type: /CS TRANSFER %[1]s %[2]s %[3]s"), chname, target, expectedCode)) csNotice(rb, fmt.Sprintf(client.t("To confirm your channel transfer, type: /CS TRANSFER %[1]s %[2]s %[3]s"), chname, target, expectedCode))
return return
} }
} }
if !isFounder {
message := fmt.Sprintf("Operator %s ran CS TRANSFER on %s to account %s", oper.Name, chname, target)
server.snomasks.Send(sno.LocalOpers, message)
server.logger.Info("opers", message)
}
status, err := channel.Transfer(client, target, hasPrivs) status, err := channel.Transfer(client, target, hasPrivs)
if err == nil { if err == nil {
switch status { switch status {
case channelTransferComplete: case channelTransferComplete:
service.Notice(rb, fmt.Sprintf(client.t("Successfully transferred channel %[1]s to account %[2]s"), chname, target)) csNotice(rb, fmt.Sprintf(client.t("Successfully transferred channel %[1]s to account %[2]s"), chname, target))
case channelTransferPending: case channelTransferPending:
sendTransferPendingNotice(service, server, target, chname) sendTransferPendingNotice(server, target, chname)
service.Notice(rb, fmt.Sprintf(client.t("Transfer of channel %[1]s to account %[2]s succeeded, pending acceptance"), chname, target)) csNotice(rb, fmt.Sprintf(client.t("Transfer of channel %[1]s to account %[2]s succeeded, pending acceptance"), chname, target))
case channelTransferCancelled: case channelTransferCancelled:
service.Notice(rb, fmt.Sprintf(client.t("Cancelled pending transfer of channel %s"), chname)) csNotice(rb, fmt.Sprintf(client.t("Cancelled pending transfer of channel %s"), chname))
} }
} else { } else {
switch err { csNotice(rb, client.t("Could not transfer channel"))
case errChannelNotOwnedByAccount:
service.Notice(rb, client.t("You don't own that channel"))
default:
service.Notice(rb, client.t("Could not transfer channel"))
}
} }
} }
func sendTransferPendingNotice(service *ircService, server *Server, account, chname string) { func sendTransferPendingNotice(server *Server, account, chname string) {
clients := server.accounts.AccountToClients(account) clients := server.accounts.AccountToClients(account)
if len(clients) == 0 { if len(clients) == 0 {
return return
@ -614,238 +504,187 @@ func sendTransferPendingNotice(service *ircService, server *Server, account, chn
break // prefer the login where the nick is the account break // prefer the login where the nick is the account
} }
} }
client.Send(nil, service.prefix, "NOTICE", client.Nick(), fmt.Sprintf(client.t("You have been offered ownership of channel %[1]s. To accept, /CS TRANSFER ACCEPT %[1]s"), chname)) client.Send(nil, chanservMask, "NOTICE", client.Nick(), fmt.Sprintf(client.t("You have been offered ownership of channel %[1]s. To accept, /CS TRANSFER ACCEPT %[1]s"), chname))
} }
func processTransferAccept(service *ircService, client *Client, chname string, rb *ResponseBuffer) { func processTransferAccept(client *Client, chname string, rb *ResponseBuffer) {
channel := client.server.channels.Get(chname) channel := client.server.channels.Get(chname)
if channel == nil { if channel == nil {
service.Notice(rb, client.t("Channel does not exist")) csNotice(rb, client.t("Channel does not exist"))
return return
} }
if !checkChanLimit(service, client, rb) { if !checkChanLimit(client, rb) {
return return
} }
switch channel.AcceptTransfer(client) { switch channel.AcceptTransfer(client) {
case nil: case nil:
service.Notice(rb, fmt.Sprintf(client.t("Successfully accepted ownership of channel %s"), channel.Name())) csNotice(rb, fmt.Sprintf(client.t("Successfully accepted ownership of channel %s"), channel.Name()))
case errChannelTransferNotOffered: case errChannelTransferNotOffered:
service.Notice(rb, fmt.Sprintf(client.t("You weren't offered ownership of channel %s"), channel.Name())) csNotice(rb, fmt.Sprintf(client.t("You weren't offered ownership of channel %s"), channel.Name()))
default: default:
service.Notice(rb, fmt.Sprintf(client.t("Could not accept ownership of channel %s"), channel.Name())) csNotice(rb, fmt.Sprintf(client.t("Could not accept ownership of channel %s"), channel.Name()))
} }
} }
func csPurgeHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func csPurgeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
oper := client.Oper() oper := client.Oper()
if oper == nil { if oper == nil {
return // should be impossible because you need oper capabs for this return // should be impossible because you need oper capabs for this
} }
switch strings.ToLower(params[0]) {
case "add":
csPurgeAddHandler(service, client, params[1:], oper.Name, rb)
case "del", "remove":
csPurgeDelHandler(service, client, params[1:], oper.Name, rb)
case "list":
csPurgeListHandler(service, client, rb)
default:
service.Notice(rb, client.t("Invalid parameters"))
}
}
func csPurgeAddHandler(service *ircService, client *Client, params []string, operName string, rb *ResponseBuffer) {
if len(params) == 0 {
service.Notice(rb, client.t("Invalid parameters"))
return
}
chname := params[0] chname := params[0]
params = params[1:]
channel := client.server.channels.Get(chname) // possibly nil
var ctime time.Time
if channel != nil {
chname = channel.Name()
ctime = channel.Ctime()
}
code := utils.ConfirmationCode(chname, ctime)
if len(params) == 0 || params[0] != code {
service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to empty this channel and remove it from the server.$b")))
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/CS PURGE ADD %s %s", chname, code)))
return
}
params = params[1:]
var reason string var reason string
if 1 < len(params) { if 1 < len(params) {
reason = params[1] reason = params[1]
} }
purgeRecord := ChannelPurgeRecord{ purgeRecord := ChannelPurgeRecord{
Oper: operName, Oper: oper.Name,
PurgedAt: time.Now().UTC(), PurgedAt: time.Now().UTC(),
Reason: reason, Reason: reason,
} }
switch client.server.channels.Purge(chname, purgeRecord) { switch server.channels.Purge(chname, purgeRecord) {
case nil: case nil:
channel := server.channels.Get(chname)
if channel != nil { // channel need not exist to be purged if channel != nil { // channel need not exist to be purged
for _, target := range channel.Members() { for _, target := range channel.Members() {
channel.Kick(client, target, "Cleared by ChanServ", rb, true) channel.Kick(client, target, "Cleared by ChanServ", rb, true)
} }
} }
service.Notice(rb, fmt.Sprintf(client.t("Successfully purged channel %s from the server"), chname)) csNotice(rb, fmt.Sprintf(client.t("Successfully purged channel %s from the server"), chname))
client.server.snomasks.Send(sno.LocalChannels, fmt.Sprintf("Operator %s purged channel %s [reason: %s]", operName, chname, reason))
case errInvalidChannelName: case errInvalidChannelName:
service.Notice(rb, fmt.Sprintf(client.t("Can't purge invalid channel %s"), chname)) csNotice(rb, fmt.Sprintf(client.t("Can't purge invalid channel %s"), chname))
default: default:
service.Notice(rb, client.t("An error occurred")) csNotice(rb, client.t("An error occurred"))
} }
} }
func csPurgeDelHandler(service *ircService, client *Client, params []string, operName string, rb *ResponseBuffer) { func csUnpurgeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if len(params) == 0 { chname := params[0]
service.Notice(rb, client.t("Invalid parameters")) switch server.channels.Unpurge(chname) {
case nil:
csNotice(rb, fmt.Sprintf(client.t("Successfully unpurged channel %s from the server"), chname))
case errNoSuchChannel:
csNotice(rb, fmt.Sprintf(client.t("Channel %s wasn't previously purged from the server"), chname))
default:
csNotice(rb, client.t("An error occurred"))
}
}
func csListHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if !client.HasRoleCapabs("chanreg") {
csNotice(rb, client.t("Insufficient privileges"))
return return
} }
chname := params[0]
switch client.server.channels.Unpurge(chname) {
case nil:
service.Notice(rb, fmt.Sprintf(client.t("Successfully unpurged channel %s from the server"), chname))
client.server.snomasks.Send(sno.LocalChannels, fmt.Sprintf("Operator %s removed purge of channel %s", operName, chname))
case errNoSuchChannel:
service.Notice(rb, fmt.Sprintf(client.t("Channel %s wasn't previously purged from the server"), chname))
default:
service.Notice(rb, client.t("An error occurred"))
}
}
func csPurgeListHandler(service *ircService, client *Client, rb *ResponseBuffer) {
l := client.server.channels.ListPurged()
service.Notice(rb, fmt.Sprintf(client.t("There are %d purged channel(s)."), len(l)))
for i, c := range l {
service.Notice(rb, fmt.Sprintf("%d: %s", i+1, c))
}
}
func csListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var searchRegex *regexp.Regexp var searchRegex *regexp.Regexp
if len(params) > 0 { if len(params) > 0 {
var err error var err error
searchRegex, err = regexp.Compile(params[0]) searchRegex, err = regexp.Compile(params[0])
if err != nil { if err != nil {
service.Notice(rb, client.t("Invalid regex")) csNotice(rb, client.t("Invalid regex"))
return return
} }
} }
service.Notice(rb, ircfmt.Unescape(client.t("*** $bChanServ LIST$b ***"))) csNotice(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)) csNotice(rb, fmt.Sprintf(" %s", channel))
} }
} }
service.Notice(rb, ircfmt.Unescape(client.t("*** $bEnd of ChanServ LIST$b ***"))) csNotice(rb, ircfmt.Unescape(client.t("*** $bEnd of ChanServ LIST$b ***")))
} }
func csInfoHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func csInfoHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if len(params) == 0 {
// #765
listRegisteredChannels(service, client.Account(), rb)
return
}
chname, err := CasefoldChannel(params[0]) chname, err := CasefoldChannel(params[0])
if err != nil { if err != nil {
service.Notice(rb, client.t("Invalid channel name")) csNotice(rb, client.t("Invalid channel name"))
return return
} }
// 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.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname)) csNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
service.Notice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper)) csNotice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
service.Notice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123))) csNotice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123)))
if purgeRecord.Reason != "" { if purgeRecord.Reason != "" {
service.Notice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason)) csNotice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason))
} }
} }
} else { } else {
if server.channels.IsPurged(chname) { if server.channels.IsPurged(chname) {
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname)) csNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
} }
} }
var chinfo RegisteredChannel var chinfo RegisteredChannel
channel := server.channels.Get(params[0]) channel := server.channels.Get(params[0])
if channel != nil { if channel != nil {
chinfo = channel.exportSummary() chinfo = channel.ExportRegistration(0)
} else {
chinfo, err = server.channelRegistry.LoadChannel(chname)
if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) {
csNotice(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.Notice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname)) csNotice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname))
return return
} }
service.Notice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name)) csNotice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name))
service.Notice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder)) csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
service.Notice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123))) csNotice(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(settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {
config := client.server.Config() config := client.server.Config()
switch strings.ToLower(settingName) { switch strings.ToLower(settingName) {
case "history": case "history":
effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History) effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
service.Notice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History))) csNotice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue))) csNotice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
case "query-cutoff":
effectiveValue := settings.QueryCutoff
if effectiveValue == HistoryCutoffDefault {
effectiveValue = config.History.Restrictions.queryCutoff
}
service.Notice(rb, fmt.Sprintf(client.t("The stored channel history query cutoff setting is: %s"), historyCutoffToString(settings.QueryCutoff)))
service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history query cutoff setting is: %s"), historyCutoffToString(effectiveValue)))
default: default:
service.Notice(rb, client.t("Invalid params")) csNotice(rb, client.t("Invalid params"))
} }
} }
func csGetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func csGetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
chname, setting := params[0], params[1] chname, setting := params[0], params[1]
channel := server.channels.Get(chname) channel := server.channels.Get(chname)
if channel == nil { if channel == nil {
service.Notice(rb, client.t("No such channel")) csNotice(rb, client.t("No such channel"))
return return
} }
info := channel.exportSummary() info := channel.ExportRegistration(IncludeSettings)
if !csPrivsCheck(service, info, client, rb) { if !csPrivsCheck(info, client, rb) {
return return
} }
displayChannelSetting(service, setting, channel.Settings(), client, rb) displayChannelSetting(setting, info.Settings, client, rb)
} }
func csSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func csSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
chname, setting, value := params[0], params[1], params[2] chname, setting, value := params[0], params[1], params[2]
channel := server.channels.Get(chname) channel := server.channels.Get(chname)
if channel == nil { if channel == nil {
service.Notice(rb, client.t("No such channel")) csNotice(rb, client.t("No such channel"))
return return
} }
info := channel.exportSummary() info := channel.ExportRegistration(IncludeSettings)
if !csPrivsCheck(service, info, client, rb) { settings := info.Settings
if !csPrivsCheck(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":
@ -856,103 +695,16 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
} }
channel.SetSettings(settings) channel.SetSettings(settings)
channel.resizeHistory(server.Config()) channel.resizeHistory(server.Config())
case "query-cutoff":
settings.QueryCutoff, err = historyCutoffFromString(value)
if err != nil {
err = errInvalidParams
break
}
channel.SetSettings(settings)
} }
switch err { switch err {
case nil: case nil:
service.Notice(rb, client.t("Successfully changed the channel settings")) csNotice(rb, client.t("Successfully changed the channel settings"))
displayChannelSetting(service, setting, settings, client, rb) displayChannelSetting(setting, settings, client, rb)
case errInvalidParams: case errInvalidParams:
service.Notice(rb, client.t("Invalid parameters")) csNotice(rb, client.t("Invalid parameters"))
default: default:
server.logger.Error("internal", "CS SET error:", err.Error()) server.logger.Error("internal", "CS SET error:", err.Error())
service.Notice(rb, client.t("An error occurred")) csNotice(rb, client.t("An error occurred"))
}
}
func csHowToBanHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
success := false
defer func() {
if success {
service.Notice(rb, client.t("Note that if the user is currently in the channel, you must /KICK them after you ban them"))
}
}()
chname, nick := params[0], params[1]
channel := server.channels.Get(chname)
if channel == nil {
service.Notice(rb, client.t("No such channel"))
return
}
if !(channel.ClientIsAtLeast(client, modes.ChannelOperator) || client.HasRoleCapabs("samode")) {
service.Notice(rb, client.t("Insufficient privileges"))
return
}
var details WhoWas
target := server.clients.Get(nick)
if target == nil {
whowasList := server.whoWas.Find(nick, 1)
if len(whowasList) == 0 {
service.Notice(rb, client.t("No such nick"))
return
}
service.Notice(rb, fmt.Sprintf(client.t("Warning: %s is not currently connected to the server. Using WHOWAS data, which may be inaccurate:"), nick))
details = whowasList[0]
} else {
details = target.Details().WhoWas
}
if details.account != "" {
if channel.getAmode(details.account) != modes.Mode(0) {
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s currently has a persistent channel privilege granted with CS AMODE. If this mode is not removed, bans will not be respected"), details.accountName))
return
} else if details.account == channel.Founder() {
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s is the channel founder and cannot be banned"), details.accountName))
return
}
}
config := server.Config()
if !config.Server.Cloaks.EnabledForAlwaysOn {
service.Notice(rb, client.t("Warning: server.ip-cloaking.enabled-for-always-on is disabled. This reduces the precision of channel bans."))
}
if details.account != "" {
if config.Accounts.NickReservation.ForceNickEqualsAccount || target.AlwaysOn() {
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s is authenticated and can be banned by nickname: /MODE %[2]s +b %[3]s!*@*"), details.nick, channel.Name(), details.nick))
success = true
return
}
}
ban := fmt.Sprintf("*!*@%s", strings.ToLower(details.hostname))
banRe, err := utils.CompileGlob(ban, false)
if err != nil {
server.logger.Error("internal", "couldn't compile ban regex", ban, err.Error())
service.Notice(rb, "An error occurred")
return
}
var collateralDamage []string
for _, mcl := range channel.Members() {
if mcl != target && banRe.MatchString(mcl.NickMaskCasefolded()) {
collateralDamage = append(collateralDamage, mcl.Nick())
}
}
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s can be banned by hostname: /MODE %[2]s +b %[3]s"), details.nick, channel.Name(), ban))
success = true
if len(collateralDamage) != 0 {
service.Notice(rb, fmt.Sprintf(client.t("Warning: this ban will affect %d other users:"), len(collateralDamage)))
for _, line := range utils.BuildTokenLines(maxLastArgLength, collateralDamage, " ") {
service.Notice(rb, line)
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,14 @@
package irc package irc
import ( import (
"regexp"
"strings" "strings"
"sync" "sync"
"time"
"github.com/ergochat/ergo/irc/caps" "github.com/oragono/oragono/irc/caps"
"github.com/ergochat/ergo/irc/modes" "github.com/oragono/oragono/irc/modes"
"github.com/ergochat/ergo/irc/utils" "github.com/oragono/oragono/irc/utils"
) )
// ClientManager keeps track of clients by nick, enforcing uniqueness of casefolded nicks // ClientManager keeps track of clients by nick, enforcing uniqueness of casefolded nicks
@ -26,6 +28,14 @@ func (clients *ClientManager) Initialize() {
clients.bySkeleton = make(map[string]*Client) clients.bySkeleton = make(map[string]*Client)
} }
// Count returns how many clients are in the manager.
func (clients *ClientManager) Count() int {
clients.RLock()
defer clients.RUnlock()
count := len(clients.byNick)
return count
}
// Get retrieves a client from the manager, if they exist. // Get retrieves a client from the manager, if they exist.
func (clients *ClientManager) Get(nick string) *Client { func (clients *ClientManager) Get(nick string) *Client {
casefoldedName, err := CasefoldName(nick) casefoldedName, err := CasefoldName(nick)
@ -38,8 +48,9 @@ func (clients *ClientManager) Get(nick string) *Client {
return nil return nil
} }
func (clients *ClientManager) removeInternal(client *Client, oldcfnick, oldskeleton string) (err error) { func (clients *ClientManager) removeInternal(client *Client) (err error) {
// requires holding the writable Lock() // requires holding the writable Lock()
oldcfnick, oldskeleton := client.uniqueIdentifiers()
if oldcfnick == "*" || oldcfnick == "" { if oldcfnick == "*" || oldcfnick == "" {
return errNickMissing return errNickMissing
} }
@ -77,14 +88,31 @@ func (clients *ClientManager) Remove(client *Client) error {
clients.Lock() clients.Lock()
defer clients.Unlock() defer clients.Unlock()
oldcfnick, oldskeleton := client.uniqueIdentifiers() return clients.removeInternal(client)
return clients.removeInternal(client, oldcfnick, oldskeleton) }
// Handles a RESUME by attaching a session to a designated client. It is the
// caller's responsibility to verify that the resume is allowed (checking tokens,
// TLS status, etc.) before calling this.
func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err error) {
clients.Lock()
defer clients.Unlock()
cfnick := oldClient.NickCasefolded()
if _, ok := clients.byNick[cfnick]; !ok {
return errNickMissing
}
success, _, _, _ := oldClient.AddSession(session)
if !success {
return errNickMissing
}
return nil
} }
// 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 func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) (setNick string, err error, returnedFromAway bool) {
// actually claiming it
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, awayChanged bool) {
config := client.server.Config() config := client.server.Config()
var newCfNick, newSkeleton string var newCfNick, newSkeleton string
@ -94,20 +122,9 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
accountName := client.accountName accountName := client.accountName
settings := client.accountSettings settings := client.accountSettings
registered := client.registered registered := client.registered
realname := client.realname
client.stateMutex.RUnlock() client.stateMutex.RUnlock()
// these restrictions have grandfather exceptions for nicknames registered
// on previous versions of Ergo:
if newNick != accountName {
// can't contain "disfavored" characters like <, or start with a $ because
// it collides with the massmessage mask syntax. '0' conflicts with the use of 0
// as a placeholder in WHOX (#1896):
if strings.ContainsAny(newNick, disfavoredNameCharacters) || strings.HasPrefix(newNick, "$") ||
newNick == "0" {
return "", errNicknameInvalid, false
}
}
// recompute always-on status, because client.alwaysOn is not set for unregistered clients // recompute always-on status, because client.alwaysOn is not set for unregistered clients
var alwaysOn, useAccountName bool var alwaysOn, useAccountName bool
if account != "" { if account != "" {
@ -115,10 +132,8 @@ 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 && newNick != "" {
return "", errNickAccountMismatch, false return "", errNickAccountMismatch, false
} }
newNick = accountName newNick = accountName
@ -133,7 +148,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
return "", errNickMissing, false return "", errNickMissing, false
} }
if account == "" && config.Accounts.NickReservation.ForceGuestFormat && !dryRun { if account == "" && config.Accounts.NickReservation.ForceGuestFormat {
newCfNick, err = CasefoldName(newNick) newCfNick, err = CasefoldName(newNick)
if err != nil { if err != nil {
return "", errNicknameInvalid, false return "", errNicknameInvalid, false
@ -158,19 +173,13 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
return "", errNicknameInvalid, false return "", errNicknameInvalid, false
} }
if config.isRelaymsgIdentifier(newNick) { if restrictedCasefoldedNicks[newCfNick] || restrictedSkeletons[newSkeleton] {
return "", errNicknameInvalid, false
}
if restrictedCasefoldedNicks.Has(newCfNick) || restrictedSkeletons.Has(newSkeleton) {
return "", errNicknameInvalid, false return "", errNicknameInvalid, false
} }
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
} }
} }
@ -192,24 +201,36 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
currentClient := clients.byNick[newCfNick] currentClient := clients.byNick[newCfNick]
// the client may just be changing case // the client may just be changing case
if currentClient != nil && currentClient != client { if currentClient != nil && currentClient != client && session != nil {
// these conditions forbid reattaching to an existing session: // these conditions forbid reattaching to an existing session:
if registered || !bouncerAllowed || account == "" || account != currentClient.Account() || if registered || !bouncerAllowed || account == "" || account != currentClient.Account() {
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
} }
if numSessions == 1 { if numSessions == 1 {
invisible := currentClient.HasMode(modes.Invisible) invisible := currentClient.HasMode(modes.Invisible)
operator := currentClient.HasMode(modes.Operator) operator := currentClient.HasMode(modes.Operator) || currentClient.HasMode(modes.LocalOperator)
client.server.stats.AddRegistered(invisible, operator) client.server.stats.AddRegistered(invisible, operator)
} }
session.autoreplayMissedSince = lastSeen session.autoreplayMissedSince = lastSeen
// TODO: transition mechanism for #1065, clean this up eventually:
if currentClient.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
} }
@ -218,19 +239,11 @@ 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 {
return "", nil, false
}
formercfnick, formerskeleton := client.uniqueIdentifiers()
if changeSuccess := client.SetNick(newNick, newCfNick, newSkeleton); !changeSuccess { if changeSuccess := client.SetNick(newNick, newCfNick, newSkeleton); !changeSuccess {
return "", errClientDestroyed, false return "", errClientDestroyed, false
} }
clients.removeInternal(client, formercfnick, formerskeleton) clients.removeInternal(client)
clients.byNick[newCfNick] = client clients.byNick[newCfNick] = client
clients.bySkeleton[newSkeleton] = client clients.bySkeleton[newSkeleton] = client
return newNick, nil, false return newNick, nil, false
@ -248,14 +261,13 @@ func (clients *ClientManager) AllClients() (result []*Client) {
return return
} }
// AllWithCapsNotify returns all sessions that support cap-notify. // AllWithCaps returns all clients with the given capabilities.
func (clients *ClientManager) AllWithCapsNotify() (sessions []*Session) { func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (sessions []*Session) {
clients.RLock() clients.RLock()
defer clients.RUnlock() defer clients.RUnlock()
for _, client := range clients.byNick { for _, client := range clients.byNick {
for _, session := range client.Sessions() { for _, session := range client.Sessions() {
// cap-notify is implicit in cap version 302 and above if session.capabilities.HasAll(capabs...) {
if session.capabilities.Has(caps.CapNotify) || 302 <= session.capVersion {
sessions = append(sessions, session) sessions = append(sessions, session)
} }
} }
@ -264,16 +276,21 @@ func (clients *ClientManager) AllWithCapsNotify() (sessions []*Session) {
return return
} }
// AllWithPushSubscriptions returns all clients that are always-on with an active push subscription. // AllWithCapsNotify returns all clients with the given capabilities, and that support cap-notify.
func (clients *ClientManager) AllWithPushSubscriptions() (result []*Client) { func (clients *ClientManager) AllWithCapsNotify(capabs ...caps.Capability) (sessions []*Session) {
capabs = append(capabs, caps.CapNotify)
clients.RLock() clients.RLock()
defer clients.RUnlock() defer clients.RUnlock()
for _, client := range clients.byNick { for _, client := range clients.byNick {
if client.hasPushSubscriptions() && client.AlwaysOn() { for _, session := range client.Sessions() {
result = append(result, client) // cap-notify is implicit in cap version 302 and above
if session.capabilities.HasAll(capabs...) || 302 <= session.capVersion {
sessions = append(sessions, session)
}
} }
} }
return result
return
} }
// FindAll returns all clients that match the given userhost mask. // FindAll returns all clients that match the given userhost mask.
@ -301,15 +318,133 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
return set return set
} }
// Determine the canonical / unfolded form of a nick, if a client matching it //
// is present (or always-on). // usermask to regexp
func (clients *ClientManager) UnfoldNick(cfnick string) (nick string) { //
clients.RLock()
c := clients.byNick[cfnick] //TODO(dan): move this over to generally using glob syntax instead?
clients.RUnlock() // kinda more expected in normal ban/etc masks, though regex is useful (probably as an extban?)
if c != nil {
return c.Nick() type MaskInfo struct {
} else { TimeCreated time.Time
return cfnick CreatorNickmask string
} CreatorAccount string
}
// UserMaskSet holds a set of client masks and lets you match hostnames to them.
type UserMaskSet struct {
sync.RWMutex
serialCacheUpdateMutex sync.Mutex
masks map[string]MaskInfo
regexp *regexp.Regexp
}
func NewUserMaskSet() *UserMaskSet {
return new(UserMaskSet)
}
// Add adds the given mask to this set.
func (set *UserMaskSet) Add(mask, creatorNickmask, creatorAccount string) (maskAdded string, err error) {
casefoldedMask, err := CanonicalizeMaskWildcard(mask)
if err != nil {
return
}
set.serialCacheUpdateMutex.Lock()
defer set.serialCacheUpdateMutex.Unlock()
set.Lock()
if set.masks == nil {
set.masks = make(map[string]MaskInfo)
}
_, present := set.masks[casefoldedMask]
if !present {
maskAdded = casefoldedMask
set.masks[casefoldedMask] = MaskInfo{
TimeCreated: time.Now().UTC(),
CreatorNickmask: creatorNickmask,
CreatorAccount: creatorAccount,
}
}
set.Unlock()
if !present {
set.setRegexp()
}
return
}
// Remove removes the given mask from this set.
func (set *UserMaskSet) Remove(mask string) (maskRemoved string, err error) {
mask, err = CanonicalizeMaskWildcard(mask)
if err != nil {
return
}
set.serialCacheUpdateMutex.Lock()
defer set.serialCacheUpdateMutex.Unlock()
set.Lock()
_, removed := set.masks[mask]
if removed {
maskRemoved = mask
delete(set.masks, mask)
}
set.Unlock()
if removed {
set.setRegexp()
}
return
}
func (set *UserMaskSet) SetMasks(masks map[string]MaskInfo) {
set.Lock()
set.masks = masks
set.Unlock()
set.setRegexp()
}
func (set *UserMaskSet) Masks() (result map[string]MaskInfo) {
set.RLock()
defer set.RUnlock()
result = make(map[string]MaskInfo, len(set.masks))
for mask, info := range set.masks {
result[mask] = info
}
return
}
// Match matches the given n!u@h.
func (set *UserMaskSet) Match(userhost string) bool {
set.RLock()
regexp := set.regexp
set.RUnlock()
if regexp == nil {
return false
}
return regexp.MatchString(userhost)
}
func (set *UserMaskSet) Length() int {
set.RLock()
defer set.RUnlock()
return len(set.masks)
}
func (set *UserMaskSet) setRegexp() {
set.RLock()
maskExprs := make([]string, len(set.masks))
for mask := range set.masks {
maskExprs = append(maskExprs, mask)
}
set.RUnlock()
re, _ := utils.CompileMasks(maskExprs)
set.Lock()
set.regexp = re
set.Unlock()
} }

View File

@ -4,16 +4,14 @@
package irc package irc
import ( import (
"fmt"
"testing" "testing"
"github.com/ergochat/ergo/irc/languages" "github.com/oragono/oragono/irc/utils"
"github.com/ergochat/ergo/irc/utils"
) )
func TestGenerateBatchID(t *testing.T) { func TestGenerateBatchID(t *testing.T) {
var session Session var session Session
s := make(utils.HashSet[string]) s := make(utils.StringSet)
count := 100000 count := 100000
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
@ -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

View File

@ -104,33 +104,3 @@ func BenchmarkCloaks(b *testing.B) {
config.ComputeCloak(v6ip) config.ComputeCloak(v6ip)
} }
} }
func TestAccountCloak(t *testing.T) {
config := cloakConfForTesting()
// just assert that we get all distinct values
assertEqual(config.ComputeAccountCloak("shivaram"), "8yu8kunudb45ztxm.oragono", t)
assertEqual(config.ComputeAccountCloak("dolph🐬n"), "hhgeqsvzeagv3wjw.oragono", t)
assertEqual(config.ComputeAccountCloak("SHIVARAM"), "bgx32x4r7qzih4uh.oragono", t)
assertEqual(config.ComputeAccountCloak("ed"), "j5autmgxtdjdyzf4.oragono", t)
}
func TestAccountCloakCollisions(t *testing.T) {
config := cloakConfForTesting()
v4ip := easyParseIP("97.97.97.97")
v4cloak := config.ComputeCloak(v4ip)
// "aaaa" is the same bytestring as 97.97.97.97
aaaacloak := config.ComputeAccountCloak("aaaa")
if v4cloak == aaaacloak {
t.Errorf("cloak collision between 97.97.97.97 and aaaa: %s", v4cloak)
}
}
func BenchmarkAccountCloaks(b *testing.B) {
config := cloakConfForTesting()
b.ResetTimer()
for i := 0; i < b.N; i++ {
config.ComputeAccountCloak("shivaram")
}
}

View File

@ -6,19 +6,18 @@ import (
"fmt" "fmt"
"net" "net"
"crypto/sha3" "golang.org/x/crypto/sha3"
"github.com/ergochat/ergo/irc/utils" "github.com/oragono/oragono/irc/utils"
) )
type CloakConfig struct { type CloakConfig struct {
Enabled bool Enabled bool
EnabledForAlwaysOn bool `yaml:"enabled-for-always-on"` Netname string
Netname string CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv4 int `yaml:"cidr-len-ipv4"` CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"` NumBits int `yaml:"num-bits"`
NumBits int `yaml:"num-bits"` LegacySecretValue string `yaml:"secret"`
LegacySecretValue string `yaml:"secret"`
secret string secret string
numBytes int numBytes int
@ -27,10 +26,14 @@ type CloakConfig struct {
} }
func (cloakConfig *CloakConfig) Initialize() { func (cloakConfig *CloakConfig) Initialize() {
if !cloakConfig.Enabled {
return
}
// sanity checks: // sanity checks:
numBits := cloakConfig.NumBits numBits := cloakConfig.NumBits
if 0 == numBits { if 0 == numBits {
numBits = 64 numBits = 80
} else if 256 < numBits { } else if 256 < numBits {
numBits = 256 numBits = 256
} }
@ -66,30 +69,12 @@ func (config *CloakConfig) ComputeCloak(ip net.IP) string {
} else { } else {
masked = ip.Mask(config.ipv6Mask) masked = ip.Mask(config.ipv6Mask)
} }
return config.macAndCompose(masked)
}
func (config *CloakConfig) macAndCompose(b []byte) string {
// SHA3(K || M): // SHA3(K || M):
// https://crypto.stackexchange.com/questions/17735/is-hmac-needed-for-a-sha-3-based-mac // https://crypto.stackexchange.com/questions/17735/is-hmac-needed-for-a-sha-3-based-mac
input := make([]byte, len(config.secret)+len(b)) input := make([]byte, len(config.secret)+len(masked))
copy(input, config.secret[:]) copy(input, config.secret[:])
copy(input[len(config.secret):], b) copy(input[len(config.secret):], masked)
digest := sha3.Sum512(input) digest := sha3.Sum512(input)
b32digest := utils.B32Encoder.EncodeToString(digest[:config.numBytes]) b32digest := utils.B32Encoder.EncodeToString(digest[:config.numBytes])
return fmt.Sprintf("%s.%s", b32digest, config.Netname) return fmt.Sprintf("%s.%s", b32digest, config.Netname)
} }
func (config *CloakConfig) ComputeAccountCloak(accountName string) string {
// XXX don't bother checking EnabledForAlwaysOn, since if it's disabled,
// we need to use the server name which we don't have
if config.NumBits == 0 || config.secret == "" {
return config.Netname
}
// pad with 16 initial bytes of zeroes, avoiding any possibility of collision
// with a masked IP that could be an input to ComputeCloak:
paddedAccountName := make([]byte, 16+len(accountName))
copy(paddedAccountName[16:], accountName[:])
return config.macAndCompose(paddedAccountName)
}

View File

@ -6,38 +6,22 @@
package irc package irc
import ( import (
"github.com/ergochat/irc-go/ircmsg" "github.com/goshuirc/irc-go/ircmsg"
"github.com/oragono/oragono/irc/modes"
) )
// Command represents a command accepted from a client. // Command represents a command accepted from a client.
type Command struct { type Command struct {
handler func(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool handler func(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool
oper bool
usablePreReg bool usablePreReg bool
allowedInBatch bool // allowed in client-to-server batches allowedInBatch bool // allowed in client-to-server batches
minParams int minParams int
capabs []string capabs []string
} }
// resolveCommand returns the command to execute in response to a user input line.
// some invalid commands (unknown command verb, invalid UTF8) get a fake handler
// to ensure that labeled-response still works as expected.
func (server *Server) resolveCommand(command string, invalidUTF8 bool) (canonicalName string, result Command) {
if invalidUTF8 {
return command, invalidUtf8Command
}
if cmd, ok := Commands[command]; ok {
return command, cmd
}
if target, ok := server.Config().Server.CommandAliases[command]; ok {
if cmd, ok := Commands[target]; ok {
return target, cmd
}
}
return command, unknownCommand
}
// Run runs this command with the given client/message. // Run runs this command with the given client/message.
func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ircmsg.Message) (exiting bool) { func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ircmsg.IrcMessage) (exiting bool) {
rb := NewResponseBuffer(session) rb := NewResponseBuffer(session)
rb.Label = GetLabel(msg) rb.Label = GetLabel(msg)
@ -48,6 +32,10 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
rb.Add(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command")) rb.Add(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command"))
return false return false
} }
if cmd.oper && !client.HasMode(modes.Operator) {
rb.Add(nil, server.name, ERR_NOPRIVILEGES, client.Nick(), client.t("Permission Denied - You're not an IRC operator"))
return false
}
if len(cmd.capabs) > 0 && !client.HasRoleCapabs(cmd.capabs...) { if len(cmd.capabs) > 0 && !client.HasRoleCapabs(cmd.capabs...) {
rb.Add(nil, server.name, ERR_NOPRIVILEGES, client.Nick(), client.t("Permission Denied")) rb.Add(nil, server.name, ERR_NOPRIVILEGES, client.Nick(), client.t("Permission Denied"))
return false return false
@ -71,7 +59,7 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
} }
if client.registered { if client.registered {
client.Touch(session) // even if `exiting`, we bump the lastSeen timestamp client.Touch(session)
} }
return exiting return exiting
@ -93,10 +81,6 @@ var Commands map[string]Command
func init() { func init() {
Commands = map[string]Command{ Commands = map[string]Command{
"ACCEPT": {
handler: acceptHandler,
minParams: 1,
},
"AMBIANCE": { "AMBIANCE": {
handler: sceneHandler, handler: sceneHandler,
minParams: 2, minParams: 2,
@ -107,15 +91,18 @@ func init() {
minParams: 1, minParams: 1,
}, },
"AWAY": { "AWAY": {
handler: awayHandler, handler: awayHandler,
usablePreReg: true, minParams: 0,
minParams: 0,
}, },
"BATCH": { "BATCH": {
handler: batchHandler, handler: batchHandler,
minParams: 1, minParams: 1,
allowedInBatch: true, allowedInBatch: true,
}, },
"BRB": {
handler: brbHandler,
minParams: 0,
},
"CAP": { "CAP": {
handler: capHandler, handler: capHandler,
usablePreReg: true, usablePreReg: true,
@ -128,7 +115,7 @@ func init() {
"DEBUG": { "DEBUG": {
handler: debugHandler, handler: debugHandler,
minParams: 1, minParams: 1,
capabs: []string{"rehash"}, oper: true,
}, },
"DEFCON": { "DEFCON": {
handler: defconHandler, handler: defconHandler,
@ -137,11 +124,12 @@ func init() {
"DEOPER": { "DEOPER": {
handler: deoperHandler, handler: deoperHandler,
minParams: 0, minParams: 0,
oper: true,
}, },
"DLINE": { "DLINE": {
handler: dlineHandler, handler: dlineHandler,
minParams: 1, minParams: 1,
capabs: []string{"ban"}, oper: true,
}, },
"EXTJWT": { "EXTJWT": {
handler: extjwtHandler, handler: extjwtHandler,
@ -170,10 +158,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,
@ -185,12 +169,13 @@ func init() {
"KILL": { "KILL": {
handler: killHandler, handler: killHandler,
minParams: 1, minParams: 1,
capabs: []string{"kill"}, oper: true,
capabs: []string{"local_kill"}, //TODO(dan): when we have S2S, this will be checked in the command handler itself
}, },
"KLINE": { "KLINE": {
handler: klineHandler, handler: klineHandler,
minParams: 1, minParams: 1,
capabs: []string{"ban"}, oper: true,
}, },
"LANGUAGE": { "LANGUAGE": {
handler: languageHandler, handler: languageHandler,
@ -205,15 +190,6 @@ func init() {
handler: lusersHandler, handler: lusersHandler,
minParams: 0, minParams: 0,
}, },
"MARKREAD": {
handler: markReadHandler,
minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS
},
"METADATA": {
handler: metadataHandler,
minParams: 2,
usablePreReg: true,
},
"MODE": { "MODE": {
handler: modeHandler, handler: modeHandler,
minParams: 1, minParams: 1,
@ -261,10 +237,6 @@ func init() {
usablePreReg: true, usablePreReg: true,
minParams: 1, minParams: 1,
}, },
"PERSISTENCE": {
handler: persistenceHandler,
minParams: 1,
},
"PING": { "PING": {
handler: pingHandler, handler: pingHandler,
usablePreReg: true, usablePreReg: true,
@ -280,19 +252,15 @@ func init() {
minParams: 2, minParams: 2,
allowedInBatch: true, allowedInBatch: true,
}, },
"RELAYMSG": {
handler: relaymsgHandler,
minParams: 3,
},
"REGISTER": {
handler: registerHandler,
minParams: 3,
usablePreReg: true,
},
"RENAME": { "RENAME": {
handler: renameHandler, handler: renameHandler,
minParams: 2, minParams: 2,
}, },
"RESUME": {
handler: resumeHandler,
usablePreReg: true,
minParams: 1,
},
"SAJOIN": { "SAJOIN": {
handler: sajoinHandler, handler: sajoinHandler,
minParams: 1, minParams: 1,
@ -301,7 +269,7 @@ func init() {
"SANICK": { "SANICK": {
handler: sanickHandler, handler: sanickHandler,
minParams: 2, minParams: 2,
capabs: []string{"samode"}, oper: true,
}, },
"SAMODE": { "SAMODE": {
handler: modeHandler, handler: modeHandler,
@ -328,13 +296,10 @@ 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,
oper: true,
capabs: []string{"rehash"}, capabs: []string{"rehash"},
}, },
"TIME": { "TIME": {
@ -345,24 +310,15 @@ func init() {
handler: topicHandler, handler: topicHandler,
minParams: 1, minParams: 1,
}, },
"UBAN": {
handler: ubanHandler,
minParams: 1,
capabs: []string{"ban"},
},
"UNDLINE": { "UNDLINE": {
handler: unDLineHandler, handler: unDLineHandler,
minParams: 1, minParams: 1,
capabs: []string{"ban"}, oper: true,
},
"UNINVITE": {
handler: inviteHandler,
minParams: 2,
}, },
"UNKLINE": { "UNKLINE": {
handler: unKLineHandler, handler: unKLineHandler,
minParams: 1, minParams: 1,
capabs: []string{"ban"}, oper: true,
}, },
"USER": { "USER": {
handler: userHandler, handler: userHandler,
@ -376,11 +332,6 @@ func init() {
"USERS": { "USERS": {
handler: usersHandler, handler: usersHandler,
}, },
"VERIFY": {
handler: verifyHandler,
usablePreReg: true,
minParams: 2,
},
"VERSION": { "VERSION": {
handler: versionHandler, handler: versionHandler,
minParams: 0, minParams: 0,
@ -390,10 +341,6 @@ func init() {
usablePreReg: true, usablePreReg: true,
minParams: 4, minParams: 4,
}, },
"WEBPUSH": {
handler: webpushHandler,
minParams: 2,
},
"WHO": { "WHO": {
handler: whoHandler, handler: whoHandler,
minParams: 1, minParams: 1,

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +0,0 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package irc
import (
"reflect"
"testing"
)
func TestEnvironmentOverrides(t *testing.T) {
var config Config
config.Server.Compatibility.SendUnprefixedSasl = true
config.History.Enabled = true
defaultUserModes := "+i"
config.Accounts.DefaultUserModes = &defaultUserModes
config.Server.WebSockets.AllowedOrigins = []string{"https://www.ircv3.net"}
config.Server.MOTD = "long.motd.txt" // overwrite this
env := []string{
`USER=shivaram`, // unrelated var
`ORAGONO_USER=oragono`, // this should be ignored as well
`ERGO__NETWORK__NAME=example.com`,
`ORAGONO__SERVER__COMPATIBILITY__FORCE_TRAILING=false`,
`ORAGONO__SERVER__COERCE_IDENT="~user"`,
`ERGO__SERVER__MOTD=short.motd.txt`,
`ORAGONO__ACCOUNTS__NICK_RESERVATION__ENABLED=true`,
`ERGO__ACCOUNTS__DEFAULT_USER_MODES="+iR"`,
`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 {
_, _, err := mungeFromEnvironment(&config, envPair)
if err != nil {
t.Errorf("couldn't apply override `%s`: %v", envPair, err)
}
}
if config.Network.Name != "example.com" {
t.Errorf("unexpected value of network.name: %s", config.Network.Name)
}
if config.Server.CoerceIdent != "~user" {
t.Errorf("unexpected value of coerce-ident: %s", config.Server.CoerceIdent)
}
if config.Server.MOTD != "short.motd.txt" {
t.Errorf("unexpected value of motd: %s", config.Server.MOTD)
}
if !config.Accounts.NickReservation.Enabled {
t.Errorf("did not set bool as expected")
}
if !config.Server.Compatibility.SendUnprefixedSasl {
t.Errorf("overwrote unrelated field")
}
if !config.History.Enabled {
t.Errorf("overwrote unrelated field")
}
if !reflect.DeepEqual(config.Server.WebSockets.AllowedOrigins, []string{"https://www.ircv3.net"}) {
t.Errorf("overwrote unrelated field: %#v", config.Server.WebSockets.AllowedOrigins)
}
cloakConf := config.Server.Cloaks
if !(cloakConf.Enabled == true && cloakConf.EnabledForAlwaysOn == true && cloakConf.Netname == "irc" && cloakConf.CidrLenIPv6 == 64) {
t.Errorf("bad value of Cloaks: %#v", config.Server.Cloaks)
}
if *config.Server.Compatibility.ForceTrailing != false {
t.Errorf("couldn't set unset ptr field to false")
}
if *config.Accounts.DefaultUserModes != "+iR" {
t.Errorf("couldn't override pre-set ptr field")
}
}
func TestEnvironmentOverrideErrors(t *testing.T) {
var config Config
config.Server.Compatibility.SendUnprefixedSasl = true
config.History.Enabled = true
invalidEnvs := []string{
`ORAGONO__=asdf`,
`ORAGONO__SERVER__=asdf`,
`ORAGONO__SERVER____=asdf`,
`ORAGONO__NONEXISTENT_KEY=1`,
`ORAGONO__SERVER__NONEXISTENT_KEY=1`,
// invalid yaml:
`ORAGONO__SERVER__IP_CLOAKING__NETNAME="`,
// invalid type:
`ORAGONO__SERVER__IP_CLOAKING__NUM_BITS=asdf`,
`ORAGONO__SERVER__STS=[]`,
// index into non-struct:
`ORAGONO__NETWORK__NAME__QUX=1`,
// private field:
`ORAGONO__SERVER__PASSWORDBYTES="asdf"`,
}
for _, env := range invalidEnvs {
success, _, err := mungeFromEnvironment(&config, env)
if err == nil || success {
t.Errorf("accepted invalid env override `%s`", env)
}
}
}

View File

@ -4,14 +4,13 @@
package connection_limits package connection_limits
import ( import (
"crypto/md5"
"errors" "errors"
"fmt" "fmt"
"net"
"sync" "sync"
"time" "time"
"github.com/ergochat/ergo/irc/flatip" "github.com/oragono/oragono/irc/utils"
"github.com/ergochat/ergo/irc/utils"
) )
var ( var (
@ -20,23 +19,14 @@ var (
) )
type CustomLimitConfig struct { type CustomLimitConfig struct {
Nets []string
MaxConcurrent int `yaml:"max-concurrent-connections"` MaxConcurrent int `yaml:"max-concurrent-connections"`
MaxPerWindow int `yaml:"max-connections-per-window"` MaxPerWindow int `yaml:"max-connections-per-window"`
} }
// tuples the key-value pair of a CIDR and its custom limit/throttle values // tuples the key-value pair of a CIDR and its custom limit/throttle values
type customLimit struct { type customLimit struct {
name [16]byte CustomLimitConfig
customID string // operator-configured identifier for a custom net ipNet net.IPNet
maxConcurrent int
maxPerWindow int
nets []flatip.IPNet
}
type limiterKey struct {
maskedIP flatip.IP
prefixLen uint8 // 0 for the fake nets we generate for custom limits
} }
// LimiterConfig controls the automated connection limits. // LimiterConfig controls the automated connection limits.
@ -48,7 +38,8 @@ type rawLimiterConfig struct {
Throttle bool Throttle bool
Window time.Duration Window time.Duration
MaxPerWindow int `yaml:"max-connections-per-window"` MaxPerWindow int `yaml:"max-connections-per-window"`
BanDuration time.Duration `yaml:"throttle-ban-duration"`
CidrLenIPv4 int `yaml:"cidr-len-ipv4"` CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"` CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
@ -61,7 +52,9 @@ type rawLimiterConfig struct {
type LimiterConfig struct { type LimiterConfig struct {
rawLimiterConfig rawLimiterConfig
exemptedNets []flatip.IPNet ipv4Mask net.IPMask
ipv6Mask net.IPMask
exemptedNets []net.IPNet
customLimits []customLimit customLimits []customLimit
} }
@ -73,42 +66,25 @@ func (config *LimiterConfig) UnmarshalYAML(unmarshal func(interface{}) error) (e
} }
func (config *LimiterConfig) postprocess() (err error) { func (config *LimiterConfig) postprocess() (err error) {
exemptedNets, err := utils.ParseNetList(config.Exempted) config.exemptedNets, err = utils.ParseNetList(config.Exempted)
if err != nil { if err != nil {
return fmt.Errorf("Could not parse limiter exemption list: %v", err.Error()) return fmt.Errorf("Could not parse limiter exemption list: %v", err.Error())
} }
config.exemptedNets = make([]flatip.IPNet, len(exemptedNets))
for i, exempted := range exemptedNets {
config.exemptedNets[i] = flatip.FromNetIPNet(exempted)
}
for identifier, customLimitConf := range config.CustomLimits { for netStr, customLimitConf := range config.CustomLimits {
nets := make([]flatip.IPNet, len(customLimitConf.Nets)) normalizedNet, err := utils.NormalizedNetFromString(netStr)
for i, netStr := range customLimitConf.Nets { if err != nil {
normalizedNet, err := flatip.ParseToNormalizedNet(netStr) return fmt.Errorf("Could not parse custom limit specification: %v", err.Error())
if err != nil {
return fmt.Errorf("Bad net %s in custom-limits block %s: %w", netStr, identifier, err)
}
nets[i] = normalizedNet
}
if len(customLimitConf.Nets) == 0 {
// see #1421: this is the legacy config format where the
// dictionary key of the block is a CIDR string
normalizedNet, err := flatip.ParseToNormalizedNet(identifier)
if err != nil {
return fmt.Errorf("Custom limit block %s has no defined nets", identifier)
}
nets = []flatip.IPNet{normalizedNet}
} }
config.customLimits = append(config.customLimits, customLimit{ config.customLimits = append(config.customLimits, customLimit{
maxConcurrent: customLimitConf.MaxConcurrent, CustomLimitConfig: customLimitConf,
maxPerWindow: customLimitConf.MaxPerWindow, ipNet: normalizedNet,
name: md5.Sum([]byte(identifier)),
customID: identifier,
nets: nets,
}) })
} }
config.ipv4Mask = net.CIDRMask(config.CidrLenIPv4, 32)
config.ipv6Mask = net.CIDRMask(config.CidrLenIPv6, 128)
return nil return nil
} }
@ -119,56 +95,51 @@ type Limiter struct {
config *LimiterConfig config *LimiterConfig
// IP/CIDR -> count of clients connected from there: // IP/CIDR -> count of clients connected from there:
limiter map[limiterKey]int limiter map[string]int
// IP/CIDR -> throttle state: // IP/CIDR -> throttle state:
throttler map[limiterKey]ThrottleDetails throttler map[string]ThrottleDetails
} }
// addrToKey canonicalizes `addr` to a string key, and returns // addrToKey canonicalizes `addr` to a string key, and returns
// the relevant connection limit and throttle max-per-window values // the relevant connection limit and throttle max-per-window values
func (cl *Limiter) addrToKey(addr flatip.IP) (key limiterKey, customID string, limit int, throttle int) { func (cl *Limiter) addrToKey(addr net.IP) (key string, limit int, throttle int) {
// `key` will be a CIDR string like "8.8.8.8/32" or "2001:0db8::/32"
for _, custom := range cl.config.customLimits { for _, custom := range cl.config.customLimits {
for _, net := range custom.nets { if custom.ipNet.Contains(addr) {
if net.Contains(addr) { return custom.ipNet.String(), custom.MaxConcurrent, custom.MaxPerWindow
return limiterKey{maskedIP: custom.name, prefixLen: 0}, custom.customID, custom.maxConcurrent, custom.maxPerWindow
}
} }
} }
var prefixLen int var ipNet net.IPNet
if addr.IsIPv4() { addrv4 := addr.To4()
prefixLen = cl.config.CidrLenIPv4 if addrv4 != nil {
addr = addr.Mask(prefixLen, 32) ipNet = net.IPNet{
prefixLen += 96 IP: addrv4.Mask(cl.config.ipv4Mask),
Mask: cl.config.ipv4Mask,
}
} else { } else {
prefixLen = cl.config.CidrLenIPv6 ipNet = net.IPNet{
addr = addr.Mask(prefixLen, 128) IP: addr.Mask(cl.config.ipv6Mask),
Mask: cl.config.ipv6Mask,
}
} }
return ipNet.String(), cl.config.MaxConcurrent, cl.config.MaxPerWindow
return limiterKey{maskedIP: addr, prefixLen: uint8(prefixLen)}, "", cl.config.MaxConcurrent, cl.config.MaxPerWindow
} }
// AddClient adds a client to our population if possible. If we can't, throws an error instead. // AddClient adds a client to our population if possible. If we can't, throws an error instead.
func (cl *Limiter) AddClient(addr flatip.IP) error { func (cl *Limiter) AddClient(addr net.IP) error {
cl.Lock() cl.Lock()
defer cl.Unlock() defer cl.Unlock()
// we don't track populations for exempted addresses or nets - this is by design // we don't track populations for exempted addresses or nets - this is by design
if flatip.IPInNets(addr, cl.config.exemptedNets) { if utils.IPInNets(addr, cl.config.exemptedNets) {
return nil return nil
} }
addrString, _, maxConcurrent, maxPerWindow := cl.addrToKey(addr) addrString, maxConcurrent, maxPerWindow := cl.addrToKey(addr)
// check limiter
var count int
if cl.config.Count {
count = cl.limiter[addrString] + 1
if count > maxConcurrent {
return ErrLimitExceeded
}
}
// XXX check throttle first; if we checked limit first and then checked throttle,
// we'd have to decrement the limit on an unsuccessful throttle check
if cl.config.Throttle { if cl.config.Throttle {
details := cl.throttler[addrString] // retrieve mutable throttle state from the map details := cl.throttler[addrString] // retrieve mutable throttle state from the map
// add in constant state to process the limiting operation // add in constant state to process the limiting operation
@ -180,13 +151,16 @@ func (cl *Limiter) AddClient(addr flatip.IP) error {
throttled, _ := g.Touch() // actually check the limit throttled, _ := g.Touch() // actually check the limit
cl.throttler[addrString] = g.ThrottleDetails // store modified mutable state cl.throttler[addrString] = g.ThrottleDetails // store modified mutable state
if throttled { if throttled {
// back out the limiter add
return ErrThrottleExceeded return ErrThrottleExceeded
} }
} }
// success, record in limiter // now check limiter
if cl.config.Count { if cl.config.Count {
count := cl.limiter[addrString] + 1
if count > maxConcurrent {
return ErrLimitExceeded
}
cl.limiter[addrString] = count cl.limiter[addrString] = count
} }
@ -194,15 +168,15 @@ func (cl *Limiter) AddClient(addr flatip.IP) error {
} }
// RemoveClient removes the given address from our population // RemoveClient removes the given address from our population
func (cl *Limiter) RemoveClient(addr flatip.IP) { func (cl *Limiter) RemoveClient(addr net.IP) {
cl.Lock() cl.Lock()
defer cl.Unlock() defer cl.Unlock()
if !cl.config.Count || flatip.IPInNets(addr, cl.config.exemptedNets) { if !cl.config.Count || utils.IPInNets(addr, cl.config.exemptedNets) {
return return
} }
addrString, _, _, _ := cl.addrToKey(addr) addrString, _, _ := cl.addrToKey(addr)
count := cl.limiter[addrString] count := cl.limiter[addrString]
count -= 1 count -= 1
if count < 0 { if count < 0 {
@ -211,56 +185,16 @@ func (cl *Limiter) RemoveClient(addr flatip.IP) {
cl.limiter[addrString] = count cl.limiter[addrString] = count
} }
type LimiterStatus struct {
Exempt bool
Count int
MaxCount int
Throttle int
MaxPerWindow int
ThrottleDuration time.Duration
}
func (cl *Limiter) Status(addr flatip.IP) (netName string, status LimiterStatus) {
cl.Lock()
defer cl.Unlock()
if flatip.IPInNets(addr, cl.config.exemptedNets) {
status.Exempt = true
return
}
status.ThrottleDuration = cl.config.Window
limiterKey, customID, maxConcurrent, maxPerWindow := cl.addrToKey(addr)
status.MaxCount = maxConcurrent
status.MaxPerWindow = maxPerWindow
status.Count = cl.limiter[limiterKey]
status.Throttle = cl.throttler[limiterKey].Count
netName = customID
if netName == "" {
netName = flatip.IPNet{
IP: limiterKey.maskedIP,
PrefixLen: limiterKey.prefixLen,
}.String()
}
return
}
// ResetThrottle resets the throttle count for an IP // ResetThrottle resets the throttle count for an IP
func (cl *Limiter) ResetThrottle(addr flatip.IP) { func (cl *Limiter) ResetThrottle(addr net.IP) {
cl.Lock() cl.Lock()
defer cl.Unlock() defer cl.Unlock()
if !cl.config.Throttle || flatip.IPInNets(addr, cl.config.exemptedNets) { if !cl.config.Throttle || utils.IPInNets(addr, cl.config.exemptedNets) {
return return
} }
addrString, _, _, _ := cl.addrToKey(addr) addrString, _, _ := cl.addrToKey(addr)
delete(cl.throttler, addrString) delete(cl.throttler, addrString)
} }
@ -270,10 +204,10 @@ func (cl *Limiter) ApplyConfig(config *LimiterConfig) {
defer cl.Unlock() defer cl.Unlock()
if cl.limiter == nil { if cl.limiter == nil {
cl.limiter = make(map[limiterKey]int) cl.limiter = make(map[string]int)
} }
if cl.throttler == nil { if cl.throttler == nil {
cl.throttler = make(map[limiterKey]ThrottleDetails) cl.throttler = make(map[string]ThrottleDetails)
} }
cl.config = config cl.config = config

View File

@ -4,17 +4,15 @@
package connection_limits package connection_limits
import ( import (
"crypto/md5" "net"
"testing" "testing"
"time" "time"
"github.com/ergochat/ergo/irc/flatip"
) )
func easyParseIP(ipstr string) (result flatip.IP) { func easyParseIP(ipstr string) (result net.IP) {
result, err := flatip.ParseIP(ipstr) result = net.ParseIP(ipstr)
if err != nil { if result == nil {
panic(err) panic(ipstr)
} }
return return
} }
@ -34,8 +32,7 @@ var baseConfig = LimiterConfig{
Exempted: []string{"localhost"}, Exempted: []string{"localhost"},
CustomLimits: map[string]CustomLimitConfig{ CustomLimits: map[string]CustomLimitConfig{
"google": { "8.8.0.0/16": {
Nets: []string{"8.8.0.0/16"},
MaxConcurrent: 128, MaxConcurrent: 128,
MaxPerWindow: 256, MaxPerWindow: 256,
}, },
@ -49,23 +46,18 @@ func TestKeying(t *testing.T) {
var limiter Limiter var limiter Limiter
limiter.ApplyConfig(&config) limiter.ApplyConfig(&config)
// an ipv4 /32 looks like a /128 to us after applying the 4-in-6 mapping key, maxConc, maxWin := limiter.addrToKey(easyParseIP("1.1.1.1"))
key, _, maxConc, maxWin := limiter.addrToKey(easyParseIP("1.1.1.1")) assertEqual(key, "1.1.1.1/32", t)
assertEqual(key.prefixLen, uint8(128), t)
assertEqual(key.maskedIP[12:], []byte{1, 1, 1, 1}, t)
assertEqual(maxConc, 4, t) assertEqual(maxConc, 4, t)
assertEqual(maxWin, 8, t) assertEqual(maxWin, 8, t)
testIPv6 := easyParseIP("2607:5301:201:3100::7426") key, maxConc, maxWin = limiter.addrToKey(easyParseIP("2607:5301:201:3100::7426"))
key, _, maxConc, maxWin = limiter.addrToKey(testIPv6) assertEqual(key, "2607:5301:201:3100::/64", t)
assertEqual(key.prefixLen, uint8(64), t)
assertEqual(flatip.IP(key.maskedIP), easyParseIP("2607:5301:201:3100::"), t)
assertEqual(maxConc, 4, t) assertEqual(maxConc, 4, t)
assertEqual(maxWin, 8, t) assertEqual(maxWin, 8, t)
key, _, maxConc, maxWin = limiter.addrToKey(easyParseIP("8.8.4.4")) key, maxConc, maxWin = limiter.addrToKey(easyParseIP("8.8.4.4"))
assertEqual(key.prefixLen, uint8(0), t) assertEqual(key, "8.8.0.0/16", t)
assertEqual([16]byte(key.maskedIP), md5.Sum([]byte("google")), t)
assertEqual(maxConc, 128, t) assertEqual(maxConc, 128, t)
assertEqual(maxWin, 256, t) assertEqual(maxWin, 256, t)
} }

View File

@ -4,6 +4,7 @@
package connection_limits package connection_limits
import ( import (
"net"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@ -82,7 +83,7 @@ func makeTestThrottler(v4len, v6len int) *Limiter {
func TestConnectionThrottle(t *testing.T) { func TestConnectionThrottle(t *testing.T) {
throttler := makeTestThrottler(32, 64) throttler := makeTestThrottler(32, 64)
addr := easyParseIP("8.8.8.8") addr := net.ParseIP("8.8.8.8")
for i := 0; i < 3; i += 1 { for i := 0; i < 3; i += 1 {
err := throttler.AddClient(addr) err := throttler.AddClient(addr)
@ -96,14 +97,14 @@ func TestConnectionThrottleIPv6(t *testing.T) {
throttler := makeTestThrottler(32, 64) throttler := makeTestThrottler(32, 64)
var err error var err error
err = throttler.AddClient(easyParseIP("2001:0db8::1")) err = throttler.AddClient(net.ParseIP("2001:0db8::1"))
assertEqual(err, nil, t) assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("2001:0db8::2")) err = throttler.AddClient(net.ParseIP("2001:0db8::2"))
assertEqual(err, nil, t) assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("2001:0db8::3")) err = throttler.AddClient(net.ParseIP("2001:0db8::3"))
assertEqual(err, nil, t) assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("2001:0db8::4")) err = throttler.AddClient(net.ParseIP("2001:0db8::4"))
assertEqual(err, ErrThrottleExceeded, t) assertEqual(err, ErrThrottleExceeded, t)
} }
@ -111,13 +112,13 @@ func TestConnectionThrottleIPv4(t *testing.T) {
throttler := makeTestThrottler(24, 64) throttler := makeTestThrottler(24, 64)
var err error var err error
err = throttler.AddClient(easyParseIP("192.168.1.101")) err = throttler.AddClient(net.ParseIP("192.168.1.101"))
assertEqual(err, nil, t) assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("192.168.1.102")) err = throttler.AddClient(net.ParseIP("192.168.1.102"))
assertEqual(err, nil, t) assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("192.168.1.103")) err = throttler.AddClient(net.ParseIP("192.168.1.103"))
assertEqual(err, nil, t) assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("192.168.1.104")) err = throttler.AddClient(net.ParseIP("192.168.1.104"))
assertEqual(err, ErrThrottleExceeded, t) assertEqual(err, ErrThrottleExceeded, t)
} }

View File

@ -5,7 +5,6 @@
package irc package irc
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@ -14,61 +13,45 @@ import (
"strings" "strings"
"time" "time"
"github.com/ergochat/ergo/irc/bunt" "github.com/oragono/oragono/irc/modes"
"github.com/ergochat/ergo/irc/datastore" "github.com/oragono/oragono/irc/utils"
"github.com/ergochat/ergo/irc/modes"
"github.com/ergochat/ergo/irc/utils"
"github.com/ergochat/ergo/irc/webpush"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
) )
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 = 24 latestDbSchema = "12"
)
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
vapidKeysUUID = utils.UUID{87, 215, 189, 5, 65, 105, 249, 44, 65, 96, 170, 56, 187, 110, 12, 235} // V9e9BUFp-SxBYKo4u24M6w
keySchemaVersion = bunt.BuntKey(datastore.TableMetadata, schemaVersionUUID)
keyCloakSecret = bunt.BuntKey(datastore.TableMetadata, cloakSecretUUID)
keyVAPIDKeys = bunt.BuntKey(datastore.TableMetadata, vapidKeysUUID)
) )
type SchemaChanger func(*Config, *buntdb.Tx) error type SchemaChanger func(*Config, *buntdb.Tx) error
type SchemaChange struct { type SchemaChange struct {
InitialVersion int // the change will take this version InitialVersion string // the change will take this version
TargetVersion int // and transform it into this version TargetVersion string // and transform it into this version
Changer SchemaChanger Changer SchemaChanger
} }
func checkDBReadyForInit(path string) error { // maps an initial version to a schema change capable of upgrading it
_, err := os.Stat(path) var schemaChanges map[string]SchemaChange
if err == nil {
return fmt.Errorf("Datastore already exists (delete it manually to continue): %s", path)
} else if !os.IsNotExist(err) {
return fmt.Errorf("Datastore path %s is inaccessible: %w", path, err)
}
return nil
}
// InitDB creates the database, implementing the `oragono initdb` command. // InitDB creates the database, implementing the `oragono initdb` command.
func InitDB(path string) error { func InitDB(path string) {
if err := checkDBReadyForInit(path); err != nil { _, err := os.Stat(path)
return err if err == nil {
log.Fatal("Datastore already exists (delete it manually to continue): ", path)
} else if !os.IsNotExist(err) {
log.Fatal("Datastore path is inaccessible: ", err.Error())
} }
if err := initializeDB(path); err != nil { err = initializeDB(path)
return fmt.Errorf("Could not save datastore: %w", err) if err != nil {
log.Fatal("Could not save datastore: ", err.Error())
} }
return nil
} }
// internal database initialization code // internal database initialization code
@ -81,17 +64,8 @@ func initializeDB(path string) error {
err = store.Update(func(tx *buntdb.Tx) error { err = store.Update(func(tx *buntdb.Tx) error {
// set schema version // set schema version
tx.Set(keySchemaVersion, strconv.Itoa(latestDbSchema), nil) tx.Set(keySchemaVersion, latestDbSchema, nil)
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil) tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
vapidKeys, err := webpush.GenerateVAPIDKeys()
if err != nil {
return err
}
j, err := json.Marshal(vapidKeys)
if err != nil {
return err
}
tx.Set(keyVAPIDKeys, string(j), nil)
return nil return nil
}) })
@ -118,9 +92,9 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
}() }()
// read the current version string // read the current version string
var version int var version string
err = db.View(func(tx *buntdb.Tx) (err error) { err = db.View(func(tx *buntdb.Tx) error {
version, err = retrieveSchemaVersion(tx) version, err = tx.Get(keySchemaVersion)
return err return err
}) })
if err != nil { if err != nil {
@ -148,22 +122,11 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
} }
} }
func retrieveSchemaVersion(tx *buntdb.Tx) (version int, err error) { func performAutoUpgrade(currentVersion string, config *Config) (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) {
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 %s to %s\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%s.%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)
if err != nil { if err != nil {
@ -193,35 +156,29 @@ func UpgradeDB(config *Config) (err error) {
} }
defer store.Close() defer store.Close()
var version int var version string
err = store.Update(func(tx *buntdb.Tx) error { err = store.Update(func(tx *buntdb.Tx) error {
for { for {
if version == 0 { version, _ = tx.Get(keySchemaVersion)
version, err = retrieveSchemaVersion(tx) change, schemaNeedsChange := schemaChanges[version]
if err != nil { if !schemaNeedsChange {
return err if version == latestDbSchema {
// success!
break
} }
}
if version == latestDbSchema {
// success!
break
}
change, ok := getSchemaChange(version)
if !ok {
// unable to upgrade to the desired version, roll back // unable to upgrade to the desired version, roll back
return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema} return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
} }
log.Printf("attempting to update schema from version %d\n", version) log.Println("attempting to update schema from version " + version)
err := change.Changer(config, tx) err := change.Changer(config, tx)
if err != nil { if err != nil {
return err return err
} }
version = change.TargetVersion _, _, err = tx.Set(keySchemaVersion, 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.Println("successfully updated schema to version " + change.TargetVersion)
} }
return nil return nil
}) })
@ -232,27 +189,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 return nil
} })
return string(val), nil return
} }
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 LoadVAPIDKeys(dstore datastore.Datastore) (*webpush.VAPIDKeys, error) {
val, err := dstore.Get(datastore.TableMetadata, vapidKeysUUID)
if err != nil {
return nil, err
}
result := new(webpush.VAPIDKeys)
err = json.Unmarshal([]byte(val), result)
return result, nil
} }
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error { func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
@ -737,646 +686,68 @@ func schemaChangeV11ToV12(config *Config, tx *buntdb.Tx) error {
return nil return nil
} }
type accountCredsLegacyV13 struct { func init() {
Version CredentialsVersion allChanges := []SchemaChange{
PassphraseHash []byte {
Certfps []string InitialVersion: "1",
} TargetVersion: "2",
Changer: schemaChangeV1toV2,
// see #212 / #284. this packs the legacy salts into a single passphrase hash, },
// allowing legacy passphrases to be verified using the new API `checkLegacyPassphrase`. {
func schemaChangeV12ToV13(config *Config, tx *buntdb.Tx) error { InitialVersion: "2",
salt, err := tx.Get("crypto.salt") TargetVersion: "3",
if err != nil { Changer: schemaChangeV2ToV3,
return nil // no change required },
} {
tx.Delete("crypto.salt") InitialVersion: "3",
rawSalt, err := base64.StdEncoding.DecodeString(salt) TargetVersion: "4",
if err != nil { Changer: schemaChangeV3ToV4,
return nil // just throw away the creds at this point },
} {
prefix := "account.credentials " InitialVersion: "4",
var accounts []string TargetVersion: "5",
var credentials []accountCredsLegacyV13 Changer: schemaChangeV4ToV5,
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { },
if !strings.HasPrefix(key, prefix) { {
return false InitialVersion: "5",
} TargetVersion: "6",
account := strings.TrimPrefix(key, prefix) Changer: schemaChangeV5ToV6,
},
var credsOld accountCredsLegacyV9 {
err = json.Unmarshal([]byte(value), &credsOld) InitialVersion: "6",
if err != nil { TargetVersion: "7",
return true Changer: schemaChangeV6ToV7,
} },
// skip if these aren't legacy creds! {
if credsOld.Version != 0 { InitialVersion: "7",
return true TargetVersion: "8",
} Changer: schemaChangeV7ToV8,
},
var credsNew accountCredsLegacyV13 {
credsNew.Version = 0 // mark hash for migration InitialVersion: "8",
credsNew.Certfps = credsOld.Certfps TargetVersion: "9",
credsNew.PassphraseHash = append(credsNew.PassphraseHash, rawSalt...) Changer: schemaChangeV8ToV9,
credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseSalt...) },
credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseHash...) {
InitialVersion: "9",
accounts = append(accounts, account) TargetVersion: "10",
credentials = append(credentials, credsNew) Changer: schemaChangeV9ToV10,
return true },
}) {
InitialVersion: "10",
for i, account := range accounts { TargetVersion: "11",
bytesOut, err := json.Marshal(credentials[i]) Changer: schemaChangeV10ToV11,
if err != nil { },
return err {
} InitialVersion: "11",
_, _, err = tx.Set(prefix+account, string(bytesOut), nil) TargetVersion: "12",
if err != nil { Changer: schemaChangeV11ToV12,
return err },
}
} }
return nil // build the index
} schemaChanges = make(map[string]SchemaChange)
// channel registration time and topic set time at nanosecond resolution
func schemaChangeV13ToV14(config *Config, tx *buntdb.Tx) error {
prefix := "channel.registered.time "
var channels, times []string
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
channel := strings.TrimPrefix(key, prefix)
channels = append(channels, channel)
times = append(times, value)
return true
})
billion := int64(time.Second)
for i, channel := range channels {
regTime, err := strconv.ParseInt(times[i], 10, 64)
if err != nil {
log.Printf("corrupt registration time entry for %s: %v\n", channel, err)
continue
}
regTime = regTime * billion
tx.Set(prefix+channel, strconv.FormatInt(regTime, 10), nil)
topicTimeKey := "channel.topic.settime " + channel
topicSetAt, err := tx.Get(topicTimeKey)
if err == nil {
if setTime, err := strconv.ParseInt(topicSetAt, 10, 64); err == nil {
tx.Set(topicTimeKey, strconv.FormatInt(setTime*billion, 10), nil)
}
}
}
return nil
}
// #1327: delete any invalid klines
func schemaChangeV14ToV15(config *Config, tx *buntdb.Tx) error {
prefix := "bans.klinev2 "
var keys []string
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
if key != strings.TrimSpace(key) {
keys = append(keys, key)
}
return true
})
// don't bother trying to fix these up
for _, key := range keys {
tx.Delete(key)
}
return nil
}
// #1330: delete any stale realname records
func schemaChangeV15ToV16(config *Config, tx *buntdb.Tx) error {
prefix := "account.realname "
verifiedPrefix := "account.verified "
var keys []string
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
acct := strings.TrimPrefix(key, prefix)
verifiedKey := verifiedPrefix + acct
_, verifiedErr := tx.Get(verifiedKey)
if verifiedErr != nil {
keys = append(keys, key)
}
return true
})
for _, key := range keys {
tx.Delete(key)
}
return nil
}
// #1346: remove vhost request queue
func schemaChangeV16ToV17(config *Config, tx *buntdb.Tx) error {
prefix := "vhostQueue "
var keys []string
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
keys = append(keys, key)
return true
})
for _, key := range keys {
tx.Delete(key)
}
return nil
}
// #1274: we used to suspend accounts by deleting their "verified" key,
// now we save some metadata under a new key
func schemaChangeV17ToV18(config *Config, tx *buntdb.Tx) error {
now := time.Now().UTC()
exists := "account.exists "
suspended := "account.suspended "
verif := "account.verified "
verifCode := "account.verificationcode "
var accounts []string
tx.AscendGreaterOrEqual("", exists, func(key, value string) bool {
if !strings.HasPrefix(key, exists) {
return false
}
account := strings.TrimPrefix(key, exists)
_, verifiedErr := tx.Get(verif + account)
_, verifCodeErr := tx.Get(verifCode + account)
if verifiedErr != nil && verifCodeErr != nil {
// verified key not present, but there's no code either,
// this is a suspension
accounts = append(accounts, account)
}
return true
})
type accountSuspensionV18 struct {
TimeCreated time.Time
Duration time.Duration
OperName string
Reason string
}
for _, account := range accounts {
var sus accountSuspensionV18
sus.TimeCreated = now
sus.OperName = "*"
sus.Reason = "[unknown]"
susBytes, err := json.Marshal(sus)
if err != nil {
return err
}
tx.Set(suspended+account, string(susBytes), nil)
}
return nil
}
// #1345: persist the channel-user modes of always-on clients
func schemaChangeV18To19(config *Config, tx *buntdb.Tx) error {
channelToAmodesCache := make(map[string]map[string]modes.Mode)
joinedto := "account.joinedto "
var accounts []string
var channels [][]string
tx.AscendGreaterOrEqual("", joinedto, func(key, value string) bool {
if !strings.HasPrefix(key, joinedto) {
return false
}
accounts = append(accounts, strings.TrimPrefix(key, joinedto))
var ch []string
if value != "" {
ch = strings.Split(value, ",")
}
channels = append(channels, ch)
return true
})
for i := 0; i < len(accounts); i++ {
account := accounts[i]
channels := channels[i]
tx.Delete(joinedto + account)
newValue := make(map[string]string, len(channels))
for _, channel := range channels {
chcfname, err := CasefoldChannel(channel)
if err != nil {
continue
}
// get amodes from the channelToAmodesCache, fill if necessary
amodes, ok := channelToAmodesCache[chcfname]
if !ok {
amodeStr, _ := tx.Get("channel.accounttoumode " + chcfname)
if amodeStr != "" {
jErr := json.Unmarshal([]byte(amodeStr), &amodes)
if jErr != nil {
log.Printf("error retrieving amodes for %s: %v\n", channel, jErr)
amodes = nil
}
}
// setting/using the nil value here is ok
channelToAmodesCache[chcfname] = amodes
}
if mode, ok := amodes[account]; ok {
newValue[channel] = string(mode)
} else {
newValue[channel] = ""
}
}
newValueBytes, jErr := json.Marshal(newValue)
if jErr != nil {
log.Printf("couldn't serialize new mode values for v19: %v\n", jErr)
continue
}
tx.Set("account.channeltomodes "+account, string(newValueBytes), nil)
}
return nil
}
// #1490: start tracking join times for always-on clients
func schemaChangeV19To20(config *Config, tx *buntdb.Tx) error {
type joinData struct {
Modes string
JoinTime int64
}
var accounts []string
var data []string
now := time.Now().UnixNano()
prefix := "account.channeltomodes "
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
accounts = append(accounts, strings.TrimPrefix(key, prefix))
data = append(data, value)
return true
})
for i, account := range accounts {
var existingMap map[string]string
err := json.Unmarshal([]byte(data[i]), &existingMap)
if err != nil {
return err
}
newMap := make(map[string]joinData)
for channel, modeStr := range existingMap {
newMap[channel] = joinData{
Modes: modeStr,
JoinTime: now,
}
}
serialized, err := json.Marshal(newMap)
if err != nil {
return err
}
tx.Set(prefix+account, string(serialized), nil)
}
return nil
}
// #734: move the email address into the settings object,
// giving people a way to change it
func schemaChangeV20To21(config *Config, tx *buntdb.Tx) error {
type accountSettingsv21 struct {
AutoreplayLines *int
NickEnforcement NickEnforcementMethod
AllowBouncer MulticlientAllowedSetting
ReplayJoins ReplayJoinsSetting
AlwaysOn PersistentStatus
AutoreplayMissed bool
DMHistory HistoryStatus
AutoAway PersistentStatus
Email string
}
var accounts []string
var emails []string
callbackPrefix := "account.callback "
tx.AscendGreaterOrEqual("", callbackPrefix, func(key, value string) bool {
if !strings.HasPrefix(key, callbackPrefix) {
return false
}
account := strings.TrimPrefix(key, callbackPrefix)
if _, err := tx.Get("account.verified " + account); err != nil {
return true
}
if strings.HasPrefix(value, "mailto:") {
accounts = append(accounts, account)
emails = append(emails, strings.TrimPrefix(value, "mailto:"))
}
return true
})
for i, account := range accounts {
var settings accountSettingsv21
email := emails[i]
settingsKey := "account.settings " + account
settingsStr, err := tx.Get(settingsKey)
if err == nil && settingsStr != "" {
json.Unmarshal([]byte(settingsStr), &settings)
}
settings.Email = email
settingsBytes, err := json.Marshal(settings)
if err != nil {
log.Printf("couldn't marshal settings for %s: %v\n", account, err)
} else {
tx.Set(settingsKey, string(settingsBytes), nil)
}
tx.Delete(callbackPrefix + account)
}
return nil
}
// #1676: we used to have ReplayJoinsNever, now it's desupported
func schemaChangeV21To22(config *Config, tx *buntdb.Tx) error {
type accountSettingsv22 struct {
AutoreplayLines *int
NickEnforcement NickEnforcementMethod
AllowBouncer MulticlientAllowedSetting
ReplayJoins ReplayJoinsSetting
AlwaysOn PersistentStatus
AutoreplayMissed bool
DMHistory HistoryStatus
AutoAway PersistentStatus
Email string
}
var accounts []string
var serializedSettings []string
settingsPrefix := "account.settings "
tx.AscendGreaterOrEqual("", settingsPrefix, func(key, value string) bool {
if !strings.HasPrefix(key, settingsPrefix) {
return false
}
if value == "" {
return true
}
account := strings.TrimPrefix(key, settingsPrefix)
if _, err := tx.Get("account.verified " + account); err != nil {
return true
}
var settings accountSettingsv22
err := json.Unmarshal([]byte(value), &settings)
if err != nil {
log.Printf("error (v21-22) processing settings for %s: %v\n", account, err)
return true
}
// if necessary, change ReplayJoinsNever (2) to ReplayJoinsCommandsOnly (0)
if settings.ReplayJoins == ReplayJoinsSetting(2) {
settings.ReplayJoins = ReplayJoinsSetting(0)
if b, err := json.Marshal(settings); err == nil {
accounts = append(accounts, account)
serializedSettings = append(serializedSettings, string(b))
} else {
log.Printf("error (v21-22) processing settings for %s: %v\n", account, err)
}
}
return true
})
for i, account := range accounts {
tx.Set(settingsPrefix+account, serializedSettings[i], 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
}
// webpush signing key
func schemaChangeV23ToV24(config *Config, tx *buntdb.Tx) error {
keys, err := webpush.GenerateVAPIDKeys()
if err != nil {
return err
}
j, err := json.Marshal(keys)
if err != nil {
return err
}
tx.Set(keyVAPIDKeys, string(j), nil)
return nil
}
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
for _, change := range allChanges { for _, change := range allChanges {
if initialVersion == change.InitialVersion { schemaChanges[change.InitialVersion] = change
return change, true
}
} }
return
}
var allChanges = []SchemaChange{
{
InitialVersion: 1,
TargetVersion: 2,
Changer: schemaChangeV1toV2,
},
{
InitialVersion: 2,
TargetVersion: 3,
Changer: schemaChangeV2ToV3,
},
{
InitialVersion: 3,
TargetVersion: 4,
Changer: schemaChangeV3ToV4,
},
{
InitialVersion: 4,
TargetVersion: 5,
Changer: schemaChangeV4ToV5,
},
{
InitialVersion: 5,
TargetVersion: 6,
Changer: schemaChangeV5ToV6,
},
{
InitialVersion: 6,
TargetVersion: 7,
Changer: schemaChangeV6ToV7,
},
{
InitialVersion: 7,
TargetVersion: 8,
Changer: schemaChangeV7ToV8,
},
{
InitialVersion: 8,
TargetVersion: 9,
Changer: schemaChangeV8ToV9,
},
{
InitialVersion: 9,
TargetVersion: 10,
Changer: schemaChangeV9ToV10,
},
{
InitialVersion: 10,
TargetVersion: 11,
Changer: schemaChangeV10ToV11,
},
{
InitialVersion: 11,
TargetVersion: 12,
Changer: schemaChangeV11ToV12,
},
{
InitialVersion: 12,
TargetVersion: 13,
Changer: schemaChangeV12ToV13,
},
{
InitialVersion: 13,
TargetVersion: 14,
Changer: schemaChangeV13ToV14,
},
{
InitialVersion: 14,
TargetVersion: 15,
Changer: schemaChangeV14ToV15,
},
{
InitialVersion: 15,
TargetVersion: 16,
Changer: schemaChangeV15ToV16,
},
{
InitialVersion: 16,
TargetVersion: 17,
Changer: schemaChangeV16ToV17,
},
{
InitialVersion: 17,
TargetVersion: 18,
Changer: schemaChangeV17ToV18,
},
{
InitialVersion: 18,
TargetVersion: 19,
Changer: schemaChangeV18To19,
},
{
InitialVersion: 19,
TargetVersion: 20,
Changer: schemaChangeV19To20,
},
{
InitialVersion: 20,
TargetVersion: 21,
Changer: schemaChangeV20To21,
},
{
InitialVersion: 21,
TargetVersion: 22,
Changer: schemaChangeV21To22,
},
{
InitialVersion: 22,
TargetVersion: 23,
Changer: schemaChangeV22ToV23,
},
{
InitialVersion: 23,
TargetVersion: 24,
Changer: schemaChangeV23ToV24,
},
} }

View File

@ -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
}

View File

@ -6,11 +6,12 @@ package irc
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/ergochat/ergo/irc/flatip" "github.com/oragono/oragono/irc/utils"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
) )
@ -20,8 +21,6 @@ const (
// IPBanInfo holds info about an IP/net ban. // IPBanInfo holds info about an IP/net ban.
type IPBanInfo struct { type IPBanInfo struct {
// RequireSASL indicates a "soft" ban; connections are allowed but they must SASL
RequireSASL bool
// Reason is the ban reason. // Reason is the ban reason.
Reason string `json:"reason"` Reason string `json:"reason"`
// OperReason is an oper ban reason. // OperReason is an oper ban reason.
@ -48,33 +47,41 @@ func (info IPBanInfo) TimeLeft() string {
// BanMessage returns the ban message. // BanMessage returns the ban message.
func (info IPBanInfo) BanMessage(message string) string { func (info IPBanInfo) BanMessage(message string) string {
reason := info.Reason message = fmt.Sprintf(message, info.Reason)
if reason == "" {
reason = "No reason given"
}
message = fmt.Sprintf(message, reason)
if info.Duration != 0 { if info.Duration != 0 {
message += fmt.Sprintf(" [%s]", info.TimeLeft()) message += fmt.Sprintf(" [%s]", info.TimeLeft())
} }
return message return message
} }
// dLineNet contains the net itself and expiration time for a given network.
type dLineNet struct {
// Network is the network that is blocked.
// This is always an IPv6 CIDR; IPv4 CIDRs are translated with the 4-in-6 prefix,
// individual IPv4 and IPV6 addresses are translated to the relevant /128.
Network net.IPNet
// Info contains information on the ban.
Info IPBanInfo
}
// DLineManager manages and dlines. // DLineManager manages and dlines.
type DLineManager struct { type DLineManager struct {
sync.RWMutex // tier 1 sync.RWMutex // tier 1
persistenceMutex sync.Mutex // tier 2 persistenceMutex sync.Mutex // tier 2
// networks that are dlined: // networks that are dlined:
networks map[flatip.IPNet]IPBanInfo // XXX: the keys of this map (which are also the database persistence keys)
// are the human-readable representations returned by NetToNormalizedString
networks map[string]dLineNet
// this keeps track of expiration timers for temporary bans // this keeps track of expiration timers for temporary bans
expirationTimers map[flatip.IPNet]*time.Timer expirationTimers map[string]*time.Timer
server *Server server *Server
} }
// NewDLineManager returns a new DLineManager. // NewDLineManager returns a new DLineManager.
func NewDLineManager(server *Server) *DLineManager { func NewDLineManager(server *Server) *DLineManager {
var dm DLineManager var dm DLineManager
dm.networks = make(map[flatip.IPNet]IPBanInfo) dm.networks = make(map[string]dLineNet)
dm.expirationTimers = make(map[flatip.IPNet]*time.Timer) dm.expirationTimers = make(map[string]*time.Timer)
dm.server = server dm.server = server
dm.loadFromDatastore() dm.loadFromDatastore()
@ -89,21 +96,21 @@ func (dm *DLineManager) AllBans() map[string]IPBanInfo {
dm.RLock() dm.RLock()
defer dm.RUnlock() defer dm.RUnlock()
// map keys are already the human-readable forms, just return a copy of the map
for key, info := range dm.networks { for key, info := range dm.networks {
allb[key.HumanReadableString()] = info allb[key] = info.Info
} }
return allb return allb
} }
// AddNetwork adds a network to the blocked list. // AddNetwork adds a network to the blocked list.
func (dm *DLineManager) AddNetwork(network flatip.IPNet, duration time.Duration, requireSASL bool, reason, operReason, operName string) error { func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, reason, operReason, operName string) error {
dm.persistenceMutex.Lock() dm.persistenceMutex.Lock()
defer dm.persistenceMutex.Unlock() defer dm.persistenceMutex.Unlock()
// assemble ban info // assemble ban info
info := IPBanInfo{ info := IPBanInfo{
RequireSASL: requireSASL,
Reason: reason, Reason: reason,
OperReason: operReason, OperReason: operReason,
OperName: operName, OperName: operName,
@ -115,8 +122,9 @@ func (dm *DLineManager) AddNetwork(network flatip.IPNet, duration time.Duration,
return dm.persistDline(id, info) return dm.persistDline(id, info)
} }
func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo) (id flatip.IPNet) { func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (id string) {
id = flatnet network = utils.NormalizeNet(network)
id = utils.NetToNormalizedString(network)
var timeLeft time.Duration var timeLeft time.Duration
if info.Duration != 0 { if info.Duration != 0 {
@ -129,9 +137,12 @@ func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo)
dm.Lock() dm.Lock()
defer dm.Unlock() defer dm.Unlock()
dm.networks[flatnet] = info dm.networks[id] = dLineNet{
Network: network,
Info: info,
}
dm.cancelTimer(flatnet) dm.cancelTimer(id)
if info.Duration == 0 { if info.Duration == 0 {
return return
@ -143,29 +154,29 @@ func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo)
dm.Lock() dm.Lock()
defer dm.Unlock() defer dm.Unlock()
banInfo, ok := dm.networks[flatnet] netBan, ok := dm.networks[id]
if ok && banInfo.TimeCreated.Equal(timeCreated) { if ok && netBan.Info.TimeCreated.Equal(timeCreated) {
delete(dm.networks, flatnet) delete(dm.networks, id)
// TODO(slingamn) here's where we'd remove it from the radix tree // TODO(slingamn) here's where we'd remove it from the radix tree
delete(dm.expirationTimers, flatnet) delete(dm.expirationTimers, id)
} }
} }
dm.expirationTimers[flatnet] = time.AfterFunc(timeLeft, processExpiration) dm.expirationTimers[id] = time.AfterFunc(timeLeft, processExpiration)
return return
} }
func (dm *DLineManager) cancelTimer(flatnet flatip.IPNet) { func (dm *DLineManager) cancelTimer(id string) {
oldTimer := dm.expirationTimers[flatnet] oldTimer := dm.expirationTimers[id]
if oldTimer != nil { if oldTimer != nil {
oldTimer.Stop() oldTimer.Stop()
delete(dm.expirationTimers, flatnet) delete(dm.expirationTimers, id)
} }
} }
func (dm *DLineManager) persistDline(id flatip.IPNet, info IPBanInfo) error { func (dm *DLineManager) persistDline(id string, info IPBanInfo) error {
// save in datastore // save in datastore
dlineKey := fmt.Sprintf(keyDlineEntry, id.String()) dlineKey := fmt.Sprintf(keyDlineEntry, id)
// assemble json from ban info // assemble json from ban info
b, err := json.Marshal(info) b, err := json.Marshal(info)
if err != nil { if err != nil {
@ -188,8 +199,8 @@ func (dm *DLineManager) persistDline(id flatip.IPNet, info IPBanInfo) error {
return err return err
} }
func (dm *DLineManager) unpersistDline(id flatip.IPNet) error { func (dm *DLineManager) unpersistDline(id string) error {
dlineKey := fmt.Sprintf(keyDlineEntry, id.String()) dlineKey := fmt.Sprintf(keyDlineEntry, id)
return dm.server.store.Update(func(tx *buntdb.Tx) error { return dm.server.store.Update(func(tx *buntdb.Tx) error {
_, err := tx.Delete(dlineKey) _, err := tx.Delete(dlineKey)
return err return err
@ -197,11 +208,11 @@ func (dm *DLineManager) unpersistDline(id flatip.IPNet) error {
} }
// RemoveNetwork removes a network from the blocked list. // RemoveNetwork removes a network from the blocked list.
func (dm *DLineManager) RemoveNetwork(network flatip.IPNet) error { func (dm *DLineManager) RemoveNetwork(network net.IPNet) error {
dm.persistenceMutex.Lock() dm.persistenceMutex.Lock()
defer dm.persistenceMutex.Unlock() defer dm.persistenceMutex.Unlock()
id := network id := utils.NetToNormalizedString(utils.NormalizeNet(network))
present := func() bool { present := func() bool {
dm.Lock() dm.Lock()
@ -219,19 +230,35 @@ func (dm *DLineManager) RemoveNetwork(network flatip.IPNet) error {
return dm.unpersistDline(id) return dm.unpersistDline(id)
} }
// AddIP adds an IP address to the blocked list.
func (dm *DLineManager) AddIP(addr net.IP, duration time.Duration, reason, operReason, operName string) error {
return dm.AddNetwork(utils.NormalizeIPToNet(addr), duration, reason, operReason, operName)
}
// RemoveIP removes an IP address from the blocked list.
func (dm *DLineManager) RemoveIP(addr net.IP) error {
return dm.RemoveNetwork(utils.NormalizeIPToNet(addr))
}
// CheckIP returns whether or not an IP address was banned, and how long it is banned for. // CheckIP returns whether or not an IP address was banned, and how long it is banned for.
func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) { func (dm *DLineManager) CheckIP(addr net.IP) (isBanned bool, info IPBanInfo) {
addr = addr.To16() // almost certainly unnecessary
if addr.IsLoopback() {
return // #671
}
dm.RLock() dm.RLock()
defer dm.RUnlock() defer dm.RUnlock()
// check networks // check networks
// TODO(slingamn) use a radix tree as the data plane for this // TODO(slingamn) use a radix tree as the data plane for this
for flatnet, info := range dm.networks { for _, netBan := range dm.networks {
if flatnet.Contains(addr) { if netBan.Network.Contains(addr) {
return true, info return true, netBan.Info
} }
} }
// no matches! // no matches!
isBanned = false
return return
} }
@ -247,7 +274,7 @@ func (dm *DLineManager) loadFromDatastore() {
key = strings.TrimPrefix(key, dlinePrefix) key = strings.TrimPrefix(key, dlinePrefix)
// load addr/net // load addr/net
hostNet, err := flatip.ParseToNormalizedNet(key) hostNet, err := utils.NormalizedNetFromString(key)
if err != nil { if err != nil {
dm.server.logger.Error("internal", "bad dline cidr", err.Error()) dm.server.logger.Error("internal", "bad dline cidr", err.Error())
return true return true

View File

@ -4,18 +4,9 @@
package email package email
import ( import (
"bytes"
"crypto"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors" "errors"
"fmt" dkim "github.com/toorop/go-dkim"
"io/ioutil"
"os"
dkim "github.com/emersion/go-msgauth/dkim"
) )
var ( var (
@ -26,77 +17,38 @@ type DKIMConfig struct {
Domain string Domain string
Selector string Selector string
KeyFile string `yaml:"key-file"` KeyFile string `yaml:"key-file"`
privKey crypto.Signer keyBytes []byte
}
func (dkim *DKIMConfig) Enabled() bool {
return dkim.Domain != ""
} }
func (dkim *DKIMConfig) Postprocess() (err error) { func (dkim *DKIMConfig) Postprocess() (err error) {
if !dkim.Enabled() { if dkim.Domain != "" {
return nil if dkim.Selector == "" || dkim.KeyFile == "" {
return ErrMissingFields
}
dkim.keyBytes, err = ioutil.ReadFile(dkim.KeyFile)
if err != nil {
return err
}
} }
if dkim.Selector == "" || dkim.KeyFile == "" {
return ErrMissingFields
}
keyBytes, err := os.ReadFile(dkim.KeyFile)
if err != nil {
return fmt.Errorf("Could not read DKIM key file: %w", err)
}
dkim.privKey, err = parseDKIMPrivKey(keyBytes)
if err != nil {
return fmt.Errorf("Could not parse DKIM key file: %w", err)
}
return nil return nil
} }
func parseDKIMPrivKey(input []byte) (crypto.Signer, error) { var defaultOptions = dkim.SigOptions{
if len(input) == 0 { Version: 1,
return nil, errors.New("DKIM private key is empty") Canonicalization: "relaxed/relaxed",
} Algo: "rsa-sha256",
Headers: []string{"from", "to", "subject", "message-id", "date"},
// raw ed25519 private key format BodyLength: 0,
if len(input) == ed25519.PrivateKeySize { QueryMethods: []string{"dns/txt"},
return ed25519.PrivateKey(input), nil AddSignatureTimestamp: true,
} SignatureExpireIn: 0,
d, _ := pem.Decode(input)
if d == nil {
return nil, errors.New("Invalid PEM data for DKIM private key")
}
if rsaKey, err := x509.ParsePKCS1PrivateKey(d.Bytes); err == nil {
return rsaKey, nil
}
if k, err := x509.ParsePKCS8PrivateKey(d.Bytes); err == nil {
switch key := k.(type) {
case *rsa.PrivateKey:
return key, nil
case ed25519.PrivateKey:
return key, nil
default:
return nil, fmt.Errorf("Unacceptable type for DKIM private key: %T", k)
}
}
return nil, errors.New("No acceptable format for DKIM private key")
} }
func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) { func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) {
options := dkim.SignOptions{ options := defaultOptions
Domain: dkimConfig.Domain, options.PrivateKey = dkimConfig.keyBytes
Selector: dkimConfig.Selector, options.Domain = dkimConfig.Domain
Signer: dkimConfig.privKey, options.Selector = dkimConfig.Selector
HeaderCanonicalization: dkim.CanonicalizationRelaxed, err = dkim.Sign(&message, options)
BodyCanonicalization: dkim.CanonicalizationRelaxed, return message, err
}
input := bytes.NewBuffer(message)
output := bytes.NewBuffer(make([]byte, 0, len(message)+1024))
err = dkim.Sign(output, input, &options)
return output.Bytes(), err
} }

View File

@ -4,131 +4,41 @@
package email package email
import ( import (
"bufio"
"bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"os"
"regexp" "regexp"
"strings" "strings"
"time"
"github.com/ergochat/ergo/irc/custime" "github.com/oragono/oragono/irc/smtp"
"github.com/ergochat/ergo/irc/smtp"
"github.com/ergochat/ergo/irc/utils"
) )
var ( var (
ErrBlacklistedAddress = errors.New("Email address is blacklisted") ErrBlacklistedAddress = errors.New("Email address is blacklisted")
ErrInvalidAddress = errors.New("Email address is invalid") ErrInvalidAddress = errors.New("Email address is blacklisted")
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 {
// legacy config format assumed the use of an MTA/smarthost, // legacy config format assumed the use of an MTA/smarthost,
// so server, port, etc. appear directly at top level // so server, port, etc. appear directly at top level
// XXX: see https://github.com/go-yaml/yaml/issues/63 // XXX: see https://github.com/go-yaml/yaml/issues/63
MTAConfig `yaml:",inline"` MTAConfig `yaml:",inline"`
Enabled bool Sender string
Sender string HeloDomain string `yaml:"helo-domain"`
HeloDomain string `yaml:"helo-domain"` RequireTLS bool `yaml:"require-tls"`
RequireTLS bool `yaml:"require-tls"` VerifyMessageSubject string `yaml:"verify-message-subject"`
Protocol string `yaml:"protocol"` DKIM DKIMConfig
LocalAddress string `yaml:"local-address"` MTAReal MTAConfig `yaml:"mta"`
localAddress net.Addr BlacklistRegexes []string `yaml:"blacklist-regexes"`
VerifyMessageSubject string `yaml:"verify-message-subject"` blacklistRegexes []*regexp.Regexp
DKIM DKIMConfig
MTAReal MTAConfig `yaml:"mta"`
AddressBlacklist []string `yaml:"address-blacklist"`
AddressBlacklistSyntax BlacklistSyntax `yaml:"address-blacklist-syntax"`
AddressBlacklistFile string `yaml:"address-blacklist-file"`
blacklistRegexes []*regexp.Regexp
Timeout time.Duration
PasswordReset struct {
Enabled bool
Cooldown custime.Duration
Timeout custime.Duration
} `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) {
@ -146,39 +56,12 @@ 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 { if err != nil {
return err return err
} }
} else if len(config.AddressBlacklist) != 0 { config.blacklistRegexes = append(config.blacklistRegexes, compiled)
config.blacklistRegexes = make([]*regexp.Regexp, 0, len(config.AddressBlacklist))
for _, reg := range config.AddressBlacklist {
compiled, err := config.compileBlacklistEntry(reg)
if err != nil {
return err
}
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 != "" {
@ -189,11 +72,6 @@ func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
return config.DKIM.Postprocess() return config.DKIM.Postprocess()
} }
// are we sending email directly, as opposed to deferring to an MTA?
func (config *MailtoConfig) DirectSendingEnabled() bool {
return config.MTAReal.Server == ""
}
// get the preferred MX record hostname, "" on error // get the preferred MX record hostname, "" on error
func lookupMX(domain string) (server string) { func lookupMX(domain string) (server string) {
var minPref uint16 var minPref uint16
@ -209,31 +87,14 @@ func lookupMX(domain string) (server string) {
return return
} }
func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.Buffer) {
fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
fmt.Fprintf(&message, "To: %s\r\n", recipient)
dkimDomain := config.DKIM.Domain
if 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, "Subject: %s\r\n", subject)
message.WriteString("\r\n") // blank line: end headers, begin message body
return message
}
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
} }
} }
if config.DKIM.Enabled() { if config.DKIM.Domain != "" {
msg, err = DKIMSign(msg, config.DKIM) msg, err = DKIMSign(msg, config.DKIM)
if err != nil { if err != nil {
return return
@ -242,13 +103,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.MTAReal.Server != "" {
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 +120,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)
addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg,
config.RequireTLS, implicitTLS, config.Protocol, config.localAddress, config.Timeout,
)
} }

View File

@ -10,7 +10,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/ergochat/ergo/irc/utils" "github.com/oragono/oragono/irc/utils"
) )
// Runtime Errors // Runtime Errors
@ -28,13 +28,12 @@ var (
errAccountAlreadyLoggedIn = errors.New("You're already logged into an account") errAccountAlreadyLoggedIn = errors.New("You're already logged into an account")
errAccountTooManyNicks = errors.New("Account has too many reserved nicks") errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
errAccountUnverified = errors.New(`Account is not yet verified`) errAccountUnverified = errors.New(`Account is not yet verified`)
errAccountSuspended = errors.New(`Account has been suspended`)
errAccountVerificationFailed = errors.New("Account verification failed") errAccountVerificationFailed = errors.New("Account verification failed")
errAccountVerificationInvalidCode = errors.New("Invalid account verification code") errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
errAccountUpdateFailed = errors.New(`Error while updating your account information`) errAccountUpdateFailed = errors.New(`Error while updating your account information`)
errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`) errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`)
errAuthRequired = errors.New("You must be logged into an account to do this")
errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`) errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`)
errCallbackFailed = errors.New("Account verification could not be sent")
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`) errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account") errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`) errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`)
@ -52,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")
@ -65,7 +63,6 @@ var (
errCASFailed = errors.New("Compare-and-swap update of database value failed") errCASFailed = errors.New("Compare-and-swap update of database value failed")
errEmptyCredentials = errors.New("No more credentials are approved") errEmptyCredentials = errors.New("No more credentials are approved")
errCredsExternallyManaged = errors.New("Credentials are externally managed and cannot be changed here") errCredsExternallyManaged = errors.New("Credentials are externally managed and cannot be changed here")
errNoSCRAMCredentials = errors.New("SCRAM credentials are not initialized for this account; consult the user guide")
errInvalidMultilineBatch = errors.New("Invalid multiline batch") errInvalidMultilineBatch = errors.New("Invalid multiline batch")
errTimedOut = errors.New("Operation timed out") errTimedOut = errors.New("Operation timed out")
errInvalidUtf8 = errors.New("Message rejected for invalid utf8") errInvalidUtf8 = errors.New("Message rejected for invalid utf8")
@ -74,10 +71,6 @@ var (
errWrongChannelKey = errors.New("Cannot join password-protected channel without the password") errWrongChannelKey = errors.New("Cannot join password-protected channel without the password")
errInviteOnly = errors.New("Cannot join invite-only channel without an invite") errInviteOnly = errors.New("Cannot join invite-only channel without an invite")
errRegisteredOnly = errors.New("Cannot join registered-only channel without an account") errRegisteredOnly = errors.New("Cannot join registered-only channel without an account")
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")
errNameReserved = errors.New(`Name reserved due to a prior registration`)
errInvalidBearerTokenType = errors.New("invalid bearer token type")
) )
// String Errors // String Errors
@ -100,5 +93,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)
} }

View File

@ -4,7 +4,6 @@
package irc package irc
import ( import (
"maps"
"time" "time"
) )
@ -37,10 +36,6 @@ type Fakelag struct {
func (fl *Fakelag) Initialize(config FakelagConfig) { func (fl *Fakelag) Initialize(config FakelagConfig) {
fl.config = config fl.config = config
// XXX don't share mutable member CommandBudgets:
if config.CommandBudgets != nil {
fl.config.CommandBudgets = maps.Clone(config.CommandBudgets)
}
fl.nowFunc = time.Now fl.nowFunc = time.Now
fl.sleepFunc = time.Sleep fl.sleepFunc = time.Sleep
fl.state = FakelagBursting fl.state = FakelagBursting
@ -63,16 +58,11 @@ func (fl *Fakelag) Unsuspend() {
} }
// register a new command, sleep if necessary to delay it // register a new command, sleep if necessary to delay it
func (fl *Fakelag) Touch(command string) { func (fl *Fakelag) Touch() {
if !fl.config.Enabled { if !fl.config.Enabled {
return return
} }
if budget, ok := fl.config.CommandBudgets[command]; ok && budget > 0 {
fl.config.CommandBudgets[command] = budget - 1
return
}
now := fl.nowFunc() now := fl.nowFunc()
// XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine // XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine
elapsed := now.Sub(fl.lastTouch) elapsed := now.Sub(fl.lastTouch)
@ -100,20 +90,10 @@ func (fl *Fakelag) Touch(command string) {
if elapsed > fl.config.Cooldown { if elapsed > fl.config.Cooldown {
// let them burst again // let them burst again
fl.state = FakelagBursting fl.state = FakelagBursting
fl.burstCount = 1
return return
} }
var sleepDuration time.Duration // space them out by at least window/messagesperwindow
if fl.config.MessagesPerWindow > 0 { sleepDuration := time.Duration((int64(fl.config.Window) / int64(fl.config.MessagesPerWindow)) - int64(elapsed))
// space them out by at least window/messagesperwindow
sleepDuration = time.Duration((int64(fl.config.Window) / int64(fl.config.MessagesPerWindow)) - int64(elapsed))
} else {
// only burst messages are allowed: sleep until cooldown expires,
// then count this as a burst message
sleepDuration = time.Duration(int64(fl.config.Cooldown) - int64(elapsed))
fl.state = FakelagBursting
fl.burstCount = 1
}
if sleepDuration > 0 { if sleepDuration > 0 {
fl.sleepFunc(sleepDuration) fl.sleepFunc(sleepDuration)
// the touch time should take into account the time we slept // the touch time should take into account the time we slept

View File

@ -60,7 +60,7 @@ func TestFakelag(t *testing.T) {
window, _ := time.ParseDuration("1s") window, _ := time.ParseDuration("1s")
fl, mt := newFakelagForTesting(window, 3, 2, window) fl, mt := newFakelagForTesting(window, 3, 2, window)
fl.Touch("") fl.Touch()
slept, _ := mt.lastSleep() slept, _ := mt.lastSleep()
if slept { if slept {
t.Fatalf("should not have slept") t.Fatalf("should not have slept")
@ -69,7 +69,7 @@ func TestFakelag(t *testing.T) {
interval, _ := time.ParseDuration("100ms") interval, _ := time.ParseDuration("100ms")
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
mt.pause(interval) mt.pause(interval)
fl.Touch("") fl.Touch()
slept, _ := mt.lastSleep() slept, _ := mt.lastSleep()
if slept { if slept {
t.Fatalf("should not have slept") t.Fatalf("should not have slept")
@ -77,7 +77,7 @@ func TestFakelag(t *testing.T) {
} }
mt.pause(interval) mt.pause(interval)
fl.Touch("") fl.Touch()
if fl.state != FakelagThrottled { if fl.state != FakelagThrottled {
t.Fatalf("should be throttled") t.Fatalf("should be throttled")
} }
@ -91,7 +91,7 @@ func TestFakelag(t *testing.T) {
} }
// send another message without a pause; we should have to sleep for 500 msec // send another message without a pause; we should have to sleep for 500 msec
fl.Touch("") fl.Touch()
if fl.state != FakelagThrottled { if fl.state != FakelagThrottled {
t.Fatalf("should be throttled") t.Fatalf("should be throttled")
} }
@ -102,7 +102,7 @@ func TestFakelag(t *testing.T) {
} }
mt.pause(interval * 6) mt.pause(interval * 6)
fl.Touch("") fl.Touch()
if fl.state != FakelagThrottled { if fl.state != FakelagThrottled {
t.Fatalf("should still be throttled") t.Fatalf("should still be throttled")
} }
@ -112,7 +112,7 @@ func TestFakelag(t *testing.T) {
} }
mt.pause(window * 2) mt.pause(window * 2)
fl.Touch("") fl.Touch()
if fl.state != FakelagBursting { if fl.state != FakelagBursting {
t.Fatalf("should be bursting again") t.Fatalf("should be bursting again")
} }
@ -125,31 +125,31 @@ func TestFakelag(t *testing.T) {
func TestSuspend(t *testing.T) { func TestSuspend(t *testing.T) {
window, _ := time.ParseDuration("1s") window, _ := time.ParseDuration("1s")
fl, _ := newFakelagForTesting(window, 3, 2, window) fl, _ := newFakelagForTesting(window, 3, 2, window)
assertEqual(fl.config.Enabled, true) assertEqual(fl.config.Enabled, true, t)
// suspend idempotently disables // suspend idempotently disables
fl.Suspend() fl.Suspend()
assertEqual(fl.config.Enabled, false) assertEqual(fl.config.Enabled, false, t)
fl.Suspend() fl.Suspend()
assertEqual(fl.config.Enabled, false) assertEqual(fl.config.Enabled, false, t)
// unsuspend idempotently enables // unsuspend idempotently enables
fl.Unsuspend() fl.Unsuspend()
assertEqual(fl.config.Enabled, true) assertEqual(fl.config.Enabled, true, t)
fl.Unsuspend() fl.Unsuspend()
assertEqual(fl.config.Enabled, true) assertEqual(fl.config.Enabled, true, t)
fl.Suspend() fl.Suspend()
assertEqual(fl.config.Enabled, false) assertEqual(fl.config.Enabled, false, t)
fl2, _ := newFakelagForTesting(window, 3, 2, window) fl2, _ := newFakelagForTesting(window, 3, 2, window)
fl2.config.Enabled = false fl2.config.Enabled = false
// if we were never enabled, suspend and unsuspend are both no-ops // if we were never enabled, suspend and unsuspend are both no-ops
fl2.Suspend() fl2.Suspend()
assertEqual(fl2.config.Enabled, false) assertEqual(fl2.config.Enabled, false, t)
fl2.Suspend() fl2.Suspend()
assertEqual(fl2.config.Enabled, false) assertEqual(fl2.config.Enabled, false, t)
fl2.Unsuspend() fl2.Unsuspend()
assertEqual(fl2.config.Enabled, false) assertEqual(fl2.config.Enabled, false, t)
fl2.Unsuspend() fl2.Unsuspend()
assertEqual(fl2.config.Enabled, false) assertEqual(fl2.config.Enabled, false, t)
} }

View File

@ -1,33 +0,0 @@
// Copyright 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// Released under the MIT license
package flatip
// begin ad-hoc utilities
// ParseToNormalizedNet attempts to interpret a string either as an IP
// network in CIDR notation, returning an IPNet, or as an IP address,
// returning an IPNet that contains only that address.
func ParseToNormalizedNet(netstr string) (ipnet IPNet, err error) {
_, ipnet, err = ParseCIDR(netstr)
if err == nil {
return
}
ip, err := ParseIP(netstr)
if err == nil {
ipnet.IP = ip
ipnet.PrefixLen = 128
}
return
}
// IPInNets is a convenience function for testing whether an IP is contained
// in any member of a slice of IPNet's.
func IPInNets(addr IP, nets []IPNet) bool {
for _, net := range nets {
if net.Contains(addr) {
return true
}
}
return false
}

View File

@ -1,220 +0,0 @@
// Copyright 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// Copyright 2009 The Go Authors
// Released under the MIT license
package flatip
import (
"bytes"
"errors"
"net"
)
var (
v4InV6Prefix = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}
IPv6loopback = IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
IPv6zero = IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
IPv4zero = IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0, 0, 0}
ErrInvalidIPString = errors.New("String could not be interpreted as an IP address")
)
// packed versions of net.IP and net.IPNet; these are pure value types,
// so they can be compared with == and used as map keys.
// IP is a 128-bit representation of an IP address, using the 4-in-6 mapping
// to represent IPv4 addresses.
type IP [16]byte
// IPNet is a IP network. In a valid value, all bits after PrefixLen are zeroes.
type IPNet struct {
IP
PrefixLen uint8
}
// NetIP converts an IP into a net.IP.
func (ip IP) NetIP() (result net.IP) {
result = make(net.IP, 16)
copy(result[:], ip[:])
return
}
// FromNetIP converts a net.IP into an IP.
func FromNetIP(ip net.IP) (result IP) {
if len(ip) == 16 {
copy(result[:], ip[:])
} else {
result[10] = 0xff
result[11] = 0xff
copy(result[12:], ip[:])
}
return
}
// IPv4 returns the IP address representation of a.b.c.d
func IPv4(a, b, c, d byte) (result IP) {
copy(result[:12], v4InV6Prefix)
result[12] = a
result[13] = b
result[14] = c
result[15] = d
return
}
// ParseIP parses a string representation of an IP address into an IP.
// Unlike net.ParseIP, it returns an error instead of a zero value on failure,
// since the zero value of `IP` is a representation of a valid IP (::0, the
// IPv6 "unspecified address").
func ParseIP(ipstr string) (ip IP, err error) {
// TODO reimplement this without net.ParseIP
netip := net.ParseIP(ipstr)
if netip == nil {
err = ErrInvalidIPString
return
}
netip = netip.To16()
copy(ip[:], netip)
return
}
// String returns the string representation of an IP
func (ip IP) String() string {
// TODO reimplement this without using (net.IP).String()
return (net.IP)(ip[:]).String()
}
// IsIPv4 returns whether the IP is an IPv4 address.
func (ip IP) IsIPv4() bool {
return bytes.Equal(ip[:12], v4InV6Prefix)
}
// IsLoopback returns whether the IP is a loopback address.
func (ip IP) IsLoopback() bool {
if ip.IsIPv4() {
return ip[12] == 127
} else {
return ip == IPv6loopback
}
}
func (ip IP) IsUnspecified() bool {
return ip == IPv4zero || ip == IPv6zero
}
func rawCidrMask(length int) (m IP) {
n := uint(length)
for i := 0; i < 16; i++ {
if n >= 8 {
m[i] = 0xff
n -= 8
continue
}
m[i] = ^byte(0xff >> n)
return
}
return
}
func (ip IP) applyMask(mask IP) (result IP) {
for i := 0; i < 16; i += 1 {
result[i] = ip[i] & mask[i]
}
return
}
func cidrMask(ones, bits int) (result IP) {
switch bits {
case 32:
return rawCidrMask(96 + ones)
case 128:
return rawCidrMask(ones)
default:
return
}
}
// Mask returns the result of masking ip with the CIDR mask of
// length 'ones', out of a total of 'bits' (which must be either
// 32 for an IPv4 subnet or 128 for an IPv6 subnet).
func (ip IP) Mask(ones, bits int) (result IP) {
return ip.applyMask(cidrMask(ones, bits))
}
// ToNetIPNet converts an IPNet into a net.IPNet.
func (cidr IPNet) ToNetIPNet() (result net.IPNet) {
return net.IPNet{
IP: cidr.IP.NetIP(),
Mask: net.CIDRMask(int(cidr.PrefixLen), 128),
}
}
// Contains retuns whether the network contains `ip`.
func (cidr IPNet) Contains(ip IP) bool {
maskedIP := ip.Mask(int(cidr.PrefixLen), 128)
return cidr.IP == maskedIP
}
func (cidr IPNet) Size() (ones, bits int) {
if cidr.IP.IsIPv4() {
return int(cidr.PrefixLen) - 96, 32
} else {
return int(cidr.PrefixLen), 128
}
}
// FromNetIPnet converts a net.IPNet into an IPNet.
func FromNetIPNet(network net.IPNet) (result IPNet) {
ones, _ := network.Mask.Size()
if len(network.IP) == 16 {
copy(result.IP[:], network.IP[:])
} else {
result.IP[10] = 0xff
result.IP[11] = 0xff
copy(result.IP[12:], network.IP[:])
ones += 96
}
// perform masking so that equal CIDRs are ==
result.IP = result.IP.Mask(ones, 128)
result.PrefixLen = uint8(ones)
return
}
// String returns a string representation of an IPNet.
func (cidr IPNet) String() string {
ip := make(net.IP, 16)
copy(ip[:], cidr.IP[:])
ipnet := net.IPNet{
IP: ip,
Mask: net.CIDRMask(int(cidr.PrefixLen), 128),
}
return ipnet.String()
}
// HumanReadableString returns a string representation of an IPNet;
// if the network contains only a single IP address, it returns
// a representation of that address.
func (cidr IPNet) HumanReadableString() string {
if cidr.PrefixLen == 128 {
return cidr.IP.String()
}
return cidr.String()
}
// IsZero tests whether ipnet is the zero value of an IPNet, 0::0/0.
// Although this is a valid subnet, it can still be used as a sentinel
// value in some contexts.
func (ipnet IPNet) IsZero() bool {
return ipnet == IPNet{}
}
// ParseCIDR parses a string representation of an IP network in CIDR notation,
// then returns it as an IPNet (along with the original, unmasked address).
func ParseCIDR(netstr string) (ip IP, ipnet IPNet, err error) {
// TODO reimplement this without net.ParseCIDR
nip, nipnet, err := net.ParseCIDR(netstr)
if err != nil {
return
}
return FromNetIP(nip), FromNetIPNet(*nipnet), nil
}

View File

@ -1,208 +0,0 @@
package flatip
import (
"bytes"
"fmt"
"math/rand"
"net"
"reflect"
"testing"
"time"
)
func easyParseIP(ipstr string) (result net.IP) {
result = net.ParseIP(ipstr)
if result == nil {
panic(ipstr)
}
return
}
func easyParseFlat(ipstr string) (result IP) {
x := easyParseIP(ipstr)
return FromNetIP(x)
}
func easyParseIPNet(nipstr string) (result net.IPNet) {
_, nip, err := net.ParseCIDR(nipstr)
if err != nil {
panic(err)
}
return *nip
}
func TestBasic(t *testing.T) {
nip := easyParseIP("8.8.8.8")
flatip := FromNetIP(nip)
if flatip.String() != "8.8.8.8" {
t.Errorf("conversions don't work")
}
}
func TestLoopback(t *testing.T) {
localhost_v4 := easyParseFlat("127.0.0.1")
localhost_v4_again := easyParseFlat("127.2.3.4")
google := easyParseFlat("8.8.8.8")
loopback_v6 := easyParseFlat("::1")
google_v6 := easyParseFlat("2607:f8b0:4006:801::2004")
if !(localhost_v4.IsLoopback() && localhost_v4_again.IsLoopback() && loopback_v6.IsLoopback()) {
t.Errorf("can't detect loopbacks")
}
if google_v6.IsLoopback() || google.IsLoopback() {
t.Errorf("incorrectly detected loopbacks")
}
}
func TestContains(t *testing.T) {
nipnet := easyParseIPNet("8.8.0.0/16")
flatipnet := FromNetIPNet(nipnet)
nip := easyParseIP("8.8.8.8")
flatip_ := FromNetIP(nip)
if !flatipnet.Contains(flatip_) {
t.Errorf("contains doesn't work")
}
}
var testIPStrs = []string{
"8.8.8.8",
"127.0.0.1",
"1.1.1.1",
"128.127.65.64",
"2001:0db8::1",
"::1",
"255.255.255.255",
}
func doMaskingTest(ip net.IP, t *testing.T) {
flat := FromNetIP(ip)
netLen := len(ip) * 8
for i := 0; i < netLen; i++ {
masked := flat.Mask(i, netLen)
netMask := net.CIDRMask(i, netLen)
netMasked := ip.Mask(netMask)
if !bytes.Equal(masked[:], netMasked.To16()) {
t.Errorf("Masking %s with %d/%d; expected %s, got %s", ip.String(), i, netLen, netMasked.String(), masked.String())
}
}
}
func assertEqual(found, expected interface{}) {
if !reflect.DeepEqual(found, expected) {
panic(fmt.Sprintf("expected %#v, found %#v", expected, found))
}
}
func TestSize(t *testing.T) {
_, net, err := ParseCIDR("8.8.8.8/24")
if err != nil {
panic(err)
}
ones, bits := net.Size()
assertEqual(ones, 24)
assertEqual(bits, 32)
_, net, err = ParseCIDR("2001::0db8/64")
if err != nil {
panic(err)
}
ones, bits = net.Size()
assertEqual(ones, 64)
assertEqual(bits, 128)
_, net, err = ParseCIDR("2001::0db8/96")
if err != nil {
panic(err)
}
ones, bits = net.Size()
assertEqual(ones, 96)
assertEqual(bits, 128)
}
func TestMasking(t *testing.T) {
for _, ipstr := range testIPStrs {
doMaskingTest(easyParseIP(ipstr), t)
}
}
func TestMaskingFuzz(t *testing.T) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
buf := make([]byte, 4)
for i := 0; i < 10000; i++ {
r.Read(buf)
doMaskingTest(net.IP(buf), t)
}
buf = make([]byte, 16)
for i := 0; i < 10000; i++ {
r.Read(buf)
doMaskingTest(net.IP(buf), t)
}
}
func BenchmarkMasking(b *testing.B) {
ip := easyParseIP("2001:0db8::42")
flat := FromNetIP(ip)
b.ResetTimer()
for i := 0; i < b.N; i++ {
flat.Mask(64, 128)
}
}
func BenchmarkMaskingLegacy(b *testing.B) {
ip := easyParseIP("2001:0db8::42")
mask := net.CIDRMask(64, 128)
b.ResetTimer()
for i := 0; i < b.N; i++ {
ip.Mask(mask)
}
}
func BenchmarkMaskingCached(b *testing.B) {
i := easyParseIP("2001:0db8::42")
flat := FromNetIP(i)
mask := cidrMask(64, 128)
b.ResetTimer()
for i := 0; i < b.N; i++ {
flat.applyMask(mask)
}
}
func BenchmarkMaskingConstruct(b *testing.B) {
for i := 0; i < b.N; i++ {
cidrMask(69, 128)
}
}
func BenchmarkContains(b *testing.B) {
ip := easyParseIP("2001:0db8::42")
flat := FromNetIP(ip)
_, ipnet, err := net.ParseCIDR("2001:0db8::/64")
if err != nil {
panic(err)
}
flatnet := FromNetIPNet(*ipnet)
b.ResetTimer()
for i := 0; i < b.N; i++ {
flatnet.Contains(flat)
}
}
func BenchmarkContainsLegacy(b *testing.B) {
ip := easyParseIP("2001:0db8::42")
_, ipnetptr, err := net.ParseCIDR("2001:0db8::/64")
if err != nil {
panic(err)
}
ipnet := *ipnetptr
b.ResetTimer()
for i := 0; i < b.N; i++ {
ipnet.Contains(ip)
}
}

View File

@ -1,24 +0,0 @@
//go:build !(plan9 || solaris)
package flock
import (
"errors"
"github.com/gofrs/flock"
)
var (
CouldntAcquire = errors.New("Couldn't acquire flock (is another Ergo running?)")
)
func TryAcquireFlock(path string) (fl Flocker, err error) {
f := flock.New(path)
success, err := f.TryLock()
if err != nil {
return nil, err
} else if !success {
return nil, CouldntAcquire
}
return f, nil
}

View File

@ -1,14 +0,0 @@
package flock
// documentation for github.com/gofrs/flock incorrectly claims that
// Flock implements sync.Locker; it does not because the Unlock method
// has a return type (err).
type Flocker interface {
Unlock() error
}
type noopFlocker struct{}
func (n *noopFlocker) Unlock() error {
return nil
}

View File

@ -1,7 +0,0 @@
//go:build plan9 || solaris
package flock
func TryAcquireFlock(path string) (fl Flocker, err error) {
return &noopFlocker{}, nil
}

View File

@ -9,9 +9,8 @@ import (
"errors" "errors"
"net" "net"
"github.com/ergochat/ergo/irc/flatip" "github.com/oragono/oragono/irc/modes"
"github.com/ergochat/ergo/irc/modes" "github.com/oragono/oragono/irc/utils"
"github.com/ergochat/ergo/irc/utils"
) )
var ( var (
@ -32,7 +31,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
} }
@ -79,20 +77,16 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP net.IP, tls boo
} }
proxiedIP = proxiedIP.To16() proxiedIP = proxiedIP.To16()
isBanned, requireSASL, banMsg := client.server.checkBans(client.server.Config(), proxiedIP, true) isBanned, banMsg := client.server.checkBans(proxiedIP)
if isBanned { if isBanned {
return errBanned, banMsg return errBanned, banMsg
} }
client.requireSASL = requireSASL
if requireSASL {
client.requireSASLMessage = banMsg
}
// successfully added a limiter entry for the proxied IP; // successfully added a limiter entry for the proxied IP;
// remove the entry for the real IP if applicable (#197) // remove the entry for the real IP if applicable (#197)
client.server.connectionLimiter.RemoveClient(flatip.FromNetIP(session.realIP)) client.server.connectionLimiter.RemoveClient(session.realIP)
// given IP is sane! override the client's current IP // given IP is sane! override the client's current IP
client.server.logger.Info("connect-ip", session.connID, "Accepted proxy IP for client", proxiedIP.String()) client.server.logger.Info("connect-ip", "Accepted proxy IP for client", proxiedIP.String())
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
@ -101,7 +95,6 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP net.IP, tls boo
// nickmask will be updated when the client completes registration // nickmask will be updated when the client completes registration
// set tls info // set tls info
session.certfp = "" session.certfp = ""
session.peerCerts = nil
client.SetMode(modes.TLS, tls) client.SetMode(modes.TLS, tls)
return nil, "" return nil, ""
@ -123,11 +116,9 @@ func handleProxyCommand(server *Server, client *Client, session *Session, line s
} }
}() }()
ip, err := utils.ParseProxyLineV1(line) ip, err := utils.ParseProxyLine(line)
if err != nil { if err != nil {
return err return err
} else if ip == nil {
return nil
} }
if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) { if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) {

View File

@ -4,21 +4,25 @@
package irc package irc
import ( import (
"fmt"
"maps"
"net" "net"
"slices" "sync/atomic"
"time" "time"
"unsafe"
"github.com/ergochat/ergo/irc/caps" "github.com/oragono/oragono/irc/languages"
"github.com/ergochat/ergo/irc/languages" "github.com/oragono/oragono/irc/modes"
"github.com/ergochat/ergo/irc/modes"
"github.com/ergochat/ergo/irc/utils"
"github.com/ergochat/ergo/irc/webpush"
) )
func (server *Server) Config() (config *Config) { func (server *Server) Config() (config *Config) {
return server.config.Load() return (*Config)(atomic.LoadPointer(&server.config))
}
func (server *Server) SetConfig(config *Config) {
atomic.StorePointer(&server.config, unsafe.Pointer(config))
}
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) {
@ -34,11 +38,11 @@ func (server *Server) Languages() (lm *languages.Manager) {
} }
func (server *Server) Defcon() uint32 { func (server *Server) Defcon() uint32 {
return server.defcon.Load() return atomic.LoadUint32(&server.defcon)
} }
func (server *Server) SetDefcon(defcon uint32) { func (server *Server) SetDefcon(defcon uint32) {
server.defcon.Store(defcon) atomic.StoreUint32(&server.defcon, defcon)
} }
func (client *Client) Sessions() (sessions []*Session) { func (client *Client) Sessions() (sessions []*Session) {
@ -48,20 +52,28 @@ func (client *Client) Sessions() (sessions []*Session) {
return return
} }
type SessionData struct { func (client *Client) GetSessionByResumeID(resumeID string) (result *Session) {
ctime time.Time client.stateMutex.RLock()
atime time.Time defer client.stateMutex.RUnlock()
ip net.IP
hostname string for _, session := range client.sessions {
certfp string if session.resumeID == resumeID {
deviceID string return session
connInfo string }
connID string }
sessionID int64 return
caps []string
} }
func (client *Client) AllSessionData(currentSession *Session, hasPrivs bool) (data []SessionData, currentIndex int) { type SessionData struct {
ctime time.Time
atime time.Time
ip net.IP
hostname string
certfp string
deviceID string
}
func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
currentIndex = -1 currentIndex = -1
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
@ -72,29 +84,22 @@ func (client *Client) AllSessionData(currentSession *Session, hasPrivs bool) (da
currentIndex = i currentIndex = i
} }
data[i] = SessionData{ data[i] = SessionData{
atime: session.lastActive, atime: session.lastActive,
ctime: session.ctime, ctime: session.ctime,
hostname: session.rawHostname, hostname: session.rawHostname,
certfp: session.certfp, certfp: session.certfp,
deviceID: session.deviceID, deviceID: session.deviceID,
connID: session.connID,
sessionID: session.sessionID,
} }
if session.proxiedIP != nil { if session.proxiedIP != nil {
data[i].ip = session.proxiedIP data[i].ip = session.proxiedIP
} else { } else {
data[i].ip = session.realIP data[i].ip = session.realIP
} }
if hasPrivs {
data[i].connInfo = utils.DescribeConn(session.socket.conn.UnderlyingConn().Conn)
}
data[i].caps = session.capabilities.Strings(caps.Cap302, nil, 300)
} }
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()
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
@ -104,32 +109,21 @@ func (client *Client) AddSession(session *Session) (success bool, numSessions in
} }
// success, attach the new session to the client // success, attach the new session to the client
session.client = client session.client = client
session.sessionID = client.nextSessionID
client.nextSessionID++
newSessions := make([]*Session, len(client.sessions)+1) newSessions := make([]*Session, len(client.sessions)+1)
copy(newSessions, client.sessions) copy(newSessions, client.sessions)
newSessions[len(newSessions)-1] = session newSessions[len(newSessions)-1] = session
if client.accountSettings.AutoreplayMissed || session.deviceID != "" { if client.accountSettings.AutoreplayMissed || session.deviceID != "" {
lastSeen = client.lastSeen[session.deviceID] lastSeen = client.lastSeen[session.deviceID]
client.setLastSeen(time.Now().UTC(), session.deviceID)
} }
client.setLastSeen(time.Now().UTC(), session.deviceID)
client.sessions = newSessions client.sessions = newSessions
wasAway = client.awayMessage if client.autoAway {
if client.autoAwayEnabledNoMutex(config) { back = true
client.setAutoAwayNoMutex(config) client.autoAway = false
} else { client.away = false
if session.awayMessage != "" && session.awayMessage != "*" { client.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 = ""
}
// 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) {
@ -149,15 +143,10 @@ func (client *Client) removeSession(session *Session) (success bool, length int)
return return
} }
// #1650: show an arbitrarily chosen session IP and hostname in RPL_WHOISACTUALLY func (session *Session) SetResumeID(resumeID string) {
func (client *Client) getWhoisActually() (ip net.IP, hostname string) { session.client.stateMutex.Lock()
client.stateMutex.RLock() session.resumeID = resumeID
defer client.stateMutex.RUnlock() session.client.stateMutex.Unlock()
for _, session := range client.sessions {
return session.IP(), session.rawHostname
}
return utils.IPv4LoopbackAddress, client.server.name
} }
func (client *Client) Nick() string { func (client *Client) Nick() string {
@ -198,71 +187,24 @@ func (client *Client) Hostname() string {
func (client *Client) Away() (result bool, message string) { func (client *Client) Away() (result bool, message string) {
client.stateMutex.Lock() client.stateMutex.Lock()
message = client.awayMessage result, message = client.away, client.awayMessage
client.stateMutex.Unlock() client.stateMutex.Unlock()
result = client.awayMessage != ""
return return
} }
func (session *Session) SetAway(awayMessage string) (wasAway, nowAway string) { func (client *Client) SetAway(away bool, awayMessage string) (changed bool) {
client := session.client
config := client.server.Config()
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() changed = away != client.away
client.away = away
session.awayMessage = awayMessage client.awayMessage = awayMessage
session.awayAt = time.Now().UTC() client.stateMutex.Unlock()
wasAway = client.awayMessage
if client.autoAwayEnabledNoMutex(config) {
client.setAutoAwayNoMutex(config)
} else if awayMessage != "*" {
client.awayMessage = awayMessage
} // else: `AWAY *`, should not modify publicly visible away state
nowAway = client.awayMessage
return return
} }
func (session *Session) ConnID() string {
if session == nil {
return "*"
}
return session.connID
}
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) {
// aggregate the away statuses of the individual sessions:
var globalAwayState string
var awaySetAt time.Time
for _, cSession := range client.sessions {
if cSession.awayMessage == "" {
// a session is active, we are not auto-away
client.awayMessage = ""
return
} else if cSession.awayAt.After(awaySetAt) && cSession.awayMessage != "*" {
// choose the latest valid away message from any session
globalAwayState = cSession.awayMessage
awaySetAt = cSession.awayAt
}
}
if awaySetAt.IsZero() {
// no sessions, enable auto-away
client.awayMessage = config.languageManager.Translate(client.languages, `User is currently disconnected`)
} else {
client.awayMessage = globalAwayState
}
}
func (client *Client) AlwaysOn() (alwaysOn bool) { func (client *Client) AlwaysOn() (alwaysOn bool) {
client.stateMutex.RLock() client.stateMutex.Lock()
alwaysOn = client.registered && client.alwaysOn alwaysOn = client.registered && client.alwaysOn
client.stateMutex.RUnlock() client.stateMutex.Unlock()
return return
} }
@ -275,6 +217,18 @@ func (client *Client) uniqueIdentifiers() (nickCasefolded string, skeleton strin
return client.nickCasefolded, client.skeleton return client.nickCasefolded, client.skeleton
} }
func (client *Client) ResumeID() string {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
return client.resumeID
}
func (client *Client) SetResumeID(id string) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
client.resumeID = id
}
func (client *Client) Oper() *Oper { func (client *Client) Oper() *Oper {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
@ -306,6 +260,12 @@ func (client *Client) AwayMessage() (result string) {
return return
} }
func (client *Client) SetAwayMessage(message string) {
client.stateMutex.Lock()
client.awayMessage = message
client.stateMutex.Unlock()
}
func (client *Client) Account() string { func (client *Client) Account() string {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
@ -331,26 +291,6 @@ func (client *Client) Login(account ClientAccount) {
return return
} }
func (client *Client) setAccountName(name string) {
// XXX this assumes validation elsewhere
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
client.accountName = name
}
func (client *Client) setCloakedHostname(cloak string) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
client.cloakedHostname = cloak
client.updateNickMaskNoMutex()
}
func (client *Client) CloakedHostname() string {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
return client.cloakedHostname
}
func (client *Client) historyCutoff() (cutoff time.Time) { func (client *Client) historyCutoff() (cutoff time.Time) {
client.stateMutex.Lock() client.stateMutex.Lock()
if client.account != "" { if client.account != "" {
@ -381,19 +321,26 @@ func (client *Client) AccountSettings() (result AccountSettings) {
func (client *Client) SetAccountSettings(settings AccountSettings) { func (client *Client) SetAccountSettings(settings AccountSettings) {
// we mark dirty if the client is transitioning to always-on // we mark dirty if the client is transitioning to always-on
var becameAlwaysOn bool var becameAlwaysOn, autoreplayMissedDisabled bool
alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn) alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
client.stateMutex.Lock() client.stateMutex.Lock()
if client.registered { if client.registered {
// only allow the client to become always-on if their nick equals their account name // only allow the client to become always-on if their nick equals their account name
alwaysOn = alwaysOn && client.nick == client.accountName alwaysOn = alwaysOn && client.nick == client.accountName
autoreplayMissedDisabled = (client.accountSettings.AutoreplayMissed && !settings.AutoreplayMissed)
becameAlwaysOn = (!client.alwaysOn && alwaysOn) becameAlwaysOn = (!client.alwaysOn && alwaysOn)
client.alwaysOn = alwaysOn client.alwaysOn = alwaysOn
if autoreplayMissedDisabled {
// clear the lastSeen entry for the default session, but not for device IDs
delete(client.lastSeen, "")
}
} }
client.accountSettings = settings client.accountSettings = settings
client.stateMutex.Unlock() client.stateMutex.Unlock()
if becameAlwaysOn { if becameAlwaysOn {
client.markDirty(IncludeAllAttrs) client.markDirty(IncludeAllAttrs)
} else if autoreplayMissedDisabled {
client.markDirty(IncludeLastSeen)
} }
} }
@ -464,7 +411,6 @@ func (client *Client) detailsNoMutex() (result ClientDetails) {
result.username = client.username result.username = client.username
result.hostname = client.hostname result.hostname = client.hostname
result.realname = client.realname result.realname = client.realname
result.ip = client.getIPNoMutex()
result.nickMask = client.nickMaskString result.nickMask = client.nickMaskString
result.nickMaskCasefolded = client.nickMaskCasefolded result.nickMaskCasefolded = client.nickMaskCasefolded
result.account = client.account result.account = client.account
@ -487,252 +433,6 @@ func (client *Client) Realname() string {
return result return result
} }
func (client *Client) IsExpiredAlwaysOn(config *Config) (result bool) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
return client.checkAlwaysOnExpirationNoMutex(config, false)
}
func (client *Client) checkAlwaysOnExpirationNoMutex(config *Config, ignoreRegistration bool) (result bool) {
if !((client.registered || ignoreRegistration) && client.alwaysOn) {
return false
}
if len(client.lastSeen) == 0 {
return true // #2252: do not precreate the client if it was never logged into at all
}
deadline := time.Duration(config.Accounts.Multiclient.AlwaysOnExpiration)
if deadline == 0 {
return false
}
now := time.Now()
for _, ts := range client.lastSeen {
if now.Sub(ts) < deadline {
return false
}
}
return true
}
func (client *Client) GetReadMarker(cfname string) (result string) {
client.stateMutex.RLock()
t, ok := client.readMarkers[cfname]
client.stateMutex.RUnlock()
if ok {
return fmt.Sprintf("timestamp=%s", t.Format(utils.IRCv3TimestampFormat))
}
return "*"
}
func (client *Client) getMarkreadTime(cfname string) (timestamp time.Time, ok bool) {
client.stateMutex.RLock()
timestamp, ok = client.readMarkers[cfname]
client.stateMutex.RUnlock()
return
}
func (client *Client) copyReadMarkers() (result map[string]time.Time) {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
return maps.Clone(client.readMarkers)
}
func (client *Client) SetReadMarker(cfname string, now time.Time) (result time.Time) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
if client.readMarkers == nil {
client.readMarkers = make(map[string]time.Time)
}
result = updateLRUMap(client.readMarkers, cfname, now, maxReadMarkers)
client.dirtyTimestamps = true
return
}
func updateLRUMap(lru map[string]time.Time, key string, val time.Time, maxItems int) (result time.Time) {
if currentVal := lru[key]; currentVal.After(val) {
return currentVal
}
lru[key] = val
// evict the least-recently-used entry if necessary
if maxItems < len(lru) {
var minKey string
var minVal time.Time
for key, val := range lru {
if minVal.IsZero() || val.Before(minVal) {
minKey, minVal = key, val
}
}
delete(lru, minKey)
}
return val
}
func (client *Client) addClearablePushMessage(cftarget string, messageTime time.Time) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
if client.clearablePushMessages == nil {
client.clearablePushMessages = make(map[string]time.Time)
}
updateLRUMap(client.clearablePushMessages, cftarget, messageTime, maxReadMarkers)
}
func (client *Client) clearClearablePushMessage(cftarget string, readTimestamp time.Time) (ok bool) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
pushMessageTime, ok := client.clearablePushMessages[cftarget]
if ok && utils.ReadMarkerLessThanOrEqual(pushMessageTime, readTimestamp) {
delete(client.clearablePushMessages, cftarget)
return true
}
return false
}
func (client *Client) shouldFlushTimestamps() (result bool) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
result = client.dirtyTimestamps && client.registered && client.alwaysOn
client.dirtyTimestamps = false
return
}
func (client *Client) setKlined() {
client.stateMutex.Lock()
client.isKlined = true
client.stateMutex.Unlock()
}
func (client *Client) refreshPushSubscription(endpoint string, keys webpush.Keys) bool {
// do not mark dirty --- defer the write to periodic maintenance
now := time.Now().UTC()
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
sub, ok := client.pushSubscriptions[endpoint]
if ok && sub.Keys.Equal(keys) {
sub.LastRefresh = now
return true
}
return false // subscription doesn't exist, we need to send a test message
}
func (client *Client) addPushSubscription(endpoint string, keys webpush.Keys) error {
changed := false
defer func() {
if changed {
client.markDirty(IncludeAllAttrs)
}
}()
config := client.server.Config()
now := time.Now().UTC()
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
if client.pushSubscriptions == nil {
client.pushSubscriptions = make(map[string]*pushSubscription)
}
sub, ok := client.pushSubscriptions[endpoint]
if ok {
changed = !sub.Keys.Equal(keys)
sub.Keys = keys
sub.LastRefresh = now
} else {
if len(client.pushSubscriptions) >= config.WebPush.MaxSubscriptions {
return errLimitExceeded
}
changed = true
sub = newPushSubscription(storedPushSubscription{
Endpoint: endpoint,
Keys: keys,
LastRefresh: now,
LastSuccess: now, // assume we just sent a successful message to confirm the sub
})
client.pushSubscriptions[endpoint] = sub
}
if changed {
client.rebuildPushSubscriptionCache()
}
return nil
}
func (client *Client) hasPushSubscriptions() bool {
return client.pushSubscriptionsExist.Load() != 0
}
func (client *Client) getPushSubscriptions(refresh bool) []storedPushSubscription {
if refresh {
func() {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
client.rebuildPushSubscriptionCache()
}()
}
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
return client.cachedPushSubscriptions
}
func (client *Client) rebuildPushSubscriptionCache() {
// must hold write lock
if len(client.pushSubscriptions) == 0 {
client.cachedPushSubscriptions = nil
client.pushSubscriptionsExist.Store(0)
return
}
client.cachedPushSubscriptions = make([]storedPushSubscription, 0, len(client.pushSubscriptions))
for _, subscription := range client.pushSubscriptions {
client.cachedPushSubscriptions = append(client.cachedPushSubscriptions, subscription.storedPushSubscription)
}
client.pushSubscriptionsExist.Store(1)
}
func (client *Client) deletePushSubscription(endpoint string, writeback bool) (changed bool) {
defer func() {
if writeback && changed {
client.markDirty(IncludeAllAttrs)
}
}()
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
_, ok := client.pushSubscriptions[endpoint]
if ok {
changed = true
delete(client.pushSubscriptions, endpoint)
client.rebuildPushSubscriptionCache()
}
return
}
func (client *Client) recordPush(endpoint string, success bool) {
now := time.Now().UTC()
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
subscription, ok := client.pushSubscriptions[endpoint]
if !ok {
return
}
if success {
subscription.LastSuccess = now
}
// TODO we may want to track failures in some way in the future
}
func (channel *Channel) Name() string { func (channel *Channel) Name() string {
channel.stateMutex.RLock() channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock() defer channel.stateMutex.RUnlock()
@ -783,11 +483,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]
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) {
@ -798,244 +496,8 @@ func (channel *Channel) Settings() (result ChannelSettings) {
} }
func (channel *Channel) SetSettings(settings ChannelSettings) { func (channel *Channel) SetSettings(settings ChannelSettings) {
defer channel.MarkDirty(IncludeSettings)
channel.stateMutex.Lock() channel.stateMutex.Lock()
defer channel.stateMutex.Unlock()
channel.settings = settings channel.settings = settings
}
func (channel *Channel) setForward(forward string) {
channel.stateMutex.Lock()
channel.forward = forward
channel.stateMutex.Unlock() channel.stateMutex.Unlock()
} channel.MarkDirty(IncludeSettings)
func (channel *Channel) Ctime() (ctime time.Time) {
channel.stateMutex.RLock()
ctime = channel.createdTime
channel.stateMutex.RUnlock()
return
}
func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) {
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
return channel.accountToUMode[cfaccount]
}
func (channel *Channel) UUID() utils.UUID {
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
return channel.uuid
}
func (session *Session) isSubscribedTo(key string) bool {
session.client.stateMutex.RLock()
defer session.client.stateMutex.RUnlock()
return session.metadataSubscriptions.Has(key)
}
func (session *Session) SubscribeTo(keys ...string) ([]string, error) {
maxSubs := session.client.server.Config().Metadata.MaxSubs
session.client.stateMutex.Lock()
defer session.client.stateMutex.Unlock()
if session.metadataSubscriptions == nil {
session.metadataSubscriptions = make(utils.HashSet[string])
}
var added []string
for _, k := range keys {
if !session.metadataSubscriptions.Has(k) {
if len(session.metadataSubscriptions) > maxSubs {
return added, errMetadataTooManySubs
}
added = append(added, k)
session.metadataSubscriptions.Add(k)
}
}
return added, nil
}
func (session *Session) UnsubscribeFrom(keys ...string) []string {
session.client.stateMutex.Lock()
defer session.client.stateMutex.Unlock()
var removed []string
for k := range session.metadataSubscriptions {
if slices.Contains(keys, k) {
removed = append(removed, k)
session.metadataSubscriptions.Remove(k)
}
}
return removed
}
func (session *Session) MetadataSubscriptions() utils.HashSet[string] {
session.client.stateMutex.Lock()
defer session.client.stateMutex.Unlock()
return maps.Clone(session.metadataSubscriptions)
}
func (channel *Channel) GetMetadata(key string) (string, bool) {
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
val, ok := channel.metadata[key]
return val, ok
}
func (channel *Channel) SetMetadata(key string, value string, limit int) (updated bool, err error) {
defer channel.MarkDirty(IncludeAllAttrs)
channel.stateMutex.Lock()
defer channel.stateMutex.Unlock()
if channel.metadata == nil {
channel.metadata = make(map[string]string)
}
existing, ok := channel.metadata[key]
if !ok && len(channel.metadata) >= limit {
return false, errLimitExceeded
}
updated = !ok || value != existing
if updated {
channel.metadata[key] = value
}
return updated, nil
}
func (channel *Channel) ListMetadata() map[string]string {
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
return maps.Clone(channel.metadata)
}
func (channel *Channel) DeleteMetadata(key string) (updated bool) {
defer channel.MarkDirty(IncludeAllAttrs)
channel.stateMutex.Lock()
defer channel.stateMutex.Unlock()
_, updated = channel.metadata[key]
if updated {
delete(channel.metadata, key)
}
return updated
}
func (channel *Channel) ClearMetadata() map[string]string {
defer channel.MarkDirty(IncludeAllAttrs)
channel.stateMutex.Lock()
defer channel.stateMutex.Unlock()
oldMap := channel.metadata
channel.metadata = nil
return oldMap
}
func (channel *Channel) CountMetadata() int {
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
return len(channel.metadata)
}
func (client *Client) GetMetadata(key string) (string, bool) {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
val, ok := client.metadata[key]
return val, ok
}
func (client *Client) SetMetadata(key string, value string, limit int) (updated bool, err error) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
if client.metadata == nil {
client.metadata = make(map[string]string)
}
existing, ok := client.metadata[key]
if !ok && len(client.metadata) >= limit {
return false, errLimitExceeded
}
updated = !ok || value != existing
if updated {
client.metadata[key] = value
}
return updated, nil
}
func (client *Client) UpdateMetadataFromPrereg(preregData map[string]string, limit int) (updates map[string]string) {
updates = make(map[string]string, len(preregData))
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
if client.metadata == nil {
client.metadata = make(map[string]string)
}
for k, v := range preregData {
// do not overwrite any existing keys
_, ok := client.metadata[k]
if ok {
continue
}
if len(client.metadata) >= limit {
return // we know this is a new key
}
client.metadata[k] = v
updates[k] = v
}
return
}
func (client *Client) ListMetadata() map[string]string {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
return maps.Clone(client.metadata)
}
func (client *Client) DeleteMetadata(key string) (updated bool) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
_, updated = client.metadata[key]
if updated {
delete(client.metadata, key)
}
return updated
}
func (client *Client) ClearMetadata() map[string]string {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
oldMap := client.metadata
client.metadata = nil
return oldMap
}
func (client *Client) CountMetadata() int {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
return len(client.metadata)
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/ergochat/ergo/irc/languages" "github.com/oragono/oragono/irc/languages"
) )
// HelpEntryType represents the different sorts of help entries that can exist. // HelpEntryType represents the different sorts of help entries that can exist.
@ -37,7 +37,7 @@ type HelpEntry struct {
var ( var (
cmodeHelpText = `== Channel Modes == cmodeHelpText = `== Channel Modes ==
Ergo supports the following channel modes: Oragono supports the following channel modes:
+b | Client masks that are banned from the channel (e.g. *!*@127.0.0.1) +b | Client masks that are banned from the channel (e.g. *!*@127.0.0.1)
+e | Client masks that are exempted from bans. +e | Client masks that are exempted from bans.
@ -45,21 +45,14 @@ Ergo supports the following channel modes:
+i | Invite-only mode, only invited clients can join the channel. +i | Invite-only mode, only invited clients can join the channel.
+k | Key required when joining the channel. +k | Key required when joining the channel.
+l | Client join limit for the channel. +l | Client join limit for the channel.
+f | Users who are unable to join this channel (due to another mode) are forwarded
to the provided channel instead.
+m | Moderated mode, only privileged clients can talk on the channel. +m | Moderated mode, only privileged clients can talk on the channel.
+n | No-outside-messages mode, only users that are on the channel can send +n | No-outside-messages mode, only users that are on the channel can send
| messages to it. | messages to it.
+R | Only registered users can join the channel. +R | Only registered users can join the channel.
+M | Only registered or voiced users can speak in the channel.
+s | Secret mode, channel won't show up in /LIST or whois replies. +s | Secret mode, channel won't show up in /LIST or whois replies.
+t | Only channel opers can modify the topic. +t | Only channel opers can modify the topic.
+E | Roleplaying commands are enabled in the channel. +E | Roleplaying commands are enabled in the channel.
+C | Clients are blocked from sending CTCP messages in the channel. +C | Clients are blocked from sending CTCP messages in the channel.
+u | Auditorium mode: JOIN, PART, QUIT, NAMES, and WHO are hidden
from unvoiced clients.
+U | Op-moderated mode: messages from unprivileged clients are sent
only to channel operators.
= Prefixes = = Prefixes =
@ -70,7 +63,7 @@ Ergo supports the following channel modes:
+v (+) | Voice channel mode.` +v (+) | Voice channel mode.`
umodeHelpText = `== User Modes == umodeHelpText = `== User Modes ==
Ergo supports the following user modes: Oragono supports the following user modes:
+a | User is marked as being away. This mode is set with the /AWAY command. +a | User is marked as being away. This mode is set with the /AWAY command.
+i | User is marked as invisible (their channels are hidden from whois replies). +i | User is marked as invisible (their channels are hidden from whois replies).
@ -80,14 +73,13 @@ Ergo supports the following user modes:
+Z | User is connected via TLS. +Z | User is connected via TLS.
+B | User is a bot. +B | User is a bot.
+E | User can receive roleplaying commands. +E | User can receive roleplaying commands.
+T | CTCP messages to the user are blocked.` +T | User is blocked from sending CTCP messages.`
snomaskHelpText = `== Server Notice Masks == snomaskHelpText = `== Server Notice Masks ==
Ergo supports the following server notice masks for operators: Oragono supports the following server notice masks for operators:
a | Local announcements. a | Local announcements.
c | Local client connections. c | Local client connections.
d | Local client disconnects.
j | Local channel actions. j | Local channel actions.
k | Local kills. k | Local kills.
n | Local nick changes. n | Local nick changes.
@ -110,12 +102,13 @@ For instance, this would set the kill, oper, account and xline snomasks on dan:
// Help contains the help strings distributed with the IRCd. // Help contains the help strings distributed with the IRCd.
var Help = map[string]HelpEntry{ var Help = map[string]HelpEntry{
// Commands // Commands
"accept": { "acc": {
text: `ACCEPT <target> text: `ACC LS
ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
ACC VERIFY <accountname> <auth_code>
ACCEPT allows the target user to send you direct messages, overriding any Used in account registration. See the relevant specs for more info:
restrictions that might otherwise prevent this. Currently, the only https://oragono.io/specs.html`,
applicable restriction is the +R registered-only mode.`,
}, },
"ambiance": { "ambiance": {
text: `AMBIANCE <target> <text to be sent> text: `AMBIANCE <target> <text to be sent>
@ -139,6 +132,14 @@ longer away.`,
BATCH initiates an IRCv3 client-to-server batch. You should never need to BATCH initiates an IRCv3 client-to-server batch. You should never need to
issue this command manually.`, issue this command manually.`,
},
"brb": {
text: `BRB [message]
Disconnects you from the server, while instructing the server to keep you
present for a short time window. During this window, you can either resume
or reattach to your nickname. If [message] is sent, it is used as your away
message (and as your quit message if you don't return in time).`,
}, },
"cap": { "cap": {
text: `CAP <subcommand> [:<capabilities>] text: `CAP <subcommand> [:<capabilities>]
@ -151,8 +152,8 @@ http://ircv3.net/specs/core/capability-negotiation-3.2.html`,
text: `CHATHISTORY [params] text: `CHATHISTORY [params]
CHATHISTORY is a history replay command associated with the IRCv3 CHATHISTORY is a history replay command associated with the IRCv3
chathistory extension. See this document: specification draft/chathistory. See this document:
https://ircv3.net/specs/extensions/chathistory`, https://github.com/ircv3/ircv3-specifications/pull/393`,
}, },
"debug": { "debug": {
oper: true, oper: true,
@ -176,8 +177,7 @@ spam or other hostile activity. It has five levels, which are cumulative
(i.e., level 3 includes all restrictions from level 4 and so on): (i.e., level 3 includes all restrictions from level 4 and so on):
5: Normal operation 5: Normal operation
4: No new account or channel registrations; if Tor is enabled, no new 4: No new account or channel registrations
unauthenticated connections from Tor
3: All users are +R; no changes to vhosts 3: All users are +R; no changes to vhosts
2: No new unauthenticated connections; all channels are +R 2: No new unauthenticated connections; all channels are +R
1: No new connections except from localhost or other trusted IPs`, 1: No new connections except from localhost or other trusted IPs`,
@ -216,9 +216,7 @@ ON <server> specifies that the ban is to be set on that specific server.
[reason] and [oper reason], if they exist, are separated by a vertical bar (|). [reason] and [oper reason], if they exist, are separated by a vertical bar (|).
If "DLINE LIST" is sent, the server sends back a list of our current DLINEs. If "DLINE LIST" is sent, the server sends back a list of our current DLINEs.`,
To remove a DLINE, use the "UNDLINE" command.`,
}, },
"extjwt": { "extjwt": {
text: `EXTJWT <target> [service_name] text: `EXTJWT <target> [service_name]
@ -238,10 +236,11 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
"history": { "history": {
text: `HISTORY <target> [limit] text: `HISTORY <target> [limit]
Replay message history. <target> can be a channel name or a nickname you have Replay message history. <target> can be a channel name, "me" to replay direct
direct message history with. [limit] can be either an integer (the maximum message history, or a nickname to replay another client's direct message
number of messages to replay), or a time duration like 10m or 1h (the time history (they must be logged into the same account as you). [limit] can be
window within which to replay messages).`, either an integer (the maximum number of messages to replay), or a time
duration like 10m or 1h (the time window within which to replay messages).`,
}, },
"info": { "info": {
text: `INFO text: `INFO
@ -258,11 +257,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>}]
@ -309,9 +303,7 @@ ON <server> specifies that the ban is to be set on that specific server.
[reason] and [oper reason], if they exist, are separated by a vertical bar (|). [reason] and [oper reason], if they exist, are separated by a vertical bar (|).
If "KLINE LIST" is sent, the server sends back a list of our current KLINEs. If "KLINE LIST" is sent, the server sends back a list of our current KLINEs.`,
To remove a KLINE, use the "UNKLINE" command.`,
}, },
"language": { "language": {
text: `LANGUAGE <code>{ <code>} text: `LANGUAGE <code>{ <code>}
@ -331,19 +323,6 @@ channels). <elistcond>s modify how the channels are selected.`,
Shows statistics about the size of the network. If <mask> is given, only Shows statistics about the size of the network. If <mask> is given, only
returns stats for servers matching the given mask. If <server> is given, the returns stats for servers matching the given mask. If <server> is given, the
command is processed by that server.`, command is processed by that server.`,
},
"markread": {
text: `MARKREAD <target> [timestamp]
MARKREAD updates an IRCv3 read message marker. It is not intended for use by
end users. For more details, see the latest draft of the read-marker
specification.`,
},
"metadata": {
text: `METADATA <target> <subcommand> [<everything else>...]
Retrieve and meddle with metadata for the given target.
Have a look at https://ircv3.net/specs/extensions/metadata for interesting technical information.`,
}, },
"mode": { "mode": {
text: `MODE <target> [<modestring> [<mode arguments>...]] text: `MODE <target> [<modestring> [<mode arguments>...]]
@ -423,13 +402,6 @@ Leaves the given channels and shows people the given reason.`,
When the server requires a connection password to join, used to send us the When the server requires a connection password to join, used to send us the
password.`, password.`,
},
"persistence": {
text: `PERSISTENCE [params]
PERSISTENCE is a command associated with an IRC protocol extension for
persistent connections. End users should probably use /NS GET ALWAYS-ON
and /NS SET ALWAYS-ON instead.`,
}, },
"ping": { "ping": {
text: `PING <args>... text: `PING <args>...
@ -445,22 +417,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": {
text: `RELAYMSG <channel> <spoofed nick> :<message>
This command lets channel operators relay messages to their
channel from other messaging systems using relay bots. The
spoofed nickname MUST contain a forwardslash.
For example:
RELAYMSG #ircv3 Mallory/D :Welp, we linked Discord...`,
}, },
"rename": { "rename": {
text: `RENAME <channel> <newname> [<reason>] text: `RENAME <channel> <newname> [<reason>]
@ -516,17 +472,18 @@ specs for more info: http://ircv3.net/specs/core/message-tags-3.3.html`,
text: `QUIT [reason] text: `QUIT [reason]
Indicates that you're leaving the server, and shows everyone the given reason.`, Indicates that you're leaving the server, and shows everyone the given reason.`,
},
"register": {
text: `REGISTER <account> <email | *> <password>
Registers an account in accordance with the draft/account-registration capability.`,
}, },
"rehash": { "rehash": {
oper: true, oper: true,
text: `REHASH text: `REHASH
Reloads the config file and updates TLS certificates on listeners`, Reloads the config file and updates TLS certificates on listeners`,
},
"resume": {
text: `RESUME <oldnick> [timestamp]
Sent before registration has completed, this indicates that the client wants to
resume their old connection <oldnick>.`,
}, },
"time": { "time": {
text: `TIME [server] text: `TIME [server]
@ -538,19 +495,6 @@ Shows the time of the current, or the given, server.`,
If [topic] is given, sets the topic in the channel to that. If [topic] is not If [topic] is given, sets the topic in the channel to that. If [topic] is not
given, views the current topic on the channel.`, given, views the current topic on the channel.`,
},
"uban": {
text: `UBAN <subcommand> [arguments]
Ergo's "unified ban" system. Accepts the following subcommands:
1. UBAN ADD <target> [REQUIRE-SASL] [DURATION <duration>] [REASON...]
2. UBAN DEL <target>
3. UBAN LIST
4. UBAN INFO <target>
<target> may be an IP, a CIDR, a nickmask with wildcards, or the name of an
account to suspend. Note that REQUIRE-SASL is only valid for IP and CIDR bans.`,
}, },
"undline": { "undline": {
oper: true, oper: true,
@ -577,11 +521,6 @@ For example:
Used in connection registration, sets your username and realname to the given Used in connection registration, sets your username and realname to the given
values (though your username may also be looked up with Ident).`, values (though your username may also be looked up with Ident).`,
},
"uninvite": {
text: `UNINVITE <nickname> <channel>
UNINVITE rescinds a channel invitation sent for an invite-only channel.`,
}, },
"users": { "users": {
text: `USERS [parameters] text: `USERS [parameters]
@ -592,11 +531,6 @@ The USERS command is not implemented.`,
text: `USERHOST <nickname>{ <nickname>} text: `USERHOST <nickname>{ <nickname>}
Shows information about the given users. Takes up to 10 nicknames.`, Shows information about the given users. Takes up to 10 nicknames.`,
},
"verify": {
text: `VERIFY <account> <code>
Verifies an account in accordance with the draft/account-registration capability.`,
}, },
"version": { "version": {
text: `VERSION [server] text: `VERSION [server]
@ -615,11 +549,6 @@ ircv3.net/specs/extensions/webirc.html
the connection from the client to the gateway, such as: the connection from the client to the gateway, such as:
- tls: this flag indicates that the client->gateway connection is secure`, - tls: this flag indicates that the client->gateway connection is secure`,
},
"webpush": {
text: `WEBPUSH <subcommand> [arguments]
Configures web push settings. Not for direct use by end users.`,
}, },
"who": { "who": {
text: `WHO <name> [o] text: `WHO <name> [o]
@ -683,15 +612,15 @@ for direct use by end users.`,
"casemapping": { "casemapping": {
text: `RPL_ISUPPORT CASEMAPPING text: `RPL_ISUPPORT CASEMAPPING
Ergo supports an experimental unicode casemapping designed for extended Oragono supports an experimental unicode casemapping designed for extended
Unicode support. This casemapping is based off RFC 7613 and the draft rfc7613 Unicode support. This casemapping is based off RFC 7613 and the draft rfc7613
casemapping spec here: https://ergo.chat/specs.html`, casemapping spec here: https://oragono.io/specs.html`,
helpType: ISupportHelpEntry, helpType: ISupportHelpEntry,
}, },
"prefix": { "prefix": {
text: `RPL_ISUPPORT PREFIX text: `RPL_ISUPPORT PREFIX
Ergo supports the following channel membership prefixes: Oragono supports the following channel membership prefixes:
+q (~) | Founder channel mode. +q (~) | Founder channel mode.
+a (&) | Admin channel mode. +a (&) | Admin channel mode.
@ -788,19 +717,22 @@ func (hm *HelpIndexManager) GenerateIndices(lm *languages.Manager) {
} }
// sendHelp sends the client help of the given string. // sendHelp sends the client help of the given string.
func (client *Client) sendHelp(helpEntry string, text string, rb *ResponseBuffer) { func (client *Client) sendHelp(name string, text string, rb *ResponseBuffer) {
helpEntry = strings.ToUpper(helpEntry) splitName := strings.Split(name, " ")
nick := client.Nick()
textLines := strings.Split(text, "\n") textLines := strings.Split(text, "\n")
for i, line := range textLines { for i, line := range textLines {
args := splitName
args = append(args, line)
if i == 0 { if i == 0 {
rb.Add(nil, client.server.name, RPL_HELPSTART, nick, helpEntry, line) rb.Add(nil, client.server.name, RPL_HELPSTART, args...)
} else { } else {
rb.Add(nil, client.server.name, RPL_HELPTXT, nick, helpEntry, line) rb.Add(nil, client.server.name, RPL_HELPTXT, args...)
} }
} }
rb.Add(nil, client.server.name, RPL_ENDOFHELP, nick, helpEntry, client.t("End of /HELPOP")) args := splitName
args = append(args, client.t("End of /HELPOP"))
rb.Add(nil, client.server.name, RPL_ENDOFHELP, args...)
} }
// GetHelpIndex returns the help index for the given language. // GetHelpIndex returns the help index for the given language.

View File

@ -4,11 +4,9 @@
package history package history
import ( import (
"slices" "github.com/oragono/oragono/irc/utils"
"sync" "sync"
"time" "time"
"github.com/ergochat/ergo/irc/utils"
) )
type ItemType uint type ItemType uint
@ -25,7 +23,6 @@ const (
Tagmsg Tagmsg
Nick Nick
Topic Topic
Invite
) )
const ( const (
@ -46,8 +43,7 @@ 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:
CfCorrespondent string `json:"CfCorrespondent,omitempty"` CfCorrespondent string
IsBot bool `json:"IsBot,omitempty"`
} }
// HasMsgid tests whether a message has the message id `msgid`. // HasMsgid tests whether a message has the message id `msgid`.
@ -57,6 +53,12 @@ func (item *Item) HasMsgid(msgid string) bool {
type Predicate func(item *Item) (matches bool) type Predicate func(item *Item) (matches bool)
func Reverse(results []Item) {
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
results[i], results[j] = results[j], results[i]
}
}
// Buffer is a ring buffer holding message/event history for a channel or user // Buffer is a ring buffer holding message/event history for a channel or user
type Buffer struct { type Buffer struct {
sync.RWMutex sync.RWMutex
@ -156,7 +158,7 @@ func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Pr
defer func() { defer func() {
if !ascending { if !ascending {
slices.Reverse(results) Reverse(results)
} }
}() }()
@ -197,78 +199,6 @@ func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Pr
return list.matchInternal(satisfies, ascending, limit), complete, nil return list.matchInternal(satisfies, ascending, limit), complete, nil
} }
// returns all correspondents, in reverse time order
func (list *Buffer) allCorrespondents() (results []TargetListing) {
seen := make(utils.HashSet[string])
list.RLock()
defer list.RUnlock()
if list.start == -1 || len(list.buffer) == 0 {
return
}
// XXX traverse in reverse order, so we get the latest timestamp
// of any message sent to/from the correspondent
pos := list.prev(list.end)
stop := list.start
for {
if !seen.Has(list.buffer[pos].CfCorrespondent) {
seen.Add(list.buffer[pos].CfCorrespondent)
results = append(results, TargetListing{
CfName: list.buffer[pos].CfCorrespondent,
Time: list.buffer[pos].Message.Time,
})
}
if pos == stop {
break
}
pos = list.prev(pos)
}
return
}
// list DM correspondents, as one input to CHATHISTORY TARGETS
func (list *Buffer) listCorrespondents(start, end Selector, cutoff time.Time, limit int) (results []TargetListing, err error) {
after := start.Time
before := end.Time
after, before, ascending := MinMaxAsc(after, before, cutoff)
correspondents := list.allCorrespondents()
if len(correspondents) == 0 {
return
}
// XXX allCorrespondents returns results in reverse order,
// so if we're ascending, we actually go backwards
var i int
if ascending {
i = len(correspondents) - 1
} else {
i = 0
}
for 0 <= i && i < len(correspondents) && (limit == 0 || len(results) < limit) {
if (after.IsZero() || correspondents[i].Time.After(after)) &&
(before.IsZero() || correspondents[i].Time.Before(before)) {
results = append(results, correspondents[i])
}
if ascending {
i--
} else {
i++
}
}
if !ascending {
slices.Reverse(results)
}
return
}
// implements history.Sequence, emulating a single history buffer (for a channel, // implements history.Sequence, emulating a single history buffer (for a channel,
// a single user's DMs, or a DM conversation) // a single user's DMs, or a DM conversation)
type bufferSequence struct { type bufferSequence struct {
@ -291,27 +221,14 @@ func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequenc
} }
} }
func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, err error) { func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, complete bool, err error) {
results, _, err = seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit) return seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
return
} }
func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) { func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
return GenericAround(seq, start, limit) return GenericAround(seq, start, limit)
} }
func (seq *bufferSequence) ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error) {
return seq.list.listCorrespondents(start, end, seq.cutoff, limit)
}
func (seq *bufferSequence) Cutoff() time.Time {
return seq.cutoff
}
func (seq *bufferSequence) Ephemeral() bool {
return true
}
// you must be holding the read lock to call this // you must be holding the read lock to call this
func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) { func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
if list.start == -1 || len(list.buffer) == 0 { if list.start == -1 || len(list.buffer) == 0 {
@ -404,6 +321,18 @@ func (list *Buffer) next(index int) int {
} }
} }
// return n such that v <= n and n == 2**i for some i
func roundUpToPowerOfTwo(v int) int {
// http://graphics.stanford.edu/~seander/bithacks.html
v -= 1
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
return v + 1
}
func (list *Buffer) maybeExpand() { func (list *Buffer) maybeExpand() {
if list.window == 0 { if list.window == 0 {
return // autoresize is disabled return // autoresize is disabled
@ -423,7 +352,7 @@ func (list *Buffer) maybeExpand() {
return // oldest element is old enough to overwrite return // oldest element is old enough to overwrite
} }
newSize := utils.RoundUpToPowerOfTwo(length + 1) newSize := roundUpToPowerOfTwo(length + 1)
if list.maximumSize < newSize { if list.maximumSize < newSize {
newSize = list.maximumSize newSize = list.maximumSize
} }

View File

@ -241,6 +241,17 @@ func TestDisabledByResize(t *testing.T) {
assertEqual(len(items), 0, t) assertEqual(len(items), 0, t)
} }
func TestRoundUp(t *testing.T) {
assertEqual(roundUpToPowerOfTwo(2), 2, t)
assertEqual(roundUpToPowerOfTwo(3), 4, t)
assertEqual(roundUpToPowerOfTwo(64), 64, t)
assertEqual(roundUpToPowerOfTwo(65), 128, t)
assertEqual(roundUpToPowerOfTwo(100), 128, t)
assertEqual(roundUpToPowerOfTwo(1000), 1024, t)
assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
}
func BenchmarkInsert(b *testing.B) { func BenchmarkInsert(b *testing.B) {
buf := NewHistoryBuffer(1024, 0) buf := NewHistoryBuffer(1024, 0)
b.ResetTimer() b.ResetTimer()

View File

@ -4,7 +4,6 @@
package history package history
import ( import (
"strings"
"time" "time"
) )
@ -18,24 +17,15 @@ type Selector struct {
// it encapsulates restrictions such as registration time cutoffs, or // it encapsulates restrictions such as registration time cutoffs, or
// only looking at a single "query buffer" (DMs with a particular correspondent) // only looking at a single "query buffer" (DMs with a particular correspondent)
type Sequence interface { type Sequence interface {
Between(start, end Selector, limit int) (results []Item, err error) Between(start, end Selector, limit int) (results []Item, complete bool, err error)
Around(start Selector, limit int) (results []Item, err error) Around(start Selector, limit int) (results []Item, err error)
ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error)
// this are weird hacks that violate the encapsulation of Sequence to some extent;
// Cutoff() returns the cutoff time for other code to use (it returns the zero time
// if none is set), and Ephemeral() returns whether the backing store is in-memory
// or a persistent database.
Cutoff() time.Time
Ephemeral() bool
} }
// This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics // This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) { func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
var halfLimit int var halfLimit int
halfLimit = (limit + 1) / 2 halfLimit = (limit + 1) / 2
initialResults, err := seq.Between(Selector{}, start, halfLimit) initialResults, _, err := seq.Between(Selector{}, start, halfLimit)
if err != nil { if err != nil {
return return
} else if len(initialResults) == 0 { } else if len(initialResults) == 0 {
@ -44,7 +34,7 @@ func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err
return return
} }
newStart := Selector{Time: initialResults[0].Message.Time} newStart := Selector{Time: initialResults[0].Message.Time}
results, err = seq.Between(newStart, Selector{}, limit) results, _, err = seq.Between(newStart, Selector{}, limit)
return return
} }
@ -78,16 +68,3 @@ func MinMaxAsc(after, before, cutoff time.Time) (min, max time.Time, ascending b
} }
return after, before, ascending return after, before, ascending
} }
// maps regular msgids from JOIN, etc. to a msgid suitable for attaching
// to a HistServ message describing the JOIN. See #491 for some history.
func HistservMungeMsgid(msgid string) string {
return "_" + msgid
}
// strips munging from a msgid. future schemes may not support a well-defined
// mapping of munged msgids to true msgids, but munged msgids should always contain
// a _, with metadata in front and data (possibly the true msgid) after.
func NormalizeMsgid(msgid string) string {
return strings.TrimPrefix(msgid, "_")
}

View File

@ -1,77 +0,0 @@
// Copyright (c) 2021 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package history
import (
"slices"
"sort"
"time"
)
type TargetListing struct {
CfName string
Time time.Time
}
// Merge `base`, a paging window of targets, with `extras` (the target entries
// for all joined channels).
func MergeTargets(base []TargetListing, extra []TargetListing, start, end time.Time, limit int) (results []TargetListing) {
if len(extra) == 0 {
return base
}
SortCorrespondents(extra)
start, end, ascending := MinMaxAsc(start, end, time.Time{})
predicate := func(t time.Time) bool {
return (start.IsZero() || start.Before(t)) && (end.IsZero() || end.After(t))
}
prealloc := len(base) + len(extra)
if limit < prealloc {
prealloc = limit
}
results = make([]TargetListing, 0, prealloc)
if !ascending {
slices.Reverse(base)
slices.Reverse(extra)
}
for len(results) < limit {
if len(extra) != 0 {
if !predicate(extra[0].Time) {
extra = extra[1:]
continue
}
if len(base) != 0 {
if base[0].Time.Before(extra[0].Time) == ascending {
results = append(results, base[0])
base = base[1:]
} else {
results = append(results, extra[0])
extra = extra[1:]
}
} else {
results = append(results, extra[0])
extra = extra[1:]
}
} else if len(base) != 0 {
results = append(results, base[0])
base = base[1:]
} else {
break
}
}
if !ascending {
slices.Reverse(results)
}
return
}
func SortCorrespondents(list []TargetListing) {
sort.Slice(list, func(i, j int) bool {
return list[i].Time.Before(list[j].Time)
})
}

View File

@ -7,24 +7,18 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"os" "os"
"runtime/debug"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/ergochat/ergo/irc/history" "github.com/oragono/oragono/irc/history"
"github.com/ergochat/ergo/irc/modes" "github.com/oragono/oragono/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.`
histServMask = "HistServ!HistServ@localhost"
) )
func histservEnabled(config *Config) bool { func histservEnabled(config *Config) bool {
@ -50,13 +44,14 @@ FORGET deletes all history messages sent by an account.`,
}, },
"delete": { "delete": {
handler: histservDeleteHandler, handler: histservDeleteHandler,
help: `Syntax: $bDELETE <target> <msgid>$b help: `Syntax: $bDELETE [target] <msgid>$b
DELETE deletes an individual message by its msgid. The target is the channel DELETE deletes an individual message by its msgid. The target is a channel
name. The msgid is the ID as can be found in the tags of that message.`, name or nickname; depending on the history implementation, this may or may not
helpShort: `$bDELETE$b deletes an individual message by its target and msgid.`, be necessary to locate the message.`,
helpShort: `$bDELETE$b deletes an individual message by its msgid.`,
enabled: histservEnabled, enabled: histservEnabled,
minParams: 2, minParams: 1,
maxParams: 2, maxParams: 2,
}, },
"export": { "export": {
@ -76,7 +71,7 @@ the request of the account holder.`,
help: `Syntax: $bPLAY <target> [limit]$b help: `Syntax: $bPLAY <target> [limit]$b
PLAY plays back history messages, rendering them into direct messages from PLAY plays back history messages, rendering them into direct messages from
HistServ. 'target' is a channel name or nickname to query, and 'limit' HistServ. 'target' is a channel name (or 'me' for direct messages), and 'limit'
is a message count or a time duration. Note that message playback may be is a message count or a time duration. Note that message playback may be
incomplete or degraded, relative to direct playback from /HISTORY or incomplete or degraded, relative to direct playback from /HISTORY or
CHATHISTORY.`, CHATHISTORY.`,
@ -88,96 +83,81 @@ CHATHISTORY.`,
} }
) )
func histservForgetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { // histNotice sends the client a notice from HistServ
func histNotice(rb *ResponseBuffer, text string) {
rb.Add(nil, histServMask, "NOTICE", rb.target.Nick(), text)
}
func histservForgetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
accountName := server.accounts.AccountToAccountName(params[0]) accountName := server.accounts.AccountToAccountName(params[0])
if accountName == "" { if accountName == "" {
service.Notice(rb, client.t("Could not look up account name, proceeding anyway")) histNotice(rb, client.t("Could not look up account name, proceeding anyway"))
accountName = params[0] accountName = params[0]
} }
server.ForgetHistory(accountName) server.ForgetHistory(accountName)
service.Notice(rb, fmt.Sprintf(client.t("Enqueued account %s for message deletion"), accountName)) histNotice(rb, fmt.Sprintf(client.t("Enqueued account %s for message deletion"), accountName))
} }
// Returns: func histservDeleteHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
// var target, msgid string
// 1. `canDeleteAny` if the client allowed to delete other users' messages from the target, ie.: if len(params) == 1 {
// - the client is a channel operator, or msgid = params[0]
// - 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 { } else {
if server.Config().History.Retention.AllowIndividualDelete { target, msgid = params[0], params[1]
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) {
target, msgid := params[0], params[1] // Fix #1881 2 params are required
canDelete := deletionPolicy(server, client, target)
accountName := "*" accountName := "*"
if canDelete == canDeleteNone { hasPrivs := client.HasRoleCapabs("history")
service.Notice(rb, client.t("Insufficient privileges")) if !hasPrivs {
return
} else if canDelete == canDeleteSelf {
accountName = client.AccountName() accountName = client.AccountName()
if accountName == "*" { if !(server.Config().History.Retention.AllowIndividualDelete && accountName != "*") {
service.Notice(rb, client.t("Insufficient privileges")) histNotice(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")) histNotice(rb, client.t("Successfully deleted message"))
} else { } else {
isOper := client.HasRoleCapabs("history") if hasPrivs {
if isOper { histNotice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err))
service.Notice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err))
} else { } else {
service.Notice(rb, client.t("Could not delete message")) histNotice(rb, client.t("Could not delete message"))
} }
} }
} }
func histservExportHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func histservExportHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
cfAccount, err := CasefoldName(params[0]) cfAccount, err := CasefoldName(params[0])
if err != nil { if err != nil {
service.Notice(rb, client.t("Invalid account name")) histNotice(rb, client.t("Invalid account name"))
return return
} }
config := server.Config() config := server.Config()
// don't include the account name in the filename because of escaping concerns // don't include the account name in the filename because of escaping concerns
filename := fmt.Sprintf("%s-%s.json", utils.GenerateSecretToken(), time.Now().UTC().Format(utils.IRCv3TimestampFormat)) filename := fmt.Sprintf("%s-%s.json", utils.GenerateSecretToken(), time.Now().UTC().Format(IRCv3TimestampFormat))
pathname := config.getOutputPath(filename) pathname := config.getOutputPath(filename)
outfile, err := os.Create(pathname) outfile, err := os.Create(pathname)
if err != nil { if err != nil {
service.Notice(rb, fmt.Sprintf(client.t("Error opening export file: %v"), err)) histNotice(rb, fmt.Sprintf(client.t("Error opening export file: %v"), err))
} else { } else {
service.Notice(rb, fmt.Sprintf(client.t("Started exporting data for account %[1]s to file %[2]s"), cfAccount, filename)) histNotice(rb, fmt.Sprintf(client.t("Started exporting data for account %[1]s to file %[2]s"), cfAccount, filename))
} }
go histservExportAndNotify(service, server, cfAccount, outfile, filename, client.Nick()) go histservExportAndNotify(server, cfAccount, outfile, filename, client.Nick())
} }
func histservExportAndNotify(service *ircService, server *Server, cfAccount string, outfile *os.File, filename, alertNick string) { func histservExportAndNotify(server *Server, cfAccount string, outfile *os.File, filename, alertNick string) {
defer server.HandlePanic(nil) defer func() {
if r := recover(); r != nil {
server.logger.Error("history",
fmt.Sprintf("Panic in history export routine: %v\n%s", r, debug.Stack()))
}
}()
defer outfile.Close() defer outfile.Close()
writer := bufio.NewWriter(outfile) writer := bufio.NewWriter(outfile)
@ -187,19 +167,19 @@ func histservExportAndNotify(service *ircService, server *Server, cfAccount stri
client := server.clients.Get(alertNick) client := server.clients.Get(alertNick)
if client != nil && client.HasRoleCapabs("history") { if client != nil && client.HasRoleCapabs("history") {
client.Send(nil, service.prefix, "NOTICE", client.Nick(), fmt.Sprintf(client.t("Data export for %[1]s completed and written to %[2]s"), cfAccount, filename)) client.Send(nil, histServMask, "NOTICE", client.Nick(), fmt.Sprintf(client.t("Data export for %[1]s completed and written to %[2]s"), cfAccount, filename))
} }
} }
func histservPlayHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func histservPlayHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
items, _, err := easySelectHistory(server, client, params) items, _, err := easySelectHistory(server, client, params)
if err != nil { if err != nil {
service.Notice(rb, client.t("Could not retrieve history")) histNotice(rb, client.t("Could not retrieve history"))
return return
} }
playMessage := func(timestamp time.Time, nick, message string) { playMessage := func(timestamp time.Time, nick, message string) {
service.Notice(rb, fmt.Sprintf("%s <%s> %s", timestamp.Format("15:04:05"), NUHToNick(nick), message)) histNotice(rb, fmt.Sprintf("%s <%s> %s", timestamp.Format("15:04:05"), stripMaskFromNick(nick), message))
} }
for _, item := range items { for _, item := range items {
@ -216,12 +196,16 @@ func histservPlayHandler(service *ircService, server *Server, client *Client, co
} }
} }
service.Notice(rb, client.t("End of history playback")) histNotice(rb, client.t("End of history playback"))
} }
// handles parameter parsing and history queries for /HISTORY and /HISTSERV PLAY // handles parameter parsing and history queries for /HISTORY and /HISTSERV PLAY
func easySelectHistory(server *Server, client *Client, params []string) (items []history.Item, channel *Channel, err error) { func easySelectHistory(server *Server, client *Client, params []string) (items []history.Item, channel *Channel, err error) {
channel, sequence, err := server.GetHistorySequence(nil, client, params[0]) target := params[0]
if strings.ToLower(target) == "me" {
target = "*"
}
channel, sequence, err := server.GetHistorySequence(nil, client, target)
if sequence == nil || err != nil { if sequence == nil || err != nil {
return nil, nil, errNoSuchChannel return nil, nil, errNoSuchChannel
@ -249,12 +233,12 @@ func easySelectHistory(server *Server, client *Client, params []string) (items [
} }
if duration == 0 { if duration == 0 {
items, err = sequence.Between(history.Selector{}, history.Selector{}, limit) items, _, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
} else { } else {
now := time.Now().UTC() now := time.Now().UTC()
start := history.Selector{Time: now} start := history.Selector{Time: now}
end := history.Selector{Time: now.Add(-duration)} end := history.Selector{Time: now.Add(-duration)}
items, err = sequence.Between(start, end, limit) items, _, err = sequence.Between(start, end, limit)
} }
return return
} }

View File

@ -7,16 +7,18 @@ import (
"errors" "errors"
"fmt" "fmt"
"regexp" "regexp"
"time"
"github.com/ergochat/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircfmt"
"github.com/ergochat/ergo/irc/sno" "github.com/oragono/oragono/irc/sno"
"github.com/ergochat/ergo/irc/utils" "github.com/oragono/oragono/irc/utils"
) )
const ( const (
hostservHelp = `HostServ lets you manage your vhost (i.e., the string displayed hostservHelp = `HostServ lets you manage your vhost (i.e., the string displayed
in place of your client's hostname/IP).` in place of your client's hostname/IP).`
hsNickMask = "HostServ!HostServ@localhost"
) )
var ( var (
@ -30,6 +32,10 @@ func hostservEnabled(config *Config) bool {
return config.Accounts.VHosts.Enabled return config.Accounts.VHosts.Enabled
} }
func hostservRequestsEnabled(config *Config) bool {
return config.Accounts.VHosts.Enabled && config.Accounts.VHosts.UserRequests.Enabled
}
var ( var (
hostservCommands = map[string]*serviceCommand{ hostservCommands = map[string]*serviceCommand{
"on": { "on": {
@ -50,20 +56,31 @@ OFF disables your vhost, if you have one approved.`,
authRequired: true, authRequired: true,
enabled: hostservEnabled, enabled: hostservEnabled,
}, },
"request": {
handler: hsRequestHandler,
help: `Syntax: $bREQUEST <vhost>$b
REQUEST requests that a new vhost by assigned to your account. The request must
then be approved by a server operator.`,
helpShort: `$bREQUEST$b requests a new vhost, pending operator approval.`,
authRequired: true,
enabled: hostservRequestsEnabled,
minParams: 1,
},
"status": { "status": {
handler: hsStatusHandler, handler: hsStatusHandler,
help: `Syntax: $bSTATUS [user]$b help: `Syntax: $bSTATUS [user]$b
STATUS displays your current vhost, if any, and whether it is enabled or STATUS displays your current vhost, if any, and the status of your most recent
disabled. A server operator can view someone else's status.`, request for a new one. A server operator can view someone else's status.`,
helpShort: `$bSTATUS$b shows your vhost status.`, helpShort: `$bSTATUS$b shows your vhost and request status.`,
enabled: hostservEnabled, enabled: hostservEnabled,
}, },
"set": { "set": {
handler: hsSetHandler, handler: hsSetHandler,
help: `Syntax: $bSET <user> <vhost>$b help: `Syntax: $bSET <user> <vhost>$b
SET sets a user's vhost.`, SET sets a user's vhost, bypassing the request system.`,
helpShort: `$bSET$b sets a user's vhost.`, helpShort: `$bSET$b sets a user's vhost.`,
capabs: []string{"vhosts"}, capabs: []string{"vhosts"},
enabled: hostservEnabled, enabled: hostservEnabled,
@ -79,6 +96,39 @@ DEL deletes a user's vhost.`,
enabled: hostservEnabled, enabled: hostservEnabled,
minParams: 1, minParams: 1,
}, },
"waiting": {
handler: hsWaitingHandler,
help: `Syntax: $bWAITING$b
WAITING shows a list of pending vhost requests, which can then be approved
or rejected.`,
helpShort: `$bWAITING$b shows a list of pending vhost requests.`,
capabs: []string{"vhosts"},
enabled: hostservEnabled,
},
"approve": {
handler: hsApproveHandler,
help: `Syntax: $bAPPROVE <user>$b
APPROVE approves a user's vhost request.`,
helpShort: `$bAPPROVE$b approves a user's vhost request.`,
capabs: []string{"vhosts"},
enabled: hostservEnabled,
minParams: 1,
},
"reject": {
handler: hsRejectHandler,
help: `Syntax: $bREJECT <user> [<reason>]$b
REJECT rejects a user's vhost request, optionally giving them a reason
for the rejection.`,
helpShort: `$bREJECT$b rejects a user's vhost request.`,
capabs: []string{"vhosts"},
enabled: hostservEnabled,
minParams: 1,
maxParams: 2,
unsplitFinalParam: true,
},
"setcloaksecret": { "setcloaksecret": {
handler: hsSetCloakSecretHandler, handler: hsSetCloakSecretHandler,
help: `Syntax: $bSETCLOAKSECRET$b <secret> [code] help: `Syntax: $bSETCLOAKSECRET$b <secret> [code]
@ -95,7 +145,25 @@ display the necessary code.`,
} }
) )
func hsOnOffHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { // hsNotice sends the client a notice from HostServ
func hsNotice(rb *ResponseBuffer, text string) {
rb.Add(nil, hsNickMask, "NOTICE", rb.target.Nick(), text)
}
// hsNotifyChannel notifies the designated channel of new vhost activity
func hsNotifyChannel(server *Server, message string) {
chname := server.Config().Accounts.VHosts.UserRequests.Channel
channel := server.channels.Get(chname)
if channel == nil {
return
}
chname = channel.Name()
for _, client := range channel.Members() {
client.Send(nil, hsNickMask, "PRIVMSG", chname, message)
}
}
func hsOnOffHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
enable := false enable := false
if command == "on" { if command == "on" {
enable = true enable = true
@ -103,28 +171,51 @@ func hsOnOffHandler(service *ircService, server *Server, client *Client, command
_, err := server.accounts.VHostSetEnabled(client, enable) _, err := server.accounts.VHostSetEnabled(client, enable)
if err == errNoVhost { if err == errNoVhost {
service.Notice(rb, client.t(err.Error())) hsNotice(rb, client.t(err.Error()))
} else if err != nil { } else if err != nil {
service.Notice(rb, client.t("An error occurred")) hsNotice(rb, client.t("An error occurred"))
} else if enable { } else if enable {
service.Notice(rb, client.t("Successfully enabled your vhost")) hsNotice(rb, client.t("Successfully enabled your vhost"))
} else { } else {
service.Notice(rb, client.t("Successfully disabled your vhost")) hsNotice(rb, client.t("Successfully disabled your vhost"))
} }
} }
func hsStatusHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func hsRequestHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
vhost := params[0]
if validateVhost(server, vhost, false) != nil {
hsNotice(rb, client.t("Invalid vhost"))
return
}
accountName := client.Account()
_, err := server.accounts.VHostRequest(accountName, vhost, time.Duration(server.Config().Accounts.VHosts.UserRequests.Cooldown))
if err != nil {
if throttled, ok := err.(*vhostThrottleExceeded); ok {
hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), throttled.timeRemaining))
} else {
hsNotice(rb, client.t("An error occurred"))
}
} else {
hsNotice(rb, client.t("Your vhost request will be reviewed by an administrator"))
chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost)
hsNotifyChannel(server, chanMsg)
server.snomasks.Send(sno.LocalVhosts, chanMsg)
}
}
func hsStatusHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var accountName string var accountName string
if len(params) > 0 { if len(params) > 0 {
if !client.HasRoleCapabs("vhosts") { if !client.HasRoleCapabs("vhosts") {
service.Notice(rb, client.t("Command restricted")) hsNotice(rb, client.t("Command restricted"))
return return
} }
accountName = params[0] accountName = params[0]
} else { } else {
accountName = client.Account() accountName = client.Account()
if accountName == "" { if accountName == "" {
service.Notice(rb, client.t("You're not logged into an account")) hsNotice(rb, client.t("You're not logged into an account"))
return return
} }
} }
@ -134,17 +225,24 @@ func hsStatusHandler(service *ircService, server *Server, client *Client, comman
if err != errAccountDoesNotExist { if err != errAccountDoesNotExist {
server.logger.Warning("internal", "error loading account info", accountName, err.Error()) server.logger.Warning("internal", "error loading account info", accountName, err.Error())
} }
service.Notice(rb, client.t("No such account")) hsNotice(rb, client.t("No such account"))
return return
} }
if account.VHost.ApprovedVHost != "" { if account.VHost.ApprovedVHost != "" {
service.Notice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost)) hsNotice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost))
if !account.VHost.Enabled { if !account.VHost.Enabled {
service.Notice(rb, client.t("This vhost is currently disabled, but can be enabled with /HS ON")) hsNotice(rb, client.t("This vhost is currently disabled, but can be enabled with /HS ON"))
} }
} else { } else {
service.Notice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName)) hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName))
}
if account.VHost.RequestedVHost != "" {
hsNotice(rb, fmt.Sprintf(client.t("A request is pending for vhost: %s"), account.VHost.RequestedVHost))
}
if account.VHost.RejectedVHost != "" {
hsNotice(rb, fmt.Sprintf(client.t("A request was previously made for vhost: %s"), account.VHost.RejectedVHost))
hsNotice(rb, fmt.Sprintf(client.t("It was rejected for reason: %s"), account.VHost.RejectionReason))
} }
} }
@ -153,21 +251,20 @@ func validateVhost(server *Server, vhost string, oper bool) error {
if len(vhost) > config.Accounts.VHosts.MaxLength { if len(vhost) > config.Accounts.VHosts.MaxLength {
return errVHostTooLong return errVHostTooLong
} }
if !config.Accounts.VHosts.validRegexp.MatchString(vhost) { if !config.Accounts.VHosts.ValidRegexp.MatchString(vhost) {
return errVHostBadCharacters return errVHostBadCharacters
} }
return nil return nil
} }
func hsSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func hsSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
oper := client.Oper()
user := params[0] user := params[0]
var vhost string var vhost string
if command == "set" { if command == "set" {
vhost = params[1] vhost = params[1]
if validateVhost(server, vhost, true) != nil { if validateVhost(server, vhost, true) != nil {
service.Notice(rb, client.t("Invalid vhost")) hsNotice(rb, client.t("Invalid vhost"))
return return
} }
} }
@ -175,24 +272,72 @@ func hsSetHandler(service *ircService, server *Server, client *Client, command s
_, err := server.accounts.VHostSet(user, vhost) _, err := server.accounts.VHostSet(user, vhost)
if err != nil { if err != nil {
service.Notice(rb, client.t("An error occurred")) hsNotice(rb, client.t("An error occurred"))
} else if vhost != "" { } else if vhost != "" {
service.Notice(rb, client.t("Successfully set vhost")) hsNotice(rb, client.t("Successfully set vhost"))
server.snomasks.Send(sno.LocalVhosts, fmt.Sprintf("Operator %[1]s set vhost %[2]s on account %[3]s", oper.Name, vhost, user))
} else { } else {
service.Notice(rb, client.t("Successfully cleared vhost")) hsNotice(rb, client.t("Successfully cleared vhost"))
server.snomasks.Send(sno.LocalVhosts, fmt.Sprintf("Operator %[1]s cleared vhost on account %[2]s", oper.Name, user))
} }
} }
func hsSetCloakSecretHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func hsWaitingHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
requests, total := server.accounts.VHostListRequests(10)
hsNotice(rb, fmt.Sprintf(client.t("There are %[1]d pending requests for vhosts (%[2]d displayed)"), total, len(requests)))
for i, request := range requests {
hsNotice(rb, fmt.Sprintf(client.t("%[1]d. User %[2]s requests vhost: %[3]s"), i+1, request.Account, request.RequestedVHost))
}
}
func hsApproveHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
user := params[0]
vhostInfo, err := server.accounts.VHostApprove(user)
if err != nil {
hsNotice(rb, client.t("An error occurred"))
} else {
hsNotice(rb, fmt.Sprintf(client.t("Successfully approved vhost request for %s"), user))
chanMsg := fmt.Sprintf("Oper %[1]s approved vhost %[2]s for account %[3]s", client.Nick(), vhostInfo.ApprovedVHost, user)
hsNotifyChannel(server, chanMsg)
server.snomasks.Send(sno.LocalVhosts, chanMsg)
for _, client := range server.accounts.AccountToClients(user) {
client.Send(nil, hsNickMask, "NOTICE", client.Nick(), client.t("Your vhost request was approved by an administrator"))
}
}
}
func hsRejectHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var reason string
user := params[0]
if len(params) > 1 {
reason = params[1]
}
vhostInfo, err := server.accounts.VHostReject(user, reason)
if err != nil {
hsNotice(rb, client.t("An error occurred"))
} else {
hsNotice(rb, fmt.Sprintf(client.t("Successfully rejected vhost request for %s"), user))
chanMsg := fmt.Sprintf("Oper %s rejected vhost %s for account %s, with the reason: %v", client.Nick(), vhostInfo.RejectedVHost, user, reason)
hsNotifyChannel(server, chanMsg)
server.snomasks.Send(sno.LocalVhosts, chanMsg)
for _, client := range server.accounts.AccountToClients(user) {
if reason == "" {
client.Send(nil, hsNickMask, "NOTICE", client.Nick(), client.t("Your vhost request was rejected by an administrator"))
} else {
client.Send(nil, hsNickMask, "NOTICE", client.Nick(), fmt.Sprintf(client.t("Your vhost request was rejected by an administrator. The reason given was: %s"), reason))
}
}
}
}
func hsSetCloakSecretHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
secret := params[0] secret := params[0]
expectedCode := utils.ConfirmationCode(secret, server.ctime) expectedCode := utils.ConfirmationCode(secret, server.ctime)
if len(params) == 1 || params[1] != expectedCode { if len(params) == 1 || params[1] != expectedCode {
service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: changing the cloak secret will invalidate stored ban/invite/exception lists.$b"))) hsNotice(rb, ircfmt.Unescape(client.t("$bWarning: changing the cloak secret will invalidate stored ban/invite/exception lists.$b")))
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode))) hsNotice(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")) hsNotice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
} }

133
irc/idletimer.go Normal file
View File

@ -0,0 +1,133 @@
// Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"time"
)
// BrbTimer is a timer on the client as a whole (not an individual session) for implementing
// the BRB command and related functionality (where a client can remain online without
// having any connected sessions).
type BrbState uint
const (
// BrbDisabled is the default state; the client will be disconnected if it has no sessions
BrbDisabled BrbState = iota
// BrbEnabled allows the client to remain online without sessions; if a timeout is
// reached, it will be removed
BrbEnabled
// BrbDead is the state of a client after its timeout has expired; it will be removed
// and therefore new sessions cannot be attached to it
BrbDead
)
type BrbTimer struct {
// XXX we use client.stateMutex for synchronization, so we can atomically test
// conditions that use both brbTimer.state and client.sessions. This code
// is tightly coupled with the rest of Client.
client *Client
state BrbState
brbAt time.Time
duration time.Duration
timer *time.Timer
}
func (bt *BrbTimer) Initialize(client *Client) {
bt.client = client
}
// attempts to enable BRB for a client, returns whether it succeeded
func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
// TODO make this configurable
duration = ResumeableTotalTimeout
bt.client.stateMutex.Lock()
defer bt.client.stateMutex.Unlock()
if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
return
}
switch bt.state {
case BrbDisabled, BrbEnabled:
bt.state = BrbEnabled
bt.duration = duration
bt.resetTimeout()
// only track the earliest BRB, if multiple sessions are BRB'ing at once
// TODO(#524) this is inaccurate in case of an auto-BRB
if bt.brbAt.IsZero() {
bt.brbAt = time.Now().UTC()
}
success = true
default:
// BrbDead
success = false
}
return
}
// turns off BRB for a client and stops the timer; used on resume and during
// client teardown
func (bt *BrbTimer) Disable() (brbAt time.Time) {
bt.client.stateMutex.Lock()
defer bt.client.stateMutex.Unlock()
if bt.state == BrbEnabled {
bt.state = BrbDisabled
brbAt = bt.brbAt
bt.brbAt = time.Time{}
}
bt.resetTimeout()
return
}
func (bt *BrbTimer) resetTimeout() {
if bt.timer != nil {
bt.timer.Stop()
}
if bt.state != BrbEnabled {
return
}
if bt.timer == nil {
bt.timer = time.AfterFunc(bt.duration, bt.processTimeout)
} else {
bt.timer.Reset(bt.duration)
}
}
func (bt *BrbTimer) processTimeout() {
dead := false
defer func() {
if dead {
bt.client.Quit(bt.client.AwayMessage(), nil)
bt.client.destroy(nil)
}
}()
bt.client.stateMutex.Lock()
defer bt.client.stateMutex.Unlock()
if bt.client.alwaysOn {
return
}
switch bt.state {
case BrbDisabled, BrbEnabled:
if len(bt.client.sessions) == 0 {
// client never returned, quit them
bt.state = BrbDead
dead = true
} else {
// client resumed, reattached, or has another active session
bt.state = BrbDisabled
bt.brbAt = time.Time{}
}
case BrbDead:
dead = true // shouldn't be possible but whatever
}
bt.resetTimeout()
}

View File

@ -1,261 +0,0 @@
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"encoding/json"
"fmt"
"log"
"os"
"strconv"
"time"
"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/webpush"
)
const (
// produce a hardcoded version of the database schema
// 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
// db of the hardcoded version)
importDBSchemaVersion = 24
)
type userImport struct {
Name string
Hash string
Email string
RegisteredAt int64 `json:"registeredAt"`
Vhost string
AdditionalNicks []string `json:"additionalNicks"`
Certfps []string
}
type channelImport struct {
Name string
Founder string
RegisteredAt int64 `json:"registeredAt"`
Topic string
TopicSetBy string `json:"topicSetBy"`
TopicSetAt int64 `json:"topicSetAt"`
Amode map[string]string
Modes string
Key string
Limit int
Forward string
}
type databaseImport struct {
Version int
Source string
Users map[string]userImport
Channels map[string]channelImport
}
func convertAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result map[string]modes.Mode, err error) {
result = make(map[string]modes.Mode)
for accountName, mode := range raw {
if len(mode) != 1 {
return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
}
cfname, err := CasefoldName(accountName)
if err != nil || !validCfUsernames.Has(cfname) {
log.Printf("skipping invalid amode recipient %s\n", accountName)
} else {
result[cfname] = modes.Mode(mode[0])
}
}
return
}
func doImportDBGeneric(config *Config, dbImport databaseImport, credsType CredentialsVersion, tx *buntdb.Tx) (err error) {
requiredVersion := 1
if dbImport.Version != requiredVersion {
return fmt.Errorf("unsupported version of the db for import: version %d is required", requiredVersion)
}
tx.Set(keySchemaVersion, strconv.Itoa(importDBSchemaVersion), nil)
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
vapidKeys, err := webpush.GenerateVAPIDKeys()
if err != nil {
return err
}
vapidKeysJSON, err := json.Marshal(vapidKeys)
if err != nil {
return err
}
tx.Set(keyVAPIDKeys, string(vapidKeysJSON), nil)
cfUsernames := make(utils.HashSet[string])
skeletonToUsername := make(map[string]string)
warnSkeletons := false
for username, userInfo := range dbImport.Users {
cfUsername, err := CasefoldName(username)
skeleton, skErr := Skeleton(username)
if err != nil || skErr != nil {
log.Printf("invalid username %s: %v\n", username, err)
continue
}
if existingSkelUser, ok := skeletonToUsername[skeleton]; ok {
log.Printf("Users %s and %s have confusable nicknames; this may render one or both accounts unusable\n", username, existingSkelUser)
warnSkeletons = true
} else {
skeletonToUsername[skeleton] = username
}
var certfps []string
for _, certfp := range userInfo.Certfps {
normalizedCertfp, err := utils.NormalizeCertfp(certfp)
if err == nil {
certfps = append(certfps, normalizedCertfp)
} else {
log.Printf("invalid certfp %s for %s\n", username, certfp)
}
}
credentials := AccountCredentials{
Version: credsType,
PassphraseHash: []byte(userInfo.Hash),
Certfps: certfps,
}
marshaledCredentials, err := json.Marshal(&credentials)
if err != nil {
log.Printf("invalid credentials for %s: %v\n", username, err)
continue
}
tx.Set(fmt.Sprintf(keyAccountExists, cfUsername), "1", nil)
tx.Set(fmt.Sprintf(keyAccountVerified, cfUsername), "1", nil)
tx.Set(fmt.Sprintf(keyAccountName, cfUsername), userInfo.Name, nil)
settings := AccountSettings{Email: userInfo.Email}
settingsBytes, _ := json.Marshal(settings)
tx.Set(fmt.Sprintf(keyAccountSettings, cfUsername), string(settingsBytes), nil)
tx.Set(fmt.Sprintf(keyAccountCredentials, cfUsername), string(marshaledCredentials), nil)
tx.Set(fmt.Sprintf(keyAccountRegTime, cfUsername), strconv.FormatInt(userInfo.RegisteredAt, 10), nil)
if userInfo.Vhost != "" {
vhinfo := VHostInfo{
Enabled: true,
ApprovedVHost: userInfo.Vhost,
}
vhBytes, err := json.Marshal(vhinfo)
if err == nil {
tx.Set(fmt.Sprintf(keyAccountVHost, cfUsername), string(vhBytes), nil)
} else {
log.Printf("couldn't serialize vhost for %s: %v\n", username, err)
}
}
if len(userInfo.AdditionalNicks) != 0 {
tx.Set(fmt.Sprintf(keyAccountAdditionalNicks, cfUsername), marshalReservedNicks(userInfo.AdditionalNicks), nil)
}
for _, certfp := range certfps {
tx.Set(fmt.Sprintf(keyCertToAccount, certfp), cfUsername, nil)
}
cfUsernames.Add(cfUsername)
}
// TODO fix this:
for chname, chInfo := range dbImport.Channels {
_, err := CasefoldChannel(chname)
if err != nil {
log.Printf("invalid channel name %s: %v", chname, err)
continue
}
cffounder, err := CasefoldName(chInfo.Founder)
if err != nil {
log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
continue
}
var regInfo RegisteredChannel
regInfo.Name = chname
regInfo.UUID = utils.GenerateUUIDv4()
regInfo.Founder = cffounder
regInfo.RegisteredAt = time.Unix(0, chInfo.RegisteredAt).UTC()
if chInfo.Topic != "" {
regInfo.Topic = chInfo.Topic
regInfo.TopicSetBy = chInfo.TopicSetBy
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 {
log.Printf("couldn't process amodes for %s: %v", chname, err)
}
}
for _, mode := range chInfo.Modes {
regInfo.Modes = append(regInfo.Modes, modes.Mode(mode))
}
regInfo.Key = chInfo.Key
if chInfo.Limit > 0 {
regInfo.UserLimit = chInfo.Limit
}
if chInfo.Forward != "" {
if _, err := CasefoldChannel(chInfo.Forward); err == nil {
regInfo.Forward = chInfo.Forward
}
}
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 {
log.Printf("NOTE: you may be able to avoid confusability issues by changing the server casemapping setting to `ascii`\n")
log.Printf("However, this will prevent the use of non-ASCII Unicode characters in nicknames\n")
}
return nil
}
func doImportDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err error) {
switch dbImport.Source {
case "atheme":
return doImportDBGeneric(config, dbImport, CredentialsAtheme, tx)
case "anope":
return doImportDBGeneric(config, dbImport, CredentialsAnope, tx)
default:
return fmt.Errorf("unsupported import source: %s", dbImport.Source)
}
}
func ImportDB(config *Config, infile string) (err error) {
data, err := os.ReadFile(infile)
if err != nil {
return
}
var dbImport databaseImport
err = json.Unmarshal(data, &dbImport)
if err != nil {
return err
}
err = checkDBReadyForInit(config.Datastore.Path)
if err != nil {
return err
}
db, err := buntdb.Open(config.Datastore.Path)
if err != nil {
return err
}
performImport := func(tx *buntdb.Tx) (err error) {
return doImportDB(config, dbImport, tx)
}
err = db.Update(performImport)
db.Close()
return
}

Some files were not shown because too many files have changed in this diff Show More