3
0
mirror of https://github.com/ergochat/ergo.git synced 2025-08-06 12:47:26 +02:00

Compare commits

..

No commits in common. "master" and "v0.11.0-beta" have entirely different histories.

1142 changed files with 9930 additions and 447109 deletions

View File

@ -1,14 +0,0 @@
#!/bin/bash
# exclude vendor/
SOURCES="./ergo.go ./irc"
if [ "$1" = "--fix" ]; then
exec gofmt -s -w $SOURCES
fi
if [ -n "$(gofmt -s -l $SOURCES)" ]; then
echo "Go code is not formatted correctly with \`gofmt -s\`:"
gofmt -s -d $SOURCES
exit 1
fi

2
.gitattributes vendored
View File

@ -1,2 +0,0 @@
vendor/* linguist-vendored
languages/* linguist-vendored

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

10
.gitignore vendored
View File

@ -95,7 +95,7 @@ _testmain.go
*.out *.out
### custom ### ### Oragono ###
/_site/ /_site/
/.vscode/* /.vscode/*
/ircd* /ircd*
@ -103,11 +103,9 @@ _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
.dccache

6
.gitmodules vendored
View File

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

View File

@ -1,82 +1,49 @@
# .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:
- 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
goarch: riscv64
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm
- goos: freebsd - goos: freebsd
goarch: arm64 goarch: arm64
- goos: freebsd archive:
goarch: riscv64 name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
- goos: openbsd format: tar.gz
goarch: arm replacements:
- goos: openbsd amd64: x64
goarch: arm64 darwin: osx
- goos: openbsd format_overrides:
goarch: riscv64 - goos: windows
- goos: plan9 format: zip
goarch: arm files:
- goos: plan9 - README
goarch: arm64 - CHANGELOG.md
- goos: plan9 - oragono.motd
goarch: riscv64 - oragono.yaml
flags: - docs/*
- -trimpath - languages/*.yaml
- languages/*.json
archives: - languages/*.md
-
name_template: >-
{{ .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_overrides:
- goos: windows
format: zip
files:
- README
- CHANGELOG.md
- LICENSE
- ergo.motd
- default.yaml
- traditional.yaml
- docs/API.md
- docs/MANUAL.md
- docs/USERGUIDE.md
- languages/*.yaml
- languages/*.json
- languages/*.md
wrap_in_directory: true
checksum: checksum:
name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt" name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt"
git:
short_hash: true

9
.travis.yml Normal file
View File

@ -0,0 +1,9 @@
language: go
install: make deps
script:
- wget https://github.com/goreleaser/goreleaser/releases/download/v0.62.2/goreleaser_Linux_x86_64.tar.gz
- tar -xzf goreleaser_Linux_x86_64.tar.gz -C $GOPATH/bin
- make
- make test

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +1,34 @@
# 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. Most development happens on the `develop` branch, which is occasionally rebased + merged into `master` when it's not incredibly broken. When this happens, the `develop` branch is usually pruned until I feel like making 'unsafe' changes again.
I may also name the branch `develop+feature` if I'm developing multiple, or particularly unstable, features.
## Golang issues The intent is to keep `master` relatively stable.
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.
If you're upgrading the Go version used by Ergo, 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)
2. `Dockerfile`, which controls the version that the Ergo binaries in our Docker images are built with
3. `go.mod`: this should be updated automatically by Go when you do module-related operations
## 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.
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.
## 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. Ensure dependencies are up-to-date.
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). 2. Run [`irctest`]() over it to make sure nothing's severely broken.
1. Run the `ircstress` chanflood benchmark to look for data races (enable race detection) and performance regressions (disable it). 3. Remove `-unreleased` from the version number in `irc/constants.go`.
1. Update the changelog with new changes and write release notes. 4. Update the changelog with new changes.
1. Update the version number `irc/version.go` (either change `-unreleased` to `-rc1`, or remove `-rc1`, as appropriate). 5. Remove unused sections from the changelog, change the date/version number and write release notes.
1. Commit the new changelog and constants change. 6. 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). 7. Tag the release with `git tag v0.0.0 -m "Release v0.0.0"` (`0.0.0` replaced with the real ver number).
1. Build binaries using `make release` 8. Build binaries using the Makefile, upload release to Github including the changelog and binaries.
1. Sign the checksums file with `gpg --sign --detach-sig --local-user <fingerprint>`
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:
* For a release candidate:
1. the channel topic
1. any operators who may be interested
1. update the testnet
* For a production release:
1. everything applicable to a release candidate
1. Twitter
1. ergo.chat/news
1. ircv3.net support tables, if applicable
1. other social media?
Once it's built and released, you need to setup the new development version. To do so: Once it's built and released, you need to setup the new development version. To do so:
1. Ensure dependencies are up-to-date. 1. In `irc/constants.go`, update the version number to `0.0.1-unreleased`, where `0.0.1` is the previous release number with the minor field incremented by one (for instance, `0.9.2` -> `0.9.3-unreleased`).
1. Bump the version number in `irc/version.go`, typically by incrementing the second number in the 3-tuple, and add '-unreleased' (for instance, `2.2.0` -> `2.3.0-unreleased`). 2. At the top of the changelog, paste a new section with the content below.
1. Commit the new version number and changelog with the message `"Setup v0.0.1-unreleased devel ver"`. 3. Commit the new version number and changelog with the message `"Setup v0.0.1-unreleased devel ver"`.
**Unreleased changelog content** **Unreleased changelog content**
```md ```md
## Unreleased ## Unreleased
New release of Ergo! New release of Oragono!
### Config Changes ### Config Changes
@ -101,28 +45,45 @@ New release of Ergo!
## Debugging ## Updating `vendor/`
It's helpful to enable all loglines while developing. Here's how to configure this: The `vendor/` directory holds our dependencies. When we import new repos, we need to update this folder to contain these new deps. This is something that I'll mostly be handling.
```yaml To update this folder:
logging:
-
method: stderr
type: "*"
level: debug
```
To debug a hang, the best thing to do is to get a stack trace. The easiest way to get stack traces is with the [pprof listener](https://golang.org/pkg/net/http/pprof/), which can be enabled in the `debug` section of the config. Once it's enabled, you can navigate to `http://localhost:6060/debug/pprof/` in your browser and go from there. If that doesn't work, try: 1. Install https://github.com/golang/dep
2. `cd` to Oragono folder
3. `dep ensure -update`
4. `cd vendor`
5. Commit the changes with the message `"Updated packages"`
6. `cd ..`
4. Commit the result with the message `"vendor: Updated submodules"`
This will make sure things stay nice and up-to-date for users.
## 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 Hangs
To debug a hang, the best thing to do is to get a stack trace. Go's nice, and you can do so by running this:
$ 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`.
@ -139,7 +100,6 @@ There are some mutexes that are "tier 0": anything in a subpackage of `irc` (e.g
We are using `buntdb` for persistence; a `buntdb.DB` has an `RWMutex` inside it, with read-write transactions getting the `Lock()` and read-only transactions getting the `RLock()`. This mutex is considered tier 1. However, it's shared globally across all consumers, so if possible you should avoid acquiring it while holding ordinary application-level mutexes. We are using `buntdb` for persistence; a `buntdb.DB` has an `RWMutex` inside it, with read-write transactions getting the `Lock()` and read-only transactions getting the `RLock()`. This mutex is considered tier 1. However, it's shared globally across all consumers, so if possible you should avoid acquiring it while holding ordinary application-level mutexes.
## Command handlers and ResponseBuffer ## Command handlers and ResponseBuffer
We support a lot of IRCv3 specs. Pretty much all of them, in fact. And a lot of proposed/draft ones. One of the draft specifications that we support is called ["labeled responses"](https://ircv3.net/specs/extensions/labeled-response.html). We support a lot of IRCv3 specs. Pretty much all of them, in fact. And a lot of proposed/draft ones. One of the draft specifications that we support is called ["labeled responses"](https://ircv3.net/specs/extensions/labeled-response.html).
@ -159,55 +119,3 @@ They receive the response with the same label, so they can match the sent comman
In order to allow this, in command handlers we don't send responses directly back to the user. Instead, we buffer the responses in an object called a ResponseBuffer. When the command handler returns, the contents of the ResponseBuffer is sent to the user with the appropriate label (and batches, if they're required). In order to allow this, in command handlers we don't send responses directly back to the user. Instead, we buffer the responses in an object called a ResponseBuffer. When the command handler returns, the contents of the ResponseBuffer is sent to the user with the appropriate label (and batches, if they're required).
Basically, if you're in a command handler and you're sending a response back to the requesting client, use `rb.Add*` instead of `client.Send*`. Doing this makes sure the labeled responses feature above works as expected. The handling around `PRIVMSG`/`NOTICE`/`TAGMSG` is strange, so simply defer to [irctest](https://github.com/DanielOaks/irctest)'s judgement about whether that's correct for the most part. Basically, if you're in a command handler and you're sending a response back to the requesting client, use `rb.Add*` instead of `client.Send*`. Doing this makes sure the labeled responses feature above works as expected. The handling around `PRIVMSG`/`NOTICE`/`TAGMSG` is strange, so simply defer to [irctest](https://github.com/DanielOaks/irctest)'s judgement about whether that's correct for the most part.
## Translated strings
The function `client.t()` is used fairly widely throughout the codebase. This function translates the given string using the client's negotiated language. If the parameter of the function is a string, the translation update script below will grab that string and mark it for translation.
In addition, throughout most of the codebase, if a string is created using the backtick characters ``(`)``, that string will also be marked for translation. This is really useful in the cases of general errors and other strings that are created far away from the final `client.t` function they are sent through.
## Updating Translations
We support translating server strings using [CrowdIn](https://crowdin.com/project/ergochat)! To send updated source strings to CrowdIn, you should:
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`.
3. Run the `updatetranslations.py` script with: `./updatetranslations.py run irc languages`
4. Commit the changes
CrowdIn's integration should grab the new translation files automagically.
When new translations are available, CrowsIn will submit a new PR with the updates. The `INFO` command should be used to see whether the credits strings has been updated/translated properly, since that can be a bit of a sticking point for our wonderful translators :)
### Updating Translations Manually
You shouldn't need to do this, but to update 'em manually:
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`.
3. Run the `updatetranslations.py` script with: `./updatetranslations.py run irc languages`
4. Install the [CrowdIn CLI tool](https://support.crowdin.com/cli-tool/).
5. Make sure the CrowdIn API key is correct in `~/.crowdin.yaml`
6. Run `crowdin upload sources`
We also support grabbing translations directly from CrowdIn. To do this:
1. `cd` to the base directory (the one this `DEVELOPING` file is in).
2. Install the [CrowdIn CLI tool](https://support.crowdin.com/cli-tool/).
3. Make sure the CrowdIn API key is correct in `~/.crowdin.yaml`
4. Run `crowdin download`
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,48 +0,0 @@
## build ergo binary
FROM docker.io/golang:1.24-alpine AS build-env
RUN apk upgrade -U --force-refresh --no-cache && apk add --no-cache --purge --clean-protected -l -u make git
# copy ergo source
WORKDIR /go/src/github.com/ergochat/ergo
COPY . .
# modify default config file so that it doesn't die on IPv6
# 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 && \
sed -i 's/^\s*\"\[::1\]:6667\":.*$//' /go/src/github.com/ergochat/ergo/default.yaml
# compile
RUN make install
## build ergo container
FROM docker.io/alpine:3.19
# metadata
LABEL maintainer="Daniel Oaks <daniel@danieloaks.net>,Daniel Thamdrup <dallemon@protonmail.com>" \
description="Ergo is a modern, experimental IRC server written in Go"
# standard ports listened on
EXPOSE 6667/tcp 6697/tcp
# ergo itself
COPY --from=build-env /go/bin/ergo \
/go/src/github.com/ergochat/ergo/default.yaml \
/go/src/github.com/ergochat/ergo/distrib/docker/run.sh \
/ircd-bin/
COPY --from=build-env /go/src/github.com/ergochat/ergo/languages /ircd-bin/languages/
# running volume holding config file, db, certs
VOLUME /ircd
WORKDIR /ircd
# default motd
COPY --from=build-env /go/src/github.com/ergochat/ergo/ergo.motd /ircd/ergo.motd
# launch
ENTRYPOINT ["/ircd-bin/run.sh"]
# # uncomment to debug
# RUN apk add --no-cache bash
# RUN apk add --no-cache vim
# CMD /bin/bash

120
Gopkg.lock generated Normal file
View File

@ -0,0 +1,120 @@
memo = "d665cbc0144a6e4c06ed8fcfbee8594a7203e47e654eeca63e11d651ae62a3a7"
[[projects]]
name = "code.cloudfoundry.org/bytefmt"
packages = ["."]
revision = "cbe033486cf0620d3bb77d8ef7f22ab346ad3628"
[[projects]]
branch = "master"
name = "github.com/docopt/docopt-go"
packages = ["."]
revision = "ee0de3bc6815ee19d4a46c7eb90f829db0e014b1"
[[projects]]
branch = "master"
name = "github.com/goshuirc/e-nfa"
packages = ["."]
revision = "7071788e394065e6456458a5e9cb503cad545154"
[[projects]]
branch = "master"
name = "github.com/goshuirc/irc-go"
packages = ["ircfmt","ircmatch","ircmsg"]
revision = "1cb16094f055aa5f5b49a1609aeada54d963433c"
[[projects]]
branch = "master"
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "7dc3415be66d7cc68bf0182f35c8d31f8d2ad8a7"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
branch = "master"
name = "github.com/mgutz/ansi"
packages = ["."]
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
[[projects]]
branch = "master"
name = "github.com/oragono/go-ident"
packages = ["."]
revision = "337fed0fd21ad538725cfcb55053ea4cf8056abc"
[[projects]]
branch = "master"
name = "github.com/stackimpact/stackimpact-go"
packages = [".","internal","internal/pprof/profile"]
revision = "8b5b02c181e477dafcf505342d8a79b5c8241da7"
[[projects]]
branch = "master"
name = "github.com/tidwall/btree"
packages = ["."]
revision = "9876f1454cf0993a53d74c27196993e345f50dd1"
[[projects]]
name = "github.com/tidwall/buntdb"
packages = ["."]
revision = "2da7c106683f522198cdf55ed8db42b374de50d7"
version = "v1.0.0"
[[projects]]
name = "github.com/tidwall/gjson"
packages = ["."]
revision = "87033efcaec6215741137e8ca61952c53ef2685d"
version = "v1.0.6"
[[projects]]
branch = "master"
name = "github.com/tidwall/grect"
packages = ["."]
revision = "ba9a043346eba55344e40d66a5e74cfda3a9d293"
[[projects]]
branch = "master"
name = "github.com/tidwall/match"
packages = ["."]
revision = "1731857f09b1f38450e2c12409748407822dc6be"
[[projects]]
branch = "master"
name = "github.com/tidwall/rtree"
packages = [".","base"]
revision = "6cd427091e0e662cb4f8e2c9eb1a41e1c46ff0d3"
[[projects]]
branch = "master"
name = "github.com/tidwall/tinyqueue"
packages = ["."]
revision = "1feaf062ef04a231c9126f99a68eaa579fd0e390"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["bcrypt","blowfish","ssh/terminal"]
revision = "5119cf507ed5294cc409c092980c7497ee5d6fd2"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix","windows"]
revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd"
[[projects]]
branch = "master"
name = "golang.org/x/text"
packages = ["cases","collate","collate/build","internal","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","runes","secure/bidirule","secure/precis","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable","width"]
revision = "4e4a3210bb54bb31f6ab2cdca2edcc0b50c420c1"
[[projects]]
branch = "v2"
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4"

104
Gopkg.toml Normal file
View File

@ -0,0 +1,104 @@
## Gopkg.toml example (these lines may be deleted)
## "required" lists a set of packages (not projects) that must be included in
## Gopkg.lock. This list is merged with the set of packages imported by the current
## project. Use it when your project needs a package it doesn't explicitly import -
## including "main" packages.
# required = ["github.com/user/thing/cmd/thing"]
## "ignored" lists a set of packages (not projects) that are ignored when
## dep statically analyzes source code. Ignored packages can be in this project,
## or in a dependency.
# ignored = ["github.com/user/project/badpkg"]
## Dependencies define constraints on dependent projects. They are respected by
## dep whether coming from the Gopkg.toml of the current project or a dependency.
# [[dependencies]]
## Required: the root import path of the project being constrained.
# name = "github.com/user/project"
#
## Recommended: the version constraint to enforce for the project.
## Only one of "branch", "version" or "revision" can be specified.
# version = "1.0.0"
# branch = "master"
# revision = "abc123"
#
## Optional: an alternate location (URL or import path) for the project's source.
# source = "https://github.com/myfork/package.git"
## Overrides have the same structure as [[dependencies]], but supercede all
## [[dependencies]] declarations from all projects. Only the current project's
## [[overrides]] are applied.
##
## Overrides are a sledgehammer. Use them only as a last resort.
# [[overrides]]
## Required: the root import path of the project being constrained.
# name = "github.com/user/project"
#
## Optional: specifying a version constraint override will cause all other
## constraints on this project to be ignored; only the overriden constraint
## need be satisfied.
## Again, only one of "branch", "version" or "revision" can be specified.
# version = "1.0.0"
# branch = "master"
# revision = "abc123"
#
## Optional: specifying an alternate source location as an override will
## enforce that the alternate location is used for that project, regardless of
## what source location any dependent projects specify.
# source = "https://github.com/myfork/package.git"
[[dependencies]]
name = "code.cloudfoundry.org/bytefmt"
revision = "cbe033486cf0620d3bb77d8ef7f22ab346ad3628"
[[dependencies]]
branch = "master"
name = "github.com/DanielOaks/girc-go"
[[dependencies]]
branch = "master"
name = "github.com/DanielOaks/go-ident"
[[dependencies]]
branch = "master"
name = "github.com/docopt/docopt-go"
[[dependencies]]
branch = "master"
name = "github.com/gorilla/mux"
[[dependencies]]
branch = "master"
name = "github.com/gorilla/websocket"
[[dependencies]]
branch = "master"
name = "github.com/mattn/go-colorable"
[[dependencies]]
branch = "master"
name = "github.com/mgutz/ansi"
[[dependencies]]
branch = "master"
name = "github.com/stackimpact/stackimpact-go"
[[dependencies]]
branch = "master"
name = "github.com/tidwall/buntdb"
[[dependencies]]
branch = "master"
name = "golang.org/x/crypto"
[[dependencies]]
branch = "master"
name = "golang.org/x/text"
[[dependencies]]
branch = "v2"
name = "gopkg.in/yaml.v2"

View File

@ -1,9 +1,8 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2012-2014 Jeremy Latt Copyright (c) 2016-2018 Daniel Oaks
Copyright (c) 2014-2015 Edmund Huber Copyright (c) 2014-2015 Edmund Huber
Copyright (c) 2016-2020 Daniel Oaks Copyright (c) 2014 Jeremy Latt
Copyright (c) 2017-2020 Shivaram Lingamneni
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

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

72
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, experimental 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
----------------------------------------------------------------------------------------------- -----------------------------------------------------------------------------------------------
@ -27,36 +23,44 @@ Ergo is a modern IRC server written in Go. Its core design principles are:
Copy the example config file to ircd.yaml with a command like: Copy the example config file to ircd.yaml with a command like:
$ cp default.yaml ircd.yaml $ cp oragono.yaml ircd.yaml
Modify the config file as needed (the recommendations at the top may be helpful). Modify the config file as you like. In particular, the `connection-throttling` and
`connection-limits` sections are working looking over and tuning for your network's needs.
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 initdb
$ 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'll also need to run this command:
$ oragono upgradedb
=== Credits === === Credits ===
* Jeremy Latt (2012-2014) * Jeremy Latt, creator of Ergonomadic, <https://github.com/jlatt>
* Edmund Huber (2014-2015) * Edmund Huber, maintainer of Ergonomadic, <https://github.com/edmund-huber>
* Daniel Oaks (2016-present) * Niels Freier, added WebSocket support to Ergonomadic, <https://github.com/stumpyfr>
* Shivaram Lingamneni (2017-present) * Daniel Oakley, maintainer of Oragono, <https://github.com/DanielOaks>
* Many other contributors and friends of the project <3 * Euan Kemp, contributor to Oragono and lots of useful fixes, <https://github.com/euank>
* Shivaram Lingamneni, has contributed a ton of fixes, refactoring, and general improvements, <https://github.com/slingamn>
* James Mills, contributed Docker support, <https://github.com/prologic>
* Vegax, implementing some commands and helping when Oragono was just getting started, <https://github.com/vegax87>
* Sean Enck, transitioned us from using a custom script to a proper Makefile, <https://github.com/enckse>
* apologies to anyone I forgot.

148
README.md
View File

@ -1,127 +1,145 @@
![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, experimental IRC server written in Go. It's designed to be simple to setup and use, and it includes features such as UTF-8 nicks / channel names, client accounts with SASL, and other assorted IRCv3 support.
* Being simple to set up and use Oragono is a fork of the [Ergonomadic](https://github.com/edmund-huber/ergonomadic) IRC daemon <3
* 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
* High customizability 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
--- ---
[![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.org/oragono/oragono.svg?branch=master)](https://travis-ci.org/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). [darwin.network](https://irc.darwin.network/) and [testnet.oragono.io](ircs://testnet.oragono.io:6697/#chat) are running Oragono in production if you want to take a look.
--- ---
## Features ## Features
* integrated services: NickServ for user accounts, ChanServ for channel registration, and HostServ for vanity hosts * UTF-8 nick and channel names with rfc7613 (PRECIS)
* bouncer-like features: storing and replaying history, allowing multiple clients to use the same nickname * [yaml](http://yaml.org/) configuration
* native TLS/SSL support, including support for client certificates * native TLS/SSL support
* [IRCv3 support](https://ircv3.net/software/servers.html) * server password (`PASS` command)
* [yaml](https://yaml.org/) configuration
* updating server config and TLS certificates on-the-fly (rehashing)
* SASL authentication
* [LDAP support](https://github.com/ergochat/ergo-ldap)
* supports [multiple languages](https://crowdin.com/project/ergochat) (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)
* 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) * on-the-fly updating server config and TLS certificates (rehashing)
* `UBAN`, a unified ban system that can target IPs, networks, masks, and registered accounts (`KLINE` and `DLINE` are also supported) * client accounts and SASL
* a focus on developing with [specifications](https://ergo.chat/specs.html) * passwords stored with [bcrypt](https://godoc.org/golang.org/x/crypto) (client account passwords also salted)
* banning ips/nets and masks with `KLINE` and `DLINE`
* supports [multiple languages](https://crowdin.com/project/oragono) (you can also set a default language for your network)
* [IRCv3 support](http://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: ## Installation
* [MANUAL.md, the operator manual](https://github.com/ergochat/ergo/blob/stable/docs/MANUAL.md) To go through the standard installation, download the latest release from this page: https://github.com/oragono/oragono/releases/latest
* [USERGUIDE.md, the guide for end users](https://github.com/ergochat/ergo/blob/stable/docs/USERGUIDE.md)
## Quick start guide
Download the latest release from this page: https://github.com/ergochat/ergo/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 oragono.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 initdb
./ergo run # server should be ready to go! oragono mkcerts
``` ```
**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:** This installation will give you self-signed certificates suitable for testing purposes.
For real certs, look into [Let's Encrypt](https://letsencrypt.org/)!
### 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
A Dockerfile and example docker-compose recipe are available in the `distrib/docker` directory. Ergo 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
[README file](https://github.com/ergochat/ergo/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.) [![Build Status](https://travis-ci.org/oragono/oragono.svg?branch=master)](https://travis-ci.org/oragono/oragono)
Clone the appropriate branch. If necessary, do `git submodule update --init` to set up vendored dependencies. From the root folder, run `make` to generate all release files for all of our target OSes:
```
make
```
You can also only build the release files for a specific system:
```
# for windows
make windows
# for linux
make linux
# for osx
make osx
# for arm6
make arm6
```
Once you have made the release files, you can find them in the `build` directory. Uncompress these to an empty directory and continue as usual.
## Configuration ## Configuration
The default config file [`default.yaml`](default.yaml) helps walk you through what each option means and changes. The default config file [`oragono.yaml`](oragono.yaml) helps walk you through what each option means and changes. The configuration's intended to be sparse, so if there are options missing it's either because that feature isn't written/configurable yet or because we don't think it should be configurable.
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
By default, logs go to stderr only. They can be configured to go to a file, or you can use systemd to direct the stderr to the system journal (see the manual for details). The configuration format of logs is designed to be easily pluggable, and is inspired by the logging config provided by InspIRCd. By default, logs are stored in the file `ircd.log`. The configuration format of logs is designed to be easily pluggable, and is inspired by the logging config provided by InspIRCd.
### Passwords ### Passwords
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 ## Running
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). After this, running the server is easy! Simply run the below command and you should see the relevant startup information pop up.
Once you have registered your nickname, you can use it to register channels: ```sh
oragono run
```
1. Join the channel with `/join #channel` ### How to register a channel
2. Register the channel with `/CS REGISTER #channel`
1. Register your account with `/NS REGISTER <username> [<password>]`
2. Join the channel with `/join #channel`
3. 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
* Jeremy Latt (2012-2014) * Jeremy Latt, creator of Ergonomadic, <https://github.com/jlatt>
* Edmund Huber (2014-2015) * Edmund Huber, maintainer of Ergonomadic, <https://github.com/edmund-huber>
* Daniel Oaks (2016-present) * Niels Freier, added WebSocket support to Ergonomadic, <https://github.com/stumpyfr>
* Shivaram Lingamneni (2017-present) * Daniel Oakley, maintainer of Oragono, <https://github.com/DanielOaks>
* [Many other contributors and friends of the project <3](https://github.com/ergochat/ergo/blob/master/CHANGELOG.md) * Euan Kemp, contributor to Oragono and lots of useful fixes, <https://github.com/euank>
* Shivaram Lingamneni, has contributed a ton of fixes, refactoring, and general improvements, <https://github.com/slingamn>
* James Mills, contributed Docker support, <https://github.com/prologic>
* Vegax, implementing some commands and helping when Oragono was just getting started, <https://github.com/vegax87>
* Sean Enck, transitioned us from using a custom script to a proper Makefile, <https://github.com/enckse>
* apologies to anyone I forgot.

View File

@ -1,53 +0,0 @@
#
# Your crowdin's credentials
#
"project_identifier" : "oragono"
# "api_key" : ""
# "base_path" : ""
#"base_url" : ""
#
# Choose file structure in crowdin
# e.g. true or false
#
"preserve_hierarchy": true
#
# Files configuration
#
files: [
{
"source" : "/languages/example/translation.lang.yaml",
"translation" : "/languages/%locale%.lang.yaml",
"dest" : "translation.lang.yaml"
},
{
"source" : "/languages/example/irc.lang.json",
"translation" : "/languages/%locale%-irc.lang.json",
"dest" : "irc.lang.json"
},
{
"source" : "/languages/example/help.lang.json",
"translation" : "/languages/%locale%-help.lang.json",
"dest" : "help.lang.json",
"update_option" : "update_as_unapproved",
},
{
"source" : "/languages/example/chanserv.lang.json",
"translation" : "/languages/%locale%-chanserv.lang.json",
"dest" : "services/chanserv.lang.json",
"update_option" : "update_as_unapproved",
},
{
"source" : "/languages/example/nickserv.lang.json",
"translation" : "/languages/%locale%-nickserv.lang.json",
"dest" : "services/nickserv.lang.json",
"update_option" : "update_as_unapproved",
},
{
"source" : "/languages/example/hostserv.lang.json",
"translation" : "/languages/%locale%-hostserv.lang.json",
"dest" : "services/hostserv.lang.json",
"update_option" : "update_as_unapproved",
},
]

File diff suppressed because it is too large Load Diff

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

View File

@ -1,21 +0,0 @@
version: "3.8"
services:
ergo:
init: true
image: ghcr.io/ergochat/ergo:stable
ports:
- "6667:6667/tcp"
- "6697:6697/tcp"
volumes:
- data:/ircd
deploy:
placement:
constraints:
- "node.role == manager"
restart_policy:
condition: on-failure
replicas: 1
volumes:
data:

View File

@ -1,26 +0,0 @@
#!/bin/sh
# make config file
if [ ! -f "/ircd/ircd.yaml" ]; then
awk '{gsub(/path: languages/,"path: /ircd-bin/languages")}1' /ircd-bin/default.yaml > /tmp/ircd.yaml
# change default oper passwd
OPERPASS=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c20)
echo "Oper username:password is admin:$OPERPASS"
ENCRYPTEDPASS=$(echo "$OPERPASS" | /ircd-bin/ergo genpasswd)
ORIGINALPASS='\$2a\$04\$0123456789abcdef0123456789abcdef0123456789abcdef01234'
awk "{gsub(/password: \\\"$ORIGINALPASS\\\"/,\"password: \\\"$ENCRYPTEDPASS\\\"\")}1" /tmp/ircd.yaml > /tmp/ircd2.yaml
unset OPERPASS
unset ENCRYPTEDPASS
unset ORIGINALPASS
mv /tmp/ircd2.yaml /ircd/ircd.yaml
fi
# make self-signed certs if they don't already exist
/ircd-bin/ergo mkcerts
# run!
exec /ircd-bin/ergo 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,23 +0,0 @@
[Unit]
Description=ergo
After=network.target
# If you are using MySQL for history storage, comment out the above line
# and uncomment these two instead (you must independently install and configure
# MySQL for your system):
# Wants=mysql.service
# After=network.target mysql.service
[Service]
Type=notify
User=ergo
WorkingDirectory=/home/ergo
ExecStart=/home/ergo/ergo run --conf /home/ergo/ircd.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
LimitNOFILE=1048576
NotifyAccess=main
# Uncomment this for a hidden service:
# PrivateNetwork=true
[Install]
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

@ -54,7 +54,3 @@ Here are the color names we support, and which IRC colors they map to:
14 | grey 14 | grey
15 | light grey 15 | light grey
-------------------- --------------------
In addition, some newer clients can make use of the colour codes 16-98, though they don't
have any names assigned. Take a look at this table to see which colours these numbers are:
https://modern.ircdocs.horse/formatting.html#colors-16-98

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

208
ergo.go
View File

@ -1,208 +0,0 @@
// Copyright (c) 2012-2014 Jeremy Latt
// Copyright (c) 2014-2015 Edmund Huber
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package main
import (
"bufio"
_ "embed"
"fmt"
"log"
"os"
"strings"
"syscall"
"golang.org/x/crypto/bcrypt"
"golang.org/x/term"
"github.com/docopt/docopt-go"
"github.com/ergochat/ergo/irc"
"github.com/ergochat/ergo/irc/logger"
"github.com/ergochat/ergo/irc/mkcerts"
"github.com/ergochat/ergo/irc/utils"
)
// set via linker flags, either by make or by goreleaser:
var commit = "" // git hash
var version = "" // tagged version
//go:embed default.yaml
var defaultConfig string
// get a password from stdin from the user
func getPasswordFromTerminal() string {
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
log.Fatal("Error reading password:", err.Error())
}
return string(bytePassword)
}
func fileDoesNotExist(file string) bool {
if _, err := os.Stat(file); os.IsNotExist(err) {
return true
}
return false
}
// implements the `ergo mkcerts` command
func doMkcerts(configFile string, quiet bool) {
config, err := irc.LoadRawConfig(configFile)
if err != nil {
log.Fatal(err)
}
if !quiet {
log.Println("making self-signed certificates")
}
certToKey := make(map[string]string)
for name, conf := range config.Server.Listeners {
if conf.TLS.Cert == "" {
continue
}
existingKey, ok := certToKey[conf.TLS.Cert]
if ok {
if existingKey == conf.TLS.Key {
continue
} else {
log.Fatal("Conflicting TLS key files for ", conf.TLS.Cert)
}
}
if !quiet {
log.Printf(" making cert for %s listener\n", name)
}
host := config.Server.Name
cert, key := conf.TLS.Cert, conf.TLS.Key
if !(fileDoesNotExist(cert) && fileDoesNotExist(key)) {
log.Fatalf("Preexisting TLS cert and/or key files: %s %s", cert, key)
}
err := mkcerts.CreateCert("Ergo", host, cert, key)
if err == nil {
if !quiet {
log.Printf(" Certificate created at %s : %s\n", cert, key)
}
certToKey[cert] = key
} else {
log.Fatal(" Could not create certificate:", err.Error())
}
}
}
func main() {
irc.SetVersionString(version, commit)
usage := `ergo.
Usage:
ergo initdb [--conf <filename>] [--quiet]
ergo upgradedb [--conf <filename>] [--quiet]
ergo importdb <database.json> [--conf <filename>] [--quiet]
ergo genpasswd [--conf <filename>] [--quiet]
ergo mkcerts [--conf <filename>] [--quiet]
ergo defaultconfig
ergo gentoken
ergo run [--conf <filename>] [--quiet] [--smoke]
ergo -h | --help
ergo --version
Options:
--conf <filename> Configuration file to use [default: ircd.yaml].
--quiet Don't show startup/shutdown lines.
-h --help Show this screen.
--version Show version.`
arguments, _ := docopt.ParseArgs(usage, nil, irc.Ver)
// don't require a config file for genpasswd
if arguments["genpasswd"].(bool) {
var password string
if term.IsTerminal(int(syscall.Stdin)) {
fmt.Print("Enter Password: ")
password = getPasswordFromTerminal()
fmt.Print("\n")
fmt.Print("Reenter Password: ")
confirm := getPasswordFromTerminal()
fmt.Print("\n")
if confirm != password {
log.Fatal("passwords do not match")
}
} else {
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
password = strings.TrimSpace(text)
}
if err := irc.ValidatePassphrase(password); err != nil {
log.Printf("WARNING: this password contains characters that may cause problems with your IRC client software.\n")
log.Printf("We strongly recommend choosing a different password.\n")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
if err != nil {
log.Fatal("encoding error:", err.Error())
}
fmt.Println(string(hash))
return
} else if arguments["defaultconfig"].(bool) {
fmt.Print(defaultConfig)
return
} else if arguments["gentoken"].(bool) {
fmt.Println(utils.GenerateSecretKey())
return
} else if arguments["mkcerts"].(bool) {
doMkcerts(arguments["--conf"].(string), arguments["--quiet"].(bool))
return
}
configfile := arguments["--conf"].(string)
config, err := irc.LoadConfig(configfile)
if err != nil {
_, isCertError := err.(*irc.CertKeyError)
if !(isCertError && arguments["mkcerts"].(bool)) {
log.Fatal("Config file did not load successfully: ", err.Error())
}
}
logman, err := logger.NewManager(config.Logging)
if err != nil {
log.Fatal("Logger did not load successfully:", err.Error())
}
if arguments["initdb"].(bool) {
err = irc.InitDB(config.Datastore.Path)
if err != nil {
log.Fatal("Error while initializing db:", err.Error())
}
if !arguments["--quiet"].(bool) {
log.Println("database initialized: ", config.Datastore.Path)
}
} else if arguments["upgradedb"].(bool) {
err = irc.UpgradeDB(config)
if err != nil {
log.Fatal("Error while upgrading db:", err.Error())
}
if !arguments["--quiet"].(bool) {
log.Println("database upgraded: ", config.Datastore.Path)
}
} else if arguments["importdb"].(bool) {
err = irc.ImportDB(config, arguments["<database.json>"].(string))
if err != nil {
log.Fatal("Error while importing db:", err.Error())
}
} else if arguments["run"].(bool) {
if !arguments["--quiet"].(bool) {
logman.Info("server", fmt.Sprintf("%s starting", irc.Ver))
}
// warning if running a non-final version
if strings.Contains(irc.Ver, "unreleased") {
logman.Warning("server", "You are currently running an unreleased beta version of Ergo that may be unstable and could corrupt your database.\nIf you are running a production network, please download the latest build from https://ergo.chat/about and run that instead.")
}
server, err := irc.NewServer(config, logman)
if err != nil {
logman.Error("server", fmt.Sprintf("Could not load server: %s", err.Error()))
os.Exit(1)
}
if !arguments["--smoke"].(bool) {
server.Run()
}
}
}

View File

@ -1,310 +0,0 @@
#!/usr/bin/env python3
"""
Updates the capability definitions at irc/caps/defs.go
To add a capability, add it to the CAPDEFS list below,
then run `make capdefs` from the project root.
"""
import io
import subprocess
import sys
from collections import namedtuple
CapDef = namedtuple("CapDef", ['identifier', 'name', 'url', 'standard'])
CAPDEFS = [
CapDef(
identifier="AccountNotify",
name="account-notify",
url="https://ircv3.net/specs/extensions/account-notify-3.1.html",
standard="IRCv3",
),
CapDef(
identifier="AccountTag",
name="account-tag",
url="https://ircv3.net/specs/extensions/account-tag-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="AwayNotify",
name="away-notify",
url="https://ircv3.net/specs/extensions/away-notify-3.1.html",
standard="IRCv3",
),
CapDef(
identifier="Batch",
name="batch",
url="https://ircv3.net/specs/extensions/batch-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="CapNotify",
name="cap-notify",
url="https://ircv3.net/specs/extensions/cap-notify-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="ChgHost",
name="chghost",
url="https://ircv3.net/specs/extensions/chghost-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="EchoMessage",
name="echo-message",
url="https://ircv3.net/specs/extensions/echo-message-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="ExtendedJoin",
name="extended-join",
url="https://ircv3.net/specs/extensions/extended-join-3.1.html",
standard="IRCv3",
),
CapDef(
identifier="ExtendedMonitor",
name="extended-monitor",
url="https://ircv3.net/specs/extensions/extended-monitor.html",
standard="IRCv3",
),
CapDef(
identifier="InviteNotify",
name="invite-notify",
url="https://ircv3.net/specs/extensions/invite-notify-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="LabeledResponse",
name="labeled-response",
url="https://ircv3.net/specs/extensions/labeled-response.html",
standard="IRCv3",
),
CapDef(
identifier="Languages",
name="draft/languages",
url="https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6",
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(
identifier="MessageTags",
name="message-tags",
url="https://ircv3.net/specs/extensions/message-tags.html",
standard="IRCv3",
),
CapDef(
identifier="MultiPrefix",
name="multi-prefix",
url="https://ircv3.net/specs/extensions/multi-prefix-3.1.html",
standard="IRCv3",
),
CapDef(
identifier="Relaymsg",
name="draft/relaymsg",
url="https://github.com/ircv3/ircv3-specifications/pull/417",
standard="proposed IRCv3",
),
CapDef(
identifier="ChannelRename",
name="draft/channel-rename",
url="https://ircv3.net/specs/extensions/channel-rename",
standard="draft IRCv3",
),
CapDef(
identifier="SASL",
name="sasl",
url="https://ircv3.net/specs/extensions/sasl-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="ServerTime",
name="server-time",
url="https://ircv3.net/specs/extensions/server-time-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="SetName",
name="setname",
url="https://ircv3.net/specs/extensions/setname.html",
standard="IRCv3",
),
CapDef(
identifier="STS",
name="sts",
url="https://ircv3.net/specs/extensions/sts.html",
standard="IRCv3",
),
CapDef(
identifier="UserhostInNames",
name="userhost-in-names",
url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html",
standard="IRCv3",
),
CapDef(
identifier="ZNCSelfMessage",
name="znc.in/self-message",
url="https://wiki.znc.in/Query_buffers",
standard="ZNC vendor",
),
CapDef(
identifier="EventPlayback",
name="draft/event-playback",
url="https://github.com/ircv3/ircv3-specifications/pull/362",
standard="proposed IRCv3",
),
CapDef(
identifier="ZNCPlayback",
name="znc.in/playback",
url="https://wiki.znc.in/Playback",
standard="ZNC vendor",
),
CapDef(
identifier="Nope",
name="ergo.chat/nope",
url="https://ergo.chat/nope",
standard="Ergo vendor",
),
CapDef(
identifier="Multiline",
name="draft/multiline",
url="https://github.com/ircv3/ircv3-specifications/pull/398",
standard="proposed IRCv3",
),
CapDef(
identifier="Chathistory",
name="draft/chathistory",
url="https://github.com/ircv3/ircv3-specifications/pull/393",
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():
CAPDEFS.sort(key=lambda d: d.name)
numCaps = len(CAPDEFS)
numNames = len(set(capdef.name for capdef in CAPDEFS))
if numCaps != numNames:
raise Exception("defs must have unique names, but found duplicates")
numIdentifiers = len(set(capdef.identifier for capdef in CAPDEFS))
if numCaps != numIdentifiers:
raise Exception("defs must have unique identifiers, but found duplicates")
def main():
validate_defs()
output = io.StringIO()
print("""
package caps
/*
WARNING: this file is autogenerated by `make capdefs`
DO NOT EDIT MANUALLY.
*/
""", file=output)
numCapabs = len(CAPDEFS)
bitsetLen = numCapabs // 32
if numCapabs % 32 > 0:
bitsetLen += 1
print ("""
const (
// number of recognized capabilities:
numCapabs = %d
// length of the uint32 array that represents the bitset:
bitsetLen = %d
)
""" % (numCapabs, bitsetLen), file=output)
print("const (", file=output)
for capdef in CAPDEFS:
print("// %s is the %s capability named \"%s\":" % (capdef.identifier, capdef.standard, capdef.name), file=output)
print("// %s" % (capdef.url,), file=output)
print("%s Capability = iota" % (capdef.identifier,), file=output)
print(file=output)
print(")", file=output)
print("// `capabilityNames[capab]` is the string name of the capability `capab`", file=output)
print("""var ( capabilityNames = [numCapabs]string{""", file=output)
for capdef in CAPDEFS:
print("\"%s\"," % (capdef.name,), file=output)
print("})", file=output)
# run the generated code through `gofmt -s`, which will print it to stdout
gofmt = subprocess.Popen(['gofmt', '-s'], stdin=subprocess.PIPE)
gofmt.communicate(input=output.getvalue().encode('utf-8'))
if gofmt.poll() != 0:
print(output.getvalue())
raise Exception("gofmt failed")
return 0
if __name__ == '__main__':
sys.exit(main())

47
go.mod
View File

@ -1,47 +0,0 @@
module github.com/ergochat/ergo
go 1.24
require (
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
github.com/ergochat/irc-go v0.5.0-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/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
github.com/onsi/ginkgo v1.12.0 // indirect
github.com/onsi/gomega v1.9.0 // indirect
github.com/stretchr/testify v1.4.0 // indirect
github.com/tidwall/buntdb v1.3.2
github.com/xdg-go/scram v1.0.2
golang.org/x/crypto v0.38.0
golang.org/x/term v0.32.0
golang.org/x/text v0.25.0
gopkg.in/yaml.v2 v2.4.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

109
go.sum
View File

@ -1,109 +0,0 @@
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=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
github.com/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/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/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A=
github.com/emersion/go-msgauth v0.6.8/go.mod h1:YDwuyTCUHu9xxmAeVj0eW4INnwB6NNZoPdLerpSxRrc=
github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc=
github.com/emersion/go-msgauth v0.7.0/go.mod h1:mmS9I6HkSovrNgq0HNXTeu8l3sRAAuQ9RMvbM4KU7Ck=
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons=
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1/go.mod h1:mov+uh1DPWsltdQnOdzn08UO9GsJ3MEvhtu0Ci37fdk=
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5AnBVGzTXM/yHVytt48rXBGcJGzSbms=
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/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
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.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
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.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
github.com/tidwall/btree v1.4.2 h1:PpkaieETJMUxYNADsjgtNRcERX7mGc/GP2zp/r5FM3g=
github.com/tidwall/btree v1.4.2/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE=
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/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.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
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-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

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

@ -1,115 +0,0 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package irc
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net"
"github.com/ergochat/ergo/irc/oauth2"
"github.com/ergochat/ergo/irc/utils"
)
// JSON-serializable input and output types for the script
type AuthScriptInput struct {
AccountName string `json:"accountName,omitempty"`
Passphrase string `json:"passphrase,omitempty"`
Certfp string `json:"certfp,omitempty"`
PeerCerts []string `json:"peerCerts,omitempty"`
peerCerts []*x509.Certificate
IP string `json:"ip,omitempty"`
OAuthBearer *oauth2.OAuthBearerOptions `json:"oauth2,omitempty"`
}
type AuthScriptOutput struct {
AccountName string `json:"accountName"`
Success bool `json:"success"`
Error string `json:"error"`
}
func CheckAuthScript(sem utils.Semaphore, config ScriptConfig, input AuthScriptInput) (output AuthScriptOutput, err error) {
if sem != nil {
sem.Acquire()
defer sem.Release()
}
// 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}))
}
}
inputBytes, err := json.Marshal(input)
if err != nil {
return
}
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 != "" {
err = fmt.Errorf("Authentication process reported error: %s", output.Error)
}
return
}
type IPScriptResult uint
const (
IPNotChecked IPScriptResult = 0
IPAccepted IPScriptResult = 1
IPBanned IPScriptResult = 2
IPRequireSASL IPScriptResult = 3
)
type IPScriptInput struct {
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()
}
inputBytes, err := json.Marshal(IPScriptInput{IP: addr.String()})
if err != nil {
return
}
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 != "" {
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
}

78
irc/batch.go Normal file
View File

@ -0,0 +1,78 @@
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
"strconv"
"time"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/oragono/oragono/irc/caps"
)
const (
// maxBatchID is the maximum ID the batch counter can get to before it rotates.
//
// Batch IDs are made up of the current unix timestamp plus a rolling int ID that's
// incremented for every new batch. It's an alright solution and will work unless we get
// more than maxId batches per nanosecond. Later on when we have S2S linking, the batch
// ID will also contain the server ID to ensure they stay unique.
maxBatchID uint64 = 60000
)
// BatchManager helps generate new batches and new batch IDs.
type BatchManager struct {
idCounter uint64
}
// NewBatchManager returns a new Manager.
func NewBatchManager() *BatchManager {
return &BatchManager{}
}
// NewID returns a new batch ID that should be unique.
func (bm *BatchManager) NewID() string {
bm.idCounter++
if maxBatchID < bm.idCounter {
bm.idCounter = 0
}
return strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatUint(bm.idCounter, 36)
}
// Batch represents an IRCv3 batch.
type Batch struct {
ID string
Type string
Params []string
}
// New returns a new batch.
func (bm *BatchManager) New(batchType string, params ...string) *Batch {
newBatch := Batch{
ID: bm.NewID(),
Type: batchType,
Params: params,
}
return &newBatch
}
// Start sends the batch start message to this client
func (b *Batch) Start(client *Client, tags *map[string]ircmsg.TagValue) {
if client.capabilities.Has(caps.Batch) {
params := []string{"+" + b.ID, b.Type}
for _, param := range b.Params {
params = append(params, param)
}
client.Send(tags, client.server.name, "BATCH", params...)
}
}
// End sends the batch end message to this client
func (b *Batch) End(client *Client) {
if client.capabilities.Has(caps.Batch) {
client.Send(nil, client.server.name, "BATCH", "-"+b.ID)
}
}

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

@ -3,30 +3,58 @@
package caps package caps
import "errors"
// Capability represents an optional feature that a client may request from the server. // Capability represents an optional feature that a client may request from the server.
type Capability uint type Capability string
// actual capability definitions appear in defs.go const (
// LabelTagName is the tag name used for the labeled-response spec.
LabelTagName = "draft/label"
var ( // AccountNotify is this IRCv3 capability: http://ircv3.net/specs/extensions/account-notify-3.1.html
nameToCapability map[string]Capability AccountNotify Capability = "account-notify"
// AccountTag is this IRCv3 capability: http://ircv3.net/specs/extensions/account-tag-3.2.html
NoSuchCap = errors.New("Unsupported capability name") AccountTag Capability = "account-tag"
// AwayNotify is this IRCv3 capability: http://ircv3.net/specs/extensions/away-notify-3.1.html
AwayNotify Capability = "away-notify"
// Batch is this IRCv3 capability: http://ircv3.net/specs/extensions/batch-3.2.html
Batch Capability = "batch"
// CapNotify is this IRCv3 capability: http://ircv3.net/specs/extensions/cap-notify-3.2.html
CapNotify Capability = "cap-notify"
// ChgHost is this IRCv3 capability: http://ircv3.net/specs/extensions/chghost-3.2.html
ChgHost Capability = "chghost"
// EchoMessage is this IRCv3 capability: http://ircv3.net/specs/extensions/echo-message-3.2.html
EchoMessage Capability = "echo-message"
// ExtendedJoin is this IRCv3 capability: http://ircv3.net/specs/extensions/extended-join-3.1.html
ExtendedJoin Capability = "extended-join"
// InviteNotify is this IRCv3 capability: http://ircv3.net/specs/extensions/invite-notify-3.2.html
InviteNotify Capability = "invite-notify"
// LabeledResponse is this draft IRCv3 capability: http://ircv3.net/specs/extensions/labeled-response.html
LabeledResponse Capability = "draft/labeled-response"
// Languages is this proposed IRCv3 capability: https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
Languages Capability = "draft/languages"
// MaxLine is this capability: https://oragono.io/maxline
MaxLine Capability = "oragono.io/maxline"
// MessageTags is this draft IRCv3 capability: http://ircv3.net/specs/core/message-tags-3.3.html
MessageTags Capability = "draft/message-tags-0.2"
// MultiPrefix is this IRCv3 capability: http://ircv3.net/specs/extensions/multi-prefix-3.1.html
MultiPrefix Capability = "multi-prefix"
// Rename is this proposed capability: https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
Rename Capability = "draft/rename"
// Resume is this proposed capability: https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
Resume Capability = "draft/resume"
// SASL is this IRCv3 capability: http://ircv3.net/specs/extensions/sasl-3.2.html
SASL Capability = "sasl"
// ServerTime is this IRCv3 capability: http://ircv3.net/specs/extensions/server-time-3.2.html
ServerTime Capability = "server-time"
// STS is this IRCv3 capability: http://ircv3.net/specs/extensions/sts.html
STS Capability = "sts"
// UserhostInNames is this IRCv3 capability: http://ircv3.net/specs/extensions/userhost-in-names-3.2.html
UserhostInNames Capability = "userhost-in-names"
) )
// Name returns the name of the given capability. // Name returns the name of the given capability.
func (capability Capability) Name() string { func (capability Capability) Name() string {
return capabilityNames[capability] return string(capability)
}
func NameToCapability(name string) (result Capability, err error) {
result, found := nameToCapability[name]
if !found {
err = NoSuchCap
}
return
} }
// Version is used to select which max version of CAP the client supports. // Version is used to select which max version of CAP the client supports.
@ -50,26 +78,3 @@ const (
// NegotiatedState means CAP negotiation has been successfully ended and reg should complete. // NegotiatedState means CAP negotiation has been successfully ended and reg should complete.
NegotiatedState State = iota NegotiatedState State = iota
) )
const (
// LabelTagName is the tag name used for the labeled-response spec.
// https://ircv3.net/specs/extensions/labeled-response.html
LabelTagName = "label"
// More draft names associated with draft/multiline:
MultilineBatchType = "draft/multiline"
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() {
nameToCapability = make(map[string]Capability, numCapabs)
for capab, name := range capabilityNames {
nameToCapability[name] = Capability(capab)
}
}

View File

@ -1,211 +0,0 @@
package caps
/*
WARNING: this file is autogenerated by `make capdefs`
DO NOT EDIT MANUALLY.
*/
const (
// number of recognized capabilities:
numCapabs = 38
// length of the uint32 array that represents the bitset:
bitsetLen = 2
)
const (
// AccountNotify is the IRCv3 capability named "account-notify":
// https://ircv3.net/specs/extensions/account-notify-3.1.html
AccountNotify Capability = iota
// AccountTag is the IRCv3 capability named "account-tag":
// https://ircv3.net/specs/extensions/account-tag-3.2.html
AccountTag Capability = iota
// AwayNotify is the IRCv3 capability named "away-notify":
// https://ircv3.net/specs/extensions/away-notify-3.1.html
AwayNotify Capability = iota
// Batch is the IRCv3 capability named "batch":
// https://ircv3.net/specs/extensions/batch-3.2.html
Batch Capability = iota
// CapNotify is the IRCv3 capability named "cap-notify":
// https://ircv3.net/specs/extensions/cap-notify-3.2.html
CapNotify Capability = iota
// ChgHost is the IRCv3 capability named "chghost":
// https://ircv3.net/specs/extensions/chghost-3.2.html
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":
// https://github.com/ircv3/ircv3-specifications/pull/393
Chathistory Capability = iota
// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
// https://github.com/ircv3/ircv3-specifications/pull/362
EventPlayback Capability = iota
// ExtendedISupport is the proposed IRCv3 capability named "draft/extended-isupport":
// https://github.com/ircv3/ircv3-specifications/pull/543
ExtendedISupport Capability = iota
// Languages is the proposed IRCv3 capability named "draft/languages":
// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
Languages Capability = iota
// 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":
// https://github.com/ircv3/ircv3-specifications/pull/398
Multiline Capability = iota
// NoImplicitNames is the proposed IRCv3 capability named "draft/no-implicit-names":
// https://github.com/ircv3/ircv3-specifications/pull/527
NoImplicitNames Capability = iota
// Persistence is the proposed IRCv3 capability named "draft/persistence":
// https://github.com/ircv3/ircv3-specifications/pull/503
Persistence Capability = iota
// Preaway is the proposed IRCv3 capability named "draft/pre-away":
// https://github.com/ircv3/ircv3-specifications/pull/514
Preaway Capability = iota
// ReadMarker is the draft IRCv3 capability named "draft/read-marker":
// 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":
// https://ircv3.net/specs/extensions/echo-message-3.2.html
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":
// https://ircv3.net/specs/extensions/extended-join-3.1.html
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":
// https://ircv3.net/specs/extensions/invite-notify-3.2.html
InviteNotify Capability = iota
// LabeledResponse is the IRCv3 capability named "labeled-response":
// https://ircv3.net/specs/extensions/labeled-response.html
LabeledResponse Capability = iota
// MessageTags is the IRCv3 capability named "message-tags":
// https://ircv3.net/specs/extensions/message-tags.html
MessageTags Capability = iota
// MultiPrefix is the IRCv3 capability named "multi-prefix":
// https://ircv3.net/specs/extensions/multi-prefix-3.1.html
MultiPrefix Capability = iota
// SASL is the IRCv3 capability named "sasl":
// https://ircv3.net/specs/extensions/sasl-3.2.html
SASL Capability = iota
// ServerTime is the IRCv3 capability named "server-time":
// https://ircv3.net/specs/extensions/server-time-3.2.html
ServerTime Capability = iota
// SetName is the IRCv3 capability named "setname":
// https://ircv3.net/specs/extensions/setname.html
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":
// https://ircv3.net/specs/extensions/sts.html
STS Capability = iota
// UserhostInNames is the IRCv3 capability named "userhost-in-names":
// https://ircv3.net/specs/extensions/userhost-in-names-3.2.html
UserhostInNames Capability = iota
// ZNCPlayback is the ZNC vendor capability named "znc.in/playback":
// https://wiki.znc.in/Playback
ZNCPlayback Capability = iota
// ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message":
// https://wiki.znc.in/Query_buffers
ZNCSelfMessage Capability = iota
)
// `capabilityNames[capab]` is the string name of the capability `capab`
var (
capabilityNames = [numCapabs]string{
"account-notify",
"account-tag",
"away-notify",
"batch",
"cap-notify",
"chghost",
"draft/account-registration",
"draft/channel-rename",
"draft/chathistory",
"draft/event-playback",
"draft/extended-isupport",
"draft/languages",
"draft/message-redaction",
"draft/metadata-2",
"draft/multiline",
"draft/no-implicit-names",
"draft/persistence",
"draft/pre-away",
"draft/read-marker",
"draft/relaymsg",
"draft/webpush",
"echo-message",
"ergo.chat/nope",
"extended-join",
"extended-monitor",
"invite-notify",
"labeled-response",
"message-tags",
"multi-prefix",
"sasl",
"server-time",
"setname",
"soju.im/webpush",
"standard-replies",
"sts",
"userhost-in-names",
"znc.in/playback",
"znc.in/self-message",
}
)

View File

@ -4,46 +4,45 @@
package caps package caps
import ( import (
"fmt" "sort"
"github.com/ergochat/ergo/irc/utils" "strings"
"sync"
) )
// Set holds a set of enabled capabilities. // Set holds a set of enabled capabilities.
type Set [bitsetLen]uint32 type Set struct {
sync.RWMutex
// Values holds capability values. // capabilities holds the capabilities this manager has.
type Values map[Capability]string capabilities map[Capability]bool
}
// NewSet returns a new Set, with the given capabilities enabled. // NewSet returns a new Set, with the given capabilities enabled.
func NewSet(capabs ...Capability) *Set { func NewSet(capabs ...Capability) *Set {
var newSet Set newSet := Set{
newSet.Enable(capabs...) capabilities: make(map[Capability]bool),
return &newSet
}
// NewCompleteSet returns a new Set, with all defined capabilities enabled.
func NewCompleteSet() *Set {
var newSet Set
asSlice := newSet[:]
for i := 0; i < numCapabs; i += 1 {
utils.BitsetSet(asSlice, uint(i), true)
} }
newSet.Enable(capabs...)
return &newSet return &newSet
} }
// Enable enables the given capabilities. // Enable enables the given capabilities.
func (s *Set) Enable(capabs ...Capability) { func (s *Set) Enable(capabs ...Capability) {
asSlice := s[:] s.Lock()
defer s.Unlock()
for _, capab := range capabs { for _, capab := range capabs {
utils.BitsetSet(asSlice, uint(capab), true) s.capabilities[capab] = true
} }
} }
// Disable disables the given capabilities. // Disable disables the given capabilities.
func (s *Set) Disable(capabs ...Capability) { func (s *Set) Disable(capabs ...Capability) {
asSlice := s[:] s.Lock()
defer s.Unlock()
for _, capab := range capabs { for _, capab := range capabs {
utils.BitsetSet(asSlice, uint(capab), false) delete(s.capabilities, capab)
} }
} }
@ -59,85 +58,60 @@ func (s *Set) Remove(capabs ...Capability) {
s.Disable(capabs...) s.Disable(capabs...)
} }
// Has returns true if this set has the given capability. // Has returns true if this set has the given capabilities.
func (s *Set) Has(capab Capability) bool { func (s *Set) Has(caps ...Capability) bool {
return utils.BitsetGet(s[:], uint(capab)) s.RLock()
} defer s.RUnlock()
// HasAll returns true if the set has all the given capabilities. for _, cap := range caps {
func (s *Set) HasAll(capabs ...Capability) bool { if !s.capabilities[cap] {
for _, capab := range capabs {
if !s.Has(capab) {
return false return false
} }
} }
return true return true
} }
// Union adds all the capabilities of another set to this set. // List return a list of our enabled capabilities.
func (s *Set) Union(other *Set) { func (s *Set) List() []Capability {
utils.BitsetUnion(s[:], other[:]) s.RLock()
} defer s.RUnlock()
// Subtract removes all the capabilities of another set from this set. var allCaps []Capability
func (s *Set) Subtract(other *Set) { for capab := range s.capabilities {
utils.BitsetSubtract(s[:], other[:]) allCaps = append(allCaps, capab)
}
// Empty returns whether the set is empty.
func (s *Set) Empty() bool {
return utils.BitsetEmpty(s[:])
}
const defaultMaxPayloadLength = 450
// Strings returns all of our enabled capabilities as a slice of strings.
func (s *Set) Strings(version Version, values Values, maxLen int) (result []string) {
if maxLen == 0 {
maxLen = defaultMaxPayloadLength
} }
var t utils.TokenLineBuilder
t.Initialize(maxLen, " ")
var capab Capability return allCaps
asSlice := s[:] }
for capab = 0; capab < numCapabs; capab++ {
// XXX clients that only support CAP LS 301 cannot handle multiline // Count returns how many enabled caps this set has.
// responses. omit some CAPs in this case, forcing the response to fit on func (s *Set) Count() int {
// a single line. this is technically buggy for CAP LIST (as opposed to LS) s.RLock()
// but it shouldn't matter defer s.RUnlock()
if version < Cap302 && !isAllowed301(capab) {
continue return len(s.capabilities)
} }
// skip any capabilities that are not enabled
if !utils.BitsetGet(asSlice, uint(capab)) { // String returns all of our enabled capabilities as a string.
continue func (s *Set) String(version Version, values *Values) string {
} s.RLock()
capString := capab.Name() defer s.RUnlock()
if version >= Cap302 {
val, exists := values[capab] var strs sort.StringSlice
for capability := range s.capabilities {
capString := capability.Name()
if version == Cap302 {
val, exists := values.Get(capability)
if exists { if exists {
capString = fmt.Sprintf("%s=%s", capString, val) capString += "=" + val
} }
} }
t.Add(capString) strs = append(strs, capString)
} }
result = t.Lines() // sort the cap string before we send it out
if result == nil { sort.Sort(strs)
result = []string{""}
}
return
}
// this is a fixed whitelist of caps that are eligible for display in CAP LS 301 return strings.Join(strs, " ")
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,23 +3,20 @@
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()
s1.Enable(AccountTag, EchoMessage, UserhostInNames) s1.Enable(AccountTag, EchoMessage, UserhostInNames)
if !(s1.Has(AccountTag) && s1.Has(EchoMessage) && s1.Has(UserhostInNames)) { if !s1.Has(AccountTag, EchoMessage, UserhostInNames) {
t.Error("Did not have the tags we expected") t.Error("Did not have the tags we expected")
} }
if s1.Has(STS) { if s1.Has(AccountTag, EchoMessage, STS, UserhostInNames) {
t.Error("Has() returned true when we don't have the given capability") t.Error("Has() returned true when we don't have all the given capabilities")
} }
s1.Disable(AccountTag) s1.Disable(AccountTag)
@ -28,9 +25,14 @@ func TestSets(t *testing.T) {
t.Error("Disable() did not correctly disable the given capability") t.Error("Disable() did not correctly disable the given capability")
} }
enabledCaps := NewSet() enabledCaps := make(map[Capability]bool)
enabledCaps.Union(s1) for _, capab := range s1.List() {
expectedCaps := NewSet(EchoMessage, UserhostInNames) enabledCaps[capab] = true
}
expectedCaps := map[Capability]bool{
EchoMessage: true,
UserhostInNames: true,
}
if !reflect.DeepEqual(enabledCaps, expectedCaps) { if !reflect.DeepEqual(enabledCaps, expectedCaps) {
t.Errorf("Enabled and expected capability lists do not match: %v, %v", enabledCaps, expectedCaps) t.Errorf("Enabled and expected capability lists do not match: %v, %v", enabledCaps, expectedCaps)
} }
@ -38,72 +40,31 @@ func TestSets(t *testing.T) {
// make sure re-enabling doesn't add to the count or something weird like that // make sure re-enabling doesn't add to the count or something weird like that
s1.Enable(EchoMessage) s1.Enable(EchoMessage)
if s1.Count() != 2 {
t.Error("Count() did not match expected capability count")
}
// make sure add and remove work fine // make sure add and remove work fine
s1.Add(InviteNotify) s1.Add(InviteNotify)
s1.Remove(EchoMessage) s1.Remove(EchoMessage)
if !s1.Has(InviteNotify) || s1.Has(EchoMessage) { if s1.Count() != 2 {
t.Error("Add/Remove don't work") t.Error("Count() did not match expected capability count")
} }
// test Strings() // test String()
values := make(Values) values := NewValues()
values[InviteNotify] = "invitemepls" values.Set(InviteNotify, "invitemepls")
actualCap301ValuesString := s1.Strings(Cap301, values, 0) actualCap301ValuesString := s1.String(Cap301, values)
expectedCap301ValuesString := []string{"invite-notify userhost-in-names"} expectedCap301ValuesString := "invite-notify userhost-in-names"
if !reflect.DeepEqual(actualCap301ValuesString, expectedCap301ValuesString) { if actualCap301ValuesString != expectedCap301ValuesString {
t.Errorf("Generated Cap301 values string [%v] did not match expected values string [%v]", actualCap301ValuesString, expectedCap301ValuesString) t.Errorf("Generated Cap301 values string [%s] did not match expected values string [%s]", actualCap301ValuesString, expectedCap301ValuesString)
} }
actualCap302ValuesString := s1.Strings(Cap302, values, 0) actualCap302ValuesString := s1.String(Cap302, values)
expectedCap302ValuesString := []string{"invite-notify=invitemepls userhost-in-names"} expectedCap302ValuesString := "invite-notify=invitemepls userhost-in-names"
if !reflect.DeepEqual(actualCap302ValuesString, expectedCap302ValuesString) { if actualCap302ValuesString != expectedCap302ValuesString {
t.Errorf("Generated Cap302 values string [%s] did not match expected values string [%s]", actualCap302ValuesString, expectedCap302ValuesString) t.Errorf("Generated Cap302 values string [%s] did not match expected values string [%s]", actualCap302ValuesString, expectedCap302ValuesString)
} }
} }
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) {
s1 := NewSet(AccountTag, EchoMessage, UserhostInNames, ServerTime)
toRemove := NewSet(UserhostInNames, EchoMessage)
s1.Subtract(toRemove)
if !reflect.DeepEqual(s1, NewSet(AccountTag, ServerTime)) {
t.Errorf("subtract doesn't work")
}
}
func BenchmarkSetReads(b *testing.B) {
set := NewSet(UserhostInNames, EchoMessage)
b.ResetTimer()
for i := 0; i < b.N; i++ {
set.Has(UserhostInNames)
set.Has(LabeledResponse)
set.Has(EchoMessage)
set.Has(Nope)
}
}
func BenchmarkSetWrites(b *testing.B) {
for i := 0; i < b.N; i++ {
set := NewSet(UserhostInNames, EchoMessage)
set.Add(Nope)
set.Add(ExtendedJoin)
set.Remove(UserhostInNames)
set.Remove(LabeledResponse)
}
}

45
irc/caps/values.go Normal file
View File

@ -0,0 +1,45 @@
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package caps
import "sync"
// Values holds capability values.
type Values struct {
sync.RWMutex
// values holds our actual capability values.
values map[Capability]string
}
// NewValues returns a new Values.
func NewValues() *Values {
return &Values{
values: make(map[Capability]string),
}
}
// Set sets the value for the given capability.
func (v *Values) Set(capab Capability, value string) {
v.Lock()
defer v.Unlock()
v.values[capab] = value
}
// Unset removes the value for the given capability, if it exists.
func (v *Values) Unset(capab Capability) {
v.Lock()
defer v.Unlock()
delete(v.values, capab)
}
// Get returns the value of the given capability, and whether one exists.
func (v *Values) Get(capab Capability) (string, bool) {
v.RLock()
defer v.RUnlock()
value, exists := v.values[capab]
return value, exists
}

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,7 @@
package irc package irc
import ( import (
"sort"
"sync" "sync"
"time"
"github.com/ergochat/ergo/irc/datastore"
"github.com/ergochat/ergo/irc/utils"
) )
type channelManagerEntry struct { type channelManagerEntry struct {
@ -18,7 +13,6 @@ type channelManagerEntry struct {
// think the channel is empty (without holding a lock across the entire Channel.Join() // think the channel is empty (without holding a lock across the entire Channel.Join()
// call) // call)
pendingJoins int pendingJoins int
skeleton string
} }
// ChannelManager keeps track of all the channels on the server, // ChannelManager keeps track of all the channels on the server,
@ -26,162 +20,96 @@ type channelManagerEntry struct {
// cleanup of empty channels on last part, and renames. // cleanup of empty channels on last part, and renames.
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 map[string]*channelManagerEntry
chans map[string]*channelManagerEntry
chansSkeletons utils.HashSet[string]
purgedChannels map[string]ChannelPurgeRecord // casefolded name to purge record
server *Server
} }
// NewChannelManager returns a new ChannelManager. // NewChannelManager returns a new ChannelManager.
func (cm *ChannelManager) Initialize(server *Server, config *Config) (err error) { func NewChannelManager() *ChannelManager {
cm.chans = make(map[string]*channelManagerEntry) return &ChannelManager{
cm.chansSkeletons = make(utils.HashSet[string]) chans: make(map[string]*channelManagerEntry),
cm.server = server
return cm.loadRegisteredChannels(config)
}
func (cm *ChannelManager) loadRegisteredChannels(config *Config) (err error) {
allChannels, err := FetchAndDeserializeAll[RegisteredChannel](datastore.TableChannels, cm.server.dstore, cm.server.logger)
if err != nil {
return
} }
allPurgeRecords, err := FetchAndDeserializeAll[ChannelPurgeRecord](datastore.TableChannelPurges, cm.server.dstore, cm.server.logger)
if err != nil {
return
}
cm.Lock()
defer cm.Unlock()
cm.purgedChannels = make(map[string]ChannelPurgeRecord, len(allPurgeRecords))
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 {
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 entry != nil {
entry := cm.chans[name] return entry.channel
if entry != nil { }
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, rb *ResponseBuffer) error {
server := client.server server := client.server
casefoldedName, err := CasefoldChannel(name) casefoldedName, err := CasefoldChannel(name)
skeleton, skerr := Skeleton(name) if err != nil || len(casefoldedName) > server.Limits().ChannelLen {
if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen { return errNoSuchChannel
return errNoSuchChannel, ""
} }
channel, err, newChannel := func() (*Channel, error, bool) { cm.Lock()
var newChannel bool entry := cm.chans[casefoldedName]
if entry == nil {
// XXX give up the lock to check for a registration, then check again
// to see if we need to create the channel. we could solve this by doing LoadChannel
// outside the lock initially on every join, so this is best thought of as an
// optimization to avoid that.
cm.Unlock()
info := client.server.channelRegistry.LoadChannel(casefoldedName)
cm.Lock() cm.Lock()
defer cm.Unlock() entry = cm.chans[casefoldedName]
// check purges first; a registered purged channel will still be present in `chans`
if _, ok := cm.purgedChannels[casefoldedName]; ok {
return nil, errChannelPurged, false
}
entry := cm.chans[casefoldedName]
if entry == nil { if entry == nil {
if server.Config().Channels.OpOnlyCreation &&
!(isSajoin || client.HasRoleCapabs("chanreg")) {
return nil, errInsufficientPrivs, false
}
// enforce confusables
if cm.chansSkeletons.Has(skeleton) {
return nil, errConfusableIdentifier, false
}
entry = &channelManagerEntry{ entry = &channelManagerEntry{
channel: NewChannel(server, name, casefoldedName, false, RegisteredChannel{}), channel: NewChannel(server, name, true, info),
pendingJoins: 0, pendingJoins: 0,
} }
cm.chansSkeletons.Add(skeleton)
entry.skeleton = skeleton
cm.chans[casefoldedName] = entry cm.chans[casefoldedName] = entry
newChannel = true
} }
entry.pendingJoins += 1
return entry.channel, nil, newChannel
}()
if err != nil {
return err, ""
} }
entry.pendingJoins += 1
cm.Unlock()
err, forward = channel.Join(client, key, isSajoin || newChannel, rb) entry.channel.Join(client, key, rb)
cm.maybeCleanup(channel, true) cm.maybeCleanup(entry.channel, true)
return return nil
} }
func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) { func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {
cm.Lock() cm.Lock()
defer cm.Unlock() defer cm.Unlock()
cfname := channel.NameCasefolded() entry := cm.chans[channel.NameCasefolded()]
entry := cm.chans[cfname]
if entry == nil || entry.channel != channel { if entry == nil || entry.channel != channel {
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
} }
if entry.pendingJoins == 0 && entry.channel.IsClean() { // TODO(slingamn) right now, registered channels cannot be cleaned up.
delete(cm.chans, cfname) // this is because once ChannelManager becomes the source of truth about a channel,
if entry.skeleton != "" { // we can't move the source of truth back to the database unless we do an ACID
delete(cm.chansSkeletons, entry.skeleton) // store while holding the ChannelManager's Lock(). This is pending more decisions
} // about where the database transaction lock fits into the overall lock model.
if !entry.channel.IsRegistered() && entry.channel.IsEmpty() && entry.pendingJoins == 0 {
// reread the name, handling the case where the channel was renamed
casefoldedName := entry.channel.NameCasefolded()
delete(cm.chans, casefoldedName)
// invalidate the entry (otherwise, a subsequent cleanup attempt could delete
// a valid, distinct entry under casefoldedName):
entry.channel = nil
} }
} }
// Part parts `client` from the channel named `name`, deleting it if it's empty. // Part parts `client` from the channel named `name`, deleting it if it's empty.
func (cm *ChannelManager) Part(client *Client, name string, message string, rb *ResponseBuffer) error { func (cm *ChannelManager) Part(client *Client, name string, message string, rb *ResponseBuffer) error {
var channel *Channel
casefoldedName, err := CasefoldChannel(name) casefoldedName, err := CasefoldChannel(name)
if err != nil { if err != nil {
return errNoSuchChannel return errNoSuchChannel
@ -189,15 +117,12 @@ func (cm *ChannelManager) Part(client *Client, name string, message string, rb *
cm.RLock() cm.RLock()
entry := cm.chans[casefoldedName] entry := cm.chans[casefoldedName]
if entry != nil {
channel = entry.channel
}
cm.RUnlock() cm.RUnlock()
if channel == nil { if entry == nil {
return errNoSuchChannel return errNoSuchChannel
} }
channel.Part(client, message, rb) entry.channel.Part(client, message, rb)
return nil return nil
} }
@ -205,141 +130,34 @@ func (cm *ChannelManager) Cleanup(channel *Channel) {
cm.maybeCleanup(channel, false) cm.maybeCleanup(channel, false)
} }
func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) {
if account == "" {
return errAuthRequired // this is already enforced by ChanServ, but do a final check
}
if cm.server.Defcon() <= 4 {
return errFeatureDisabled
}
var channel *Channel
cfname, err := CasefoldChannel(channelName)
if err != nil {
return err
}
var entry *channelManagerEntry
defer func() {
if err == nil && channel != nil {
// registration was successful: make the database reflect it
err = channel.Store(IncludeAllAttrs)
}
}()
cm.Lock()
defer cm.Unlock()
entry = cm.chans[cfname]
if entry == nil {
return errNoSuchChannel
}
channel = entry.channel
err = channel.SetRegistered(account)
if err != nil {
return err
}
return nil
}
func (cm *ChannelManager) SetUnregistered(channelName string, account string) (err error) {
cfname, err := CasefoldChannel(channelName)
if err != nil {
return err
}
var uuid utils.UUID
defer func() {
if err == nil {
if delErr := cm.server.dstore.Delete(datastore.TableChannels, uuid); delErr != nil {
cm.server.logger.Error("datastore", "couldn't delete channel registration", cfname, delErr.Error())
}
}
}()
cm.Lock()
defer cm.Unlock()
entry := cm.chans[cfname]
if entry != nil {
if entry.channel.Founder() != account {
return errChannelNotOwnedByAccount
}
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
}
// 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) error {
oldCfname, err := CasefoldChannel(name) cfname, err := CasefoldChannel(name)
if err != nil { if err != nil {
return errNoSuchChannel return errNoSuchChannel
} }
newCfname, err := CasefoldChannel(newName) cfnewname, err := CasefoldChannel(newname)
if err != nil { if err != nil {
return errInvalidChannelName return errInvalidChannelName
} }
newSkeleton, err := Skeleton(newName)
if err != nil {
return errInvalidChannelName
}
var channel *Channel
var info RegisteredChannel
defer func() {
if channel != nil && info.Founder != "" {
channel.MarkDirty(IncludeAllAttrs)
}
// always-on clients need to update their saved channel memberships
for _, member := range channel.Members() {
member.markDirty(IncludeChannels)
}
}()
cm.Lock() cm.Lock()
defer cm.Unlock() defer cm.Unlock()
entry := cm.chans[oldCfname] if cm.chans[cfnewname] != nil {
return errChannelNameInUse
}
entry := cm.chans[cfname]
if entry == nil { if entry == nil {
return errNoSuchChannel return errNoSuchChannel
} }
channel = entry.channel delete(cm.chans, cfname)
info = channel.ExportRegistration() cm.chans[cfnewname] = entry
registered := info.Founder != "" entry.channel.setName(newname)
entry.channel.setNameCasefolded(cfnewname)
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
delete(cm.chansSkeletons, oldSkeleton)
cm.chansSkeletons.Add(newSkeleton)
entry.channel.Rename(newName, newCfname)
return nil return nil
} }
// Len returns the number of channels // Len returns the number of channels
@ -353,163 +171,8 @@ func (cm *ChannelManager) Len() int {
func (cm *ChannelManager) Channels() (result []*Channel) { func (cm *ChannelManager) Channels() (result []*Channel) {
cm.RLock() cm.RLock()
defer cm.RUnlock() defer cm.RUnlock()
result = make([]*Channel, 0, len(cm.chans))
for _, entry := range cm.chans { for _, entry := range cm.chans {
result = append(result, entry.channel) result = append(result, entry.channel)
} }
return 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)
}
}
return
}
// Purge marks a channel as purged.
func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err error) {
chname, err = CasefoldChannel(chname)
if err != nil {
return errInvalidChannelName
}
record.NameCasefolded = chname
record.UUID = utils.GenerateUUIDv4()
channel, err := func() (channel *Channel, err error) {
cm.Lock()
defer cm.Unlock()
if _, ok := cm.purgedChannels[chname]; ok {
return nil, errChannelPurgedAlready
}
entry := cm.chans[chname]
// atomically prevent anyone from rejoining
cm.purgedChannels[chname] = record
if entry != nil {
channel = entry.channel
}
return
}()
if err != nil {
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.
func (cm *ChannelManager) IsPurged(chname string) (result bool) {
chname, err := CasefoldChannel(chname)
if err != nil {
return false
}
cm.RLock()
_, result = cm.purgedChannels[chname]
cm.RUnlock()
return
}
// Unpurge deletes a channel's purged status.
func (cm *ChannelManager) Unpurge(chname string) (err error) {
chname, err = CasefoldChannel(chname)
if err != nil {
return errNoSuchChannel
}
cm.Lock()
record, found := cm.purgedChannels[chname]
delete(cm.purgedChannels, chname)
cm.Unlock()
if !found {
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
}
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

@ -4,37 +4,51 @@
package irc package irc
import ( import (
"encoding/json" "fmt"
"strconv"
"sync"
"time" "time"
"github.com/ergochat/ergo/irc/modes" "encoding/json"
"github.com/ergochat/ergo/irc/utils"
"github.com/tidwall/buntdb"
) )
// 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
// 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
const ( const (
IncludeInitial uint = 1 << iota keyChannelExists = "channel.exists %s"
IncludeTopic keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
IncludeModes keyChannelRegTime = "channel.registered.time %s"
IncludeLists keyChannelFounder = "channel.founder %s"
IncludeSettings 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"
) )
// this is an OR of all possible flags var (
const ( channelKeyStrings = []string{
IncludeAllAttrs = ^uint(0) keyChannelExists,
keyChannelName,
keyChannelRegTime,
keyChannelFounder,
keyChannelTopic,
keyChannelTopicSetBy,
keyChannelTopicSetTime,
keyChannelBanlist,
keyChannelExceptlist,
keyChannelInvitelist,
}
) )
// RegisteredChannel holds details about a given registered channel. // RegisteredChannel holds details about a given registered channel.
type RegisteredChannel struct { type RegisteredChannel struct {
// Name of the channel. // Name of the channel.
Name string Name string
// UUID for the datastore.
UUID utils.UUID
// 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.
@ -45,48 +59,166 @@ type RegisteredChannel struct {
TopicSetBy string TopicSetBy string
// TopicSetTime represents the time the topic was set. // TopicSetTime represents the time the topic was set.
TopicSetTime time.Time TopicSetTime time.Time
// Modes represents the channel modes // Banlist represents the bans set on the channel.
Modes []modes.Mode Banlist []string
// Key represents the channel key / password // Exceptlist represents the exceptions set on the channel.
Key string Exceptlist []string
// Forward is the forwarding/overflow (+f) channel // Invitelist represents the invite exceptions set on the channel.
Forward string Invitelist []string
// UserLimit is the user limit (0 for no limit)
UserLimit int
// AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h)
AccountToUMode map[string]modes.Mode
// Bans represents the bans set on the channel.
Bans map[string]MaskInfo
// Excepts represents the exceptions set on the channel.
Excepts map[string]MaskInfo
// Invites represents the invite exceptions set on the channel.
Invites map[string]MaskInfo
// Settings are the chanserv-modifiable settings
Settings ChannelSettings
// Metadata set using the METADATA command
Metadata map[string]string
} }
func (r *RegisteredChannel) Serialize() ([]byte, error) { // ChannelRegistry manages registered channels.
return json.Marshal(r) type ChannelRegistry struct {
// This serializes operations of the form (read channel state, synchronously persist it);
// this is enough to guarantee eventual consistency of the database with the
// ChannelManager and Channel objects, which are the source of truth.
//
// We could use the buntdb RW transaction lock for this purpose but we share
// that with all the other modules, so let's not.
sync.Mutex // tier 2
server *Server
} }
func (r *RegisteredChannel) Deserialize(b []byte) (err error) { // NewChannelRegistry returns a new ChannelRegistry.
return json.Unmarshal(b, r) func NewChannelRegistry(server *Server) *ChannelRegistry {
return &ChannelRegistry{
server: server,
}
} }
type ChannelPurgeRecord struct { // StoreChannel obtains a consistent view of a channel, then persists it to the store.
NameCasefolded string `json:"Name"` func (reg *ChannelRegistry) StoreChannel(channel *Channel, includeLists bool) {
UUID utils.UUID if !reg.server.ChannelRegistrationEnabled() {
Oper string return
PurgedAt time.Time }
Reason string
reg.Lock()
defer reg.Unlock()
key := channel.NameCasefolded()
info := channel.ExportRegistration(includeLists)
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, key, info, includeLists)
return nil
})
} }
func (c *ChannelPurgeRecord) Serialize() ([]byte, error) { // LoadChannel loads a channel from the store.
return json.Marshal(c) func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info *RegisteredChannel) {
if !reg.server.ChannelRegistrationEnabled() {
return nil
}
channelKey := nameCasefolded
// nice to have: do all JSON (de)serialization outside of the buntdb transaction
reg.server.store.View(func(tx *buntdb.Tx) error {
_, err := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
if err == buntdb.ErrNotFound {
// chan does not already exist, return
return nil
}
// 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)
banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey))
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
var banlist []string
_ = json.Unmarshal([]byte(banlistString), &banlist)
var exceptlist []string
_ = json.Unmarshal([]byte(exceptlistString), &exceptlist)
var invitelist []string
_ = json.Unmarshal([]byte(invitelistString), &invitelist)
info = &RegisteredChannel{
Name: name,
RegisteredAt: time.Unix(regTimeInt, 0),
Founder: founder,
Topic: topic,
TopicSetBy: topicSetBy,
TopicSetTime: time.Unix(topicSetTimeInt, 0),
Banlist: banlist,
Exceptlist: exceptlist,
Invitelist: invitelist,
}
return nil
})
return info
} }
func (c *ChannelPurgeRecord) Deserialize(b []byte) error { // Rename handles the persistence part of a channel rename: the channel is
return json.Unmarshal(b, c) // persisted under its new name, and the old name is cleaned up if necessary.
func (reg *ChannelRegistry) Rename(channel *Channel, casefoldedOldName string) {
if !reg.server.ChannelRegistrationEnabled() {
return
}
reg.Lock()
defer reg.Unlock()
includeLists := true
oldKey := casefoldedOldName
key := channel.NameCasefolded()
info := channel.ExportRegistration(includeLists)
if info.Founder == "" {
return
}
reg.server.store.Update(func(tx *buntdb.Tx) error {
reg.deleteChannel(tx, oldKey, info)
reg.saveChannel(tx, key, info, includeLists)
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)
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 == info.RegisteredAt {
for _, keyFmt := range channelKeyStrings {
tx.Delete(fmt.Sprintf(keyFmt, key))
}
}
}
}
// saveChannel saves a channel to the store.
func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelKey string, channelInfo RegisteredChannel, includeLists bool) {
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)
tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.Unix(), 10), nil)
if includeLists {
banlistString, _ := json.Marshal(channelInfo.Banlist)
tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil)
exceptlistString, _ := json.Marshal(channelInfo.Exceptlist)
tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil)
invitelistString, _ := json.Marshal(channelInfo.Invitelist)
tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil)
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,25 +5,50 @@
package irc package irc
import ( import (
"fmt"
"log"
"regexp"
"strings" "strings"
"sync"
"github.com/ergochat/ergo/irc/caps" "github.com/goshuirc/irc-go/ircmatch"
"github.com/ergochat/ergo/irc/modes" "github.com/oragono/oragono/irc/caps"
"github.com/ergochat/ergo/irc/utils"
"sync"
) )
// ExpandUserHost takes a userhost, and returns an expanded version.
func ExpandUserHost(userhost string) (expanded string) {
expanded = userhost
// fill in missing wildcards for nicks
//TODO(dan): this would fail with dan@lol, fix that.
if !strings.Contains(expanded, "!") {
expanded += "!*"
}
if !strings.Contains(expanded, "@") {
expanded += "@*"
}
return
}
// ClientManager keeps track of clients by nick, enforcing uniqueness of casefolded nicks // ClientManager keeps track of clients by nick, enforcing uniqueness of casefolded nicks
type ClientManager struct { type ClientManager struct {
sync.RWMutex // tier 2 sync.RWMutex // tier 2
byNick map[string]*Client byNick map[string]*Client
bySkeleton map[string]*Client
} }
// Initialize initializes a ClientManager. // NewClientManager returns a new ClientManager.
func (clients *ClientManager) Initialize() { func NewClientManager() *ClientManager {
clients.byNick = make(map[string]*Client) return &ClientManager{
clients.bySkeleton = make(map[string]*Client) byNick: 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.
@ -38,37 +63,19 @@ 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) (removed bool) {
// requires holding the writable Lock() // requires holding the writable Lock()
if oldcfnick == "*" || oldcfnick == "" { oldcfnick := client.NickCasefolded()
return errNickMissing
}
currentEntry, present := clients.byNick[oldcfnick] currentEntry, present := clients.byNick[oldcfnick]
if present { if present {
if currentEntry == client { if currentEntry == client {
delete(clients.byNick, oldcfnick) delete(clients.byNick, oldcfnick)
removed = true
} else { } else {
// this shouldn't happen, but we can ignore it // this shouldn't happen, but we can ignore it
client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick) client.server.logger.Warning("internal", fmt.Sprintf("clients for nick %s out of sync", oldcfnick))
err = errNickMissing
} }
} else {
err = errNickMissing
} }
currentEntry, present = clients.bySkeleton[oldskeleton]
if present {
if currentEntry == client {
delete(clients.bySkeleton, oldskeleton)
} else {
client.server.logger.Warning("internal", "clients for skeleton out of sync", oldskeleton)
err = errNickMissing
}
} else {
err = errNickMissing
}
return return
} }
@ -77,163 +84,42 @@ func (clients *ClientManager) Remove(client *Client) error {
clients.Lock() clients.Lock()
defer clients.Unlock() defer clients.Unlock()
oldcfnick, oldskeleton := client.uniqueIdentifiers() if !client.HasNick() {
return clients.removeInternal(client, oldcfnick, oldskeleton) return errNickMissing
}
clients.removeInternal(client)
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, newNick string) error {
// actually claiming it newcfnick, err := CasefoldName(newNick)
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, awayChanged bool) { if err != nil {
config := client.server.Config() return err
var newCfNick, newSkeleton string
client.stateMutex.RLock()
account := client.account
accountName := client.accountName
settings := client.accountSettings
registered := client.registered
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 var reservedAccount string
var alwaysOn, useAccountName bool var method NickReservationMethod
if account != "" { if client.server.AccountConfig().NickReservation.Enabled {
alwaysOn = persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn) reservedAccount = client.server.accounts.NickToAccount(newcfnick)
useAccountName = alwaysOn || config.Accounts.NickReservation.ForceNickEqualsAccount method = client.server.AccountConfig().NickReservation.Method
}
nickIsReserved := false
if useAccountName {
if registered && newNick != accountName {
return "", errNickAccountMismatch, false
}
newNick = accountName
newCfNick = account
newSkeleton, err = Skeleton(newNick)
if err != nil {
return "", errNicknameInvalid, false
}
} else {
newNick = strings.TrimSpace(newNick)
if len(newNick) == 0 {
return "", errNickMissing, false
}
if account == "" && config.Accounts.NickReservation.ForceGuestFormat && !dryRun {
newCfNick, err = CasefoldName(newNick)
if err != nil {
return "", errNicknameInvalid, false
}
if !config.Accounts.NickReservation.guestRegexpFolded.MatchString(newCfNick) {
newNick = strings.Replace(config.Accounts.NickReservation.GuestFormat, "*", newNick, 1)
newCfNick = "" // re-fold it below
}
}
if newCfNick == "" {
newCfNick, err = CasefoldName(newNick)
}
if err != nil {
return "", errNicknameInvalid, false
}
if len(newNick) > config.Limits.NickLen || len(newCfNick) > config.Limits.NickLen {
return "", errNicknameInvalid, false
}
newSkeleton, err = Skeleton(newNick)
if err != nil {
return "", errNicknameInvalid, false
}
if config.isRelaymsgIdentifier(newNick) {
return "", errNicknameInvalid, false
}
if restrictedCasefoldedNicks.Has(newCfNick) || restrictedSkeletons.Has(newSkeleton) {
return "", errNicknameInvalid, false
}
reservedAccount, method := client.server.accounts.EnforcementStatus(newCfNick, newSkeleton)
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
// see #2135: we want to enter the critical section, see if the nick is actually in use,
// and return errNicknameInUse in that case
nickIsReserved = true
}
}
var bouncerAllowed bool
if config.Accounts.Multiclient.Enabled {
if useAccountName {
bouncerAllowed = true
} else {
if config.Accounts.Multiclient.AllowedByDefault && settings.AllowBouncer != MulticlientDisallowedByUser {
bouncerAllowed = true
} else if settings.AllowBouncer == MulticlientAllowedByUser {
bouncerAllowed = true
}
}
} }
clients.Lock() clients.Lock()
defer clients.Unlock() defer clients.Unlock()
currentClient := clients.byNick[newCfNick] clients.removeInternal(client)
currentNewEntry := clients.byNick[newcfnick]
// the client may just be changing case // the client may just be changing case
if currentClient != nil && currentClient != client { if currentNewEntry != nil && currentNewEntry != client {
// these conditions forbid reattaching to an existing session: return errNicknameInUse
if registered || !bouncerAllowed || account == "" || account != currentClient.Account() ||
dryRun || session == nil {
return "", errNicknameInUse, false
}
reattachSuccessful, numSessions, lastSeen, wasAway, nowAway := currentClient.AddSession(session)
if !reattachSuccessful {
return "", errNicknameInUse, false
}
if numSessions == 1 {
invisible := currentClient.HasMode(modes.Invisible)
operator := currentClient.HasMode(modes.Operator)
client.server.stats.AddRegistered(invisible, operator)
}
session.autoreplayMissedSince = lastSeen
// successful reattach!
return newNick, nil, wasAway != nowAway
} else if currentClient == client && currentClient.Nick() == newNick {
return "", errNoop, false
} }
// analogous checks for skeletons if method == NickReservationStrict && reservedAccount != client.Account() {
skeletonHolder := clients.bySkeleton[newSkeleton] return errNicknameReserved
if skeletonHolder != nil && skeletonHolder != client {
return "", errNicknameInUse, false
} }
if nickIsReserved { clients.byNick[newcfnick] = client
return "", errNicknameReserved, false client.updateNickMask(newNick)
} return nil
if dryRun {
return "", nil, false
}
formercfnick, formerskeleton := client.uniqueIdentifiers()
if changeSuccess := client.SetNick(newNick, newCfNick, newSkeleton); !changeSuccess {
return "", errClientDestroyed, false
}
clients.removeInternal(client, formercfnick, formerskeleton)
clients.byNick[newCfNick] = client
clients.bySkeleton[newSkeleton] = client
return newNick, nil, false
} }
func (clients *ClientManager) AllClients() (result []*Client) { func (clients *ClientManager) AllClients() (result []*Client) {
@ -248,52 +134,41 @@ 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) (set ClientSet) {
set = make(ClientSet)
clients.RLock() clients.RLock()
defer clients.RUnlock() defer clients.RUnlock()
for _, client := range clients.byNick { var client *Client
for _, session := range client.Sessions() { for _, client = range clients.byNick {
// cap-notify is implicit in cap version 302 and above // make sure they have all the required caps
if session.capabilities.Has(caps.CapNotify) || 302 <= session.capVersion { for _, capab := range capabs {
sessions = append(sessions, session) if !client.capabilities.Has(capab) {
continue
} }
} }
set.Add(client)
} }
return return set
}
// AllWithPushSubscriptions returns all clients that are always-on with an active push subscription.
func (clients *ClientManager) AllWithPushSubscriptions() (result []*Client) {
clients.RLock()
defer clients.RUnlock()
for _, client := range clients.byNick {
if client.hasPushSubscriptions() && client.AlwaysOn() {
result = append(result, client)
}
}
return result
} }
// FindAll returns all clients that match the given userhost mask. // FindAll returns all clients that match the given userhost mask.
func (clients *ClientManager) FindAll(userhost string) (set ClientSet) { func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
set = make(ClientSet) set = make(ClientSet)
userhost, err := CanonicalizeMaskWildcard(userhost) userhost, err := Casefold(ExpandUserHost(userhost))
if err != nil { if err != nil {
return set return set
} }
matcher, err := utils.CompileGlob(userhost, false) matcher := ircmatch.MakeMatch(userhost)
if err != nil {
// not much we can do here
return
}
clients.RLock() clients.RLock()
defer clients.RUnlock() defer clients.RUnlock()
for _, client := range clients.byNick { for _, client := range clients.byNick {
if matcher.MatchString(client.NickMaskCasefolded()) { if matcher.Match(client.nickMaskCasefolded) {
set.Add(client) set.Add(client)
} }
} }
@ -301,15 +176,166 @@ 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 // Find returns the first client that matches the given userhost mask.
// is present (or always-on). func (clients *ClientManager) Find(userhost string) *Client {
func (clients *ClientManager) UnfoldNick(cfnick string) (nick string) { userhost, err := Casefold(ExpandUserHost(userhost))
if err != nil {
return nil
}
matcher := ircmatch.MakeMatch(userhost)
var matchedClient *Client
clients.RLock() clients.RLock()
c := clients.byNick[cfnick] defer clients.RUnlock()
clients.RUnlock() for _, client := range clients.byNick {
if c != nil { if matcher.Match(client.nickMaskCasefolded) {
return c.Nick() matchedClient = client
} else { break
return cfnick }
}
return matchedClient
}
//
// usermask to regexp
//
//TODO(dan): move this over to generally using glob syntax instead?
// kinda more expected in normal ban/etc masks, though regex is useful (probably as an extban?)
// UserMaskSet holds a set of client masks and lets you match hostnames to them.
type UserMaskSet struct {
sync.RWMutex
masks map[string]bool
regexp *regexp.Regexp
}
// NewUserMaskSet returns a new UserMaskSet.
func NewUserMaskSet() *UserMaskSet {
return &UserMaskSet{
masks: make(map[string]bool),
} }
} }
// Add adds the given mask to this set.
func (set *UserMaskSet) Add(mask string) (added bool) {
casefoldedMask, err := Casefold(mask)
if err != nil {
log.Println(fmt.Sprintf("ERROR: Could not add mask to usermaskset: [%s]", mask))
return false
}
set.Lock()
added = !set.masks[casefoldedMask]
if added {
set.masks[casefoldedMask] = true
}
set.Unlock()
if added {
set.setRegexp()
}
return
}
// AddAll adds the given masks to this set.
func (set *UserMaskSet) AddAll(masks []string) (added bool) {
set.Lock()
defer set.Unlock()
for _, mask := range masks {
if !added && !set.masks[mask] {
added = true
}
set.masks[mask] = true
}
if added {
set.setRegexp()
}
return
}
// Remove removes the given mask from this set.
func (set *UserMaskSet) Remove(mask string) (removed bool) {
set.Lock()
removed = set.masks[mask]
if removed {
delete(set.masks, mask)
}
set.Unlock()
if removed {
set.setRegexp()
}
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)
}
// String returns the masks in this set.
func (set *UserMaskSet) String() string {
set.RLock()
masks := make([]string, len(set.masks))
index := 0
for mask := range set.masks {
masks[index] = mask
index++
}
set.RUnlock()
return strings.Join(masks, " ")
}
func (set *UserMaskSet) Length() int {
set.RLock()
defer set.RUnlock()
return len(set.masks)
}
// setRegexp generates a regular expression from the set of user mask
// strings. Masks are split at the two types of wildcards, `*` and
// `?`. All the pieces are meta-escaped. `*` is replaced with `.*`,
// the regexp equivalent. Likewise, `?` is replaced with `.`. The
// parts are re-joined and finally all masks are joined into a big
// or-expression.
func (set *UserMaskSet) setRegexp() {
var re *regexp.Regexp
set.RLock()
maskExprs := make([]string, len(set.masks))
index := 0
for mask := range set.masks {
manyParts := strings.Split(mask, "*")
manyExprs := make([]string, len(manyParts))
for mindex, manyPart := range manyParts {
oneParts := strings.Split(manyPart, "?")
oneExprs := make([]string, len(oneParts))
for oindex, onePart := range oneParts {
oneExprs[oindex] = regexp.QuoteMeta(onePart)
}
manyExprs[mindex] = strings.Join(oneExprs, ".")
}
maskExprs[index] = strings.Join(manyExprs, ".*")
index++
}
set.RUnlock()
if index > 0 {
expr := "^" + strings.Join(maskExprs, "|") + "$"
re, _ = regexp.Compile(expr)
}
set.Lock()
set.regexp = re
set.Unlock()
}

View File

@ -1,133 +0,0 @@
// Copyright (c) 2019 Shivaram Lingamneni
// released under the MIT license
package irc
import (
"fmt"
"testing"
"github.com/ergochat/ergo/irc/languages"
"github.com/ergochat/ergo/irc/utils"
)
func TestGenerateBatchID(t *testing.T) {
var session Session
s := make(utils.HashSet[string])
count := 100000
for i := 0; i < count; i++ {
s.Add(session.generateBatchID())
}
if len(s) != count {
t.Error("duplicate batch ID detected")
}
}
func BenchmarkGenerateBatchID(b *testing.B) {
var session Session
for i := 0; i < b.N; i++ {
session.generateBatchID()
}
}
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) {
var um UserMaskSet
if um.Match("horse_!user@tor-network.onion") {
t.Error("bad match")
}
um.Add("_!*@*", "x", "x")
if !um.Match("_!user@tor-network.onion") {
t.Error("failure to match")
}
if um.Match("horse_!user@tor-network.onion") {
t.Error("bad match")
}
um.Add("beer*!*@*", "x", "x")
if !um.Match("beergarden!user@tor-network.onion") {
t.Error("failure to match")
}
if um.Match("horse_!user@tor-network.onion") {
t.Error("bad match")
}
um.Add("horse*!user@*", "x", "x")
if !um.Match("horse_!user@tor-network.onion") {
t.Error("failure to match")
}
}
func TestWhoFields(t *testing.T) {
var w whoxFields
if w.Has('a') {
t.Error("zero value of whoxFields must be empty")
}
w = w.Add('a')
if !w.Has('a') {
t.Error("failed to set and get")
}
if w.Has('A') {
t.Error("false positive")
}
if w.Has('o') {
t.Error("false positive")
}
w = w.Add('🐬')
if w.Has('🐬') {
t.Error("should not be able to set invalid who field")
}
w = w.Add('o')
if !w.Has('o') {
t.Error("failed to set and get")
}
w = w.Add('z')
if !w.Has('z') {
t.Error("failed to set and get")
}
}

View File

@ -1,136 +0,0 @@
// Copyright (c) 2019 Shivaram Lingamneni
// released under the MIT license
package cloaks
import (
"net"
"reflect"
"testing"
)
func assertEqual(supplied, expected interface{}, t *testing.T) {
if !reflect.DeepEqual(supplied, expected) {
t.Errorf("expected %v but got %v", expected, supplied)
}
}
func easyParseIP(ipstr string) (result net.IP) {
result = net.ParseIP(ipstr)
if result == nil {
panic(ipstr)
}
return
}
func cloakConfForTesting() CloakConfig {
config := CloakConfig{
Enabled: true,
Netname: "oragono",
secret: "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
CidrLenIPv4: 32,
CidrLenIPv6: 64,
NumBits: 80,
}
config.Initialize()
return config
}
func TestCloakDeterminism(t *testing.T) {
config := cloakConfForTesting()
v4ip := easyParseIP("8.8.8.8").To4()
assertEqual(config.ComputeCloak(v4ip), "d2z5guriqhzwazyr.oragono", t)
// use of the 4-in-6 mapping should not affect the cloak
v6mappedIP := v4ip.To16()
assertEqual(config.ComputeCloak(v6mappedIP), "d2z5guriqhzwazyr.oragono", t)
v6ip := easyParseIP("2001:0db8::1")
assertEqual(config.ComputeCloak(v6ip), "w7ren6nxii6f3i3d.oragono", t)
// same CIDR, so same cloak:
v6ipsamecidr := easyParseIP("2001:0db8::2")
assertEqual(config.ComputeCloak(v6ipsamecidr), "w7ren6nxii6f3i3d.oragono", t)
v6ipdifferentcidr := easyParseIP("2001:0db9::1")
// different CIDR, different cloak:
assertEqual(config.ComputeCloak(v6ipdifferentcidr), "ccmptyrjwsxv4f4d.oragono", t)
// cloak values must be sensitive to changes in the secret key
config.SetSecret("HJcXK4lLawxBE4-9SIdPji_21YiL3N5r5f5-SPNrGVY")
assertEqual(config.ComputeCloak(v4ip), "4khy3usk8mfu42pe.oragono", t)
assertEqual(config.ComputeCloak(v6mappedIP), "4khy3usk8mfu42pe.oragono", t)
assertEqual(config.ComputeCloak(v6ip), "mxpk3c83vdxkek9j.oragono", t)
assertEqual(config.ComputeCloak(v6ipsamecidr), "mxpk3c83vdxkek9j.oragono", t)
}
func TestCloakShortv4Cidr(t *testing.T) {
config := CloakConfig{
Enabled: true,
Netname: "oragono",
secret: "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
CidrLenIPv4: 24,
CidrLenIPv6: 64,
NumBits: 60,
}
config.Initialize()
v4ip := easyParseIP("8.8.8.8")
assertEqual(config.ComputeCloak(v4ip), "3cay3zc72tnui.oragono", t)
v4ipsamecidr := easyParseIP("8.8.8.9")
assertEqual(config.ComputeCloak(v4ipsamecidr), "3cay3zc72tnui.oragono", t)
}
func TestCloakZeroBits(t *testing.T) {
config := cloakConfForTesting()
config.NumBits = 0
config.Netname = "example.com"
config.Initialize()
v4ip := easyParseIP("8.8.8.8").To4()
assertEqual(config.ComputeCloak(v4ip), "example.com", t)
}
func TestCloakDisabled(t *testing.T) {
config := cloakConfForTesting()
config.Enabled = false
v4ip := easyParseIP("8.8.8.8").To4()
assertEqual(config.ComputeCloak(v4ip), "", t)
}
func BenchmarkCloaks(b *testing.B) {
config := cloakConfForTesting()
v6ip := easyParseIP("2001:0db8::1")
b.ResetTimer()
for i := 0; i < b.N; i++ {
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

@ -1,95 +0,0 @@
// Copyright (c) 2019 Shivaram Lingamneni
package cloaks
import (
"fmt"
"net"
"crypto/sha3"
"github.com/ergochat/ergo/irc/utils"
)
type CloakConfig struct {
Enabled bool
EnabledForAlwaysOn bool `yaml:"enabled-for-always-on"`
Netname string
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
NumBits int `yaml:"num-bits"`
LegacySecretValue string `yaml:"secret"`
secret string
numBytes int
ipv4Mask net.IPMask
ipv6Mask net.IPMask
}
func (cloakConfig *CloakConfig) Initialize() {
// sanity checks:
numBits := cloakConfig.NumBits
if 0 == numBits {
numBits = 64
} else if 256 < numBits {
numBits = 256
}
// derived values:
cloakConfig.numBytes = numBits / 8
// round up to the nearest byte
if numBits%8 != 0 {
cloakConfig.numBytes += 1
}
cloakConfig.ipv4Mask = net.CIDRMask(cloakConfig.CidrLenIPv4, 32)
cloakConfig.ipv6Mask = net.CIDRMask(cloakConfig.CidrLenIPv6, 128)
}
func (cloakConfig *CloakConfig) SetSecret(secret string) {
cloakConfig.secret = secret
}
// simple cloaking algorithm: normalize the IP to its CIDR,
// then hash the resulting bytes with a secret key,
// then truncate to the desired length, b32encode, and append the fake TLD.
func (config *CloakConfig) ComputeCloak(ip net.IP) string {
if !config.Enabled {
return ""
} else if config.NumBits == 0 || config.secret == "" {
return config.Netname
}
var masked net.IP
v4ip := ip.To4()
if v4ip != nil {
masked = v4ip.Mask(config.ipv4Mask)
} else {
masked = ip.Mask(config.ipv6Mask)
}
return config.macAndCompose(masked)
}
func (config *CloakConfig) macAndCompose(b []byte) string {
// SHA3(K || M):
// https://crypto.stackexchange.com/questions/17735/is-hmac-needed-for-a-sha-3-based-mac
input := make([]byte, len(config.secret)+len(b))
copy(input, config.secret[:])
copy(input[len(config.secret):], b)
digest := sha3.Sum512(input)
b32digest := utils.B32Encoder.EncodeToString(digest[:config.numBytes])
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,96 +6,73 @@
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
usablePreReg bool oper bool
allowedInBatch bool // allowed in client-to-server batches usablePreReg bool
minParams int leaveClientActive bool // if true, leaves the client active time alone. reversed because we can't default a struct element to True
capabs []string leaveClientIdle bool
} minParams int
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, msg ircmsg.IrcMessage) bool {
rb := NewResponseBuffer(session) if !client.registered && !cmd.usablePreReg {
rb.Label = GetLabel(msg) client.Send(nil, server.name, ERR_NOTREGISTERED, client.nick, client.t("You need to register before you can use that command"))
return false
exiting = func() bool { }
defer rb.Send(true) if cmd.oper && !client.HasMode(modes.Operator) {
client.Send(nil, server.name, ERR_NOPRIVILEGES, client.nick, client.t("Permission Denied - You're not an IRC operator"))
if !client.registered && !cmd.usablePreReg { return false
rb.Add(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command")) }
return false if len(cmd.capabs) > 0 && !client.HasRoleCapabs(cmd.capabs...) {
} client.Send(nil, server.name, ERR_NOPRIVILEGES, client.nick, client.t("Permission Denied"))
if len(cmd.capabs) > 0 && !client.HasRoleCapabs(cmd.capabs...) { return false
rb.Add(nil, server.name, ERR_NOPRIVILEGES, client.Nick(), client.t("Permission Denied")) }
return false if len(msg.Params) < cmd.minParams {
} client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
if len(msg.Params) < cmd.minParams { return false
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), msg.Command, rb.target.t("Not enough parameters"))
return false
}
if session.batch.label != "" && !cmd.allowedInBatch {
rb.Add(nil, server.name, "FAIL", "BATCH", "MULTILINE_INVALID", client.t("Command not allowed during a multiline batch"))
session.EndMultilineBatch("")
return false
}
return cmd.handler(server, client, msg, rb)
}()
// after each command, see if we can send registration to the client
if !exiting && !client.registered {
exiting = server.tryRegister(client, session)
} }
if client.registered { if client.registered {
client.Touch(session) // even if `exiting`, we bump the lastSeen timestamp client.fakelag.Touch()
}
rb := NewResponseBuffer(client)
rb.Label = GetLabel(msg)
exiting := cmd.handler(server, client, msg, rb)
rb.Send()
// after each command, see if we can send registration to the client
if !client.registered {
server.tryRegister(client)
}
if !cmd.leaveClientIdle {
client.Touch()
}
if !cmd.leaveClientActive {
client.Active()
} }
return exiting return exiting
} }
// fake handler for unknown commands (see #994: this ensures the response tags are correct)
var unknownCommand = Command{
handler: unknownCommandHandler,
usablePreReg: true,
}
var invalidUtf8Command = Command{
handler: invalidUtf8Handler,
usablePreReg: true,
}
// Commands holds all commands executable by a client connected to us. // Commands holds all commands executable by a client connected to us.
var Commands map[string]Command var Commands map[string]Command
func init() { func init() {
Commands = map[string]Command{ Commands = map[string]Command{
"ACCEPT": { "ACC": {
handler: acceptHandler, handler: accHandler,
minParams: 1, minParams: 3,
}, },
"AMBIANCE": { "AMBIANCE": {
handler: sceneHandler, handler: sceneHandler,
@ -107,45 +84,31 @@ func init() {
minParams: 1, minParams: 1,
}, },
"AWAY": { "AWAY": {
handler: awayHandler, handler: awayHandler,
usablePreReg: true, minParams: 0,
minParams: 0,
},
"BATCH": {
handler: batchHandler,
minParams: 1,
allowedInBatch: true,
}, },
"CAP": { "CAP": {
handler: capHandler, handler: capHandler,
usablePreReg: true, usablePreReg: true,
minParams: 1, minParams: 1,
}, },
"CHATHISTORY": { "CHANSERV": {
handler: chathistoryHandler, handler: csHandler,
minParams: 4, minParams: 1,
},
"CS": {
handler: csHandler,
minParams: 1,
}, },
"DEBUG": { "DEBUG": {
handler: debugHandler, handler: debugHandler,
minParams: 1, minParams: 1,
capabs: []string{"rehash"}, oper: true,
},
"DEFCON": {
handler: defconHandler,
capabs: []string{"defcon"},
},
"DEOPER": {
handler: deoperHandler,
minParams: 0,
}, },
"DLINE": { "DLINE": {
handler: dlineHandler, handler: dlineHandler,
minParams: 1, minParams: 1,
capabs: []string{"ban"}, oper: true,
},
"EXTJWT": {
handler: extjwtHandler,
minParams: 1,
}, },
"HELP": { "HELP": {
handler: helpHandler, handler: helpHandler,
@ -155,10 +118,6 @@ func init() {
handler: helpHandler, handler: helpHandler,
minParams: 0, minParams: 0,
}, },
"HISTORY": {
handler: historyHandler,
minParams: 1,
},
"INFO": { "INFO": {
handler: infoHandler, handler: infoHandler,
}, },
@ -170,10 +129,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 +140,13 @@ func init() {
"KILL": { "KILL": {
handler: killHandler, handler: killHandler,
minParams: 1, minParams: 1,
capabs: []string{"kill"}, oper: true,
capabs: []string{"oper: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 +161,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,
@ -235,10 +182,13 @@ func init() {
usablePreReg: true, usablePreReg: true,
minParams: 1, minParams: 1,
}, },
"NICKSERV": {
handler: nsHandler,
minParams: 1,
},
"NOTICE": { "NOTICE": {
handler: messageHandler, handler: noticeHandler,
minParams: 2, minParams: 2,
allowedInBatch: true,
}, },
"NPC": { "NPC": {
handler: npcHandler, handler: npcHandler,
@ -248,9 +198,13 @@ func init() {
handler: npcaHandler, handler: npcaHandler,
minParams: 3, minParams: 3,
}, },
"NS": {
handler: nsHandler,
minParams: 1,
},
"OPER": { "OPER": {
handler: operHandler, handler: operHandler,
minParams: 1, minParams: 2,
}, },
"PART": { "PART": {
handler: partHandler, handler: partHandler,
@ -261,47 +215,40 @@ 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,
minParams: 1, minParams: 1,
leaveClientActive: true,
}, },
"PONG": { "PONG": {
handler: pongHandler, handler: pongHandler,
usablePreReg: true, usablePreReg: true,
minParams: 1, minParams: 1,
leaveClientActive: true,
}, },
"PRIVMSG": { "PRIVMSG": {
handler: messageHandler, handler: privmsgHandler,
minParams: 2, minParams: 2,
allowedInBatch: true,
}, },
"RELAYMSG": { "PROXY": {
handler: relaymsgHandler, handler: proxyHandler,
minParams: 3,
},
"REGISTER": {
handler: registerHandler,
minParams: 3,
usablePreReg: true, usablePreReg: true,
minParams: 5,
}, },
"RENAME": { "RENAME": {
handler: renameHandler, handler: renameHandler,
minParams: 2, minParams: 2,
}, },
"SAJOIN": { "RESUME": {
handler: sajoinHandler, handler: resumeHandler,
minParams: 1, usablePreReg: true,
capabs: []string{"sajoin"}, minParams: 1,
}, },
"SANICK": { "SANICK": {
handler: sanickHandler, handler: sanickHandler,
minParams: 2, minParams: 2,
capabs: []string{"samode"}, oper: true,
}, },
"SAMODE": { "SAMODE": {
handler: modeHandler, handler: modeHandler,
@ -312,15 +259,8 @@ func init() {
handler: sceneHandler, handler: sceneHandler,
minParams: 2, minParams: 2,
}, },
"SETNAME": {
handler: setnameHandler,
minParams: 1,
},
"SUMMON": {
handler: summonHandler,
},
"TAGMSG": { "TAGMSG": {
handler: messageHandler, handler: tagmsgHandler,
minParams: 1, minParams: 1,
}, },
"QUIT": { "QUIT": {
@ -328,14 +268,11 @@ 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,
capabs: []string{"rehash"}, oper: true,
capabs: []string{"oper:rehash"},
}, },
"TIME": { "TIME": {
handler: timeHandler, handler: timeHandler,
@ -345,24 +282,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,
@ -373,14 +301,6 @@ func init() {
handler: userhostHandler, handler: userhostHandler,
minParams: 1, minParams: 1,
}, },
"USERS": {
handler: usersHandler,
},
"VERIFY": {
handler: verifyHandler,
usablePreReg: true,
minParams: 2,
},
"VERSION": { "VERSION": {
handler: versionHandler, handler: versionHandler,
minParams: 0, minParams: 0,
@ -390,10 +310,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,
@ -406,11 +322,5 @@ func init() {
handler: whowasHandler, handler: whowasHandler,
minParams: 1, minParams: 1,
}, },
"ZNC": {
handler: zncHandler,
minParams: 1,
},
} }
initializeServices()
} }

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,277 +4,155 @@
package connection_limits package connection_limits
import ( import (
"crypto/md5"
"errors" "errors"
"fmt" "fmt"
"net"
"sync" "sync"
"time"
"github.com/ergochat/ergo/irc/flatip"
"github.com/ergochat/ergo/irc/utils"
) )
var (
ErrLimitExceeded = errors.New("too many concurrent connections")
ErrThrottleExceeded = errors.New("too many recent connection attempts")
)
type CustomLimitConfig struct {
Nets []string
MaxConcurrent int `yaml:"max-concurrent-connections"`
MaxPerWindow int `yaml:"max-connections-per-window"`
}
// tuples the key-value pair of a CIDR and its custom limit/throttle values
type customLimit struct {
name [16]byte
customID string // operator-configured identifier for a custom net
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.
// rawLimiterConfig contains all the YAML-visible fields;
// LimiterConfig contains additional denormalized private fields
type rawLimiterConfig struct {
Count bool
MaxConcurrent int `yaml:"max-concurrent-connections"`
Throttle bool
Window time.Duration
MaxPerWindow int `yaml:"max-connections-per-window"`
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
Exempted []string
CustomLimits map[string]CustomLimitConfig `yaml:"custom-limits"`
}
type LimiterConfig struct { type LimiterConfig struct {
rawLimiterConfig Enabled bool
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
exemptedNets []flatip.IPNet CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
customLimits []customLimit ConnsPerSubnet int `yaml:"connections-per-subnet"`
IPsPerSubnet int `yaml:"ips-per-subnet"` // legacy name for ConnsPerSubnet
Exempted []string
} }
func (config *LimiterConfig) UnmarshalYAML(unmarshal func(interface{}) error) (err error) { var (
if err = unmarshal(&config.rawLimiterConfig); err != nil { errTooManyClients = errors.New("Too many clients in subnet")
return err )
}
return config.postprocess()
}
func (config *LimiterConfig) postprocess() (err error) {
exemptedNets, err := utils.ParseNetList(config.Exempted)
if err != nil {
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 {
nets := make([]flatip.IPNet, len(customLimitConf.Nets))
for i, netStr := range customLimitConf.Nets {
normalizedNet, err := flatip.ParseToNormalizedNet(netStr)
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{
maxConcurrent: customLimitConf.MaxConcurrent,
maxPerWindow: customLimitConf.MaxPerWindow,
name: md5.Sum([]byte(identifier)),
customID: identifier,
nets: nets,
})
}
return nil
}
// Limiter manages the automated client connection limits. // Limiter manages the automated client connection limits.
type Limiter struct { type Limiter struct {
sync.Mutex sync.Mutex
config *LimiterConfig enabled bool
ipv4Mask net.IPMask
ipv6Mask net.IPMask
// subnetLimit is the maximum number of clients per subnet
subnetLimit int
// population holds IP -> count of clients connected from there
population map[string]int
// IP/CIDR -> count of clients connected from there: // exemptedIPs holds IPs that are exempt from limits
limiter map[limiterKey]int exemptedIPs map[string]bool
// IP/CIDR -> throttle state: // exemptedNets holds networks that are exempt from limits
throttler map[limiterKey]ThrottleDetails exemptedNets []net.IPNet
} }
// addrToKey canonicalizes `addr` to a string key, and returns // maskAddr masks the given IPv4/6 address with our cidr limit masks.
// the relevant connection limit and throttle max-per-window values func (cl *Limiter) maskAddr(addr net.IP) net.IP {
func (cl *Limiter) addrToKey(addr flatip.IP) (key limiterKey, customID string, limit int, throttle int) { if addr.To4() == nil {
for _, custom := range cl.config.customLimits { // IPv6 addr
for _, net := range custom.nets { addr = addr.Mask(cl.ipv6Mask)
if net.Contains(addr) {
return limiterKey{maskedIP: custom.name, prefixLen: 0}, custom.customID, custom.maxConcurrent, custom.maxPerWindow
}
}
}
var prefixLen int
if addr.IsIPv4() {
prefixLen = cl.config.CidrLenIPv4
addr = addr.Mask(prefixLen, 32)
prefixLen += 96
} else { } else {
prefixLen = cl.config.CidrLenIPv6 // IPv4 addr
addr = addr.Mask(prefixLen, 128) addr = addr.Mask(cl.ipv4Mask)
} }
return limiterKey{maskedIP: addr, prefixLen: uint8(prefixLen)}, "", cl.config.MaxConcurrent, cl.config.MaxPerWindow return addr
} }
// 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 { // 'force' is used to add already-existing clients (i.e. ones that are already on the network).
func (cl *Limiter) AddClient(addr net.IP, force bool) 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 if !cl.enabled {
if flatip.IPInNets(addr, cl.config.exemptedNets) {
return nil return nil
} }
addrString, _, maxConcurrent, maxPerWindow := cl.addrToKey(addr) // check exempted lists
// we don't track populations for exempted addresses or nets - this is by design
// check limiter if cl.exemptedIPs[addr.String()] {
var count int return nil
if cl.config.Count { }
count = cl.limiter[addrString] + 1 for _, ex := range cl.exemptedNets {
if count > maxConcurrent { if ex.Contains(addr) {
return ErrLimitExceeded return nil
} }
} }
if cl.config.Throttle { // check population
details := cl.throttler[addrString] // retrieve mutable throttle state from the map cl.maskAddr(addr)
// add in constant state to process the limiting operation addrString := addr.String()
g := GenericThrottle{
ThrottleDetails: details, if cl.population[addrString]+1 > cl.subnetLimit && !force {
Duration: cl.config.Window, return errTooManyClients
Limit: maxPerWindow,
}
throttled, _ := g.Touch() // actually check the limit
cl.throttler[addrString] = g.ThrottleDetails // store modified mutable state
if throttled {
// back out the limiter add
return ErrThrottleExceeded
}
} }
// success, record in limiter cl.population[addrString] = cl.population[addrString] + 1
if cl.config.Count {
cl.limiter[addrString] = count
}
return nil return nil
} }
// 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.enabled {
return return
} }
addrString, _, _, _ := cl.addrToKey(addr) addrString := addr.String()
count := cl.limiter[addrString] cl.population[addrString] = cl.population[addrString] - 1
count -= 1
if count < 0 { // safety limiter
count = 0 if cl.population[addrString] < 0 {
cl.population[addrString] = 0
} }
cl.limiter[addrString] = count
} }
type LimiterStatus struct { // NewLimiter returns a new connection limit handler.
Exempt bool // The handler is functional, but disabled; it can be enabled via `ApplyConfig`.
func NewLimiter() *Limiter {
var cl Limiter
Count int // initialize empty population; all other state is configurable
MaxCount int cl.population = make(map[string]int)
Throttle int return &cl
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
func (cl *Limiter) ResetThrottle(addr flatip.IP) {
cl.Lock()
defer cl.Unlock()
if !cl.config.Throttle || flatip.IPInNets(addr, cl.config.exemptedNets) {
return
}
addrString, _, _, _ := cl.addrToKey(addr)
delete(cl.throttler, addrString)
} }
// ApplyConfig atomically applies a config update to a connection limit handler // ApplyConfig atomically applies a config update to a connection limit handler
func (cl *Limiter) ApplyConfig(config *LimiterConfig) { func (cl *Limiter) ApplyConfig(config LimiterConfig) error {
// assemble exempted nets
exemptedIPs := make(map[string]bool)
var exemptedNets []net.IPNet
for _, cidr := range config.Exempted {
ipaddr := net.ParseIP(cidr)
_, netaddr, err := net.ParseCIDR(cidr)
if ipaddr == nil && err != nil {
return fmt.Errorf("Could not parse exempted IP/network [%s]", cidr)
}
if ipaddr != nil {
exemptedIPs[ipaddr.String()] = true
} else {
exemptedNets = append(exemptedNets, *netaddr)
}
}
cl.Lock() cl.Lock()
defer cl.Unlock() defer cl.Unlock()
if cl.limiter == nil { cl.enabled = config.Enabled
cl.limiter = make(map[limiterKey]int) cl.ipv4Mask = net.CIDRMask(config.CidrLenIPv4, 32)
} cl.ipv6Mask = net.CIDRMask(config.CidrLenIPv6, 128)
if cl.throttler == nil { // subnetLimit is explicitly NOT capped at a minimum of one.
cl.throttler = make(map[limiterKey]ThrottleDetails) // this is so that CL config can be used to allow ONLY clients from exempted IPs/nets
cl.subnetLimit = config.ConnsPerSubnet
// but: check if the current key was left unset, but the legacy was set:
if cl.subnetLimit == 0 && config.IPsPerSubnet != 0 {
cl.subnetLimit = config.IPsPerSubnet
} }
cl.exemptedIPs = exemptedIPs
cl.exemptedNets = exemptedNets
cl.config = config return nil
} }

View File

@ -1,95 +0,0 @@
// Copyright (c) 2018 Shivaram Lingamneni
// released under the MIT license
package connection_limits
import (
"crypto/md5"
"testing"
"time"
"github.com/ergochat/ergo/irc/flatip"
)
func easyParseIP(ipstr string) (result flatip.IP) {
result, err := flatip.ParseIP(ipstr)
if err != nil {
panic(err)
}
return
}
var baseConfig = LimiterConfig{
rawLimiterConfig: rawLimiterConfig{
Count: true,
MaxConcurrent: 4,
Throttle: true,
Window: time.Second * 600,
MaxPerWindow: 8,
CidrLenIPv4: 32,
CidrLenIPv6: 64,
Exempted: []string{"localhost"},
CustomLimits: map[string]CustomLimitConfig{
"google": {
Nets: []string{"8.8.0.0/16"},
MaxConcurrent: 128,
MaxPerWindow: 256,
},
},
},
}
func TestKeying(t *testing.T) {
config := baseConfig
config.postprocess()
var limiter Limiter
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"))
assertEqual(key.prefixLen, uint8(128), t)
assertEqual(key.maskedIP[12:], []byte{1, 1, 1, 1}, t)
assertEqual(maxConc, 4, t)
assertEqual(maxWin, 8, t)
testIPv6 := easyParseIP("2607:5301:201:3100::7426")
key, _, maxConc, maxWin = limiter.addrToKey(testIPv6)
assertEqual(key.prefixLen, uint8(64), t)
assertEqual(flatip.IP(key.maskedIP), easyParseIP("2607:5301:201:3100::"), t)
assertEqual(maxConc, 4, t)
assertEqual(maxWin, 8, t)
key, _, maxConc, maxWin = limiter.addrToKey(easyParseIP("8.8.4.4"))
assertEqual(key.prefixLen, uint8(0), t)
assertEqual([16]byte(key.maskedIP), md5.Sum([]byte("google")), t)
assertEqual(maxConc, 128, t)
assertEqual(maxWin, 256, t)
}
func TestLimits(t *testing.T) {
regularIP := easyParseIP("2607:5301:201:3100::7426")
config := baseConfig
config.postprocess()
var limiter Limiter
limiter.ApplyConfig(&config)
for i := 0; i < 4; i++ {
err := limiter.AddClient(regularIP)
if err != nil {
t.Errorf("ip should not be blocked, but %v", err)
}
}
err := limiter.AddClient(regularIP)
if err != ErrLimitExceeded {
t.Errorf("ip should be blocked, but %v", err)
}
limiter.RemoveClient(regularIP)
err = limiter.AddClient(regularIP)
if err != nil {
t.Errorf("ip should not be blocked, but %v", err)
}
}

View File

@ -4,48 +4,178 @@
package connection_limits package connection_limits
import ( import (
"fmt"
"net"
"sync"
"time" "time"
) )
// ThrottlerConfig controls the automated connection throttling.
type ThrottlerConfig struct {
Enabled bool
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
ConnectionsPerCidr int `yaml:"max-connections"`
DurationString string `yaml:"duration"`
Duration time.Duration `yaml:"duration-time"`
BanDurationString string `yaml:"ban-duration"`
BanDuration time.Duration
BanMessage string `yaml:"ban-message"`
Exempted []string
}
// ThrottleDetails holds the connection-throttling details for a subnet/IP. // ThrottleDetails holds the connection-throttling details for a subnet/IP.
type ThrottleDetails struct { type ThrottleDetails struct {
Start time.Time Start time.Time
Count int ClientCount int
} }
// GenericThrottle allows enforcing limits of the form // Throttler manages automated client connection throttling.
// "at most X events per time window of duration Y" type Throttler struct {
type GenericThrottle struct { sync.RWMutex
ThrottleDetails // variable state: what events have been seen
// these are constant after creation: enabled bool
Duration time.Duration // window length to consider ipv4Mask net.IPMask
Limit int // number of events allowed per window ipv6Mask net.IPMask
subnetLimit int
duration time.Duration
population map[string]ThrottleDetails
// used by the server to ban clients that go over this limit
banDuration time.Duration
banMessage string
// exemptedIPs holds IPs that are exempt from limits
exemptedIPs map[string]bool
// exemptedNets holds networks that are exempt from limits
exemptedNets []net.IPNet
} }
// Touch checks whether an additional event is allowed: // maskAddr masks the given IPv4/6 address with our cidr limit masks.
// it either denies it (by returning false) or allows it (by returning true) func (ct *Throttler) maskAddr(addr net.IP) net.IP {
// and records it if addr.To4() == nil {
func (g *GenericThrottle) Touch() (throttled bool, remainingTime time.Duration) { // IPv6 addr
return g.touch(time.Now().UTC()) addr = addr.Mask(ct.ipv6Mask)
}
func (g *GenericThrottle) touch(now time.Time) (throttled bool, remainingTime time.Duration) {
if g.Limit == 0 {
return // limit of 0 disables throttling
}
elapsed := now.Sub(g.Start)
if elapsed > g.Duration {
// reset window, record the operation
g.Start = now
g.Count = 1
return false, 0
} else if g.Count >= g.Limit {
// we are throttled
return true, g.Start.Add(g.Duration).Sub(now)
} else { } else {
// we are not throttled, record the operation // IPv4 addr
g.Count += 1 addr = addr.Mask(ct.ipv4Mask)
return false, 0
} }
return addr
}
// ResetFor removes any existing count for the given address.
func (ct *Throttler) ResetFor(addr net.IP) {
ct.Lock()
defer ct.Unlock()
if !ct.enabled {
return
}
// remove
ct.maskAddr(addr)
addrString := addr.String()
delete(ct.population, addrString)
}
// AddClient introduces a new client connection if possible. If we can't, throws an error instead.
func (ct *Throttler) AddClient(addr net.IP) error {
ct.Lock()
defer ct.Unlock()
if !ct.enabled {
return nil
}
// check exempted lists
if ct.exemptedIPs[addr.String()] {
return nil
}
for _, ex := range ct.exemptedNets {
if ex.Contains(addr) {
return nil
}
}
// check throttle
ct.maskAddr(addr)
addrString := addr.String()
details, exists := ct.population[addrString]
if !exists || details.Start.Add(ct.duration).Before(time.Now()) {
details = ThrottleDetails{
Start: time.Now(),
}
}
if details.ClientCount+1 > ct.subnetLimit {
return errTooManyClients
}
details.ClientCount++
ct.population[addrString] = details
return nil
}
func (ct *Throttler) BanDuration() time.Duration {
ct.RLock()
defer ct.RUnlock()
return ct.banDuration
}
func (ct *Throttler) BanMessage() string {
ct.RLock()
defer ct.RUnlock()
return ct.banMessage
}
// NewThrottler returns a new client connection throttler.
// The throttler is functional, but disabled; it can be enabled via `ApplyConfig`.
func NewThrottler() *Throttler {
var ct Throttler
// initialize empty population; all other state is configurable
ct.population = make(map[string]ThrottleDetails)
return &ct
}
// ApplyConfig atomically applies a config update to a throttler
func (ct *Throttler) ApplyConfig(config ThrottlerConfig) error {
// assemble exempted nets
exemptedIPs := make(map[string]bool)
var exemptedNets []net.IPNet
for _, cidr := range config.Exempted {
ipaddr := net.ParseIP(cidr)
_, netaddr, err := net.ParseCIDR(cidr)
if ipaddr == nil && err != nil {
return fmt.Errorf("Could not parse exempted IP/network [%s]", cidr)
}
if ipaddr != nil {
exemptedIPs[ipaddr.String()] = true
} else {
exemptedNets = append(exemptedNets, *netaddr)
}
}
ct.Lock()
defer ct.Unlock()
ct.enabled = config.Enabled
ct.ipv4Mask = net.CIDRMask(config.CidrLenIPv4, 32)
ct.ipv6Mask = net.CIDRMask(config.CidrLenIPv6, 128)
ct.subnetLimit = config.ConnectionsPerCidr
ct.duration = config.Duration
ct.banDuration = config.BanDuration
ct.banMessage = config.BanMessage
ct.exemptedIPs = exemptedIPs
ct.exemptedNets = exemptedNets
return nil
} }

View File

@ -1,123 +0,0 @@
// Copyright (c) 2018 Shivaram Lingamneni
// released under the MIT license
package connection_limits
import (
"reflect"
"testing"
"time"
)
func assertEqual(supplied, expected interface{}, t *testing.T) {
if !reflect.DeepEqual(supplied, expected) {
t.Errorf("expected %v but got %v", expected, supplied)
}
}
func TestGenericThrottle(t *testing.T) {
minute, _ := time.ParseDuration("1m")
second, _ := time.ParseDuration("1s")
zero, _ := time.ParseDuration("0s")
throttler := GenericThrottle{
Duration: minute,
Limit: 2,
}
now := time.Now()
throttled, remaining := throttler.touch(now)
assertEqual(throttled, false, t)
assertEqual(remaining, zero, t)
now = now.Add(second)
throttled, remaining = throttler.touch(now)
assertEqual(throttled, false, t)
assertEqual(remaining, zero, t)
now = now.Add(second)
throttled, remaining = throttler.touch(now)
assertEqual(throttled, true, t)
assertEqual(remaining, 58*second, t)
now = now.Add(minute)
throttled, remaining = throttler.touch(now)
assertEqual(throttled, false, t)
assertEqual(remaining, zero, t)
}
func TestGenericThrottleDisabled(t *testing.T) {
minute, _ := time.ParseDuration("1m")
throttler := GenericThrottle{
Duration: minute,
Limit: 0,
}
for i := 0; i < 1024; i += 1 {
throttled, _ := throttler.Touch()
if throttled {
t.Error("disabled throttler should not throttle")
}
}
}
func makeTestThrottler(v4len, v6len int) *Limiter {
minute, _ := time.ParseDuration("1m")
maxConnections := 3
config := LimiterConfig{
rawLimiterConfig: rawLimiterConfig{
Count: false,
Throttle: true,
CidrLenIPv4: v4len,
CidrLenIPv6: v6len,
MaxPerWindow: maxConnections,
Window: minute,
},
}
config.postprocess()
var limiter Limiter
limiter.ApplyConfig(&config)
return &limiter
}
func TestConnectionThrottle(t *testing.T) {
throttler := makeTestThrottler(32, 64)
addr := easyParseIP("8.8.8.8")
for i := 0; i < 3; i += 1 {
err := throttler.AddClient(addr)
assertEqual(err, nil, t)
}
err := throttler.AddClient(addr)
assertEqual(err, ErrThrottleExceeded, t)
}
func TestConnectionThrottleIPv6(t *testing.T) {
throttler := makeTestThrottler(32, 64)
var err error
err = throttler.AddClient(easyParseIP("2001:0db8::1"))
assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("2001:0db8::2"))
assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("2001:0db8::3"))
assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("2001:0db8::4"))
assertEqual(err, ErrThrottleExceeded, t)
}
func TestConnectionThrottleIPv4(t *testing.T) {
throttler := makeTestThrottler(24, 64)
var err error
err = throttler.AddClient(easyParseIP("192.168.1.101"))
assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("192.168.1.102"))
assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("192.168.1.103"))
assertEqual(err, nil, t)
err = throttler.AddClient(easyParseIP("192.168.1.104"))
assertEqual(err, ErrThrottleExceeded, t)
}

View File

@ -1,49 +0,0 @@
// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package connection_limits
import (
"sync"
"time"
)
// TorLimiter is a combined limiter and throttler for use on connections
// proxied from a Tor hidden service (so we don't have meaningful IPs,
// a notion of CIDR width, etc.)
type TorLimiter struct {
sync.Mutex
numConnections int
maxConnections int
throttle GenericThrottle
}
func (tl *TorLimiter) Configure(maxConnections int, duration time.Duration, maxConnectionsPerDuration int) {
tl.Lock()
defer tl.Unlock()
tl.maxConnections = maxConnections
tl.throttle.Duration = duration
tl.throttle.Limit = maxConnectionsPerDuration
}
func (tl *TorLimiter) AddClient() error {
tl.Lock()
defer tl.Unlock()
if tl.maxConnections != 0 && tl.maxConnections <= tl.numConnections {
return ErrLimitExceeded
}
throttled, _ := tl.throttle.Touch()
if throttled {
return ErrThrottleExceeded
}
tl.numConnections += 1
return nil
}
func (tl *TorLimiter) RemoveClient() {
tl.Lock()
tl.numConnections -= 1
tl.Unlock()
}

View File

@ -5,7 +5,20 @@
package irc package irc
import "fmt"
const ( const (
// SemVer is the semantic version of Oragono.
SemVer = "0.11.0-beta"
)
var (
// Commit is the current git commit.
Commit = ""
// Ver is the full version of Oragono, used in responses to clients.
Ver = fmt.Sprintf("oragono-%s", SemVer)
// maxLastArgLength is used to simply cap off the final argument when creating general messages where we need to select a limit. // maxLastArgLength is used to simply cap off the final argument when creating general messages where we need to select a limit.
// for instance, in MONITOR lists, RPL_ISUPPORT lists, etc. // for instance, in MONITOR lists, RPL_ISUPPORT lists, etc.
maxLastArgLength = 400 maxLastArgLength = 400

View File

@ -75,9 +75,8 @@ var unitMap = map[string]int64{
"m": int64(time.Minute), "m": int64(time.Minute),
"h": int64(time.Hour), "h": int64(time.Hour),
"d": int64(time.Hour * 24), "d": int64(time.Hour * 24),
"w": int64(time.Hour * 24 * 7),
"mo": int64(time.Hour * 24 * 30), "mo": int64(time.Hour * 24 * 30),
"y": int64(time.Hour * 24 * 365), "y": int64(time.Hour * 24 * 265),
} }
// ParseDuration parses a duration string. // ParseDuration parses a duration string.
@ -182,18 +181,3 @@ func ParseDuration(s string) (time.Duration, error) {
} }
return time.Duration(d), nil return time.Duration(d), nil
} }
type Duration time.Duration
func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
var orig string
var err error
if err = unmarshal(&orig); err != nil {
return err
}
result, err := ParseDuration(orig)
if err == nil {
*d = Duration(result)
}
return err
}

File diff suppressed because it is too large Load Diff

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

@ -4,81 +4,84 @@
package irc package irc
import ( import (
"encoding/json"
"fmt" "fmt"
"strings" "net"
"sync" "sync"
"time" "time"
"github.com/ergochat/ergo/irc/flatip" "encoding/json"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
) )
const ( const (
keyDlineEntry = "bans.dlinev2 %s" keyDlineEntry = "bans.dline %s"
) )
// IPRestrictTime contains the expiration info about the given IP.
type IPRestrictTime struct {
// Duration is how long this block lasts for.
Duration time.Duration `json:"duration"`
// Expires is when this block expires.
Expires time.Time `json:"expires"`
}
// IsExpired returns true if the time has expired.
func (iptime *IPRestrictTime) IsExpired() bool {
return iptime.Expires.Before(time.Now())
}
// 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.
OperReason string `json:"oper_reason"` OperReason string `json:"oper_reason"`
// OperName is the oper who set the ban. // OperName is the oper who set the ban.
OperName string `json:"oper_name"` OperName string `json:"oper_name"`
// time of ban creation // Time holds details about the duration, if it exists.
TimeCreated time.Time Time *IPRestrictTime `json:"time"`
// duration of the ban; 0 means "permanent"
Duration time.Duration
}
func (info IPBanInfo) timeLeft() time.Duration {
return time.Until(info.TimeCreated.Add(info.Duration))
}
func (info IPBanInfo) TimeLeft() string {
if info.Duration == 0 {
return "indefinite"
} else {
return info.timeLeft().Truncate(time.Second).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 == "" { if info.Time != nil {
reason = "No reason given" message += fmt.Sprintf(" [%s]", info.Time.Duration.String())
}
message = fmt.Sprintf(message, reason)
if info.Duration != 0 {
message += fmt.Sprintf(" [%s]", info.TimeLeft())
} }
return message return message
} }
// dLineAddr contains the address itself and expiration time for a given network.
type dLineAddr struct {
// Address is the address that is blocked.
Address net.IP
// Info contains information on the ban.
Info IPBanInfo
}
// dLineNet contains the net itself and expiration time for a given network.
type dLineNet struct {
// Network is the network that is blocked.
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 // addresses that are dlined
// networks that are dlined: addresses map[string]*dLineAddr
networks map[flatip.IPNet]IPBanInfo // networks that are dlined
// this keeps track of expiration timers for temporary bans networks map[string]*dLineNet
expirationTimers map[flatip.IPNet]*time.Timer
server *Server
} }
// NewDLineManager returns a new DLineManager. // NewDLineManager returns a new DLineManager.
func NewDLineManager(server *Server) *DLineManager { func NewDLineManager() *DLineManager {
var dm DLineManager var dm DLineManager
dm.networks = make(map[flatip.IPNet]IPBanInfo) dm.addresses = make(map[string]*dLineAddr)
dm.expirationTimers = make(map[flatip.IPNet]*time.Timer) dm.networks = make(map[string]*dLineNet)
dm.server = server
dm.loadFromDatastore()
return &dm return &dm
} }
@ -89,192 +92,154 @@ func (dm *DLineManager) AllBans() map[string]IPBanInfo {
dm.RLock() dm.RLock()
defer dm.RUnlock() defer dm.RUnlock()
for key, info := range dm.networks { for name, info := range dm.addresses {
allb[key.HumanReadableString()] = info allb[name] = info.Info
}
for name, info := range dm.networks {
allb[name] = 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, length *IPRestrictTime, reason, operReason, operName string) {
dm.persistenceMutex.Lock() netString := network.String()
defer dm.persistenceMutex.Unlock() dln := dLineNet{
Network: network,
// assemble ban info Info: IPBanInfo{
info := IPBanInfo{ Time: length,
RequireSASL: requireSASL, Reason: reason,
Reason: reason, OperReason: operReason,
OperReason: operReason, OperName: operName,
OperName: operName, },
TimeCreated: time.Now().UTC(),
Duration: duration,
} }
id := dm.addNetworkInternal(network, info)
return dm.persistDline(id, info)
}
func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo) (id flatip.IPNet) {
id = flatnet
var timeLeft time.Duration
if info.Duration != 0 {
timeLeft = info.timeLeft()
if timeLeft <= 0 {
return
}
}
dm.Lock() dm.Lock()
defer dm.Unlock() dm.networks[netString] = &dln
dm.Unlock()
dm.networks[flatnet] = info
dm.cancelTimer(flatnet)
if info.Duration == 0 {
return
}
// set up new expiration timer
timeCreated := info.TimeCreated
processExpiration := func() {
dm.Lock()
defer dm.Unlock()
banInfo, ok := dm.networks[flatnet]
if ok && banInfo.TimeCreated.Equal(timeCreated) {
delete(dm.networks, flatnet)
// TODO(slingamn) here's where we'd remove it from the radix tree
delete(dm.expirationTimers, flatnet)
}
}
dm.expirationTimers[flatnet] = time.AfterFunc(timeLeft, processExpiration)
return
}
func (dm *DLineManager) cancelTimer(flatnet flatip.IPNet) {
oldTimer := dm.expirationTimers[flatnet]
if oldTimer != nil {
oldTimer.Stop()
delete(dm.expirationTimers, flatnet)
}
}
func (dm *DLineManager) persistDline(id flatip.IPNet, info IPBanInfo) error {
// save in datastore
dlineKey := fmt.Sprintf(keyDlineEntry, id.String())
// assemble json from ban info
b, err := json.Marshal(info)
if err != nil {
dm.server.logger.Error("internal", "couldn't marshal d-line", err.Error())
return err
}
bstr := string(b)
var setOptions *buntdb.SetOptions
if info.Duration != 0 {
setOptions = &buntdb.SetOptions{Expires: true, TTL: info.Duration}
}
err = dm.server.store.Update(func(tx *buntdb.Tx) error {
_, _, err := tx.Set(dlineKey, bstr, setOptions)
return err
})
if err != nil {
dm.server.logger.Error("internal", "couldn't store d-line", err.Error())
}
return err
}
func (dm *DLineManager) unpersistDline(id flatip.IPNet) error {
dlineKey := fmt.Sprintf(keyDlineEntry, id.String())
return dm.server.store.Update(func(tx *buntdb.Tx) error {
_, err := tx.Delete(dlineKey)
return err
})
} }
// 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) {
dm.persistenceMutex.Lock() netString := network.String()
defer dm.persistenceMutex.Unlock() dm.Lock()
delete(dm.networks, netString)
dm.Unlock()
}
id := network // AddIP adds an IP address to the blocked list.
func (dm *DLineManager) AddIP(addr net.IP, length *IPRestrictTime, reason, operReason, operName string) {
present := func() bool { addrString := addr.String()
dm.Lock() dla := dLineAddr{
defer dm.Unlock() Address: addr,
_, ok := dm.networks[id] Info: IPBanInfo{
delete(dm.networks, id) Time: length,
dm.cancelTimer(id) Reason: reason,
return ok OperReason: operReason,
}() OperName: operName,
},
if !present {
return errNoExistingBan
} }
dm.Lock()
dm.addresses[addrString] = &dla
dm.Unlock()
}
return dm.unpersistDline(id) // RemoveIP removes an IP from the blocked list.
func (dm *DLineManager) RemoveIP(addr net.IP) {
addrString := addr.String()
dm.Lock()
delete(dm.addresses, addrString)
dm.Unlock()
} }
// 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) {
// check IP addr
addrString := addr.String()
dm.RLock()
addrInfo := dm.addresses[addrString]
dm.RUnlock()
if addrInfo != nil {
if addrInfo.Info.Time != nil {
if addrInfo.Info.Time.IsExpired() {
// ban on IP has expired, remove it from our blocked list
dm.RemoveIP(addr)
} else {
return true, &addrInfo.Info
}
} else {
return true, &addrInfo.Info
}
}
// check networks
doCleanup := false
defer func() {
if doCleanup {
go func() {
dm.Lock()
defer dm.Unlock()
for key, netInfo := range dm.networks {
if netInfo.Info.Time.IsExpired() {
delete(dm.networks, key)
}
}
}()
}
}()
dm.RLock() dm.RLock()
defer dm.RUnlock() defer dm.RUnlock()
// check networks for _, netInfo := range dm.networks {
// TODO(slingamn) use a radix tree as the data plane for this if netInfo.Info.Time != nil && netInfo.Info.Time.IsExpired() {
for flatnet, info := range dm.networks { // expired ban, ignore and clean up later
if flatnet.Contains(addr) { doCleanup = true
return true, info } else if netInfo.Network.Contains(addr) {
return true, &netInfo.Info
} }
} }
// no matches! // no matches!
return return false, nil
} }
func (dm *DLineManager) loadFromDatastore() { func (s *Server) loadDLines() {
dlinePrefix := fmt.Sprintf(keyDlineEntry, "") s.dlines = NewDLineManager()
dm.server.store.View(func(tx *buntdb.Tx) error {
tx.AscendGreaterOrEqual("", dlinePrefix, func(key, value string) bool {
if !strings.HasPrefix(key, dlinePrefix) {
return false
}
// load from datastore
s.store.View(func(tx *buntdb.Tx) error {
//TODO(dan): We could make this safer
tx.AscendKeys("bans.dline *", func(key, value string) bool {
// get address name // get address name
key = strings.TrimPrefix(key, dlinePrefix) key = key[len("bans.dline "):]
// load addr/net // load addr/net
hostNet, err := flatip.ParseToNormalizedNet(key) var hostAddr net.IP
var hostNet *net.IPNet
_, hostNet, err := net.ParseCIDR(key)
if err != nil { if err != nil {
dm.server.logger.Error("internal", "bad dline cidr", err.Error()) hostAddr = net.ParseIP(key)
return true
} }
// load ban info // load ban info
var info IPBanInfo var info IPBanInfo
err = json.Unmarshal([]byte(value), &info) json.Unmarshal([]byte(value), &info)
if err != nil {
dm.server.logger.Error("internal", "bad dline data", err.Error())
return true
}
// set opername if it isn't already set // set opername if it isn't already set
if info.OperName == "" { if info.OperName == "" {
info.OperName = dm.server.name info.OperName = s.name
} }
// add to the server // add to the server
dm.addNetworkInternal(hostNet, info) if hostNet == nil {
s.dlines.AddIP(hostAddr, info.Time, info.Reason, info.OperReason, info.OperName)
} else {
s.dlines.AddNetwork(*hostNet, info.Time, info.Reason, info.OperReason, info.OperName)
}
return true return true // true to continue I guess?
}) })
return nil return nil
}) })
} }
func (s *Server) loadDLines() {
s.dlines = NewDLineManager(s)
}

View File

@ -1,102 +0,0 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package email
import (
"bytes"
"crypto"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
dkim "github.com/emersion/go-msgauth/dkim"
)
var (
ErrMissingFields = errors.New("DKIM config is missing fields")
)
type DKIMConfig struct {
Domain string
Selector string
KeyFile string `yaml:"key-file"`
privKey crypto.Signer
}
func (dkim *DKIMConfig) Enabled() bool {
return dkim.Domain != ""
}
func (dkim *DKIMConfig) Postprocess() (err error) {
if !dkim.Enabled() {
return nil
}
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
}
func parseDKIMPrivKey(input []byte) (crypto.Signer, error) {
if len(input) == 0 {
return nil, errors.New("DKIM private key is empty")
}
// raw ed25519 private key format
if len(input) == ed25519.PrivateKeySize {
return ed25519.PrivateKey(input), nil
}
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) {
options := dkim.SignOptions{
Domain: dkimConfig.Domain,
Selector: dkimConfig.Selector,
Signer: dkimConfig.privKey,
HeaderCanonicalization: dkim.CanonicalizationRelaxed,
BodyCanonicalization: dkim.CanonicalizationRelaxed,
}
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

@ -1,268 +0,0 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package email
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
"os"
"regexp"
"strings"
"time"
"github.com/ergochat/ergo/irc/custime"
"github.com/ergochat/ergo/irc/smtp"
"github.com/ergochat/ergo/irc/utils"
)
var (
ErrBlacklistedAddress = errors.New("Email address is blacklisted")
ErrInvalidAddress = errors.New("Email address is invalid")
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 {
Server string
Port int
Username string
Password string
ImplicitTLS bool `yaml:"implicit-tls"`
}
type MailtoConfig struct {
// legacy config format assumed the use of an MTA/smarthost,
// so server, port, etc. appear directly at top level
// XXX: see https://github.com/go-yaml/yaml/issues/63
MTAConfig `yaml:",inline"`
Enabled bool
Sender string
HeloDomain string `yaml:"helo-domain"`
RequireTLS bool `yaml:"require-tls"`
Protocol string `yaml:"protocol"`
LocalAddress string `yaml:"local-address"`
localAddress net.Addr
VerifyMessageSubject string `yaml:"verify-message-subject"`
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) {
if config.Sender == "" {
return errors.New("Invalid mailto sender address")
}
// check for MTA config fields at top level,
// copy to MTAReal if present
if config.Server != "" && config.MTAReal.Server == "" {
config.MTAReal = config.MTAConfig
}
if config.HeloDomain == "" {
config.HeloDomain = heloDomain
}
if config.AddressBlacklistFile != "" {
config.blacklistRegexes, err = config.processBlacklistFile(config.AddressBlacklistFile)
if err != nil {
return err
}
} else if len(config.AddressBlacklist) != 0 {
config.blacklistRegexes = make([]*regexp.Regexp, 0, len(config.AddressBlacklist))
for _, reg := range config.AddressBlacklist {
compiled, err := config.compileBlacklistEntry(reg)
if err != nil {
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 != "" {
// smarthost, nothing more to validate
return nil
}
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
func lookupMX(domain string) (server string) {
var minPref uint16
results, err := net.LookupMX(domain)
if err != nil {
return
}
for _, result := range results {
if minPref == 0 || result.Pref < minPref {
server, minPref = result.Host, result.Pref
}
}
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) {
recipientLower := strings.ToLower(recipient)
for _, reg := range config.blacklistRegexes {
if reg.MatchString(recipientLower) {
return ErrBlacklistedAddress
}
}
if config.DKIM.Enabled() {
msg, err = DKIMSign(msg, config.DKIM)
if err != nil {
return
}
}
var addr string
var auth smtp.Auth
var implicitTLS bool
if !config.DirectSendingEnabled() {
addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
}
implicitTLS = config.MTAReal.ImplicitTLS
} else {
idx := strings.IndexByte(recipient, '@')
if idx == -1 {
return ErrInvalidAddress
}
mx := lookupMX(recipient[idx+1:])
if mx == "" {
return ErrNoMXRecord
}
addr = fmt.Sprintf("%s:smtp", mx)
}
return smtp.SendMail(
addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg,
config.RequireTLS, implicitTLS, config.Protocol, config.localAddress, config.Timeout,
)
}

View File

@ -5,79 +5,42 @@
package irc package irc
import ( import "errors"
"errors"
"fmt"
"time"
"github.com/ergochat/ergo/irc/utils"
)
// Runtime Errors // Runtime Errors
var ( var (
errAccountAlreadyRegistered = errors.New(`Account already exists`) errAccountAlreadyRegistered = errors.New("Account already exists")
errAccountAlreadyUnregistered = errors.New(`That account name was registered previously and can't be reused`)
errAccountAlreadyVerified = errors.New(`Account is already verified`)
errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname")
errAccountCreation = errors.New("Account could not be created") errAccountCreation = errors.New("Account could not be created")
errAccountDoesNotExist = errors.New("Account does not exist") errAccountDoesNotExist = errors.New("Account does not exist")
errAccountInvalidCredentials = errors.New("Invalid account credentials")
errAccountBadPassphrase = errors.New(`Passphrase contains forbidden characters or is otherwise invalid`)
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
errAccountNotLoggedIn = errors.New("You're not logged into an account") errAccountNotLoggedIn = errors.New("You're not logged into an account")
errAccountAlreadyLoggedIn = errors.New("You're already logged into an account")
errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
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`) errAccountUnverified = errors.New("Account is not yet verified")
errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`) errAccountAlreadyVerified = errors.New("Account is already verified")
errAuthRequired = errors.New("You must be logged into an account to do this") errAccountInvalidCredentials = errors.New("Invalid account credentials")
errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`) errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`) errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account") errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname")
errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`) errCallbackFailed = errors.New("Account verification could not be sent")
errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
errChannelAlreadyRegistered = errors.New("Channel is already registered") errChannelAlreadyRegistered = errors.New("Channel is already registered")
errChannelNotRegistered = errors.New("Channel is not registered") errChannelNameInUse = errors.New("Channel name in use")
errChannelNameInUse = errors.New(`Channel name in use`) errInvalidChannelName = errors.New("Invalid channel name")
errInvalidChannelName = errors.New(`Invalid channel name`)
errMonitorLimitExceeded = errors.New("Monitor limit exceeded") errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
errNickMissing = errors.New("nick missing") errNickMissing = errors.New("nick missing")
errNicknameInvalid = errors.New("invalid nickname")
errNicknameInUse = errors.New("nickname in use") errNicknameInUse = errors.New("nickname in use")
errInsecureReattach = errors.New("insecure reattach")
errNicknameReserved = errors.New("nickname is reserved") errNicknameReserved = errors.New("nickname is reserved")
errNickAccountMismatch = errors.New(`Your nickname must match your account name; try logging out and logging back in with SASL`)
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`) errRenamePrivsNeeded = errors.New("Only chanops can rename channels")
errChannelPurgedAlready = errors.New(`This channel was already purged and cannot be purged again`) errSaslFail = errors.New("SASL failed")
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use") )
errInsufficientPrivs = errors.New("Insufficient privileges")
errInvalidUsername = errors.New("Invalid username") // Socket Errors
errFeatureDisabled = errors.New(`That feature is disabled`) var (
errBanned = errors.New("IP or nickmask banned") errNoPeerCerts = errors.New("Client did not provide a certificate")
errInvalidParams = utils.ErrInvalidParams errNotTLS = errors.New("Not a TLS connection")
errNoVhost = errors.New(`You do not have an approved vhost`) errReadQ = errors.New("ReadQ Exceeded")
errLimitExceeded = errors.New("Limit exceeded")
errNoop = errors.New("Action was a no-op")
errCASFailed = errors.New("Compare-and-swap update of database value failed")
errEmptyCredentials = errors.New("No more credentials are approved")
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")
errTimedOut = errors.New("Operation timed out")
errInvalidUtf8 = errors.New("Message rejected for invalid utf8")
errClientDestroyed = errors.New("Client was already destroyed")
errTooManyChannels = errors.New("You have joined too many channels")
errWrongChannelKey = errors.New("Cannot join password-protected channel without the password")
errInviteOnly = errors.New("Cannot join invite-only channel without an invite")
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
@ -87,18 +50,19 @@ var (
errInvalidCharacter = errors.New("Invalid character") errInvalidCharacter = errors.New("Invalid character")
) )
type CertKeyError struct { // Config Errors
Err error var (
} ErrDatastorePathMissing = errors.New("Datastore path missing")
ErrInvalidCertKeyPair = errors.New("tls cert+key: invalid pair")
func (ck *CertKeyError) Error() string { ErrLimitsAreInsane = errors.New("Limits aren't setup properly, check them and make them sane")
return fmt.Sprintf("Invalid TLS cert/key pair: %v", ck.Err) ErrLineLengthsTooSmall = errors.New("Line lengths must be 512 or greater (check the linelen section under server->limits)")
} ErrLoggerExcludeEmpty = errors.New("Encountered logging type '-' with no type to exclude")
ErrLoggerFilenameMissing = errors.New("Logging configuration specifies 'file' method but 'filename' is empty")
type ThrottleError struct { ErrLoggerHasNoTypes = errors.New("Logger has no types to log")
time.Duration ErrNetworkNameMissing = errors.New("Network name missing")
} ErrNoFingerprintOrPassword = errors.New("Fingerprint or password needs to be specified")
ErrNoListenersDefined = errors.New("Server listening addresses missing")
func (te *ThrottleError) Error() string { ErrOperClassDependencies = errors.New("OperClasses contains a looping dependency, or a class extends from a class that doesn't exist")
return fmt.Sprintf(`Please wait at least %v and try again`, te.Duration.Round(time.Millisecond)) ErrServerNameMissing = errors.New("Server name missing")
} ErrServerNameNotHostname = errors.New("Server name must match the format of a hostname")
)

View File

@ -4,7 +4,6 @@
package irc package irc
import ( import (
"maps"
"time" "time"
) )
@ -25,51 +24,33 @@ const (
// this is intentionally not threadsafe, because it should only be touched // this is intentionally not threadsafe, because it should only be touched
// from the loop that accepts the client's input and runs commands // from the loop that accepts the client's input and runs commands
type Fakelag struct { type Fakelag struct {
config FakelagConfig window time.Duration
suspended bool burstLimit uint
nowFunc func() time.Time throttleMessagesPerWindow uint
sleepFunc func(time.Duration) cooldown time.Duration
nowFunc func() time.Time
sleepFunc func(time.Duration)
state FakelagState state FakelagState
burstCount uint // number of messages sent in the current burst burstCount uint // number of messages sent in the current burst
lastTouch time.Time lastTouch time.Time
} }
func (fl *Fakelag) Initialize(config FakelagConfig) { func NewFakelag(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint, cooldown time.Duration) *Fakelag {
fl.config = config return &Fakelag{
// XXX don't share mutable member CommandBudgets: window: window,
if config.CommandBudgets != nil { burstLimit: burstLimit,
fl.config.CommandBudgets = maps.Clone(config.CommandBudgets) throttleMessagesPerWindow: throttleMessagesPerWindow,
} cooldown: cooldown,
fl.nowFunc = time.Now nowFunc: time.Now,
fl.sleepFunc = time.Sleep sleepFunc: time.Sleep,
fl.state = FakelagBursting state: FakelagBursting,
}
// Idempotently turn off fakelag if it's enabled
func (fl *Fakelag) Suspend() {
if fl.config.Enabled {
fl.suspended = true
fl.config.Enabled = false
}
}
// Idempotently turn fakelag back on if it was previously Suspend'ed
func (fl *Fakelag) Unsuspend() {
if fl.suspended {
fl.config.Enabled = true
fl.suspended = false
} }
} }
// 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 == nil {
return
}
if budget, ok := fl.config.CommandBudgets[command]; ok && budget > 0 {
fl.config.CommandBudgets[command] = budget - 1
return return
} }
@ -80,12 +61,12 @@ func (fl *Fakelag) Touch(command string) {
if fl.state == FakelagBursting { if fl.state == FakelagBursting {
// determine if the previous burst is over // determine if the previous burst is over
if elapsed > fl.config.Cooldown { if elapsed > fl.cooldown {
fl.burstCount = 0 fl.burstCount = 0
} }
fl.burstCount++ fl.burstCount++
if fl.burstCount > fl.config.BurstLimit { if fl.burstCount > fl.burstLimit {
// reset burst window for next time // reset burst window for next time
fl.burstCount = 0 fl.burstCount = 0
// transition to throttling // transition to throttling
@ -97,27 +78,16 @@ func (fl *Fakelag) Touch(command string) {
} }
if fl.state == FakelagThrottled { if fl.state == FakelagThrottled {
if elapsed > fl.config.Cooldown { if elapsed > fl.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.window) / int64(fl.throttleMessagesPerWindow)) - int64(elapsed))
// space them out by at least window/messagesperwindow if sleepDuration < 0 {
sleepDuration = time.Duration((int64(fl.config.Window) / int64(fl.config.MessagesPerWindow)) - int64(elapsed)) sleepDuration = 0
} 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 {
fl.sleepFunc(sleepDuration)
// the touch time should take into account the time we slept
fl.lastTouch = fl.nowFunc()
} }
fl.sleepFunc(sleepDuration)
} }
} }

View File

@ -40,27 +40,20 @@ func (mt *mockTime) lastSleep() (slept bool, duration time.Duration) {
} }
func newFakelagForTesting(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint, cooldown time.Duration) (*Fakelag, *mockTime) { func newFakelagForTesting(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint, cooldown time.Duration) (*Fakelag, *mockTime) {
fl := Fakelag{} fl := NewFakelag(window, burstLimit, throttleMessagesPerWindow, cooldown)
fl.config = FakelagConfig{
Enabled: true,
Window: window,
BurstLimit: burstLimit,
MessagesPerWindow: throttleMessagesPerWindow,
Cooldown: cooldown,
}
mt := new(mockTime) mt := new(mockTime)
mt.now, _ = time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2006") mt.now, _ = time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2006")
mt.lastCheckedSleep = -1 mt.lastCheckedSleep = -1
fl.nowFunc = mt.Now fl.nowFunc = mt.Now
fl.sleepFunc = mt.Sleep fl.sleepFunc = mt.Sleep
return &fl, mt return fl, mt
} }
func TestFakelag(t *testing.T) { 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 +62,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 +70,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")
} }
@ -90,19 +83,17 @@ func TestFakelag(t *testing.T) {
t.Fatalf("incorrect sleep time: %v != %v", expected, duration) t.Fatalf("incorrect sleep time: %v != %v", expected, duration)
} }
// 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")
} }
slept, duration = mt.lastSleep() slept, duration = mt.lastSleep()
expected, _ = time.ParseDuration("500ms") if duration != interval {
if duration != expected { t.Fatalf("incorrect sleep time: %v != %v", interval, duration)
t.Fatalf("incorrect sleep time: %v != %v", duration, expected)
} }
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 +103,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")
} }
@ -121,35 +112,3 @@ func TestFakelag(t *testing.T) {
t.Fatalf("should not have slept") t.Fatalf("should not have slept")
} }
} }
func TestSuspend(t *testing.T) {
window, _ := time.ParseDuration("1s")
fl, _ := newFakelagForTesting(window, 3, 2, window)
assertEqual(fl.config.Enabled, true)
// suspend idempotently disables
fl.Suspend()
assertEqual(fl.config.Enabled, false)
fl.Suspend()
assertEqual(fl.config.Enabled, false)
// unsuspend idempotently enables
fl.Unsuspend()
assertEqual(fl.config.Enabled, true)
fl.Unsuspend()
assertEqual(fl.config.Enabled, true)
fl.Suspend()
assertEqual(fl.config.Enabled, false)
fl2, _ := newFakelagForTesting(window, 3, 2, window)
fl2.config.Enabled = false
// if we were never enabled, suspend and unsuspend are both no-ops
fl2.Suspend()
assertEqual(fl2.config.Enabled, false)
fl2.Suspend()
assertEqual(fl2.config.Enabled, false)
fl2.Unsuspend()
assertEqual(fl2.config.Enabled, false)
fl2.Unsuspend()
assertEqual(fl2.config.Enabled, false)
}

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
}

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