mirror of
https://github.com/ergochat/ergo.git
synced 2025-08-03 03:07:27 +02:00
Compare commits
No commits in common. "master" and "v0.3.0" have entirely different histories.
@ -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 }}
|
13
.gitignore
vendored
13
.gitignore
vendored
@ -62,8 +62,6 @@ Temporary Items
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# vim swapfiles
|
||||
*.swp
|
||||
|
||||
### Go ###
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
@ -95,19 +93,12 @@ _testmain.go
|
||||
*.out
|
||||
|
||||
|
||||
### custom ###
|
||||
### Oragono ###
|
||||
/_site/
|
||||
/.vscode/*
|
||||
/ircd*
|
||||
/web-*
|
||||
/web.*
|
||||
/ssl.*
|
||||
/tls.*
|
||||
/ergo
|
||||
/oragono
|
||||
/build/*
|
||||
_test
|
||||
ergo.prof
|
||||
ergo.mprof
|
||||
/dist
|
||||
*.pem
|
||||
.dccache
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "irctest"]
|
||||
path = irctest
|
||||
url = https://github.com/ergochat/irctest
|
@ -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"
|
1715
CHANGELOG.md
1715
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
213
DEVELOPING.md
213
DEVELOPING.md
@ -1,213 +0,0 @@
|
||||
# Developing Ergo
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Golang issues
|
||||
|
||||
You should use the [latest distribution of the Go language for your OS and architecture](https://golang.org/dl/). (If `uname -m` on your Raspberry Pi reports `armv7l`, use the `armv6l` distribution of Go; if it reports v8, you may be able to use the `arm64` distribution.)
|
||||
|
||||
Ergo vendors all its dependencies. Because of this, Ergo is self-contained and you should not need to fetch any dependencies with `go get`. Doing so is not recommended, since it may fetch incompatible versions of the dependencies.
|
||||
|
||||
If you're upgrading the Go version used by Ergo, there are several places where it's hard-coded and must be changed:
|
||||
|
||||
1. `.github/workflows/build.yml`, which controls the version that our CI test suite uses to build and test the code (e.g., for a PR)
|
||||
2. `Dockerfile`, which controls the version that the Ergo binaries in our Docker images are built with
|
||||
3. `go.mod`: this should be updated automatically by Go when you do module-related operations
|
||||
|
||||
|
||||
## Branches
|
||||
|
||||
The recommended workflow for development is to create a new branch starting from the current `master`. Even though `master` is not recommended for production use, we strive to keep it in a usable state. Starting from `master` increases the likelihood that your patches will be accepted.
|
||||
|
||||
Long-running feature branches that aren't ready for merge into `master` may be maintained under a `devel+` prefix, e.g. `devel+metadata` for a feature branch implementing the IRCv3 METADATA extension.
|
||||
|
||||
|
||||
## Workflow
|
||||
|
||||
We have two test suites:
|
||||
|
||||
1. `make test`, which runs some relatively shallow unit tests, checks `go vet`, and does some other internal consistency checks
|
||||
1. `make irctest`, which runs the [irctest](https://github.com/ProgVal/irctest) integration test suite
|
||||
|
||||
Barring special circumstances, both must pass for a PR to be accepted. irctest will test the `ergo` binary visible on `$PATH`; make sure your development version is the one being tested. (If you have `~/go/bin` on your `$PATH`, a successful `make install` will accomplish this.)
|
||||
|
||||
The project style is [gofmt](https://go.dev/blog/gofmt); it is enforced by `make test`. You can fix any style issues automatically by running `make gofmt`.
|
||||
|
||||
|
||||
## Updating dependencies
|
||||
|
||||
Ergo vendors all dependencies using `go mod vendor`. To update a dependency, or add a new one:
|
||||
|
||||
1. `go get -v bazbat.com/path/to/dependency` ; this downloads the new dependency
|
||||
2. `go mod vendor` ; this writes the dependency's source files to the `vendor/` directory
|
||||
3. `git add go.mod go.sum vendor/` ; this stages all relevant changes to the vendor directory, including file deletions. Take care that spurious changes (such as editor swapfiles) aren't added.
|
||||
4. `git commit`
|
||||
|
||||
|
||||
## 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:
|
||||
|
||||
$ 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
|
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
|
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
|
||||
|
48
Makefile
48
Makefile
@ -1,48 +0,0 @@
|
||||
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null)
|
||||
GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1)
|
||||
|
||||
# disable linking against native libc / libpthread by default;
|
||||
# this can be overridden by passing CGO_ENABLED=1 to make
|
||||
export CGO_ENABLED ?= 0
|
||||
|
||||
capdef_file = ./irc/caps/defs.go
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
goreleaser --skip=publish --clean
|
||||
|
||||
.PHONY: capdefs
|
||||
capdefs:
|
||||
python3 ./gencapdefs.py > ${capdef_file}
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
python3 ./gencapdefs.py | diff - ${capdef_file}
|
||||
go test ./...
|
||||
go vet ./...
|
||||
./.check-gofmt.sh
|
||||
|
||||
.PHONY: smoke
|
||||
smoke: install
|
||||
ergo mkcerts --conf ./default.yaml || true
|
||||
ergo run --conf ./default.yaml --smoke
|
||||
|
||||
.PHONY: gofmt
|
||||
gofmt:
|
||||
./.check-gofmt.sh --fix
|
||||
|
||||
.PHONY: irctest
|
||||
irctest: install
|
||||
git submodule update --init
|
||||
cd irctest && make ergo
|
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
|
135
README.md
135
README.md
@ -1,127 +1,68 @@
|
||||

|
||||

|
||||
|
||||
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 to provide the majority of features that IRC users expect today.
|
||||
|
||||
* Being simple to set up and use
|
||||
* Combining the features of an ircd, a services framework, and a bouncer (integrated account management, history storage, 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
|
||||
It includes features such as UTF-8 nicks and channel names, client accounts and SASL, and other assorted IRCv3 support.
|
||||
|
||||
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
|
||||
|
||||
Also see the [mammon](https://github.com/mammon-ircd/mammon) IRC daemon for a similar project written in Python instead.
|
||||
|
||||
---
|
||||
|
||||
[](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/DanielOaks/oragono)
|
||||
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
## 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)
|
||||
* an extensible privilege system for IRC operators
|
||||
* UTF-8 nick and channel names with rfc7700
|
||||
* [yaml](http://yaml.org/) configuration
|
||||
* native TLS/SSL support
|
||||
* server password (`PASS` command)
|
||||
* channels with most standard modes
|
||||
* 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)
|
||||
* passwords stored in [bcrypt][go-crypto] format
|
||||
* client accounts and SASL
|
||||
* IRCv3 support
|
||||
|
||||
For more detailed information on Ergo's functionality, see:
|
||||
|
||||
* [MANUAL.md, the operator manual](https://github.com/ergochat/ergo/blob/stable/docs/MANUAL.md)
|
||||
* [USERGUIDE.md, the guide for end users](https://github.com/ergochat/ergo/blob/stable/docs/USERGUIDE.md)
|
||||
|
||||
## Quick start guide
|
||||
|
||||
Download the latest release from this page: https://github.com/ergochat/ergo/releases/latest
|
||||
|
||||
Extract it into a folder, then run the following commands:
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
cp default.yaml ircd.yaml
|
||||
go get
|
||||
go install
|
||||
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.
|
||||
|
||||
### Platform Packages
|
||||
|
||||
Some platforms/distros also have Ergo 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).
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
For information on contributing to Ergo, see [DEVELOPING.md](https://github.com/ergochat/ergo/blob/master/DEVELOPING.md).
|
||||
|
||||
#### 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.)
|
||||
**Note:** This installation will give you unsigned certificates only suitable for teting purposes.
|
||||
For real crets, look into [Let's Encrypt](https://letsencrypt.org/).
|
||||
|
||||
## Configuration
|
||||
|
||||
The default config file [`default.yaml`](default.yaml) helps walk you through what each option means and changes.
|
||||
|
||||
You can use the `--conf` parameter when launching Ergo to control where it looks for the config file. For instance: `ergo run --conf /path/to/ircd.yaml`. The configuration file also stores where the log, database, certificate, and other files are opened. Normally, all these files use relative paths, but you can change them to be absolute (such as `/var/log/ircd.log`) when running Ergo as a service.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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:
|
||||
See the example [`oragono.yaml`](oragono.yaml). Passwords are stored using bcrypt. You can generate encrypted password strings for use in the config with the `genpasswd` subcommand.
|
||||
|
||||
```sh
|
||||
ergo genpasswd
|
||||
oragono genpasswd
|
||||
```
|
||||
|
||||
With this, you receive a blob of text which you can plug into your configuration file.
|
||||
## Running the server
|
||||
|
||||
### Nickname and channel registration
|
||||
```sh
|
||||
oragono run
|
||||
```
|
||||
|
||||
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).
|
||||
## Credits
|
||||
|
||||
Once you have registered your nickname, you can use it to register channels:
|
||||
* 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.
|
||||
|
||||
1. Join the channel with `/join #channel`
|
||||
2. Register the channel with `/CS REGISTER #channel`
|
||||
|
||||
After this, your channel will remember the fact that you're the owner, the topic, and any modes set on it!
|
||||
|
||||
|
||||
# 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)
|
||||
[go-crypto]: https://godoc.org/golang.org/x/crypto
|
||||
|
54
build.sh
Executable file
54
build.sh
Executable file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env sh
|
||||
# release build script
|
||||
# to be run inside the Oragono dir
|
||||
|
||||
## windows ##
|
||||
rm -rf ./build/win/
|
||||
mkdir -p ./build/win/docs/
|
||||
|
||||
GOOS=windows GOATCH=amd64 go build oragono.go
|
||||
mv oragono.exe ./build/win/
|
||||
|
||||
cp LICENSE ./build/win/
|
||||
cp oragono.yaml oragono.motd ./build/win
|
||||
cp ./docs/README ./build/win/
|
||||
cp ./CHANGELOG.md ./build/win/docs
|
||||
cp ./docs/logo* ./build/win/docs
|
||||
|
||||
pushd ./build/win
|
||||
zip -r ../oragono-XXX-windows.zip *
|
||||
popd
|
||||
|
||||
## osx ##
|
||||
rm -rf ./build/osx/
|
||||
mkdir -p ./build/osx/docs/
|
||||
|
||||
GOOS=darwin GOATCH=amd64 go build oragono.go
|
||||
mv oragono ./build/osx/
|
||||
|
||||
cp LICENSE ./build/osx/
|
||||
cp oragono.yaml oragono.motd ./build/osx
|
||||
cp ./docs/README ./build/osx/
|
||||
cp ./CHANGELOG.md ./build/osx/docs
|
||||
cp ./docs/logo* ./build/osx/docs
|
||||
|
||||
pushd ./build/osx
|
||||
tar -czvf ../oragono-XXX-osx.tgz *
|
||||
popd
|
||||
|
||||
## linux ##
|
||||
rm -rf ./build/linux
|
||||
mkdir -p ./build/linux/docs/
|
||||
|
||||
GOOS=linux GOATCH=amd64 go build oragono.go
|
||||
mv oragono ./build/linux/
|
||||
|
||||
cp LICENSE ./build/linux/
|
||||
cp oragono.yaml oragono.motd ./build/linux
|
||||
cp ./docs/README ./build/linux/
|
||||
cp ./CHANGELOG.md ./build/linux/docs
|
||||
cp ./docs/logo* ./build/linux/docs
|
||||
|
||||
pushd ./build/linux
|
||||
tar -czvf ../oragono-XXX-linux.tgz *
|
||||
popd
|
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
|
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
|
36
docs/README
Normal file
36
docs/README
Normal file
@ -0,0 +1,36 @@
|
||||
|
||||
▄▄▄ ▄▄▄· ▄▄ • ▐ ▄
|
||||
▪ ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪ •█▌▐█▪
|
||||
▄█▀▄ ▐▀▀▄ ▄█▀▀█ ▄█ ▀█▄ ▄█▀▄▪▐█▐▐▌ ▄█▀▄
|
||||
▐█▌.▐▌▐█•█▌▐█ ▪▐▌▐█▄▪▐█▐█▌ ▐▌██▐█▌▐█▌.▐▌
|
||||
▀█▄▀▪.▀ ▀ ▀ ▀ ·▀▀▀▀ ▀█▄▀ ▀▀ █▪ ▀█▄▀▪
|
||||
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
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.
|
||||
|
||||
http://oragono.io/
|
||||
https://github.com/DanielOaks/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.
|
||||
|
||||
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!
|
@ -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)
|
||||
}
|
||||
}
|
2708
irc/accounts.go
2708
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
|
||||
}
|
||||
}
|
173
irc/capability.go
Normal file
173
irc/capability.go
Normal file
@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2012-2014 Jeremy Latt
|
||||
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/DanielOaks/girc-go/ircmsg"
|
||||
)
|
||||
|
||||
// Capabilities are optional features a client may request from a server.
|
||||
type Capability string
|
||||
|
||||
const (
|
||||
AccountTag Capability = "account-tag"
|
||||
AccountNotify Capability = "account-notify"
|
||||
AwayNotify Capability = "away-notify"
|
||||
CapNotify Capability = "cap-notify"
|
||||
EchoMessage Capability = "echo-message"
|
||||
ExtendedJoin Capability = "extended-join"
|
||||
InviteNotify Capability = "invite-notify"
|
||||
MessageTags Capability = "draft/message-tags"
|
||||
MultiPrefix Capability = "multi-prefix"
|
||||
SASL Capability = "sasl"
|
||||
ServerTime Capability = "server-time"
|
||||
UserhostInNames Capability = "userhost-in-names"
|
||||
)
|
||||
|
||||
var (
|
||||
SupportedCapabilities = CapabilitySet{
|
||||
AccountTag: true,
|
||||
AccountNotify: true,
|
||||
AwayNotify: true,
|
||||
CapNotify: true,
|
||||
EchoMessage: true,
|
||||
ExtendedJoin: true,
|
||||
InviteNotify: true,
|
||||
MessageTags: true,
|
||||
MultiPrefix: true,
|
||||
// SASL is set during server startup
|
||||
ServerTime: true,
|
||||
UserhostInNames: true,
|
||||
}
|
||||
CapValues = map[Capability]string{
|
||||
SASL: "PLAIN,EXTERNAL",
|
||||
}
|
||||
)
|
||||
|
||||
func (capability Capability) String() string {
|
||||
return string(capability)
|
||||
}
|
||||
|
||||
// CapModifiers are indicators showing the state of a capability after a REQ or
|
||||
// ACK.
|
||||
type CapModifier rune
|
||||
|
||||
const (
|
||||
Ack CapModifier = '~'
|
||||
Disable CapModifier = '-'
|
||||
Sticky CapModifier = '='
|
||||
)
|
||||
|
||||
func (mod CapModifier) String() string {
|
||||
return string(mod)
|
||||
}
|
||||
|
||||
type CapState uint
|
||||
|
||||
const (
|
||||
CapNone CapState = iota
|
||||
CapNegotiating CapState = iota
|
||||
CapNegotiated CapState = iota
|
||||
)
|
||||
|
||||
// CapVersion is used to select which max version of CAP the client supports.
|
||||
type CapVersion uint
|
||||
|
||||
const (
|
||||
// Cap301 refers to the base CAP spec.
|
||||
Cap301 CapVersion = 301
|
||||
// Cap302 refers to the IRCv3.2 CAP spec.
|
||||
Cap302 CapVersion = 302
|
||||
)
|
||||
|
||||
// CapabilitySet is used to track supported, enabled, and existing caps.
|
||||
type CapabilitySet map[Capability]bool
|
||||
|
||||
func (set CapabilitySet) String(version CapVersion) string {
|
||||
strs := make([]string, len(set))
|
||||
index := 0
|
||||
for capability := range set {
|
||||
capString := string(capability)
|
||||
if version == Cap302 {
|
||||
val, exists := CapValues[capability]
|
||||
if exists {
|
||||
capString += "=" + val
|
||||
}
|
||||
}
|
||||
strs[index] = capString
|
||||
index++
|
||||
}
|
||||
return strings.Join(strs, " ")
|
||||
}
|
||||
|
||||
func (set CapabilitySet) DisableString() string {
|
||||
parts := make([]string, len(set))
|
||||
index := 0
|
||||
for capability := range set {
|
||||
parts[index] = Disable.String() + capability.String()
|
||||
index += 1
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// CAP <subcmd> [<caps>]
|
||||
func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
subCommand := strings.ToUpper(msg.Params[0])
|
||||
capabilities := make(CapabilitySet)
|
||||
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[Capability(str)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
case "LIST":
|
||||
client.Send(nil, server.name, "CAP", client.nick, subCommand, client.capabilities.String(Cap301)) // values not sent on LIST so force 3.1
|
||||
|
||||
case "REQ":
|
||||
// make sure all capabilities actually exist
|
||||
for capability := range capabilities {
|
||||
if !SupportedCapabilities[capability] {
|
||||
client.Send(nil, server.name, "CAP", client.nick, "NAK", capString)
|
||||
return false
|
||||
}
|
||||
}
|
||||
for capability := range capabilities {
|
||||
client.capabilities[capability] = true
|
||||
}
|
||||
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
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package caps
|
||||
|
||||
import "errors"
|
||||
|
||||
// Capability represents an optional feature that a client may request from the server.
|
||||
type Capability uint
|
||||
|
||||
// actual capability definitions appear in defs.go
|
||||
|
||||
var (
|
||||
nameToCapability map[string]Capability
|
||||
|
||||
NoSuchCap = errors.New("Unsupported capability name")
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
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",
|
||||
}
|
||||
)
|
143
irc/caps/set.go
143
irc/caps/set.go
@ -1,143 +0,0 @@
|
||||
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package caps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// Set holds a set of enabled capabilities.
|
||||
type Set [bitsetLen]uint32
|
||||
|
||||
// Values holds capability values.
|
||||
type Values map[Capability]string
|
||||
|
||||
// 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)
|
||||
}
|
||||
return &newSet
|
||||
}
|
||||
|
||||
// Enable enables the given capabilities.
|
||||
func (s *Set) Enable(capabs ...Capability) {
|
||||
asSlice := s[:]
|
||||
for _, capab := range capabs {
|
||||
utils.BitsetSet(asSlice, uint(capab), true)
|
||||
}
|
||||
}
|
||||
|
||||
// Disable disables the given capabilities.
|
||||
func (s *Set) Disable(capabs ...Capability) {
|
||||
asSlice := s[:]
|
||||
for _, capab := range capabs {
|
||||
utils.BitsetSet(asSlice, uint(capab), false)
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds the given capabilities to this set.
|
||||
// this is just a wrapper to allow more clear use.
|
||||
func (s *Set) Add(capabs ...Capability) {
|
||||
s.Enable(capabs...)
|
||||
}
|
||||
|
||||
// Remove removes the given capabilities from this set.
|
||||
// this is just a wrapper to allow more clear use.
|
||||
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))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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[:])
|
||||
}
|
||||
|
||||
// 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 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]
|
||||
if exists {
|
||||
capString = fmt.Sprintf("%s=%s", capString, val)
|
||||
}
|
||||
}
|
||||
t.Add(capString)
|
||||
}
|
||||
|
||||
result = t.Lines()
|
||||
if result == nil {
|
||||
result = []string{""}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// this is a fixed whitelist of caps that are eligible for display in CAP LS 301
|
||||
func isAllowed301(capab Capability) bool {
|
||||
switch capab {
|
||||
case AccountNotify, AccountTag, AwayNotify, Batch, ChgHost, Chathistory, EventPlayback,
|
||||
Relaymsg, EchoMessage, Nope, ExtendedJoin, InviteNotify, LabeledResponse, MessageTags,
|
||||
MultiPrefix, SASL, ServerTime, SetName, STS, UserhostInNames, ZNCSelfMessage, ZNCPlayback:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
1908
irc/channel.go
1908
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
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
// this is an OR of all possible flags
|
||||
const (
|
||||
IncludeAllAttrs = ^uint(0)
|
||||
)
|
||||
|
||||
// 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.
|
||||
Founder string
|
||||
// Topic represents the channel topic.
|
||||
Topic string
|
||||
// TopicSetBy represents the host that set the topic.
|
||||
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
|
||||
}
|
||||
|
||||
func (r *RegisteredChannel) Serialize() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
func (r *RegisteredChannel) Deserialize(b []byte) (err error) {
|
||||
return json.Unmarshal(b, r)
|
||||
}
|
||||
|
||||
type ChannelPurgeRecord struct {
|
||||
NameCasefolded string `json:"Name"`
|
||||
UUID utils.UUID
|
||||
Oper string
|
||||
PurgedAt time.Time
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (c *ChannelPurgeRecord) Serialize() ([]byte, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
func (c *ChannelPurgeRecord) Deserialize(b []byte) error {
|
||||
return json.Unmarshal(b, c)
|
||||
}
|
958
irc/chanserv.go
958
irc/chanserv.go
@ -1,958 +0,0 @@
|
||||
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/sno"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"github.com/ergochat/irc-go/ircfmt"
|
||||
)
|
||||
|
||||
const chanservHelp = `ChanServ lets you register and manage channels.`
|
||||
|
||||
func chanregEnabled(config *Config) bool {
|
||||
return config.Channels.Registration.Enabled
|
||||
}
|
||||
|
||||
var (
|
||||
chanservCommands = map[string]*serviceCommand{
|
||||
"op": {
|
||||
handler: csOpHandler,
|
||||
help: `Syntax: $bOP #channel [nickname]$b
|
||||
|
||||
OP makes the given nickname, or yourself, a channel admin. You can only use
|
||||
this command if you're a founder or in the AMODEs of the channel.`,
|
||||
helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`,
|
||||
authRequired: true,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 1,
|
||||
},
|
||||
"deop": {
|
||||
handler: csDeopHandler,
|
||||
help: `Syntax: $bDEOP #channel [nickname]$b
|
||||
|
||||
DEOP removes the given nickname, or yourself, the channel admin. You can only use
|
||||
this command if you're the founder of the channel.`,
|
||||
helpShort: `$bDEOP$b removes the given user (or yourself) from a channel admin.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 1,
|
||||
},
|
||||
"register": {
|
||||
handler: csRegisterHandler,
|
||||
help: `Syntax: $bREGISTER #channel$b
|
||||
|
||||
REGISTER lets you own the given channel. If you rejoin this channel, you'll be
|
||||
given admin privs on it. Modes set on the channel and the topic will also be
|
||||
remembered.`,
|
||||
helpShort: `$bREGISTER$b lets you own a given channel.`,
|
||||
authRequired: true,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 1,
|
||||
},
|
||||
"unregister": {
|
||||
handler: csUnregisterHandler,
|
||||
help: `Syntax: $bUNREGISTER #channel [code]$b
|
||||
|
||||
UNREGISTER deletes a channel registration, allowing someone else to claim it.
|
||||
To prevent accidental unregistrations, a verification code is required;
|
||||
invoking the command without a code will display the necessary code.`,
|
||||
helpShort: `$bUNREGISTER$b deletes a channel registration.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 1,
|
||||
},
|
||||
"drop": {
|
||||
aliasOf: "unregister",
|
||||
},
|
||||
"amode": {
|
||||
handler: csAmodeHandler,
|
||||
help: `Syntax: $bAMODE #channel [mode change] [account]$b
|
||||
|
||||
AMODE lists or modifies persistent mode settings that affect channel members.
|
||||
For example, $bAMODE #channel +o dan$b grants the holder of the "dan"
|
||||
account the +o operator mode every time they join #channel. To list current
|
||||
accounts and modes, use $bAMODE #channel$b. Note that users are always
|
||||
referenced by their registered account names, not their nicknames.
|
||||
The permissions hierarchy for adding and removing modes is the same as in
|
||||
the ordinary /MODE command.`,
|
||||
helpShort: `$bAMODE$b modifies persistent mode settings for channel members.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 1,
|
||||
},
|
||||
"clear": {
|
||||
handler: csClearHandler,
|
||||
help: `Syntax: $bCLEAR #channel target$b
|
||||
|
||||
CLEAR removes users or settings from a channel. Specifically:
|
||||
|
||||
$bCLEAR #channel users$b kicks all users except for you.
|
||||
$bCLEAR #channel access$b resets all stored bans, invites, ban exceptions,
|
||||
and persistent user-mode grants made with CS AMODE.`,
|
||||
helpShort: `$bCLEAR$b removes users or settings from a channel.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 2,
|
||||
},
|
||||
"transfer": {
|
||||
handler: csTransferHandler,
|
||||
help: `Syntax: $bTRANSFER [accept] #channel user [code]$b
|
||||
|
||||
TRANSFER transfers ownership of a channel from one user to another.
|
||||
To prevent accidental transfers, a verification code is required. For
|
||||
example, $bTRANSFER #channel alice$b displays the required confirmation
|
||||
code, then $bTRANSFER #channel alice 2930242125$b initiates the transfer.
|
||||
Unless you are an IRC operator with the correct permissions, alice must
|
||||
then accept the transfer, which she can do with $bTRANSFER accept #channel$b.
|
||||
To cancel a pending transfer, transfer the channel to yourself.`,
|
||||
helpShort: `$bTRANSFER$b transfers ownership of a channel to another user.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 2,
|
||||
},
|
||||
"purge": {
|
||||
handler: csPurgeHandler,
|
||||
help: `Syntax: $bPURGE <ADD | DEL | LIST> #channel [code] [reason]$b
|
||||
|
||||
PURGE ADD blacklists a channel from the server, making it impossible to join
|
||||
or otherwise interact with the channel. If the channel currently has members,
|
||||
they will be kicked from it. PURGE may also be applied preemptively to
|
||||
channels that do not currently have members. A purge can be undone with
|
||||
PURGE DEL. To list purged channels, use PURGE LIST.`,
|
||||
helpShort: `$bPURGE$b blacklists a channel from the server.`,
|
||||
capabs: []string{"chanreg"},
|
||||
minParams: 1,
|
||||
maxParams: 3,
|
||||
unsplitFinalParam: true,
|
||||
},
|
||||
"list": {
|
||||
handler: csListHandler,
|
||||
help: `Syntax: $bLIST [regex]$b
|
||||
|
||||
LIST returns the list of registered channels, which match the given regex.
|
||||
If no regex is provided, all registered channels are returned.`,
|
||||
helpShort: `$bLIST$b searches the list of registered channels.`,
|
||||
capabs: []string{"chanreg"},
|
||||
minParams: 0,
|
||||
},
|
||||
"info": {
|
||||
handler: csInfoHandler,
|
||||
help: `Syntax: $INFO #channel$b
|
||||
|
||||
INFO displays info about a registered channel.`,
|
||||
helpShort: `$bINFO$b displays info about a registered channel.`,
|
||||
enabled: chanregEnabled,
|
||||
},
|
||||
"get": {
|
||||
handler: csGetHandler,
|
||||
help: `Syntax: $bGET #channel <setting>$b
|
||||
|
||||
GET queries the current values of the channel settings. For more information
|
||||
on the settings and their possible values, see HELP SET.`,
|
||||
helpShort: `$bGET$b queries the current values of a channel's settings`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 2,
|
||||
},
|
||||
"set": {
|
||||
handler: csSetHandler,
|
||||
helpShort: `$bSET$b modifies a channel's settings`,
|
||||
// these are broken out as separate strings so they can be translated separately
|
||||
helpStrings: []string{
|
||||
`Syntax $bSET #channel <setting> <value>$b
|
||||
|
||||
SET modifies a channel's settings. The following settings are available:`,
|
||||
|
||||
`$bHISTORY$b
|
||||
'history' lets you control how channel history is stored. Your options are:
|
||||
1. 'off' [no history]
|
||||
2. 'ephemeral' [a limited amount of temporary history, not stored on disk]
|
||||
3. 'on' [history stored in a permanent database, if available]
|
||||
4. 'default' [use the server default]`,
|
||||
`$bQUERY-CUTOFF$b
|
||||
'query-cutoff' lets you restrict how much channel history can be retrieved
|
||||
by unprivileged users. Your options are:
|
||||
1. 'none' [no restrictions]
|
||||
2. 'registration-time' [users can view history from after their account was
|
||||
registered, plus a grace period]
|
||||
3. 'join-time' [users can view history from after they joined the
|
||||
channel; note that history will be effectively
|
||||
unavailable to clients that are not always-on]
|
||||
4. 'default' [use the server default]`,
|
||||
},
|
||||
enabled: chanregEnabled,
|
||||
minParams: 3,
|
||||
},
|
||||
"howtoban": {
|
||||
handler: csHowToBanHandler,
|
||||
helpShort: `$bHOWTOBAN$b suggests the best available way of banning a user`,
|
||||
help: `Syntax: $bHOWTOBAN #channel <nick>
|
||||
|
||||
The best way to ban a user from a channel will depend on how they are
|
||||
connected to the server. $bHOWTOBAN$b suggests a ban command that will
|
||||
(ideally) prevent the user from returning to the channel.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func csAmodeHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
channelName := params[0]
|
||||
|
||||
channel := server.channels.Get(channelName)
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
} else if channel.Founder() == "" {
|
||||
service.Notice(rb, client.t("Channel is not registered"))
|
||||
return
|
||||
}
|
||||
|
||||
modeChanges, unknown := modes.ParseChannelModeChanges(params[1:]...)
|
||||
invalid := len(unknown) != 0
|
||||
// #2002: +f takes an argument but is not a channel-user mode,
|
||||
// check for anything valid as a channel mode change that is not valid
|
||||
// as an AMODE change
|
||||
for _, modeChange := range modeChanges {
|
||||
if !slices.Contains(modes.ChannelUserModes, modeChange.Mode) {
|
||||
invalid = true
|
||||
}
|
||||
}
|
||||
var change modes.ModeChange
|
||||
if len(modeChanges) > 1 || invalid {
|
||||
service.Notice(rb, client.t("Invalid mode change"))
|
||||
return
|
||||
} else if len(modeChanges) == 1 {
|
||||
change = modeChanges[0]
|
||||
} else {
|
||||
change = modes.ModeChange{Op: modes.List}
|
||||
}
|
||||
|
||||
// normalize and validate the account argument
|
||||
accountIsValid := false
|
||||
change.Arg, _ = CasefoldName(change.Arg)
|
||||
switch change.Op {
|
||||
case modes.List:
|
||||
accountIsValid = true
|
||||
case modes.Add:
|
||||
// if we're adding a mode, the account must exist
|
||||
if change.Arg != "" {
|
||||
_, err := server.accounts.LoadAccount(change.Arg)
|
||||
accountIsValid = (err == nil)
|
||||
}
|
||||
case modes.Remove:
|
||||
// allow removal of accounts that may have been deleted
|
||||
accountIsValid = (change.Arg != "")
|
||||
}
|
||||
if !accountIsValid {
|
||||
service.Notice(rb, client.t("Account does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
affectedModes, err := channel.ProcessAccountToUmodeChange(client, change)
|
||||
|
||||
if err == errInsufficientPrivs {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
} else if err != nil {
|
||||
service.Notice(rb, client.t("Internal error"))
|
||||
return
|
||||
}
|
||||
|
||||
switch change.Op {
|
||||
case modes.List:
|
||||
// sort the persistent modes in descending order of priority
|
||||
sort.Slice(affectedModes, func(i, j int) bool {
|
||||
return umodeGreaterThan(affectedModes[i].Mode, affectedModes[j].Mode)
|
||||
})
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %[1]s has %[2]d persistent modes set"), channelName, len(affectedModes)))
|
||||
for _, modeChange := range affectedModes {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Account %[1]s receives mode +%[2]s"), modeChange.Arg, string(modeChange.Mode)))
|
||||
}
|
||||
case modes.Add, modes.Remove:
|
||||
if len(affectedModes) > 0 {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Successfully set persistent mode %[1]s on %[2]s"), strings.Join([]string{string(change.Op), string(change.Mode)}, ""), change.Arg))
|
||||
// #729: apply change to current membership
|
||||
for _, member := range channel.Members() {
|
||||
if member.Account() == change.Arg {
|
||||
// applyModeToMember takes the nickname, not the account name,
|
||||
// so translate:
|
||||
modeChange := change
|
||||
modeChange.Arg = member.Nick()
|
||||
applied, modeChange := channel.applyModeToMember(client, modeChange, rb)
|
||||
if applied {
|
||||
announceCmodeChanges(channel, modes.ModeChanges{modeChange}, server.name, "*", "", false, rb)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
service.Notice(rb, client.t("No changes were made"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func csOpHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
channelInfo := server.channels.Get(params[0])
|
||||
if channelInfo == nil {
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
channelName := channelInfo.Name()
|
||||
founder := channelInfo.Founder()
|
||||
|
||||
clientAccount := client.Account()
|
||||
if clientAccount == "" {
|
||||
service.Notice(rb, client.t("You're not logged into an account"))
|
||||
return
|
||||
}
|
||||
|
||||
var target *Client
|
||||
if len(params) > 1 {
|
||||
target = server.clients.Get(params[1])
|
||||
if target == nil {
|
||||
service.Notice(rb, client.t("Could not find given client"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
target = client
|
||||
}
|
||||
|
||||
var givenMode modes.Mode
|
||||
if target == client {
|
||||
if clientAccount == founder {
|
||||
givenMode = modes.ChannelFounder
|
||||
} else {
|
||||
givenMode = channelInfo.getAmode(clientAccount)
|
||||
if givenMode == modes.Mode(0) {
|
||||
service.Notice(rb, client.t("You don't have any stored privileges on that channel"))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if clientAccount == founder {
|
||||
givenMode = modes.ChannelOperator
|
||||
} else {
|
||||
service.Notice(rb, client.t("Only the channel founder can do this"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
applied, change := channelInfo.applyModeToMember(client,
|
||||
modes.ModeChange{Mode: givenMode,
|
||||
Op: modes.Add,
|
||||
Arg: target.NickCasefolded(),
|
||||
},
|
||||
rb)
|
||||
if applied {
|
||||
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, server.name, "*", "", false, rb)
|
||||
}
|
||||
|
||||
service.Notice(rb, client.t("Successfully granted operator privileges"))
|
||||
|
||||
tnick := target.Nick()
|
||||
server.logger.Info("services", fmt.Sprintf("Client %s op'd [%s] in channel %s", client.Nick(), tnick, channelName))
|
||||
server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] CS OP'd $c[grey][$r%s$c[grey]] in channel $c[grey][$r%s$c[grey]]"), client.NickMaskString(), tnick, channelName))
|
||||
}
|
||||
|
||||
func csDeopHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
channel := server.channels.Get(params[0])
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
if !channel.hasClient(client) {
|
||||
service.Notice(rb, client.t("You're not on that channel"))
|
||||
return
|
||||
}
|
||||
|
||||
var target *Client
|
||||
if len(params) > 1 {
|
||||
target = server.clients.Get(params[1])
|
||||
if target == nil {
|
||||
service.Notice(rb, client.t("Could not find given client"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
target = client
|
||||
}
|
||||
|
||||
present, _, cumodes := channel.ClientStatus(target)
|
||||
if !present || len(cumodes) == 0 {
|
||||
service.Notice(rb, client.t("Target has no privileges to remove"))
|
||||
return
|
||||
}
|
||||
|
||||
tnick := target.Nick()
|
||||
modeChanges := make(modes.ModeChanges, len(cumodes))
|
||||
for i, mode := range cumodes {
|
||||
modeChanges[i] = modes.ModeChange{
|
||||
Mode: mode,
|
||||
Op: modes.Remove,
|
||||
Arg: tnick,
|
||||
}
|
||||
}
|
||||
|
||||
// use the user's own permissions for the check, then announce
|
||||
// the changes as coming from chanserv
|
||||
applied := channel.ApplyChannelModeChanges(client, false, modeChanges, rb)
|
||||
details := client.Details()
|
||||
isBot := client.HasMode(modes.Bot)
|
||||
announceCmodeChanges(channel, applied, details.nickMask, details.accountName, details.account, isBot, rb)
|
||||
|
||||
if len(applied) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
service.Notice(rb, client.t("Successfully removed operator privileges"))
|
||||
}
|
||||
|
||||
func csRegisterHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
if server.Config().Channels.Registration.OperatorOnly && !client.HasRoleCapabs("chanreg") {
|
||||
service.Notice(rb, client.t("Channel registration is restricted to server operators"))
|
||||
return
|
||||
}
|
||||
channelName := params[0]
|
||||
channelInfo := server.channels.Get(channelName)
|
||||
if channelInfo == nil {
|
||||
service.Notice(rb, client.t("No such channel"))
|
||||
return
|
||||
}
|
||||
if !channelInfo.ClientIsAtLeast(client, modes.ChannelOperator) {
|
||||
service.Notice(rb, client.t("You must be an oper on the channel to register it"))
|
||||
return
|
||||
}
|
||||
|
||||
account := client.Account()
|
||||
if !checkChanLimit(service, client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
// this provides the synchronization that allows exactly one registration of the channel:
|
||||
err := server.channels.SetRegistered(channelName, account)
|
||||
if err != nil {
|
||||
service.Notice(rb, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName))
|
||||
|
||||
server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.Nick(), channelName))
|
||||
server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString))
|
||||
|
||||
// give them founder privs
|
||||
applied, change := channelInfo.applyModeToMember(client,
|
||||
modes.ModeChange{
|
||||
Mode: modes.ChannelFounder,
|
||||
Op: modes.Add,
|
||||
Arg: client.NickCasefolded(),
|
||||
},
|
||||
rb)
|
||||
if applied {
|
||||
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, service.prefix, "*", "", false, rb)
|
||||
}
|
||||
}
|
||||
|
||||
// check whether a client has already registered too many channels
|
||||
func checkChanLimit(service *ircService, client *Client, rb *ResponseBuffer) (ok bool) {
|
||||
account := client.Account()
|
||||
channelsAlreadyRegistered := client.server.channels.ChannelsForAccount(account)
|
||||
ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
|
||||
if !ok {
|
||||
service.Notice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func csPrivsCheck(service *ircService, channel RegisteredChannel, client *Client, rb *ResponseBuffer) (success bool) {
|
||||
founder := channel.Founder
|
||||
if founder == "" {
|
||||
service.Notice(rb, client.t("That channel is not registered"))
|
||||
return false
|
||||
}
|
||||
if client.HasRoleCapabs("chanreg") {
|
||||
return true
|
||||
}
|
||||
if founder != client.Account() {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func csUnregisterHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
channelName := params[0]
|
||||
var verificationCode string
|
||||
if len(params) > 1 {
|
||||
verificationCode = params[1]
|
||||
}
|
||||
|
||||
channel := server.channels.Get(channelName)
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("No such channel"))
|
||||
return
|
||||
}
|
||||
|
||||
info := channel.exportSummary()
|
||||
channelKey := channel.NameCasefolded()
|
||||
if !csPrivsCheck(service, info, client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt)
|
||||
if expectedCode != verificationCode {
|
||||
service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/CS UNREGISTER %s %s", channelKey, expectedCode)))
|
||||
return
|
||||
}
|
||||
|
||||
server.channels.SetUnregistered(channelKey, info.Founder)
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
|
||||
}
|
||||
|
||||
func csClearHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
channel := server.channels.Get(params[0])
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
if !csPrivsCheck(service, channel.exportSummary(), client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
switch strings.ToLower(params[1]) {
|
||||
case "access":
|
||||
channel.resetAccess()
|
||||
service.Notice(rb, client.t("Successfully reset channel access"))
|
||||
case "users":
|
||||
for _, target := range channel.Members() {
|
||||
if target != client {
|
||||
channel.Kick(client, target, "Cleared by ChanServ", rb, true)
|
||||
}
|
||||
}
|
||||
default:
|
||||
service.Notice(rb, client.t("Invalid parameters"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func csTransferHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
if strings.ToLower(params[0]) == "accept" {
|
||||
processTransferAccept(service, client, params[1], rb)
|
||||
return
|
||||
}
|
||||
chname := params[0]
|
||||
channel := server.channels.Get(chname)
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
regInfo := channel.exportSummary()
|
||||
chname = regInfo.Name
|
||||
account := client.Account()
|
||||
isFounder := account != "" && account == regInfo.Founder
|
||||
oper := client.Oper()
|
||||
hasPrivs := oper.HasRoleCapab("chanreg")
|
||||
if !isFounder && !hasPrivs {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
target := params[1]
|
||||
targetAccount, err := server.accounts.LoadAccount(params[1])
|
||||
if err != nil {
|
||||
service.Notice(rb, client.t("Account does not exist"))
|
||||
return
|
||||
}
|
||||
if targetAccount.NameCasefolded != account {
|
||||
expectedCode := utils.ConfirmationCode(regInfo.Name, regInfo.RegisteredAt)
|
||||
codeValidated := 2 < len(params) && params[2] == expectedCode
|
||||
if !codeValidated {
|
||||
service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to transfer control of your channel to another user.$b")))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("To confirm your channel transfer, type: /CS TRANSFER %[1]s %[2]s %[3]s"), chname, target, expectedCode))
|
||||
return
|
||||
}
|
||||
}
|
||||
if !isFounder {
|
||||
message := fmt.Sprintf("Operator %s ran CS TRANSFER on %s to account %s", oper.Name, chname, target)
|
||||
server.snomasks.Send(sno.LocalOpers, message)
|
||||
server.logger.Info("opers", message)
|
||||
}
|
||||
status, err := channel.Transfer(client, target, hasPrivs)
|
||||
if err == nil {
|
||||
switch status {
|
||||
case channelTransferComplete:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Successfully transferred channel %[1]s to account %[2]s"), chname, target))
|
||||
case channelTransferPending:
|
||||
sendTransferPendingNotice(service, server, target, chname)
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Transfer of channel %[1]s to account %[2]s succeeded, pending acceptance"), chname, target))
|
||||
case channelTransferCancelled:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Cancelled pending transfer of channel %s"), chname))
|
||||
}
|
||||
} else {
|
||||
switch err {
|
||||
case errChannelNotOwnedByAccount:
|
||||
service.Notice(rb, client.t("You don't own that channel"))
|
||||
default:
|
||||
service.Notice(rb, client.t("Could not transfer channel"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendTransferPendingNotice(service *ircService, server *Server, account, chname string) {
|
||||
clients := server.accounts.AccountToClients(account)
|
||||
if len(clients) == 0 {
|
||||
return
|
||||
}
|
||||
var client *Client
|
||||
for _, candidate := range clients {
|
||||
client = candidate
|
||||
if candidate.NickCasefolded() == candidate.Account() {
|
||||
break // prefer the login where the nick is the account
|
||||
}
|
||||
}
|
||||
client.Send(nil, service.prefix, "NOTICE", client.Nick(), fmt.Sprintf(client.t("You have been offered ownership of channel %[1]s. To accept, /CS TRANSFER ACCEPT %[1]s"), chname))
|
||||
}
|
||||
|
||||
func processTransferAccept(service *ircService, client *Client, chname string, rb *ResponseBuffer) {
|
||||
channel := client.server.channels.Get(chname)
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
if !checkChanLimit(service, client, rb) {
|
||||
return
|
||||
}
|
||||
switch channel.AcceptTransfer(client) {
|
||||
case nil:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Successfully accepted ownership of channel %s"), channel.Name()))
|
||||
case errChannelTransferNotOffered:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("You weren't offered ownership of channel %s"), channel.Name()))
|
||||
default:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Could not accept ownership of channel %s"), channel.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
func csPurgeHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
oper := client.Oper()
|
||||
if oper == nil {
|
||||
return // should be impossible because you need oper capabs for this
|
||||
}
|
||||
|
||||
switch strings.ToLower(params[0]) {
|
||||
case "add":
|
||||
csPurgeAddHandler(service, client, params[1:], oper.Name, rb)
|
||||
case "del", "remove":
|
||||
csPurgeDelHandler(service, client, params[1:], oper.Name, rb)
|
||||
case "list":
|
||||
csPurgeListHandler(service, client, rb)
|
||||
default:
|
||||
service.Notice(rb, client.t("Invalid parameters"))
|
||||
}
|
||||
}
|
||||
|
||||
func csPurgeAddHandler(service *ircService, client *Client, params []string, operName string, rb *ResponseBuffer) {
|
||||
if len(params) == 0 {
|
||||
service.Notice(rb, client.t("Invalid parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
chname := params[0]
|
||||
params = params[1:]
|
||||
channel := client.server.channels.Get(chname) // possibly nil
|
||||
var ctime time.Time
|
||||
if channel != nil {
|
||||
chname = channel.Name()
|
||||
ctime = channel.Ctime()
|
||||
}
|
||||
code := utils.ConfirmationCode(chname, ctime)
|
||||
|
||||
if len(params) == 0 || params[0] != code {
|
||||
service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to empty this channel and remove it from the server.$b")))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/CS PURGE ADD %s %s", chname, code)))
|
||||
return
|
||||
}
|
||||
params = params[1:]
|
||||
|
||||
var reason string
|
||||
if 1 < len(params) {
|
||||
reason = params[1]
|
||||
}
|
||||
|
||||
purgeRecord := ChannelPurgeRecord{
|
||||
Oper: operName,
|
||||
PurgedAt: time.Now().UTC(),
|
||||
Reason: reason,
|
||||
}
|
||||
switch client.server.channels.Purge(chname, purgeRecord) {
|
||||
case nil:
|
||||
if channel != nil { // channel need not exist to be purged
|
||||
for _, target := range channel.Members() {
|
||||
channel.Kick(client, target, "Cleared by ChanServ", rb, true)
|
||||
}
|
||||
}
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Successfully purged channel %s from the server"), chname))
|
||||
client.server.snomasks.Send(sno.LocalChannels, fmt.Sprintf("Operator %s purged channel %s [reason: %s]", operName, chname, reason))
|
||||
case errInvalidChannelName:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Can't purge invalid channel %s"), chname))
|
||||
default:
|
||||
service.Notice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
func csPurgeDelHandler(service *ircService, client *Client, params []string, operName string, rb *ResponseBuffer) {
|
||||
if len(params) == 0 {
|
||||
service.Notice(rb, client.t("Invalid parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
chname := params[0]
|
||||
switch client.server.channels.Unpurge(chname) {
|
||||
case nil:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Successfully unpurged channel %s from the server"), chname))
|
||||
client.server.snomasks.Send(sno.LocalChannels, fmt.Sprintf("Operator %s removed purge of channel %s", operName, chname))
|
||||
case errNoSuchChannel:
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s wasn't previously purged from the server"), chname))
|
||||
default:
|
||||
service.Notice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
func csPurgeListHandler(service *ircService, client *Client, rb *ResponseBuffer) {
|
||||
l := client.server.channels.ListPurged()
|
||||
service.Notice(rb, fmt.Sprintf(client.t("There are %d purged channel(s)."), len(l)))
|
||||
for i, c := range l {
|
||||
service.Notice(rb, fmt.Sprintf("%d: %s", i+1, c))
|
||||
}
|
||||
}
|
||||
|
||||
func csListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
var searchRegex *regexp.Regexp
|
||||
if len(params) > 0 {
|
||||
var err error
|
||||
searchRegex, err = regexp.Compile(params[0])
|
||||
if err != nil {
|
||||
service.Notice(rb, client.t("Invalid regex"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
service.Notice(rb, ircfmt.Unescape(client.t("*** $bChanServ LIST$b ***")))
|
||||
|
||||
channels := server.channels.AllRegisteredChannels()
|
||||
for _, channel := range channels {
|
||||
if searchRegex == nil || searchRegex.MatchString(channel) {
|
||||
service.Notice(rb, fmt.Sprintf(" %s", channel))
|
||||
}
|
||||
}
|
||||
|
||||
service.Notice(rb, ircfmt.Unescape(client.t("*** $bEnd of ChanServ LIST$b ***")))
|
||||
}
|
||||
|
||||
func csInfoHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
if len(params) == 0 {
|
||||
// #765
|
||||
listRegisteredChannels(service, client.Account(), rb)
|
||||
return
|
||||
}
|
||||
|
||||
chname, err := CasefoldChannel(params[0])
|
||||
if err != nil {
|
||||
service.Notice(rb, client.t("Invalid channel name"))
|
||||
return
|
||||
}
|
||||
|
||||
// purge status
|
||||
if client.HasRoleCapabs("chanreg") {
|
||||
purgeRecord, err := server.channels.LoadPurgeRecord(chname)
|
||||
if err == nil {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123)))
|
||||
if purgeRecord.Reason != "" {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if server.channels.IsPurged(chname) {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||
}
|
||||
}
|
||||
|
||||
var chinfo RegisteredChannel
|
||||
channel := server.channels.Get(params[0])
|
||||
if channel != nil {
|
||||
chinfo = channel.exportSummary()
|
||||
}
|
||||
|
||||
// channel exists but is unregistered, or doesn't exist:
|
||||
if chinfo.Founder == "" {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname))
|
||||
return
|
||||
}
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
|
||||
}
|
||||
|
||||
func displayChannelSetting(service *ircService, settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {
|
||||
config := client.server.Config()
|
||||
|
||||
switch strings.ToLower(settingName) {
|
||||
case "history":
|
||||
effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
|
||||
service.Notice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
|
||||
case "query-cutoff":
|
||||
effectiveValue := settings.QueryCutoff
|
||||
if effectiveValue == HistoryCutoffDefault {
|
||||
effectiveValue = config.History.Restrictions.queryCutoff
|
||||
}
|
||||
service.Notice(rb, fmt.Sprintf(client.t("The stored channel history query cutoff setting is: %s"), historyCutoffToString(settings.QueryCutoff)))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history query cutoff setting is: %s"), historyCutoffToString(effectiveValue)))
|
||||
default:
|
||||
service.Notice(rb, client.t("Invalid params"))
|
||||
}
|
||||
}
|
||||
|
||||
func csGetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
chname, setting := params[0], params[1]
|
||||
channel := server.channels.Get(chname)
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("No such channel"))
|
||||
return
|
||||
}
|
||||
info := channel.exportSummary()
|
||||
if !csPrivsCheck(service, info, client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
displayChannelSetting(service, setting, channel.Settings(), client, rb)
|
||||
}
|
||||
|
||||
func csSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
chname, setting, value := params[0], params[1], params[2]
|
||||
channel := server.channels.Get(chname)
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("No such channel"))
|
||||
return
|
||||
}
|
||||
info := channel.exportSummary()
|
||||
if !csPrivsCheck(service, info, client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
settings := channel.Settings()
|
||||
var err error
|
||||
switch strings.ToLower(setting) {
|
||||
case "history":
|
||||
settings.History, err = historyStatusFromString(value)
|
||||
if err != nil {
|
||||
err = errInvalidParams
|
||||
break
|
||||
}
|
||||
channel.SetSettings(settings)
|
||||
channel.resizeHistory(server.Config())
|
||||
case "query-cutoff":
|
||||
settings.QueryCutoff, err = historyCutoffFromString(value)
|
||||
if err != nil {
|
||||
err = errInvalidParams
|
||||
break
|
||||
}
|
||||
channel.SetSettings(settings)
|
||||
}
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
service.Notice(rb, client.t("Successfully changed the channel settings"))
|
||||
displayChannelSetting(service, setting, settings, client, rb)
|
||||
case errInvalidParams:
|
||||
service.Notice(rb, client.t("Invalid parameters"))
|
||||
default:
|
||||
server.logger.Error("internal", "CS SET error:", err.Error())
|
||||
service.Notice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
func csHowToBanHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
success := false
|
||||
defer func() {
|
||||
if success {
|
||||
service.Notice(rb, client.t("Note that if the user is currently in the channel, you must /KICK them after you ban them"))
|
||||
}
|
||||
}()
|
||||
|
||||
chname, nick := params[0], params[1]
|
||||
channel := server.channels.Get(chname)
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("No such channel"))
|
||||
return
|
||||
}
|
||||
|
||||
if !(channel.ClientIsAtLeast(client, modes.ChannelOperator) || client.HasRoleCapabs("samode")) {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
|
||||
var details WhoWas
|
||||
target := server.clients.Get(nick)
|
||||
if target == nil {
|
||||
whowasList := server.whoWas.Find(nick, 1)
|
||||
if len(whowasList) == 0 {
|
||||
service.Notice(rb, client.t("No such nick"))
|
||||
return
|
||||
}
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Warning: %s is not currently connected to the server. Using WHOWAS data, which may be inaccurate:"), nick))
|
||||
details = whowasList[0]
|
||||
} else {
|
||||
details = target.Details().WhoWas
|
||||
}
|
||||
|
||||
if details.account != "" {
|
||||
if channel.getAmode(details.account) != modes.Mode(0) {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s currently has a persistent channel privilege granted with CS AMODE. If this mode is not removed, bans will not be respected"), details.accountName))
|
||||
return
|
||||
} else if details.account == channel.Founder() {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s is the channel founder and cannot be banned"), details.accountName))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
config := server.Config()
|
||||
if !config.Server.Cloaks.EnabledForAlwaysOn {
|
||||
service.Notice(rb, client.t("Warning: server.ip-cloaking.enabled-for-always-on is disabled. This reduces the precision of channel bans."))
|
||||
}
|
||||
|
||||
if details.account != "" {
|
||||
if config.Accounts.NickReservation.ForceNickEqualsAccount || target.AlwaysOn() {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s is authenticated and can be banned by nickname: /MODE %[2]s +b %[3]s!*@*"), details.nick, channel.Name(), details.nick))
|
||||
success = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ban := fmt.Sprintf("*!*@%s", strings.ToLower(details.hostname))
|
||||
banRe, err := utils.CompileGlob(ban, false)
|
||||
if err != nil {
|
||||
server.logger.Error("internal", "couldn't compile ban regex", ban, err.Error())
|
||||
service.Notice(rb, "An error occurred")
|
||||
return
|
||||
}
|
||||
var collateralDamage []string
|
||||
for _, mcl := range channel.Members() {
|
||||
if mcl != target && banRe.MatchString(mcl.NickMaskCasefolded()) {
|
||||
collateralDamage = append(collateralDamage, mcl.Nick())
|
||||
}
|
||||
}
|
||||
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s can be banned by hostname: /MODE %[2]s +b %[3]s"), details.nick, channel.Name(), ban))
|
||||
success = true
|
||||
if len(collateralDamage) != 0 {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Warning: this ban will affect %d other users:"), len(collateralDamage)))
|
||||
for _, line := range utils.BuildTokenLines(maxLastArgLength, collateralDamage, " ") {
|
||||
service.Notice(rb, line)
|
||||
}
|
||||
}
|
||||
}
|
2250
irc/client.go
2250
irc/client.go
File diff suppressed because it is too large
Load Diff
@ -1,299 +1,116 @@
|
||||
// Copyright (c) 2012-2014 Jeremy Latt
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
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/DanielOaks/girc-go/ircmatch"
|
||||
)
|
||||
|
||||
// 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")
|
||||
)
|
||||
|
||||
func ExpandUserHost(userhost string) (expanded string) {
|
||||
expanded = userhost
|
||||
// fill in missing wildcards for nicks
|
||||
//TODO(dan): this would fail with dan@lol, do we want to accommodate 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)
|
||||
type ClientLookupSet struct {
|
||||
ByNick map[string]*Client
|
||||
}
|
||||
|
||||
// Get retrieves a client from the manager, if they exist.
|
||||
func (clients *ClientManager) Get(nick string) *Client {
|
||||
func NewClientLookupSet() *ClientLookupSet {
|
||||
return &ClientLookupSet{
|
||||
ByNick: make(map[string]*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 cli
|
||||
return false
|
||||
}
|
||||
_, exists := clients.ByNick[casefoldedName]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) Get(nick string) *Client {
|
||||
casefoldedName, err := CasefoldName(nick)
|
||||
if err == nil {
|
||||
return clients.ByNick[casefoldedName]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (clients *ClientManager) removeInternal(client *Client, oldcfnick, oldskeleton string) (err error) {
|
||||
// requires holding the writable Lock()
|
||||
if oldcfnick == "*" || oldcfnick == "" {
|
||||
return errNickMissing
|
||||
func (clients *ClientLookupSet) Add(client *Client) error {
|
||||
if !client.HasNick() {
|
||||
return ErrNickMissing
|
||||
}
|
||||
|
||||
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
|
||||
if clients.Get(client.nick) != nil {
|
||||
return ErrNicknameInUse
|
||||
}
|
||||
|
||||
currentEntry, present = clients.bySkeleton[oldskeleton]
|
||||
if present {
|
||||
if currentEntry == client {
|
||||
delete(clients.bySkeleton, oldskeleton)
|
||||
} else {
|
||||
client.server.logger.Warning("internal", "clients for skeleton out of sync", oldskeleton)
|
||||
err = errNickMissing
|
||||
}
|
||||
} else {
|
||||
err = errNickMissing
|
||||
}
|
||||
|
||||
return
|
||||
clients.ByNick[client.nickCasefolded] = 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
|
||||
}
|
||||
if clients.Get(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
|
||||
}
|
||||
}
|
||||
|
||||
// recompute always-on status, because client.alwaysOn is not set for unregistered clients
|
||||
var alwaysOn, useAccountName bool
|
||||
if account != "" {
|
||||
alwaysOn = persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
|
||||
useAccountName = alwaysOn || config.Accounts.NickReservation.ForceNickEqualsAccount
|
||||
}
|
||||
|
||||
nickIsReserved := false
|
||||
|
||||
if useAccountName {
|
||||
if registered && newNick != accountName {
|
||||
return "", errNickAccountMismatch, false
|
||||
}
|
||||
newNick = accountName
|
||||
newCfNick = account
|
||||
newSkeleton, err = Skeleton(newNick)
|
||||
if err != nil {
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
} else {
|
||||
newNick = strings.TrimSpace(newNick)
|
||||
if len(newNick) == 0 {
|
||||
return "", errNickMissing, false
|
||||
}
|
||||
|
||||
if account == "" && config.Accounts.NickReservation.ForceGuestFormat && !dryRun {
|
||||
newCfNick, err = CasefoldName(newNick)
|
||||
if err != nil {
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
if !config.Accounts.NickReservation.guestRegexpFolded.MatchString(newCfNick) {
|
||||
newNick = strings.Replace(config.Accounts.NickReservation.GuestFormat, "*", newNick, 1)
|
||||
newCfNick = "" // re-fold it below
|
||||
}
|
||||
}
|
||||
|
||||
if newCfNick == "" {
|
||||
newCfNick, err = CasefoldName(newNick)
|
||||
}
|
||||
if err != nil {
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
if len(newNick) > config.Limits.NickLen || len(newCfNick) > config.Limits.NickLen {
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
newSkeleton, err = Skeleton(newNick)
|
||||
if err != nil {
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
|
||||
if config.isRelaymsgIdentifier(newNick) {
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
|
||||
if restrictedCasefoldedNicks.Has(newCfNick) || restrictedSkeletons.Has(newSkeleton) {
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
|
||||
reservedAccount, method := client.server.accounts.EnforcementStatus(newCfNick, newSkeleton)
|
||||
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
|
||||
// see #2135: we want to enter the critical section, see if the nick is actually in use,
|
||||
// and return errNicknameInUse in that case
|
||||
nickIsReserved = true
|
||||
}
|
||||
}
|
||||
|
||||
var bouncerAllowed bool
|
||||
if config.Accounts.Multiclient.Enabled {
|
||||
if useAccountName {
|
||||
bouncerAllowed = true
|
||||
} else {
|
||||
if config.Accounts.Multiclient.AllowedByDefault && settings.AllowBouncer != MulticlientDisallowedByUser {
|
||||
bouncerAllowed = true
|
||||
} else if settings.AllowBouncer == MulticlientAllowedByUser {
|
||||
bouncerAllowed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// FindAll returns all clients that match the given userhost mask.
|
||||
func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
|
||||
func (clients *ClientLookupSet) AllWithCaps(caps ...Capability) (set ClientSet) {
|
||||
set = make(ClientSet)
|
||||
|
||||
userhost, err := CanonicalizeMaskWildcard(userhost)
|
||||
var client *Client
|
||||
for _, client = range clients.ByNick {
|
||||
// make sure they have all the required caps
|
||||
for _, Cap := range caps {
|
||||
if !client.capabilities[Cap] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
set.Add(client)
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) {
|
||||
set = make(ClientSet)
|
||||
|
||||
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()) {
|
||||
for _, client := range clients.ByNick {
|
||||
if matcher.Match(client.nickMaskCasefolded) {
|
||||
set.Add(client)
|
||||
}
|
||||
}
|
||||
@ -301,15 +118,117 @@ 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
|
||||
func (clients *ClientLookupSet) Find(userhost string) *Client {
|
||||
userhost, err := Casefold(ExpandUserHost(userhost))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
matcher := ircmatch.MakeMatch(userhost)
|
||||
|
||||
for _, client := range clients.ByNick {
|
||||
if matcher.Match(client.nickMaskCasefolded) {
|
||||
return client
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// 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")
|
||||
}
|
||||
}
|
126
irc/clientsocket.go
Normal file
126
irc/clientsocket.go
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/DanielOaks/girc-go/ircmsg"
|
||||
)
|
||||
|
||||
// ClientSocket listens to a socket using the IRC protocol, processes events,
|
||||
// and also sends IRC lines out of that socket.
|
||||
type ClientSocket struct {
|
||||
receiveLines chan string
|
||||
ReceiveEvents chan Message
|
||||
SendLines chan string
|
||||
socket Socket
|
||||
client Client
|
||||
}
|
||||
|
||||
// NewClientSocket returns a new ClientSocket.
|
||||
func NewClientSocket(conn net.Conn, client Client) ClientSocket {
|
||||
return ClientSocket{
|
||||
receiveLines: make(chan string),
|
||||
ReceiveEvents: make(chan Message),
|
||||
SendLines: make(chan string),
|
||||
socket: NewSocket(conn),
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// Start creates and starts running the necessary event loops.
|
||||
func (cs *ClientSocket) Start() {
|
||||
go cs.RunEvents()
|
||||
go cs.RunSocketSender()
|
||||
go cs.RunSocketListener()
|
||||
}
|
||||
|
||||
// RunEvents handles received IRC lines and processes incoming commands.
|
||||
func (cs *ClientSocket) RunEvents() {
|
||||
var exiting bool
|
||||
var line string
|
||||
for !exiting {
|
||||
select {
|
||||
case line = <-cs.receiveLines:
|
||||
if line != "" {
|
||||
fmt.Println("<- ", strings.TrimRight(line, "\r\n"))
|
||||
exiting = cs.processIncomingLine(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
// empty the receiveLines queue
|
||||
cs.socket.Close()
|
||||
select {
|
||||
case <-cs.receiveLines:
|
||||
// empty
|
||||
default:
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
// RunSocketSender sends lines to the IRC socket.
|
||||
func (cs *ClientSocket) RunSocketSender() {
|
||||
var err error
|
||||
var line string
|
||||
for {
|
||||
line = <-cs.SendLines
|
||||
err = cs.socket.Write(line)
|
||||
fmt.Println(" ->", strings.TrimRight(line, "\r\n"))
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunSocketListener receives lines from the IRC socket.
|
||||
func (cs *ClientSocket) RunSocketListener() {
|
||||
var errConn error
|
||||
var line string
|
||||
|
||||
for {
|
||||
line, errConn = cs.socket.Read()
|
||||
cs.receiveLines <- line
|
||||
if errConn != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !cs.socket.Closed {
|
||||
cs.Send(nil, "", "ERROR", "Closing connection")
|
||||
cs.socket.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Send sends an IRC line to the listener.
|
||||
func (cs *ClientSocket) Send(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) error {
|
||||
ircmsg := ircmsg.MakeMessage(tags, prefix, command, params...)
|
||||
line, err := ircmsg.Line()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cs.SendLines <- line
|
||||
return nil
|
||||
}
|
||||
|
||||
// processIncomingLine splits and handles the given command line.
|
||||
// Returns true if client is exiting (sent a QUIT command, etc).
|
||||
func (cs *ClientSocket) processIncomingLine(line string) bool {
|
||||
msg, err := ircmsg.ParseLine(line)
|
||||
if err != nil {
|
||||
cs.Send(nil, "", "ERROR", "Your client sent a malformed line")
|
||||
return true
|
||||
}
|
||||
|
||||
command, canBeParsed := Commands[msg.Command]
|
||||
|
||||
if canBeParsed {
|
||||
return command.Run(cs.client.server, &cs.client, msg)
|
||||
}
|
||||
//TODO(dan): This is an error+disconnect purely for reasons of testing.
|
||||
// Later it may be downgraded to not-that-bad.
|
||||
cs.Send(nil, "", "ERROR", fmt.Sprintf("Your client sent a command that could not be parsed [%s]", msg.Command))
|
||||
return true
|
||||
}
|
@ -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)
|
||||
}
|
575
irc/commands.go
575
irc/commands.go
@ -1,416 +1,201 @@
|
||||
// Copyright (c) 2012-2014 Jeremy Latt
|
||||
// Copyright (c) 2014-2015 Edmund Huber
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
import "github.com/DanielOaks/girc-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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// command silently ignored
|
||||
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(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()
|
||||
}
|
||||
if !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{
|
||||
"AUTHENTICATE": {
|
||||
handler: authenticateHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"AWAY": {
|
||||
handler: awayHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"CAP": {
|
||||
handler: capHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"DEBUG": {
|
||||
handler: debugHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"HELP": {
|
||||
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,
|
||||
},
|
||||
"LIST": {
|
||||
handler: listHandler,
|
||||
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,
|
||||
},
|
||||
"NOTICE": {
|
||||
handler: noticeHandler,
|
||||
minParams: 2,
|
||||
},
|
||||
"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,
|
||||
},
|
||||
"SANICK": {
|
||||
handler: sanickHandler,
|
||||
minParams: 2,
|
||||
oper: true,
|
||||
},
|
||||
"QUIT": {
|
||||
handler: quitHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 0,
|
||||
},
|
||||
"REG": {
|
||||
handler: regHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"REHASH": {
|
||||
handler: rehashHandler,
|
||||
minParams: 0,
|
||||
oper: true,
|
||||
},
|
||||
"TIME": {
|
||||
handler: timeHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"TOPIC": {
|
||||
handler: topicHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"USER": {
|
||||
handler: userHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 4,
|
||||
},
|
||||
"VERSION": {
|
||||
handler: versionHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"WHO": {
|
||||
handler: whoHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"WHOIS": {
|
||||
handler: whoisHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"WHOWAS": {
|
||||
handler: whowasHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
}
|
||||
|
1950
irc/config.go
1950
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -1,14 +1,18 @@
|
||||
// Copyright (c) 2012-2014 Jeremy Latt
|
||||
// Copyright (c) 2014-2015 Edmund Huber
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
// 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
|
||||
// maxTargets is the maximum number of targets for PRIVMSG and NOTICE.
|
||||
maxTargets = 4
|
||||
// SemVer is the semantic version of Oragono.
|
||||
SemVer = "0.3.0"
|
||||
)
|
||||
|
||||
var (
|
||||
// Ver is the full version of Oragono, used in responses to clients.
|
||||
Ver = fmt.Sprintf("oragono-%s", SemVer)
|
||||
)
|
||||
|
@ -1,199 +0,0 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
|
||||
package custime
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// see https://github.com/golang/go/blob/7ad512e7ffe576c4894ea84b02e954846fbda643/src/time/format.go#L1251
|
||||
|
||||
// This is a forked version of the ParseDuration function that also handles days/months/years
|
||||
|
||||
var errLeadingInt = errors.New("time: bad [0-9]*") // never printed
|
||||
|
||||
// leadingInt consumes the leading [0-9]* from s.
|
||||
func leadingInt(s string) (x int64, rem string, err error) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
if x > (1<<63-1)/10 {
|
||||
// overflow
|
||||
return 0, "", errLeadingInt
|
||||
}
|
||||
x = x*10 + int64(c) - '0'
|
||||
if x < 0 {
|
||||
// overflow
|
||||
return 0, "", errLeadingInt
|
||||
}
|
||||
}
|
||||
return x, s[i:], nil
|
||||
}
|
||||
|
||||
// leadingFraction consumes the leading [0-9]* from s.
|
||||
// It is used only for fractions, so does not return an error on overflow,
|
||||
// it just stops accumulating precision.
|
||||
func leadingFraction(s string) (x int64, scale float64, rem string) {
|
||||
i := 0
|
||||
scale = 1
|
||||
overflow := false
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
if overflow {
|
||||
continue
|
||||
}
|
||||
if x > (1<<63-1)/10 {
|
||||
// It's possible for overflow to give a positive number, so take care.
|
||||
overflow = true
|
||||
continue
|
||||
}
|
||||
y := x*10 + int64(c) - '0'
|
||||
if y < 0 {
|
||||
overflow = true
|
||||
continue
|
||||
}
|
||||
x = y
|
||||
scale *= 10
|
||||
}
|
||||
return x, scale, s[i:]
|
||||
}
|
||||
|
||||
var unitMap = map[string]int64{
|
||||
"ns": int64(time.Nanosecond),
|
||||
"us": int64(time.Microsecond),
|
||||
"µs": int64(time.Microsecond), // U+00B5 = micro symbol
|
||||
"μs": int64(time.Microsecond), // U+03BC = Greek letter mu
|
||||
"ms": int64(time.Millisecond),
|
||||
"s": int64(time.Second),
|
||||
"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),
|
||||
}
|
||||
|
||||
// ParseDuration parses a duration string.
|
||||
// A duration string is a possibly signed sequence of
|
||||
// decimal numbers, each with optional fraction and a unit suffix,
|
||||
// such as "300ms", "-1.5h" or "2h45m".
|
||||
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
func ParseDuration(s string) (time.Duration, error) {
|
||||
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
|
||||
orig := s
|
||||
var d int64
|
||||
neg := false
|
||||
|
||||
// Consume [-+]?
|
||||
if s != "" {
|
||||
c := s[0]
|
||||
if c == '-' || c == '+' {
|
||||
neg = c == '-'
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
// Special case: if all that is left is "0", this is zero.
|
||||
if s == "0" {
|
||||
return 0, nil
|
||||
}
|
||||
if s == "" {
|
||||
return 0, errors.New("time: invalid duration " + orig)
|
||||
}
|
||||
for s != "" {
|
||||
var (
|
||||
v, f int64 // integers before, after decimal point
|
||||
scale float64 = 1 // value = v + f/scale
|
||||
)
|
||||
|
||||
var err error
|
||||
|
||||
// The next character must be [0-9.]
|
||||
if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') {
|
||||
return 0, errors.New("time: invalid duration " + orig)
|
||||
}
|
||||
// Consume [0-9]*
|
||||
pl := len(s)
|
||||
v, s, err = leadingInt(s)
|
||||
if err != nil {
|
||||
return 0, errors.New("time: invalid duration " + orig)
|
||||
}
|
||||
pre := pl != len(s) // whether we consumed anything before a period
|
||||
|
||||
// Consume (\.[0-9]*)?
|
||||
post := false
|
||||
if s != "" && s[0] == '.' {
|
||||
s = s[1:]
|
||||
pl := len(s)
|
||||
f, scale, s = leadingFraction(s)
|
||||
post = pl != len(s)
|
||||
}
|
||||
if !pre && !post {
|
||||
// no digits (e.g. ".s" or "-.s")
|
||||
return 0, errors.New("time: invalid duration " + orig)
|
||||
}
|
||||
|
||||
// Consume unit.
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c == '.' || '0' <= c && c <= '9' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
return 0, errors.New("time: missing unit in duration " + orig)
|
||||
}
|
||||
u := s[:i]
|
||||
s = s[i:]
|
||||
unit, ok := unitMap[u]
|
||||
if !ok {
|
||||
return 0, errors.New("time: unknown unit " + u + " in duration " + orig)
|
||||
}
|
||||
if v > (1<<63-1)/unit {
|
||||
// overflow
|
||||
return 0, errors.New("time: invalid duration " + orig)
|
||||
}
|
||||
v *= unit
|
||||
if f > 0 {
|
||||
// float64 is needed to be nanosecond accurate for fractions of hours.
|
||||
// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
|
||||
v += int64(float64(f) * (float64(unit) / scale))
|
||||
if v < 0 {
|
||||
// overflow
|
||||
return 0, errors.New("time: invalid duration " + orig)
|
||||
}
|
||||
}
|
||||
d += v
|
||||
if d < 0 {
|
||||
// overflow
|
||||
return 0, errors.New("time: invalid duration " + orig)
|
||||
}
|
||||
}
|
||||
|
||||
if neg {
|
||||
d = -d
|
||||
}
|
||||
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
|
||||
}
|
1367
irc/database.go
1367
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
|
||||
}
|
75
irc/debug.go
Normal file
75
irc/debug.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2012-2014 Jeremy Latt
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/DanielOaks/girc-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
|
||||
}
|
280
irc/dline.go
280
irc/dline.go
@ -1,280 +0,0 @@
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/flatip"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
keyDlineEntry = "bans.dlinev2 %s"
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NewDLineManager returns a new DLineManager.
|
||||
func NewDLineManager(server *Server) *DLineManager {
|
||||
var dm DLineManager
|
||||
dm.networks = make(map[flatip.IPNet]IPBanInfo)
|
||||
dm.expirationTimers = make(map[flatip.IPNet]*time.Timer)
|
||||
dm.server = server
|
||||
|
||||
dm.loadFromDatastore()
|
||||
|
||||
return &dm
|
||||
}
|
||||
|
||||
// AllBans returns all bans (for use with APIs, etc).
|
||||
func (dm *DLineManager) AllBans() map[string]IPBanInfo {
|
||||
allb := make(map[string]IPBanInfo)
|
||||
|
||||
dm.RLock()
|
||||
defer dm.RUnlock()
|
||||
|
||||
for key, info := range dm.networks {
|
||||
allb[key.HumanReadableString()] = 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,
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveNetwork removes a network from the blocked list.
|
||||
func (dm *DLineManager) RemoveNetwork(network flatip.IPNet) error {
|
||||
dm.persistenceMutex.Lock()
|
||||
defer dm.persistenceMutex.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
|
||||
}
|
||||
|
||||
return dm.unpersistDline(id)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
// no matches!
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// get address name
|
||||
key = strings.TrimPrefix(key, dlinePrefix)
|
||||
|
||||
// load addr/net
|
||||
hostNet, err := flatip.ParseToNormalizedNet(key)
|
||||
if err != nil {
|
||||
dm.server.logger.Error("internal", "bad dline cidr", err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// add to the server
|
||||
dm.addNetworkInternal(hostNet, info)
|
||||
|
||||
return true
|
||||
})
|
||||
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))
|
||||
}
|
123
irc/fakelag.go
123
irc/fakelag.go
@ -1,123 +0,0 @@
|
||||
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fakelag is a system for artificially delaying commands when a user issues
|
||||
// them too rapidly
|
||||
|
||||
type FakelagState uint
|
||||
|
||||
const (
|
||||
// initially, the client is "bursting" and can send n commands without
|
||||
// encountering fakelag
|
||||
FakelagBursting FakelagState = iota
|
||||
// after that, they're "throttled" and we sleep in between commands until
|
||||
// they're spaced sufficiently far apart
|
||||
FakelagThrottled
|
||||
)
|
||||
|
||||
// this is intentionally not threadsafe, because it should only be touched
|
||||
// from the loop that accepts the client's input and runs commands
|
||||
type Fakelag struct {
|
||||
config FakelagConfig
|
||||
suspended bool
|
||||
nowFunc func() time.Time
|
||||
sleepFunc func(time.Duration)
|
||||
|
||||
state FakelagState
|
||||
burstCount uint // number of messages sent in the current burst
|
||||
lastTouch time.Time
|
||||
}
|
||||
|
||||
func (fl *Fakelag) Initialize(config FakelagConfig) {
|
||||
fl.config = config
|
||||
// XXX don't share mutable member CommandBudgets:
|
||||
if config.CommandBudgets != nil {
|
||||
fl.config.CommandBudgets = maps.Clone(config.CommandBudgets)
|
||||
}
|
||||
fl.nowFunc = time.Now
|
||||
fl.sleepFunc = time.Sleep
|
||||
fl.state = FakelagBursting
|
||||
}
|
||||
|
||||
// Idempotently turn off fakelag if it's enabled
|
||||
func (fl *Fakelag) Suspend() {
|
||||
if fl.config.Enabled {
|
||||
fl.suspended = true
|
||||
fl.config.Enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// Idempotently turn fakelag back on if it was previously Suspend'ed
|
||||
func (fl *Fakelag) Unsuspend() {
|
||||
if fl.suspended {
|
||||
fl.config.Enabled = true
|
||||
fl.suspended = false
|
||||
}
|
||||
}
|
||||
|
||||
// register a new command, sleep if necessary to delay it
|
||||
func (fl *Fakelag) Touch(command string) {
|
||||
if !fl.config.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if budget, ok := fl.config.CommandBudgets[command]; ok && budget > 0 {
|
||||
fl.config.CommandBudgets[command] = budget - 1
|
||||
return
|
||||
}
|
||||
|
||||
now := fl.nowFunc()
|
||||
// XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine
|
||||
elapsed := now.Sub(fl.lastTouch)
|
||||
fl.lastTouch = now
|
||||
|
||||
if fl.state == FakelagBursting {
|
||||
// determine if the previous burst is over
|
||||
if elapsed > fl.config.Cooldown {
|
||||
fl.burstCount = 0
|
||||
}
|
||||
|
||||
fl.burstCount++
|
||||
if fl.burstCount > fl.config.BurstLimit {
|
||||
// reset burst window for next time
|
||||
fl.burstCount = 0
|
||||
// transition to throttling
|
||||
fl.state = FakelagThrottled
|
||||
// continue to throttling logic
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if fl.state == FakelagThrottled {
|
||||
if elapsed > fl.config.Cooldown {
|
||||
// let them burst again
|
||||
fl.state = FakelagBursting
|
||||
fl.burstCount = 1
|
||||
return
|
||||
}
|
||||
var sleepDuration time.Duration
|
||||
if fl.config.MessagesPerWindow > 0 {
|
||||
// space them out by at least window/messagesperwindow
|
||||
sleepDuration = time.Duration((int64(fl.config.Window) / int64(fl.config.MessagesPerWindow)) - int64(elapsed))
|
||||
} else {
|
||||
// only burst messages are allowed: sleep until cooldown expires,
|
||||
// then count this as a burst message
|
||||
sleepDuration = time.Duration(int64(fl.config.Cooldown) - int64(elapsed))
|
||||
fl.state = FakelagBursting
|
||||
fl.burstCount = 1
|
||||
}
|
||||
if sleepDuration > 0 {
|
||||
fl.sleepFunc(sleepDuration)
|
||||
// the touch time should take into account the time we slept
|
||||
fl.lastTouch = fl.nowFunc()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,155 +0,0 @@
|
||||
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mockTime struct {
|
||||
now time.Time
|
||||
sleepList []time.Duration
|
||||
lastCheckedSleep int
|
||||
}
|
||||
|
||||
func (mt *mockTime) Now() (now time.Time) {
|
||||
return mt.now
|
||||
}
|
||||
|
||||
func (mt *mockTime) Sleep(dur time.Duration) {
|
||||
mt.sleepList = append(mt.sleepList, dur)
|
||||
mt.pause(dur)
|
||||
}
|
||||
|
||||
func (mt *mockTime) pause(dur time.Duration) {
|
||||
mt.now = mt.now.Add(dur)
|
||||
}
|
||||
|
||||
func (mt *mockTime) lastSleep() (slept bool, duration time.Duration) {
|
||||
if mt.lastCheckedSleep == len(mt.sleepList)-1 {
|
||||
slept = false
|
||||
return
|
||||
}
|
||||
|
||||
slept = true
|
||||
mt.lastCheckedSleep += 1
|
||||
duration = mt.sleepList[mt.lastCheckedSleep]
|
||||
return
|
||||
}
|
||||
|
||||
func newFakelagForTesting(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint, cooldown time.Duration) (*Fakelag, *mockTime) {
|
||||
fl := Fakelag{}
|
||||
fl.config = FakelagConfig{
|
||||
Enabled: true,
|
||||
Window: window,
|
||||
BurstLimit: burstLimit,
|
||||
MessagesPerWindow: throttleMessagesPerWindow,
|
||||
Cooldown: cooldown,
|
||||
}
|
||||
mt := new(mockTime)
|
||||
mt.now, _ = time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2006")
|
||||
mt.lastCheckedSleep = -1
|
||||
fl.nowFunc = mt.Now
|
||||
fl.sleepFunc = mt.Sleep
|
||||
return &fl, mt
|
||||
}
|
||||
|
||||
func TestFakelag(t *testing.T) {
|
||||
window, _ := time.ParseDuration("1s")
|
||||
fl, mt := newFakelagForTesting(window, 3, 2, window)
|
||||
|
||||
fl.Touch("")
|
||||
slept, _ := mt.lastSleep()
|
||||
if slept {
|
||||
t.Fatalf("should not have slept")
|
||||
}
|
||||
|
||||
interval, _ := time.ParseDuration("100ms")
|
||||
for i := 0; i < 2; i++ {
|
||||
mt.pause(interval)
|
||||
fl.Touch("")
|
||||
slept, _ := mt.lastSleep()
|
||||
if slept {
|
||||
t.Fatalf("should not have slept")
|
||||
}
|
||||
}
|
||||
|
||||
mt.pause(interval)
|
||||
fl.Touch("")
|
||||
if fl.state != FakelagThrottled {
|
||||
t.Fatalf("should be throttled")
|
||||
}
|
||||
slept, duration := mt.lastSleep()
|
||||
if !slept {
|
||||
t.Fatalf("should have slept due to fakelag")
|
||||
}
|
||||
expected, _ := time.ParseDuration("400ms")
|
||||
if duration != expected {
|
||||
t.Fatalf("incorrect sleep time: %v != %v", expected, duration)
|
||||
}
|
||||
|
||||
// send another message without a pause; we should have to sleep for 500 msec
|
||||
fl.Touch("")
|
||||
if fl.state != FakelagThrottled {
|
||||
t.Fatalf("should be throttled")
|
||||
}
|
||||
slept, duration = mt.lastSleep()
|
||||
expected, _ = time.ParseDuration("500ms")
|
||||
if duration != expected {
|
||||
t.Fatalf("incorrect sleep time: %v != %v", duration, expected)
|
||||
}
|
||||
|
||||
mt.pause(interval * 6)
|
||||
fl.Touch("")
|
||||
if fl.state != FakelagThrottled {
|
||||
t.Fatalf("should still be throttled")
|
||||
}
|
||||
slept, duration = mt.lastSleep()
|
||||
if duration != 0 {
|
||||
t.Fatalf("we paused for long enough that we shouldn't sleep here")
|
||||
}
|
||||
|
||||
mt.pause(window * 2)
|
||||
fl.Touch("")
|
||||
if fl.state != FakelagBursting {
|
||||
t.Fatalf("should be bursting again")
|
||||
}
|
||||
slept, _ = mt.lastSleep()
|
||||
if slept {
|
||||
t.Fatalf("should not have slept")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuspend(t *testing.T) {
|
||||
window, _ := time.ParseDuration("1s")
|
||||
fl, _ := newFakelagForTesting(window, 3, 2, window)
|
||||
assertEqual(fl.config.Enabled, true)
|
||||
|
||||
// suspend idempotently disables
|
||||
fl.Suspend()
|
||||
assertEqual(fl.config.Enabled, false)
|
||||
fl.Suspend()
|
||||
assertEqual(fl.config.Enabled, false)
|
||||
// unsuspend idempotently enables
|
||||
fl.Unsuspend()
|
||||
assertEqual(fl.config.Enabled, true)
|
||||
fl.Unsuspend()
|
||||
assertEqual(fl.config.Enabled, true)
|
||||
fl.Suspend()
|
||||
assertEqual(fl.config.Enabled, false)
|
||||
|
||||
fl2, _ := newFakelagForTesting(window, 3, 2, window)
|
||||
fl2.config.Enabled = false
|
||||
|
||||
// if we were never enabled, suspend and unsuspend are both no-ops
|
||||
fl2.Suspend()
|
||||
assertEqual(fl2.config.Enabled, false)
|
||||
fl2.Suspend()
|
||||
assertEqual(fl2.config.Enabled, false)
|
||||
fl2.Unsuspend()
|
||||
assertEqual(fl2.config.Enabled, false)
|
||||
fl2.Unsuspend()
|
||||
assertEqual(fl2.config.Enabled, false)
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
// Copyright 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// Released under the MIT license
|
||||
|
||||
package flatip
|
||||
|
||||
// begin ad-hoc utilities
|
||||
|
||||
// ParseToNormalizedNet attempts to interpret a string either as an IP
|
||||
// network in CIDR notation, returning an IPNet, or as an IP address,
|
||||
// returning an IPNet that contains only that address.
|
||||
func ParseToNormalizedNet(netstr string) (ipnet IPNet, err error) {
|
||||
_, ipnet, err = ParseCIDR(netstr)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
ip, err := ParseIP(netstr)
|
||||
if err == nil {
|
||||
ipnet.IP = ip
|
||||
ipnet.PrefixLen = 128
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IPInNets is a convenience function for testing whether an IP is contained
|
||||
// in any member of a slice of IPNet's.
|
||||
func IPInNets(addr IP, nets []IPNet) bool {
|
||||
for _, net := range nets {
|
||||
if net.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,220 +0,0 @@
|
||||
// Copyright 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// Copyright 2009 The Go Authors
|
||||
// Released under the MIT license
|
||||
|
||||
package flatip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
v4InV6Prefix = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}
|
||||
|
||||
IPv6loopback = IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||
IPv6zero = IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
IPv4zero = IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0, 0, 0}
|
||||
|
||||
ErrInvalidIPString = errors.New("String could not be interpreted as an IP address")
|
||||
)
|
||||
|
||||
// packed versions of net.IP and net.IPNet; these are pure value types,
|
||||
// so they can be compared with == and used as map keys.
|
||||
|
||||
// IP is a 128-bit representation of an IP address, using the 4-in-6 mapping
|
||||
// to represent IPv4 addresses.
|
||||
type IP [16]byte
|
||||
|
||||
// IPNet is a IP network. In a valid value, all bits after PrefixLen are zeroes.
|
||||
type IPNet struct {
|
||||
IP
|
||||
PrefixLen uint8
|
||||
}
|
||||
|
||||
// NetIP converts an IP into a net.IP.
|
||||
func (ip IP) NetIP() (result net.IP) {
|
||||
result = make(net.IP, 16)
|
||||
copy(result[:], ip[:])
|
||||
return
|
||||
}
|
||||
|
||||
// FromNetIP converts a net.IP into an IP.
|
||||
func FromNetIP(ip net.IP) (result IP) {
|
||||
if len(ip) == 16 {
|
||||
copy(result[:], ip[:])
|
||||
} else {
|
||||
result[10] = 0xff
|
||||
result[11] = 0xff
|
||||
copy(result[12:], ip[:])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IPv4 returns the IP address representation of a.b.c.d
|
||||
func IPv4(a, b, c, d byte) (result IP) {
|
||||
copy(result[:12], v4InV6Prefix)
|
||||
result[12] = a
|
||||
result[13] = b
|
||||
result[14] = c
|
||||
result[15] = d
|
||||
return
|
||||
}
|
||||
|
||||
// ParseIP parses a string representation of an IP address into an IP.
|
||||
// Unlike net.ParseIP, it returns an error instead of a zero value on failure,
|
||||
// since the zero value of `IP` is a representation of a valid IP (::0, the
|
||||
// IPv6 "unspecified address").
|
||||
func ParseIP(ipstr string) (ip IP, err error) {
|
||||
// TODO reimplement this without net.ParseIP
|
||||
netip := net.ParseIP(ipstr)
|
||||
if netip == nil {
|
||||
err = ErrInvalidIPString
|
||||
return
|
||||
}
|
||||
netip = netip.To16()
|
||||
copy(ip[:], netip)
|
||||
return
|
||||
}
|
||||
|
||||
// String returns the string representation of an IP
|
||||
func (ip IP) String() string {
|
||||
// TODO reimplement this without using (net.IP).String()
|
||||
return (net.IP)(ip[:]).String()
|
||||
}
|
||||
|
||||
// IsIPv4 returns whether the IP is an IPv4 address.
|
||||
func (ip IP) IsIPv4() bool {
|
||||
return bytes.Equal(ip[:12], v4InV6Prefix)
|
||||
}
|
||||
|
||||
// IsLoopback returns whether the IP is a loopback address.
|
||||
func (ip IP) IsLoopback() bool {
|
||||
if ip.IsIPv4() {
|
||||
return ip[12] == 127
|
||||
} else {
|
||||
return ip == IPv6loopback
|
||||
}
|
||||
}
|
||||
|
||||
func (ip IP) IsUnspecified() bool {
|
||||
return ip == IPv4zero || ip == IPv6zero
|
||||
}
|
||||
|
||||
func rawCidrMask(length int) (m IP) {
|
||||
n := uint(length)
|
||||
for i := 0; i < 16; i++ {
|
||||
if n >= 8 {
|
||||
m[i] = 0xff
|
||||
n -= 8
|
||||
continue
|
||||
}
|
||||
m[i] = ^byte(0xff >> n)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ip IP) applyMask(mask IP) (result IP) {
|
||||
for i := 0; i < 16; i += 1 {
|
||||
result[i] = ip[i] & mask[i]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func cidrMask(ones, bits int) (result IP) {
|
||||
switch bits {
|
||||
case 32:
|
||||
return rawCidrMask(96 + ones)
|
||||
case 128:
|
||||
return rawCidrMask(ones)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Mask returns the result of masking ip with the CIDR mask of
|
||||
// length 'ones', out of a total of 'bits' (which must be either
|
||||
// 32 for an IPv4 subnet or 128 for an IPv6 subnet).
|
||||
func (ip IP) Mask(ones, bits int) (result IP) {
|
||||
return ip.applyMask(cidrMask(ones, bits))
|
||||
}
|
||||
|
||||
// ToNetIPNet converts an IPNet into a net.IPNet.
|
||||
func (cidr IPNet) ToNetIPNet() (result net.IPNet) {
|
||||
return net.IPNet{
|
||||
IP: cidr.IP.NetIP(),
|
||||
Mask: net.CIDRMask(int(cidr.PrefixLen), 128),
|
||||
}
|
||||
}
|
||||
|
||||
// Contains retuns whether the network contains `ip`.
|
||||
func (cidr IPNet) Contains(ip IP) bool {
|
||||
maskedIP := ip.Mask(int(cidr.PrefixLen), 128)
|
||||
return cidr.IP == maskedIP
|
||||
}
|
||||
|
||||
func (cidr IPNet) Size() (ones, bits int) {
|
||||
if cidr.IP.IsIPv4() {
|
||||
return int(cidr.PrefixLen) - 96, 32
|
||||
} else {
|
||||
return int(cidr.PrefixLen), 128
|
||||
}
|
||||
}
|
||||
|
||||
// FromNetIPnet converts a net.IPNet into an IPNet.
|
||||
func FromNetIPNet(network net.IPNet) (result IPNet) {
|
||||
ones, _ := network.Mask.Size()
|
||||
if len(network.IP) == 16 {
|
||||
copy(result.IP[:], network.IP[:])
|
||||
} else {
|
||||
result.IP[10] = 0xff
|
||||
result.IP[11] = 0xff
|
||||
copy(result.IP[12:], network.IP[:])
|
||||
ones += 96
|
||||
}
|
||||
// perform masking so that equal CIDRs are ==
|
||||
result.IP = result.IP.Mask(ones, 128)
|
||||
result.PrefixLen = uint8(ones)
|
||||
return
|
||||
}
|
||||
|
||||
// String returns a string representation of an IPNet.
|
||||
func (cidr IPNet) String() string {
|
||||
ip := make(net.IP, 16)
|
||||
copy(ip[:], cidr.IP[:])
|
||||
ipnet := net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.CIDRMask(int(cidr.PrefixLen), 128),
|
||||
}
|
||||
return ipnet.String()
|
||||
}
|
||||
|
||||
// HumanReadableString returns a string representation of an IPNet;
|
||||
// if the network contains only a single IP address, it returns
|
||||
// a representation of that address.
|
||||
func (cidr IPNet) HumanReadableString() string {
|
||||
if cidr.PrefixLen == 128 {
|
||||
return cidr.IP.String()
|
||||
}
|
||||
return cidr.String()
|
||||
}
|
||||
|
||||
// IsZero tests whether ipnet is the zero value of an IPNet, 0::0/0.
|
||||
// Although this is a valid subnet, it can still be used as a sentinel
|
||||
// value in some contexts.
|
||||
func (ipnet IPNet) IsZero() bool {
|
||||
return ipnet == IPNet{}
|
||||
}
|
||||
|
||||
// ParseCIDR parses a string representation of an IP network in CIDR notation,
|
||||
// then returns it as an IPNet (along with the original, unmasked address).
|
||||
func ParseCIDR(netstr string) (ip IP, ipnet IPNet, err error) {
|
||||
// TODO reimplement this without net.ParseCIDR
|
||||
nip, nipnet, err := net.ParseCIDR(netstr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return FromNetIP(nip), FromNetIPNet(*nipnet), nil
|
||||
}
|
@ -1,208 +0,0 @@
|
||||
package flatip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func easyParseIP(ipstr string) (result net.IP) {
|
||||
result = net.ParseIP(ipstr)
|
||||
if result == nil {
|
||||
panic(ipstr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func easyParseFlat(ipstr string) (result IP) {
|
||||
x := easyParseIP(ipstr)
|
||||
return FromNetIP(x)
|
||||
}
|
||||
|
||||
func easyParseIPNet(nipstr string) (result net.IPNet) {
|
||||
_, nip, err := net.ParseCIDR(nipstr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return *nip
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
nip := easyParseIP("8.8.8.8")
|
||||
flatip := FromNetIP(nip)
|
||||
if flatip.String() != "8.8.8.8" {
|
||||
t.Errorf("conversions don't work")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoopback(t *testing.T) {
|
||||
localhost_v4 := easyParseFlat("127.0.0.1")
|
||||
localhost_v4_again := easyParseFlat("127.2.3.4")
|
||||
google := easyParseFlat("8.8.8.8")
|
||||
loopback_v6 := easyParseFlat("::1")
|
||||
google_v6 := easyParseFlat("2607:f8b0:4006:801::2004")
|
||||
|
||||
if !(localhost_v4.IsLoopback() && localhost_v4_again.IsLoopback() && loopback_v6.IsLoopback()) {
|
||||
t.Errorf("can't detect loopbacks")
|
||||
}
|
||||
|
||||
if google_v6.IsLoopback() || google.IsLoopback() {
|
||||
t.Errorf("incorrectly detected loopbacks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
nipnet := easyParseIPNet("8.8.0.0/16")
|
||||
flatipnet := FromNetIPNet(nipnet)
|
||||
nip := easyParseIP("8.8.8.8")
|
||||
flatip_ := FromNetIP(nip)
|
||||
if !flatipnet.Contains(flatip_) {
|
||||
t.Errorf("contains doesn't work")
|
||||
}
|
||||
}
|
||||
|
||||
var testIPStrs = []string{
|
||||
"8.8.8.8",
|
||||
"127.0.0.1",
|
||||
"1.1.1.1",
|
||||
"128.127.65.64",
|
||||
"2001:0db8::1",
|
||||
"::1",
|
||||
"255.255.255.255",
|
||||
}
|
||||
|
||||
func doMaskingTest(ip net.IP, t *testing.T) {
|
||||
flat := FromNetIP(ip)
|
||||
netLen := len(ip) * 8
|
||||
for i := 0; i < netLen; i++ {
|
||||
masked := flat.Mask(i, netLen)
|
||||
netMask := net.CIDRMask(i, netLen)
|
||||
netMasked := ip.Mask(netMask)
|
||||
if !bytes.Equal(masked[:], netMasked.To16()) {
|
||||
t.Errorf("Masking %s with %d/%d; expected %s, got %s", ip.String(), i, netLen, netMasked.String(), masked.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(found, expected interface{}) {
|
||||
if !reflect.DeepEqual(found, expected) {
|
||||
panic(fmt.Sprintf("expected %#v, found %#v", expected, found))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
_, net, err := ParseCIDR("8.8.8.8/24")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ones, bits := net.Size()
|
||||
assertEqual(ones, 24)
|
||||
assertEqual(bits, 32)
|
||||
|
||||
_, net, err = ParseCIDR("2001::0db8/64")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ones, bits = net.Size()
|
||||
assertEqual(ones, 64)
|
||||
assertEqual(bits, 128)
|
||||
|
||||
_, net, err = ParseCIDR("2001::0db8/96")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ones, bits = net.Size()
|
||||
assertEqual(ones, 96)
|
||||
assertEqual(bits, 128)
|
||||
}
|
||||
|
||||
func TestMasking(t *testing.T) {
|
||||
for _, ipstr := range testIPStrs {
|
||||
doMaskingTest(easyParseIP(ipstr), t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskingFuzz(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
buf := make([]byte, 4)
|
||||
for i := 0; i < 10000; i++ {
|
||||
r.Read(buf)
|
||||
doMaskingTest(net.IP(buf), t)
|
||||
}
|
||||
|
||||
buf = make([]byte, 16)
|
||||
for i := 0; i < 10000; i++ {
|
||||
r.Read(buf)
|
||||
doMaskingTest(net.IP(buf), t)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasking(b *testing.B) {
|
||||
ip := easyParseIP("2001:0db8::42")
|
||||
flat := FromNetIP(ip)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
flat.Mask(64, 128)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMaskingLegacy(b *testing.B) {
|
||||
ip := easyParseIP("2001:0db8::42")
|
||||
mask := net.CIDRMask(64, 128)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
ip.Mask(mask)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMaskingCached(b *testing.B) {
|
||||
i := easyParseIP("2001:0db8::42")
|
||||
flat := FromNetIP(i)
|
||||
mask := cidrMask(64, 128)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
flat.applyMask(mask)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMaskingConstruct(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
cidrMask(69, 128)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkContains(b *testing.B) {
|
||||
ip := easyParseIP("2001:0db8::42")
|
||||
flat := FromNetIP(ip)
|
||||
_, ipnet, err := net.ParseCIDR("2001:0db8::/64")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
flatnet := FromNetIPNet(*ipnet)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
flatnet.Contains(flat)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkContainsLegacy(b *testing.B) {
|
||||
ip := easyParseIP("2001:0db8::42")
|
||||
_, ipnetptr, err := net.ParseCIDR("2001:0db8::/64")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ipnet := *ipnetptr
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
ipnet.Contains(ip)
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
//go:build !(plan9 || solaris)
|
||||
|
||||
package flock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gofrs/flock"
|
||||
)
|
||||
|
||||
var (
|
||||
CouldntAcquire = errors.New("Couldn't acquire flock (is another Ergo running?)")
|
||||
)
|
||||
|
||||
func TryAcquireFlock(path string) (fl Flocker, err error) {
|
||||
f := flock.New(path)
|
||||
success, err := f.TryLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !success {
|
||||
return nil, CouldntAcquire
|
||||
}
|
||||
return f, nil
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package flock
|
||||
|
||||
// documentation for github.com/gofrs/flock incorrectly claims that
|
||||
// Flock implements sync.Locker; it does not because the Unlock method
|
||||
// has a return type (err).
|
||||
type Flocker interface {
|
||||
Unlock() error
|
||||
}
|
||||
|
||||
type noopFlocker struct{}
|
||||
|
||||
func (n *noopFlocker) Unlock() error {
|
||||
return nil
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
//go:build plan9 || solaris
|
||||
|
||||
package flock
|
||||
|
||||
func TryAcquireFlock(path string) (fl Flocker, err error) {
|
||||
return &noopFlocker{}, nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user