mirror of
https://github.com/ergochat/ergo.git
synced 2025-08-06 04:37:28 +02:00
Compare commits
No commits in common. "master" and "v0.9.2-alpha" have entirely different histories.
master
...
v0.9.2-alp
@ -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 }}
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -95,7 +95,7 @@ _testmain.go
|
||||
*.out
|
||||
|
||||
|
||||
### custom ###
|
||||
### Oragono ###
|
||||
/_site/
|
||||
/.vscode/*
|
||||
/ircd*
|
||||
@ -103,11 +103,6 @@ _testmain.go
|
||||
/web.*
|
||||
/ssl.*
|
||||
/tls.*
|
||||
/ergo
|
||||
/oragono
|
||||
/build/*
|
||||
_test
|
||||
ergo.prof
|
||||
ergo.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 +0,0 @@
|
||||
# .goreleaser.yml
|
||||
# Build customization
|
||||
version: 2
|
||||
project_name: ergo
|
||||
builds:
|
||||
- main: ergo.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
binary: ergo
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- freebsd
|
||||
- openbsd
|
||||
- plan9
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
goarm:
|
||||
- 6
|
||||
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
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt"
|
7
.travis.yml
Normal file
7
.travis.yml
Normal file
@ -0,0 +1,7 @@
|
||||
language: go
|
||||
|
||||
install: make deps
|
||||
|
||||
script:
|
||||
- make
|
||||
- make test
|
1458
CHANGELOG.md
1458
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
223
DEVELOPING.md
223
DEVELOPING.md
@ -1,213 +1,44 @@
|
||||
# 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.
|
||||
|
||||
The intent is to keep `master` relatively stable.
|
||||
|
||||
|
||||
## Golang issues
|
||||
## Updating `vendor/`
|
||||
|
||||
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.)
|
||||
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.
|
||||
|
||||
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.
|
||||
To update this folder:
|
||||
|
||||
If you're upgrading the Go version used by Ergo, there are several places where it's hard-coded and must be changed:
|
||||
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"`
|
||||
|
||||
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
|
||||
This will make sure things stay nice and up-to-date for users.
|
||||
|
||||
|
||||
## Branches
|
||||
## Fuzzing and Testing
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
|
||||
## Workflow
|
||||
## Debugging Hangs
|
||||
|
||||
We have two test suites:
|
||||
|
||||
1. `make test`, which runs some relatively shallow unit tests, checks `go vet`, and does some other internal consistency checks
|
||||
1. `make irctest`, which runs the [irctest](https://github.com/ProgVal/irctest) integration test suite
|
||||
|
||||
Barring special circumstances, both must pass for a PR to be accepted. irctest will test the `ergo` binary visible on `$PATH`; make sure your development version is the one being tested. (If you have `~/go/bin` on your `$PATH`, a successful `make install` will accomplish this.)
|
||||
|
||||
The project style is [gofmt](https://go.dev/blog/gofmt); it is enforced by `make test`. You can fix any style issues automatically by running `make gofmt`.
|
||||
|
||||
|
||||
## Updating dependencies
|
||||
|
||||
Ergo vendors all dependencies using `go mod vendor`. To update a dependency, or add a new one:
|
||||
|
||||
1. `go get -v bazbat.com/path/to/dependency` ; this downloads the new dependency
|
||||
2. `go mod vendor` ; this writes the dependency's source files to the `vendor/` directory
|
||||
3. `git add go.mod go.sum vendor/` ; this stages all relevant changes to the vendor directory, including file deletions. Take care that spurious changes (such as editor swapfiles) aren't added.
|
||||
4. `git commit`
|
||||
|
||||
|
||||
## Releasing a new version
|
||||
|
||||
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?
|
||||
|
||||
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"`.
|
||||
|
||||
**Unreleased changelog content**
|
||||
|
||||
```md
|
||||
## Unreleased
|
||||
New release of Ergo!
|
||||
|
||||
### Config Changes
|
||||
|
||||
### Security
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Debugging
|
||||
|
||||
It's helpful to enable all loglines while developing. Here's how to configure this:
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
-
|
||||
method: stderr
|
||||
type: "*"
|
||||
level: debug
|
||||
```
|
||||
|
||||
To debug a hang, the best thing to do is to get a stack trace. The easiest way to get stack traces is with the [pprof listener](https://golang.org/pkg/net/http/pprof/), which can be enabled in the `debug` section of the config. Once it's enabled, you can navigate to `http://localhost:6060/debug/pprof/` in your browser and go from there. If that doesn't work, try:
|
||||
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.
|
||||
|
||||
|
||||
## Concurrency design
|
||||
|
||||
Ergo 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`.
|
||||
1. The server has a few of its own goroutines, for listening on sockets and handing off new client connections to their dedicated goroutines.
|
||||
1. A few tasks are done asynchronously in ad-hoc goroutines.
|
||||
|
||||
In consequence, there is a lot of state (in particular, server and channel state) that can be read and written from multiple goroutines. This state is protected with mutexes. To avoid deadlocks, mutexes are arranged in "tiers"; while holding a mutex of one tier, you're only allowed to acquire mutexes of a strictly *higher* tier. The tiers are:
|
||||
|
||||
1. Tier 1 mutexes: these are the "innermost" mutexes. They typically protect getters and setters on objects, or invariants that are local to the state of a single object. Example: `Channel.stateMutex`.
|
||||
1. Tier 2 mutexes: these protect some invariants of their own, but also need to access fields on other objects that themselves require synchronization. Example: `ChannelManager.RWMutex`.
|
||||
1. Tier 3 mutexes: these protect macroscopic operations, where it doesn't make sense for more than one to occur concurrently. Example; `Server.rehashMutex`, which prevents rehashes from overlapping.
|
||||
|
||||
There are some mutexes that are "tier 0": anything in a subpackage of `irc` (e.g., `irc/logger` or `irc/connection_limits`) shouldn't acquire mutexes defined in `irc`.
|
||||
|
||||
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).
|
||||
|
||||
With labeled responses, when a client sends a label along with their command, they are assured that they will receive the response messages with that same label.
|
||||
|
||||
For example, if the client sends this to the server:
|
||||
|
||||
@label=pQraCjj82e PRIVMSG #channel :hi!
|
||||
|
||||
They will expect to receive this (with echo-message also enabled):
|
||||
|
||||
@label=pQraCjj82e :nick!user@host PRIVMSG #channel :hi!
|
||||
|
||||
They receive the response with the same label, so they can match the sent command to the received response. They can also do the same with any other command.
|
||||
|
||||
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
|
||||
This will kill Oragono and print out a stack trace for you to take a look at.
|
||||
|
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
|
132
Gopkg.lock
generated
Normal file
132
Gopkg.lock
generated
Normal file
@ -0,0 +1,132 @@
|
||||
memo = "2e64ed5ab0c87a6416bd5d57e50cf8391e9e0d581421083a375cc930d1e2fe92"
|
||||
|
||||
[[projects]]
|
||||
name = "code.cloudfoundry.org/bytefmt"
|
||||
packages = ["."]
|
||||
revision = "cbe033486cf0620d3bb77d8ef7f22ab346ad3628"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docopt/docopt-go"
|
||||
packages = ["."]
|
||||
revision = "784ddc588536785e7299f7272f39101f7faccc3f"
|
||||
version = "0.6.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/context"
|
||||
packages = ["."]
|
||||
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
|
||||
version = "v1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "3f19343c7d9ce75569b952758bd236af94956061"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
revision = "6f34763140ed8887aed6a044912009832b4733d7"
|
||||
|
||||
[[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 = "a5eafb7ec8061edf1ee666c8eb3b39bba3fe072f"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
revision = "ad5389df28cdac544c99bd7b9161a0b5b6ca9d1b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[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 = "9cf49ba87a179fe027efa8b0d6b0235cb9cbc7fe"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/btree"
|
||||
packages = ["."]
|
||||
revision = "9876f1454cf0993a53d74c27196993e345f50dd1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/buntdb"
|
||||
packages = ["."]
|
||||
revision = "b67b1b8c1658cb01502801c14e33c61e6c4cbb95"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/tidwall/gjson"
|
||||
packages = ["."]
|
||||
revision = "be96719f990978a867f52c48f29d43f6b591da28"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/grect"
|
||||
packages = ["."]
|
||||
revision = "ba9a043346eba55344e40d66a5e74cfda3a9d293"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/match"
|
||||
packages = ["."]
|
||||
revision = "173748da739a410c5b0b813b956f89ff94730b4c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/rtree"
|
||||
packages = ["."]
|
||||
revision = "d4a8a3d30d5729f85edfba1745241f3a621d0359"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["bcrypt","blowfish","ssh/terminal"]
|
||||
revision = "7d9177d70076375b9a59c8fde23d52d9c4a7ecd5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "429f518978ab01db8bb6f44b66785088e7fba58b"
|
||||
|
||||
[[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 = "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
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,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-2014 Jeremy Latt
|
||||
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
|
||||
|
84
Makefile
84
Makefile
@ -1,48 +1,54 @@
|
||||
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null)
|
||||
GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1)
|
||||
BUILD=./build
|
||||
WIN=$(BUILD)/win
|
||||
LINUX=$(BUILD)/linux
|
||||
OSX=$(BUILD)/osx
|
||||
ARM6=$(BUILD)/arm
|
||||
SOURCE=oragono.go
|
||||
VERS=XXX
|
||||
|
||||
# disable linking against native libc / libpthread by default;
|
||||
# this can be overridden by passing CGO_ENABLED=1 to make
|
||||
export CGO_ENABLED ?= 0
|
||||
.PHONY: all clean windows osx linux arm6
|
||||
|
||||
capdef_file = ./irc/caps/defs.go
|
||||
add-files = mkdir -p $1; \
|
||||
cp oragono.yaml $1; \
|
||||
cp oragono.motd $1; \
|
||||
cp LICENSE $1; \
|
||||
cp ./docs/README $1; \
|
||||
mkdir -p $1/docs; \
|
||||
cp ./CHANGELOG.md $1/docs/; \
|
||||
cp ./docs/logo* $1/docs/;
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
all: clean windows osx linux arm6
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||
clean:
|
||||
rm -rf $(BUILD)
|
||||
mkdir -p $(BUILD)
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||
windows:
|
||||
GOOS=windows GOARCH=amd64 go build $(SOURCE)
|
||||
$(call add-files,$(WIN))
|
||||
mv oragono.exe $(WIN)
|
||||
cd $(WIN) && zip -r ../oragono-$(VERS)-windows.zip *
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
goreleaser --skip=publish --clean
|
||||
osx:
|
||||
GOOS=darwin GOARCH=amd64 go build oragono.go
|
||||
$(call add-files,$(OSX))
|
||||
mv oragono $(OSX)
|
||||
cd $(OSX) && tar -czvf ../oragono-$(VERS)-osx.tgz *
|
||||
|
||||
.PHONY: capdefs
|
||||
capdefs:
|
||||
python3 ./gencapdefs.py > ${capdef_file}
|
||||
linux:
|
||||
GOOS=linux GOARCH=amd64 go build oragono.go
|
||||
$(call add-files,$(LINUX))
|
||||
mv oragono $(LINUX)
|
||||
cd $(LINUX) && tar -czvf ../oragono-$(VERS)-linux.tgz *
|
||||
|
||||
arm6:
|
||||
GOARM=6 GOARCH=arm go build oragono.go
|
||||
$(call add-files,$(ARM6))
|
||||
mv oragono $(ARM6)
|
||||
cd $(ARM6) && tar -czvf ../oragono-$(VERS)-arm.tgz *
|
||||
|
||||
deps:
|
||||
go get -v -d
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
python3 ./gencapdefs.py | diff - ${capdef_file}
|
||||
go test ./...
|
||||
go vet ./...
|
||||
./.check-gofmt.sh
|
||||
|
||||
.PHONY: smoke
|
||||
smoke: install
|
||||
ergo mkcerts --conf ./default.yaml || true
|
||||
ergo run --conf ./default.yaml --smoke
|
||||
|
||||
.PHONY: gofmt
|
||||
gofmt:
|
||||
./.check-gofmt.sh --fix
|
||||
|
||||
.PHONY: irctest
|
||||
irctest: install
|
||||
git submodule update --init
|
||||
cd irctest && make ergo
|
||||
cd irc && go test .
|
||||
|
62
README
62
README
@ -1,62 +0,0 @@
|
||||
___ _ __ __ _ ___
|
||||
/ _ \ '__/ _` |/ _ \
|
||||
| __/ | | (_| | (_) |
|
||||
\___|_| \__, |\___/
|
||||
__/ |
|
||||
|___/
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
Ergo is a modern IRC server written in Go. Its core design principles are:
|
||||
|
||||
* 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
|
||||
|
||||
https://ergo.chat/
|
||||
https://github.com/ergochat/ergo
|
||||
#ergo on irc.ergo.chat or irc.libera.chat
|
||||
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
=== Installing ===
|
||||
|
||||
Copy the example config file to ircd.yaml with a command like:
|
||||
|
||||
$ cp default.yaml ircd.yaml
|
||||
|
||||
Modify the config file as needed (the recommendations at the top may be helpful).
|
||||
|
||||
To generate passwords for opers and connect passwords, you can use this command:
|
||||
|
||||
$ ./ergo genpasswd
|
||||
|
||||
If you need to generate self-signed TLS certificates, use this command:
|
||||
|
||||
$ ./ergo mkcerts
|
||||
|
||||
You are now ready to start Ergo!
|
||||
|
||||
$ ./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
|
||||
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!
|
||||
|
||||
=== 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
|
166
README.md
166
README.md
@ -1,127 +1,161 @@
|
||||

|
||||

|
||||
|
||||
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)
|
||||
|
||||
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).
|
||||
[](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)
|
||||
|
||||
---
|
||||
|
||||
This project adheres to [Semantic Versioning](http://semver.org/). For the purposes of versioning, we consider the "public API" to refer to the configuration files, CLI interface and database format.
|
||||
|
||||
# Oragono
|
||||
|
||||
## 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
|
||||
* [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`
|
||||
* [IRCv3 support](http://ircv3.net/software/servers.html)
|
||||
* a heavy focus on developing with [specifications](http://oragono.io/specs.html)
|
||||
* integrated (alpha) REST API and web interface
|
||||
|
||||
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 unsigned 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. 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 `/quote ACC REGISTER <username> * passphrase :<password>`
|
||||
2. Join the channel with `/join #channel`
|
||||
3. Register the channel with `/msg ChanServ 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.
|
||||
|
||||
|
||||
<!--# Web interface
|
||||
|
||||
Oragono also includes a web interface, which works with the REST API to provide a way to manage user accounts and bans.
|
||||
|
||||
This interface is an early alpha, is in no way secure and will not be in a final release for a while. Requires the alpha REST API to be enabled (check your server config to enable that if you really want to).
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
go build oragono-web.go
|
||||
cp oragono-web.yaml web.yaml
|
||||
vim web.yaml # modify the config file to your liking
|
||||
oragono-web mkcerts
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
oragono-web run
|
||||
```-->
|
||||
|
||||
# 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>
|
||||
* apologies to anyone I forgot.
|
||||
|
67
cmd/oragono-web/oragono-web.go
Normal file
67
cmd/oragono-web/oragono-web.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/docopt/docopt-go"
|
||||
"github.com/oragono/oragono/mkcerts"
|
||||
"github.com/oragono/oragono/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
version := irc.SemVer
|
||||
usage := `oragono-web.
|
||||
Usage:
|
||||
oragono-web mkcerts [--conf <filename>] [--quiet]
|
||||
oragono-web run [--conf <filename>] [--quiet]
|
||||
oragono-web -h | --help
|
||||
oragono-web --version
|
||||
Options:
|
||||
--conf <filename> Configuration file to use [default: web.yaml].
|
||||
--quiet Don't show startup/shutdown lines.
|
||||
-h --help Show this screen.
|
||||
--version Show version.`
|
||||
|
||||
arguments, _ := docopt.Parse(usage, nil, true, version, false)
|
||||
|
||||
configfile := arguments["--conf"].(string)
|
||||
config, err := web.LoadConfig(configfile)
|
||||
if err != nil {
|
||||
log.Fatal("Config file did not load successfully:", err.Error())
|
||||
}
|
||||
|
||||
if arguments["mkcerts"].(bool) {
|
||||
if !arguments["--quiet"].(bool) {
|
||||
log.Println("making self-signed certificates")
|
||||
}
|
||||
|
||||
for name, conf := range config.TLSListenersConf {
|
||||
log.Printf(" making cert for %s listener\n", name)
|
||||
host := config.Host
|
||||
err := mkcerts.CreateCert("Oragono web interface", host, conf.Cert, conf.Key)
|
||||
if err == nil {
|
||||
if !arguments["--quiet"].(bool) {
|
||||
log.Printf(" Certificate created at %s : %s\n", conf.Cert, conf.Key)
|
||||
}
|
||||
} else {
|
||||
log.Fatal(" Could not create certificate:", err.Error())
|
||||
}
|
||||
}
|
||||
} else if arguments["run"].(bool) {
|
||||
irc.Log.SetLevel(config.Log)
|
||||
server := web.NewServer(config)
|
||||
if server == nil {
|
||||
log.Println("Could not load server")
|
||||
return
|
||||
}
|
||||
if !arguments["--quiet"].(bool) {
|
||||
log.Println(fmt.Sprintf("Oragono web interface v%s running", irc.SemVer))
|
||||
defer log.Println(irc.SemVer, "exiting")
|
||||
}
|
||||
server.Run()
|
||||
}
|
||||
}
|
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
|
107
docs/INFO.md
Normal file
107
docs/INFO.md
Normal file
@ -0,0 +1,107 @@
|
||||
# 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!
|
||||
|
||||
|
||||
## REST API
|
||||
|
||||
Oragono contains a draft, very early REST API implementation. My plans for this is to allow
|
||||
external web interfaces or other automated programs to monitor what's going on with the
|
||||
server, apply/remove bans, and to essentially allow administration of the server without
|
||||
being connected to it and opered-up. This sort of API mimics InspIRCd and Anope, which
|
||||
contain similar APIs.
|
||||
|
||||
I'm not sure exactly how it's going to continue to be developed, and I'm sure there'll be
|
||||
lots of changes around appropriately restricting access to the API, which is why it's
|
||||
disabled for now and not exposed in our Docker builds. As well, while it's very unstable,
|
||||
the REST API doesn't count for our SemVer versioning. When this feature is more developed
|
||||
and I'm happy with where it's at, I'll provide proper support and documentation for the API.
|
||||
|
||||
|
||||
## 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 :)
|
1242
docs/MANUAL.md
1242
docs/MANUAL.md
File diff suppressed because it is too large
Load Diff
@ -1,60 +0,0 @@
|
||||
# MOTD Formatting Codes
|
||||
|
||||
If `motd-formatting` is enabled in the config file, you can use special escape codes to
|
||||
easily get bold, coloured, italic, and other types of specially-formatted text.
|
||||
|
||||
Our formatting character is '$', and this followed by specific letters means that the text
|
||||
after it is formatted in the given way. Here are the character pairs and what they output:
|
||||
|
||||
--------------------------
|
||||
Escape | Output
|
||||
--------------------------
|
||||
$$ | Dollar sign ($)
|
||||
$b | Bold
|
||||
$c | Color code
|
||||
$i | Italics
|
||||
$u | Underscore
|
||||
$r | Reset
|
||||
--------------------------
|
||||
|
||||
|
||||
## Color codes
|
||||
|
||||
After the color code (`$c`), you can use square brackets to specify which foreground and
|
||||
background colors to output. For example:
|
||||
|
||||
This line outputs red text:
|
||||
`This is $c[red]really cool text!`
|
||||
|
||||
This line outputs red text with a light blue background:
|
||||
`This is $c[red,light blue]22% cooler!`
|
||||
|
||||
If you're familiar with IRC colors you can also use the raw numbers you're used to:
|
||||
`This is $c13pink text`
|
||||
|
||||
Here are the color names we support, and which IRC colors they map to:
|
||||
|
||||
--------------------
|
||||
Code | Name
|
||||
--------------------
|
||||
00 | white
|
||||
01 | black
|
||||
02 | blue
|
||||
03 | green
|
||||
04 | red
|
||||
05 | brown
|
||||
06 | magenta
|
||||
07 | orange
|
||||
08 | yellow
|
||||
09 | light green
|
||||
10 | cyan
|
||||
11 | light cyan
|
||||
12 | light blue
|
||||
13 | pink
|
||||
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
|
51
docs/README
Normal file
51
docs/README
Normal file
@ -0,0 +1,51 @@
|
||||
|
||||
▄▄▄ ▄▄▄· ▄▄ • ▐ ▄
|
||||
▪ ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪ •█▌▐█▪
|
||||
▄█▀▄ ▐▀▀▄ ▄█▀▀█ ▄█ ▀█▄ ▄█▀▄▪▐█▐▐▌ ▄█▀▄
|
||||
▐█▌.▐▌▐█•█▌▐█ ▪▐▌▐█▄▪▐█▐█▌ ▐▌██▐█▌▐█▌.▐▌
|
||||
▀█▄▀▪.▀ ▀ ▀ ▀ ·▀▀▀▀ ▀█▄▀ ▀▀ █▪ ▀█▄▀▪
|
||||
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
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.
|
||||
|
||||
It includes features such as UTF-8 nicks and channel names, client accounts and SASL, and other
|
||||
assorted IRCv3 support.
|
||||
|
||||
https://oragono.io/
|
||||
https://github.com/oragono/oragono
|
||||
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
=== Installing ===
|
||||
|
||||
Copy the example config file to ircd.yaml with a command like:
|
||||
|
||||
$ cp oragono.yaml ircd.yaml
|
||||
|
||||
Modify the config file as you like.
|
||||
|
||||
To generate passwords for opers and connect passwords, you can use this command:
|
||||
|
||||
$ oragono genpasswd
|
||||
|
||||
Run these commands in order -- these will setup each section of the server:
|
||||
|
||||
$ oragono initdb
|
||||
$ oragono mkcerts
|
||||
$ oragono run
|
||||
|
||||
And you should now be running Oragono!
|
||||
|
||||
|
||||
=== Updating ===
|
||||
|
||||
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
|
@ -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: 5.0 KiB |
@ -1,2 +1,38 @@
|
||||
<?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>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="200" viewBox="0, 0, 800, 200">
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<path d="M86.334,81.496 L86.334,100.351 L66.729,100.351 L66.729,118.504 L56.935,118.504 L56.935,152.773 L66.729,152.773 L66.729,171.627 L86.334,171.627 L86.334,189.78 L47.125,189.78 L47.125,171.627 L27.521,171.627 L27.521,154.142 L17.727,154.142 L17.727,117.135 L27.521,117.135 L27.521,100.351 L47.125,100.351 L47.125,81.496 L86.334,81.496 z" fill="#000000"/>
|
||||
<path d="M537.229,81.496 L537.229,100.351 L517.625,100.351 L517.625,118.504 L507.831,118.504 L507.831,152.773 L517.625,152.773 L517.625,171.627 L537.229,171.627 L537.229,189.78 L498.021,189.78 L498.021,171.627 L478.417,171.627 L478.417,154.142 L468.623,154.142 L468.623,117.135 L478.417,117.135 L478.417,100.351 L498.021,100.351 L498.021,81.496 L537.229,81.496 z" fill="#000000"/>
|
||||
<path d="M615.646,10.22 L615.646,45.858 L625.456,45.858 L625.456,81.496 L635.25,81.496 L635.25,117.135 L645.06,117.135 L645.06,47.227 L635.25,47.227 L635.25,29.075 L654.854,29.075 L654.854,45.858 L674.458,45.858 L674.458,82.865 L664.664,82.865 L664.664,154.142 L654.854,154.142 L654.854,189.78 L635.25,189.78 L635.25,154.142 L625.456,154.142 L625.456,82.865 L615.646,82.865 L615.646,171.627 L576.437,171.627 L576.437,117.135 L586.247,117.135 L586.247,81.496 L596.041,81.496 L596.041,45.858 L605.852,45.858 L605.852,10.22 L615.646,10.22 z" fill="#000000"/>
|
||||
<path d="M752.875,81.496 L752.875,100.351 L733.271,100.351 L733.271,118.504 L723.477,118.504 L723.477,152.773 L733.271,152.773 L733.271,171.627 L752.875,171.627 L752.875,189.78 L713.667,189.78 L713.667,171.627 L694.063,171.627 L694.063,154.142 L684.268,154.142 L684.268,117.135 L694.063,117.135 L694.063,100.351 L713.667,100.351 L713.667,81.496 L752.875,81.496 z" fill="#000000"/>
|
||||
<path d="M135.639,176.125 Q136.532,176.126 137.17,176.756 Q137.808,177.386 137.808,178.279 Q137.808,179.204 137.162,179.826 Q136.516,180.448 135.639,180.448 L135.065,180.448 Q134.172,180.448 133.534,179.818 Q132.895,179.188 132.895,178.295 Q132.895,177.37 133.541,176.748 Q134.188,176.126 135.065,176.126 L135.639,176.125 z" fill="#000000"/>
|
||||
<path d="M668.7,165.773 L668.7,173.86 L660.613,173.86 L660.613,165.773 L668.7,165.773 z" fill="#000000"/>
|
||||
<path d="M119.783,165.773 L119.783,173.86 L111.696,173.86 L111.696,165.773 L119.783,165.773 z" fill="#000000"/>
|
||||
<path d="M786.325,165.773 L786.325,173.86 L778.237,173.86 L778.237,165.773 L786.325,165.773 z" fill="#000000"/>
|
||||
<path d="M772.479,100.351 L772.479,117.135 L782.289,117.135 L782.289,154.142 L772.479,154.142 L772.479,171.627 L752.875,171.627 L752.875,152.773 L762.685,152.773 L762.685,118.504 L752.875,118.504 L752.875,100.351 L772.479,100.351 z" fill="#000000"/>
|
||||
<path d="M556.833,100.351 L556.833,117.135 L566.643,117.135 L566.643,154.142 L556.833,154.142 L556.833,171.627 L537.229,171.627 L537.229,152.773 L547.039,152.773 L547.039,118.504 L537.229,118.504 L537.229,100.351 L556.833,100.351 z" fill="#000000"/>
|
||||
<path d="M400,29.075 L400,45.858 L419.604,45.858 L419.604,64.713 L400,64.713 L400,47.227 L380.396,47.227 L380.396,135.989 L400,135.989 L400,152.773 L429.414,152.773 L429.414,118.504 L419.604,118.504 L419.604,100.351 L400,100.351 L400,81.496 L439.208,81.496 L439.208,100.351 L458.813,100.351 L458.813,154.142 L439.208,154.142 L439.208,171.627 L360.792,171.627 L360.792,154.142 L350.998,154.142 L350.998,118.504 L341.187,118.504 L341.187,100.351 L360.792,100.351 L360.792,82.865 L350.998,82.865 L350.998,45.858 L360.792,45.858 L360.792,29.075 L400,29.075 z" fill="#000000"/>
|
||||
<path d="M184.354,29.075 L184.354,45.858 L203.958,45.858 L203.958,82.865 L184.354,82.865 L184.354,100.351 L145.146,100.351 L145.146,117.135 L164.75,117.135 L164.75,171.627 L145.146,171.627 L145.146,154.142 L135.352,154.142 L135.352,81.496 L145.146,81.496 L145.146,64.713 L164.75,64.713 L164.75,81.496 L184.354,81.496 L184.354,47.227 L145.146,47.227 L145.146,64.713 L125.542,64.713 L125.542,29.075 L184.354,29.075 z" fill="#000000"/>
|
||||
<path d="M105.938,100.351 L105.938,117.135 L115.748,117.135 L115.748,154.142 L105.938,154.142 L105.938,171.627 L86.334,171.627 L86.334,152.773 L96.144,152.773 L96.144,118.504 L86.334,118.504 L86.334,100.351 L105.938,100.351 z" fill="#000000"/>
|
||||
<path d="M203.958,100.351 L203.958,117.135 L213.768,117.135 L213.768,152.773 L223.563,152.773 L223.563,171.627 L203.958,171.627 L203.958,154.142 L184.354,154.142 L184.354,100.351 L203.958,100.351 z" fill="#000000"/>
|
||||
<path d="M301.979,29.075 L301.979,45.858 L321.583,45.858 L321.583,117.135 L331.393,117.135 L331.393,154.142 L321.583,154.142 L321.583,171.627 L301.979,171.627 L301.979,152.773 L311.789,152.773 L311.789,118.504 L301.979,118.504 L301.979,100.351 L262.771,100.351 L262.771,171.627 L243.167,171.627 L243.167,154.142 L233.373,154.142 L233.373,118.504 L223.563,118.504 L223.563,100.351 L243.167,100.351 L243.167,82.865 L233.373,82.865 L233.373,45.858 L243.167,45.858 L243.167,29.075 L301.979,29.075 z M282.375,47.227 L262.771,47.227 L262.771,81.496 L301.979,81.496 L301.979,64.713 L282.375,64.713 L282.375,47.227 z" fill="#000000"/>
|
||||
<path d="M354.124,167.432 Q354.794,167.432 355.273,167.911 Q355.751,168.389 355.751,169.075 Q355.751,169.745 355.273,170.224 Q354.794,170.702 354.124,170.702 Q353.438,170.702 352.96,170.224 Q352.481,169.745 352.481,169.075 Q352.481,168.389 352.96,167.911 Q353.438,167.432 354.124,167.432 z" fill="#000000"/>
|
||||
<path d="M76.826,140.487 Q77.72,140.487 78.358,141.118 Q78.996,141.748 78.996,142.641 Q78.996,143.566 78.35,144.188 Q77.704,144.81 76.827,144.81 L76.252,144.81 Q75.359,144.81 74.721,144.18 Q74.083,143.55 74.083,142.657 Q74.083,141.732 74.729,141.11 Q75.375,140.487 76.252,140.487 L76.826,140.487 z" fill="#000000"/>
|
||||
<path d="M743.368,140.487 Q744.261,140.487 744.899,141.118 Q745.537,141.748 745.537,142.641 Q745.537,143.566 744.891,144.188 Q744.245,144.81 743.368,144.81 L742.793,144.81 Q741.901,144.81 741.262,144.18 Q740.624,143.55 740.624,142.657 Q740.624,141.732 741.27,141.11 Q741.916,140.487 742.794,140.487 L743.368,140.487 z" fill="#000000"/>
|
||||
<path d="M296.221,130.135 L296.221,138.222 L288.134,138.222 L288.134,130.135 L296.221,130.135 z" fill="#000000"/>
|
||||
<path d="M413.846,130.135 L413.846,138.222 L405.758,138.222 L405.758,130.135 L413.846,130.135 z" fill="#000000"/>
|
||||
<path d="M174.544,130.231 Q176.187,130.231 177.344,131.387 Q178.5,132.544 178.5,134.171 Q178.5,135.814 177.336,136.97 Q176.171,138.127 174.544,138.127 Q172.917,138.127 171.761,136.97 Q170.604,135.814 170.604,134.171 Q170.604,132.544 171.761,131.387 Q172.917,130.231 174.544,130.231 z" fill="#000000"/>
|
||||
<path d="M570.679,94.497 L570.679,102.584 L562.592,102.584 L562.592,94.497 L570.679,94.497 z" fill="#000000"/>
|
||||
<path d="M453.054,58.859 L453.054,66.946 L444.967,66.946 L444.967,58.859 L453.054,58.859 z" fill="#000000"/>
|
||||
<path d="M21.763,58.859 L21.763,66.946 L13.675,66.946 L13.675,58.859 L21.763,58.859 z" fill="#000000"/>
|
||||
<path d="M472.658,58.859 L472.658,66.946 L464.571,66.946 L464.571,58.859 L472.658,58.859 z" fill="#000000"/>
|
||||
<path d="M688.304,58.859 L688.304,66.946 L680.217,66.946 L680.217,58.859 L688.304,58.859 z" fill="#000000"/>
|
||||
<path d="M586.232,58.954 Q587.875,58.955 589.031,60.111 Q590.188,61.267 590.188,62.895 Q590.188,64.537 589.023,65.694 Q587.859,66.85 586.232,66.85 Q584.605,66.85 583.448,65.694 Q582.292,64.537 582.292,62.895 Q582.292,61.267 583.448,60.111 Q584.605,58.955 586.232,58.955 z" fill="#000000"/>
|
||||
<path d="M216.895,60.518 L217.517,60.637 L218.043,60.996 Q218.522,61.475 218.522,62.161 Q218.522,62.831 218.044,63.309 Q217.565,63.788 216.895,63.788 Q216.209,63.788 215.731,63.309 Q215.252,62.831 215.252,62.161 Q215.252,61.475 215.731,60.996 C216.439,60.517 216.051,60.677 216.895,60.518 z" fill="#000000"/>
|
||||
<path d="M429.398,23.316 Q431.041,23.316 432.198,24.473 Q433.354,25.629 433.354,27.256 Q433.354,28.899 432.19,30.056 Q431.025,31.212 429.398,31.212 Q427.771,31.212 426.615,30.056 Q425.458,28.899 425.458,27.256 Q425.458,25.629 426.615,24.473 Q427.771,23.316 429.398,23.316 z" fill="#000000"/>
|
||||
<path d="M314.916,24.88 Q315.586,24.88 316.064,25.358 Q316.543,25.837 316.543,26.523 Q316.543,27.193 316.064,27.671 Q315.586,28.15 314.916,28.15 Q314.23,28.15 313.751,27.671 Q313.273,27.193 313.273,26.523 Q313.273,25.837 313.751,25.358 Q314.23,24.88 314.916,24.88 z" fill="#000000"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 8.5 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()
|
||||
}
|
||||
}
|
||||
}
|
35
ergo.motd
35
ergo.motd
@ -1,35 +0,0 @@
|
||||
__ __ ______ ___ ______ ___
|
||||
__/ // /_/ ____/ __ \/ ____/ __ \
|
||||
/_ // __/ __/ / /_/ / / __/ / / /
|
||||
/_ // __/ /___/ _, _/ /_/ / /_/ /
|
||||
/_//_/ /_____/_/ |_|\____/\____/
|
||||
|
||||
|
||||
This is the default Ergo MOTD.
|
||||
|
||||
|
||||
If motd-formatting is enabled in the config file, you can use the dollarsign character to
|
||||
create special formatting such as bold, italics and color codes.
|
||||
|
||||
For example, here are a few formatted lines (enable motd-formatting to see these in action):
|
||||
|
||||
- this is $bbold text$r.
|
||||
- this is $iitalics text$r.
|
||||
- this is $c[red]red$c and $c[blue]blue$c text.
|
||||
- this is $c[red,light blue]red text with a light blue background$c.
|
||||
- this is a normal escaped dollarsign: $$
|
||||
|
||||
And now a few fun colour charts!
|
||||
|
||||
$c1,0 00 $c0,1 01 $c0,2 02 $c0,3 03 $c1,4 04 $c0,5 05 $c0,6 06 $c1,7 07
|
||||
$c1,8 08 $c1,9 09 $c0,10 10 $c1,11 11 $c0,12 12 $c1,13 13 $c1,14 14 $c1,15 15
|
||||
|
||||
$c0,16 16 $c0,17 17 $c0,18 18 $c0,19 19 $c0,20 20 $c0,21 21 $c0,22 22 $c0,23 23 $c0,24 24 $c0,25 25 $c0,26 26 $c0,27 27
|
||||
$c0,28 28 $c0,29 29 $c0,30 30 $c0,31 31 $c0,32 32 $c0,33 33 $c0,34 34 $c0,35 35 $c0,36 36 $c0,37 37 $c0,38 38 $c0,39 39
|
||||
$c0,40 40 $c0,41 41 $c0,42 42 $c0,43 43 $c0,44 44 $c0,45 45 $c0,46 46 $c0,47 47 $c0,48 48 $c0,49 49 $c0,50 50 $c0,51 51
|
||||
$c0,52 52 $c0,53 53 $c1,54 54 $c1,55 55 $c1,56 56 $c1,57 57 $c1,58 58 $c0,59 59 $c0,60 60 $c0,61 61 $c0,62 62 $c0,63 63
|
||||
$c0,64 64 $c1,65 65 $c1,66 66 $c1,67 67 $c1,68 68 $c1,69 69 $c1,70 70 $c1,71 71 $c0,72 72 $c0,73 73 $c0,74 74 $c0,75 75
|
||||
$c1,76 76 $c1,77 77 $c1,78 78 $c1,79 79 $c1,80 80 $c1,81 81 $c1,82 82 $c1,83 83 $c1,84 84 $c1,85 85 $c1,86 86 $c1,87 87
|
||||
$c0,88 88 $c0,89 89 $c0,90 90 $c0,91 91 $c0,92 92 $c0,93 93 $c0,94 94 $c0,95 95 $c1,96 96 $c1,97 97 $c1,98 98 $c99,99 99
|
||||
|
||||
For more information on using these, see MOTDFORMATTING.md
|
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)
|
||||
}
|
||||
}
|
296
irc/accountreg.go
Normal file
296
irc/accountreg.go
Normal file
@ -0,0 +1,296 @@
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
var (
|
||||
errAccountCreation = errors.New("Account could not be created")
|
||||
errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
|
||||
)
|
||||
|
||||
// AccountRegistration manages the registration of accounts.
|
||||
type AccountRegistration struct {
|
||||
Enabled bool
|
||||
EnabledCallbacks []string
|
||||
EnabledCredentialTypes []string
|
||||
AllowMultiplePerConnection bool
|
||||
}
|
||||
|
||||
// AccountCredentials stores the various methods for verifying accounts.
|
||||
type AccountCredentials struct {
|
||||
PassphraseSalt []byte
|
||||
PassphraseHash []byte
|
||||
Certificate string // fingerprint
|
||||
}
|
||||
|
||||
// NewAccountRegistration returns a new AccountRegistration, configured correctly.
|
||||
func NewAccountRegistration(config AccountRegistrationConfig) (accountReg AccountRegistration) {
|
||||
if config.Enabled {
|
||||
accountReg.Enabled = true
|
||||
accountReg.AllowMultiplePerConnection = config.AllowMultiplePerConnection
|
||||
for _, name := range config.EnabledCallbacks {
|
||||
// we store "none" as "*" internally
|
||||
if name == "none" {
|
||||
name = "*"
|
||||
}
|
||||
accountReg.EnabledCallbacks = append(accountReg.EnabledCallbacks, name)
|
||||
}
|
||||
// no need to make this configurable, right now at least
|
||||
accountReg.EnabledCredentialTypes = []string{
|
||||
"passphrase",
|
||||
"certfp",
|
||||
}
|
||||
}
|
||||
return accountReg
|
||||
}
|
||||
|
||||
// accHandler parses the ACC command.
|
||||
func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
subcommand := strings.ToLower(msg.Params[0])
|
||||
|
||||
if subcommand == "register" {
|
||||
return accRegisterHandler(server, client, msg)
|
||||
} else if subcommand == "verify" {
|
||||
client.Notice("VERIFY is not yet implemented")
|
||||
} else {
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], "Unknown subcommand")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// removeFailedAccRegisterData removes the data created by ACC REGISTER if the account creation fails early.
|
||||
func removeFailedAccRegisterData(store *buntdb.DB, account string) {
|
||||
// error is ignored here, we can't do much about it anyways
|
||||
store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Delete(fmt.Sprintf(keyAccountExists, account))
|
||||
tx.Delete(fmt.Sprintf(keyAccountRegTime, account))
|
||||
tx.Delete(fmt.Sprintf(keyAccountCredentials, account))
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// accRegisterHandler parses the ACC REGISTER command.
|
||||
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
// make sure reg is enabled
|
||||
if !server.accountRegistration.Enabled {
|
||||
client.Send(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", "Account registration is disabled")
|
||||
return false
|
||||
}
|
||||
|
||||
// clients can't reg new accounts if they're already logged in
|
||||
if client.LoggedIntoAccount() {
|
||||
if server.accountRegistration.AllowMultiplePerConnection {
|
||||
client.LogoutOfAccount()
|
||||
} else {
|
||||
client.Send(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", "You're already logged into an account")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// get and sanitise account name
|
||||
account := strings.TrimSpace(msg.Params[1])
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
|
||||
if err != nil || msg.Params[1] == "*" {
|
||||
client.Send(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, account, "Account name is not valid")
|
||||
return false
|
||||
}
|
||||
|
||||
// check whether account exists
|
||||
// do it all in one write tx to prevent races
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
|
||||
_, err := tx.Get(accountKey)
|
||||
if err != buntdb.ErrNotFound {
|
||||
//TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be
|
||||
client.Send(nil, server.name, ERR_ACCOUNT_ALREADY_EXISTS, client.nick, account, "Account already exists")
|
||||
return errAccountCreation
|
||||
}
|
||||
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
|
||||
tx.Set(accountKey, "1", nil)
|
||||
tx.Set(fmt.Sprintf(keyAccountName, casefoldedAccount), account, nil)
|
||||
tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil)
|
||||
return nil
|
||||
})
|
||||
|
||||
// account could not be created and relevant numerics have been dispatched, abort
|
||||
if err != nil {
|
||||
if err != errAccountCreation {
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", "Could not register")
|
||||
log.Println("Could not save registration initial data:", err.Error())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// account didn't already exist, continue with account creation and dispatching verification (if required)
|
||||
callback := strings.ToLower(msg.Params[2])
|
||||
var callbackNamespace, callbackValue string
|
||||
|
||||
if callback == "*" {
|
||||
callbackNamespace = "*"
|
||||
} else if strings.Contains(callback, ":") {
|
||||
callbackValues := strings.SplitN(callback, ":", 2)
|
||||
callbackNamespace, callbackValue = callbackValues[0], callbackValues[1]
|
||||
} else {
|
||||
callbackNamespace = server.accountRegistration.EnabledCallbacks[0]
|
||||
callbackValue = callback
|
||||
}
|
||||
|
||||
// ensure the callback namespace is valid
|
||||
// need to search callback list, maybe look at using a map later?
|
||||
var callbackValid bool
|
||||
for _, name := range server.accountRegistration.EnabledCallbacks {
|
||||
if callbackNamespace == name {
|
||||
callbackValid = true
|
||||
}
|
||||
}
|
||||
|
||||
if !callbackValid {
|
||||
client.Send(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackNamespace, "Callback namespace is not supported")
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
// get credential type/value
|
||||
var credentialType, credentialValue string
|
||||
|
||||
if len(msg.Params) > 4 {
|
||||
credentialType = strings.ToLower(msg.Params[3])
|
||||
credentialValue = msg.Params[4]
|
||||
} else if len(msg.Params) == 4 {
|
||||
credentialType = "passphrase" // default from the spec
|
||||
credentialValue = msg.Params[3]
|
||||
} else {
|
||||
client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, "Not enough parameters")
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
// ensure the credential type is valid
|
||||
var credentialValid bool
|
||||
for _, name := range server.accountRegistration.EnabledCredentialTypes {
|
||||
if credentialType == name {
|
||||
credentialValid = true
|
||||
}
|
||||
}
|
||||
if credentialType == "certfp" && client.certfp == "" {
|
||||
client.Send(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, "You are not using a TLS certificate")
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
if !credentialValid {
|
||||
client.Send(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, "Credential type is not supported")
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
// store details
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
// certfp special lookup key
|
||||
if credentialType == "certfp" {
|
||||
assembledKeyCertToAccount := fmt.Sprintf(keyCertToAccount, client.certfp)
|
||||
|
||||
// make sure certfp doesn't already exist because that'd be silly
|
||||
_, err := tx.Get(assembledKeyCertToAccount)
|
||||
if err != buntdb.ErrNotFound {
|
||||
return errCertfpAlreadyExists
|
||||
}
|
||||
|
||||
tx.Set(assembledKeyCertToAccount, casefoldedAccount, nil)
|
||||
}
|
||||
|
||||
// make creds
|
||||
var creds AccountCredentials
|
||||
|
||||
// always set passphrase salt
|
||||
creds.PassphraseSalt, err = NewSalt()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not create passphrase salt: %s", err.Error())
|
||||
}
|
||||
|
||||
if credentialType == "certfp" {
|
||||
creds.Certificate = client.certfp
|
||||
} else if credentialType == "passphrase" {
|
||||
creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, credentialValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not hash password: %s", err)
|
||||
}
|
||||
}
|
||||
credText, err := json.Marshal(creds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not marshal creds: %s", err)
|
||||
}
|
||||
tx.Set(fmt.Sprintf(keyAccountCredentials, account), string(credText), nil)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// details could not be stored and relevant numerics have been dispatched, abort
|
||||
if err != nil {
|
||||
errMsg := "Could not register"
|
||||
if err == errCertfpAlreadyExists {
|
||||
errMsg = "An account already exists for your certificate fingerprint"
|
||||
}
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", errMsg)
|
||||
log.Println("Could not save registration creds:", err.Error())
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
// automatically complete registration
|
||||
if callbackNamespace == "*" {
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(fmt.Sprintf(keyAccountVerified, casefoldedAccount), "1", nil)
|
||||
|
||||
// load acct info inside store tx
|
||||
account := ClientAccount{
|
||||
Name: strings.TrimSpace(msg.Params[1]),
|
||||
RegisteredAt: time.Now(),
|
||||
Clients: []*Client{client},
|
||||
}
|
||||
//TODO(dan): Consider creating ircd-wide account adding/removing/affecting lock for protecting access to these sorts of variables
|
||||
server.accounts[casefoldedAccount] = &account
|
||||
client.account = &account
|
||||
|
||||
client.Send(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, account.Name, "Account created")
|
||||
client.Send(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account.Name, fmt.Sprintf("You are now logged in as %s", account.Name))
|
||||
client.Send(nil, server.name, RPL_SASLSUCCESS, client.nick, "Authentication successful")
|
||||
server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), account.Name, client.nickMaskString))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", "Could not register")
|
||||
log.Println("Could not save verification confirmation (*):", err.Error())
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// dispatch callback
|
||||
client.Notice(fmt.Sprintf("We should dispatch a real callback here to %s:%s", callbackNamespace, callbackValue))
|
||||
|
||||
return false
|
||||
}
|
2770
irc/accounts.go
2770
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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
90
irc/capability.go
Normal file
90
irc/capability.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2012-2014 Jeremy Latt
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
)
|
||||
|
||||
var (
|
||||
// SupportedCapabilities are the caps we advertise.
|
||||
// MaxLine, SASL and STS are set during server startup.
|
||||
SupportedCapabilities = caps.NewSet(caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.ServerTime, caps.UserhostInNames)
|
||||
|
||||
// CapValues are the actual values we advertise to v3.2 clients.
|
||||
// actual values are set during server startup.
|
||||
CapValues = caps.NewValues()
|
||||
)
|
||||
|
||||
// CapState shows whether we're negotiating caps, finished, etc for connection registration.
|
||||
type CapState uint
|
||||
|
||||
const (
|
||||
// CapNone means CAP hasn't been negotiated at all.
|
||||
CapNone CapState = iota
|
||||
// CapNegotiating means CAP is being negotiated and registration should be paused.
|
||||
CapNegotiating CapState = iota
|
||||
// CapNegotiated means CAP negotiation has been successfully ended and reg should complete.
|
||||
CapNegotiated CapState = iota
|
||||
)
|
||||
|
||||
// CAP <subcmd> [<caps>]
|
||||
func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
subCommand := strings.ToUpper(msg.Params[0])
|
||||
capabilities := caps.NewSet()
|
||||
var capString string
|
||||
|
||||
if len(msg.Params) > 1 {
|
||||
capString = msg.Params[1]
|
||||
strs := strings.Split(capString, " ")
|
||||
for _, str := range strs {
|
||||
if len(str) > 0 {
|
||||
capabilities.Enable(caps.Capability(str))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch subCommand {
|
||||
case "LS":
|
||||
if !client.registered {
|
||||
client.capState = CapNegotiating
|
||||
}
|
||||
if len(msg.Params) > 1 && msg.Params[1] == "302" {
|
||||
client.capVersion = 302
|
||||
}
|
||||
// weechat 1.4 has a bug here where it won't accept the CAP reply unless it contains
|
||||
// the server.name source... otherwise it doesn't respond to the CAP message with
|
||||
// anything and just hangs on connection.
|
||||
//TODO(dan): limit number of caps and send it multiline in 3.2 style as appropriate.
|
||||
client.Send(nil, server.name, "CAP", client.nick, subCommand, SupportedCapabilities.String(client.capVersion, CapValues))
|
||||
|
||||
case "LIST":
|
||||
client.Send(nil, server.name, "CAP", client.nick, subCommand, client.capabilities.String(caps.Cap301, CapValues)) // values not sent on LIST so force 3.1
|
||||
|
||||
case "REQ":
|
||||
// make sure all capabilities actually exist
|
||||
for _, capability := range capabilities.List() {
|
||||
if !SupportedCapabilities.Has(capability) {
|
||||
client.Send(nil, server.name, "CAP", client.nick, "NAK", capString)
|
||||
return false
|
||||
}
|
||||
}
|
||||
client.capabilities.Enable(capabilities.List()...)
|
||||
client.Send(nil, server.name, "CAP", client.nick, "ACK", capString)
|
||||
|
||||
case "END":
|
||||
if !client.registered {
|
||||
client.capState = CapNegotiated
|
||||
server.tryRegister(client)
|
||||
}
|
||||
|
||||
default:
|
||||
client.Send(nil, server.name, ERR_INVALIDCAPCMD, client.nick, subCommand, "Invalid CAP subcommand")
|
||||
}
|
||||
return false
|
||||
}
|
@ -3,73 +3,49 @@
|
||||
|
||||
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
|
||||
|
||||
var (
|
||||
nameToCapability map[string]Capability
|
||||
|
||||
NoSuchCap = errors.New("Unsupported capability name")
|
||||
const (
|
||||
// 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"
|
||||
// MaxLine is this proposed capability: https://github.com/DanielOaks/ircv3-specifications/blob/master+line-lengths/extensions/line-lengths.md
|
||||
MaxLine Capability = "draft/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"
|
||||
// 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 draft IRCv3 capability: http://ircv3.net/specs/core/sts-3.3.html
|
||||
STS Capability = "draft/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
|
||||
}
|
||||
|
||||
// Version is used to select which max version of CAP the client supports.
|
||||
type Version uint
|
||||
|
||||
const (
|
||||
// Cap301 refers to the base CAP spec.
|
||||
Cap301 Version = 301
|
||||
// Cap302 refers to the IRCv3.2 CAP spec.
|
||||
Cap302 Version = 302
|
||||
)
|
||||
|
||||
// State shows whether we're negotiating caps, finished, etc for connection registration.
|
||||
type State uint
|
||||
|
||||
const (
|
||||
// NoneState means CAP hasn't been negotiated at all.
|
||||
NoneState State = iota
|
||||
// NegotiatingState means CAP is being negotiated and registration should be paused.
|
||||
NegotiatingState State = iota
|
||||
// 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)
|
||||
}
|
||||
return string(capability)
|
||||
}
|
||||
|
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, " ")
|
||||
}
|
||||
|
@ -1,109 +0,0 @@
|
||||
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package caps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSets(t *testing.T) {
|
||||
s1 := NewSet()
|
||||
|
||||
s1.Enable(AccountTag, EchoMessage, UserhostInNames)
|
||||
|
||||
if !(s1.Has(AccountTag) && s1.Has(EchoMessage) && s1.Has(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")
|
||||
}
|
||||
|
||||
s1.Disable(AccountTag)
|
||||
|
||||
if s1.Has(AccountTag) {
|
||||
t.Error("Disable() did not correctly disable the given capability")
|
||||
}
|
||||
|
||||
enabledCaps := NewSet()
|
||||
enabledCaps.Union(s1)
|
||||
expectedCaps := NewSet(EchoMessage, UserhostInNames)
|
||||
if !reflect.DeepEqual(enabledCaps, expectedCaps) {
|
||||
t.Errorf("Enabled and expected capability lists do not match: %v, %v", enabledCaps, expectedCaps)
|
||||
}
|
||||
|
||||
// make sure re-enabling doesn't add to the count or something weird like that
|
||||
s1.Enable(EchoMessage)
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// test Strings()
|
||||
values := make(Values)
|
||||
values[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)
|
||||
}
|
||||
|
||||
actualCap302ValuesString := s1.Strings(Cap302, values, 0)
|
||||
expectedCap302ValuesString := []string{"invite-notify=invitemepls userhost-in-names"}
|
||||
if !reflect.DeepEqual(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
|
||||
}
|
14
irc/caps/version.go
Normal file
14
irc/caps/version.go
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package caps
|
||||
|
||||
// Version is used to select which max version of CAP the client supports.
|
||||
type Version uint
|
||||
|
||||
const (
|
||||
// Cap301 refers to the base CAP spec.
|
||||
Cap301 Version = 301
|
||||
// Cap302 refers to the IRCv3.2 CAP spec.
|
||||
Cap302 Version = 302
|
||||
)
|
1993
irc/channel.go
1993
irc/channel.go
File diff suppressed because it is too large
Load Diff
@ -1,515 +0,0 @@
|
||||
// Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
type channelManagerEntry struct {
|
||||
channel *Channel
|
||||
// this is a refcount for joins, so we can avoid a race where we incorrectly
|
||||
// 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,
|
||||
// providing synchronization for creation of new channels on first join,
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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) {
|
||||
name, err := CasefoldChannel(name)
|
||||
if err != nil {
|
||||
return 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) {
|
||||
server := client.server
|
||||
casefoldedName, err := CasefoldChannel(name)
|
||||
skeleton, skerr := Skeleton(name)
|
||||
if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen {
|
||||
return errNoSuchChannel, ""
|
||||
}
|
||||
|
||||
channel, err, newChannel := func() (*Channel, error, bool) {
|
||||
var newChannel bool
|
||||
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]
|
||||
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{}),
|
||||
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, ""
|
||||
}
|
||||
|
||||
err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
|
||||
|
||||
cm.maybeCleanup(channel, true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {
|
||||
cm.Lock()
|
||||
defer cm.Unlock()
|
||||
|
||||
cfname := channel.NameCasefolded()
|
||||
|
||||
entry := cm.chans[cfname]
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
cm.RLock()
|
||||
entry := cm.chans[casefoldedName]
|
||||
if entry != nil {
|
||||
channel = entry.channel
|
||||
}
|
||||
cm.RUnlock()
|
||||
|
||||
if channel == nil {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
channel.Part(client, message, rb)
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
|
||||
newCfname, 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 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)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len returns the number of channels
|
||||
func (cm *ChannelManager) Len() int {
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
return len(cm.chans)
|
||||
}
|
||||
|
||||
// Channels returns a slice containing all current channels
|
||||
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,37 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"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 (
|
||||
errChanExists = errors.New("Channel already exists")
|
||||
)
|
||||
|
||||
// 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 +45,84 @@ 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)
|
||||
// deleteChannelNoMutex deletes a given channel from our store.
|
||||
func (server *Server) deleteChannelNoMutex(tx *buntdb.Tx, channelKey string) {
|
||||
tx.Delete(fmt.Sprintf(keyChannelExists, channelKey))
|
||||
server.registeredChannels[channelKey] = nil
|
||||
}
|
||||
|
||||
func (r *RegisteredChannel) Deserialize(b []byte) (err error) {
|
||||
return json.Unmarshal(b, r)
|
||||
// loadChannelNoMutex loads a channel from the store.
|
||||
func (server *Server) loadChannelNoMutex(tx *buntdb.Tx, channelKey string) *RegisteredChannel {
|
||||
// return loaded chan if it already exists
|
||||
if server.registeredChannels[channelKey] != nil {
|
||||
return server.registeredChannels[channelKey]
|
||||
}
|
||||
_, 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)
|
||||
|
||||
chanInfo := 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,
|
||||
}
|
||||
server.registeredChannels[channelKey] = &chanInfo
|
||||
|
||||
return &chanInfo
|
||||
}
|
||||
|
||||
type ChannelPurgeRecord struct {
|
||||
NameCasefolded string `json:"Name"`
|
||||
UUID utils.UUID
|
||||
Oper string
|
||||
PurgedAt time.Time
|
||||
Reason string
|
||||
}
|
||||
// saveChannelNoMutex saves a channel to the store.
|
||||
func (server *Server) saveChannelNoMutex(tx *buntdb.Tx, channelKey string, channelInfo RegisteredChannel) {
|
||||
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)
|
||||
|
||||
func (c *ChannelPurgeRecord) Serialize() ([]byte, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
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)
|
||||
|
||||
func (c *ChannelPurgeRecord) Deserialize(b []byte) error {
|
||||
return json.Unmarshal(b, c)
|
||||
server.registeredChannels[channelKey] = &channelInfo
|
||||
}
|
||||
|
1025
irc/chanserv.go
1025
irc/chanserv.go
File diff suppressed because it is too large
Load Diff
2325
irc/client.go
2325
irc/client.go
File diff suppressed because it is too large
Load Diff
@ -5,295 +5,191 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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
|
||||
var (
|
||||
ErrNickMissing = errors.New("nick missing")
|
||||
ErrNicknameInUse = errors.New("nickname in use")
|
||||
ErrNicknameMismatch = errors.New("nickname mismatch")
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Initialize initializes a ClientManager.
|
||||
func (clients *ClientManager) Initialize() {
|
||||
clients.byNick = make(map[string]*Client)
|
||||
clients.bySkeleton = make(map[string]*Client)
|
||||
// ClientLookupSet represents a way to store, search and lookup clients.
|
||||
type ClientLookupSet struct {
|
||||
ByNickMutex sync.RWMutex
|
||||
ByNick map[string]*Client
|
||||
}
|
||||
|
||||
// Get retrieves a client from the manager, if they exist.
|
||||
func (clients *ClientManager) Get(nick string) *Client {
|
||||
// NewClientLookupSet returns a new lookup set.
|
||||
func NewClientLookupSet() *ClientLookupSet {
|
||||
return &ClientLookupSet{
|
||||
ByNick: make(map[string]*Client),
|
||||
}
|
||||
}
|
||||
|
||||
// Count returns how many clients are in the lookup set.
|
||||
func (clients *ClientLookupSet) Count() int {
|
||||
clients.ByNickMutex.RLock()
|
||||
defer clients.ByNickMutex.RUnlock()
|
||||
count := len(clients.ByNick)
|
||||
return count
|
||||
}
|
||||
|
||||
// Has returns whether or not the given client exists.
|
||||
//TODO(dan): This seems like ripe ground for a race, if code does Has then Get, and assumes the Get will return a client.
|
||||
func (clients *ClientLookupSet) Has(nick string) bool {
|
||||
casefoldedName, err := CasefoldName(nick)
|
||||
if err == nil {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
cli := clients.byNick[casefoldedName]
|
||||
return false
|
||||
}
|
||||
clients.ByNickMutex.RLock()
|
||||
defer clients.ByNickMutex.RUnlock()
|
||||
_, exists := clients.ByNick[casefoldedName]
|
||||
return exists
|
||||
}
|
||||
|
||||
// getNoMutex is used internally, for getting clients when no mutex is required (i.e. is already set).
|
||||
func (clients *ClientLookupSet) getNoMutex(nick string) *Client {
|
||||
casefoldedName, err := CasefoldName(nick)
|
||||
if err == nil {
|
||||
cli := clients.ByNick[casefoldedName]
|
||||
return cli
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (clients *ClientManager) removeInternal(client *Client, oldcfnick, oldskeleton string) (err error) {
|
||||
// requires holding the writable Lock()
|
||||
if oldcfnick == "*" || oldcfnick == "" {
|
||||
return errNickMissing
|
||||
// Get retrieves a client from the set, if they exist.
|
||||
func (clients *ClientLookupSet) Get(nick string) *Client {
|
||||
casefoldedName, err := CasefoldName(nick)
|
||||
if err == nil {
|
||||
clients.ByNickMutex.RLock()
|
||||
defer clients.ByNickMutex.RUnlock()
|
||||
cli := clients.ByNick[casefoldedName]
|
||||
return cli
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
currentEntry, present := clients.byNick[oldcfnick]
|
||||
if present {
|
||||
if currentEntry == client {
|
||||
delete(clients.byNick, oldcfnick)
|
||||
} else {
|
||||
// this shouldn't happen, but we can ignore it
|
||||
client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick)
|
||||
err = errNickMissing
|
||||
}
|
||||
} else {
|
||||
err = errNickMissing
|
||||
// Add adds a client to the lookup set.
|
||||
func (clients *ClientLookupSet) Add(client *Client, nick string) error {
|
||||
nick, err := CasefoldName(nick)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
clients.ByNickMutex.Lock()
|
||||
defer clients.ByNickMutex.Unlock()
|
||||
if clients.getNoMutex(nick) != nil {
|
||||
return ErrNicknameInUse
|
||||
}
|
||||
|
||||
return
|
||||
clients.ByNick[nick] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a client from the lookup set.
|
||||
func (clients *ClientManager) Remove(client *Client) error {
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
oldcfnick, oldskeleton := client.uniqueIdentifiers()
|
||||
return clients.removeInternal(client, oldcfnick, oldskeleton)
|
||||
func (clients *ClientLookupSet) Remove(client *Client) error {
|
||||
if !client.HasNick() {
|
||||
return ErrNickMissing
|
||||
}
|
||||
clients.ByNickMutex.Lock()
|
||||
defer clients.ByNickMutex.Unlock()
|
||||
if clients.getNoMutex(client.nick) != client {
|
||||
return ErrNicknameMismatch
|
||||
}
|
||||
delete(clients.ByNick, client.nickCasefolded)
|
||||
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
|
||||
}
|
||||
// Replace renames an existing client in the lookup set.
|
||||
func (clients *ClientLookupSet) Replace(oldNick, newNick string, client *Client) error {
|
||||
// get casefolded nicknames
|
||||
oldNick, err := CasefoldName(oldNick)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newNick, 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
|
||||
}
|
||||
// remove and replace
|
||||
clients.ByNickMutex.Lock()
|
||||
defer clients.ByNickMutex.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
oldClient := clients.ByNick[newNick]
|
||||
if oldClient == nil || oldClient == client {
|
||||
// whoo
|
||||
} 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
|
||||
}
|
||||
return ErrNicknameInUse
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
if oldNick == newNick {
|
||||
// if they're only changing case, don't need to remove+re-add them
|
||||
return nil
|
||||
}
|
||||
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
currentClient := 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
|
||||
}
|
||||
// analogous checks for skeletons
|
||||
skeletonHolder := clients.bySkeleton[newSkeleton]
|
||||
if skeletonHolder != nil && skeletonHolder != client {
|
||||
return "", errNicknameInUse, false
|
||||
}
|
||||
if nickIsReserved {
|
||||
return "", errNicknameReserved, false
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
formercfnick, formerskeleton := client.uniqueIdentifiers()
|
||||
if changeSuccess := client.SetNick(newNick, newCfNick, newSkeleton); !changeSuccess {
|
||||
return "", errClientDestroyed, false
|
||||
}
|
||||
clients.removeInternal(client, formercfnick, formerskeleton)
|
||||
clients.byNick[newCfNick] = client
|
||||
clients.bySkeleton[newSkeleton] = client
|
||||
return newNick, nil, false
|
||||
delete(clients.ByNick, oldNick)
|
||||
clients.ByNick[newNick] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (clients *ClientManager) AllClients() (result []*Client) {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
result = make([]*Client, len(clients.byNick))
|
||||
i := 0
|
||||
for _, client := range clients.byNick {
|
||||
result[i] = client
|
||||
i++
|
||||
}
|
||||
return
|
||||
}
|
||||
// AllWithCaps returns all clients with the given capabilities.
|
||||
func (clients *ClientLookupSet) AllWithCaps(capabs ...caps.Capability) (set ClientSet) {
|
||||
set = make(ClientSet)
|
||||
|
||||
// AllWithCapsNotify returns all sessions that support cap-notify.
|
||||
func (clients *ClientManager) AllWithCapsNotify() (sessions []*Session) {
|
||||
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)
|
||||
clients.ByNickMutex.RLock()
|
||||
defer clients.ByNickMutex.RUnlock()
|
||||
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) {
|
||||
func (clients *ClientLookupSet) 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()) {
|
||||
clients.ByNickMutex.RLock()
|
||||
defer clients.ByNickMutex.RUnlock()
|
||||
for _, client := range clients.ByNick {
|
||||
if matcher.Match(client.nickMaskCasefolded) {
|
||||
set.Add(client)
|
||||
}
|
||||
}
|
||||
@ -301,15 +197,122 @@ 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) {
|
||||
clients.RLock()
|
||||
c := clients.byNick[cfnick]
|
||||
clients.RUnlock()
|
||||
if c != nil {
|
||||
return c.Nick()
|
||||
} else {
|
||||
return cfnick
|
||||
// Find returns the first client that matches the given userhost mask.
|
||||
func (clients *ClientLookupSet) Find(userhost string) *Client {
|
||||
userhost, err := Casefold(ExpandUserHost(userhost))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
matcher := ircmatch.MakeMatch(userhost)
|
||||
var matchedClient *Client
|
||||
|
||||
clients.ByNickMutex.RLock()
|
||||
defer clients.ByNickMutex.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?)
|
||||
type UserMaskSet struct {
|
||||
masks map[string]bool
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewUserMaskSet() *UserMaskSet {
|
||||
return &UserMaskSet{
|
||||
masks: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (set *UserMaskSet) Add(mask string) bool {
|
||||
casefoldedMask, err := Casefold(mask)
|
||||
if err != nil {
|
||||
log.Println(fmt.Sprintf("ERROR: Could not add mask to usermaskset: [%s]", mask))
|
||||
return false
|
||||
}
|
||||
if set.masks[casefoldedMask] {
|
||||
return false
|
||||
}
|
||||
set.masks[casefoldedMask] = true
|
||||
set.setRegexp()
|
||||
return true
|
||||
}
|
||||
|
||||
func (set *UserMaskSet) AddAll(masks []string) (added bool) {
|
||||
for _, mask := range masks {
|
||||
if !added && !set.masks[mask] {
|
||||
added = true
|
||||
}
|
||||
set.masks[mask] = true
|
||||
}
|
||||
set.setRegexp()
|
||||
return
|
||||
}
|
||||
|
||||
func (set *UserMaskSet) Remove(mask string) bool {
|
||||
if !set.masks[mask] {
|
||||
return false
|
||||
}
|
||||
delete(set.masks, mask)
|
||||
set.setRegexp()
|
||||
return true
|
||||
}
|
||||
|
||||
func (set *UserMaskSet) Match(userhost string) bool {
|
||||
if set.regexp == nil {
|
||||
return false
|
||||
}
|
||||
return set.regexp.MatchString(userhost)
|
||||
}
|
||||
|
||||
func (set *UserMaskSet) String() string {
|
||||
masks := make([]string, len(set.masks))
|
||||
index := 0
|
||||
for mask := range set.masks {
|
||||
masks[index] = mask
|
||||
index += 1
|
||||
}
|
||||
return strings.Join(masks, " ")
|
||||
}
|
||||
|
||||
// Generate 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() {
|
||||
if len(set.masks) == 0 {
|
||||
set.regexp = nil
|
||||
return
|
||||
}
|
||||
|
||||
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, ".*")
|
||||
}
|
||||
expr := "^" + strings.Join(maskExprs, "|") + "$"
|
||||
set.regexp, _ = regexp.Compile(expr)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
663
irc/commands.go
663
irc/commands.go
@ -5,412 +5,287 @@
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
import "github.com/goshuirc/irc-go/ircmsg"
|
||||
|
||||
// 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) 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)
|
||||
}()
|
||||
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, "You need to register before you can use that command")
|
||||
return false
|
||||
}
|
||||
if cmd.oper && !client.flags[Operator] {
|
||||
client.Send(nil, server.name, ERR_NOPRIVILEGES, client.nick, "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, "Permission Denied")
|
||||
return false
|
||||
}
|
||||
if len(msg.Params) < cmd.minParams {
|
||||
client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, "Not enough parameters")
|
||||
return false
|
||||
}
|
||||
if !cmd.leaveClientActive {
|
||||
client.Active()
|
||||
}
|
||||
// only touch client if they're registered so that unregistered clients timeout appropriately
|
||||
if client.registered && !cmd.leaveClientIdle {
|
||||
client.Touch()
|
||||
}
|
||||
exiting := cmd.handler(server, client, msg)
|
||||
|
||||
// after each command, see if we can send registration to the client
|
||||
if !exiting && !client.registered {
|
||||
exiting = server.tryRegister(client, session)
|
||||
}
|
||||
|
||||
if client.registered {
|
||||
client.Touch(session) // even if `exiting`, we bump the lastSeen timestamp
|
||||
if !client.registered {
|
||||
server.tryRegister(client)
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
"AMBIANCE": {
|
||||
handler: sceneHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"AUTHENTICATE": {
|
||||
handler: authenticateHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"AWAY": {
|
||||
handler: awayHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 0,
|
||||
},
|
||||
"BATCH": {
|
||||
handler: batchHandler,
|
||||
minParams: 1,
|
||||
allowedInBatch: true,
|
||||
},
|
||||
"CAP": {
|
||||
handler: capHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"CHATHISTORY": {
|
||||
handler: chathistoryHandler,
|
||||
minParams: 4,
|
||||
},
|
||||
"DEBUG": {
|
||||
handler: debugHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"rehash"},
|
||||
},
|
||||
"DEFCON": {
|
||||
handler: defconHandler,
|
||||
capabs: []string{"defcon"},
|
||||
},
|
||||
"DEOPER": {
|
||||
handler: deoperHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"DLINE": {
|
||||
handler: dlineHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"EXTJWT": {
|
||||
handler: extjwtHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"HELP": {
|
||||
handler: helpHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"HELPOP": {
|
||||
handler: helpHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"HISTORY": {
|
||||
handler: historyHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"INFO": {
|
||||
handler: infoHandler,
|
||||
},
|
||||
"INVITE": {
|
||||
handler: inviteHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"ISON": {
|
||||
handler: isonHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"ISUPPORT": {
|
||||
handler: isupportHandler,
|
||||
usablePreReg: true,
|
||||
},
|
||||
"JOIN": {
|
||||
handler: joinHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"KICK": {
|
||||
handler: kickHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"KILL": {
|
||||
handler: killHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"kill"},
|
||||
},
|
||||
"KLINE": {
|
||||
handler: klineHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"LANGUAGE": {
|
||||
handler: languageHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"LIST": {
|
||||
handler: listHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"LUSERS": {
|
||||
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,
|
||||
},
|
||||
"MONITOR": {
|
||||
handler: monitorHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"MOTD": {
|
||||
handler: motdHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"NAMES": {
|
||||
handler: namesHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"NICK": {
|
||||
handler: nickHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"NOTICE": {
|
||||
handler: messageHandler,
|
||||
minParams: 2,
|
||||
allowedInBatch: true,
|
||||
},
|
||||
"NPC": {
|
||||
handler: npcHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"NPCA": {
|
||||
handler: npcaHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"OPER": {
|
||||
handler: operHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"PART": {
|
||||
handler: partHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"PASS": {
|
||||
handler: passHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"PERSISTENCE": {
|
||||
handler: persistenceHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"PING": {
|
||||
handler: pingHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"PONG": {
|
||||
handler: pongHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"PRIVMSG": {
|
||||
handler: messageHandler,
|
||||
minParams: 2,
|
||||
allowedInBatch: true,
|
||||
},
|
||||
"RELAYMSG": {
|
||||
handler: relaymsgHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"REGISTER": {
|
||||
handler: registerHandler,
|
||||
minParams: 3,
|
||||
usablePreReg: true,
|
||||
},
|
||||
"RENAME": {
|
||||
handler: renameHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"SAJOIN": {
|
||||
handler: sajoinHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"sajoin"},
|
||||
},
|
||||
"SANICK": {
|
||||
handler: sanickHandler,
|
||||
minParams: 2,
|
||||
capabs: []string{"samode"},
|
||||
},
|
||||
"SAMODE": {
|
||||
handler: modeHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"samode"},
|
||||
},
|
||||
"SCENE": {
|
||||
handler: sceneHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"SETNAME": {
|
||||
handler: setnameHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"SUMMON": {
|
||||
handler: summonHandler,
|
||||
},
|
||||
"TAGMSG": {
|
||||
handler: messageHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"QUIT": {
|
||||
handler: quitHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 0,
|
||||
},
|
||||
"REDACT": {
|
||||
handler: redactHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"REHASH": {
|
||||
handler: rehashHandler,
|
||||
minParams: 0,
|
||||
capabs: []string{"rehash"},
|
||||
},
|
||||
"TIME": {
|
||||
handler: timeHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"TOPIC": {
|
||||
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,
|
||||
},
|
||||
"UNKLINE": {
|
||||
handler: unKLineHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"USER": {
|
||||
handler: userHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 4,
|
||||
},
|
||||
"USERHOST": {
|
||||
handler: userhostHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"USERS": {
|
||||
handler: usersHandler,
|
||||
},
|
||||
"VERIFY": {
|
||||
handler: verifyHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 2,
|
||||
},
|
||||
"VERSION": {
|
||||
handler: versionHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"WEBIRC": {
|
||||
handler: webircHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 4,
|
||||
},
|
||||
"WEBPUSH": {
|
||||
handler: webpushHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"WHO": {
|
||||
handler: whoHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"WHOIS": {
|
||||
handler: whoisHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"WHOWAS": {
|
||||
handler: whowasHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"ZNC": {
|
||||
handler: zncHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
}
|
||||
|
||||
initializeServices()
|
||||
var Commands = map[string]Command{
|
||||
"ACC": {
|
||||
handler: accHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"AMBIANCE": {
|
||||
handler: sceneHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"AUTHENTICATE": {
|
||||
handler: authenticateHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"AWAY": {
|
||||
handler: awayHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"CAP": {
|
||||
handler: capHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"CHANSERV": {
|
||||
handler: csHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"CS": {
|
||||
handler: csHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"DEBUG": {
|
||||
handler: debugHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"DLINE": {
|
||||
handler: dlineHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
},
|
||||
"HELP": {
|
||||
handler: helpHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"HELPOP": {
|
||||
handler: helpHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"INVITE": {
|
||||
handler: inviteHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"ISON": {
|
||||
handler: isonHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"JOIN": {
|
||||
handler: joinHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"KICK": {
|
||||
handler: kickHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"KILL": {
|
||||
handler: killHandler,
|
||||
minParams: 1,
|
||||
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,
|
||||
oper: true,
|
||||
},
|
||||
"LIST": {
|
||||
handler: listHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"LUSERS": {
|
||||
handler: lusersHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"MODE": {
|
||||
handler: modeHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"MONITOR": {
|
||||
handler: monitorHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"MOTD": {
|
||||
handler: motdHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"NAMES": {
|
||||
handler: namesHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"NICK": {
|
||||
handler: nickHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"NICKSERV": {
|
||||
handler: nsHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"NOTICE": {
|
||||
handler: noticeHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"NPC": {
|
||||
handler: npcHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"NPCA": {
|
||||
handler: npcaHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"NS": {
|
||||
handler: nsHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"OPER": {
|
||||
handler: operHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"PART": {
|
||||
handler: partHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"PASS": {
|
||||
handler: passHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"PING": {
|
||||
handler: pingHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
leaveClientActive: true,
|
||||
},
|
||||
"PONG": {
|
||||
handler: pongHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
leaveClientActive: true,
|
||||
},
|
||||
"PRIVMSG": {
|
||||
handler: privmsgHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"PROXY": {
|
||||
handler: proxyHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 5,
|
||||
},
|
||||
"RENAME": {
|
||||
handler: renameHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"SANICK": {
|
||||
handler: sanickHandler,
|
||||
minParams: 2,
|
||||
oper: true,
|
||||
},
|
||||
"SAMODE": {
|
||||
handler: modeHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"samode"},
|
||||
},
|
||||
"SCENE": {
|
||||
handler: sceneHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"TAGMSG": {
|
||||
handler: tagmsgHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"QUIT": {
|
||||
handler: quitHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 0,
|
||||
},
|
||||
"REHASH": {
|
||||
handler: rehashHandler,
|
||||
minParams: 0,
|
||||
oper: true,
|
||||
capabs: []string{"oper:rehash"},
|
||||
},
|
||||
"TIME": {
|
||||
handler: timeHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"TOPIC": {
|
||||
handler: topicHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"UNDLINE": {
|
||||
handler: unDLineHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
},
|
||||
"UNKLINE": {
|
||||
handler: unKLineHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
},
|
||||
"USER": {
|
||||
handler: userHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 4,
|
||||
},
|
||||
"USERHOST": {
|
||||
handler: userhostHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"VERSION": {
|
||||
handler: versionHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"WHO": {
|
||||
handler: whoHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"WHOIS": {
|
||||
handler: whoisHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"WHOWAS": {
|
||||
handler: whowasHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
}
|
||||
|
1862
irc/config.go
1862
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)
|
||||
}
|
||||
}
|
||||
}
|
122
irc/connection_limits.go
Normal file
122
irc/connection_limits.go
Normal file
@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
errTooManyClients = errors.New("Too many clients in subnet")
|
||||
)
|
||||
|
||||
// ConnectionLimits manages the automated client connection limits.
|
||||
type ConnectionLimits struct {
|
||||
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
|
||||
|
||||
// exemptedIPs holds IPs that are exempt from limits
|
||||
exemptedIPs map[string]bool
|
||||
// exemptedNets holds networks that are exempt from limits
|
||||
exemptedNets []net.IPNet
|
||||
}
|
||||
|
||||
// maskAddr masks the given IPv4/6 address with our cidr limit masks.
|
||||
func (cl *ConnectionLimits) maskAddr(addr net.IP) net.IP {
|
||||
if addr.To4() == nil {
|
||||
// IPv6 addr
|
||||
addr = addr.Mask(cl.ipv6Mask)
|
||||
} else {
|
||||
// IPv4 addr
|
||||
addr = addr.Mask(cl.ipv4Mask)
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
// AddClient adds a client to our population if possible. If we can't, throws an error instead.
|
||||
// 'force' is used to add already-existing clients (i.e. ones that are already on the network).
|
||||
func (cl *ConnectionLimits) AddClient(addr net.IP, force bool) error {
|
||||
if !cl.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// check population
|
||||
cl.maskAddr(addr)
|
||||
addrString := addr.String()
|
||||
|
||||
if cl.population[addrString]+1 > cl.subnetLimit && !force {
|
||||
return errTooManyClients
|
||||
}
|
||||
|
||||
cl.population[addrString] = cl.population[addrString] + 1
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveClient removes the given address from our population
|
||||
func (cl *ConnectionLimits) RemoveClient(addr net.IP) {
|
||||
if !cl.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
addrString := addr.String()
|
||||
cl.population[addrString] = cl.population[addrString] - 1
|
||||
|
||||
// safety limiter
|
||||
if cl.population[addrString] < 0 {
|
||||
cl.population[addrString] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// NewConnectionLimits returns a new connection limit handler.
|
||||
func NewConnectionLimits(config ConnectionLimitsConfig) (*ConnectionLimits, error) {
|
||||
var cl ConnectionLimits
|
||||
cl.enabled = config.Enabled
|
||||
|
||||
cl.population = make(map[string]int)
|
||||
cl.exemptedIPs = make(map[string]bool)
|
||||
|
||||
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.IPsPerCidr
|
||||
|
||||
// assemble exempted nets
|
||||
for _, cidr := range config.Exempted {
|
||||
ipaddr := net.ParseIP(cidr)
|
||||
_, netaddr, err := net.ParseCIDR(cidr)
|
||||
|
||||
if ipaddr == nil && err != nil {
|
||||
return nil, fmt.Errorf("Could not parse exempted IP/network [%s]", cidr)
|
||||
}
|
||||
|
||||
if ipaddr != nil {
|
||||
cl.exemptedIPs[ipaddr.String()] = true
|
||||
} else {
|
||||
cl.exemptedNets = append(cl.exemptedNets, *netaddr)
|
||||
}
|
||||
}
|
||||
|
||||
return &cl, nil
|
||||
}
|
@ -1,280 +0,0 @@
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package connection_limits
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Limiter manages the automated client connection limits.
|
||||
type Limiter struct {
|
||||
sync.Mutex
|
||||
|
||||
config *LimiterConfig
|
||||
|
||||
// IP/CIDR -> count of clients connected from there:
|
||||
limiter map[limiterKey]int
|
||||
// IP/CIDR -> throttle state:
|
||||
throttler map[limiterKey]ThrottleDetails
|
||||
}
|
||||
|
||||
// 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
|
||||
} else {
|
||||
prefixLen = cl.config.CidrLenIPv6
|
||||
addr = addr.Mask(prefixLen, 128)
|
||||
}
|
||||
|
||||
return limiterKey{maskedIP: addr, prefixLen: uint8(prefixLen)}, "", cl.config.MaxConcurrent, cl.config.MaxPerWindow
|
||||
}
|
||||
|
||||
// AddClient adds a client to our population if possible. If we can't, throws an error instead.
|
||||
func (cl *Limiter) AddClient(addr flatip.IP) 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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// success, record in limiter
|
||||
if cl.config.Count {
|
||||
cl.limiter[addrString] = count
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveClient removes the given address from our population
|
||||
func (cl *Limiter) RemoveClient(addr flatip.IP) {
|
||||
cl.Lock()
|
||||
defer cl.Unlock()
|
||||
|
||||
if !cl.config.Count || flatip.IPInNets(addr, cl.config.exemptedNets) {
|
||||
return
|
||||
}
|
||||
|
||||
addrString, _, _, _ := cl.addrToKey(addr)
|
||||
count := cl.limiter[addrString]
|
||||
count -= 1
|
||||
if count < 0 {
|
||||
count = 0
|
||||
}
|
||||
cl.limiter[addrString] = count
|
||||
}
|
||||
|
||||
type LimiterStatus struct {
|
||||
Exempt bool
|
||||
|
||||
Count int
|
||||
MaxCount int
|
||||
|
||||
Throttle int
|
||||
MaxPerWindow int
|
||||
ThrottleDuration time.Duration
|
||||
}
|
||||
|
||||
func (cl *Limiter) Status(addr flatip.IP) (netName string, status LimiterStatus) {
|
||||
cl.Lock()
|
||||
defer cl.Unlock()
|
||||
|
||||
if flatip.IPInNets(addr, cl.config.exemptedNets) {
|
||||
status.Exempt = true
|
||||
return
|
||||
}
|
||||
|
||||
status.ThrottleDuration = cl.config.Window
|
||||
|
||||
limiterKey, customID, maxConcurrent, maxPerWindow := cl.addrToKey(addr)
|
||||
status.MaxCount = maxConcurrent
|
||||
status.MaxPerWindow = maxPerWindow
|
||||
|
||||
status.Count = cl.limiter[limiterKey]
|
||||
status.Throttle = cl.throttler[limiterKey].Count
|
||||
|
||||
netName = customID
|
||||
if netName == "" {
|
||||
netName = flatip.IPNet{
|
||||
IP: limiterKey.maskedIP,
|
||||
PrefixLen: limiterKey.prefixLen,
|
||||
}.String()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ResetThrottle resets the throttle count for an IP
|
||||
func (cl *Limiter) ResetThrottle(addr flatip.IP) {
|
||||
cl.Lock()
|
||||
defer cl.Unlock()
|
||||
|
||||
if !cl.config.Throttle || flatip.IPInNets(addr, cl.config.exemptedNets) {
|
||||
return
|
||||
}
|
||||
|
||||
addrString, _, _, _ := cl.addrToKey(addr)
|
||||
delete(cl.throttler, addrString)
|
||||
}
|
||||
|
||||
// ApplyConfig atomically applies a config update to a connection limit handler
|
||||
func (cl *Limiter) ApplyConfig(config *LimiterConfig) {
|
||||
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.config = config
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package connection_limits
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ThrottleDetails holds the connection-throttling details for a subnet/IP.
|
||||
type ThrottleDetails struct {
|
||||
Start time.Time
|
||||
Count 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
// we are not throttled, record the operation
|
||||
g.Count += 1
|
||||
return false, 0
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
134
irc/connection_throttling.go
Normal file
134
irc/connection_throttling.go
Normal file
@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ThrottleDetails holds the connection-throttling details for a subnet/IP.
|
||||
type ThrottleDetails struct {
|
||||
Start time.Time
|
||||
ClientCount int
|
||||
}
|
||||
|
||||
// ConnectionThrottle manages automated client connection throttling.
|
||||
type ConnectionThrottle struct {
|
||||
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
|
||||
BanMessageBytes []byte
|
||||
|
||||
// exemptedIPs holds IPs that are exempt from limits
|
||||
exemptedIPs map[string]bool
|
||||
// exemptedNets holds networks that are exempt from limits
|
||||
exemptedNets []net.IPNet
|
||||
}
|
||||
|
||||
// maskAddr masks the given IPv4/6 address with our cidr limit masks.
|
||||
func (ct *ConnectionThrottle) maskAddr(addr net.IP) net.IP {
|
||||
if addr.To4() == nil {
|
||||
// IPv6 addr
|
||||
addr = addr.Mask(ct.ipv6Mask)
|
||||
} else {
|
||||
// IPv4 addr
|
||||
addr = addr.Mask(ct.ipv4Mask)
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
// ResetFor removes any existing count for the given address.
|
||||
func (ct *ConnectionThrottle) ResetFor(addr net.IP) {
|
||||
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 *ConnectionThrottle) AddClient(addr net.IP) error {
|
||||
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
|
||||
}
|
||||
|
||||
// NewConnectionThrottle returns a new client connection throttler.
|
||||
func NewConnectionThrottle(config ConnectionThrottleConfig) (*ConnectionThrottle, error) {
|
||||
var ct ConnectionThrottle
|
||||
ct.enabled = config.Enabled
|
||||
|
||||
ct.population = make(map[string]ThrottleDetails)
|
||||
ct.exemptedIPs = make(map[string]bool)
|
||||
|
||||
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
|
||||
|
||||
// assemble exempted nets
|
||||
for _, cidr := range config.Exempted {
|
||||
ipaddr := net.ParseIP(cidr)
|
||||
_, netaddr, err := net.ParseCIDR(cidr)
|
||||
|
||||
if ipaddr == nil && err != nil {
|
||||
return nil, fmt.Errorf("Could not parse exempted IP/network [%s]", cidr)
|
||||
}
|
||||
|
||||
if ipaddr != nil {
|
||||
ct.exemptedIPs[ipaddr.String()] = true
|
||||
} else {
|
||||
ct.exemptedNets = append(ct.exemptedNets, *netaddr)
|
||||
}
|
||||
}
|
||||
|
||||
return &ct, nil
|
||||
}
|
@ -5,7 +5,17 @@
|
||||
|
||||
package irc
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
// SemVer is the semantic version of Oragono.
|
||||
SemVer = "0.9.2-unreleased"
|
||||
)
|
||||
|
||||
var (
|
||||
// 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
|
||||
}
|
||||
|
1401
irc/database.go
1401
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
|
||||
}
|
76
irc/debug.go
Normal file
76
irc/debug.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2012-2014 Jeremy Latt
|
||||
// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
// DEBUG GCSTATS/NUMGOROUTINE/etc
|
||||
func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
if !client.flags[Operator] {
|
||||
return false
|
||||
}
|
||||
|
||||
switch msg.Params[0] {
|
||||
case "GCSTATS":
|
||||
stats := debug.GCStats{
|
||||
Pause: make([]time.Duration, 10),
|
||||
PauseQuantiles: make([]time.Duration, 5),
|
||||
}
|
||||
debug.ReadGCStats(&stats)
|
||||
|
||||
client.Notice(fmt.Sprintf("last GC: %s", stats.LastGC.Format(time.RFC1123)))
|
||||
client.Notice(fmt.Sprintf("num GC: %d", stats.NumGC))
|
||||
client.Notice(fmt.Sprintf("pause total: %s", stats.PauseTotal))
|
||||
client.Notice(fmt.Sprintf("pause quantiles min%%: %s", stats.PauseQuantiles[0]))
|
||||
client.Notice(fmt.Sprintf("pause quantiles 25%%: %s", stats.PauseQuantiles[1]))
|
||||
client.Notice(fmt.Sprintf("pause quantiles 50%%: %s", stats.PauseQuantiles[2]))
|
||||
client.Notice(fmt.Sprintf("pause quantiles 75%%: %s", stats.PauseQuantiles[3]))
|
||||
client.Notice(fmt.Sprintf("pause quantiles max%%: %s", stats.PauseQuantiles[4]))
|
||||
|
||||
case "NUMGOROUTINE":
|
||||
count := runtime.NumGoroutine()
|
||||
client.Notice(fmt.Sprintf("num goroutines: %d", count))
|
||||
|
||||
case "PROFILEHEAP":
|
||||
profFile := "ergonomadic.mprof"
|
||||
file, err := os.Create(profFile)
|
||||
if err != nil {
|
||||
client.Notice(fmt.Sprintf("error: %s", err))
|
||||
break
|
||||
}
|
||||
defer file.Close()
|
||||
pprof.Lookup("heap").WriteTo(file, 0)
|
||||
client.Notice(fmt.Sprintf("written to %s", profFile))
|
||||
|
||||
case "STARTCPUPROFILE":
|
||||
profFile := "ergonomadic.prof"
|
||||
file, err := os.Create(profFile)
|
||||
if err != nil {
|
||||
client.Notice(fmt.Sprintf("error: %s", err))
|
||||
break
|
||||
}
|
||||
if err := pprof.StartCPUProfile(file); err != nil {
|
||||
defer file.Close()
|
||||
client.Notice(fmt.Sprintf("error: %s", err))
|
||||
break
|
||||
}
|
||||
|
||||
client.Notice(fmt.Sprintf("CPU profile writing to %s", profFile))
|
||||
|
||||
case "STOPCPUPROFILE":
|
||||
pprof.StopCPUProfile()
|
||||
client.Notice(fmt.Sprintf("CPU profiling stopped"))
|
||||
}
|
||||
return false
|
||||
}
|
597
irc/dline.go
597
irc/dline.go
@ -4,81 +4,94 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"net"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/flatip"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
keyDlineEntry = "bans.dlinev2 %s"
|
||||
keyDlineEntry = "bans.dline %s"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoExistingBan = errors.New("Ban does not exist")
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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 +102,392 @@ 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 string, operReason string) {
|
||||
netString := network.String()
|
||||
dln := dLineNet{
|
||||
Network: network,
|
||||
Info: IPBanInfo{
|
||||
Time: length,
|
||||
Reason: reason,
|
||||
OperReason: operReason,
|
||||
},
|
||||
}
|
||||
|
||||
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 string, operReason string) {
|
||||
addrString := addr.String()
|
||||
dla := dLineAddr{
|
||||
Address: addr,
|
||||
Info: IPBanInfo{
|
||||
Time: length,
|
||||
Reason: reason,
|
||||
OperReason: operReason,
|
||||
},
|
||||
}
|
||||
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
|
||||
// DLINE [ANDKILL] [MYSELF] [duration] <ip>/<net> [ON <server>] [reason [| oper reason]]
|
||||
func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
// check oper permissions
|
||||
if !client.class.Capabilities["oper:local_ban"] {
|
||||
client.Send(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, "Insufficient oper privs")
|
||||
return false
|
||||
}
|
||||
|
||||
currentArg := 0
|
||||
|
||||
// when setting a ban, if they say "ANDKILL" we should also kill all users who match it
|
||||
var andKill bool
|
||||
if len(msg.Params) > currentArg+1 && strings.ToLower(msg.Params[currentArg]) == "andkill" {
|
||||
andKill = true
|
||||
currentArg++
|
||||
}
|
||||
|
||||
// when setting a ban that covers the oper's current connection, we require them to say
|
||||
// "DLINE MYSELF" so that we're sure they really mean it.
|
||||
var dlineMyself bool
|
||||
if len(msg.Params) > currentArg+1 && strings.ToLower(msg.Params[currentArg]) == "myself" {
|
||||
dlineMyself = true
|
||||
currentArg++
|
||||
}
|
||||
|
||||
// duration
|
||||
duration, err := custime.ParseDuration(msg.Params[currentArg])
|
||||
durationIsUsed := err == nil
|
||||
if durationIsUsed {
|
||||
currentArg++
|
||||
}
|
||||
|
||||
// get host
|
||||
if len(msg.Params) < currentArg+1 {
|
||||
client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, "Not enough parameters")
|
||||
return false
|
||||
}
|
||||
hostString := msg.Params[currentArg]
|
||||
currentArg++
|
||||
|
||||
// check host
|
||||
var hostAddr net.IP
|
||||
var hostNet *net.IPNet
|
||||
|
||||
_, hostNet, err = net.ParseCIDR(hostString)
|
||||
if err != nil {
|
||||
hostAddr = net.ParseIP(hostString)
|
||||
}
|
||||
|
||||
if hostAddr == nil && hostNet == nil {
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "Could not parse IP address or CIDR network")
|
||||
return false
|
||||
}
|
||||
|
||||
if hostNet == nil {
|
||||
hostString = hostAddr.String()
|
||||
if !dlineMyself && hostAddr.Equal(client.IP()) {
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "This ban matches you. To DLINE yourself, you must use the command: /DLINE MYSELF <arguments>")
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
hostString = hostNet.String()
|
||||
if !dlineMyself && hostNet.Contains(client.IP()) {
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "This ban matches you. To DLINE yourself, you must use the command: /DLINE MYSELF <arguments>")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// check remote
|
||||
if len(msg.Params) > currentArg && msg.Params[currentArg] == "ON" {
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "Remote servers not yet supported")
|
||||
return false
|
||||
}
|
||||
|
||||
// get comment(s)
|
||||
reason := "No reason given"
|
||||
operReason := "No reason given"
|
||||
if len(msg.Params) > currentArg {
|
||||
tempReason := strings.TrimSpace(msg.Params[currentArg])
|
||||
if len(tempReason) > 0 && tempReason != "|" {
|
||||
tempReasons := strings.SplitN(tempReason, "|", 2)
|
||||
if tempReasons[0] != "" {
|
||||
reason = tempReasons[0]
|
||||
}
|
||||
if len(tempReasons) > 1 && tempReasons[1] != "" {
|
||||
operReason = tempReasons[1]
|
||||
} else {
|
||||
operReason = reason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assemble ban info
|
||||
var banTime *IPRestrictTime
|
||||
if durationIsUsed {
|
||||
banTime = &IPRestrictTime{
|
||||
Duration: duration,
|
||||
Expires: time.Now().Add(duration),
|
||||
}
|
||||
}
|
||||
|
||||
info := IPBanInfo{
|
||||
Reason: reason,
|
||||
OperReason: operReason,
|
||||
Time: banTime,
|
||||
}
|
||||
|
||||
// save in datastore
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
dlineKey := fmt.Sprintf(keyDlineEntry, hostString)
|
||||
|
||||
// assemble json from ban info
|
||||
b, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx.Set(dlineKey, string(b), nil)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
client.Notice(fmt.Sprintf("Could not successfully save new D-LINE: %s", err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
if hostNet == nil {
|
||||
server.dlines.AddIP(hostAddr, banTime, reason, operReason)
|
||||
} else {
|
||||
server.dlines.AddNetwork(*hostNet, banTime, reason, operReason)
|
||||
}
|
||||
|
||||
var snoDescription string
|
||||
if durationIsUsed {
|
||||
client.Notice(fmt.Sprintf("Added temporary (%s) D-Line for %s", duration.String(), hostString))
|
||||
snoDescription = fmt.Sprintf(ircfmt.Unescape("%s$r added temporary (%s) D-Line for %s"), client.nick, duration.String(), hostString)
|
||||
} else {
|
||||
client.Notice(fmt.Sprintf("Added D-Line for %s", hostString))
|
||||
snoDescription = fmt.Sprintf(ircfmt.Unescape("%s$r added D-Line for %s"), client.nick, hostString)
|
||||
}
|
||||
server.snomasks.Send(sno.LocalXline, snoDescription)
|
||||
|
||||
var killClient bool
|
||||
if andKill {
|
||||
var clientsToKill []*Client
|
||||
var killedClientNicks []string
|
||||
var toKill bool
|
||||
|
||||
server.clients.ByNickMutex.RLock()
|
||||
for _, mcl := range server.clients.ByNick {
|
||||
if hostNet == nil {
|
||||
toKill = hostAddr.Equal(mcl.IP())
|
||||
} else {
|
||||
toKill = hostNet.Contains(mcl.IP())
|
||||
}
|
||||
|
||||
if toKill {
|
||||
clientsToKill = append(clientsToKill, mcl)
|
||||
killedClientNicks = append(killedClientNicks, mcl.nick)
|
||||
}
|
||||
}
|
||||
server.clients.ByNickMutex.RUnlock()
|
||||
|
||||
for _, mcl := range clientsToKill {
|
||||
mcl.exitedSnomaskSent = true
|
||||
mcl.Quit(fmt.Sprintf("You have been banned from this server (%s)", reason))
|
||||
if mcl == client {
|
||||
killClient = true
|
||||
} else {
|
||||
// if mcl == client, we kill them below
|
||||
mcl.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// send snomask
|
||||
sort.Strings(killedClientNicks)
|
||||
server.snomasks.Send(sno.LocalKills, fmt.Sprintf(ircfmt.Unescape("%s killed %d clients with a DLINE $c[grey][$r%s$c[grey]]"), client.nick, len(killedClientNicks), strings.Join(killedClientNicks, ", ")))
|
||||
}
|
||||
|
||||
return killClient
|
||||
}
|
||||
|
||||
func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
// check oper permissions
|
||||
if !client.class.Capabilities["oper:local_unban"] {
|
||||
client.Send(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, "Insufficient oper privs")
|
||||
return false
|
||||
}
|
||||
|
||||
// get host
|
||||
hostString := msg.Params[0]
|
||||
|
||||
// check host
|
||||
var hostAddr net.IP
|
||||
var hostNet *net.IPNet
|
||||
|
||||
_, hostNet, err := net.ParseCIDR(hostString)
|
||||
if err != nil {
|
||||
hostAddr = net.ParseIP(hostString)
|
||||
}
|
||||
|
||||
if hostAddr == nil && hostNet == nil {
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "Could not parse IP address or CIDR network")
|
||||
return false
|
||||
}
|
||||
|
||||
if hostNet == nil {
|
||||
hostString = hostAddr.String()
|
||||
} else {
|
||||
hostString = hostNet.String()
|
||||
}
|
||||
|
||||
// save in datastore
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
dlineKey := fmt.Sprintf(keyDlineEntry, hostString)
|
||||
|
||||
// check if it exists or not
|
||||
val, err := tx.Get(dlineKey)
|
||||
if val == "" {
|
||||
return errNoExistingBan
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx.Delete(dlineKey)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, fmt.Sprintf("Could not remove ban [%s]", err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
if hostNet == nil {
|
||||
server.dlines.RemoveIP(hostAddr)
|
||||
} else {
|
||||
server.dlines.RemoveNetwork(*hostNet)
|
||||
}
|
||||
|
||||
client.Notice(fmt.Sprintf("Removed D-Line for %s", hostString))
|
||||
server.snomasks.Send(sno.LocalXline, fmt.Sprintf(ircfmt.Unescape("%s$r removed D-Line for %s"), client.nick, hostString))
|
||||
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
|
||||
}
|
||||
|
||||
// set opername if it isn't already set
|
||||
if info.OperName == "" {
|
||||
info.OperName = dm.server.name
|
||||
}
|
||||
json.Unmarshal([]byte(value), &info)
|
||||
|
||||
// add to the server
|
||||
dm.addNetworkInternal(hostNet, info)
|
||||
if hostNet == nil {
|
||||
s.dlines.AddIP(hostAddr, info.Time, info.Reason, info.OperReason)
|
||||
} else {
|
||||
s.dlines.AddNetwork(*hostNet, info.Time, info.Reason, info.OperReason)
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
104
irc/errors.go
104
irc/errors.go
@ -1,104 +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 irc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// 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")
|
||||
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`)
|
||||
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`)
|
||||
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")
|
||||
)
|
||||
|
||||
// String Errors
|
||||
var (
|
||||
errCouldNotStabilize = errors.New("Could not stabilize string while casefolding")
|
||||
errStringIsEmpty = errors.New("String is empty")
|
||||
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))
|
||||
}
|
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