mirror of
https://github.com/ergochat/ergo.git
synced 2025-08-05 20:27:48 +02:00
Compare commits
No commits in common. "master" and "v0.11.0-beta" have entirely different histories.
master
...
v0.11.0-be
@ -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
2
.gitattributes
vendored
@ -1,2 +0,0 @@
|
||||
vendor/* linguist-vendored
|
||||
languages/* linguist-vendored
|
32
.github/workflows/build.yml
vendored
32
.github/workflows/build.yml
vendored
@ -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"
|
48
.github/workflows/docker-image.yml
vendored
48
.github/workflows/docker-image.yml
vendored
@ -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
10
.gitignore
vendored
@ -95,7 +95,7 @@ _testmain.go
|
||||
*.out
|
||||
|
||||
|
||||
### custom ###
|
||||
### Oragono ###
|
||||
/_site/
|
||||
/.vscode/*
|
||||
/ircd*
|
||||
@ -103,11 +103,9 @@ _testmain.go
|
||||
/web.*
|
||||
/ssl.*
|
||||
/tls.*
|
||||
/ergo
|
||||
/oragono
|
||||
/build/*
|
||||
_test
|
||||
ergo.prof
|
||||
ergo.mprof
|
||||
oragono.prof
|
||||
oragono.mprof
|
||||
/dist
|
||||
*.pem
|
||||
.dccache
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -1,3 +1,3 @@
|
||||
[submodule "irctest"]
|
||||
path = irctest
|
||||
url = https://github.com/ergochat/irctest
|
||||
[submodule "vendor"]
|
||||
path = vendor
|
||||
url = https://github.com/oragono/oragono-vendor.git
|
||||
|
@ -1,82 +1,49 @@
|
||||
# .goreleaser.yml
|
||||
# Build customization
|
||||
version: 2
|
||||
project_name: ergo
|
||||
project_name: oragono
|
||||
builds:
|
||||
- main: ergo.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
binary: ergo
|
||||
- main: oragono.go
|
||||
binary: oragono
|
||||
goos:
|
||||
- linux
|
||||
- freebsd
|
||||
- windows
|
||||
- darwin
|
||||
- freebsd
|
||||
- openbsd
|
||||
- plan9
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: riscv64
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
- goos: freebsd
|
||||
goarch: riscv64
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: riscv64
|
||||
- goos: plan9
|
||||
goarch: arm
|
||||
- goos: plan9
|
||||
goarch: arm64
|
||||
- goos: plan9
|
||||
goarch: riscv64
|
||||
flags:
|
||||
- -trimpath
|
||||
|
||||
archives:
|
||||
-
|
||||
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
|
||||
archive:
|
||||
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
format: tar.gz
|
||||
replacements:
|
||||
amd64: x64
|
||||
darwin: osx
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- README
|
||||
- CHANGELOG.md
|
||||
- oragono.motd
|
||||
- oragono.yaml
|
||||
- docs/*
|
||||
- languages/*.yaml
|
||||
- languages/*.json
|
||||
- languages/*.md
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt"
|
||||
git:
|
||||
short_hash: true
|
||||
|
9
.travis.yml
Normal file
9
.travis.yml
Normal 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
|
1341
CHANGELOG.md
1341
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
184
DEVELOPING.md
184
DEVELOPING.md
@ -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
|
||||
|
||||
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`
|
||||
The intent is to keep `master` relatively stable.
|
||||
|
||||
|
||||
## Releasing a new version
|
||||
|
||||
1. Ensure the tests pass, locally on travis (`make test`, `make smoke`, and `make irctest`)
|
||||
1. Test backwards compatibility guarantees. Get an example config file and an example database from the previous stable release. Make sure the current build still works with them (modulo anything explicitly called out in the changelog as a breaking change).
|
||||
1. Run the `ircstress` chanflood benchmark to look for data races (enable race detection) and performance regressions (disable it).
|
||||
1. Update the changelog with new changes and write release notes.
|
||||
1. Update the version number `irc/version.go` (either change `-unreleased` to `-rc1`, or remove `-rc1`, as appropriate).
|
||||
1. Commit the new changelog and constants change.
|
||||
1. Tag the release with `git tag --sign v0.0.0 -m "Release v0.0.0"` (`0.0.0` replaced with the real ver number).
|
||||
1. Build binaries using `make release`
|
||||
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?
|
||||
1. Ensure dependencies are up-to-date.
|
||||
2. Run [`irctest`]() over it to make sure nothing's severely broken.
|
||||
3. Remove `-unreleased` from the version number in `irc/constants.go`.
|
||||
4. Update the changelog with new changes.
|
||||
5. Remove unused sections from the changelog, change the date/version number and write release notes.
|
||||
6. Commit the new changelog and constants change.
|
||||
7. Tag the release with `git tag v0.0.0 -m "Release v0.0.0"` (`0.0.0` replaced with the real ver number).
|
||||
8. Build binaries using the Makefile, upload release to Github including the changelog and binaries.
|
||||
|
||||
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. 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`).
|
||||
1. Commit the new version number and changelog with the message `"Setup v0.0.1-unreleased devel ver"`.
|
||||
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`).
|
||||
2. At the top of the changelog, paste a new section with the content below.
|
||||
3. Commit the new version number and changelog with the message `"Setup v0.0.1-unreleased devel ver"`.
|
||||
|
||||
**Unreleased changelog content**
|
||||
|
||||
```md
|
||||
## Unreleased
|
||||
New release of Ergo!
|
||||
New release of Oragono!
|
||||
|
||||
### 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
|
||||
logging:
|
||||
-
|
||||
method: stderr
|
||||
type: "*"
|
||||
level: debug
|
||||
```
|
||||
To update this folder:
|
||||
|
||||
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>
|
||||
|
||||
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
|
||||
|
||||
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. 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.
|
||||
|
||||
|
||||
## 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).
|
||||
@ -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).
|
||||
|
||||
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
|
||||
|
48
Dockerfile
48
Dockerfile
@ -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
120
Gopkg.lock
generated
Normal 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
104
Gopkg.toml
Normal 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"
|
5
LICENSE
5
LICENSE
@ -1,9 +1,8 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-2014 Jeremy Latt
|
||||
Copyright (c) 2016-2018 Daniel Oaks
|
||||
Copyright (c) 2014-2015 Edmund Huber
|
||||
Copyright (c) 2016-2020 Daniel Oaks
|
||||
Copyright (c) 2017-2020 Shivaram Lingamneni
|
||||
Copyright (c) 2014 Jeremy Latt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
49
Makefile
49
Makefile
@ -1,48 +1,13 @@
|
||||
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null)
|
||||
GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1)
|
||||
.PHONY: all build
|
||||
|
||||
# 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
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||
goreleaser --snapshot --rm-dist
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
goreleaser --skip=publish --clean
|
||||
|
||||
.PHONY: capdefs
|
||||
capdefs:
|
||||
python3 ./gencapdefs.py > ${capdef_file}
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
python3 ./gencapdefs.py | diff - ${capdef_file}
|
||||
go test ./...
|
||||
go vet ./...
|
||||
./.check-gofmt.sh
|
||||
|
||||
.PHONY: smoke
|
||||
smoke: install
|
||||
ergo mkcerts --conf ./default.yaml || true
|
||||
ergo run --conf ./default.yaml --smoke
|
||||
|
||||
.PHONY: gofmt
|
||||
gofmt:
|
||||
./.check-gofmt.sh --fix
|
||||
|
||||
.PHONY: irctest
|
||||
irctest: install
|
||||
deps:
|
||||
git submodule update --init
|
||||
cd irctest && make ergo
|
||||
|
||||
test:
|
||||
cd irc && go test .
|
||||
cd irc && go vet .
|
||||
|
72
README
72
README
@ -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
|
||||
* Combining the features of an ircd, a services framework, and a bouncer:
|
||||
* Integrated account management
|
||||
* History storage
|
||||
* Bouncer functionality
|
||||
* Bleeding-edge IRCv3 support
|
||||
* High customizability via a rehashable (i.e., reloadable at runtime) YAML config
|
||||
It includes features such as UTF-8 nicks and channel names, client accounts and SASL, and other
|
||||
assorted IRCv3 support.
|
||||
|
||||
https://ergo.chat/
|
||||
https://github.com/ergochat/ergo
|
||||
#ergo on irc.ergo.chat or irc.libera.chat
|
||||
https://oragono.io/
|
||||
https://github.com/oragono/oragono
|
||||
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
@ -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:
|
||||
|
||||
$ 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:
|
||||
|
||||
$ ./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 ===
|
||||
|
||||
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,
|
||||
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 ===
|
||||
|
||||
* Jeremy Latt (2012-2014)
|
||||
* Edmund Huber (2014-2015)
|
||||
* Daniel Oaks (2016-present)
|
||||
* Shivaram Lingamneni (2017-present)
|
||||
* Many other contributors and friends of the project <3
|
||||
* Jeremy Latt, creator of Ergonomadic, <https://github.com/jlatt>
|
||||
* Edmund Huber, maintainer of Ergonomadic, <https://github.com/edmund-huber>
|
||||
* Niels Freier, added WebSocket support to Ergonomadic, <https://github.com/stumpyfr>
|
||||
* Daniel Oakley, maintainer of Oragono, <https://github.com/DanielOaks>
|
||||
* 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
148
README.md
@ -1,127 +1,145 @@
|
||||

|
||||

|
||||
|
||||
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
|
||||
* 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
|
||||
Oragono is a fork of the [Ergonomadic](https://github.com/edmund-huber/ergonomadic) IRC daemon <3
|
||||
|
||||
---
|
||||
|
||||
[](https://goreportcard.com/report/github.com/ergochat/ergo)
|
||||
[](https://github.com/ergochat/ergo/actions/workflows/build.yml)
|
||||
[](https://github.com/ergochat/ergo/releases/latest)
|
||||
[](https://crowdin.com/project/ergochat)
|
||||
[](https://goreportcard.com/report/github.com/oragono/oragono)
|
||||
[](https://travis-ci.org/oragono/oragono)
|
||||
[](https://github.com/oragono/oragono/releases/latest)
|
||||
[](https://www.irccloud.com/invite?channel=%23oragono&hostname=irc.freenode.net&port=6697&ssl=1)
|
||||
[](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
|
||||
|
||||
* integrated services: NickServ for user accounts, ChanServ for channel registration, and HostServ for vanity hosts
|
||||
* bouncer-like features: storing and replaying history, allowing multiple clients to use the same nickname
|
||||
* native TLS/SSL support, including support for client certificates
|
||||
* [IRCv3 support](https://ircv3.net/software/servers.html)
|
||||
* [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)
|
||||
* UTF-8 nick and channel names with rfc7613 (PRECIS)
|
||||
* [yaml](http://yaml.org/) configuration
|
||||
* native TLS/SSL support
|
||||
* server password (`PASS` command)
|
||||
* an extensible privilege system for IRC operators
|
||||
* ident lookups for usernames
|
||||
* automated client connection limits
|
||||
* passwords stored with [bcrypt](https://godoc.org/golang.org/x/crypto)
|
||||
* `UBAN`, a unified ban system that can target IPs, networks, masks, and registered accounts (`KLINE` and `DLINE` are also supported)
|
||||
* a focus on developing with [specifications](https://ergo.chat/specs.html)
|
||||
* on-the-fly updating server config and TLS certificates (rehashing)
|
||||
* client accounts and SASL
|
||||
* 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)
|
||||
* [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
|
||||
To go through the standard installation, download the latest release from this page: https://github.com/oragono/oragono/releases/latest
|
||||
|
||||
Extract it into a folder, then run the following commands:
|
||||
|
||||
```sh
|
||||
cp default.yaml ircd.yaml
|
||||
cp oragono.yaml ircd.yaml
|
||||
vim ircd.yaml # modify the config file to your liking
|
||||
./ergo mkcerts
|
||||
./ergo run # server should be ready to go!
|
||||
oragono initdb
|
||||
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
|
||||
|
||||
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).
|
||||
* [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).
|
||||
* Arch Linux [AUR](https://aur.archlinux.org/packages/oragono/) - Maintained by [Sean Enck (@enckse)](https://github.com/enckse).
|
||||
|
||||
### 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
|
||||
|
||||
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.)
|
||||
[](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
|
||||
|
||||
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
|
||||
|
||||
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 (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
|
||||
ergo genpasswd
|
||||
oragono genpasswd
|
||||
```
|
||||
|
||||
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`
|
||||
2. Register the channel with `/CS REGISTER #channel`
|
||||
### How to register a 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!
|
||||
|
||||
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
|
||||
|
||||
* Jeremy Latt (2012-2014)
|
||||
* Edmund Huber (2014-2015)
|
||||
* Daniel Oaks (2016-present)
|
||||
* Shivaram Lingamneni (2017-present)
|
||||
* [Many other contributors and friends of the project <3](https://github.com/ergochat/ergo/blob/master/CHANGELOG.md)
|
||||
* Jeremy Latt, creator of Ergonomadic, <https://github.com/jlatt>
|
||||
* Edmund Huber, maintainer of Ergonomadic, <https://github.com/edmund-huber>
|
||||
* Niels Freier, added WebSocket support to Ergonomadic, <https://github.com/stumpyfr>
|
||||
* Daniel Oakley, maintainer of Oragono, <https://github.com/DanielOaks>
|
||||
* 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.
|
||||
|
53
crowdin.yml
53
crowdin.yml
@ -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",
|
||||
},
|
||||
]
|
1137
default.yaml
1137
default.yaml
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -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 $?
|
@ -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>
|
@ -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())
|
@ -1,34 +0,0 @@
|
||||
include <tunables/global>
|
||||
|
||||
# Georg Pfuetzenreuter <georg+ergo@lysergic.dev>
|
||||
# AppArmor confinement for ergo and ergo-ldap
|
||||
|
||||
profile ergo /usr/bin/ergo {
|
||||
include <abstractions/base>
|
||||
include <abstractions/consoles>
|
||||
include <abstractions/nameservice>
|
||||
|
||||
/etc/ergo/ircd.{motd,yaml} r,
|
||||
/etc/ssl/irc/{crt,key} r,
|
||||
/etc/ssl/ergo/{crt,key} r,
|
||||
/usr/bin/ergo mr,
|
||||
/proc/sys/net/core/somaxconn r,
|
||||
/sys/kernel/mm/transparent_hugepage/hpage_pmd_size r,
|
||||
/usr/share/ergo/languages/{,*.lang.json,*.yaml} r,
|
||||
owner /run/ergo/ircd.lock rwk,
|
||||
owner /var/lib/ergo/ircd.db rw,
|
||||
|
||||
include if exists <local/ergo>
|
||||
|
||||
}
|
||||
|
||||
profile ergo-ldap /usr/bin/ergo-ldap {
|
||||
include <abstractions/openssl>
|
||||
include <abstractions/ssl_certs>
|
||||
|
||||
/usr/bin/ergo-ldap rm,
|
||||
/etc/ergo/ldap.yaml r,
|
||||
|
||||
include if exists <local/ergo-ldap>
|
||||
|
||||
}
|
@ -1,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())
|
@ -1,29 +0,0 @@
|
||||
Ergo init script for bsd-rc
|
||||
===
|
||||
|
||||
Written for and tested using FreeBSD.
|
||||
|
||||
## Installation
|
||||
Copy the `ergo` file from this folder to `/etc/rc.d/ergo`,
|
||||
permissions should be `555`.
|
||||
|
||||
You should create a system user for Ergo.
|
||||
This script defaults to running Ergo as a user named `ergo`,
|
||||
but that can be changed using `/etc/rc.conf`.
|
||||
|
||||
Here are all `rc.conf` variables and their defaults:
|
||||
- `ergo_enable`, defaults to `NO`. Whether to run `ergo` at system start.
|
||||
- `ergo_user`, defaults to `ergo`. Run using this user.
|
||||
- `ergo_group`, defaults to `ergo`. Run using this group.
|
||||
- `ergo_chdir`, defaults to `/var/db/ergo`. Path to the working directory for the server. Should be writable for `ergo_user`.
|
||||
- `ergo_conf`, defaults to `/usr/local/etc/ergo/ircd.yaml`. Config file path. Make sure `ergo_user` can read it.
|
||||
|
||||
This script assumes ergo to be installed at `/usr/local/bin/ergo`.
|
||||
|
||||
## Usage
|
||||
|
||||
```shell
|
||||
/etc/rc.d/ergo <command>
|
||||
```
|
||||
In addition to the obvious `start` and `stop` commands, this
|
||||
script also has a `reload` command that sends `SIGHUP` to the Ergo process.
|
@ -1,45 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# PROVIDE: ergo
|
||||
# REQUIRE: DAEMON
|
||||
# KEYWORD: shutdown
|
||||
|
||||
#
|
||||
# Add the following lines to /etc/rc.conf to enable Ergo
|
||||
#
|
||||
# ergo_enable (bool): Set to YES to enable ergo.
|
||||
# Default is "NO".
|
||||
# ergo_user (user): Set user to run ergo.
|
||||
# Default is "ergo".
|
||||
# ergo_group (group): Set group to run ergo.
|
||||
# Default is "ergo".
|
||||
# ergo_config (file): Set ergo config file path.
|
||||
# Default is "/usr/local/etc/ergo/config.yaml".
|
||||
# ergo_chdir (dir): Set ergo working directory
|
||||
# Default is "/var/db/ergo".
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name=ergo
|
||||
rcvar=ergo_enable
|
||||
desc="Ergo IRCv3 server"
|
||||
|
||||
load_rc_config "$name"
|
||||
|
||||
: ${ergo_enable:=NO}
|
||||
: ${ergo_user:=ergo}
|
||||
: ${ergo_group:=ergo}
|
||||
: ${ergo_chdir:=/var/db/ergo}
|
||||
: ${ergo_conf:=/usr/local/etc/ergo/ircd.yaml}
|
||||
|
||||
# If you don't define a custom reload function,
|
||||
# rc automagically sends SIGHUP to the process on reload.
|
||||
# But you have to list reload as an extra_command for that.
|
||||
extra_commands="reload"
|
||||
|
||||
procname="/usr/local/bin/${name}"
|
||||
command=/usr/sbin/daemon
|
||||
command_args="-S -T ${name} ${procname} run --conf ${ergo_conf}"
|
||||
|
||||
run_rc_command "$1"
|
||||
|
@ -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 .
|
||||
```
|
||||
|
@ -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:
|
@ -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
|
@ -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
|
@ -1,3 +0,0 @@
|
||||
# /etc/conf.d/ergo: config file for /etc/init.d/ergo
|
||||
ERGO_CONFIGFILE="/etc/ergo/ircd.yaml"
|
||||
ERGO_USERNAME="ergo"
|
@ -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 $?
|
||||
}
|
@ -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/
|
@ -1 +0,0 @@
|
||||
ergo-srv
|
@ -1 +0,0 @@
|
||||
3
|
@ -1 +0,0 @@
|
||||
ergo
|
@ -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
|
@ -1 +0,0 @@
|
||||
longrun
|
@ -1 +0,0 @@
|
||||
ergo-log
|
@ -1,4 +0,0 @@
|
||||
#!/usr/bin/execlineb -P
|
||||
fdmove -c 2 1
|
||||
execline-cd /opt/ergo
|
||||
s6-setuidgid ergo ./ergo run
|
@ -1 +0,0 @@
|
||||
longrun
|
@ -1,2 +0,0 @@
|
||||
# This configures the directives used for s6-log in the log service.
|
||||
DIRECTIVES="n3 s2000000"
|
@ -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
|
124
docs/API.md
124
docs/API.md
@ -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
92
docs/INFO.md
Normal 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 :)
|
1013
docs/MANUAL.md
1013
docs/MANUAL.md
File diff suppressed because it is too large
Load Diff
@ -54,7 +54,3 @@ Here are the color names we support, and which IRC colors they map to:
|
||||
14 | 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
|
||||
|
@ -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).
|
BIN
docs/logo.png
BIN
docs/logo.png
Binary file not shown.
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 3.1 KiB |
@ -1,2 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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>
|
||||
<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>
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.8 KiB |
208
ergo.go
208
ergo.go
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
310
gencapdefs.py
310
gencapdefs.py
@ -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
47
go.mod
@ -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
109
go.sum
@ -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=
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
2141
irc/accounts.go
2141
irc/accounts.go
File diff suppressed because it is too large
Load Diff
311
irc/api.go
311
irc/api.go
@ -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)
|
||||
}
|
@ -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
78
irc/batch.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -3,30 +3,58 @@
|
||||
|
||||
package caps
|
||||
|
||||
import "errors"
|
||||
|
||||
// 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 (
|
||||
nameToCapability map[string]Capability
|
||||
|
||||
NoSuchCap = errors.New("Unsupported capability name")
|
||||
// AccountNotify is this IRCv3 capability: http://ircv3.net/specs/extensions/account-notify-3.1.html
|
||||
AccountNotify Capability = "account-notify"
|
||||
// AccountTag is this IRCv3 capability: http://ircv3.net/specs/extensions/account-tag-3.2.html
|
||||
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.
|
||||
func (capability Capability) Name() string {
|
||||
return capabilityNames[capability]
|
||||
}
|
||||
|
||||
func NameToCapability(name string) (result Capability, err error) {
|
||||
result, found := nameToCapability[name]
|
||||
if !found {
|
||||
err = NoSuchCap
|
||||
}
|
||||
return
|
||||
return string(capability)
|
||||
}
|
||||
|
||||
// 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 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)
|
||||
}
|
||||
}
|
||||
|
211
irc/caps/defs.go
211
irc/caps/defs.go
@ -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",
|
||||
}
|
||||
)
|
146
irc/caps/set.go
146
irc/caps/set.go
@ -4,46 +4,45 @@
|
||||
package caps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Set holds a set of enabled capabilities.
|
||||
type Set [bitsetLen]uint32
|
||||
|
||||
// Values holds capability values.
|
||||
type Values map[Capability]string
|
||||
type Set struct {
|
||||
sync.RWMutex
|
||||
// capabilities holds the capabilities this manager has.
|
||||
capabilities map[Capability]bool
|
||||
}
|
||||
|
||||
// NewSet returns a new Set, with the given capabilities enabled.
|
||||
func NewSet(capabs ...Capability) *Set {
|
||||
var newSet Set
|
||||
newSet.Enable(capabs...)
|
||||
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 := Set{
|
||||
capabilities: make(map[Capability]bool),
|
||||
}
|
||||
newSet.Enable(capabs...)
|
||||
|
||||
return &newSet
|
||||
}
|
||||
|
||||
// Enable enables the given capabilities.
|
||||
func (s *Set) Enable(capabs ...Capability) {
|
||||
asSlice := s[:]
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
for _, capab := range capabs {
|
||||
utils.BitsetSet(asSlice, uint(capab), true)
|
||||
s.capabilities[capab] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Disable disables the given capabilities.
|
||||
func (s *Set) Disable(capabs ...Capability) {
|
||||
asSlice := s[:]
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
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...)
|
||||
}
|
||||
|
||||
// Has returns true if this set has the given capability.
|
||||
func (s *Set) Has(capab Capability) bool {
|
||||
return utils.BitsetGet(s[:], uint(capab))
|
||||
}
|
||||
// Has returns true if this set has the given capabilities.
|
||||
func (s *Set) Has(caps ...Capability) bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
// HasAll returns true if the set has all the given capabilities.
|
||||
func (s *Set) HasAll(capabs ...Capability) bool {
|
||||
for _, capab := range capabs {
|
||||
if !s.Has(capab) {
|
||||
for _, cap := range caps {
|
||||
if !s.capabilities[cap] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Union adds all the capabilities of another set to this set.
|
||||
func (s *Set) Union(other *Set) {
|
||||
utils.BitsetUnion(s[:], other[:])
|
||||
}
|
||||
// List return a list of our enabled capabilities.
|
||||
func (s *Set) List() []Capability {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
// Subtract removes all the capabilities of another set from this set.
|
||||
func (s *Set) Subtract(other *Set) {
|
||||
utils.BitsetSubtract(s[:], other[:])
|
||||
}
|
||||
|
||||
// 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 allCaps []Capability
|
||||
for capab := range s.capabilities {
|
||||
allCaps = append(allCaps, capab)
|
||||
}
|
||||
var t utils.TokenLineBuilder
|
||||
t.Initialize(maxLen, " ")
|
||||
|
||||
var capab Capability
|
||||
asSlice := s[:]
|
||||
for capab = 0; capab < numCapabs; capab++ {
|
||||
// XXX clients that only support CAP LS 301 cannot handle multiline
|
||||
// responses. omit some CAPs in this case, forcing the response to fit on
|
||||
// a single line. this is technically buggy for CAP LIST (as opposed to LS)
|
||||
// but it shouldn't matter
|
||||
if version < Cap302 && !isAllowed301(capab) {
|
||||
continue
|
||||
}
|
||||
// skip any capabilities that are not enabled
|
||||
if !utils.BitsetGet(asSlice, uint(capab)) {
|
||||
continue
|
||||
}
|
||||
capString := capab.Name()
|
||||
if version >= Cap302 {
|
||||
val, exists := values[capab]
|
||||
return allCaps
|
||||
}
|
||||
|
||||
// Count returns how many enabled caps this set has.
|
||||
func (s *Set) Count() int {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return len(s.capabilities)
|
||||
}
|
||||
|
||||
// String returns all of our enabled capabilities as a string.
|
||||
func (s *Set) String(version Version, values *Values) string {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
var strs sort.StringSlice
|
||||
|
||||
for capability := range s.capabilities {
|
||||
capString := capability.Name()
|
||||
if version == Cap302 {
|
||||
val, exists := values.Get(capability)
|
||||
if exists {
|
||||
capString = fmt.Sprintf("%s=%s", capString, val)
|
||||
capString += "=" + val
|
||||
}
|
||||
}
|
||||
t.Add(capString)
|
||||
strs = append(strs, capString)
|
||||
}
|
||||
|
||||
result = t.Lines()
|
||||
if result == nil {
|
||||
result = []string{""}
|
||||
}
|
||||
return
|
||||
}
|
||||
// sort the cap string before we send it out
|
||||
sort.Sort(strs)
|
||||
|
||||
// this is a fixed whitelist of caps that are eligible for display in CAP LS 301
|
||||
func isAllowed301(capab Capability) bool {
|
||||
switch capab {
|
||||
case AccountNotify, AccountTag, AwayNotify, Batch, ChgHost, Chathistory, EventPlayback,
|
||||
Relaymsg, EchoMessage, Nope, ExtendedJoin, InviteNotify, LabeledResponse, MessageTags,
|
||||
MultiPrefix, SASL, ServerTime, SetName, STS, UserhostInNames, ZNCSelfMessage, ZNCPlayback:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return strings.Join(strs, " ")
|
||||
}
|
||||
|
@ -3,23 +3,20 @@
|
||||
|
||||
package caps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
import "testing"
|
||||
import "reflect"
|
||||
|
||||
func TestSets(t *testing.T) {
|
||||
s1 := NewSet()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if s1.Has(STS) {
|
||||
t.Error("Has() returned true when we don't have the given capability")
|
||||
if s1.Has(AccountTag, EchoMessage, STS, UserhostInNames) {
|
||||
t.Error("Has() returned true when we don't have all the given capabilities")
|
||||
}
|
||||
|
||||
s1.Disable(AccountTag)
|
||||
@ -28,9 +25,14 @@ func TestSets(t *testing.T) {
|
||||
t.Error("Disable() did not correctly disable the given capability")
|
||||
}
|
||||
|
||||
enabledCaps := NewSet()
|
||||
enabledCaps.Union(s1)
|
||||
expectedCaps := NewSet(EchoMessage, UserhostInNames)
|
||||
enabledCaps := make(map[Capability]bool)
|
||||
for _, capab := range s1.List() {
|
||||
enabledCaps[capab] = true
|
||||
}
|
||||
expectedCaps := map[Capability]bool{
|
||||
EchoMessage: true,
|
||||
UserhostInNames: true,
|
||||
}
|
||||
if !reflect.DeepEqual(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
|
||||
s1.Enable(EchoMessage)
|
||||
|
||||
if s1.Count() != 2 {
|
||||
t.Error("Count() did not match expected capability count")
|
||||
}
|
||||
|
||||
// make sure add and remove work fine
|
||||
s1.Add(InviteNotify)
|
||||
s1.Remove(EchoMessage)
|
||||
|
||||
if !s1.Has(InviteNotify) || s1.Has(EchoMessage) {
|
||||
t.Error("Add/Remove don't work")
|
||||
if s1.Count() != 2 {
|
||||
t.Error("Count() did not match expected capability count")
|
||||
}
|
||||
|
||||
// test Strings()
|
||||
values := make(Values)
|
||||
values[InviteNotify] = "invitemepls"
|
||||
// test String()
|
||||
values := NewValues()
|
||||
values.Set(InviteNotify, "invitemepls")
|
||||
|
||||
actualCap301ValuesString := s1.Strings(Cap301, values, 0)
|
||||
expectedCap301ValuesString := []string{"invite-notify userhost-in-names"}
|
||||
if !reflect.DeepEqual(actualCap301ValuesString, expectedCap301ValuesString) {
|
||||
t.Errorf("Generated Cap301 values string [%v] did not match expected values string [%v]", actualCap301ValuesString, expectedCap301ValuesString)
|
||||
actualCap301ValuesString := s1.String(Cap301, values)
|
||||
expectedCap301ValuesString := "invite-notify userhost-in-names"
|
||||
if actualCap301ValuesString != expectedCap301ValuesString {
|
||||
t.Errorf("Generated Cap301 values string [%s] did not match expected values string [%s]", actualCap301ValuesString, expectedCap301ValuesString)
|
||||
}
|
||||
|
||||
actualCap302ValuesString := s1.Strings(Cap302, values, 0)
|
||||
expectedCap302ValuesString := []string{"invite-notify=invitemepls userhost-in-names"}
|
||||
if !reflect.DeepEqual(actualCap302ValuesString, expectedCap302ValuesString) {
|
||||
actualCap302ValuesString := s1.String(Cap302, values)
|
||||
expectedCap302ValuesString := "invite-notify=invitemepls userhost-in-names"
|
||||
if 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
45
irc/caps/values.go
Normal 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
|
||||
}
|
1915
irc/channel.go
1915
irc/channel.go
File diff suppressed because it is too large
Load Diff
@ -4,12 +4,7 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
type channelManagerEntry struct {
|
||||
@ -18,7 +13,6 @@ type channelManagerEntry struct {
|
||||
// think the channel is empty (without holding a lock across the entire Channel.Join()
|
||||
// call)
|
||||
pendingJoins int
|
||||
skeleton string
|
||||
}
|
||||
|
||||
// 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.
|
||||
type ChannelManager struct {
|
||||
sync.RWMutex // tier 2
|
||||
// chans is the main data structure, mapping casefolded name -> *Channel
|
||||
chans map[string]*channelManagerEntry
|
||||
chansSkeletons utils.HashSet[string]
|
||||
purgedChannels map[string]ChannelPurgeRecord // casefolded name to purge record
|
||||
server *Server
|
||||
chans map[string]*channelManagerEntry
|
||||
}
|
||||
|
||||
// NewChannelManager returns a new ChannelManager.
|
||||
func (cm *ChannelManager) Initialize(server *Server, config *Config) (err error) {
|
||||
cm.chans = make(map[string]*channelManagerEntry)
|
||||
cm.chansSkeletons = make(utils.HashSet[string])
|
||||
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
|
||||
func NewChannelManager() *ChannelManager {
|
||||
return &ChannelManager{
|
||||
chans: make(map[string]*channelManagerEntry),
|
||||
}
|
||||
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
|
||||
func (cm *ChannelManager) Get(name string) (channel *Channel) {
|
||||
func (cm *ChannelManager) Get(name string) *Channel {
|
||||
name, err := CasefoldChannel(name)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
entry := cm.chans[name]
|
||||
if entry != nil {
|
||||
return entry.channel
|
||||
if err == nil {
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
entry := cm.chans[name]
|
||||
if entry != nil {
|
||||
return entry.channel
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
casefoldedName, err := CasefoldChannel(name)
|
||||
skeleton, skerr := Skeleton(name)
|
||||
if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen {
|
||||
return errNoSuchChannel, ""
|
||||
if err != nil || len(casefoldedName) > server.Limits().ChannelLen {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
|
||||
channel, err, newChannel := func() (*Channel, error, bool) {
|
||||
var newChannel bool
|
||||
cm.Lock()
|
||||
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()
|
||||
defer cm.Unlock()
|
||||
|
||||
// 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]
|
||||
entry = cm.chans[casefoldedName]
|
||||
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{
|
||||
channel: NewChannel(server, name, casefoldedName, false, RegisteredChannel{}),
|
||||
channel: NewChannel(server, name, true, info),
|
||||
pendingJoins: 0,
|
||||
}
|
||||
cm.chansSkeletons.Add(skeleton)
|
||||
entry.skeleton = skeleton
|
||||
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) {
|
||||
cm.Lock()
|
||||
defer cm.Unlock()
|
||||
|
||||
cfname := channel.NameCasefolded()
|
||||
|
||||
entry := cm.chans[cfname]
|
||||
entry := cm.chans[channel.NameCasefolded()]
|
||||
if entry == nil || entry.channel != channel {
|
||||
return
|
||||
}
|
||||
|
||||
cm.maybeCleanupInternal(cfname, entry, afterJoin)
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) maybeCleanupInternal(cfname string, entry *channelManagerEntry, afterJoin bool) {
|
||||
if afterJoin {
|
||||
entry.pendingJoins -= 1
|
||||
}
|
||||
if entry.pendingJoins == 0 && entry.channel.IsClean() {
|
||||
delete(cm.chans, cfname)
|
||||
if entry.skeleton != "" {
|
||||
delete(cm.chansSkeletons, entry.skeleton)
|
||||
}
|
||||
// TODO(slingamn) right now, registered channels cannot be cleaned up.
|
||||
// this is because once ChannelManager becomes the source of truth about a channel,
|
||||
// we can't move the source of truth back to the database unless we do an ACID
|
||||
// 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.
|
||||
func (cm *ChannelManager) Part(client *Client, name string, message string, rb *ResponseBuffer) error {
|
||||
var channel *Channel
|
||||
|
||||
casefoldedName, err := CasefoldChannel(name)
|
||||
if err != nil {
|
||||
return errNoSuchChannel
|
||||
@ -189,15 +117,12 @@ func (cm *ChannelManager) Part(client *Client, name string, message string, rb *
|
||||
|
||||
cm.RLock()
|
||||
entry := cm.chans[casefoldedName]
|
||||
if entry != nil {
|
||||
channel = entry.channel
|
||||
}
|
||||
cm.RUnlock()
|
||||
|
||||
if channel == nil {
|
||||
if entry == nil {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
channel.Part(client, message, rb)
|
||||
entry.channel.Part(client, message, rb)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -205,141 +130,34 @@ func (cm *ChannelManager) Cleanup(channel *Channel) {
|
||||
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)
|
||||
func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||
oldCfname, err := CasefoldChannel(name)
|
||||
func (cm *ChannelManager) Rename(name string, newname string) error {
|
||||
cfname, err := CasefoldChannel(name)
|
||||
if err != nil {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
|
||||
newCfname, err := CasefoldChannel(newName)
|
||||
cfnewname, err := CasefoldChannel(newname)
|
||||
if err != nil {
|
||||
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()
|
||||
defer cm.Unlock()
|
||||
|
||||
entry := cm.chans[oldCfname]
|
||||
if cm.chans[cfnewname] != nil {
|
||||
return errChannelNameInUse
|
||||
}
|
||||
entry := cm.chans[cfname]
|
||||
if entry == nil {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
channel = entry.channel
|
||||
info = channel.ExportRegistration()
|
||||
registered := info.Founder != ""
|
||||
|
||||
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)
|
||||
delete(cm.chans, cfname)
|
||||
cm.chans[cfnewname] = entry
|
||||
entry.channel.setName(newname)
|
||||
entry.channel.setNameCasefolded(cfnewname)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Len returns the number of channels
|
||||
@ -353,163 +171,8 @@ func (cm *ChannelManager) Len() int {
|
||||
func (cm *ChannelManager) Channels() (result []*Channel) {
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
result = make([]*Channel, 0, len(cm.chans))
|
||||
for _, entry := range cm.chans {
|
||||
result = append(result, entry.channel)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -4,37 +4,51 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
// this is exclusively the *persistence* layer for channel registration;
|
||||
// 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 (
|
||||
IncludeInitial uint = 1 << iota
|
||||
IncludeTopic
|
||||
IncludeModes
|
||||
IncludeLists
|
||||
IncludeSettings
|
||||
keyChannelExists = "channel.exists %s"
|
||||
keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
|
||||
keyChannelRegTime = "channel.registered.time %s"
|
||||
keyChannelFounder = "channel.founder %s"
|
||||
keyChannelTopic = "channel.topic %s"
|
||||
keyChannelTopicSetBy = "channel.topic.setby %s"
|
||||
keyChannelTopicSetTime = "channel.topic.settime %s"
|
||||
keyChannelBanlist = "channel.banlist %s"
|
||||
keyChannelExceptlist = "channel.exceptlist %s"
|
||||
keyChannelInvitelist = "channel.invitelist %s"
|
||||
)
|
||||
|
||||
// this is an OR of all possible flags
|
||||
const (
|
||||
IncludeAllAttrs = ^uint(0)
|
||||
var (
|
||||
channelKeyStrings = []string{
|
||||
keyChannelExists,
|
||||
keyChannelName,
|
||||
keyChannelRegTime,
|
||||
keyChannelFounder,
|
||||
keyChannelTopic,
|
||||
keyChannelTopicSetBy,
|
||||
keyChannelTopicSetTime,
|
||||
keyChannelBanlist,
|
||||
keyChannelExceptlist,
|
||||
keyChannelInvitelist,
|
||||
}
|
||||
)
|
||||
|
||||
// RegisteredChannel holds details about a given registered channel.
|
||||
type RegisteredChannel struct {
|
||||
// Name of the channel.
|
||||
Name string
|
||||
// UUID for the datastore.
|
||||
UUID utils.UUID
|
||||
// RegisteredAt represents the time that the channel was registered.
|
||||
RegisteredAt time.Time
|
||||
// Founder indicates the founder of the channel.
|
||||
@ -45,48 +59,166 @@ type RegisteredChannel struct {
|
||||
TopicSetBy string
|
||||
// TopicSetTime represents the time the topic was set.
|
||||
TopicSetTime time.Time
|
||||
// Modes represents the channel modes
|
||||
Modes []modes.Mode
|
||||
// Key represents the channel key / password
|
||||
Key string
|
||||
// Forward is the forwarding/overflow (+f) channel
|
||||
Forward 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
|
||||
// Banlist represents the bans set on the channel.
|
||||
Banlist []string
|
||||
// Exceptlist represents the exceptions set on the channel.
|
||||
Exceptlist []string
|
||||
// Invitelist represents the invite exceptions set on the channel.
|
||||
Invitelist []string
|
||||
}
|
||||
|
||||
func (r *RegisteredChannel) Serialize() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
// ChannelRegistry manages registered channels.
|
||||
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) {
|
||||
return json.Unmarshal(b, r)
|
||||
// NewChannelRegistry returns a new ChannelRegistry.
|
||||
func NewChannelRegistry(server *Server) *ChannelRegistry {
|
||||
return &ChannelRegistry{
|
||||
server: server,
|
||||
}
|
||||
}
|
||||
|
||||
type ChannelPurgeRecord struct {
|
||||
NameCasefolded string `json:"Name"`
|
||||
UUID utils.UUID
|
||||
Oper string
|
||||
PurgedAt time.Time
|
||||
Reason string
|
||||
// StoreChannel obtains a consistent view of a channel, then persists it to the store.
|
||||
func (reg *ChannelRegistry) StoreChannel(channel *Channel, includeLists bool) {
|
||||
if !reg.server.ChannelRegistrationEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
return json.Marshal(c)
|
||||
// LoadChannel loads a channel from the store.
|
||||
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 {
|
||||
return json.Unmarshal(b, c)
|
||||
// Rename handles the persistence part of a channel rename: the channel is
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
1062
irc/chanserv.go
1062
irc/chanserv.go
File diff suppressed because it is too large
Load Diff
2402
irc/client.go
2402
irc/client.go
File diff suppressed because it is too large
Load Diff
@ -5,25 +5,50 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ergochat/ergo/irc/caps"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"github.com/goshuirc/irc-go/ircmatch"
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
|
||||
"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
|
||||
type ClientManager struct {
|
||||
sync.RWMutex // tier 2
|
||||
byNick map[string]*Client
|
||||
bySkeleton map[string]*Client
|
||||
}
|
||||
|
||||
// Initialize initializes a ClientManager.
|
||||
func (clients *ClientManager) Initialize() {
|
||||
clients.byNick = make(map[string]*Client)
|
||||
clients.bySkeleton = make(map[string]*Client)
|
||||
// NewClientManager returns a new ClientManager.
|
||||
func NewClientManager() *ClientManager {
|
||||
return &ClientManager{
|
||||
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.
|
||||
@ -38,37 +63,19 @@ func (clients *ClientManager) Get(nick string) *Client {
|
||||
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()
|
||||
if oldcfnick == "*" || oldcfnick == "" {
|
||||
return errNickMissing
|
||||
}
|
||||
|
||||
oldcfnick := client.NickCasefolded()
|
||||
currentEntry, present := clients.byNick[oldcfnick]
|
||||
if present {
|
||||
if currentEntry == client {
|
||||
delete(clients.byNick, oldcfnick)
|
||||
removed = true
|
||||
} else {
|
||||
// this shouldn't happen, but we can ignore it
|
||||
client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick)
|
||||
err = errNickMissing
|
||||
client.server.logger.Warning("internal", fmt.Sprintf("clients for nick %s out of sync", oldcfnick))
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
@ -77,163 +84,42 @@ func (clients *ClientManager) Remove(client *Client) error {
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
oldcfnick, oldskeleton := client.uniqueIdentifiers()
|
||||
return clients.removeInternal(client, oldcfnick, oldskeleton)
|
||||
if !client.HasNick() {
|
||||
return errNickMissing
|
||||
}
|
||||
clients.removeInternal(client)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetNick sets a client's nickname, validating it against nicknames in use
|
||||
// XXX: dryRun validates a client's ability to claim a nick, without
|
||||
// actually claiming it
|
||||
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, awayChanged bool) {
|
||||
config := client.server.Config()
|
||||
|
||||
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
|
||||
}
|
||||
func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
||||
newcfnick, err := CasefoldName(newNick)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// recompute always-on status, because client.alwaysOn is not set for unregistered clients
|
||||
var alwaysOn, useAccountName bool
|
||||
if account != "" {
|
||||
alwaysOn = persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
|
||||
useAccountName = alwaysOn || config.Accounts.NickReservation.ForceNickEqualsAccount
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
var reservedAccount string
|
||||
var method NickReservationMethod
|
||||
if client.server.AccountConfig().NickReservation.Enabled {
|
||||
reservedAccount = client.server.accounts.NickToAccount(newcfnick)
|
||||
method = client.server.AccountConfig().NickReservation.Method
|
||||
}
|
||||
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
currentClient := clients.byNick[newCfNick]
|
||||
clients.removeInternal(client)
|
||||
currentNewEntry := clients.byNick[newcfnick]
|
||||
// the client may just be changing case
|
||||
if currentClient != nil && currentClient != client {
|
||||
// these conditions forbid reattaching to an existing session:
|
||||
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
|
||||
if currentNewEntry != nil && currentNewEntry != client {
|
||||
return errNicknameInUse
|
||||
}
|
||||
// analogous checks for skeletons
|
||||
skeletonHolder := clients.bySkeleton[newSkeleton]
|
||||
if skeletonHolder != nil && skeletonHolder != client {
|
||||
return "", errNicknameInUse, false
|
||||
if method == NickReservationStrict && reservedAccount != client.Account() {
|
||||
return errNicknameReserved
|
||||
}
|
||||
if nickIsReserved {
|
||||
return "", errNicknameReserved, false
|
||||
}
|
||||
|
||||
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
|
||||
clients.byNick[newcfnick] = client
|
||||
client.updateNickMask(newNick)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (clients *ClientManager) AllClients() (result []*Client) {
|
||||
@ -248,52 +134,41 @@ func (clients *ClientManager) AllClients() (result []*Client) {
|
||||
return
|
||||
}
|
||||
|
||||
// AllWithCapsNotify returns all sessions that support cap-notify.
|
||||
func (clients *ClientManager) AllWithCapsNotify() (sessions []*Session) {
|
||||
// AllWithCaps returns all clients with the given capabilities.
|
||||
func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (set ClientSet) {
|
||||
set = make(ClientSet)
|
||||
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
for _, client := range clients.byNick {
|
||||
for _, session := range client.Sessions() {
|
||||
// cap-notify is implicit in cap version 302 and above
|
||||
if session.capabilities.Has(caps.CapNotify) || 302 <= session.capVersion {
|
||||
sessions = append(sessions, session)
|
||||
var client *Client
|
||||
for _, client = range clients.byNick {
|
||||
// make sure they have all the required caps
|
||||
for _, capab := range capabs {
|
||||
if !client.capabilities.Has(capab) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
set.Add(client)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
return set
|
||||
}
|
||||
|
||||
// FindAll returns all clients that match the given userhost mask.
|
||||
func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
|
||||
set = make(ClientSet)
|
||||
|
||||
userhost, err := CanonicalizeMaskWildcard(userhost)
|
||||
userhost, err := Casefold(ExpandUserHost(userhost))
|
||||
if err != nil {
|
||||
return set
|
||||
}
|
||||
matcher, err := utils.CompileGlob(userhost, false)
|
||||
if err != nil {
|
||||
// not much we can do here
|
||||
return
|
||||
}
|
||||
matcher := ircmatch.MakeMatch(userhost)
|
||||
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
for _, client := range clients.byNick {
|
||||
if matcher.MatchString(client.NickMaskCasefolded()) {
|
||||
if matcher.Match(client.nickMaskCasefolded) {
|
||||
set.Add(client)
|
||||
}
|
||||
}
|
||||
@ -301,15 +176,166 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
|
||||
return set
|
||||
}
|
||||
|
||||
// Determine the canonical / unfolded form of a nick, if a client matching it
|
||||
// is present (or always-on).
|
||||
func (clients *ClientManager) UnfoldNick(cfnick string) (nick string) {
|
||||
// Find returns the first client that matches the given userhost mask.
|
||||
func (clients *ClientManager) Find(userhost string) *Client {
|
||||
userhost, err := Casefold(ExpandUserHost(userhost))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
matcher := ircmatch.MakeMatch(userhost)
|
||||
var matchedClient *Client
|
||||
|
||||
clients.RLock()
|
||||
c := clients.byNick[cfnick]
|
||||
clients.RUnlock()
|
||||
if c != nil {
|
||||
return c.Nick()
|
||||
} else {
|
||||
return cfnick
|
||||
defer clients.RUnlock()
|
||||
for _, client := range clients.byNick {
|
||||
if matcher.Match(client.nickMaskCasefolded) {
|
||||
matchedClient = client
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
280
irc/commands.go
280
irc/commands.go
@ -6,96 +6,73 @@
|
||||
package irc
|
||||
|
||||
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.
|
||||
type Command struct {
|
||||
handler func(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool
|
||||
usablePreReg bool
|
||||
allowedInBatch bool // allowed in client-to-server batches
|
||||
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
|
||||
handler func(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool
|
||||
oper bool
|
||||
usablePreReg bool
|
||||
leaveClientActive bool // if true, leaves the client active time alone. reversed because we can't default a struct element to True
|
||||
leaveClientIdle bool
|
||||
minParams int
|
||||
capabs []string
|
||||
}
|
||||
|
||||
// Run runs this command with the given client/message.
|
||||
func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ircmsg.Message) (exiting bool) {
|
||||
rb := NewResponseBuffer(session)
|
||||
rb.Label = GetLabel(msg)
|
||||
|
||||
exiting = func() bool {
|
||||
defer rb.Send(true)
|
||||
|
||||
if !client.registered && !cmd.usablePreReg {
|
||||
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...) {
|
||||
rb.Add(nil, server.name, ERR_NOPRIVILEGES, client.Nick(), client.t("Permission Denied"))
|
||||
return false
|
||||
}
|
||||
if len(msg.Params) < cmd.minParams {
|
||||
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)
|
||||
func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
if !client.registered && !cmd.usablePreReg {
|
||||
client.Send(nil, server.name, ERR_NOTREGISTERED, client.nick, client.t("You need to register before you can use that command"))
|
||||
return false
|
||||
}
|
||||
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"))
|
||||
return false
|
||||
}
|
||||
if len(cmd.capabs) > 0 && !client.HasRoleCapabs(cmd.capabs...) {
|
||||
client.Send(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"))
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
var Commands map[string]Command
|
||||
|
||||
func init() {
|
||||
Commands = map[string]Command{
|
||||
"ACCEPT": {
|
||||
handler: acceptHandler,
|
||||
minParams: 1,
|
||||
"ACC": {
|
||||
handler: accHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"AMBIANCE": {
|
||||
handler: sceneHandler,
|
||||
@ -107,45 +84,31 @@ func init() {
|
||||
minParams: 1,
|
||||
},
|
||||
"AWAY": {
|
||||
handler: awayHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 0,
|
||||
},
|
||||
"BATCH": {
|
||||
handler: batchHandler,
|
||||
minParams: 1,
|
||||
allowedInBatch: true,
|
||||
handler: awayHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"CAP": {
|
||||
handler: capHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"CHATHISTORY": {
|
||||
handler: chathistoryHandler,
|
||||
minParams: 4,
|
||||
"CHANSERV": {
|
||||
handler: csHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"CS": {
|
||||
handler: csHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"DEBUG": {
|
||||
handler: debugHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"rehash"},
|
||||
},
|
||||
"DEFCON": {
|
||||
handler: defconHandler,
|
||||
capabs: []string{"defcon"},
|
||||
},
|
||||
"DEOPER": {
|
||||
handler: deoperHandler,
|
||||
minParams: 0,
|
||||
oper: true,
|
||||
},
|
||||
"DLINE": {
|
||||
handler: dlineHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"EXTJWT": {
|
||||
handler: extjwtHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
},
|
||||
"HELP": {
|
||||
handler: helpHandler,
|
||||
@ -155,10 +118,6 @@ func init() {
|
||||
handler: helpHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"HISTORY": {
|
||||
handler: historyHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"INFO": {
|
||||
handler: infoHandler,
|
||||
},
|
||||
@ -170,10 +129,6 @@ func init() {
|
||||
handler: isonHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"ISUPPORT": {
|
||||
handler: isupportHandler,
|
||||
usablePreReg: true,
|
||||
},
|
||||
"JOIN": {
|
||||
handler: joinHandler,
|
||||
minParams: 1,
|
||||
@ -185,12 +140,13 @@ func init() {
|
||||
"KILL": {
|
||||
handler: killHandler,
|
||||
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": {
|
||||
handler: klineHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"ban"},
|
||||
oper: true,
|
||||
},
|
||||
"LANGUAGE": {
|
||||
handler: languageHandler,
|
||||
@ -205,15 +161,6 @@ func init() {
|
||||
handler: lusersHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"MARKREAD": {
|
||||
handler: markReadHandler,
|
||||
minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS
|
||||
},
|
||||
"METADATA": {
|
||||
handler: metadataHandler,
|
||||
minParams: 2,
|
||||
usablePreReg: true,
|
||||
},
|
||||
"MODE": {
|
||||
handler: modeHandler,
|
||||
minParams: 1,
|
||||
@ -235,10 +182,13 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"NICKSERV": {
|
||||
handler: nsHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"NOTICE": {
|
||||
handler: messageHandler,
|
||||
minParams: 2,
|
||||
allowedInBatch: true,
|
||||
handler: noticeHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"NPC": {
|
||||
handler: npcHandler,
|
||||
@ -248,9 +198,13 @@ func init() {
|
||||
handler: npcaHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"NS": {
|
||||
handler: nsHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"OPER": {
|
||||
handler: operHandler,
|
||||
minParams: 1,
|
||||
minParams: 2,
|
||||
},
|
||||
"PART": {
|
||||
handler: partHandler,
|
||||
@ -261,47 +215,40 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"PERSISTENCE": {
|
||||
handler: persistenceHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"PING": {
|
||||
handler: pingHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
handler: pingHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
leaveClientActive: true,
|
||||
},
|
||||
"PONG": {
|
||||
handler: pongHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
handler: pongHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
leaveClientActive: true,
|
||||
},
|
||||
"PRIVMSG": {
|
||||
handler: messageHandler,
|
||||
minParams: 2,
|
||||
allowedInBatch: true,
|
||||
handler: privmsgHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"RELAYMSG": {
|
||||
handler: relaymsgHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"REGISTER": {
|
||||
handler: registerHandler,
|
||||
minParams: 3,
|
||||
"PROXY": {
|
||||
handler: proxyHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 5,
|
||||
},
|
||||
"RENAME": {
|
||||
handler: renameHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"SAJOIN": {
|
||||
handler: sajoinHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"sajoin"},
|
||||
"RESUME": {
|
||||
handler: resumeHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"SANICK": {
|
||||
handler: sanickHandler,
|
||||
minParams: 2,
|
||||
capabs: []string{"samode"},
|
||||
oper: true,
|
||||
},
|
||||
"SAMODE": {
|
||||
handler: modeHandler,
|
||||
@ -312,15 +259,8 @@ func init() {
|
||||
handler: sceneHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"SETNAME": {
|
||||
handler: setnameHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"SUMMON": {
|
||||
handler: summonHandler,
|
||||
},
|
||||
"TAGMSG": {
|
||||
handler: messageHandler,
|
||||
handler: tagmsgHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"QUIT": {
|
||||
@ -328,14 +268,11 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 0,
|
||||
},
|
||||
"REDACT": {
|
||||
handler: redactHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"REHASH": {
|
||||
handler: rehashHandler,
|
||||
minParams: 0,
|
||||
capabs: []string{"rehash"},
|
||||
oper: true,
|
||||
capabs: []string{"oper:rehash"},
|
||||
},
|
||||
"TIME": {
|
||||
handler: timeHandler,
|
||||
@ -345,24 +282,15 @@ func init() {
|
||||
handler: topicHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"UBAN": {
|
||||
handler: ubanHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"UNDLINE": {
|
||||
handler: unDLineHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"UNINVITE": {
|
||||
handler: inviteHandler,
|
||||
minParams: 2,
|
||||
oper: true,
|
||||
},
|
||||
"UNKLINE": {
|
||||
handler: unKLineHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"ban"},
|
||||
oper: true,
|
||||
},
|
||||
"USER": {
|
||||
handler: userHandler,
|
||||
@ -373,14 +301,6 @@ func init() {
|
||||
handler: userhostHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"USERS": {
|
||||
handler: usersHandler,
|
||||
},
|
||||
"VERIFY": {
|
||||
handler: verifyHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 2,
|
||||
},
|
||||
"VERSION": {
|
||||
handler: versionHandler,
|
||||
minParams: 0,
|
||||
@ -390,10 +310,6 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 4,
|
||||
},
|
||||
"WEBPUSH": {
|
||||
handler: webpushHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"WHO": {
|
||||
handler: whoHandler,
|
||||
minParams: 1,
|
||||
@ -406,11 +322,5 @@ func init() {
|
||||
handler: whowasHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"ZNC": {
|
||||
handler: zncHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
}
|
||||
|
||||
initializeServices()
|
||||
}
|
||||
|
1934
irc/config.go
1934
irc/config.go
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -4,277 +4,155 @@
|
||||
package connection_limits
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"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.
|
||||
// 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 {
|
||||
rawLimiterConfig
|
||||
|
||||
exemptedNets []flatip.IPNet
|
||||
customLimits []customLimit
|
||||
Enabled bool
|
||||
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
|
||||
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
|
||||
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) {
|
||||
if err = unmarshal(&config.rawLimiterConfig); err != nil {
|
||||
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
|
||||
}
|
||||
var (
|
||||
errTooManyClients = errors.New("Too many clients in subnet")
|
||||
)
|
||||
|
||||
// Limiter manages the automated client connection limits.
|
||||
type Limiter struct {
|
||||
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:
|
||||
limiter map[limiterKey]int
|
||||
// IP/CIDR -> throttle state:
|
||||
throttler map[limiterKey]ThrottleDetails
|
||||
// exemptedIPs holds IPs that are exempt from limits
|
||||
exemptedIPs map[string]bool
|
||||
// exemptedNets holds networks that are exempt from limits
|
||||
exemptedNets []net.IPNet
|
||||
}
|
||||
|
||||
// addrToKey canonicalizes `addr` to a string key, and returns
|
||||
// the relevant connection limit and throttle max-per-window values
|
||||
func (cl *Limiter) addrToKey(addr flatip.IP) (key limiterKey, customID string, limit int, throttle int) {
|
||||
for _, custom := range cl.config.customLimits {
|
||||
for _, net := range custom.nets {
|
||||
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
|
||||
// maskAddr masks the given IPv4/6 address with our cidr limit masks.
|
||||
func (cl *Limiter) maskAddr(addr net.IP) net.IP {
|
||||
if addr.To4() == nil {
|
||||
// IPv6 addr
|
||||
addr = addr.Mask(cl.ipv6Mask)
|
||||
} else {
|
||||
prefixLen = cl.config.CidrLenIPv6
|
||||
addr = addr.Mask(prefixLen, 128)
|
||||
// IPv4 addr
|
||||
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.
|
||||
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()
|
||||
defer cl.Unlock()
|
||||
|
||||
// we don't track populations for exempted addresses or nets - this is by design
|
||||
if flatip.IPInNets(addr, cl.config.exemptedNets) {
|
||||
if !cl.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
addrString, _, maxConcurrent, maxPerWindow := cl.addrToKey(addr)
|
||||
|
||||
// check limiter
|
||||
var count int
|
||||
if cl.config.Count {
|
||||
count = cl.limiter[addrString] + 1
|
||||
if count > maxConcurrent {
|
||||
return ErrLimitExceeded
|
||||
// check exempted lists
|
||||
// we don't track populations for exempted addresses or nets - this is by design
|
||||
if cl.exemptedIPs[addr.String()] {
|
||||
return nil
|
||||
}
|
||||
for _, ex := range cl.exemptedNets {
|
||||
if ex.Contains(addr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if cl.config.Throttle {
|
||||
details := cl.throttler[addrString] // retrieve mutable throttle state from the map
|
||||
// add in constant state to process the limiting operation
|
||||
g := GenericThrottle{
|
||||
ThrottleDetails: details,
|
||||
Duration: cl.config.Window,
|
||||
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
|
||||
}
|
||||
// check population
|
||||
cl.maskAddr(addr)
|
||||
addrString := addr.String()
|
||||
|
||||
if cl.population[addrString]+1 > cl.subnetLimit && !force {
|
||||
return errTooManyClients
|
||||
}
|
||||
|
||||
// success, record in limiter
|
||||
if cl.config.Count {
|
||||
cl.limiter[addrString] = count
|
||||
}
|
||||
cl.population[addrString] = cl.population[addrString] + 1
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveClient removes the given address from our population
|
||||
func (cl *Limiter) RemoveClient(addr flatip.IP) {
|
||||
func (cl *Limiter) RemoveClient(addr net.IP) {
|
||||
cl.Lock()
|
||||
defer cl.Unlock()
|
||||
|
||||
if !cl.config.Count || flatip.IPInNets(addr, cl.config.exemptedNets) {
|
||||
if !cl.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
addrString, _, _, _ := cl.addrToKey(addr)
|
||||
count := cl.limiter[addrString]
|
||||
count -= 1
|
||||
if count < 0 {
|
||||
count = 0
|
||||
addrString := addr.String()
|
||||
cl.population[addrString] = cl.population[addrString] - 1
|
||||
|
||||
// safety limiter
|
||||
if cl.population[addrString] < 0 {
|
||||
cl.population[addrString] = 0
|
||||
}
|
||||
cl.limiter[addrString] = count
|
||||
}
|
||||
|
||||
type LimiterStatus struct {
|
||||
Exempt bool
|
||||
// NewLimiter returns a new connection limit handler.
|
||||
// The handler is functional, but disabled; it can be enabled via `ApplyConfig`.
|
||||
func NewLimiter() *Limiter {
|
||||
var cl Limiter
|
||||
|
||||
Count int
|
||||
MaxCount int
|
||||
// initialize empty population; all other state is configurable
|
||||
cl.population = make(map[string]int)
|
||||
|
||||
Throttle int
|
||||
MaxPerWindow int
|
||||
ThrottleDuration time.Duration
|
||||
}
|
||||
|
||||
func (cl *Limiter) Status(addr flatip.IP) (netName string, status LimiterStatus) {
|
||||
cl.Lock()
|
||||
defer cl.Unlock()
|
||||
|
||||
if flatip.IPInNets(addr, cl.config.exemptedNets) {
|
||||
status.Exempt = true
|
||||
return
|
||||
}
|
||||
|
||||
status.ThrottleDuration = cl.config.Window
|
||||
|
||||
limiterKey, customID, maxConcurrent, maxPerWindow := cl.addrToKey(addr)
|
||||
status.MaxCount = maxConcurrent
|
||||
status.MaxPerWindow = maxPerWindow
|
||||
|
||||
status.Count = cl.limiter[limiterKey]
|
||||
status.Throttle = cl.throttler[limiterKey].Count
|
||||
|
||||
netName = customID
|
||||
if netName == "" {
|
||||
netName = flatip.IPNet{
|
||||
IP: limiterKey.maskedIP,
|
||||
PrefixLen: limiterKey.prefixLen,
|
||||
}.String()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ResetThrottle resets the throttle count for an IP
|
||||
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)
|
||||
return &cl
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer cl.Unlock()
|
||||
|
||||
if cl.limiter == nil {
|
||||
cl.limiter = make(map[limiterKey]int)
|
||||
}
|
||||
if cl.throttler == nil {
|
||||
cl.throttler = make(map[limiterKey]ThrottleDetails)
|
||||
cl.enabled = config.Enabled
|
||||
cl.ipv4Mask = net.CIDRMask(config.CidrLenIPv4, 32)
|
||||
cl.ipv6Mask = net.CIDRMask(config.CidrLenIPv6, 128)
|
||||
// subnetLimit is explicitly NOT capped at a minimum of one.
|
||||
// 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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -4,48 +4,178 @@
|
||||
package connection_limits
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"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.
|
||||
type ThrottleDetails struct {
|
||||
Start time.Time
|
||||
Count int
|
||||
Start time.Time
|
||||
ClientCount int
|
||||
}
|
||||
|
||||
// GenericThrottle allows enforcing limits of the form
|
||||
// "at most X events per time window of duration Y"
|
||||
type GenericThrottle struct {
|
||||
ThrottleDetails // variable state: what events have been seen
|
||||
// these are constant after creation:
|
||||
Duration time.Duration // window length to consider
|
||||
Limit int // number of events allowed per window
|
||||
// Throttler manages automated client connection throttling.
|
||||
type Throttler struct {
|
||||
sync.RWMutex
|
||||
|
||||
enabled bool
|
||||
ipv4Mask net.IPMask
|
||||
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:
|
||||
// it either denies it (by returning false) or allows it (by returning true)
|
||||
// and records it
|
||||
func (g *GenericThrottle) Touch() (throttled bool, remainingTime time.Duration) {
|
||||
return g.touch(time.Now().UTC())
|
||||
}
|
||||
|
||||
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)
|
||||
// maskAddr masks the given IPv4/6 address with our cidr limit masks.
|
||||
func (ct *Throttler) maskAddr(addr net.IP) net.IP {
|
||||
if addr.To4() == nil {
|
||||
// IPv6 addr
|
||||
addr = addr.Mask(ct.ipv6Mask)
|
||||
} else {
|
||||
// we are not throttled, record the operation
|
||||
g.Count += 1
|
||||
return false, 0
|
||||
// IPv4 addr
|
||||
addr = addr.Mask(ct.ipv4Mask)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
@ -5,7 +5,20 @@
|
||||
|
||||
package irc
|
||||
|
||||
import "fmt"
|
||||
|
||||
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.
|
||||
// for instance, in MONITOR lists, RPL_ISUPPORT lists, etc.
|
||||
maxLastArgLength = 400
|
||||
|
@ -75,9 +75,8 @@ var unitMap = map[string]int64{
|
||||
"m": int64(time.Minute),
|
||||
"h": int64(time.Hour),
|
||||
"d": int64(time.Hour * 24),
|
||||
"w": int64(time.Hour * 24 * 7),
|
||||
"mo": int64(time.Hour * 24 * 30),
|
||||
"y": int64(time.Hour * 24 * 365),
|
||||
"y": int64(time.Hour * 24 * 265),
|
||||
}
|
||||
|
||||
// ParseDuration parses a duration string.
|
||||
@ -182,18 +181,3 @@ func ParseDuration(s string) (time.Duration, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
1395
irc/database.go
1395
irc/database.go
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
339
irc/dline.go
339
irc/dline.go
@ -4,81 +4,84 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/flatip"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
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.
|
||||
type IPBanInfo struct {
|
||||
// RequireSASL indicates a "soft" ban; connections are allowed but they must SASL
|
||||
RequireSASL bool
|
||||
// Reason is the ban reason.
|
||||
Reason string `json:"reason"`
|
||||
// OperReason is an oper ban reason.
|
||||
OperReason string `json:"oper_reason"`
|
||||
// OperName is the oper who set the ban.
|
||||
OperName string `json:"oper_name"`
|
||||
// time of ban creation
|
||||
TimeCreated time.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()
|
||||
}
|
||||
// Time holds details about the duration, if it exists.
|
||||
Time *IPRestrictTime `json:"time"`
|
||||
}
|
||||
|
||||
// BanMessage returns the ban message.
|
||||
func (info IPBanInfo) BanMessage(message string) string {
|
||||
reason := info.Reason
|
||||
if reason == "" {
|
||||
reason = "No reason given"
|
||||
}
|
||||
message = fmt.Sprintf(message, reason)
|
||||
if info.Duration != 0 {
|
||||
message += fmt.Sprintf(" [%s]", info.TimeLeft())
|
||||
message = fmt.Sprintf(message, info.Reason)
|
||||
if info.Time != nil {
|
||||
message += fmt.Sprintf(" [%s]", info.Time.Duration.String())
|
||||
}
|
||||
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.
|
||||
type DLineManager struct {
|
||||
sync.RWMutex // tier 1
|
||||
persistenceMutex sync.Mutex // tier 2
|
||||
// networks that are dlined:
|
||||
networks map[flatip.IPNet]IPBanInfo
|
||||
// this keeps track of expiration timers for temporary bans
|
||||
expirationTimers map[flatip.IPNet]*time.Timer
|
||||
server *Server
|
||||
sync.RWMutex // tier 1
|
||||
// addresses that are dlined
|
||||
addresses map[string]*dLineAddr
|
||||
// networks that are dlined
|
||||
networks map[string]*dLineNet
|
||||
}
|
||||
|
||||
// NewDLineManager returns a new DLineManager.
|
||||
func NewDLineManager(server *Server) *DLineManager {
|
||||
func NewDLineManager() *DLineManager {
|
||||
var dm DLineManager
|
||||
dm.networks = make(map[flatip.IPNet]IPBanInfo)
|
||||
dm.expirationTimers = make(map[flatip.IPNet]*time.Timer)
|
||||
dm.server = server
|
||||
|
||||
dm.loadFromDatastore()
|
||||
|
||||
dm.addresses = make(map[string]*dLineAddr)
|
||||
dm.networks = make(map[string]*dLineNet)
|
||||
return &dm
|
||||
}
|
||||
|
||||
@ -89,192 +92,154 @@ func (dm *DLineManager) AllBans() map[string]IPBanInfo {
|
||||
dm.RLock()
|
||||
defer dm.RUnlock()
|
||||
|
||||
for key, info := range dm.networks {
|
||||
allb[key.HumanReadableString()] = info
|
||||
for name, info := range dm.addresses {
|
||||
allb[name] = info.Info
|
||||
}
|
||||
for name, info := range dm.networks {
|
||||
allb[name] = info.Info
|
||||
}
|
||||
|
||||
return allb
|
||||
}
|
||||
|
||||
// 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 {
|
||||
dm.persistenceMutex.Lock()
|
||||
defer dm.persistenceMutex.Unlock()
|
||||
|
||||
// assemble ban info
|
||||
info := IPBanInfo{
|
||||
RequireSASL: requireSASL,
|
||||
Reason: reason,
|
||||
OperReason: operReason,
|
||||
OperName: operName,
|
||||
TimeCreated: time.Now().UTC(),
|
||||
Duration: duration,
|
||||
func (dm *DLineManager) AddNetwork(network net.IPNet, length *IPRestrictTime, reason, operReason, operName string) {
|
||||
netString := network.String()
|
||||
dln := dLineNet{
|
||||
Network: network,
|
||||
Info: IPBanInfo{
|
||||
Time: length,
|
||||
Reason: reason,
|
||||
OperReason: operReason,
|
||||
OperName: operName,
|
||||
},
|
||||
}
|
||||
|
||||
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()
|
||||
defer 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
|
||||
})
|
||||
dm.networks[netString] = &dln
|
||||
dm.Unlock()
|
||||
}
|
||||
|
||||
// RemoveNetwork removes a network from the blocked list.
|
||||
func (dm *DLineManager) RemoveNetwork(network flatip.IPNet) error {
|
||||
dm.persistenceMutex.Lock()
|
||||
defer dm.persistenceMutex.Unlock()
|
||||
func (dm *DLineManager) RemoveNetwork(network net.IPNet) {
|
||||
netString := network.String()
|
||||
dm.Lock()
|
||||
delete(dm.networks, netString)
|
||||
dm.Unlock()
|
||||
}
|
||||
|
||||
id := network
|
||||
|
||||
present := func() bool {
|
||||
dm.Lock()
|
||||
defer dm.Unlock()
|
||||
_, ok := dm.networks[id]
|
||||
delete(dm.networks, id)
|
||||
dm.cancelTimer(id)
|
||||
return ok
|
||||
}()
|
||||
|
||||
if !present {
|
||||
return errNoExistingBan
|
||||
// AddIP adds an IP address to the blocked list.
|
||||
func (dm *DLineManager) AddIP(addr net.IP, length *IPRestrictTime, reason, operReason, operName string) {
|
||||
addrString := addr.String()
|
||||
dla := dLineAddr{
|
||||
Address: addr,
|
||||
Info: IPBanInfo{
|
||||
Time: length,
|
||||
Reason: reason,
|
||||
OperReason: operReason,
|
||||
OperName: operName,
|
||||
},
|
||||
}
|
||||
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.
|
||||
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()
|
||||
defer dm.RUnlock()
|
||||
|
||||
// check networks
|
||||
// TODO(slingamn) use a radix tree as the data plane for this
|
||||
for flatnet, info := range dm.networks {
|
||||
if flatnet.Contains(addr) {
|
||||
return true, info
|
||||
for _, netInfo := range dm.networks {
|
||||
if netInfo.Info.Time != nil && netInfo.Info.Time.IsExpired() {
|
||||
// expired ban, ignore and clean up later
|
||||
doCleanup = true
|
||||
} else if netInfo.Network.Contains(addr) {
|
||||
return true, &netInfo.Info
|
||||
}
|
||||
}
|
||||
// no matches!
|
||||
return
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (dm *DLineManager) loadFromDatastore() {
|
||||
dlinePrefix := fmt.Sprintf(keyDlineEntry, "")
|
||||
dm.server.store.View(func(tx *buntdb.Tx) error {
|
||||
tx.AscendGreaterOrEqual("", dlinePrefix, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, dlinePrefix) {
|
||||
return false
|
||||
}
|
||||
func (s *Server) loadDLines() {
|
||||
s.dlines = NewDLineManager()
|
||||
|
||||
// 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
|
||||
key = strings.TrimPrefix(key, dlinePrefix)
|
||||
key = key[len("bans.dline "):]
|
||||
|
||||
// load addr/net
|
||||
hostNet, err := flatip.ParseToNormalizedNet(key)
|
||||
var hostAddr net.IP
|
||||
var hostNet *net.IPNet
|
||||
_, hostNet, err := net.ParseCIDR(key)
|
||||
if err != nil {
|
||||
dm.server.logger.Error("internal", "bad dline cidr", err.Error())
|
||||
return true
|
||||
hostAddr = net.ParseIP(key)
|
||||
}
|
||||
|
||||
// load ban info
|
||||
var info IPBanInfo
|
||||
err = json.Unmarshal([]byte(value), &info)
|
||||
if err != nil {
|
||||
dm.server.logger.Error("internal", "bad dline data", err.Error())
|
||||
return true
|
||||
}
|
||||
json.Unmarshal([]byte(value), &info)
|
||||
|
||||
// set opername if it isn't already set
|
||||
if info.OperName == "" {
|
||||
info.OperName = dm.server.name
|
||||
info.OperName = s.name
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) loadDLines() {
|
||||
s.dlines = NewDLineManager(s)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
112
irc/errors.go
112
irc/errors.go
@ -5,79 +5,42 @@
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
import "errors"
|
||||
|
||||
// Runtime Errors
|
||||
var (
|
||||
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")
|
||||
errAccountAlreadyRegistered = errors.New("Account already exists")
|
||||
errAccountCreation = errors.New("Account could not be created")
|
||||
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")
|
||||
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")
|
||||
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
|
||||
errAccountUpdateFailed = errors.New(`Error while updating your account information`)
|
||||
errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`)
|
||||
errAuthRequired = errors.New("You must be logged into an account to do this")
|
||||
errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`)
|
||||
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
|
||||
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
||||
errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`)
|
||||
errAccountUnverified = errors.New("Account is not yet verified")
|
||||
errAccountAlreadyVerified = errors.New("Account is already verified")
|
||||
errAccountInvalidCredentials = errors.New("Invalid account credentials")
|
||||
errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
|
||||
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
|
||||
errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname")
|
||||
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")
|
||||
errChannelNotRegistered = errors.New("Channel is not registered")
|
||||
errChannelNameInUse = errors.New(`Channel name in use`)
|
||||
errInvalidChannelName = errors.New(`Invalid channel name`)
|
||||
errChannelNameInUse = errors.New("Channel name in use")
|
||||
errInvalidChannelName = errors.New("Invalid channel name")
|
||||
errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
|
||||
errNickMissing = errors.New("nick missing")
|
||||
errNicknameInvalid = errors.New("invalid nickname")
|
||||
errNicknameInUse = errors.New("nickname in use")
|
||||
errInsecureReattach = errors.New("insecure reattach")
|
||||
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")
|
||||
errNoSuchChannel = errors.New(`No such channel`)
|
||||
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
||||
errChannelPurgedAlready = errors.New(`This channel was already purged and cannot be purged again`)
|
||||
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use")
|
||||
errInsufficientPrivs = errors.New("Insufficient privileges")
|
||||
errInvalidUsername = errors.New("Invalid username")
|
||||
errFeatureDisabled = errors.New(`That feature is disabled`)
|
||||
errBanned = errors.New("IP or nickmask banned")
|
||||
errInvalidParams = utils.ErrInvalidParams
|
||||
errNoVhost = errors.New(`You do not have an approved vhost`)
|
||||
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")
|
||||
errNoSuchChannel = errors.New("No such channel")
|
||||
errRenamePrivsNeeded = errors.New("Only chanops can rename channels")
|
||||
errSaslFail = errors.New("SASL failed")
|
||||
)
|
||||
|
||||
// Socket Errors
|
||||
var (
|
||||
errNoPeerCerts = errors.New("Client did not provide a certificate")
|
||||
errNotTLS = errors.New("Not a TLS connection")
|
||||
errReadQ = errors.New("ReadQ Exceeded")
|
||||
)
|
||||
|
||||
// String Errors
|
||||
@ -87,18 +50,19 @@ var (
|
||||
errInvalidCharacter = errors.New("Invalid character")
|
||||
)
|
||||
|
||||
type CertKeyError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (ck *CertKeyError) Error() string {
|
||||
return fmt.Sprintf("Invalid TLS cert/key pair: %v", ck.Err)
|
||||
}
|
||||
|
||||
type ThrottleError struct {
|
||||
time.Duration
|
||||
}
|
||||
|
||||
func (te *ThrottleError) Error() string {
|
||||
return fmt.Sprintf(`Please wait at least %v and try again`, te.Duration.Round(time.Millisecond))
|
||||
}
|
||||
// Config Errors
|
||||
var (
|
||||
ErrDatastorePathMissing = errors.New("Datastore path missing")
|
||||
ErrInvalidCertKeyPair = errors.New("tls cert+key: invalid pair")
|
||||
ErrLimitsAreInsane = errors.New("Limits aren't setup properly, check them and make them sane")
|
||||
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")
|
||||
ErrLoggerHasNoTypes = errors.New("Logger has no types to log")
|
||||
ErrNetworkNameMissing = errors.New("Network name missing")
|
||||
ErrNoFingerprintOrPassword = errors.New("Fingerprint or password needs to be specified")
|
||||
ErrNoListenersDefined = errors.New("Server listening addresses missing")
|
||||
ErrOperClassDependencies = errors.New("OperClasses contains a looping dependency, or a class extends from a class that doesn't exist")
|
||||
ErrServerNameMissing = errors.New("Server name missing")
|
||||
ErrServerNameNotHostname = errors.New("Server name must match the format of a hostname")
|
||||
)
|
||||
|
@ -4,7 +4,6 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -25,51 +24,33 @@ const (
|
||||
// this is intentionally not threadsafe, because it should only be touched
|
||||
// from the loop that accepts the client's input and runs commands
|
||||
type Fakelag struct {
|
||||
config FakelagConfig
|
||||
suspended bool
|
||||
nowFunc func() time.Time
|
||||
sleepFunc func(time.Duration)
|
||||
window time.Duration
|
||||
burstLimit uint
|
||||
throttleMessagesPerWindow uint
|
||||
cooldown time.Duration
|
||||
nowFunc func() time.Time
|
||||
sleepFunc func(time.Duration)
|
||||
|
||||
state FakelagState
|
||||
burstCount uint // number of messages sent in the current burst
|
||||
lastTouch time.Time
|
||||
}
|
||||
|
||||
func (fl *Fakelag) Initialize(config FakelagConfig) {
|
||||
fl.config = config
|
||||
// XXX don't share mutable member CommandBudgets:
|
||||
if config.CommandBudgets != nil {
|
||||
fl.config.CommandBudgets = maps.Clone(config.CommandBudgets)
|
||||
}
|
||||
fl.nowFunc = time.Now
|
||||
fl.sleepFunc = time.Sleep
|
||||
fl.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
|
||||
func NewFakelag(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint, cooldown time.Duration) *Fakelag {
|
||||
return &Fakelag{
|
||||
window: window,
|
||||
burstLimit: burstLimit,
|
||||
throttleMessagesPerWindow: throttleMessagesPerWindow,
|
||||
cooldown: cooldown,
|
||||
nowFunc: time.Now,
|
||||
sleepFunc: time.Sleep,
|
||||
state: FakelagBursting,
|
||||
}
|
||||
}
|
||||
|
||||
// register a new command, sleep if necessary to delay it
|
||||
func (fl *Fakelag) Touch(command string) {
|
||||
if !fl.config.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if budget, ok := fl.config.CommandBudgets[command]; ok && budget > 0 {
|
||||
fl.config.CommandBudgets[command] = budget - 1
|
||||
func (fl *Fakelag) Touch() {
|
||||
if fl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -80,12 +61,12 @@ func (fl *Fakelag) Touch(command string) {
|
||||
|
||||
if fl.state == FakelagBursting {
|
||||
// determine if the previous burst is over
|
||||
if elapsed > fl.config.Cooldown {
|
||||
if elapsed > fl.cooldown {
|
||||
fl.burstCount = 0
|
||||
}
|
||||
|
||||
fl.burstCount++
|
||||
if fl.burstCount > fl.config.BurstLimit {
|
||||
if fl.burstCount > fl.burstLimit {
|
||||
// reset burst window for next time
|
||||
fl.burstCount = 0
|
||||
// transition to throttling
|
||||
@ -97,27 +78,16 @@ func (fl *Fakelag) Touch(command string) {
|
||||
}
|
||||
|
||||
if fl.state == FakelagThrottled {
|
||||
if elapsed > fl.config.Cooldown {
|
||||
if elapsed > fl.cooldown {
|
||||
// let them burst again
|
||||
fl.state = FakelagBursting
|
||||
fl.burstCount = 1
|
||||
return
|
||||
}
|
||||
var sleepDuration time.Duration
|
||||
if fl.config.MessagesPerWindow > 0 {
|
||||
// space them out by at least window/messagesperwindow
|
||||
sleepDuration = time.Duration((int64(fl.config.Window) / int64(fl.config.MessagesPerWindow)) - int64(elapsed))
|
||||
} else {
|
||||
// only burst messages are allowed: sleep until cooldown expires,
|
||||
// then count this as a burst message
|
||||
sleepDuration = time.Duration(int64(fl.config.Cooldown) - int64(elapsed))
|
||||
fl.state = FakelagBursting
|
||||
fl.burstCount = 1
|
||||
}
|
||||
if sleepDuration > 0 {
|
||||
fl.sleepFunc(sleepDuration)
|
||||
// the touch time should take into account the time we slept
|
||||
fl.lastTouch = fl.nowFunc()
|
||||
// space them out by at least window/messagesperwindow
|
||||
sleepDuration := time.Duration((int64(fl.window) / int64(fl.throttleMessagesPerWindow)) - int64(elapsed))
|
||||
if sleepDuration < 0 {
|
||||
sleepDuration = 0
|
||||
}
|
||||
fl.sleepFunc(sleepDuration)
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
fl := Fakelag{}
|
||||
fl.config = FakelagConfig{
|
||||
Enabled: true,
|
||||
Window: window,
|
||||
BurstLimit: burstLimit,
|
||||
MessagesPerWindow: throttleMessagesPerWindow,
|
||||
Cooldown: cooldown,
|
||||
}
|
||||
fl := NewFakelag(window, burstLimit, throttleMessagesPerWindow, cooldown)
|
||||
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.lastCheckedSleep = -1
|
||||
fl.nowFunc = mt.Now
|
||||
fl.sleepFunc = mt.Sleep
|
||||
return &fl, mt
|
||||
return fl, mt
|
||||
}
|
||||
|
||||
func TestFakelag(t *testing.T) {
|
||||
window, _ := time.ParseDuration("1s")
|
||||
fl, mt := newFakelagForTesting(window, 3, 2, window)
|
||||
|
||||
fl.Touch("")
|
||||
fl.Touch()
|
||||
slept, _ := mt.lastSleep()
|
||||
if slept {
|
||||
t.Fatalf("should not have slept")
|
||||
@ -69,7 +62,7 @@ func TestFakelag(t *testing.T) {
|
||||
interval, _ := time.ParseDuration("100ms")
|
||||
for i := 0; i < 2; i++ {
|
||||
mt.pause(interval)
|
||||
fl.Touch("")
|
||||
fl.Touch()
|
||||
slept, _ := mt.lastSleep()
|
||||
if slept {
|
||||
t.Fatalf("should not have slept")
|
||||
@ -77,7 +70,7 @@ func TestFakelag(t *testing.T) {
|
||||
}
|
||||
|
||||
mt.pause(interval)
|
||||
fl.Touch("")
|
||||
fl.Touch()
|
||||
if fl.state != FakelagThrottled {
|
||||
t.Fatalf("should be throttled")
|
||||
}
|
||||
@ -90,19 +83,17 @@ func TestFakelag(t *testing.T) {
|
||||
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 {
|
||||
t.Fatalf("should be throttled")
|
||||
}
|
||||
slept, duration = mt.lastSleep()
|
||||
expected, _ = time.ParseDuration("500ms")
|
||||
if duration != expected {
|
||||
t.Fatalf("incorrect sleep time: %v != %v", duration, expected)
|
||||
if duration != interval {
|
||||
t.Fatalf("incorrect sleep time: %v != %v", interval, duration)
|
||||
}
|
||||
|
||||
mt.pause(interval * 6)
|
||||
fl.Touch("")
|
||||
fl.Touch()
|
||||
if fl.state != FakelagThrottled {
|
||||
t.Fatalf("should still be throttled")
|
||||
}
|
||||
@ -112,7 +103,7 @@ func TestFakelag(t *testing.T) {
|
||||
}
|
||||
|
||||
mt.pause(window * 2)
|
||||
fl.Touch("")
|
||||
fl.Touch()
|
||||
if fl.state != FakelagBursting {
|
||||
t.Fatalf("should be bursting again")
|
||||
}
|
||||
@ -121,35 +112,3 @@ func TestFakelag(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
Loading…
x
Reference in New Issue
Block a user