mirror of
https://github.com/ergochat/ergo.git
synced 2026-04-08 16:38:06 +02:00
Compare commits
No commits in common. "master" and "v2.16.0-rc1" have entirely different histories.
master
...
v2.16.0-rc
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -15,17 +15,13 @@ jobs:
|
|||||||
runs-on: "ubuntu-24.04"
|
runs-on: "ubuntu-24.04"
|
||||||
steps:
|
steps:
|
||||||
- name: "checkout repository"
|
- name: "checkout repository"
|
||||||
uses: "actions/checkout@v6"
|
uses: "actions/checkout@v3"
|
||||||
- name: "setup go"
|
- name: "setup go"
|
||||||
uses: "actions/setup-go@v6"
|
uses: "actions/setup-go@v3"
|
||||||
with:
|
with:
|
||||||
go-version: "1.26"
|
go-version: "1.24"
|
||||||
- name: "install python3-pytest"
|
- name: "install python3-pytest"
|
||||||
run: "sudo apt install -y python3-pytest python3-websockets"
|
run: "sudo apt install -y python3-pytest"
|
||||||
- name: "make minimal"
|
|
||||||
run: "make minimal"
|
|
||||||
- name: "make build"
|
|
||||||
run: "make build"
|
|
||||||
- name: "make install"
|
- name: "make install"
|
||||||
run: "make install"
|
run: "make install"
|
||||||
- name: "make test"
|
- name: "make test"
|
||||||
|
|||||||
2
.github/workflows/docker-image.yml
vendored
2
.github/workflows/docker-image.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Git repository
|
- name: Checkout Git repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Authenticate to container registry
|
- name: Authenticate to container registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
|
|||||||
@ -52,7 +52,6 @@ builds:
|
|||||||
goarch: riscv64
|
goarch: riscv64
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
- -tags={{.Env.ERGO_BUILD_TAGS}}
|
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
-
|
-
|
||||||
@ -73,7 +72,6 @@ archives:
|
|||||||
- default.yaml
|
- default.yaml
|
||||||
- traditional.yaml
|
- traditional.yaml
|
||||||
- docs/API.md
|
- docs/API.md
|
||||||
- docs/BUILD.md
|
|
||||||
- docs/MANUAL.md
|
- docs/MANUAL.md
|
||||||
- docs/USERGUIDE.md
|
- docs/USERGUIDE.md
|
||||||
- languages/*.yaml
|
- languages/*.yaml
|
||||||
|
|||||||
80
CHANGELOG.md
80
CHANGELOG.md
@ -1,90 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to Ergo will be documented in this file.
|
All notable changes to Ergo will be documented in this file.
|
||||||
|
|
||||||
## [2.18.0] - 2026-03-22
|
## [2.16.0-rc1] - 2025-05-11
|
||||||
|
We're pleased to be publishing the release candidate for v2.16.0 (the official release should follow within a week or so). This release contains bug fixes and some minor updates.
|
||||||
We're pleased to be publishing v2.18.0, a new stable release. This release adds support for PostgreSQL and SQLite as history backends, expands the HTTP API, and includes bug fixes and minor improvements.
|
|
||||||
|
|
||||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format.
|
|
||||||
|
|
||||||
Due to the additional database drivers included in the default build, the size of the Ergo executable binary has increased since v2.17.0 (for example, the Linux binary for x86-64 has increased from 16.2 MiB to 26.1 MiB). See [docs/BUILD.md](https://github.com/ergochat/ergo/blob/stable/docs/BUILD.md) if you need to build a smaller binary. Conversely, if you were already building from source, you may need to adjust your build commands in order to maintain parity; consult that file for details.
|
|
||||||
|
|
||||||
Many thanks to [@clbm87](https://github.com/clbm87), [@emersion](https://github.com/emersion), [@felix](https://github.com/felix), flurry, [@furudean](https://github.com/furudean), [@k4ct0](https://github.com/k4ct0), [@mauropcorrea](https://github.com/mauropcorrea), [@NyaaaWhatsUpDoc](https://github.com/NyaaaWhatsUpDoc), [@poVoq](https://github.com/poVoq), [@progval](https://github.com/progval), [@rys](https://github.com/rys), Stryker, and th0th for helpful discussions, contributing patches, reporting issues, and helping test.
|
|
||||||
|
|
||||||
### Config changes
|
|
||||||
* Added `server.postgresql` block to configure a PostgreSQL history backend. (#2322, #2347)
|
|
||||||
* Added `server.sqlite` block to configure a SQLite history backend. (#2352)
|
|
||||||
* Added `metadata.operator-only-modification` to restrict metadata changes to IRC operators with the `metadata` capability (#2287, #2369, thanks [@clbm87](https://github.com/clbm87)!)
|
|
||||||
* Added `server.initial-notice` to send a configurable notice to clients immediately after they connect, which can be used for open proxy detection (e.g., with HOPM) (#2317, thanks Stryker!)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
* Added support for PostgreSQL and SQLite as history backends (#2322, #2352, thanks [@felix](https://github.com/felix), [@NyaaaWhatsUpDoc](https://github.com/NyaaaWhatsUpDoc), [@poVoq](https://github.com/poVoq)!)
|
|
||||||
* Added new HTTP API endpoints (thanks [@clbm87](https://github.com/clbm87), flurry, [@furudean](https://github.com/furudean), [@mauropcorrea](https://github.com/mauropcorrea)!):
|
|
||||||
* `/v1/list` to list channels (#2358)
|
|
||||||
* `/v1/defcon` to view or modify the DEFCON level (#2359)
|
|
||||||
* `/v1/ns/passwd` to change account passwords (#2329)
|
|
||||||
* Added support for `draft/ACCOUNTREQUIRED` in `005` ISUPPORT tokens when `accounts.require-sasl` is enabled (#2341)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
* Push notifications now include the `msgid` tag, as required by the specification (#2350)
|
|
||||||
* Fixed some cases where environment variable config overrides did not apply as expected (e.g., individual oper entries can now be overridden without replacing the entire `opers` block) (#2275, #2362, thanks th0th!)
|
|
||||||
* Fixed error cases in `CS DEOP` and `EXTJWT` causing client disconnection (#2345, #2346, thanks [@k4ct0](https://github.com/k4ct0)!)
|
|
||||||
* Fixed some `REDACT` responses (#2319, #2320)
|
|
||||||
* Fixed some `FAIL` responses to `WEBPUSH` (#2351)
|
|
||||||
* The `+l` (user limit) channel mode now rejects non-positive values with an appropriate error (#2325, thanks [@progval](https://github.com/progval)!)
|
|
||||||
* Clients monitoring a user now receive `METADATA` notifications for that user even without the `extended-monitor` capability (#2309, #2310)
|
|
||||||
* Improved handling of PROXY protocol errors on `proxy: true` listeners (#2334)
|
|
||||||
* The `accounts.bcrypt-cost` config value is now validated at config load time (#2311, #2312, thanks [@rys](https://github.com/rys)!)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
* HTTP API: reorganized NickServ-related endpoints under a `/v1/ns/` prefix (`/v1/ns/info`, `/v1/ns/list`, `/v1/ns/passwd`). The previous endpoint names (`/v1/account_details`, `/v1/account_list`) are retained as aliases for backwards compatibility. (#2329)
|
|
||||||
* The `/v1/check_auth` API endpoint now supports certificate fingerprint authentication (#2354, thanks flurry!)
|
|
||||||
* Reduced the deadline for `proxy: true` listeners to read the PROXY protocol header from 1 minute to 5 seconds (#2334)
|
|
||||||
|
|
||||||
### Internal
|
|
||||||
* Added build tags to control which optional features are built; see [docs/BUILD.md](https://github.com/ergochat/ergo/blob/stable/docs/BUILD.md) for details (#2356)
|
|
||||||
* Official release builds use Go 1.26.1 (#2330)
|
|
||||||
|
|
||||||
## [2.17.0] - 2025-12-22
|
|
||||||
|
|
||||||
We're pleased to be publishing v2.17.0, a new stable release. This release adds support for the [IRCv3 metadata specification](https://ircv3.net/specs/extensions/metadata), thanks to [@thatcher-gaming](https://github.com/thatcher-gaming), as well as bug fixes and minor updates.
|
|
||||||
|
|
||||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format.
|
|
||||||
|
|
||||||
Many thanks to [@branchgrove](https://github.com/branchgrove), [@Brutus5000](https://github.com/Brutus5000), [@progval](https://github.com/progval), [@SarahRoseLives](https://github.com/SarahRoseLives), [@thatcher-gaming](https://github.com/thatcher-gaming), [@ValwareIRC](https://github.com/ValwareIRC), and Xogium for contributing patches, reporting issues, and helping test.
|
|
||||||
|
|
||||||
### Config changes
|
|
||||||
* Added `accounts.metadata` block to configure the new metadata feature. If this block is absent, metadata is disabled. See `default.yaml` for an example. (#2273)
|
|
||||||
* Added `server.idle-timeouts` for configurable idle timeouts; when unset, the previous hardcoded defaults are used (#2292, thanks [@Brutus5000](https://github.com/Brutus5000)!)
|
|
||||||
* Added `server.oper-throttle` to configure throttling for failed `OPER` attempts; when unset, this defaults to 1 attempt every 10 seconds (#2296)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
* Implemented support for the [draft/metadata-2](https://ircv3.net/specs/extensions/metadata) specification, allowing clients to set and retrieve metadata on accounts and channels (#2273, #2277, #2281, #2282, #2301, thanks [@thatcher-gaming](https://github.com/thatcher-gaming)!)
|
|
||||||
* Added `/v1/status` and `/v1/account_list` HTTP API endpoints (#2261, thanks [@SarahRoseLives](https://github.com/SarahRoseLives)!)
|
|
||||||
* Enhanced `/v1/account_details` API response with additional fields (#2261, thanks [@SarahRoseLives](https://github.com/SarahRoseLives)!)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
* Fixed `REGISTER` command to strip guest format when applicable, matching `NS REGISTER` behavior (#2270, #2271, thanks [@ValwareIRC](https://github.com/ValwareIRC) and [@thatcher-gaming](https://github.com/thatcher-gaming)!)
|
|
||||||
* Fixed invalid `FAIL` codes in `REGISTER` command (#2269, thanks [@ValwareIRC](https://github.com/ValwareIRC)!)
|
|
||||||
* Fixed validation of web push URLs to reject non-HTTPS URLs (#2295)
|
|
||||||
* Fixed inconsistent behavior when `history.enabled` is set but `history.chathistory-maxmessages` is not (#2303, #2304, thanks [@branchgrove](https://github.com/branchgrove)!)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
* The `OPER` command now imposes a throttle on all attempts, never disconnects the client on failure, and logs non-sensitive information about failed attempts (#2296, #2298, thanks Xogium!)
|
|
||||||
|
|
||||||
### Internal
|
|
||||||
* Official release builds use Go 1.25 (#2290)
|
|
||||||
* Upgraded the Docker base image from Alpine 3.19 to 3.22 (#2306)
|
|
||||||
|
|
||||||
## [2.16.0] - 2025-05-18
|
|
||||||
We're pleased to be publishing v2.16.0, a new stable release. This release contains bug fixes and some minor updates.
|
|
||||||
|
|
||||||
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format.
|
This release includes changes to the config file format, all of which are fully backwards-compatible and do not require updating the file before upgrading. It includes no changes to the database file format.
|
||||||
|
|
||||||
Many thanks to [@csmith](https://github.com/csmith), [@delthas](https://github.com/delthas), donio, [@emersion](https://github.com/emersion), [@KlaasT](https://github.com/KlaasT), [@knolley](https://github.com/knolley), [@Mailaender](https://github.com/Mailaender), and [@prdes](https://github.com/prdes) for reporting issues and helping test.
|
Many thanks to [@csmith](https://github.com/csmith), [@delthas](https://github.com/delthas), donio, [@emersion](https://github.com/emersion), [@KlaasT](https://github.com/KlaasT), [@knolley](https://github.com/knolley), [@Mailaender](https://github.com/Mailaender), and [@prdes](https://github.com/prdes) for reporting issues and helping test.
|
||||||
|
|
||||||
### Config changes
|
### Config changes
|
||||||
* Added `api` block for configuring the new HTTP API. If this block is absent, the API is disabled (#2231)
|
|
||||||
* Added `server.additional-isupport` for publishing arbitrary ISUPPORT tokens (#2220, #2240)
|
* Added `server.additional-isupport` for publishing arbitrary ISUPPORT tokens (#2220, #2240)
|
||||||
* Added `server.command-aliases` to configure aliases for server commands (#2229, #2236)
|
* Added `server.command-aliases` to configure aliases for server commands (#2229, #2236)
|
||||||
* Added options to `roleplay` to customize the NUH's sent for `NPC` and `SCENE`. Roleplay remains deprecated and disabled by default. (#2237)
|
* Added options to `roleplay` to customize the NUH's sent for `NPC` and `SCENE`. Roleplay remains deprecated and disabled by default. (#2237)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
## build ergo binary
|
## build ergo binary
|
||||||
FROM docker.io/golang:1.26-alpine3.22 AS build-env
|
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
|
RUN apk upgrade -U --force-refresh --no-cache && apk add --no-cache --purge --clean-protected -l -u make git
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ RUN sed -i 's/^\(\s*\)\"127.0.0.1:6667\":.*$/\1":6667":/' /go/src/github.com/erg
|
|||||||
RUN make install
|
RUN make install
|
||||||
|
|
||||||
## build ergo container
|
## build ergo container
|
||||||
FROM docker.io/alpine:3.22
|
FROM docker.io/alpine:3.19
|
||||||
|
|
||||||
# metadata
|
# metadata
|
||||||
LABEL maintainer="Daniel Oaks <daniel@danieloaks.net>,Daniel Thamdrup <dallemon@protonmail.com>" \
|
LABEL maintainer="Daniel Oaks <daniel@danieloaks.net>,Daniel Thamdrup <dallemon@protonmail.com>" \
|
||||||
|
|||||||
37
Makefile
37
Makefile
@ -1,3 +1,5 @@
|
|||||||
|
.PHONY: all install build release capdefs test smoke gofmt irctest
|
||||||
|
|
||||||
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null)
|
GIT_COMMIT := $(shell git rev-parse HEAD 2> /dev/null)
|
||||||
GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1)
|
GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1)
|
||||||
|
|
||||||
@ -5,56 +7,35 @@ GIT_TAG := $(shell git tag --points-at HEAD 2> /dev/null | head -n 1)
|
|||||||
# this can be overridden by passing CGO_ENABLED=1 to make
|
# this can be overridden by passing CGO_ENABLED=1 to make
|
||||||
export CGO_ENABLED ?= 0
|
export CGO_ENABLED ?= 0
|
||||||
|
|
||||||
# build tags for the maximalist build with everything included
|
|
||||||
full_tags = i18n mysql postgresql sqlite
|
|
||||||
|
|
||||||
# build everything by default; override by passing, e.g. ERGO_BUILD_TAGS="mysql postgresql"
|
|
||||||
ERGO_BUILD_TAGS ?= $(full_tags)
|
|
||||||
|
|
||||||
capdef_file = ./irc/caps/defs.go
|
capdef_file = ./irc/caps/defs.go
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build:
|
|
||||||
go build -v -tags "$(ERGO_BUILD_TAGS)" -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
|
||||||
|
|
||||||
.PHONY: install
|
|
||||||
install:
|
install:
|
||||||
go install -v -tags "$(ERGO_BUILD_TAGS)" -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
go install -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -v -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
||||||
|
|
||||||
.PHONY: release
|
|
||||||
release:
|
release:
|
||||||
ERGO_BUILD_TAGS="$(ERGO_BUILD_TAGS)" goreleaser --skip=publish --clean
|
goreleaser --skip=publish --clean
|
||||||
|
|
||||||
.PHONY: minimal
|
|
||||||
minimal:
|
|
||||||
go build -v -tags "" -ldflags "-X main.commit=$(GIT_COMMIT) -X main.version=$(GIT_TAG)"
|
|
||||||
|
|
||||||
.PHONY: capdefs
|
|
||||||
capdefs:
|
capdefs:
|
||||||
python3 ./gencapdefs.py > ${capdef_file}
|
python3 ./gencapdefs.py > ${capdef_file}
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test:
|
test:
|
||||||
python3 ./gencapdefs.py | diff - ${capdef_file}
|
python3 ./gencapdefs.py | diff - ${capdef_file}
|
||||||
go test -tags "$(full_tags)" ./...
|
go test ./...
|
||||||
go vet -tags "$(full_tags)" ./...
|
go vet ./...
|
||||||
go test -tags "" ./...
|
|
||||||
go vet -tags "" ./...
|
|
||||||
./.check-gofmt.sh
|
./.check-gofmt.sh
|
||||||
|
|
||||||
.PHONY: smoke
|
|
||||||
smoke: install
|
smoke: install
|
||||||
ergo mkcerts --conf ./default.yaml || true
|
ergo mkcerts --conf ./default.yaml || true
|
||||||
ergo run --conf ./default.yaml --smoke
|
ergo run --conf ./default.yaml --smoke
|
||||||
|
|
||||||
.PHONY: gofmt
|
|
||||||
gofmt:
|
gofmt:
|
||||||
./.check-gofmt.sh --fix
|
./.check-gofmt.sh --fix
|
||||||
|
|
||||||
.PHONY: irctest
|
|
||||||
irctest: install
|
irctest: install
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
cd irctest && make ergo
|
cd irctest && make ergo
|
||||||
|
|||||||
14
README.md
14
README.md
@ -76,18 +76,16 @@ to the GitHub Container Registry at [ghcr.io/ergochat/ergo](https://ghcr.io/ergo
|
|||||||
|
|
||||||
### From Source
|
### From Source
|
||||||
|
|
||||||
You can also clone this repository and build from source. A quick start guide:
|
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.
|
||||||
|
|
||||||
1. Obtain an [up-to-date distribution of the Go language for your OS and architecture](https://golang.org/dl/). Check the output of `go version` to ensure it was installed correctly.
|
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.
|
||||||
1. Clone the repository.
|
|
||||||
1. `git checkout stable`
|
|
||||||
1. `make`
|
|
||||||
1. You should now have a binary named `ergo` in the working directory.
|
|
||||||
|
|
||||||
Ergo vendors all its dependencies, so you will not need to fetch any dependencies remotely. For more information, including on build customization, see [docs/BUILD.md](https://github.com/ergochat/ergo/blob/stable/docs/BUILD.md).
|
|
||||||
|
|
||||||
For information on contributing to Ergo, see [DEVELOPING.md](https://github.com/ergochat/ergo/blob/master/DEVELOPING.md).
|
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.)
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The default config file [`default.yaml`](default.yaml) helps walk you through what each option means and changes.
|
The default config file [`default.yaml`](default.yaml) helps walk you through what each option means and changes.
|
||||||
|
|||||||
81
default.yaml
81
default.yaml
@ -180,21 +180,6 @@ server:
|
|||||||
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
|
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
|
||||||
motd-formatting: true
|
motd-formatting: true
|
||||||
|
|
||||||
# send a configurable notice to clients immediately after they connect,
|
|
||||||
# as a way of detecting open proxies
|
|
||||||
#initial-notice: "*** Welcome to the Ergo IRC server"
|
|
||||||
|
|
||||||
# idle timeouts for inactive clients
|
|
||||||
idle-timeouts:
|
|
||||||
# give the client this long to complete connection registration (i.e. the initial
|
|
||||||
# IRC handshake, including capability negotiation and SASL)
|
|
||||||
registration: 60s
|
|
||||||
# if the client hasn't sent anything for this long, send them a PING
|
|
||||||
ping: 1m30s
|
|
||||||
# if the client hasn't sent anything for this long (including the PONG to the
|
|
||||||
# above PING), disconnect them
|
|
||||||
disconnect: 2m30s
|
|
||||||
|
|
||||||
# relaying using the RELAYMSG command
|
# relaying using the RELAYMSG command
|
||||||
relaymsg:
|
relaymsg:
|
||||||
# is relaymsg enabled at all?
|
# is relaymsg enabled at all?
|
||||||
@ -373,10 +358,6 @@ server:
|
|||||||
secure-nets:
|
secure-nets:
|
||||||
# - "10.0.0.0/8"
|
# - "10.0.0.0/8"
|
||||||
|
|
||||||
# allow attempts to OPER with a password at most this often. defaults to
|
|
||||||
# 10 seconds when unset.
|
|
||||||
oper-throttle: 10s
|
|
||||||
|
|
||||||
# Ergo will write files to disk under certain circumstances, e.g.,
|
# Ergo will write files to disk under certain circumstances, e.g.,
|
||||||
# CPU profiling or data export. by default, these files will be written
|
# CPU profiling or data export. by default, these files will be written
|
||||||
# to the working directory. set this to customize:
|
# to the working directory. set this to customize:
|
||||||
@ -541,7 +522,7 @@ accounts:
|
|||||||
# 1. these nicknames cannot be registered or reserved
|
# 1. these nicknames cannot be registered or reserved
|
||||||
# 2. if a client is automatically renamed by the server,
|
# 2. if a client is automatically renamed by the server,
|
||||||
# this is the template that will be used (e.g., Guest-nccj6rgmt97cg)
|
# this is the template that will be used (e.g., Guest-nccj6rgmt97cg)
|
||||||
# 3. if force-guest-format (see below) is enabled, clients without
|
# 3. if enforce-guest-format (see below) is enabled, clients without
|
||||||
# a registered account will have this template applied to their
|
# a registered account will have this template applied to their
|
||||||
# nicknames (e.g., 'katie' will become 'Guest-katie')
|
# nicknames (e.g., 'katie' will become 'Guest-katie')
|
||||||
guest-nickname-format: "Guest-*"
|
guest-nickname-format: "Guest-*"
|
||||||
@ -743,7 +724,6 @@ oper-classes:
|
|||||||
- "history" # modify or delete history messages
|
- "history" # modify or delete history messages
|
||||||
- "defcon" # use the DEFCON command (restrict server capabilities)
|
- "defcon" # use the DEFCON command (restrict server capabilities)
|
||||||
- "massmessage" # message all users on the server
|
- "massmessage" # message all users on the server
|
||||||
- "metadata" # modify arbitrary metadata on channels and users
|
|
||||||
|
|
||||||
# ircd operators
|
# ircd operators
|
||||||
opers:
|
opers:
|
||||||
@ -875,43 +855,6 @@ datastore:
|
|||||||
# this may be necessary to prevent middleware from closing your connections:
|
# this may be necessary to prevent middleware from closing your connections:
|
||||||
#conn-max-lifetime: 180s
|
#conn-max-lifetime: 180s
|
||||||
|
|
||||||
# connection information for PostgreSQL (currently only used for persistent history)
|
|
||||||
postgresql:
|
|
||||||
enabled: false
|
|
||||||
host: "localhost"
|
|
||||||
port: 5432
|
|
||||||
# if socket-path is set, it will be used instead of host:port
|
|
||||||
# PostgreSQL uses the socket directory, not the socket file path
|
|
||||||
#socket-path: "/var/run/postgresql"
|
|
||||||
# PostgreSQL SSL/TLS configuration:
|
|
||||||
ssl-mode: "disable" # options: disable, require, verify-ca, verify-full
|
|
||||||
#ssl-cert: "/path/to/client-cert.pem"
|
|
||||||
#ssl-key: "/path/to/client-key.pem"
|
|
||||||
#ssl-root-cert: "/path/to/ca-cert.pem"
|
|
||||||
user: "ergo"
|
|
||||||
password: "hunter2"
|
|
||||||
history-database: "ergo_history"
|
|
||||||
# uri takes a postgresql:// (libpq) URI, overriding the above parameters if present:
|
|
||||||
# uri: "postgresql://ergo:hunter2@localhost/ergo_history"
|
|
||||||
timeout: 3s
|
|
||||||
max-conns: 4
|
|
||||||
# this may be necessary to prevent middleware from closing your connections:
|
|
||||||
#conn-max-lifetime: 180s
|
|
||||||
# application name shown in pg_stat_activity for operational visibility:
|
|
||||||
#application-name: "ergo"
|
|
||||||
# timeout for establishing initial connections to PostgreSQL:
|
|
||||||
#connect-timeout: 10s
|
|
||||||
|
|
||||||
# connection information for SQLite (currently only used for persistent history)
|
|
||||||
sqlite:
|
|
||||||
enabled: false
|
|
||||||
# path to the SQLite database file
|
|
||||||
database-path: "ergo_history.db"
|
|
||||||
# timeout when waiting for write lock
|
|
||||||
busy-timeout: 5s
|
|
||||||
# maximum concurrent connections
|
|
||||||
max-conns: 1
|
|
||||||
|
|
||||||
# languages config
|
# languages config
|
||||||
languages:
|
languages:
|
||||||
# whether to load languages
|
# whether to load languages
|
||||||
@ -1044,12 +987,10 @@ history:
|
|||||||
# in your country and the countries of your users.
|
# in your country and the countries of your users.
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# if the in-memory backend is enabled for a channel, how many channel-specific events
|
# how many channel-specific events (messages, joins, parts) should be tracked per channel?
|
||||||
# (messages, joins, parts) should be retained?
|
|
||||||
channel-length: 2048
|
channel-length: 2048
|
||||||
|
|
||||||
# if the in-memory backend is enabled for a user, how many direct messages
|
# how many direct messages and notices should be tracked per user?
|
||||||
# and notices should be retained?
|
|
||||||
client-length: 256
|
client-length: 256
|
||||||
|
|
||||||
# how long should we try to preserve messages?
|
# how long should we try to preserve messages?
|
||||||
@ -1146,22 +1087,6 @@ history:
|
|||||||
# e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
# e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
||||||
allow-environment-overrides: true
|
allow-environment-overrides: true
|
||||||
|
|
||||||
# metadata support for setting key/value data on channels and nicknames.
|
|
||||||
metadata:
|
|
||||||
# can clients store metadata?
|
|
||||||
enabled: true
|
|
||||||
# if this is true, only server operators with the `metadata` capability can edit metadata:
|
|
||||||
operator-only-modification: false
|
|
||||||
# how many keys can a client subscribe to?
|
|
||||||
max-subs: 100
|
|
||||||
# how many keys can be stored per entity?
|
|
||||||
max-keys: 100
|
|
||||||
# rate limiting for client metadata updates, which are expensive to process
|
|
||||||
client-throttle:
|
|
||||||
enabled: true
|
|
||||||
duration: 2m
|
|
||||||
max-attempts: 10
|
|
||||||
|
|
||||||
# experimental support for mobile push notifications
|
# experimental support for mobile push notifications
|
||||||
# see the manual for potential security, privacy, and performance implications.
|
# see the manual for potential security, privacy, and performance implications.
|
||||||
# DO NOT enable if you are running a Tor or I2P hidden service (i.e. one
|
# DO NOT enable if you are running a Tor or I2P hidden service (i.e. one
|
||||||
|
|||||||
127
docs/API.md
127
docs/API.md
@ -39,61 +39,9 @@ This returns:
|
|||||||
Endpoints
|
Endpoints
|
||||||
=========
|
=========
|
||||||
|
|
||||||
`/v1/check_auth`
|
`/v1/account_details`
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
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
|
|
||||||
* `certfp`: string, alleged certificate fingerprint (hex-encoded SHA-256 checksum of the decoded raw certificate) associated with the account
|
|
||||||
|
|
||||||
Each individual field is optional, since a user may be authenticated either by account-passphrase pair or by certificate.
|
|
||||||
|
|
||||||
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/defcon`
|
|
||||||
------------
|
|
||||||
|
|
||||||
This endpoint can be used to view or modify the DEFCON level (see `/helpop defcon` for details). If the request is empty, the existing level is returned. To change the level, send a JSON object with fields:
|
|
||||||
|
|
||||||
* `defcon`: integer, desired new value of the DEFCON setting (between 5 for normal operation and 1 for the most restrictive)
|
|
||||||
|
|
||||||
The response is a JSON object with fields:
|
|
||||||
|
|
||||||
* `defcon`: integer, current (or new) value of the DEFCON setting
|
|
||||||
|
|
||||||
`/v1/list`
|
|
||||||
----------
|
|
||||||
|
|
||||||
This endpoint returns a list of channels that exist on the network. The request body is ignored and can be empty.
|
|
||||||
|
|
||||||
The response is a JSON object with fields:
|
|
||||||
|
|
||||||
* `success`: whether the request was successful
|
|
||||||
* `channels`: a list of channel objects, as described below
|
|
||||||
|
|
||||||
Each channel object has fields:
|
|
||||||
|
|
||||||
* `name`: canonical name of the channel without case-normalization
|
|
||||||
* `hasKey`: boolean, whether the channel has a key set with the `+k` mode
|
|
||||||
* `inviteOnly`: boolean, whether the channel has the `+i` invite-only mode set
|
|
||||||
* `secret`: boolean, whether the channel has the `+s` secret mode set (and would be hidden from an unprivileged `LIST` command)
|
|
||||||
* `userCount`: integer, number of users in the channel
|
|
||||||
* `topic`: string, channel topic
|
|
||||||
* `topicSetAt`: string, time the topic was last updated (in ISO8601 format)
|
|
||||||
* `createdAt`: string, time the channel was created (in ISO8601 format)
|
|
||||||
* `registered`: boolean, whether the channel is registered
|
|
||||||
* `owner`: string, account name of the registered owner if the channel is registered
|
|
||||||
* `registeredAt`: string, registration date/time of the channel (in ISO8601 format) if it is registered
|
|
||||||
|
|
||||||
|
|
||||||
`/v1/ns/info`
|
|
||||||
-------------
|
|
||||||
|
|
||||||
This endpoint fetches account details and returns them as JSON. The request is a JSON object with fields:
|
This endpoint fetches account details and returns them as JSON. The request is a JSON object with fields:
|
||||||
|
|
||||||
* `accountName`: string, name of the account
|
* `accountName`: string, name of the account
|
||||||
@ -103,41 +51,30 @@ The response is a JSON object with fields:
|
|||||||
* `success`: whether the account exists or not
|
* `success`: whether the account exists or not
|
||||||
* `accountName`: canonical, case-unfolded version of the account name
|
* `accountName`: canonical, case-unfolded version of the account name
|
||||||
* `email`: email address of the account provided
|
* `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
|
|
||||||
|
|
||||||
Note: this endpoint was previously named `/v1/account_details`. The old name is still accepted for backwards compatibility.
|
`/v1/check_auth`
|
||||||
|
----------------
|
||||||
|
|
||||||
`/v1/ns/list`
|
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:
|
||||||
-------------
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Note: this endpoint was previously named `/v1/account_list`. The old name is still accepted for backwards compatibility.
|
|
||||||
|
|
||||||
`/v1/ns/passwd`
|
|
||||||
---------------
|
|
||||||
|
|
||||||
This endpoint changes the password of an existing NickServ account. The request is a JSON object with fields:
|
|
||||||
|
|
||||||
* `accountName`: string, name of the account
|
* `accountName`: string, name of the account
|
||||||
* `passphrase`: string, new passphrase for the account
|
* `passphrase`: string, alleged passphrase of the account
|
||||||
|
|
||||||
The response is a JSON object with fields:
|
The response is a JSON object with fields:
|
||||||
|
|
||||||
* `success`: whether the password change succeeded
|
* `success`: whether the credentials provided were valid
|
||||||
* `errorCode`: string, optional, machine-readable description of the error. Possible values include: `ACCOUNT_DOES_NOT_EXIST`, `INVALID_PASSPHRASE`, `CREDENTIALS_EXTERNALLY_MANAGED`, `UNKNOWN_ERROR`.
|
* `accountName`: canonical, case-unfolded version of the account name
|
||||||
|
|
||||||
`/v1/ns/saregister`
|
`/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:
|
This endpoint registers an account in NickServ, with the same semantics as `NS SAREGISTER`. The request is a JSON object with fields:
|
||||||
|
|
||||||
@ -149,33 +86,3 @@ The response is a JSON object with fields:
|
|||||||
* `success`: whether the account creation succeeded
|
* `success`: whether the account creation succeeded
|
||||||
* `errorCode`: string, optional, machine-readable description of the error. Possible values include: `ACCOUNT_EXISTS`, `INVALID_PASSPHRASE`, `UNKNOWN_ERROR`.
|
* `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.
|
* `error`: string, optional, human-readable description of the failure.
|
||||||
|
|
||||||
Note: this endpoint was previously named `/v1/saregister`. The old name is still accepted for backwards compatibility.
|
|
||||||
|
|
||||||
`/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/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
|
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
__ __ ______ ___ ______ ___
|
|
||||||
__/ // /_/ ____/ __ \/ ____/ __ \
|
|
||||||
/_ // __/ __/ / /_/ / / __/ / / /
|
|
||||||
/_ // __/ /___/ _, _/ /_/ / /_/ /
|
|
||||||
/_//_/ /_____/_/ |_|\____/\____/
|
|
||||||
|
|
||||||
Ergo Build Guide
|
|
||||||
https://ergo.chat/
|
|
||||||
|
|
||||||
_Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn@cs.stanford.edu>_
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
This guide is for building Ergo from source. You can also obtain a pre-built release binary from our [GitHub page](https://github.com/ergochat/ergo/releases).
|
|
||||||
|
|
||||||
# Prerequisites
|
|
||||||
|
|
||||||
You will need an [up-to-date distribution of the Go language for your OS and architecture](https://golang.org/dl/). Use the latest version available. (As of this writing, only Google's Go distribution is supported, since `gccgo` lacks support for current language features.) Check the output of `go version` to ensure it was installed correctly.
|
|
||||||
|
|
||||||
You will need to either clone the repository from GitHub at [https://github.com/ergochat/ergo], or obtain a source tarball from our releases page on GitHub.
|
|
||||||
|
|
||||||
# What to build
|
|
||||||
|
|
||||||
Typical deployments should build 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.
|
|
||||||
|
|
||||||
# Build tags and options
|
|
||||||
|
|
||||||
By default, Ergo is built with cgo disabled, producing a fully statically linked binary. You can disable this with `export CGO_ENABLED=1` before running `make`.
|
|
||||||
|
|
||||||
Ergo can be cross-compiled using [standard Go environment variables](https://go.dev/doc/install/source#environment), e.g. `GOOS=linux GOARCH=arm GOARM=v6 make build` will build an `ergo` binary suitable for a 32-bit Raspberry Pi.
|
|
||||||
|
|
||||||
The default Ergo binary (built with `make` or `make build`) includes support for all optional features. Each optional feature is controlled via a separate build tag; to override the build tags, pass the environment variable `ERGO_BUILD_TAGS` with a space-separated list of tags. (For example, for parity with v2.17.0 and earlier, you can run `ERGO_BUILD_TAGS="i18n mysql" make`. Passing the empty string disables all optional features.)
|
|
||||||
|
|
||||||
The supported build tags are:
|
|
||||||
|
|
||||||
* `i18n` enables support for non-ASCII casemappings (allowing Unicode in nicknames and channel names). (This was a default feature in Ergo v2.17.0 and earlier, but was not enabled by default at runtime. See the `server.casemapping` value of the config file.)
|
|
||||||
* `mysql` enables support for MySQL as a persistent history backend. (This was a default feature in v2.17.0 and earlier.)
|
|
||||||
* `postgresql` enables support for PostgreSQL as a persistent history backend.
|
|
||||||
* `sqlite` enables support for SQLite as a persistent history backend.
|
|
||||||
|
|
||||||
`sqlite` is particularly memory-intensive to compile (but not to run), so if you're building Ergo for a memory-constrained environment, you may want to consider cross-compilation.
|
|
||||||
@ -41,7 +41,7 @@ _Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn
|
|||||||
- [Language](#language)
|
- [Language](#language)
|
||||||
- [Multiclient ("Bouncer")](#multiclient-bouncer)
|
- [Multiclient ("Bouncer")](#multiclient-bouncer)
|
||||||
- [History](#history)
|
- [History](#history)
|
||||||
- [Persistent history](#persistent-history)
|
- [Persistent history with MySQL](#persistent-history-with-mysql)
|
||||||
- [IP cloaking](#ip-cloaking)
|
- [IP cloaking](#ip-cloaking)
|
||||||
- [Moderation](#moderation)
|
- [Moderation](#moderation)
|
||||||
- [Push notifications](#push-notifications)
|
- [Push notifications](#push-notifications)
|
||||||
@ -171,7 +171,6 @@ Rehashing also reloads TLS certificates and the MOTD. Some configuration setting
|
|||||||
|
|
||||||
Ergo can also be configured using environment variables, using the following technique:
|
Ergo can also be configured using environment variables, using the following technique:
|
||||||
|
|
||||||
1. Ensure that `allow-environment-variables` is set to `true` in the YAML config file itself (see `default.yaml` for an example)
|
|
||||||
1. Find the "path" of the config variable you want to override in the YAML file, e.g., `server.websockets.allowed-origins`
|
1. Find the "path" of the config variable you want to override in the YAML file, e.g., `server.websockets.allowed-origins`
|
||||||
1. Convert each path component from "kebab case" to "screaming snake case", e.g., `SERVER`, `WEBSOCKETS`, and `ALLOWED_ORIGINS`.
|
1. Convert each path component from "kebab case" to "screaming snake case", e.g., `SERVER`, `WEBSOCKETS`, and `ALLOWED_ORIGINS`.
|
||||||
1. Prepend `ERGO` to the components, then join them all together using `__` as the separator, e.g., `ERGO__SERVER__WEBSOCKETS__ALLOWED_ORIGINS`.
|
1. Prepend `ERGO` to the components, then join them all together using `__` as the separator, e.g., `ERGO__SERVER__WEBSOCKETS__ALLOWED_ORIGINS`.
|
||||||
@ -179,9 +178,6 @@ Ergo can also be configured using environment variables, using the following tec
|
|||||||
|
|
||||||
However, settings that were overridden using this technique cannot be rehashed --- changing them will require restarting the server.
|
However, settings that were overridden using this technique cannot be rehashed --- changing them will require restarting the server.
|
||||||
|
|
||||||
Due to implementation details, this technique has some limitations. Here are the known issues:
|
|
||||||
|
|
||||||
1. `accounts.auth-script` and `server.ip-check-script` do not work as expected (see [#2275](https://github.com/ergochat/ergo/issues/2275) for workarounds).
|
|
||||||
|
|
||||||
## Productionizing with systemd
|
## Productionizing with systemd
|
||||||
|
|
||||||
@ -429,32 +425,9 @@ Unfortunately, client support for history playback is still patchy. In descendin
|
|||||||
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`.
|
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`.
|
||||||
|
|
||||||
|
|
||||||
## Persistent history
|
## Persistent history with MySQL
|
||||||
|
|
||||||
Persistent history means storing chat history (messages, but also events like JOINs and PARTs) on disk. This increases the amount of history that can be stored, and also ensures that message data will be retained on server restart (you can still use the configuration options to set a time limit for retention). Ergo supports three backends for persistent history: MySQL, PostgreSQL, and SQLite. If you have a default build of Ergo (for example, a release build from our GitHub page, or our official Docker image), all three backends are available.
|
On most Linux and POSIX systems, it's straightforward to set up MySQL (or MariaDB) as a backend for persistent history. This increases the amount of history that can be stored, and ensures that message data will be retained on server restart (you can still use the configuration options to set a time limit for retention). Here's a quick start guide for Ubuntu based on [Digital Ocean's documentation](https://www.digitalocean.com/community/tutorials/how-to-install-mysql-on-ubuntu-20-04):
|
||||||
|
|
||||||
To configure persistent history, you must set `history.persistent.enabled` to `true` in the Ergo config file. You may want to adjust other options in the `history` section at this time. Then you must additionally enable and configure one of the backends. Here are per-backend instructions:
|
|
||||||
|
|
||||||
### SQLite
|
|
||||||
|
|
||||||
SQLite is the easiest backend to enable; it's an embedded database that runs inside the Ergo process, without needing to talk to an external database server. Find `datastore.sqlite` in your config (or add it, following an up-to-date `default.yaml` as a guide):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
sqlite:
|
|
||||||
enabled: true
|
|
||||||
# path to the SQLite database file
|
|
||||||
database-path: "ergo_history.db"
|
|
||||||
# timeout when waiting for write lock
|
|
||||||
busy-timeout: 5s
|
|
||||||
# maximum concurrent connections
|
|
||||||
max-conns: 1
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates an on-disk file `ergo_history.db` for the history storage, by default in the same working directory as the Ergo process. We believe SQLite should scale to the needs of most Ergo deployments (in our initial benchmarks, there is a write bottleneck of approximately 1K messages/events per second).
|
|
||||||
|
|
||||||
### MySQL
|
|
||||||
|
|
||||||
Here's a quick start guide for MySQL on Ubuntu based on [Digital Ocean's documentation](https://www.digitalocean.com/community/tutorials/how-to-install-mysql-on-ubuntu-20-04). (Ergo is also compatible with MariaDB; a compatible implementation is available on most Linux and POSIX platforms.)
|
|
||||||
|
|
||||||
1. Install the `mysql-server` package
|
1. Install the `mysql-server` package
|
||||||
1. Run `mysql_secure_installation` as root; this corrects some insecure package defaults
|
1. Run `mysql_secure_installation` as root; this corrects some insecure package defaults
|
||||||
@ -475,10 +448,6 @@ Here's a quick start guide for MySQL on Ubuntu based on [Digital Ocean's documen
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
```
|
```
|
||||||
|
|
||||||
### PostgreSQL
|
|
||||||
|
|
||||||
If you don't already have a PostgreSQL database, follow [Digital Ocean's quick start guide](https://www.digitalocean.com/community/tutorials/how-to-install-postgresql-on-ubuntu-22-04-quickstart) to set one up, then edit `datastore.postgresql` in the Ergo config file with `enabled: true` and your database parameters.
|
|
||||||
|
|
||||||
|
|
||||||
## IP cloaking
|
## IP cloaking
|
||||||
|
|
||||||
@ -557,23 +526,18 @@ If your client or bot is failing to connect to Ergo, here are some things to che
|
|||||||
|
|
||||||
## Why can't I oper?
|
## Why can't I oper?
|
||||||
|
|
||||||
If your `OPER` command fails, check your server logs for more information. Here are some general issues to double-check:
|
If you try to oper unsuccessfully, Ergo will disconnect you from the network. If you're unable to oper, here are some things to double-check:
|
||||||
|
|
||||||
1. Did you correctly generate the hashed password with `ergo genpasswd`?
|
1. Did you correctly generate the hashed password with `ergo genpasswd`?
|
||||||
1. Did you add the password hash to the correct config file, then save the file?
|
1. Did you add the password hash to the correct config file, then save the file?
|
||||||
1. Did you rehash or restart Ergo after saving the file?
|
1. Did you rehash or restart Ergo after saving the file?
|
||||||
1. Does your password contain spaces or non-ASCII characters? Although such passwords are theoretically compatible with Ergo, they are likely to cause problems with your client. (The period character `.` is an acceptable alternative separator if your password is based on randomly chosen words.)
|
|
||||||
|
|
||||||
The config file accepts hashed passwords, not plaintext passwords. You must run `ergo genpasswd`, type your actual password in, and then receive a hashed blob back (it will look like `$2a$04$GvCFlShLZQjId3dARzwOWu9Nvq6lndXINw2Sdm6mUcwxhtx1U/hIm`). Enter that into the relevant `opers` block in your config file, then save the file.
|
The config file accepts hashed passwords, not plaintext passwords. You must run `ergo genpasswd`, type your actual password in, and then receive a hashed blob back (it will look like `$2a$04$GvCFlShLZQjId3dARzwOWu9Nvq6lndXINw2Sdm6mUcwxhtx1U/hIm`). Enter that into the relevant `opers` block in your config file, then save the file.
|
||||||
|
|
||||||
|
Although it's theoretically possible to use an operator password that contains spaces, your client may not support it correctly, so it's advisable to choose a password without spaces. (The period character `.` is an acceptable alternative separator if your password is based on randomly chosen words.)
|
||||||
|
|
||||||
After that, you must rehash or restart Ergo to apply the config change. If a rehash didn't accomplish the desired effects, you might want to try a restart instead.
|
After that, you must rehash or restart Ergo to apply the config change. If a rehash didn't accomplish the desired effects, you might want to try a restart instead.
|
||||||
|
|
||||||
If you're still having problems, your client or bouncer may be mangling the OPER command. You can try connecting to Ergo directly via the `nc` ("netcat") command to test this:
|
|
||||||
|
|
||||||
1. From the machine where Ergo is running, run `nc -v 127.0.0.1 6667`. (If you are using Docker, you will first need to get a shell inside the Docker container, e.g. with `docker exec -it $CONTAINER_ID /bin/sh`.)
|
|
||||||
1. Type `NICK unique_nickname`, press enter, type `USER u s e r`, and press enter. You may need to retry with a different nickname if the first one is in use.
|
|
||||||
1. Once you see a burst of lines starting with an `001` command, indicating a successful connection, type: `OPER <opername> <password>` and press enter.
|
|
||||||
1. If you see a successful response including the `381` command, this indicates that your password was accepted by Ergo and the problem is with your client or bouncer setup. If you see an error response, then there is an issue with your password or configuration file.
|
|
||||||
|
|
||||||
## Why is Ergo ignoring my ident response / USER command?
|
## Why is Ergo ignoring my ident response / USER command?
|
||||||
|
|
||||||
@ -1107,12 +1071,9 @@ You can import user and channel registrations from an Anope or Atheme database i
|
|||||||
|
|
||||||
## Hybrid Open Proxy Monitor (HOPM)
|
## Hybrid Open Proxy Monitor (HOPM)
|
||||||
|
|
||||||
[hopm](https://github.com/ircd-hybrid/hopm) can be used to monitor your server for connections from open proxies, then automatically ban them. To configure hopm to work with Ergo, configure `server.initial_notice` and add operator blocks like this to your Ergo config file, which grant hopm the necessary privileges:
|
[hopm](https://github.com/ircd-hybrid/hopm) can be used to monitor your server for connections from open proxies, then automatically ban them. To configure hopm to work with Ergo, add operator blocks like this to your Ergo config file, which grant hopm the necessary privileges:
|
||||||
|
|
||||||
````yaml
|
````yaml
|
||||||
server:
|
|
||||||
initial-notice: "Welcome to the Ergo IRC server"
|
|
||||||
|
|
||||||
# operator classes
|
# operator classes
|
||||||
oper-classes:
|
oper-classes:
|
||||||
# hopm
|
# hopm
|
||||||
@ -1147,9 +1108,6 @@ opers:
|
|||||||
Then configure hopm like this:
|
Then configure hopm like this:
|
||||||
|
|
||||||
````
|
````
|
||||||
/* replace with the exact notice your server sends on first connection */
|
|
||||||
target_string = ":ergo.test NOTICE * :*** Welcome to the Ergo IRC server"
|
|
||||||
|
|
||||||
/* ergo */
|
/* ergo */
|
||||||
connregex = ".+-.+CONNECT.+-.+ Client Connected \\[([^ ]+)\\] \\[u:([^ ]+)\\] \\[h:([^ ]+)\\] \\[ip:([^ ]+)\\] .+";
|
connregex = ".+-.+CONNECT.+-.+ Client Connected \\[([^ ]+)\\] \\[u:([^ ]+)\\] \\[h:([^ ]+)\\] \\[ip:([^ ]+)\\] .+";
|
||||||
|
|
||||||
|
|||||||
@ -86,7 +86,7 @@ Once you've registered your nickname, you can use it to register channels. By de
|
|||||||
/msg ChanServ register #myChannel
|
/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.
|
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
|
# Always-on
|
||||||
|
|
||||||
|
|||||||
@ -237,13 +237,6 @@ CAPDEFS = [
|
|||||||
url="https://github.com/ircv3/ircv3-specifications/pull/471",
|
url="https://github.com/ircv3/ircv3-specifications/pull/471",
|
||||||
standard="Soju/Goguma vendor",
|
standard="Soju/Goguma vendor",
|
||||||
),
|
),
|
||||||
CapDef(
|
|
||||||
identifier="Metadata",
|
|
||||||
name="draft/metadata-2",
|
|
||||||
url="https://ircv3.net/specs/extensions/metadata",
|
|
||||||
standard="draft IRCv3",
|
|
||||||
),
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_defs():
|
def validate_defs():
|
||||||
|
|||||||
37
go.mod
37
go.mod
@ -1,6 +1,6 @@
|
|||||||
module github.com/ergochat/ergo
|
module github.com/ergochat/ergo
|
||||||
|
|
||||||
go 1.26
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
|
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
|
||||||
@ -8,41 +8,29 @@ require (
|
|||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
|
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
|
||||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
|
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
|
||||||
github.com/ergochat/irc-go v0.5.0
|
github.com/ergochat/irc-go v0.5.0-rc2
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.7.0
|
||||||
github.com/gofrs/flock v0.8.1
|
github.com/gofrs/flock v0.8.1
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
|
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
|
||||||
github.com/onsi/ginkgo v1.12.0 // indirect
|
github.com/onsi/ginkgo v1.12.0 // indirect
|
||||||
github.com/onsi/gomega v1.9.0 // indirect
|
github.com/onsi/gomega v1.9.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
github.com/tidwall/buntdb v1.3.2
|
github.com/tidwall/buntdb v1.3.2
|
||||||
github.com/xdg-go/scram v1.0.2
|
github.com/xdg-go/scram v1.0.2
|
||||||
golang.org/x/crypto v0.46.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/term v0.38.0
|
golang.org/x/term v0.28.0
|
||||||
golang.org/x/text v0.32.0
|
golang.org/x/text v0.21.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/emersion/go-msgauth v0.7.0
|
github.com/emersion/go-msgauth v0.6.8
|
||||||
github.com/ergochat/webpush-go/v2 v2.0.0
|
github.com/ergochat/webpush-go/v2 v2.0.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/jackc/pgx/v5 v5.8.0
|
|
||||||
modernc.org/sqlite v1.42.2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.1 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
||||||
github.com/rogpeppe/go-internal v1.6.1 // indirect
|
|
||||||
github.com/tidwall/btree v1.4.2 // indirect
|
github.com/tidwall/btree v1.4.2 // indirect
|
||||||
github.com/tidwall/gjson v1.14.3 // indirect
|
github.com/tidwall/gjson v1.14.3 // indirect
|
||||||
github.com/tidwall/grect v0.1.4 // indirect
|
github.com/tidwall/grect v0.1.4 // indirect
|
||||||
@ -51,12 +39,7 @@ require (
|
|||||||
github.com/tidwall/rtred v0.1.2 // indirect
|
github.com/tidwall/rtred v0.1.2 // indirect
|
||||||
github.com/tidwall/tinyqueue v0.1.1 // indirect
|
github.com/tidwall/tinyqueue v0.1.1 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
|
||||||
modernc.org/libc v1.66.10 // indirect
|
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
|
||||||
modernc.org/memory v1.11.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1
|
replace github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1
|
||||||
|
|||||||
121
go.sum
121
go.sum
@ -1,25 +1,19 @@
|
|||||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9xClkGV/t0rmHrdhX4+trQUcBqjwc9xE=
|
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9xClkGV/t0rmHrdhX4+trQUcBqjwc9xE=
|
||||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
|
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
|
||||||
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
|
||||||
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
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/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/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 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons=
|
||||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1/go.mod h1:mov+uh1DPWsltdQnOdzn08UO9GsJ3MEvhtu0Ci37fdk=
|
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 h1:+J5m88nvybxB5AnBVGzTXM/yHVytt48rXBGcJGzSbms=
|
||||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881/go.mod h1:ASYJtQujNitna6cVHsNQTGrfWvMPJ5Sa2lZlmsH65uM=
|
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881/go.mod h1:ASYJtQujNitna6cVHsNQTGrfWvMPJ5Sa2lZlmsH65uM=
|
||||||
github.com/ergochat/irc-go v0.5.0 h1:woQ1RS9YbfgqPgSpPBBQeczXGIGzR0aC7dEgk469fTw=
|
github.com/ergochat/irc-go v0.5.0-rc2 h1:VuSQJF5K4hWvYSzGa4b8vgL6kzw8HF6LSOejE+RWpAo=
|
||||||
github.com/ergochat/irc-go v0.5.0/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
|
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 h1:2bYXiRFQH636pT0msOG39fmEYl4Eq+OuutcyDsCix/g=
|
||||||
github.com/ergochat/scram v1.0.2-ergo1/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
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 h1:n6eoJk8RpzJFeBJ6gxvqo/dngnVEmJbzJwzKtCZbByo=
|
||||||
@ -27,38 +21,15 @@ github.com/ergochat/webpush-go/v2 v2.0.0/go.mod h1:OQlhnq8JeHDzRzAy6bdDObr19uqbH
|
|||||||
github.com/ergochat/websocket v1.4.2-oragono1 h1:plMUunFBM6UoSCIYCKKclTdy/TkkHfUslhOfJQzfueM=
|
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/ergochat/websocket v1.4.2-oragono1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
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 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
|
||||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
|
||||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd h1:+iAPaTbi1gZpcpDwe/BW1fx7Xoesv69hLNGPheoyhBs=
|
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/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd/go.mod h1:4soZNh0zW0LtYGdQ416i0jO0EIqMGcbtaspRS4BDvRQ=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@ -69,15 +40,9 @@ github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
|
|||||||
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
|
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/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 h1:PpkaieETJMUxYNADsjgtNRcERX7mGc/GP2zp/r5FM3g=
|
||||||
@ -103,72 +68,32 @@ 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/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 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
|
||||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
|
||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
|
||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
|
||||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
|
||||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
|
||||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
|
||||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
|
||||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
|
||||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
|
||||||
modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
|
|
||||||
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -53,7 +52,6 @@ const (
|
|||||||
// (not to be confused with their amodes, which a non-always-on client can have):
|
// (not to be confused with their amodes, which a non-always-on client can have):
|
||||||
keyAccountChannelToModes = "account.channeltomodes %s"
|
keyAccountChannelToModes = "account.channeltomodes %s"
|
||||||
keyAccountPushSubscriptions = "account.pushsubscriptions %s"
|
keyAccountPushSubscriptions = "account.pushsubscriptions %s"
|
||||||
keyAccountMetadata = "account.metadata %s"
|
|
||||||
|
|
||||||
maxCertfpsPerAccount = 5
|
maxCertfpsPerAccount = 5
|
||||||
)
|
)
|
||||||
@ -139,7 +137,6 @@ func (am *AccountManager) createAlwaysOnClients(config *Config) {
|
|||||||
am.loadModes(accountName),
|
am.loadModes(accountName),
|
||||||
am.loadRealname(accountName),
|
am.loadRealname(accountName),
|
||||||
am.loadPushSubscriptions(accountName),
|
am.loadPushSubscriptions(accountName),
|
||||||
am.loadMetadata(accountName),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -754,40 +751,6 @@ func (am *AccountManager) loadPushSubscriptions(account string) (result []stored
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) saveMetadata(account string, metadata map[string]string) {
|
|
||||||
j, err := json.Marshal(metadata)
|
|
||||||
if err != nil {
|
|
||||||
am.server.logger.Error("internal", "error storing metadata", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val := string(j)
|
|
||||||
key := fmt.Sprintf(keyAccountMetadata, account)
|
|
||||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
|
||||||
tx.Set(key, val, nil)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *AccountManager) loadMetadata(account string) (result map[string]string) {
|
|
||||||
key := fmt.Sprintf(keyAccountMetadata, account)
|
|
||||||
var val string
|
|
||||||
am.server.store.View(func(tx *buntdb.Tx) error {
|
|
||||||
val, _ = tx.Get(key)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if val == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(val), &result); err == nil {
|
|
||||||
return result
|
|
||||||
} else {
|
|
||||||
am.server.logger.Error("internal", "error loading metadata", err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
|
func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
|
||||||
certfp, err = utils.NormalizeCertfp(certfp)
|
certfp, err = utils.NormalizeCertfp(certfp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1917,7 +1880,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
pwResetKey := fmt.Sprintf(keyAccountPwReset, casefoldedAccount)
|
pwResetKey := fmt.Sprintf(keyAccountPwReset, casefoldedAccount)
|
||||||
emailChangeKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
|
emailChangeKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
|
||||||
pushSubscriptionsKey := fmt.Sprintf(keyAccountPushSubscriptions, casefoldedAccount)
|
pushSubscriptionsKey := fmt.Sprintf(keyAccountPushSubscriptions, casefoldedAccount)
|
||||||
metadataKey := fmt.Sprintf(keyAccountMetadata, casefoldedAccount)
|
|
||||||
|
|
||||||
var clients []*Client
|
var clients []*Client
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -1977,7 +1939,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
tx.Delete(pwResetKey)
|
tx.Delete(pwResetKey)
|
||||||
tx.Delete(emailChangeKey)
|
tx.Delete(emailChangeKey)
|
||||||
tx.Delete(pushSubscriptionsKey)
|
tx.Delete(pushSubscriptionsKey)
|
||||||
tx.Delete(metadataKey)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -2035,21 +1996,8 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin
|
|||||||
return errAccountInvalidCredentials
|
return errAccountInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
clientAccount, err := am.checkCertAuth(client.IP(), certfp, peerCerts, authzid)
|
var clientAccount ClientAccount
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if client.registered {
|
|
||||||
if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() {
|
|
||||||
err = errNickAccountMismatch
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
am.Login(client, clientAccount)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x509.Certificate, authzid string) (clientAccount ClientAccount, err error) {
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -2060,19 +2008,22 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x
|
|||||||
err = errAccountSuspended
|
err = errAccountSuspended
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// TODO(#1109) clean this check up?
|
||||||
|
if client.registered {
|
||||||
|
if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() {
|
||||||
|
err = errNickAccountMismatch
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
am.Login(client, clientAccount)
|
||||||
|
return
|
||||||
}()
|
}()
|
||||||
|
|
||||||
config := am.server.Config()
|
config := am.server.Config()
|
||||||
if config.Accounts.AuthScript.Enabled {
|
if config.Accounts.AuthScript.Enabled {
|
||||||
var output AuthScriptOutput
|
var output AuthScriptOutput
|
||||||
var ipString string
|
output, err = CheckAuthScript(am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig,
|
||||||
if ip != nil {
|
AuthScriptInput{Certfp: certfp, IP: client.IP().String(), peerCerts: peerCerts})
|
||||||
ipString = ip.String()
|
|
||||||
}
|
|
||||||
output, err = CheckAuthScript(
|
|
||||||
am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig,
|
|
||||||
AuthScriptInput{Certfp: certfp, IP: ipString, peerCerts: peerCerts},
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
||||||
} else if output.Success && output.AccountName != "" {
|
} else if output.Success && output.AccountName != "" {
|
||||||
@ -2093,19 +2044,18 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if authzid != "" {
|
if authzid != "" {
|
||||||
if cfAuthzid, cErr := CasefoldName(authzid); cErr != nil || cfAuthzid != account {
|
if cfAuthzid, err := CasefoldName(authzid); err != nil || cfAuthzid != account {
|
||||||
err = errAuthzidAuthcidMismatch
|
return errAuthzidAuthcidMismatch
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ok, we found an account corresponding to their certificate
|
// ok, we found an account corresponding to their certificate
|
||||||
clientAccount, err = am.LoadAccount(account)
|
clientAccount, err = am.LoadAccount(account)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
|
type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
|
||||||
@ -2349,7 +2299,7 @@ func (ac *AccountCredentials) Serialize() (result string, err error) {
|
|||||||
return string(credText), nil
|
return string(credText), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost int) (err error) {
|
func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint) (err error) {
|
||||||
if passphrase == "" {
|
if passphrase == "" {
|
||||||
ac.PassphraseHash = nil
|
ac.PassphraseHash = nil
|
||||||
ac.SCRAMCreds = SCRAMCreds{}
|
ac.SCRAMCreds = SCRAMCreds{}
|
||||||
@ -2360,7 +2310,7 @@ func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost int) (
|
|||||||
return errAccountBadPassphrase
|
return errAccountBadPassphrase
|
||||||
}
|
}
|
||||||
|
|
||||||
ac.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost)
|
ac.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), int(bcryptCost))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errAccountBadPassphrase
|
return errAccountBadPassphrase
|
||||||
}
|
}
|
||||||
|
|||||||
259
irc/api.go
259
irc/api.go
@ -5,12 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
|
||||||
"github.com/ergochat/ergo/irc/sno"
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newAPIHandler(server *Server) http.Handler {
|
func newAPIHandler(server *Server) http.Handler {
|
||||||
@ -19,25 +14,10 @@ func newAPIHandler(server *Server) http.Handler {
|
|||||||
mux: http.NewServeMux(),
|
mux: http.NewServeMux(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// server-level functionality:
|
|
||||||
api.mux.HandleFunc("POST /v1/rehash", api.handleRehash)
|
api.mux.HandleFunc("POST /v1/rehash", api.handleRehash)
|
||||||
api.mux.HandleFunc("POST /v1/status", api.handleStatus)
|
|
||||||
api.mux.HandleFunc("POST /v1/list", api.handleList)
|
|
||||||
api.mux.HandleFunc("POST /v1/defcon", api.handleDefcon)
|
|
||||||
|
|
||||||
// use Ergo as a source of truth for authentication in other services:
|
|
||||||
api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth)
|
api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth)
|
||||||
|
|
||||||
// legacy names for /v1/ns endpoints:
|
|
||||||
api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister)
|
api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister)
|
||||||
api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails)
|
api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails)
|
||||||
api.mux.HandleFunc("POST /v1/account_list", api.handleAccountList)
|
|
||||||
|
|
||||||
// /v1/ns: nickserv functionality
|
|
||||||
api.mux.HandleFunc("POST /v1/ns/info", api.handleAccountDetails)
|
|
||||||
api.mux.HandleFunc("POST /v1/ns/list", api.handleAccountList)
|
|
||||||
api.mux.HandleFunc("POST /v1/ns/passwd", api.handleNsPasswd)
|
|
||||||
api.mux.HandleFunc("POST /v1/ns/saregister", api.handleSaregister)
|
|
||||||
|
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
@ -49,6 +29,7 @@ type ergoAPI struct {
|
|||||||
|
|
||||||
func (a *ergoAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (a *ergoAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
defer a.server.HandlePanic(nil)
|
defer a.server.HandlePanic(nil)
|
||||||
|
|
||||||
defer a.server.logger.Debug("api", r.URL.Path)
|
defer a.server.logger.Debug("api", r.URL.Path)
|
||||||
|
|
||||||
if a.checkBearerAuth(r.Header.Get("Authorization")) {
|
if a.checkBearerAuth(r.Header.Get("Authorization")) {
|
||||||
@ -121,34 +102,6 @@ func (a *ergoAPI) handleRehash(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.writeJSONResponse(response, w, r)
|
a.writeJSONResponse(response, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
type defconRequestResponse struct {
|
|
||||||
apiGenericResponse
|
|
||||||
Defcon int `json:"defcon"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ergoAPI) handleDefcon(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var changeRequested uint32
|
|
||||||
var request defconRequestResponse
|
|
||||||
// ignore errors or invalid values
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&request); err == nil {
|
|
||||||
if 1 <= request.Defcon && request.Defcon <= 5 {
|
|
||||||
changeRequested = uint32(request.Defcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if changeRequested != 0 {
|
|
||||||
a.server.SetDefcon(changeRequested)
|
|
||||||
message := fmt.Sprintf("API set DEFCON level to %d", changeRequested)
|
|
||||||
a.server.logger.Info("server", message)
|
|
||||||
a.server.snomasks.Send(sno.LocalAnnouncements, message)
|
|
||||||
}
|
|
||||||
a.writeJSONResponse(
|
|
||||||
defconRequestResponse{
|
|
||||||
apiGenericResponse: apiGenericResponse{Success: true},
|
|
||||||
Defcon: int(a.server.Defcon()),
|
|
||||||
}, w, r,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiCheckAuthResponse struct {
|
type apiCheckAuthResponse struct {
|
||||||
apiGenericResponse
|
apiGenericResponse
|
||||||
AccountName string `json:"accountName,omitempty"`
|
AccountName string `json:"accountName,omitempty"`
|
||||||
@ -162,29 +115,28 @@ func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var response apiCheckAuthResponse
|
var response apiCheckAuthResponse
|
||||||
|
|
||||||
var account ClientAccount
|
// try passphrase if present
|
||||||
var err error
|
|
||||||
|
|
||||||
// try whatever credentials are present
|
|
||||||
if request.AccountName != "" && request.Passphrase != "" {
|
if request.AccountName != "" && request.Passphrase != "" {
|
||||||
account, err = a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase)
|
// TODO this only checks the internal database, not auth-script;
|
||||||
} else if request.Certfp != "" {
|
// it's a little weird to use both auth-script and the API but we should probably handle it
|
||||||
account, err = a.server.accounts.checkCertAuth(nil, request.Certfp, nil, "")
|
account, err := a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase)
|
||||||
} else {
|
switch err {
|
||||||
err = errAccountInvalidCredentials
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch err {
|
// try certfp if present
|
||||||
case nil:
|
if !response.Success && request.Certfp != "" {
|
||||||
// success, no error
|
// TODO support cerftp
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a.writeJSONResponse(response, w, r)
|
a.writeJSONResponse(response, w, r)
|
||||||
@ -221,37 +173,10 @@ func (a *ergoAPI) handleSaregister(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.writeJSONResponse(response, w, r)
|
a.writeJSONResponse(response, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ergoAPI) handleNsPasswd(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.setPassword(request.AccountName, request.Passphrase, true)
|
|
||||||
switch err {
|
|
||||||
case nil:
|
|
||||||
response.Success = true
|
|
||||||
case errAccountDoesNotExist:
|
|
||||||
response.ErrorCode = "ACCOUNT_DOES_NOT_EXIST"
|
|
||||||
case errAccountBadPassphrase, errEmptyCredentials:
|
|
||||||
response.ErrorCode = "INVALID_PASSPHRASE"
|
|
||||||
case errCredsExternallyManaged:
|
|
||||||
response.ErrorCode = "CREDENTIALS_EXTERNALLY_MANAGED"
|
|
||||||
default:
|
|
||||||
a.server.logger.Error("api", "could not change user password:", err.Error())
|
|
||||||
response.ErrorCode = "UNKNOWN_ERROR"
|
|
||||||
}
|
|
||||||
|
|
||||||
a.writeJSONResponse(response, w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiAccountDetailsResponse struct {
|
type apiAccountDetailsResponse struct {
|
||||||
apiGenericResponse
|
apiGenericResponse
|
||||||
AccountName string `json:"accountName,omitempty"`
|
AccountName string `json:"accountName,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
RegisteredAt string `json:"registeredAt,omitempty"`
|
|
||||||
Channels []string `json:"channels,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountDetailsRequest struct {
|
type apiAccountDetailsRequest struct {
|
||||||
@ -266,6 +191,8 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var response apiAccountDetailsResponse
|
var response apiAccountDetailsResponse
|
||||||
|
|
||||||
|
// TODO could probably use better error handling and more details
|
||||||
|
|
||||||
if request.AccountName != "" {
|
if request.AccountName != "" {
|
||||||
accountData, err := a.server.accounts.LoadAccount(request.AccountName)
|
accountData, err := a.server.accounts.LoadAccount(request.AccountName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -280,12 +207,6 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) {
|
|||||||
case nil:
|
case nil:
|
||||||
response.AccountName = accountData.Name
|
response.AccountName = accountData.Name
|
||||||
response.Email = accountData.Settings.Email
|
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
|
response.Success = true
|
||||||
case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
|
case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
|
||||||
response.Success = false
|
response.Success = false
|
||||||
@ -301,135 +222,3 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
a.writeJSONResponse(response, w, r)
|
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, 0, len(accounts))
|
|
||||||
for _, account := range accounts {
|
|
||||||
accountData, err := a.server.accounts.LoadAccount(account)
|
|
||||||
if err != nil {
|
|
||||||
// shouldn't happen
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Accounts = append(
|
|
||||||
response.Accounts,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiChannelData struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
HasKey bool `json:"hasKey"`
|
|
||||||
InviteOnly bool `json:"inviteOnly"`
|
|
||||||
Secret bool `json:"secret"`
|
|
||||||
UserCount int `json:"userCount"`
|
|
||||||
Topic string `json:"topic"`
|
|
||||||
TopicSetAt string `json:"topicSetAt,omitempty"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
Registered bool `json:"registered"`
|
|
||||||
Owner string `json:"owner,omitempty"`
|
|
||||||
RegisteredAt string `json:"registeredAt,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) apiData() (result apiChannelData) {
|
|
||||||
channel.stateMutex.RLock()
|
|
||||||
defer channel.stateMutex.RUnlock()
|
|
||||||
result.Name = channel.name
|
|
||||||
result.HasKey = channel.key != ""
|
|
||||||
result.InviteOnly = channel.flags.HasMode(modes.InviteOnly)
|
|
||||||
result.Secret = channel.flags.HasMode(modes.Secret)
|
|
||||||
result.UserCount = len(channel.members)
|
|
||||||
result.Topic = channel.topic
|
|
||||||
if !channel.topicSetTime.IsZero() {
|
|
||||||
result.TopicSetAt = channel.topicSetTime.UTC().Format(utils.IRCv3TimestampFormat)
|
|
||||||
}
|
|
||||||
result.CreatedAt = channel.createdTime.UTC().Format(utils.IRCv3TimestampFormat)
|
|
||||||
result.Registered = channel.registeredFounder != ""
|
|
||||||
if result.Registered {
|
|
||||||
result.Owner = channel.registeredFounder
|
|
||||||
if !channel.registeredTime.IsZero() {
|
|
||||||
result.RegisteredAt = channel.registeredTime.UTC().Format(utils.IRCv3TimestampFormat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiListResponse struct {
|
|
||||||
apiGenericResponse
|
|
||||||
Channels []apiChannelData `json:"channels"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ergoAPI) handleList(w http.ResponseWriter, r *http.Request) {
|
|
||||||
channels := a.server.channels.ListableChannels()
|
|
||||||
response := apiListResponse{
|
|
||||||
apiGenericResponse: apiGenericResponse{Success: true},
|
|
||||||
Channels: make([]apiChannelData, 0, len(channels)),
|
|
||||||
}
|
|
||||||
for _, channel := range channels {
|
|
||||||
response.Channels = append(response.Channels, channel.apiData())
|
|
||||||
}
|
|
||||||
a.writeJSONResponse(response, w, r)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -44,11 +44,10 @@ func (b *buntdbDatastore) GetAll(table datastore.Table) (result []datastore.KV,
|
|||||||
tablePrefix := fmt.Sprintf("%x ", table)
|
tablePrefix := fmt.Sprintf("%x ", table)
|
||||||
err = b.db.View(func(tx *buntdb.Tx) error {
|
err = b.db.View(func(tx *buntdb.Tx) error {
|
||||||
err := tx.AscendGreaterOrEqual("", tablePrefix, func(key, value string) bool {
|
err := tx.AscendGreaterOrEqual("", tablePrefix, func(key, value string) bool {
|
||||||
encUUID, ok := strings.CutPrefix(key, tablePrefix)
|
if !strings.HasPrefix(key, tablePrefix) {
|
||||||
if !ok {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
uuid, err := utils.DecodeUUID(encUUID)
|
uuid, err := utils.DecodeUUID(strings.TrimPrefix(key, tablePrefix))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
result = append(result, datastore.KV{UUID: uuid, Value: []byte(value)})
|
result = append(result, datastore.KV{UUID: uuid, Value: []byte(value)})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ package caps
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 38
|
numCapabs = 37
|
||||||
// length of the uint32 array that represents the bitset:
|
// length of the uint32 array that represents the bitset:
|
||||||
bitsetLen = 2
|
bitsetLen = 2
|
||||||
)
|
)
|
||||||
@ -65,10 +65,6 @@ const (
|
|||||||
// https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md
|
// https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md
|
||||||
MessageRedaction Capability = iota
|
MessageRedaction Capability = iota
|
||||||
|
|
||||||
// Metadata is the draft IRCv3 capability named "draft/metadata-2":
|
|
||||||
// https://ircv3.net/specs/extensions/metadata
|
|
||||||
Metadata Capability = iota
|
|
||||||
|
|
||||||
// Multiline is the proposed IRCv3 capability named "draft/multiline":
|
// Multiline is the proposed IRCv3 capability named "draft/multiline":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/398
|
// https://github.com/ircv3/ircv3-specifications/pull/398
|
||||||
Multiline Capability = iota
|
Multiline Capability = iota
|
||||||
@ -182,7 +178,6 @@ var (
|
|||||||
"draft/extended-isupport",
|
"draft/extended-isupport",
|
||||||
"draft/languages",
|
"draft/languages",
|
||||||
"draft/message-redaction",
|
"draft/message-redaction",
|
||||||
"draft/metadata-2",
|
|
||||||
"draft/multiline",
|
"draft/multiline",
|
||||||
"draft/no-implicit-names",
|
"draft/no-implicit-names",
|
||||||
"draft/persistence",
|
"draft/persistence",
|
||||||
|
|||||||
@ -7,7 +7,6 @@ package irc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"iter"
|
|
||||||
"maps"
|
"maps"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -56,7 +55,6 @@ type Channel struct {
|
|||||||
dirtyBits uint
|
dirtyBits uint
|
||||||
settings ChannelSettings
|
settings ChannelSettings
|
||||||
uuid utils.UUID
|
uuid utils.UUID
|
||||||
metadata map[string]string
|
|
||||||
// these caches are paired to allow iteration over channel members without holding the lock
|
// these caches are paired to allow iteration over channel members without holding the lock
|
||||||
membersCache []*Client
|
membersCache []*Client
|
||||||
memberDataCache []*memberData
|
memberDataCache []*memberData
|
||||||
@ -128,7 +126,6 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
|
|||||||
channel.userLimit = chanReg.UserLimit
|
channel.userLimit = chanReg.UserLimit
|
||||||
channel.settings = chanReg.Settings
|
channel.settings = chanReg.Settings
|
||||||
channel.forward = chanReg.Forward
|
channel.forward = chanReg.Forward
|
||||||
channel.metadata = chanReg.Metadata
|
|
||||||
|
|
||||||
for _, mode := range chanReg.Modes {
|
for _, mode := range chanReg.Modes {
|
||||||
channel.flags.SetMode(mode, true)
|
channel.flags.SetMode(mode, true)
|
||||||
@ -166,7 +163,6 @@ func (channel *Channel) ExportRegistration() (info RegisteredChannel) {
|
|||||||
info.AccountToUMode = maps.Clone(channel.accountToUMode)
|
info.AccountToUMode = maps.Clone(channel.accountToUMode)
|
||||||
|
|
||||||
info.Settings = channel.settings
|
info.Settings = channel.settings
|
||||||
info.Metadata = channel.metadata
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -551,11 +547,7 @@ func (channel *Channel) ClientStatus(client *Client) (present bool, joinTimeSecs
|
|||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
memberData, present := channel.members[client]
|
memberData, present := channel.members[client]
|
||||||
if present {
|
return present, time.Unix(0, memberData.joinTime).Unix(), memberData.modes.AllModes()
|
||||||
return present, time.Unix(0, memberData.joinTime).Unix(), memberData.modes.AllModes()
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper for persisting channel-user modes for always-on clients;
|
// helper for persisting channel-user modes for always-on clients;
|
||||||
@ -734,9 +726,6 @@ func (channel *Channel) AddHistoryItem(item history.Item, account string) (err e
|
|||||||
status, target, _ := channel.historyStatus(channel.server.Config())
|
status, target, _ := channel.historyStatus(channel.server.Config())
|
||||||
if status == HistoryPersistent {
|
if status == HistoryPersistent {
|
||||||
err = channel.server.historyDB.AddChannelItem(target, item, account)
|
err = channel.server.historyDB.AddChannelItem(target, item, account)
|
||||||
if err != nil {
|
|
||||||
channel.server.logger.Error("history", "could not add channel message to history", err.Error())
|
|
||||||
}
|
|
||||||
} else if status == HistoryEphemeral {
|
} else if status == HistoryEphemeral {
|
||||||
channel.history.Add(item)
|
channel.history.Add(item)
|
||||||
}
|
}
|
||||||
@ -903,10 +892,6 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
rb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
|
rb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
|
||||||
}
|
}
|
||||||
|
|
||||||
if rb.session.capabilities.Has(caps.Metadata) {
|
|
||||||
syncChannelMetadata(client.server, rb, channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rb.session.client == client {
|
if rb.session.client == client {
|
||||||
// don't send topic and names for a SAJOIN of a different client
|
// don't send topic and names for a SAJOIN of a different client
|
||||||
channel.SendTopic(client, rb, false)
|
channel.SendTopic(client, rb, false)
|
||||||
@ -1477,8 +1462,8 @@ func (channel *Channel) ShowMaskList(client *Client, mode modes.Mode, rb *Respon
|
|||||||
rpllist = RPL_EXCEPTLIST
|
rpllist = RPL_EXCEPTLIST
|
||||||
rplendoflist = RPL_ENDOFEXCEPTLIST
|
rplendoflist = RPL_ENDOFEXCEPTLIST
|
||||||
} else if mode == modes.InviteMask {
|
} else if mode == modes.InviteMask {
|
||||||
rpllist = RPL_INVEXLIST
|
rpllist = RPL_INVITELIST
|
||||||
rplendoflist = RPL_ENDOFINVEXLIST
|
rplendoflist = RPL_ENDOFINVITELIST
|
||||||
}
|
}
|
||||||
|
|
||||||
nick := client.Nick()
|
nick := client.Nick()
|
||||||
@ -1684,20 +1669,6 @@ func (channel *Channel) auditoriumFriends(client *Client) (friends []*Client) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) sessionsWithCaps(capabs ...caps.Capability) iter.Seq[*Session] {
|
|
||||||
return func(yield func(*Session) bool) {
|
|
||||||
for _, member := range channel.Members() {
|
|
||||||
for _, sess := range member.Sessions() {
|
|
||||||
if sess.capabilities.HasAll(capabs...) {
|
|
||||||
if !yield(sess) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns whether the client is visible to unprivileged users in the channel
|
// returns whether the client is visible to unprivileged users in the channel
|
||||||
// (i.e., respecting auditorium mode). note that this assumes that the client
|
// (i.e., respecting auditorium mode). note that this assumes that the client
|
||||||
// is a member; if the client is not, it may return true anyway
|
// is a member; if the client is not, it may return true anyway
|
||||||
|
|||||||
@ -206,10 +206,6 @@ func (cm *ChannelManager) Cleanup(channel *Channel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) {
|
func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) {
|
||||||
if account == "" {
|
|
||||||
return errAuthRequired // this is already enforced by ChanServ, but do a final check
|
|
||||||
}
|
|
||||||
|
|
||||||
if cm.server.Defcon() <= 4 {
|
if cm.server.Defcon() <= 4 {
|
||||||
return errFeatureDisabled
|
return errFeatureDisabled
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,8 +63,6 @@ type RegisteredChannel struct {
|
|||||||
Invites map[string]MaskInfo
|
Invites map[string]MaskInfo
|
||||||
// Settings are the chanserv-modifiable settings
|
// Settings are the chanserv-modifiable settings
|
||||||
Settings ChannelSettings
|
Settings ChannelSettings
|
||||||
// Metadata set using the METADATA command
|
|
||||||
Metadata map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RegisteredChannel) Serialize() ([]byte, error) {
|
func (r *RegisteredChannel) Serialize() ([]byte, error) {
|
||||||
|
|||||||
133
irc/client.go
133
irc/client.go
@ -41,7 +41,8 @@ const (
|
|||||||
DefaultMaxLineLen = 512
|
DefaultMaxLineLen = 512
|
||||||
|
|
||||||
// IdentTimeout is how long before our ident (username) check times out.
|
// IdentTimeout is how long before our ident (username) check times out.
|
||||||
IdentTimeout = time.Second + 500*time.Millisecond
|
IdentTimeout = time.Second + 500*time.Millisecond
|
||||||
|
IRCv3TimestampFormat = utils.IRCv3TimestampFormat
|
||||||
// limit the number of device IDs a client can use, as a DoS mitigation
|
// limit the number of device IDs a client can use, as a DoS mitigation
|
||||||
maxDeviceIDsPerClient = 64
|
maxDeviceIDsPerClient = 64
|
||||||
// maximum total read markers that can be stored
|
// maximum total read markers that can be stored
|
||||||
@ -53,16 +54,18 @@ const (
|
|||||||
pushQueueLengthPerClient = 16
|
pushQueueLengthPerClient = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
// idle timeouts for client connections, set from the config
|
|
||||||
RegisterTimeout, PingTimeout, DisconnectTimeout time.Duration
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// RegisterTimeout is how long clients have to register before we disconnect them
|
||||||
|
RegisterTimeout = time.Minute
|
||||||
|
// DefaultIdleTimeout is how long without traffic before we send the client a PING
|
||||||
|
DefaultIdleTimeout = time.Minute + 30*time.Second
|
||||||
// For Tor clients, we send a PING at least every 30 seconds, as a workaround for this bug
|
// For Tor clients, we send a PING at least every 30 seconds, as a workaround for this bug
|
||||||
// (single-onion circuits will close unless the client sends data once every 60 seconds):
|
// (single-onion circuits will close unless the client sends data once every 60 seconds):
|
||||||
// https://bugs.torproject.org/29665
|
// https://bugs.torproject.org/29665
|
||||||
TorPingTimeout = time.Second * 30
|
TorIdleTimeout = time.Second * 30
|
||||||
|
// This is how long a client gets without sending any message, including the PONG to our
|
||||||
|
// PING, before we disconnect them:
|
||||||
|
DefaultTotalTimeout = 2*time.Minute + 30*time.Second
|
||||||
|
|
||||||
// round off the ping interval by this much, see below:
|
// round off the ping interval by this much, see below:
|
||||||
PingCoalesceThreshold = time.Second
|
PingCoalesceThreshold = time.Second
|
||||||
@ -129,8 +132,6 @@ type Client struct {
|
|||||||
clearablePushMessages map[string]time.Time
|
clearablePushMessages map[string]time.Time
|
||||||
pushSubscriptionsExist atomic.Uint32 // this is a cache on len(pushSubscriptions) != 0
|
pushSubscriptionsExist atomic.Uint32 // this is a cache on len(pushSubscriptions) != 0
|
||||||
pushQueue pushQueue
|
pushQueue pushQueue
|
||||||
metadata map[string]string
|
|
||||||
metadataThrottle connection_limits.ThrottleDetails
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type saslStatus struct {
|
type saslStatus struct {
|
||||||
@ -188,8 +189,6 @@ type Session struct {
|
|||||||
fakelag Fakelag
|
fakelag Fakelag
|
||||||
deferredFakelagCount int
|
deferredFakelagCount int
|
||||||
|
|
||||||
lastOperAttempt time.Time
|
|
||||||
|
|
||||||
certfp string
|
certfp string
|
||||||
peerCerts []*x509.Certificate
|
peerCerts []*x509.Certificate
|
||||||
sasl saslStatus
|
sasl saslStatus
|
||||||
@ -216,9 +215,6 @@ type Session struct {
|
|||||||
batch MultilineBatch
|
batch MultilineBatch
|
||||||
|
|
||||||
webPushEndpoint string // goroutine-local: web push endpoint registered by the current session
|
webPushEndpoint string // goroutine-local: web push endpoint registered by the current session
|
||||||
|
|
||||||
metadataSubscriptions utils.HashSet[string]
|
|
||||||
metadataPreregVals map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultilineBatch tracks the state of a client-to-server multiline batch.
|
// MultilineBatch tracks the state of a client-to-server multiline batch.
|
||||||
@ -414,11 +410,6 @@ func (server *Server) RunClient(conn IRCConn) {
|
|||||||
session.certfp, session.peerCerts, _ = utils.GetCertFP(wConn.Conn, RegisterTimeout)
|
session.certfp, session.peerCerts, _ = utils.GetCertFP(wConn.Conn, RegisterTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Server.InitialNotice != "" {
|
|
||||||
// send initial notice for HOPM to recognize
|
|
||||||
client.Send(nil, client.server.name, "NOTICE", "*", config.Server.InitialNotice)
|
|
||||||
}
|
|
||||||
|
|
||||||
if session.isTor {
|
if session.isTor {
|
||||||
session.rawHostname = config.Server.TorListeners.Vhost
|
session.rawHostname = config.Server.TorListeners.Vhost
|
||||||
client.rawHostname = session.rawHostname
|
client.rawHostname = session.rawHostname
|
||||||
@ -433,7 +424,7 @@ func (server *Server) RunClient(conn IRCConn) {
|
|||||||
client.run(session)
|
client.run(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus map[string]alwaysOnChannelStatus, lastSeen, readMarkers map[string]time.Time, uModes modes.Modes, realname string, pushSubscriptions []storedPushSubscription, metadata map[string]string) {
|
func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus map[string]alwaysOnChannelStatus, lastSeen, readMarkers map[string]time.Time, uModes modes.Modes, realname string, pushSubscriptions []storedPushSubscription) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
if lastSeen == nil && account.Settings.AutoreplayMissed {
|
if lastSeen == nil && account.Settings.AutoreplayMissed {
|
||||||
@ -518,10 +509,6 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
client.rebuildPushSubscriptionCache()
|
client.rebuildPushSubscriptionCache()
|
||||||
|
|
||||||
if len(metadata) != 0 {
|
|
||||||
client.metadata = metadata
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) resizeHistory(config *Config) {
|
func (client *Client) resizeHistory(config *Config) {
|
||||||
@ -688,7 +675,7 @@ func (client *Client) run(session *Session) {
|
|||||||
isReattach := client.Registered()
|
isReattach := client.Registered()
|
||||||
if isReattach {
|
if isReattach {
|
||||||
client.Touch(session)
|
client.Touch(session)
|
||||||
client.performReattach(session)
|
client.playReattachMessages(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
firstLine := !isReattach
|
firstLine := !isReattach
|
||||||
@ -788,9 +775,7 @@ func (client *Client) run(session *Session) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) performReattach(session *Session) {
|
func (client *Client) playReattachMessages(session *Session) {
|
||||||
client.applyPreregMetadata(session)
|
|
||||||
|
|
||||||
client.server.playRegistrationBurst(session)
|
client.server.playRegistrationBurst(session)
|
||||||
hasHistoryCaps := session.HasHistoryCaps()
|
hasHistoryCaps := session.HasHistoryCaps()
|
||||||
for _, channel := range session.client.Channels() {
|
for _, channel := range session.client.Channels() {
|
||||||
@ -814,34 +799,6 @@ func (client *Client) performReattach(session *Session) {
|
|||||||
session.autoreplayMissedSince = time.Time{}
|
session.autoreplayMissedSince = time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) applyPreregMetadata(session *Session) {
|
|
||||||
if session.metadataPreregVals == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
session.metadataPreregVals = nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
updates := client.UpdateMetadataFromPrereg(session.metadataPreregVals, client.server.Config().Metadata.MaxKeys)
|
|
||||||
if len(updates) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO this is expensive
|
|
||||||
friends := client.FriendsMonitors(caps.Metadata)
|
|
||||||
for _, s := range client.Sessions() {
|
|
||||||
if s != session {
|
|
||||||
friends.Add(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target := client.Nick()
|
|
||||||
for k, v := range updates {
|
|
||||||
broadcastMetadataUpdate(client.server, maps.Keys(friends), session, target, k, v, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// idle, quit, timers and timeouts
|
// idle, quit, timers and timeouts
|
||||||
//
|
//
|
||||||
@ -873,19 +830,19 @@ func (client *Client) updateIdleTimer(session *Session, now time.Time) {
|
|||||||
session.pingSent = false
|
session.pingSent = false
|
||||||
|
|
||||||
if session.idleTimer == nil {
|
if session.idleTimer == nil {
|
||||||
pingTimeout := PingTimeout
|
pingTimeout := DefaultIdleTimeout
|
||||||
if session.isTor && TorPingTimeout < pingTimeout {
|
if session.isTor {
|
||||||
pingTimeout = TorPingTimeout
|
pingTimeout = TorIdleTimeout
|
||||||
}
|
}
|
||||||
session.idleTimer = time.AfterFunc(pingTimeout, session.handleIdleTimeout)
|
session.idleTimer = time.AfterFunc(pingTimeout, session.handleIdleTimeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *Session) handleIdleTimeout() {
|
func (session *Session) handleIdleTimeout() {
|
||||||
totalTimeout := DisconnectTimeout
|
totalTimeout := DefaultTotalTimeout
|
||||||
pingTimeout := PingTimeout
|
pingTimeout := DefaultIdleTimeout
|
||||||
if session.isTor && TorPingTimeout < pingTimeout {
|
if session.isTor {
|
||||||
pingTimeout = TorPingTimeout
|
pingTimeout = TorIdleTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
session.client.stateMutex.Lock()
|
session.client.stateMutex.Lock()
|
||||||
@ -1173,7 +1130,6 @@ func (client *Client) SetNick(nick, nickCasefolded, skeleton string) (success bo
|
|||||||
client.nickCasefolded = nickCasefolded
|
client.nickCasefolded = nickCasefolded
|
||||||
client.skeleton = skeleton
|
client.skeleton = skeleton
|
||||||
client.updateNickMaskNoMutex()
|
client.updateNickMaskNoMutex()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1407,7 +1363,7 @@ func (client *Client) destroy(session *Session) {
|
|||||||
|
|
||||||
// alert monitors
|
// alert monitors
|
||||||
if registered {
|
if registered {
|
||||||
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false, nil)
|
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up channels
|
// clean up channels
|
||||||
@ -1507,7 +1463,7 @@ func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Ti
|
|||||||
|
|
||||||
func composeMultilineBatch(batchID, fromNickMask, fromAccount string, isBot bool, tags map[string]string, command, target string, message utils.SplitMessage) (result []ircmsg.Message) {
|
func composeMultilineBatch(batchID, fromNickMask, fromAccount string, isBot bool, tags map[string]string, command, target string, message utils.SplitMessage) (result []ircmsg.Message) {
|
||||||
batchStart := ircmsg.MakeMessage(tags, fromNickMask, "BATCH", "+"+batchID, caps.MultilineBatchType, target)
|
batchStart := ircmsg.MakeMessage(tags, fromNickMask, "BATCH", "+"+batchID, caps.MultilineBatchType, target)
|
||||||
batchStart.SetTag("time", message.Time.Format(utils.IRCv3TimestampFormat))
|
batchStart.SetTag("time", message.Time.Format(IRCv3TimestampFormat))
|
||||||
batchStart.SetTag("msgid", message.Msgid)
|
batchStart.SetTag("msgid", message.Msgid)
|
||||||
if fromAccount != "*" {
|
if fromAccount != "*" {
|
||||||
batchStart.SetTag("account", fromAccount)
|
batchStart.SetTag("account", fromAccount)
|
||||||
@ -1615,7 +1571,7 @@ func (session *Session) setTimeTag(msg *ircmsg.Message, serverTime time.Time) {
|
|||||||
if serverTime.IsZero() {
|
if serverTime.IsZero() {
|
||||||
serverTime = time.Now()
|
serverTime = time.Now()
|
||||||
}
|
}
|
||||||
msg.SetTag("time", serverTime.UTC().Format(utils.IRCv3TimestampFormat))
|
msg.SetTag("time", serverTime.UTC().Format(IRCv3TimestampFormat))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1774,15 +1730,12 @@ func (client *Client) addHistoryItem(target *Client, item history.Item, details,
|
|||||||
}
|
}
|
||||||
if cStatus == HistoryPersistent || tStatus == HistoryPersistent {
|
if cStatus == HistoryPersistent || tStatus == HistoryPersistent {
|
||||||
targetedItem.CfCorrespondent = ""
|
targetedItem.CfCorrespondent = ""
|
||||||
err = client.server.historyDB.AddDirectMessage(details.nickCasefolded, details.account, tDetails.nickCasefolded, tDetails.account, targetedItem)
|
client.server.historyDB.AddDirectMessage(details.nickCasefolded, details.account, tDetails.nickCasefolded, tDetails.account, targetedItem)
|
||||||
if err != nil {
|
|
||||||
client.server.logger.Error("history", "could not add direct message to history", err.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) listTargets(start, end time.Time, limit int) (results []history.TargetListing, err error) {
|
func (client *Client) listTargets(start, end history.Selector, limit int) (results []history.TargetListing, err error) {
|
||||||
var base, extras []history.TargetListing
|
var base, extras []history.TargetListing
|
||||||
var chcfnames []string
|
var chcfnames []string
|
||||||
for _, channel := range client.Channels() {
|
for _, channel := range client.Channels() {
|
||||||
@ -1803,35 +1756,27 @@ func (client *Client) listTargets(start, end time.Time, limit int) (results []hi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
persistentExtras, err := client.server.historyDB.ListChannels(chcfnames)
|
persistentExtras, err := client.server.historyDB.ListChannels(chcfnames)
|
||||||
if err != nil {
|
if err == nil && len(persistentExtras) != 0 {
|
||||||
client.server.logger.Error("history", "could not list persistent channels", err.Error())
|
|
||||||
} else if len(persistentExtras) != 0 {
|
|
||||||
extras = append(extras, persistentExtras...)
|
extras = append(extras, persistentExtras...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get DM correspondents from the in-memory buffer or the database, as applicable
|
_, cSeq, err := client.server.GetHistorySequence(nil, client, "")
|
||||||
var cErr error
|
if err == nil && cSeq != nil {
|
||||||
status, target := client.historyStatus(client.server.Config())
|
correspondents, err := cSeq.ListCorrespondents(start, end, limit)
|
||||||
switch status {
|
if err == nil {
|
||||||
case HistoryEphemeral:
|
base = correspondents
|
||||||
base, cErr = client.history.ListCorrespondents(start, end, limit)
|
}
|
||||||
case HistoryPersistent:
|
|
||||||
base, cErr = client.server.historyDB.ListCorrespondents(target, start, end, limit)
|
|
||||||
default:
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
if cErr != nil {
|
|
||||||
base = nil
|
|
||||||
client.server.logger.Error("history", "could not list correspondents", cErr.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results = history.MergeTargets(base, extras, start, end, limit)
|
results = history.MergeTargets(base, extras, start.Time, end.Time, limit)
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// latest PRIVMSG from all DM targets
|
// latest PRIVMSG from all DM targets
|
||||||
func (client *Client) privmsgsBetween(startTime, endTime time.Time, targetLimit, messageLimit int) (results []history.Item, err error) {
|
func (client *Client) privmsgsBetween(startTime, endTime time.Time, targetLimit, messageLimit int) (results []history.Item, err error) {
|
||||||
targets, err := client.listTargets(startTime, endTime, targetLimit)
|
start := history.Selector{Time: startTime}
|
||||||
|
end := history.Selector{Time: endTime}
|
||||||
|
targets, err := client.listTargets(start, end, targetLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1841,7 +1786,7 @@ func (client *Client) privmsgsBetween(startTime, endTime time.Time, targetLimit,
|
|||||||
}
|
}
|
||||||
_, seq, err := client.server.GetHistorySequence(nil, client, target.CfName)
|
_, seq, err := client.server.GetHistorySequence(nil, client, target.CfName)
|
||||||
if err == nil && seq != nil {
|
if err == nil && seq != nil {
|
||||||
items, err := seq.Between(history.Selector{Time: startTime}, history.Selector{Time: endTime}, messageLimit)
|
items, err := seq.Between(start, end, messageLimit)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
results = append(results, items...)
|
results = append(results, items...)
|
||||||
} else {
|
} else {
|
||||||
@ -1870,7 +1815,6 @@ const (
|
|||||||
IncludeUserModes
|
IncludeUserModes
|
||||||
IncludeRealname
|
IncludeRealname
|
||||||
IncludePushSubscriptions
|
IncludePushSubscriptions
|
||||||
IncludeMetadata
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (client *Client) markDirty(dirtyBits uint) {
|
func (client *Client) markDirty(dirtyBits uint) {
|
||||||
@ -1952,9 +1896,6 @@ func (client *Client) performWrite(additionalDirtyBits uint) {
|
|||||||
if (dirtyBits & IncludePushSubscriptions) != 0 {
|
if (dirtyBits & IncludePushSubscriptions) != 0 {
|
||||||
client.server.accounts.savePushSubscriptions(account, client.getPushSubscriptions(true))
|
client.server.accounts.savePushSubscriptions(account, client.getPushSubscriptions(true))
|
||||||
}
|
}
|
||||||
if (dirtyBits & IncludeMetadata) != 0 {
|
|
||||||
client.server.accounts.saveMetadata(account, client.ListMetadata())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blocking store; see Channel.Store and Socket.BlockingWrite
|
// Blocking store; see Channel.Store and Socket.BlockingWrite
|
||||||
|
|||||||
@ -209,11 +209,6 @@ func init() {
|
|||||||
handler: markReadHandler,
|
handler: markReadHandler,
|
||||||
minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS
|
minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS
|
||||||
},
|
},
|
||||||
"METADATA": {
|
|
||||||
handler: metadataHandler,
|
|
||||||
minParams: 2,
|
|
||||||
usablePreReg: true,
|
|
||||||
},
|
|
||||||
"MODE": {
|
"MODE": {
|
||||||
handler: modeHandler,
|
handler: modeHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
|
|||||||
286
irc/config.go
286
irc/config.go
@ -33,7 +33,6 @@ import (
|
|||||||
"github.com/ergochat/ergo/irc/connection_limits"
|
"github.com/ergochat/ergo/irc/connection_limits"
|
||||||
"github.com/ergochat/ergo/irc/custime"
|
"github.com/ergochat/ergo/irc/custime"
|
||||||
"github.com/ergochat/ergo/irc/email"
|
"github.com/ergochat/ergo/irc/email"
|
||||||
"github.com/ergochat/ergo/irc/i18n"
|
|
||||||
"github.com/ergochat/ergo/irc/isupport"
|
"github.com/ergochat/ergo/irc/isupport"
|
||||||
"github.com/ergochat/ergo/irc/jwt"
|
"github.com/ergochat/ergo/irc/jwt"
|
||||||
"github.com/ergochat/ergo/irc/languages"
|
"github.com/ergochat/ergo/irc/languages"
|
||||||
@ -42,16 +41,10 @@ import (
|
|||||||
"github.com/ergochat/ergo/irc/mysql"
|
"github.com/ergochat/ergo/irc/mysql"
|
||||||
"github.com/ergochat/ergo/irc/oauth2"
|
"github.com/ergochat/ergo/irc/oauth2"
|
||||||
"github.com/ergochat/ergo/irc/passwd"
|
"github.com/ergochat/ergo/irc/passwd"
|
||||||
"github.com/ergochat/ergo/irc/postgresql"
|
|
||||||
"github.com/ergochat/ergo/irc/sqlite"
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
"github.com/ergochat/ergo/irc/webpush"
|
"github.com/ergochat/ergo/irc/webpush"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
defaultProxyDeadline = 5 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// here's how this works: exported (capitalized) members of the config structs
|
// here's how this works: exported (capitalized) members of the config structs
|
||||||
// are defined in the YAML file and deserialized directly from there. They may
|
// are defined in the YAML file and deserialized directly from there. They may
|
||||||
// be postprocessed and overwritten by LoadConfig. Unexported (lowercase) members
|
// be postprocessed and overwritten by LoadConfig. Unexported (lowercase) members
|
||||||
@ -378,7 +371,7 @@ type AccountRegistrationConfig struct {
|
|||||||
Mailto email.MailtoConfig
|
Mailto email.MailtoConfig
|
||||||
} `yaml:"callbacks"`
|
} `yaml:"callbacks"`
|
||||||
VerifyTimeout custime.Duration `yaml:"verify-timeout"`
|
VerifyTimeout custime.Duration `yaml:"verify-timeout"`
|
||||||
BcryptCost int `yaml:"bcrypt-cost"`
|
BcryptCost uint `yaml:"bcrypt-cost"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VHostConfig struct {
|
type VHostConfig struct {
|
||||||
@ -448,6 +441,31 @@ func (nr *NickEnforcementMethod) UnmarshalYAML(unmarshal func(interface{}) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cm *Casemapping) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {
|
||||||
|
var orig string
|
||||||
|
if err = unmarshal(&orig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result Casemapping
|
||||||
|
switch strings.ToLower(orig) {
|
||||||
|
case "ascii":
|
||||||
|
result = CasemappingASCII
|
||||||
|
case "precis", "rfc7613", "rfc8265":
|
||||||
|
result = CasemappingPRECIS
|
||||||
|
case "permissive", "fun":
|
||||||
|
result = CasemappingPermissive
|
||||||
|
case "rfc1459":
|
||||||
|
result = CasemappingRFC1459
|
||||||
|
case "rfc1459-strict":
|
||||||
|
result = CasemappingRFC1459Strict
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid casemapping value: %s", orig)
|
||||||
|
}
|
||||||
|
*cm = result
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// OperClassConfig defines a specific operator class.
|
// OperClassConfig defines a specific operator class.
|
||||||
type OperClassConfig struct {
|
type OperClassConfig struct {
|
||||||
Title string
|
Title string
|
||||||
@ -558,14 +576,8 @@ type Config struct {
|
|||||||
CoerceIdent string `yaml:"coerce-ident"`
|
CoerceIdent string `yaml:"coerce-ident"`
|
||||||
MOTD string
|
MOTD string
|
||||||
motdLines []string
|
motdLines []string
|
||||||
MOTDFormatting bool `yaml:"motd-formatting"`
|
MOTDFormatting bool `yaml:"motd-formatting"`
|
||||||
InitialNotice string `yaml:"initial-notice"`
|
Relaymsg struct {
|
||||||
IdleTimeouts struct {
|
|
||||||
Registration time.Duration
|
|
||||||
Ping time.Duration
|
|
||||||
Disconnect time.Duration
|
|
||||||
} `yaml:"idle-timeouts"`
|
|
||||||
Relaymsg struct {
|
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Separators string
|
Separators string
|
||||||
AvailableToChanops bool `yaml:"available-to-chanops"`
|
AvailableToChanops bool `yaml:"available-to-chanops"`
|
||||||
@ -587,11 +599,10 @@ type Config struct {
|
|||||||
Cloaks cloaks.CloakConfig `yaml:"ip-cloaking"`
|
Cloaks cloaks.CloakConfig `yaml:"ip-cloaking"`
|
||||||
SecureNetDefs []string `yaml:"secure-nets"`
|
SecureNetDefs []string `yaml:"secure-nets"`
|
||||||
secureNets []net.IPNet
|
secureNets []net.IPNet
|
||||||
OperThrottle time.Duration `yaml:"oper-throttle"`
|
|
||||||
supportedCaps *caps.Set
|
supportedCaps *caps.Set
|
||||||
supportedCapsWithoutSTS *caps.Set
|
supportedCapsWithoutSTS *caps.Set
|
||||||
capValues caps.Values
|
capValues caps.Values
|
||||||
Casemapping i18n.Casemapping
|
Casemapping Casemapping
|
||||||
EnforceUtf8 bool `yaml:"enforce-utf8"`
|
EnforceUtf8 bool `yaml:"enforce-utf8"`
|
||||||
OutputPath string `yaml:"output-path"`
|
OutputPath string `yaml:"output-path"`
|
||||||
IPCheckScript IPCheckScriptConfig `yaml:"ip-check-script"`
|
IPCheckScript IPCheckScriptConfig `yaml:"ip-check-script"`
|
||||||
@ -640,8 +651,6 @@ type Config struct {
|
|||||||
Path string
|
Path string
|
||||||
AutoUpgrade bool
|
AutoUpgrade bool
|
||||||
MySQL mysql.Config
|
MySQL mysql.Config
|
||||||
PostgreSQL postgresql.Config
|
|
||||||
SQLite sqlite.Config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Accounts AccountConfig
|
Accounts AccountConfig
|
||||||
@ -714,15 +723,6 @@ type Config struct {
|
|||||||
} `yaml:"tagmsg-storage"`
|
} `yaml:"tagmsg-storage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Metadata struct {
|
|
||||||
Enabled bool
|
|
||||||
OperatorOnlyModification bool `yaml:"operator-only-modification"`
|
|
||||||
MaxSubs int `yaml:"max-subs"`
|
|
||||||
MaxKeys int `yaml:"max-keys"`
|
|
||||||
MaxValueBytes int `yaml:"max-value-length"`
|
|
||||||
ClientThrottle ThrottleConfig `yaml:"client-throttle"`
|
|
||||||
}
|
|
||||||
|
|
||||||
WebPush struct {
|
WebPush struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
@ -979,7 +979,7 @@ func (conf *Config) prepareListeners() (err error) {
|
|||||||
conf.Server.trueListeners = make(map[string]utils.ListenerConfig)
|
conf.Server.trueListeners = make(map[string]utils.ListenerConfig)
|
||||||
for addr, block := range conf.Server.Listeners {
|
for addr, block := range conf.Server.Listeners {
|
||||||
var lconf utils.ListenerConfig
|
var lconf utils.ListenerConfig
|
||||||
lconf.ProxyDeadline = defaultProxyDeadline
|
lconf.ProxyDeadline = RegisterTimeout
|
||||||
lconf.Tor = block.Tor
|
lconf.Tor = block.Tor
|
||||||
lconf.STSOnly = block.STSOnly
|
lconf.STSOnly = block.STSOnly
|
||||||
if lconf.STSOnly && !conf.Server.STS.Enabled {
|
if lconf.STSOnly && !conf.Server.STS.Enabled {
|
||||||
@ -1121,104 +1121,55 @@ func mungeFromEnvironment(config *Config, envPair string) (applied bool, name st
|
|||||||
pathComponents[i] = screamingSnakeToKebab(pathComponent)
|
pathComponents[i] = screamingSnakeToKebab(pathComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
type mapInsertion struct {
|
|
||||||
m reflect.Value
|
|
||||||
k reflect.Value
|
|
||||||
v reflect.Value
|
|
||||||
}
|
|
||||||
var mapStack []mapInsertion
|
|
||||||
|
|
||||||
v := reflect.Indirect(reflect.ValueOf(config))
|
v := reflect.Indirect(reflect.ValueOf(config))
|
||||||
t := v.Type()
|
t := v.Type()
|
||||||
for _, component := range pathComponents {
|
for _, component := range pathComponents {
|
||||||
if component == "" {
|
if component == "" {
|
||||||
return false, "", &configPathError{name, "invalid", nil}
|
return false, "", &configPathError{name, "invalid", nil}
|
||||||
}
|
}
|
||||||
if v.Kind() == reflect.Struct {
|
if v.Kind() != reflect.Struct {
|
||||||
var nextField reflect.StructField
|
return false, "", &configPathError{name, "index into non-struct", nil}
|
||||||
success := false
|
}
|
||||||
n := t.NumField()
|
var nextField reflect.StructField
|
||||||
// preferentially get a field with an exact yaml tag match,
|
success := false
|
||||||
// then fall back to case-insensitive comparison of field names
|
n := t.NumField()
|
||||||
|
// preferentially get a field with an exact yaml tag match,
|
||||||
|
// then fall back to case-insensitive comparison of field names
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
if isExported(field) && field.Tag.Get("yaml") == component {
|
||||||
|
nextField = field
|
||||||
|
success = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
field := t.Field(i)
|
field := t.Field(i)
|
||||||
if isExported(field) && field.Tag.Get("yaml") == component {
|
if isExported(field) && strings.ToLower(field.Name) == component {
|
||||||
nextField = field
|
nextField = field
|
||||||
success = true
|
success = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !success {
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
field := t.Field(i)
|
|
||||||
if isExported(field) && strings.ToLower(field.Name) == component {
|
|
||||||
nextField = field
|
|
||||||
success = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !success {
|
|
||||||
return false, "", &configPathError{name, fmt.Sprintf("couldn't resolve path component: `%s`", component), nil}
|
|
||||||
}
|
|
||||||
v = v.FieldByName(nextField.Name)
|
|
||||||
// dereference pointer field if necessary, initialize new value if necessary
|
|
||||||
switch v.Kind() {
|
|
||||||
case reflect.Ptr:
|
|
||||||
if v.IsNil() {
|
|
||||||
v.Set(reflect.New(v.Type().Elem()))
|
|
||||||
}
|
|
||||||
v = reflect.Indirect(v)
|
|
||||||
case reflect.Map:
|
|
||||||
if v.IsNil() {
|
|
||||||
v.Set(reflect.MakeMap(v.Type()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t = v.Type()
|
|
||||||
} else if v.Kind() == reflect.Map {
|
|
||||||
keyType := v.Type().Key()
|
|
||||||
valueType := v.Type().Elem()
|
|
||||||
if keyType.Kind() != reflect.String {
|
|
||||||
return false, "", &configPathError{name, "can't index into map unless its keys are strings", nil}
|
|
||||||
}
|
|
||||||
// index into the map, returns the zero value (invalid) if not found
|
|
||||||
key := reflect.ValueOf(component)
|
|
||||||
v2 := v.MapIndex(key)
|
|
||||||
if v2.IsValid() {
|
|
||||||
// make an addressable copy of the existing value:
|
|
||||||
v3 := reflect.New(valueType).Elem()
|
|
||||||
v3.Set(v2)
|
|
||||||
v2 = v3
|
|
||||||
} else {
|
|
||||||
// make an addressable value of the map value type:
|
|
||||||
v2 = reflect.New(valueType).Elem()
|
|
||||||
// if the map value type is *Baz, set it to a new(Baz):
|
|
||||||
if valueType.Kind() == reflect.Pointer {
|
|
||||||
v2.Set(reflect.New(valueType.Elem()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// we are not operating directly on the current map member,
|
|
||||||
// we need to go back later and insert v2 into the map:
|
|
||||||
mapStack = append(mapStack, mapInsertion{m: v, k: key, v: v2})
|
|
||||||
if valueType.Kind() != reflect.Pointer {
|
|
||||||
v = v2
|
|
||||||
} else {
|
|
||||||
v = reflect.Indirect(v2)
|
|
||||||
}
|
|
||||||
t = v.Type()
|
|
||||||
} else {
|
|
||||||
return false, "", &configPathError{name, "can't index into fields other than struct or map", nil}
|
|
||||||
}
|
}
|
||||||
|
if !success {
|
||||||
|
return false, "", &configPathError{name, fmt.Sprintf("couldn't resolve path component: `%s`", component), nil}
|
||||||
|
}
|
||||||
|
v = v.FieldByName(nextField.Name)
|
||||||
|
// dereference pointer field if necessary, initialize new value if necessary
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
v = reflect.Indirect(v)
|
||||||
|
}
|
||||||
|
t = v.Type()
|
||||||
}
|
}
|
||||||
yamlErr := yaml.Unmarshal([]byte(value), v.Addr().Interface())
|
yamlErr := yaml.Unmarshal([]byte(value), v.Addr().Interface())
|
||||||
if yamlErr != nil {
|
if yamlErr != nil {
|
||||||
return false, "", &configPathError{name, "couldn't deserialize YAML", yamlErr}
|
return false, "", &configPathError{name, "couldn't deserialize YAML", yamlErr}
|
||||||
}
|
}
|
||||||
// go back and do all map assignments
|
|
||||||
for i := len(mapStack) - 1; i >= 0; i-- {
|
|
||||||
elem := mapStack[i]
|
|
||||||
elem.m.SetMapIndex(elem.k, elem.v)
|
|
||||||
}
|
|
||||||
return true, name, nil
|
return true, name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1273,59 +1224,10 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
config.Server.MaxLineLen = DefaultMaxLineLen
|
config.Server.MaxLineLen = DefaultMaxLineLen
|
||||||
}
|
}
|
||||||
if config.Datastore.MySQL.Enabled {
|
if config.Datastore.MySQL.Enabled {
|
||||||
if !mysql.Enabled {
|
|
||||||
return nil, fmt.Errorf("MySQL is enabled in the config, but this binary was not built with MySQL support. Rebuild with `make build_full` to enable")
|
|
||||||
}
|
|
||||||
if config.Limits.NickLen > mysql.MaxTargetLength || config.Limits.ChannelLen > mysql.MaxTargetLength {
|
if config.Limits.NickLen > mysql.MaxTargetLength || config.Limits.ChannelLen > mysql.MaxTargetLength {
|
||||||
return nil, fmt.Errorf("to use MySQL, nick and channel length limits must be %d or lower", mysql.MaxTargetLength)
|
return nil, fmt.Errorf("to use MySQL, nick and channel length limits must be %d or lower", mysql.MaxTargetLength)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config.Datastore.PostgreSQL.Enabled {
|
|
||||||
if !postgresql.Enabled {
|
|
||||||
return nil, fmt.Errorf("PostgreSQL is enabled in the config, but this binary was not built with PostgreSQL support. Rebuild with `make build_full` to enable")
|
|
||||||
}
|
|
||||||
if config.Limits.NickLen > postgresql.MaxTargetLength || config.Limits.ChannelLen > postgresql.MaxTargetLength {
|
|
||||||
return nil, fmt.Errorf("to use PostgreSQL, nick and channel length limits must be %d or lower", postgresql.MaxTargetLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.Datastore.SQLite.Enabled {
|
|
||||||
if !sqlite.Enabled {
|
|
||||||
return nil, fmt.Errorf("SQLite is enabled in the config, but this binary was not built with SQLite support. Rebuild with `make build_full` to enable")
|
|
||||||
}
|
|
||||||
if config.Limits.NickLen > sqlite.MaxTargetLength || config.Limits.ChannelLen > sqlite.MaxTargetLength {
|
|
||||||
return nil, fmt.Errorf("to use SQLite, nick and channel length limits must be %d or lower", sqlite.MaxTargetLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enabledBackends := 0
|
|
||||||
if config.Datastore.MySQL.Enabled {
|
|
||||||
enabledBackends++
|
|
||||||
}
|
|
||||||
if config.Datastore.PostgreSQL.Enabled {
|
|
||||||
enabledBackends++
|
|
||||||
}
|
|
||||||
if config.Datastore.SQLite.Enabled {
|
|
||||||
enabledBackends++
|
|
||||||
}
|
|
||||||
if enabledBackends > 1 {
|
|
||||||
return nil, fmt.Errorf("cannot enable multiple history database backends simultaneously")
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Server.IdleTimeouts.Registration <= 0 {
|
|
||||||
config.Server.IdleTimeouts.Registration = time.Minute
|
|
||||||
}
|
|
||||||
if config.Server.IdleTimeouts.Ping <= 0 {
|
|
||||||
config.Server.IdleTimeouts.Ping = time.Minute + 30*time.Second
|
|
||||||
}
|
|
||||||
if config.Server.IdleTimeouts.Disconnect <= 0 {
|
|
||||||
config.Server.IdleTimeouts.Disconnect = 2*time.Minute + 30*time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(config.Server.IdleTimeouts.Ping < config.Server.IdleTimeouts.Disconnect) {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"ping timeout %v must be strictly less than disconnect timeout %v, to give the client time to respond",
|
|
||||||
config.Server.IdleTimeouts.Ping, config.Server.IdleTimeouts.Disconnect,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Server.CoerceIdent != "" {
|
if config.Server.CoerceIdent != "" {
|
||||||
if config.Server.CheckIdent {
|
if config.Server.CheckIdent {
|
||||||
@ -1403,12 +1305,6 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
config.Server.capValues[caps.Multiline] = multilineCapValue
|
config.Server.capValues[caps.Multiline] = multilineCapValue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !i18n.Enabled {
|
|
||||||
if config.Server.Casemapping != i18n.CasemappingASCII {
|
|
||||||
return nil, fmt.Errorf("i18n support was compiled out; set casemapping to 'ascii' or recompile")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle legacy name 'bouncer' for 'multiclient' section:
|
// handle legacy name 'bouncer' for 'multiclient' section:
|
||||||
if config.Accounts.Bouncer != nil {
|
if config.Accounts.Bouncer != nil {
|
||||||
config.Accounts.Multiclient = *config.Accounts.Bouncer
|
config.Accounts.Multiclient = *config.Accounts.Bouncer
|
||||||
@ -1577,10 +1473,6 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
config.Server.supportedCaps.Disable(caps.SASL)
|
config.Server.supportedCaps.Disable(caps.SASL)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Server.OperThrottle <= 0 {
|
|
||||||
config.Server.OperThrottle = 10 * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := config.Accounts.OAuth2.Postprocess(); err != nil {
|
if err := config.Accounts.OAuth2.Postprocess(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1664,12 +1556,6 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
if config.Accounts.Registration.BcryptCost == 0 {
|
if config.Accounts.Registration.BcryptCost == 0 {
|
||||||
config.Accounts.Registration.BcryptCost = passwd.DefaultCost
|
config.Accounts.Registration.BcryptCost = passwd.DefaultCost
|
||||||
}
|
}
|
||||||
if config.Accounts.Registration.BcryptCost < passwd.MinCost || config.Accounts.Registration.BcryptCost > passwd.MaxCost {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"invalid bcrypt-cost %d (require %d <= cost <= %d)",
|
|
||||||
config.Accounts.Registration.BcryptCost, passwd.MinCost, passwd.MaxCost,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Channels.MaxChannelsPerClient == 0 {
|
if config.Channels.MaxChannelsPerClient == 0 {
|
||||||
config.Channels.MaxChannelsPerClient = 100
|
config.Channels.MaxChannelsPerClient = 100
|
||||||
@ -1686,7 +1572,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
// in the current implementation, we disable history by creating a history buffer
|
// in the current implementation, we disable history by creating a history buffer
|
||||||
// with zero capacity. but the `enabled` config option MUST be respected regardless
|
// with zero capacity. but the `enabled` config option MUST be respected regardless
|
||||||
// of this detail
|
// of this detail
|
||||||
if !config.History.Enabled || config.History.ChathistoryMax == 0 {
|
if !config.History.Enabled {
|
||||||
config.History.ChannelLength = 0
|
config.History.ChannelLength = 0
|
||||||
config.History.ClientLength = 0
|
config.History.ClientLength = 0
|
||||||
config.Server.supportedCaps.Disable(caps.Chathistory)
|
config.Server.supportedCaps.Disable(caps.Chathistory)
|
||||||
@ -1702,8 +1588,8 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
config.History.Persistent.DirectMessages = PersistentDisabled
|
config.History.Persistent.DirectMessages = PersistentDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.History.Persistent.Enabled && !config.Datastore.MySQL.Enabled && !config.Datastore.PostgreSQL.Enabled && !config.Datastore.SQLite.Enabled {
|
if config.History.Persistent.Enabled && !config.Datastore.MySQL.Enabled {
|
||||||
return nil, fmt.Errorf("You must configure a MySQL, PostgreSQL, or SQLite database in order to enable persistent history")
|
return nil, fmt.Errorf("You must configure a MySQL server in order to enable persistent history")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.History.ZNCMax == 0 {
|
if config.History.ZNCMax == 0 {
|
||||||
@ -1743,15 +1629,6 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
// same machine:
|
// same machine:
|
||||||
config.Datastore.MySQL.MaxConns = runtime.NumCPU()
|
config.Datastore.MySQL.MaxConns = runtime.NumCPU()
|
||||||
}
|
}
|
||||||
// do the same for postgresql
|
|
||||||
config.Datastore.PostgreSQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime)
|
|
||||||
config.Datastore.PostgreSQL.TrackAccountMessages = config.History.Retention.EnableAccountIndexing
|
|
||||||
if config.Datastore.PostgreSQL.MaxConns == 0 {
|
|
||||||
config.Datastore.PostgreSQL.MaxConns = runtime.NumCPU()
|
|
||||||
}
|
|
||||||
// and for sqlite
|
|
||||||
config.Datastore.SQLite.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime)
|
|
||||||
config.Datastore.SQLite.TrackAccountMessages = config.History.Retention.EnableAccountIndexing
|
|
||||||
|
|
||||||
config.Server.Cloaks.Initialize()
|
config.Server.Cloaks.Initialize()
|
||||||
if config.Server.Cloaks.Enabled {
|
if config.Server.Cloaks.Enabled {
|
||||||
@ -1760,27 +1637,6 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.Metadata.Enabled {
|
|
||||||
config.Server.supportedCaps.Disable(caps.Metadata)
|
|
||||||
} else {
|
|
||||||
metadataValues := make([]string, 0, 4)
|
|
||||||
metadataValues = append(metadataValues, "before-connect")
|
|
||||||
// these are required for normal operation, so set sane defaults:
|
|
||||||
if config.Metadata.MaxSubs == 0 {
|
|
||||||
config.Metadata.MaxSubs = 10
|
|
||||||
}
|
|
||||||
metadataValues = append(metadataValues, fmt.Sprintf("max-subs=%d", config.Metadata.MaxSubs))
|
|
||||||
if config.Metadata.MaxKeys == 0 {
|
|
||||||
config.Metadata.MaxKeys = 10
|
|
||||||
}
|
|
||||||
metadataValues = append(metadataValues, fmt.Sprintf("max-keys=%d", config.Metadata.MaxKeys))
|
|
||||||
// this is not required since we enforce a hardcoded upper bound on key+value
|
|
||||||
if config.Metadata.MaxValueBytes > 0 {
|
|
||||||
metadataValues = append(metadataValues, fmt.Sprintf("max-value-bytes=%d", config.Metadata.MaxValueBytes))
|
|
||||||
}
|
|
||||||
config.Server.capValues[caps.Metadata] = strings.Join(metadataValues, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = config.processExtjwt()
|
err = config.processExtjwt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -1867,9 +1723,9 @@ func (config *Config) generateISupport() (err error) {
|
|||||||
switch config.Server.Casemapping {
|
switch config.Server.Casemapping {
|
||||||
default:
|
default:
|
||||||
casemappingToken = "ascii" // this is published for ascii, precis, or permissive
|
casemappingToken = "ascii" // this is published for ascii, precis, or permissive
|
||||||
case i18n.CasemappingRFC1459:
|
case CasemappingRFC1459:
|
||||||
casemappingToken = "rfc1459"
|
casemappingToken = "rfc1459"
|
||||||
case i18n.CasemappingRFC1459Strict:
|
case CasemappingRFC1459Strict:
|
||||||
casemappingToken = "rfc1459-strict"
|
casemappingToken = "rfc1459-strict"
|
||||||
}
|
}
|
||||||
isupport.Add("CASEMAPPING", casemappingToken)
|
isupport.Add("CASEMAPPING", casemappingToken)
|
||||||
@ -1908,7 +1764,7 @@ func (config *Config) generateISupport() (err error) {
|
|||||||
isupport.Add("STATUSMSG", "~&@%+")
|
isupport.Add("STATUSMSG", "~&@%+")
|
||||||
isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d", maxTargetsString, maxTargetsString, maxTargetsString, config.Limits.MonitorEntries))
|
isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d", maxTargetsString, maxTargetsString, maxTargetsString, config.Limits.MonitorEntries))
|
||||||
isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen))
|
isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen))
|
||||||
if config.Server.Casemapping == i18n.CasemappingPRECIS {
|
if config.Server.Casemapping == CasemappingPRECIS {
|
||||||
isupport.Add("UTF8MAPPING", precisUTF8MappingToken)
|
isupport.Add("UTF8MAPPING", precisUTF8MappingToken)
|
||||||
}
|
}
|
||||||
if config.Server.EnforceUtf8 {
|
if config.Server.EnforceUtf8 {
|
||||||
@ -1923,10 +1779,6 @@ func (config *Config) generateISupport() (err error) {
|
|||||||
}
|
}
|
||||||
isupport.Add("WHOX", "")
|
isupport.Add("WHOX", "")
|
||||||
|
|
||||||
if config.Accounts.RequireSasl.Enabled {
|
|
||||||
isupport.Add("draft/ACCOUNTREQUIRED", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range config.Server.AdditionalISupport {
|
for key, value := range config.Server.AdditionalISupport {
|
||||||
if !isupport.Contains(key) {
|
if !isupport.Contains(key) {
|
||||||
isupport.Add(key, value)
|
isupport.Add(key, value)
|
||||||
@ -1985,7 +1837,7 @@ func (config *Config) historyChangedFrom(oldConfig *Config) bool {
|
|||||||
config.History.Persistent != oldConfig.History.Persistent
|
config.History.Persistent != oldConfig.History.Persistent
|
||||||
}
|
}
|
||||||
|
|
||||||
func compileGuestRegexp(guestFormat string, casemapping i18n.Casemapping) (standard, folded *regexp.Regexp, err error) {
|
func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, folded *regexp.Regexp, err error) {
|
||||||
if strings.Count(guestFormat, "?") != 0 || strings.Count(guestFormat, "*") != 1 {
|
if strings.Count(guestFormat, "?") != 0 || strings.Count(guestFormat, "*") != 1 {
|
||||||
err = errors.New("guest format must contain 1 '*' and no '?'s")
|
err = errors.New("guest format must contain 1 '*' and no '?'s")
|
||||||
return
|
return
|
||||||
@ -1999,11 +1851,11 @@ func compileGuestRegexp(guestFormat string, casemapping i18n.Casemapping) (stand
|
|||||||
starIndex := strings.IndexByte(guestFormat, '*')
|
starIndex := strings.IndexByte(guestFormat, '*')
|
||||||
initial := guestFormat[:starIndex]
|
initial := guestFormat[:starIndex]
|
||||||
final := guestFormat[starIndex+1:]
|
final := guestFormat[starIndex+1:]
|
||||||
initialFolded, err := i18n.CasefoldWithSetting(initial, casemapping)
|
initialFolded, err := casefoldWithSetting(initial, casemapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
finalFolded, err := i18n.CasefoldWithSetting(final, casemapping)
|
finalFolded, err := casefoldWithSetting(final, casemapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,15 +8,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mungeEnvForTesting(config *Config, env []string, t *testing.T) {
|
|
||||||
for _, envPair := range env {
|
|
||||||
_, _, err := mungeFromEnvironment(config, envPair)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("couldn't apply override `%s`: %v", envPair, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvironmentOverrides(t *testing.T) {
|
func TestEnvironmentOverrides(t *testing.T) {
|
||||||
var config Config
|
var config Config
|
||||||
config.Server.Compatibility.SendUnprefixedSasl = true
|
config.Server.Compatibility.SendUnprefixedSasl = true
|
||||||
@ -25,12 +16,6 @@ func TestEnvironmentOverrides(t *testing.T) {
|
|||||||
config.Accounts.DefaultUserModes = &defaultUserModes
|
config.Accounts.DefaultUserModes = &defaultUserModes
|
||||||
config.Server.WebSockets.AllowedOrigins = []string{"https://www.ircv3.net"}
|
config.Server.WebSockets.AllowedOrigins = []string{"https://www.ircv3.net"}
|
||||||
config.Server.MOTD = "long.motd.txt" // overwrite this
|
config.Server.MOTD = "long.motd.txt" // overwrite this
|
||||||
config.Opers = map[string]*OperConfig{
|
|
||||||
"admin": {
|
|
||||||
Class: "server-admin",
|
|
||||||
Password: "adminpassword",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
env := []string{
|
env := []string{
|
||||||
`USER=shivaram`, // unrelated var
|
`USER=shivaram`, // unrelated var
|
||||||
`ORAGONO_USER=oragono`, // this should be ignored as well
|
`ORAGONO_USER=oragono`, // this should be ignored as well
|
||||||
@ -41,11 +26,13 @@ func TestEnvironmentOverrides(t *testing.T) {
|
|||||||
`ORAGONO__ACCOUNTS__NICK_RESERVATION__ENABLED=true`,
|
`ORAGONO__ACCOUNTS__NICK_RESERVATION__ENABLED=true`,
|
||||||
`ERGO__ACCOUNTS__DEFAULT_USER_MODES="+iR"`,
|
`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}`,
|
`ORAGONO__SERVER__IP_CLOAKING={"enabled": true, "enabled-for-always-on": true, "netname": "irc", "cidr-len-ipv4": 32, "cidr-len-ipv6": 64, "num-bits": 64}`,
|
||||||
`ERGO__OPERS__ADMIN__PASSWORD="newadminpassword"`,
|
|
||||||
`ERGO__OPERS__OPERUSER={"class": "server-admin", "whois-line": "is a server admin", "password": "operpassword"}`,
|
|
||||||
}
|
}
|
||||||
|
for _, envPair := range env {
|
||||||
mungeEnvForTesting(&config, env, t)
|
_, _, err := mungeFromEnvironment(&config, envPair)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't apply override `%s`: %v", envPair, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if config.Network.Name != "example.com" {
|
if config.Network.Name != "example.com" {
|
||||||
t.Errorf("unexpected value of network.name: %s", config.Network.Name)
|
t.Errorf("unexpected value of network.name: %s", config.Network.Name)
|
||||||
@ -81,56 +68,6 @@ func TestEnvironmentOverrides(t *testing.T) {
|
|||||||
if *config.Accounts.DefaultUserModes != "+iR" {
|
if *config.Accounts.DefaultUserModes != "+iR" {
|
||||||
t.Errorf("couldn't override pre-set ptr field")
|
t.Errorf("couldn't override pre-set ptr field")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*config.Opers["admin"]).Password != "newadminpassword" {
|
|
||||||
t.Errorf("couldn't index into map and then overwrite")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (*config.Opers["operuser"]).Password != "operpassword" {
|
|
||||||
t.Errorf("couldn't create new entry in map")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvironmentInitializeNilMap(t *testing.T) {
|
|
||||||
var config Config
|
|
||||||
env := []string{
|
|
||||||
`ERGO__OPERS__OPERUSER={"class": "server-admin", "whois-line": "is a server admin", "password": "operpassword"}`,
|
|
||||||
}
|
|
||||||
|
|
||||||
mungeEnvForTesting(&config, env, t)
|
|
||||||
assertEqual((*config.Opers["operuser"]).Password, "operpassword")
|
|
||||||
|
|
||||||
// try with an initialized but empty map:
|
|
||||||
config.Opers = make(map[string]*OperConfig)
|
|
||||||
mungeEnvForTesting(&config, env, t)
|
|
||||||
assertEqual((*config.Opers["operuser"]).Password, "operpassword")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvironmentCreateNewMap(t *testing.T) {
|
|
||||||
var config Config
|
|
||||||
env := []string{
|
|
||||||
`ERGO__OPERS={"operuser": {"class": "server-admin", "whois-line": "is a server admin", "password": "operpassword"}}`,
|
|
||||||
}
|
|
||||||
|
|
||||||
mungeEnvForTesting(&config, env, t)
|
|
||||||
assertEqual((*config.Opers["operuser"]).Password, "operpassword")
|
|
||||||
|
|
||||||
// try with an initialized but empty map:
|
|
||||||
config.Opers = make(map[string]*OperConfig)
|
|
||||||
mungeEnvForTesting(&config, env, t)
|
|
||||||
assertEqual((*config.Opers["operuser"]).Password, "operpassword")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvironmentNonPointerMap(t *testing.T) {
|
|
||||||
// edge cases that should not panic, even though the results are unusable
|
|
||||||
// since all "field names" get lowercased:
|
|
||||||
var config Config
|
|
||||||
config.Server.AdditionalISupport = map[string]string{"extban": "a"}
|
|
||||||
env := []string{
|
|
||||||
`ERGO__SERVER__ADDITIONAL_ISUPPORT__EXTBAN=~,a`,
|
|
||||||
`ERGO__FAKELAG__COMMAND_BUDGETS__PRIVMSG=10`,
|
|
||||||
}
|
|
||||||
mungeEnvForTesting(&config, env, t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnvironmentOverrideErrors(t *testing.T) {
|
func TestEnvironmentOverrideErrors(t *testing.T) {
|
||||||
@ -139,20 +76,20 @@ func TestEnvironmentOverrideErrors(t *testing.T) {
|
|||||||
config.History.Enabled = true
|
config.History.Enabled = true
|
||||||
|
|
||||||
invalidEnvs := []string{
|
invalidEnvs := []string{
|
||||||
`ERGO__=asdf`,
|
`ORAGONO__=asdf`,
|
||||||
`ERGO__SERVER__=asdf`,
|
`ORAGONO__SERVER__=asdf`,
|
||||||
`ERGO__SERVER____=asdf`,
|
`ORAGONO__SERVER____=asdf`,
|
||||||
`ERGO__NONEXISTENT_KEY=1`,
|
`ORAGONO__NONEXISTENT_KEY=1`,
|
||||||
`ERGO__SERVER__NONEXISTENT_KEY=1`,
|
`ORAGONO__SERVER__NONEXISTENT_KEY=1`,
|
||||||
// invalid yaml:
|
// invalid yaml:
|
||||||
`ERGO__SERVER__IP_CLOAKING__NETNAME="`,
|
`ORAGONO__SERVER__IP_CLOAKING__NETNAME="`,
|
||||||
// invalid type:
|
// invalid type:
|
||||||
`ERGO__SERVER__IP_CLOAKING__NUM_BITS=asdf`,
|
`ORAGONO__SERVER__IP_CLOAKING__NUM_BITS=asdf`,
|
||||||
`ERGO__SERVER__STS=[]`,
|
`ORAGONO__SERVER__STS=[]`,
|
||||||
// index into non-struct:
|
// index into non-struct:
|
||||||
`ERGO__NETWORK__NAME__QUX=1`,
|
`ORAGONO__NETWORK__NAME__QUX=1`,
|
||||||
// private field:
|
// private field:
|
||||||
`ERGO__SERVER__PASSWORDBYTES="asdf"`,
|
`ORAGONO__SERVER__PASSWORDBYTES="asdf"`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, env := range invalidEnvs {
|
for _, env := range invalidEnvs {
|
||||||
|
|||||||
@ -33,7 +33,6 @@ var (
|
|||||||
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
|
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
|
||||||
errAccountUpdateFailed = errors.New(`Error while updating your account information`)
|
errAccountUpdateFailed = errors.New(`Error while updating your account information`)
|
||||||
errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`)
|
errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`)
|
||||||
errAuthRequired = errors.New("You must be logged into an account to do this")
|
|
||||||
errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`)
|
errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`)
|
||||||
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
|
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
|
||||||
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
||||||
@ -82,8 +81,9 @@ var (
|
|||||||
|
|
||||||
// String Errors
|
// String Errors
|
||||||
var (
|
var (
|
||||||
errStringIsEmpty = errors.New("String is empty")
|
errCouldNotStabilize = errors.New("Could not stabilize string while casefolding")
|
||||||
errInvalidCharacter = errors.New("Invalid character")
|
errStringIsEmpty = errors.New("String is empty")
|
||||||
|
errInvalidCharacter = errors.New("Invalid character")
|
||||||
)
|
)
|
||||||
|
|
||||||
type CertKeyError struct {
|
type CertKeyError struct {
|
||||||
|
|||||||
269
irc/getters.go
269
irc/getters.go
@ -7,11 +7,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/caps"
|
"github.com/ergochat/ergo/irc/caps"
|
||||||
"github.com/ergochat/ergo/irc/connection_limits"
|
|
||||||
"github.com/ergochat/ergo/irc/languages"
|
"github.com/ergochat/ergo/irc/languages"
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
@ -519,7 +517,7 @@ func (client *Client) GetReadMarker(cfname string) (result string) {
|
|||||||
t, ok := client.readMarkers[cfname]
|
t, ok := client.readMarkers[cfname]
|
||||||
client.stateMutex.RUnlock()
|
client.stateMutex.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
return fmt.Sprintf("timestamp=%s", t.Format(utils.IRCv3TimestampFormat))
|
return fmt.Sprintf("timestamp=%s", t.Format(IRCv3TimestampFormat))
|
||||||
}
|
}
|
||||||
return "*"
|
return "*"
|
||||||
}
|
}
|
||||||
@ -799,12 +797,10 @@ func (channel *Channel) Settings() (result ChannelSettings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) SetSettings(settings ChannelSettings) {
|
func (channel *Channel) SetSettings(settings ChannelSettings) {
|
||||||
defer channel.MarkDirty(IncludeSettings)
|
|
||||||
|
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
defer channel.stateMutex.Unlock()
|
|
||||||
|
|
||||||
channel.settings = settings
|
channel.settings = settings
|
||||||
|
channel.stateMutex.Unlock()
|
||||||
|
channel.MarkDirty(IncludeSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) setForward(forward string) {
|
func (channel *Channel) setForward(forward string) {
|
||||||
@ -831,262 +827,3 @@ func (channel *Channel) UUID() utils.UUID {
|
|||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
return channel.uuid
|
return channel.uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *Session) isSubscribedTo(key string) bool {
|
|
||||||
session.client.stateMutex.RLock()
|
|
||||||
defer session.client.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
return session.metadataSubscriptions.Has(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (session *Session) SubscribeTo(keys ...string) ([]string, error) {
|
|
||||||
maxSubs := session.client.server.Config().Metadata.MaxSubs
|
|
||||||
|
|
||||||
session.client.stateMutex.Lock()
|
|
||||||
defer session.client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
if session.metadataSubscriptions == nil {
|
|
||||||
session.metadataSubscriptions = make(utils.HashSet[string])
|
|
||||||
}
|
|
||||||
|
|
||||||
var added []string
|
|
||||||
|
|
||||||
for _, k := range keys {
|
|
||||||
if !session.metadataSubscriptions.Has(k) {
|
|
||||||
if len(session.metadataSubscriptions) > maxSubs {
|
|
||||||
return added, errMetadataTooManySubs
|
|
||||||
}
|
|
||||||
added = append(added, k)
|
|
||||||
session.metadataSubscriptions.Add(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return added, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (session *Session) UnsubscribeFrom(keys ...string) []string {
|
|
||||||
session.client.stateMutex.Lock()
|
|
||||||
defer session.client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
var removed []string
|
|
||||||
|
|
||||||
for k := range session.metadataSubscriptions {
|
|
||||||
if slices.Contains(keys, k) {
|
|
||||||
removed = append(removed, k)
|
|
||||||
session.metadataSubscriptions.Remove(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (session *Session) MetadataSubscriptions() utils.HashSet[string] {
|
|
||||||
session.client.stateMutex.Lock()
|
|
||||||
defer session.client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
return maps.Clone(session.metadataSubscriptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) GetMetadata(key string) (string, bool) {
|
|
||||||
channel.stateMutex.RLock()
|
|
||||||
defer channel.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
val, ok := channel.metadata[key]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) SetMetadata(key string, value string, limit int) (updated bool, err error) {
|
|
||||||
defer channel.MarkDirty(IncludeAllAttrs)
|
|
||||||
|
|
||||||
channel.stateMutex.Lock()
|
|
||||||
defer channel.stateMutex.Unlock()
|
|
||||||
|
|
||||||
if channel.metadata == nil {
|
|
||||||
channel.metadata = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, ok := channel.metadata[key]
|
|
||||||
if !ok && len(channel.metadata) >= limit {
|
|
||||||
return false, errLimitExceeded
|
|
||||||
}
|
|
||||||
updated = !ok || value != existing
|
|
||||||
if updated {
|
|
||||||
channel.metadata[key] = value
|
|
||||||
}
|
|
||||||
return updated, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) ListMetadata() map[string]string {
|
|
||||||
channel.stateMutex.RLock()
|
|
||||||
defer channel.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
return maps.Clone(channel.metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) DeleteMetadata(key string) (updated bool) {
|
|
||||||
defer channel.MarkDirty(IncludeAllAttrs)
|
|
||||||
|
|
||||||
channel.stateMutex.Lock()
|
|
||||||
defer channel.stateMutex.Unlock()
|
|
||||||
|
|
||||||
_, updated = channel.metadata[key]
|
|
||||||
if updated {
|
|
||||||
delete(channel.metadata, key)
|
|
||||||
}
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) ClearMetadata() map[string]string {
|
|
||||||
defer channel.MarkDirty(IncludeAllAttrs)
|
|
||||||
channel.stateMutex.Lock()
|
|
||||||
defer channel.stateMutex.Unlock()
|
|
||||||
|
|
||||||
oldMap := channel.metadata
|
|
||||||
channel.metadata = nil
|
|
||||||
|
|
||||||
return oldMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) CountMetadata() int {
|
|
||||||
channel.stateMutex.RLock()
|
|
||||||
defer channel.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
return len(channel.metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) GetMetadata(key string) (string, bool) {
|
|
||||||
client.stateMutex.RLock()
|
|
||||||
defer client.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
val, ok := client.metadata[key]
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) SetMetadata(key string, value string, limit int) (updated bool, err error) {
|
|
||||||
var alwaysOn bool
|
|
||||||
defer func() {
|
|
||||||
if alwaysOn && updated {
|
|
||||||
client.markDirty(IncludeMetadata)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
|
||||||
defer client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
alwaysOn = client.registered && client.alwaysOn
|
|
||||||
|
|
||||||
if client.metadata == nil {
|
|
||||||
client.metadata = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, ok := client.metadata[key]
|
|
||||||
if !ok && len(client.metadata) >= limit {
|
|
||||||
return false, errLimitExceeded
|
|
||||||
}
|
|
||||||
updated = !ok || value != existing
|
|
||||||
if updated {
|
|
||||||
client.metadata[key] = value
|
|
||||||
}
|
|
||||||
return updated, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) UpdateMetadataFromPrereg(preregData map[string]string, limit int) (updates map[string]string) {
|
|
||||||
var alwaysOn bool
|
|
||||||
defer func() {
|
|
||||||
if alwaysOn && len(updates) > 0 {
|
|
||||||
client.markDirty(IncludeMetadata)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
updates = make(map[string]string, len(preregData))
|
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
|
||||||
defer client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
alwaysOn = client.registered && client.alwaysOn
|
|
||||||
|
|
||||||
if client.metadata == nil {
|
|
||||||
client.metadata = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range preregData {
|
|
||||||
// do not overwrite any existing keys
|
|
||||||
_, ok := client.metadata[k]
|
|
||||||
if ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(client.metadata) >= limit {
|
|
||||||
return // we know this is a new key
|
|
||||||
}
|
|
||||||
client.metadata[k] = v
|
|
||||||
updates[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) ListMetadata() map[string]string {
|
|
||||||
client.stateMutex.RLock()
|
|
||||||
defer client.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
return maps.Clone(client.metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) DeleteMetadata(key string) (updated bool) {
|
|
||||||
defer func() {
|
|
||||||
if updated {
|
|
||||||
client.markDirty(IncludeMetadata)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
|
||||||
defer client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
_, updated = client.metadata[key]
|
|
||||||
if updated {
|
|
||||||
delete(client.metadata, key)
|
|
||||||
}
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) ClearMetadata() (oldMap map[string]string) {
|
|
||||||
defer func() {
|
|
||||||
if len(oldMap) > 0 {
|
|
||||||
client.markDirty(IncludeMetadata)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
|
||||||
defer client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
oldMap = client.metadata
|
|
||||||
client.metadata = nil
|
|
||||||
|
|
||||||
return oldMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) CountMetadata() int {
|
|
||||||
client.stateMutex.RLock()
|
|
||||||
defer client.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
return len(client.metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) checkMetadataThrottle() (throttled bool, remainingTime time.Duration) {
|
|
||||||
config := client.server.Config()
|
|
||||||
if !config.Metadata.ClientThrottle.Enabled {
|
|
||||||
return false, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
|
||||||
defer client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
// copy client.metadataThrottle locally and then back for processing
|
|
||||||
var throttle connection_limits.GenericThrottle
|
|
||||||
throttle.ThrottleDetails = client.metadataThrottle
|
|
||||||
throttle.Duration = config.Metadata.ClientThrottle.Duration
|
|
||||||
throttle.Limit = config.Metadata.ClientThrottle.MaxAttempts
|
|
||||||
throttled, remainingTime = throttle.Touch()
|
|
||||||
client.metadataThrottle = throttle.ThrottleDetails
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
432
irc/handlers.go
432
irc/handlers.go
@ -9,13 +9,11 @@ package irc
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -151,8 +149,11 @@ func (server *Server) sendLoginSnomask(nickMask, accountName string) {
|
|||||||
// to indicate that it should be removed from the list
|
// to indicate that it should be removed from the list
|
||||||
func acceptHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func acceptHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
for _, tNick := range strings.Split(msg.Params[0], ",") {
|
for _, tNick := range strings.Split(msg.Params[0], ",") {
|
||||||
tNick, negPrefix := strings.CutPrefix(tNick, "-")
|
add := true
|
||||||
add := !negPrefix
|
if strings.HasPrefix(tNick, "-") {
|
||||||
|
add = false
|
||||||
|
tNick = strings.TrimPrefix(tNick, "-")
|
||||||
|
}
|
||||||
|
|
||||||
target := server.clients.Get(tNick)
|
target := server.clients.Get(tNick)
|
||||||
if target == nil {
|
if target == nil {
|
||||||
@ -700,13 +701,11 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
|||||||
var channel *Channel
|
var channel *Channel
|
||||||
var sequence history.Sequence
|
var sequence history.Sequence
|
||||||
var err error
|
var err error
|
||||||
var disabled, listTargets bool
|
var listTargets bool
|
||||||
var targets []history.TargetListing
|
var targets []history.TargetListing
|
||||||
defer func() {
|
defer func() {
|
||||||
// errors are sent either without a batch, or in a draft/labeled-response batch as usual
|
// errors are sent either without a batch, or in a draft/labeled-response batch as usual
|
||||||
if disabled {
|
if err == utils.ErrInvalidParams {
|
||||||
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("That feature is disabled"))
|
|
||||||
} else if err == utils.ErrInvalidParams {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMS", msg.Params[0], client.t("Invalid parameters"))
|
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMS", msg.Params[0], client.t("Invalid parameters"))
|
||||||
} else if !listTargets && sequence == nil {
|
} else if !listTargets && sequence == nil {
|
||||||
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_TARGET", msg.Params[0], utils.SafeErrorParam(target), client.t("Messages could not be retrieved"))
|
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_TARGET", msg.Params[0], utils.SafeErrorParam(target), client.t("Messages could not be retrieved"))
|
||||||
@ -720,7 +719,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
|||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
name := server.UnfoldName(target.CfName)
|
name := server.UnfoldName(target.CfName)
|
||||||
rb.Add(nil, server.name, "CHATHISTORY", "TARGETS", name,
|
rb.Add(nil, server.name, "CHATHISTORY", "TARGETS", name,
|
||||||
target.Time.Format(utils.IRCv3TimestampFormat))
|
target.Time.Format(IRCv3TimestampFormat))
|
||||||
}
|
}
|
||||||
} else if channel != nil {
|
} else if channel != nil {
|
||||||
channel.replayHistoryItems(rb, items, true)
|
channel.replayHistoryItems(rb, items, true)
|
||||||
@ -732,8 +731,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
|||||||
|
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
maxChathistoryLimit := config.History.ChathistoryMax
|
maxChathistoryLimit := config.History.ChathistoryMax
|
||||||
if !config.History.Enabled || maxChathistoryLimit == 0 {
|
if maxChathistoryLimit == 0 {
|
||||||
disabled = true
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
preposition := strings.ToLower(msg.Params[0])
|
preposition := strings.ToLower(msg.Params[0])
|
||||||
@ -756,7 +754,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
|||||||
msgid, err = history.NormalizeMsgid(value), nil
|
msgid, err = history.NormalizeMsgid(value), nil
|
||||||
return
|
return
|
||||||
} else if identifier == "timestamp" {
|
} else if identifier == "timestamp" {
|
||||||
timestamp, err = time.Parse(utils.IRCv3TimestampFormat, value)
|
timestamp, err = time.Parse(IRCv3TimestampFormat, value)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
timestamp = timestamp.UTC()
|
timestamp = timestamp.UTC()
|
||||||
if timestamp.Before(unixEpoch) {
|
if timestamp.Before(unixEpoch) {
|
||||||
@ -844,12 +842,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if listTargets {
|
if listTargets {
|
||||||
// TARGETS must take time= selectors
|
targets, err = client.listTargets(start, end, limit)
|
||||||
if start.Time.IsZero() || end.Time.IsZero() {
|
|
||||||
err = utils.ErrInvalidParams
|
|
||||||
return
|
|
||||||
}
|
|
||||||
targets, err = client.listTargets(start.Time, end.Time, limit)
|
|
||||||
} else {
|
} else {
|
||||||
channel, sequence, err = server.GetHistorySequence(nil, client, target)
|
channel, sequence, err = server.GetHistorySequence(nil, client, target)
|
||||||
if err != nil || sequence == nil {
|
if err != nil || sequence == nil {
|
||||||
@ -942,9 +935,7 @@ func defconHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||||||
level, err := strconv.Atoi(msg.Params[0])
|
level, err := strconv.Atoi(msg.Params[0])
|
||||||
if err == nil && 1 <= level && level <= 5 {
|
if err == nil && 1 <= level && level <= 5 {
|
||||||
server.SetDefcon(uint32(level))
|
server.SetDefcon(uint32(level))
|
||||||
message := fmt.Sprintf("%s [%s] set DEFCON level to %d", client.Nick(), client.Oper().Name, level)
|
server.snomasks.Send(sno.LocalAnnouncements, fmt.Sprintf("%s [%s] set DEFCON level to %d", client.Nick(), client.Oper().Name, level))
|
||||||
server.logger.Info("server", message)
|
|
||||||
server.snomasks.Send(sno.LocalAnnouncements, message)
|
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), msg.Command, client.t("Invalid DEFCON parameter"))
|
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), msg.Command, client.t("Invalid DEFCON parameter"))
|
||||||
return false
|
return false
|
||||||
@ -2555,19 +2546,8 @@ func operHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
config := server.Config()
|
|
||||||
now := time.Now()
|
|
||||||
nextAllowableAttempt := rb.session.lastOperAttempt.Add(config.Server.OperThrottle)
|
|
||||||
if now.Before(nextAllowableAttempt) {
|
|
||||||
timeLeft := nextAllowableAttempt.Sub(now).Round(time.Millisecond)
|
|
||||||
rb.Add(nil, server.name, ERR_NOOPERHOST, client.Nick(), fmt.Sprintf(client.t("You must wait %v before issuing OPER again"), timeLeft))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
rb.session.lastOperAttempt = now
|
|
||||||
|
|
||||||
// must pass at least one check, and all enabled checks
|
// must pass at least one check, and all enabled checks
|
||||||
var checkPassed, checkFailed, certFailed, passwordFailed bool
|
var checkPassed, checkFailed, passwordFailed bool
|
||||||
oper := server.GetOperator(msg.Params[0])
|
oper := server.GetOperator(msg.Params[0])
|
||||||
if oper != nil {
|
if oper != nil {
|
||||||
if oper.Certfp != "" {
|
if oper.Certfp != "" {
|
||||||
@ -2575,13 +2555,11 @@ func operHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
|||||||
checkPassed = true
|
checkPassed = true
|
||||||
} else {
|
} else {
|
||||||
checkFailed = true
|
checkFailed = true
|
||||||
certFailed = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !checkFailed && oper.Pass != nil {
|
if !checkFailed && oper.Pass != nil {
|
||||||
if len(msg.Params) == 1 {
|
if len(msg.Params) == 1 {
|
||||||
checkFailed = true
|
checkFailed = true
|
||||||
passwordFailed = true
|
|
||||||
} else if bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil {
|
} else if bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil {
|
||||||
checkFailed = true
|
checkFailed = true
|
||||||
passwordFailed = true
|
passwordFailed = true
|
||||||
@ -2592,21 +2570,14 @@ func operHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !checkPassed || checkFailed {
|
if !checkPassed || checkFailed {
|
||||||
rb.Add(nil, server.name, ERR_NOOPERHOST, client.Nick(), client.t("OPER failed; check the server logs for details."))
|
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect"))
|
||||||
|
// #951: only disconnect them if we actually tried to check a password for them
|
||||||
// hopefully not too spammy given the throttling:
|
if passwordFailed {
|
||||||
if oper == nil {
|
client.Quit(client.t("Password incorrect"), rb.session)
|
||||||
server.logger.Info("opers", "OPER failed with invalid oper name", msg.Params[0])
|
return true
|
||||||
} else if certFailed {
|
|
||||||
server.logger.Info("opers", "OPER attempt for", msg.Params[0], "failed with invalid certfp")
|
|
||||||
} else if passwordFailed {
|
|
||||||
server.logger.Info("opers", "OPER attempt for", msg.Params[0], "failed with invalid password")
|
|
||||||
} else {
|
} else {
|
||||||
// should not be possible given config validation
|
return false
|
||||||
server.logger.Info("opers", "OPER attempt for", msg.Params[0], "failed with invalid config")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if oper != nil {
|
if oper != nil {
|
||||||
@ -2799,10 +2770,8 @@ func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||||||
targetmsgid := msg.Params[1]
|
targetmsgid := msg.Params[1]
|
||||||
//clientOnlyTags := msg.ClientOnlyTags()
|
//clientOnlyTags := msg.ClientOnlyTags()
|
||||||
var reason string
|
var reason string
|
||||||
var reasonPresent bool
|
|
||||||
if len(msg.Params) > 2 {
|
if len(msg.Params) > 2 {
|
||||||
reason = msg.Params[2]
|
reason = msg.Params[2]
|
||||||
reasonPresent = true
|
|
||||||
}
|
}
|
||||||
var members []*Client // members of a channel, or both parties of a PM
|
var members []*Client // members of a channel, or both parties of a PM
|
||||||
var canDelete CanDelete
|
var canDelete CanDelete
|
||||||
@ -2815,7 +2784,7 @@ func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||||||
if target[0] == '#' {
|
if target[0] == '#' {
|
||||||
channel := server.channels.Get(target)
|
channel := server.channels.Get(target)
|
||||||
if channel == nil {
|
if channel == nil {
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "INVALID_TARGET", utils.SafeErrorParam(target), client.t("No such channel"))
|
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
members = channel.Members()
|
members = channel.Members()
|
||||||
@ -2844,16 +2813,10 @@ func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := server.DeleteMessage(target, targetmsgid, accountName)
|
err := server.DeleteMessage(target, targetmsgid, accountName)
|
||||||
switch err {
|
if err == errNoop {
|
||||||
case history.ErrNotFound:
|
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "UNKNOWN_MSGID", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("This message does not exist or is too old"))
|
rb.Add(nil, server.name, "FAIL", "REDACT", "UNKNOWN_MSGID", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("This message does not exist or is too old"))
|
||||||
return false
|
return false
|
||||||
case history.ErrDisallowed:
|
} else if err != nil {
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("You are not authorized to delete this message"))
|
|
||||||
return false
|
|
||||||
case nil:
|
|
||||||
// OK
|
|
||||||
default:
|
|
||||||
isOper := client.HasRoleCapabs("history")
|
isOper := client.HasRoleCapabs("history")
|
||||||
if isOper {
|
if isOper {
|
||||||
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
||||||
@ -2868,8 +2831,7 @@ func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||||||
// now we have to remove it from the buffer of the client who sent the REDACT command
|
// now we have to remove it from the buffer of the client who sent the REDACT command
|
||||||
err := server.DeleteMessage(client.Nick(), targetmsgid, accountName)
|
err := server.DeleteMessage(client.Nick(), targetmsgid, accountName)
|
||||||
|
|
||||||
// ErrNotFound is expected if both clients are using persistent history
|
if err != nil {
|
||||||
if err != nil && err != history.ErrNotFound {
|
|
||||||
client.server.logger.Error("internal", fmt.Sprintf("Private message %s is not deletable by %s from their own buffer's even though we just deleted it from %s's. This is a bug, please report it in details.", targetmsgid, client.Nick(), target), client.Nick())
|
client.server.logger.Error("internal", fmt.Sprintf("Private message %s is not deletable by %s from their own buffer's even though we just deleted it from %s's. This is a bug, please report it in details.", targetmsgid, client.Nick(), target), client.Nick())
|
||||||
isOper := client.HasRoleCapabs("history")
|
isOper := client.HasRoleCapabs("history")
|
||||||
if isOper {
|
if isOper {
|
||||||
@ -2883,11 +2845,7 @@ func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||||||
for _, member := range members {
|
for _, member := range members {
|
||||||
for _, session := range member.Sessions() {
|
for _, session := range member.Sessions() {
|
||||||
if session.capabilities.Has(caps.MessageRedaction) {
|
if session.capabilities.Has(caps.MessageRedaction) {
|
||||||
if reasonPresent {
|
session.sendFromClientInternal(false, time, msgid, details.nickMask, details.accountName, isBot, nil, "REDACT", target, targetmsgid, reason)
|
||||||
session.sendFromClientInternal(false, time, msgid, details.nickMask, details.accountName, isBot, nil, "REDACT", target, targetmsgid, reason)
|
|
||||||
} else {
|
|
||||||
session.sendFromClientInternal(false, time, msgid, details.nickMask, details.accountName, isBot, nil, "REDACT", target, targetmsgid)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// If we wanted to send a fallback to clients which do not support
|
// If we wanted to send a fallback to clients which do not support
|
||||||
// draft/message-redaction, we would do it from here.
|
// draft/message-redaction, we would do it from here.
|
||||||
@ -2952,23 +2910,11 @@ func quitHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
|||||||
|
|
||||||
// REGISTER < account | * > < email | * > <password>
|
// REGISTER < account | * > < email | * > <password>
|
||||||
func registerHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) {
|
func registerHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) {
|
||||||
var accountName string
|
accountName := client.Nick()
|
||||||
if client.registered {
|
if accountName == "*" {
|
||||||
accountName = client.Nick()
|
|
||||||
} else {
|
|
||||||
accountName = client.preregNick
|
accountName = client.preregNick
|
||||||
}
|
}
|
||||||
|
|
||||||
config := server.Config()
|
|
||||||
if client.registered && config.Accounts.NickReservation.ForceGuestFormat {
|
|
||||||
matches := config.Accounts.NickReservation.guestRegexp.FindStringSubmatch(accountName)
|
|
||||||
if matches == nil || len(matches) < 2 {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "REGISTER", "INVALID_USERNAME", utils.SafeErrorParam(accountName), client.t("Username invalid or not given"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
accountName = matches[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg.Params[0] {
|
switch msg.Params[0] {
|
||||||
case "*", accountName:
|
case "*", accountName:
|
||||||
// ok
|
// ok
|
||||||
@ -2985,6 +2931,7 @@ func registerHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config := server.Config()
|
||||||
if !config.Accounts.Registration.Enabled {
|
if !config.Accounts.Registration.Enabled {
|
||||||
rb.Add(nil, server.name, "FAIL", "REGISTER", "DISALLOWED", accountName, client.t("Account registration is disabled"))
|
rb.Add(nil, server.name, "FAIL", "REGISTER", "DISALLOWED", accountName, client.t("Account registration is disabled"))
|
||||||
return
|
return
|
||||||
@ -3030,7 +2977,7 @@ func registerHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
|
|||||||
announcePendingReg(client, rb, accountName)
|
announcePendingReg(client, rb, accountName)
|
||||||
}
|
}
|
||||||
case errAccountAlreadyRegistered, errAccountAlreadyUnregistered, errAccountMustHoldNick:
|
case errAccountAlreadyRegistered, errAccountAlreadyUnregistered, errAccountMustHoldNick:
|
||||||
rb.Add(nil, server.name, "FAIL", "REGISTER", "ACCOUNT_EXISTS", accountName, client.t("Username is already registered or otherwise unavailable"))
|
rb.Add(nil, server.name, "FAIL", "REGISTER", "USERNAME_EXISTS", accountName, client.t("Username is already registered or otherwise unavailable"))
|
||||||
case errAccountBadPassphrase:
|
case errAccountBadPassphrase:
|
||||||
rb.Add(nil, server.name, "FAIL", "REGISTER", "INVALID_PASSWORD", accountName, client.t("Password was invalid"))
|
rb.Add(nil, server.name, "FAIL", "REGISTER", "INVALID_PASSWORD", accountName, client.t("Password was invalid"))
|
||||||
default:
|
default:
|
||||||
@ -3108,14 +3055,14 @@ func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
|
|||||||
|
|
||||||
// "MARKREAD client set command": MARKREAD <target> <timestamp>
|
// "MARKREAD client set command": MARKREAD <target> <timestamp>
|
||||||
readTimestamp := strings.TrimPrefix(msg.Params[1], "timestamp=")
|
readTimestamp := strings.TrimPrefix(msg.Params[1], "timestamp=")
|
||||||
readTime, err := time.Parse(utils.IRCv3TimestampFormat, readTimestamp)
|
readTime, err := time.Parse(IRCv3TimestampFormat, readTimestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rb.Add(nil, server.name, "FAIL", "MARKREAD", "INVALID_PARAMS", utils.SafeErrorParam(readTimestamp), client.t("Invalid timestamp"))
|
rb.Add(nil, server.name, "FAIL", "MARKREAD", "INVALID_PARAMS", utils.SafeErrorParam(readTimestamp), client.t("Invalid timestamp"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
readTime = readTime.UTC()
|
readTime = readTime.UTC()
|
||||||
result := client.SetReadMarker(cftarget, readTime)
|
result := client.SetReadMarker(cftarget, readTime)
|
||||||
readTimestamp = fmt.Sprintf("timestamp=%s", result.Format(utils.IRCv3TimestampFormat))
|
readTimestamp = fmt.Sprintf("timestamp=%s", result.Format(IRCv3TimestampFormat))
|
||||||
// inform the originating session whether it was a success or a no-op:
|
// inform the originating session whether it was a success or a no-op:
|
||||||
rb.Add(nil, server.name, "MARKREAD", unfoldedTarget, readTimestamp)
|
rb.Add(nil, server.name, "MARKREAD", unfoldedTarget, readTimestamp)
|
||||||
if result.Equal(readTime) {
|
if result.Equal(readTime) {
|
||||||
@ -3127,9 +3074,7 @@ func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if client.clearClearablePushMessage(cftarget, readTime) {
|
if client.clearClearablePushMessage(cftarget, readTime) {
|
||||||
markreadPushMessage := ircmsg.MakeMessage(nil, server.name, "MARKREAD", unfoldedTarget, readTimestamp)
|
line, err := webpush.MakePushLine(time.Now().UTC(), "*", server.name, "MARKREAD", unfoldedTarget, readTimestamp)
|
||||||
markreadPushMessage.SetTag("time", time.Now().UTC().Format(utils.IRCv3TimestampFormat))
|
|
||||||
line, err := webpush.MakePushLine(markreadPushMessage)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
client.dispatchPushMessage(pushMessage{
|
client.dispatchPushMessage(pushMessage{
|
||||||
msg: line,
|
msg: line,
|
||||||
@ -3144,293 +3089,6 @@ func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// METADATA <target> <subcommand> [<and so on>...]
|
|
||||||
func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) {
|
|
||||||
config := server.Config()
|
|
||||||
if !config.Metadata.Enabled {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "FORBIDDEN", utils.SafeErrorParam(msg.Params[0]), client.t("Metadata is disabled on this server"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
subcommand := strings.ToLower(msg.Params[1])
|
|
||||||
needsKey := subcommand == "set" || subcommand == "get" || subcommand == "sub" || subcommand == "unsub"
|
|
||||||
if needsKey && len(msg.Params) < 3 {
|
|
||||||
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), msg.Command, client.t("Not enough parameters"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch subcommand {
|
|
||||||
case "sub", "unsub", "subs":
|
|
||||||
// these are session-local and function the same whether or not the client is registered
|
|
||||||
return metadataSubsHandler(client, subcommand, msg.Params, rb)
|
|
||||||
case "set", "clear":
|
|
||||||
if config.Metadata.OperatorOnlyModification && !client.HasRoleCapabs("metadata") {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "FORBIDDEN", utils.SafeErrorParam(msg.Params[0]), client.t("Only server operators can modify metadata"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
case "get", "list", "sync":
|
|
||||||
if client.registered {
|
|
||||||
return metadataRegisteredHandler(client, config, subcommand, msg.Params, rb)
|
|
||||||
} else {
|
|
||||||
return metadataUnregisteredHandler(client, config, subcommand, msg.Params, rb)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "SUBCOMMAND_INVALID", utils.SafeErrorParam(msg.Params[1]), client.t("Invalid subcommand"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// metadataRegisteredHandler handles metadata-modifying commands from registered clients
|
|
||||||
func metadataRegisteredHandler(client *Client, config *Config, subcommand string, params []string, rb *ResponseBuffer) (exiting bool) {
|
|
||||||
server := client.server
|
|
||||||
target := params[0]
|
|
||||||
|
|
||||||
noKeyPerms := func(key string) {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NO_PERMISSION", target, key, client.t("You do not have permission to perform this action"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if target == "*" {
|
|
||||||
target = client.Nick()
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetObj MetadataHaver
|
|
||||||
var targetClient *Client
|
|
||||||
var targetChannel *Channel
|
|
||||||
if strings.HasPrefix(target, "#") {
|
|
||||||
targetChannel = server.channels.Get(target)
|
|
||||||
if targetChannel != nil {
|
|
||||||
targetObj = targetChannel
|
|
||||||
target = targetChannel.Name() // canonicalize case
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
targetClient = server.clients.Get(target)
|
|
||||||
if targetClient != nil {
|
|
||||||
targetObj = targetClient
|
|
||||||
target = targetClient.Nick() // canonicalize case
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if targetObj == nil {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_TARGET", target, client.t("Invalid metadata target"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch subcommand {
|
|
||||||
case "set":
|
|
||||||
key := params[2]
|
|
||||||
if metadataKeyIsEvil(key) {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !metadataCanIEditThisKey(client, targetObj, key) {
|
|
||||||
noKeyPerms(key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// only rate limit clients changing their own metadata:
|
|
||||||
// channel metadata updates are not any more costly than a PRIVMSG
|
|
||||||
if client == targetClient {
|
|
||||||
if throttled, remainingTime := client.checkMetadataThrottle(); throttled {
|
|
||||||
retryAfter := strconv.Itoa(int(remainingTime.Seconds()) + 1)
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "RATE_LIMITED",
|
|
||||||
target, utils.SafeErrorParam(key), retryAfter,
|
|
||||||
fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime.Round(time.Millisecond)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(params) > 3 {
|
|
||||||
value := params[3]
|
|
||||||
|
|
||||||
config := client.server.Config()
|
|
||||||
if failMsg := metadataValueIsEvil(config, key, value); failMsg != "" {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t(failMsg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updated, err := targetObj.SetMetadata(key, value, config.Metadata.MaxKeys)
|
|
||||||
if err != nil {
|
|
||||||
// errLimitExceeded is the only possible error
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", client.t("Too many metadata keys"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// echo the value to the client whether or not there was a real update
|
|
||||||
rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, "*", value)
|
|
||||||
if updated {
|
|
||||||
notifySubscribers(server, rb.session, targetObj, target, key, value, true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if updated := targetObj.DeleteMetadata(key); updated {
|
|
||||||
notifySubscribers(server, rb.session, targetObj, target, key, "", false)
|
|
||||||
rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key deleted"))
|
|
||||||
} else {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NOT_SET", utils.SafeErrorParam(key), client.t("Metadata key not set"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "get":
|
|
||||||
if !metadataCanISeeThisTarget(client, targetObj) {
|
|
||||||
noKeyPerms("*")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
batchId := rb.StartNestedBatch("metadata", target)
|
|
||||||
defer rb.EndNestedBatch(batchId)
|
|
||||||
|
|
||||||
for _, key := range params[2:] {
|
|
||||||
if metadataKeyIsEvil(key) {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val, ok := targetObj.GetMetadata(key)
|
|
||||||
if !ok {
|
|
||||||
rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key is not set"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
visibility := "*"
|
|
||||||
rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, visibility, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "list":
|
|
||||||
playMetadataList(rb, client.Nick(), target, targetObj.ListMetadata())
|
|
||||||
|
|
||||||
case "clear":
|
|
||||||
if !metadataCanIEditThisTarget(client, targetObj) {
|
|
||||||
noKeyPerms("*")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
values := targetObj.ClearMetadata()
|
|
||||||
|
|
||||||
playMetadataList(rb, client.Nick(), target, values)
|
|
||||||
|
|
||||||
case "sync":
|
|
||||||
if targetChannel != nil {
|
|
||||||
syncChannelMetadata(server, rb, targetChannel)
|
|
||||||
}
|
|
||||||
if targetClient != nil {
|
|
||||||
syncClientMetadata(server, rb, targetClient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// metadataUnregisteredHandler handles metadata-modifying commands for pre-connection-registration
|
|
||||||
// clients. these operations act on a session-local buffer; if/when the client completes registration,
|
|
||||||
// they are applied to the final Client object (possibly a different client if there was a reattach)
|
|
||||||
// on a best-effort basis.
|
|
||||||
func metadataUnregisteredHandler(client *Client, config *Config, subcommand string, params []string, rb *ResponseBuffer) (exiting bool) {
|
|
||||||
server := client.server
|
|
||||||
if params[0] != "*" {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NO_PERMISSION", utils.SafeErrorParam(params[0]), "*", client.t("You can only modify your own metadata before completing connection registration"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch subcommand {
|
|
||||||
case "set":
|
|
||||||
if rb.session.metadataPreregVals == nil {
|
|
||||||
rb.session.metadataPreregVals = make(map[string]string)
|
|
||||||
}
|
|
||||||
key := params[2]
|
|
||||||
if metadataKeyIsEvil(key) {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(params) >= 4 {
|
|
||||||
value := params[3]
|
|
||||||
// enforce a sane limit on prereg keys. we don't need to enforce the exact limit,
|
|
||||||
// that will be done when applying the buffer after registration
|
|
||||||
if len(rb.session.metadataPreregVals) > config.Metadata.MaxKeys {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", client.t("Too many metadata keys"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if failMsg := metadataValueIsEvil(config, key, value); failMsg != "" {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t(failMsg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rb.session.metadataPreregVals[key] = value
|
|
||||||
rb.Add(nil, server.name, RPL_KEYVALUE, "*", "*", key, "*", value)
|
|
||||||
} else {
|
|
||||||
// unset
|
|
||||||
_, present := rb.session.metadataPreregVals[key]
|
|
||||||
if present {
|
|
||||||
delete(rb.session.metadataPreregVals, key)
|
|
||||||
rb.Add(nil, server.name, RPL_KEYNOTSET, "*", "*", key, client.t("Key deleted"))
|
|
||||||
} else {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NOT_SET", utils.SafeErrorParam(key), client.t("Metadata key not set"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "list":
|
|
||||||
playMetadataList(rb, "*", "*", rb.session.metadataPreregVals)
|
|
||||||
case "clear":
|
|
||||||
oldMetadata := rb.session.metadataPreregVals
|
|
||||||
rb.session.metadataPreregVals = nil
|
|
||||||
playMetadataList(rb, "*", "*", oldMetadata)
|
|
||||||
case "sync":
|
|
||||||
rb.Add(nil, server.name, RPL_METADATASYNCLATER, "*", utils.SafeErrorParam(params[1]), "60") // lol
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// metadataSubsHandler handles subscription-related commands;
|
|
||||||
// these are handled the same whether the client is registered or not
|
|
||||||
func metadataSubsHandler(client *Client, subcommand string, params []string, rb *ResponseBuffer) (exiting bool) {
|
|
||||||
server := client.server
|
|
||||||
switch subcommand {
|
|
||||||
case "sub":
|
|
||||||
keys := params[2:]
|
|
||||||
for _, key := range keys {
|
|
||||||
if metadataKeyIsEvil(key) {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
added, err := rb.session.SubscribeTo(keys...)
|
|
||||||
if err == errMetadataTooManySubs {
|
|
||||||
bad := keys[len(added)] // get the key that broke the camel's back
|
|
||||||
rb.Add(nil, server.name, "FAIL", "METADATA", "TOO_MANY_SUBS", utils.SafeErrorParam(bad), client.t("Too many subscriptions"))
|
|
||||||
}
|
|
||||||
|
|
||||||
lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBOK) - len(client.Nick()) - 10
|
|
||||||
|
|
||||||
chunked := utils.ChunkifyParams(slices.Values(added), lineLength)
|
|
||||||
for _, line := range chunked {
|
|
||||||
params := append([]string{client.Nick()}, line...)
|
|
||||||
rb.Add(nil, server.name, RPL_METADATASUBOK, params...)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "unsub":
|
|
||||||
keys := params[2:]
|
|
||||||
removed := rb.session.UnsubscribeFrom(keys...)
|
|
||||||
|
|
||||||
lineLength := MaxLineLen - len(server.name) - len(RPL_METADATAUNSUBOK) - len(client.Nick()) - 10
|
|
||||||
chunked := utils.ChunkifyParams(slices.Values(removed), lineLength)
|
|
||||||
for _, line := range chunked {
|
|
||||||
params := append([]string{client.Nick()}, line...)
|
|
||||||
rb.Add(nil, server.name, RPL_METADATAUNSUBOK, params...)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "subs":
|
|
||||||
lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBS) - len(client.Nick()) - 10
|
|
||||||
|
|
||||||
subs := rb.session.MetadataSubscriptions()
|
|
||||||
|
|
||||||
batchID := rb.StartNestedBatch("metadata-subs")
|
|
||||||
defer rb.EndNestedBatch(batchID)
|
|
||||||
|
|
||||||
chunked := utils.ChunkifyParams(maps.Keys(subs), lineLength)
|
|
||||||
for _, line := range chunked {
|
|
||||||
params := append([]string{client.Nick()}, line...)
|
|
||||||
rb.Add(nil, server.name, RPL_METADATASUBS, params...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// REHASH
|
// REHASH
|
||||||
func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
nick := client.Nick()
|
nick := client.Nick()
|
||||||
@ -3971,16 +3629,15 @@ func webircHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||||||
// WEBPUSH <subcommand> <endpoint> [key]
|
// WEBPUSH <subcommand> <endpoint> [key]
|
||||||
func webpushHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func webpushHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
subcommand := strings.ToUpper(msg.Params[0])
|
subcommand := strings.ToUpper(msg.Params[0])
|
||||||
endpoint := msg.Params[1]
|
|
||||||
|
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
if !config.WebPush.Enabled {
|
if !config.WebPush.Enabled {
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "FORBIDDEN", subcommand, utils.SafeErrorParam(endpoint), client.t("Web push is disabled"))
|
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "FORBIDDEN", subcommand, client.t("Web push is disabled"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.Account() == "" {
|
if client.Account() == "" {
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "FORBIDDEN", subcommand, utils.SafeErrorParam(endpoint), client.t("You must be logged in to receive push messages"))
|
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "FORBIDDEN", subcommand, client.t("You must be logged in to receive push messages"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3990,25 +3647,26 @@ func webpushHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp
|
|||||||
// should disable web push. However, as a sanity check, disallow enabling it over a Tor
|
// should disable web push. However, as a sanity check, disallow enabling it over a Tor
|
||||||
// connection:
|
// connection:
|
||||||
if rb.session.isTor {
|
if rb.session.isTor {
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "FORBIDDEN", subcommand, utils.SafeErrorParam(endpoint), client.t("Web push cannot be enabled over Tor"))
|
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "FORBIDDEN", subcommand, client.t("Web push cannot be enabled over Tor"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endpoint := msg.Params[1]
|
||||||
|
|
||||||
if err := webpush.SanityCheckWebPushEndpoint(endpoint); err != nil {
|
if err := webpush.SanityCheckWebPushEndpoint(endpoint); err != nil {
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", subcommand, utils.SafeErrorParam(endpoint), client.t("Invalid web push URL"))
|
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", subcommand, client.t("Invalid web push URL"))
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch subcommand {
|
switch subcommand {
|
||||||
case "REGISTER":
|
case "REGISTER":
|
||||||
// allow web push enable even if they are not always-on (they just won't get push messages)
|
// allow web push enable even if they are not always-on (they just won't get push messages)
|
||||||
if len(msg.Params) < 3 {
|
if len(msg.Params) < 3 {
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", subcommand, utils.SafeErrorParam(endpoint), client.t("Insufficient parameters for WEBPUSH REGISTER"))
|
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", subcommand, client.t("Insufficient parameters for WEBPUSH REGISTER"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
keys, err := webpush.DecodeSubscriptionKeys(msg.Params[2])
|
keys, err := webpush.DecodeSubscriptionKeys(msg.Params[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", subcommand, utils.SafeErrorParam(endpoint), client.t("Invalid subscription keys for WEBPUSH REGISTER"))
|
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", subcommand, client.t("Invalid subscription keys for WEBPUSH REGISTER"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if client.refreshPushSubscription(endpoint, keys) {
|
if client.refreshPushSubscription(endpoint, keys) {
|
||||||
@ -4031,22 +3689,20 @@ func webpushHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp
|
|||||||
rb.Add(nil, server.name, "WARN", "WEBPUSH", "PERSISTENCE_REQUIRED", client.t("You have enabled push notifications, but you will not receive them unless you become always-on. Try: /msg nickserv set always-on true"))
|
rb.Add(nil, server.name, "WARN", "WEBPUSH", "PERSISTENCE_REQUIRED", client.t("You have enabled push notifications, but you will not receive them unless you become always-on. Try: /msg nickserv set always-on true"))
|
||||||
}
|
}
|
||||||
} else if err == errLimitExceeded {
|
} else if err == errLimitExceeded {
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "MAX_REGISTRATIONS", "REGISTER", utils.SafeErrorParam(endpoint), client.t("You have too many push subscriptions already"))
|
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "FORBIDDEN", "REGISTER", client.t("You have too many push subscriptions already"))
|
||||||
} else {
|
} else {
|
||||||
server.logger.Error("webpush", "Failed to add webpush subscription", err.Error())
|
server.logger.Error("webpush", "Failed to add webpush subscription", err.Error())
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INTERNAL_ERROR", "REGISTER", utils.SafeErrorParam(endpoint), client.t("An error occurred"))
|
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INTERNAL_ERROR", "REGISTER", client.t("An error occurred"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
server.logger.Debug("webpush", "WEBPUSH REGISTER failed validation", endpoint, err.Error())
|
server.logger.Debug("webpush", "WEBPUSH REGISTER failed validation", endpoint, err.Error())
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", "REGISTER", utils.SafeErrorParam(endpoint), client.t("Test push message failed to send"))
|
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", "REGISTER", client.t("Test push message failed to send"))
|
||||||
}
|
}
|
||||||
case "UNREGISTER":
|
case "UNREGISTER":
|
||||||
client.deletePushSubscription(endpoint, true)
|
client.deletePushSubscription(endpoint, true)
|
||||||
rb.session.webPushEndpoint = ""
|
rb.session.webPushEndpoint = ""
|
||||||
// this always succeeds
|
// this always succeeds
|
||||||
rb.Add(nil, server.name, "WEBPUSH", "UNREGISTER", endpoint)
|
rb.Add(nil, server.name, "WEBPUSH", "UNREGISTER", endpoint)
|
||||||
default:
|
|
||||||
rb.Add(nil, server.name, "FAIL", "WEBPUSH", "INVALID_PARAMS", subcommand, utils.SafeErrorParam(endpoint), client.t("Unknown subcommand"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -4462,9 +4118,9 @@ func zncHandler(server *Server, client *Client, msg ircmsg.Message, rb *Response
|
|||||||
// fake handler for unknown commands
|
// fake handler for unknown commands
|
||||||
func unknownCommandHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func unknownCommandHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
var message string
|
var message string
|
||||||
if trimmedCmd, initialSlash := strings.CutPrefix(msg.Command, "/"); initialSlash {
|
if strings.HasPrefix(msg.Command, "/") {
|
||||||
message = fmt.Sprintf(client.t("Unknown command; if you are using /QUOTE, the correct syntax is /QUOTE %[1]s, not /QUOTE %[2]s"),
|
message = fmt.Sprintf(client.t("Unknown command; if you are using /QUOTE, the correct syntax is /QUOTE %[1]s, not /QUOTE %[2]s"),
|
||||||
trimmedCmd, msg.Command)
|
strings.TrimPrefix(msg.Command, "/"), msg.Command)
|
||||||
} else {
|
} else {
|
||||||
message = client.t("Unknown command")
|
message = client.t("Unknown command")
|
||||||
}
|
}
|
||||||
|
|||||||
15
irc/help.go
15
irc/help.go
@ -238,10 +238,11 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
|
|||||||
"history": {
|
"history": {
|
||||||
text: `HISTORY <target> [limit]
|
text: `HISTORY <target> [limit]
|
||||||
|
|
||||||
Replay message history. <target> can be a channel name or a nickname you have
|
Replay message history. <target> can be a channel name, "me" to replay direct
|
||||||
direct message history with. [limit] can be either an integer (the maximum
|
message history, or a nickname to replay another client's direct message
|
||||||
number of messages to replay), or a time duration like 10m or 1h (the time
|
history (they must be logged into the same account as you). [limit] can be
|
||||||
window within which to replay messages).`,
|
either an integer (the maximum number of messages to replay), or a time
|
||||||
|
duration like 10m or 1h (the time window within which to replay messages).`,
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
text: `INFO
|
text: `INFO
|
||||||
@ -338,12 +339,6 @@ command is processed by that server.`,
|
|||||||
MARKREAD updates an IRCv3 read message marker. It is not intended for use by
|
MARKREAD updates an IRCv3 read message marker. It is not intended for use by
|
||||||
end users. For more details, see the latest draft of the read-marker
|
end users. For more details, see the latest draft of the read-marker
|
||||||
specification.`,
|
specification.`,
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
text: `METADATA <target> <subcommand> [<everything else>...]
|
|
||||||
|
|
||||||
Retrieve and meddle with metadata for the given target.
|
|
||||||
Have a look at https://ircv3.net/specs/extensions/metadata for interesting technical information.`,
|
|
||||||
},
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
text: `MODE <target> [<modestring> [<mode arguments>...]]
|
text: `MODE <target> [<modestring> [<mode arguments>...]]
|
||||||
|
|||||||
@ -1,126 +0,0 @@
|
|||||||
// Copyright (c) 2025 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package history
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrDisallowed = errors.New("disallowed")
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Database is an interface for persistent history storage backends.
|
|
||||||
type Database interface {
|
|
||||||
// Close closes the database connection and releases resources.
|
|
||||||
io.Closer
|
|
||||||
|
|
||||||
// AddChannelItem adds a history item for a channel.
|
|
||||||
// target is the casefolded channel name.
|
|
||||||
// account is the sender's casefolded account name ("" for no account).
|
|
||||||
AddChannelItem(target string, item Item, account string) error
|
|
||||||
|
|
||||||
// AddDirectMessage adds a history item for a direct message.
|
|
||||||
// All identifiers are casefolded; account identifiers are "" for no account.
|
|
||||||
AddDirectMessage(sender, senderAccount, recipient, recipientAccount string, item Item) error
|
|
||||||
|
|
||||||
// DeleteMsgid deletes a message by its msgid.
|
|
||||||
// accountName is the unfolded account name, or "*" to skip
|
|
||||||
// account validation
|
|
||||||
DeleteMsgid(msgid, accountName string) error
|
|
||||||
|
|
||||||
// MakeSequence creates a Sequence for querying history.
|
|
||||||
// target is the primary target (channel or account), casefolded.
|
|
||||||
// correspondent is the casefolded DM correspondent (empty for channels).
|
|
||||||
// cutoff is the earliest time to include in results.
|
|
||||||
MakeSequence(target, correspondent string, cutoff time.Time) Sequence
|
|
||||||
|
|
||||||
// ListChannels returns the timestamp of the latest message in each
|
|
||||||
// of the given channels (specified as casefolded names).
|
|
||||||
ListChannels(cfchannels []string) (results []TargetListing, err error)
|
|
||||||
|
|
||||||
// ListCorrespondents lists the DM correspondents associated with an account,
|
|
||||||
// in order to implement CHATHISTORY TARGETS.
|
|
||||||
ListCorrespondents(cftarget string, start, end time.Time, limit int) ([]TargetListing, error)
|
|
||||||
|
|
||||||
// these are for theoretical GDPR compliance, not actual chat functionality,
|
|
||||||
// and are not essential:
|
|
||||||
|
|
||||||
// Forget enqueues an account (casefolded) for message deletion.
|
|
||||||
// This is used for GDPR-style "right to be forgotten" requests.
|
|
||||||
// The actual deletion happens asynchronously.
|
|
||||||
Forget(account string)
|
|
||||||
|
|
||||||
// Export exports all messages for an account (casefolded) to the given writer.
|
|
||||||
Export(account string, writer io.Writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
type noopDatabase struct{}
|
|
||||||
|
|
||||||
// NewNoopDatabase returns a Database implementation that does nothing.
|
|
||||||
func NewNoopDatabase() Database {
|
|
||||||
return noopDatabase{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopDatabase) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopDatabase) AddChannelItem(target string, item Item, account string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopDatabase) AddDirectMessage(sender, senderAccount, recipient, recipientAccount string, item Item) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopDatabase) DeleteMsgid(msgid, accountName string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopDatabase) Forget(account string) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopDatabase) Export(account string, writer io.Writer) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopDatabase) ListChannels(cfchannels []string) (results []TargetListing, err error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopDatabase) ListCorrespondents(target string, start, end time.Time, limit int) (results []TargetListing, err error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopDatabase) MakeSequence(target, correspondent string, cutoff time.Time) Sequence {
|
|
||||||
return noopSequence{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// noopSequence is a no-op implementation of Sequence.
|
|
||||||
// XXX: this should never be accessed, because if persistent history is disabled,
|
|
||||||
// we should always be working with a bufferSequence instead. But we might as well
|
|
||||||
// be defensive in case there's an edge case where (noopDatabase).MakeSequence ends
|
|
||||||
// up getting called.
|
|
||||||
type noopSequence struct{}
|
|
||||||
|
|
||||||
func (n noopSequence) Between(start, end Selector, limit int) (results []Item, err error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopSequence) Around(start Selector, limit int) (results []Item, err error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopSequence) Cutoff() time.Time {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n noopSequence) Ephemeral() bool {
|
|
||||||
return false // we're pretending to be an empty database
|
|
||||||
}
|
|
||||||
@ -230,8 +230,10 @@ func (list *Buffer) allCorrespondents() (results []TargetListing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// list DM correspondents, as one input to CHATHISTORY TARGETS
|
// list DM correspondents, as one input to CHATHISTORY TARGETS
|
||||||
func (list *Buffer) ListCorrespondents(start, end time.Time, limit int) (results []TargetListing, err error) {
|
func (list *Buffer) listCorrespondents(start, end Selector, cutoff time.Time, limit int) (results []TargetListing, err error) {
|
||||||
after, before, ascending := MinMaxAsc(start, end, time.Time{})
|
after := start.Time
|
||||||
|
before := end.Time
|
||||||
|
after, before, ascending := MinMaxAsc(after, before, cutoff)
|
||||||
|
|
||||||
correspondents := list.allCorrespondents()
|
correspondents := list.allCorrespondents()
|
||||||
if len(correspondents) == 0 {
|
if len(correspondents) == 0 {
|
||||||
@ -298,6 +300,10 @@ func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, er
|
|||||||
return GenericAround(seq, start, limit)
|
return GenericAround(seq, start, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (seq *bufferSequence) ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error) {
|
||||||
|
return seq.list.listCorrespondents(start, end, seq.cutoff, limit)
|
||||||
|
}
|
||||||
|
|
||||||
func (seq *bufferSequence) Cutoff() time.Time {
|
func (seq *bufferSequence) Cutoff() time.Time {
|
||||||
return seq.cutoff
|
return seq.cutoff
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,8 @@ type Sequence interface {
|
|||||||
Between(start, end Selector, limit int) (results []Item, err error)
|
Between(start, end Selector, limit int) (results []Item, err error)
|
||||||
Around(start Selector, limit int) (results []Item, err error)
|
Around(start Selector, limit int) (results []Item, err error)
|
||||||
|
|
||||||
|
ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error)
|
||||||
|
|
||||||
// this are weird hacks that violate the encapsulation of Sequence to some extent;
|
// this are weird hacks that violate the encapsulation of Sequence to some extent;
|
||||||
// Cutoff() returns the cutoff time for other code to use (it returns the zero time
|
// Cutoff() returns the cutoff time for other code to use (it returns the zero time
|
||||||
// if none is set), and Ephemeral() returns whether the backing store is in-memory
|
// if none is set), and Ephemeral() returns whether the backing store is in-memory
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
// Copyright (c) 2020 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package history
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 123 / '{' is the magic number that means JSON;
|
|
||||||
// if we want to do a binary encoding later, we just have to add different magic version numbers
|
|
||||||
|
|
||||||
func MarshalItem(item *Item) (result []byte, err error) {
|
|
||||||
return json.Marshal(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnmarshalItem(data []byte, result *Item) (err error) {
|
|
||||||
return json.Unmarshal(data, result)
|
|
||||||
}
|
|
||||||
@ -164,7 +164,7 @@ func histservExportHandler(service *ircService, server *Server, client *Client,
|
|||||||
|
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
// don't include the account name in the filename because of escaping concerns
|
// don't include the account name in the filename because of escaping concerns
|
||||||
filename := fmt.Sprintf("%s-%s.json", utils.GenerateSecretToken(), time.Now().UTC().Format(utils.IRCv3TimestampFormat))
|
filename := fmt.Sprintf("%s-%s.json", utils.GenerateSecretToken(), time.Now().UTC().Format(IRCv3TimestampFormat))
|
||||||
pathname := config.getOutputPath(filename)
|
pathname := config.getOutputPath(filename)
|
||||||
outfile, err := os.Create(pathname)
|
outfile, err := os.Create(pathname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -1,79 +0,0 @@
|
|||||||
package i18n
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Casemapping represents a set of algorithm for case normalization
|
|
||||||
// and confusables prevention for IRC identifiers (nicknames and channel names)
|
|
||||||
type Casemapping uint
|
|
||||||
|
|
||||||
const (
|
|
||||||
// "precis" is the default / zero value:
|
|
||||||
// casefolding/validation: PRECIS + ircd restrictions (like no *)
|
|
||||||
// confusables detection: standard skeleton algorithm
|
|
||||||
CasemappingPRECIS Casemapping = iota
|
|
||||||
// "ascii" is the traditional ircd behavior:
|
|
||||||
// casefolding/validation: must be pure ASCII and follow ircd restrictions, ASCII lowercasing
|
|
||||||
// confusables detection: none
|
|
||||||
CasemappingASCII
|
|
||||||
// "permissive" is an insecure mode:
|
|
||||||
// casefolding/validation: arbitrary unicodes that follow ircd restrictions, unicode casefolding
|
|
||||||
// confusables detection: standard skeleton algorithm (which may be ineffective
|
|
||||||
// over the larger set of permitted identifiers)
|
|
||||||
CasemappingPermissive
|
|
||||||
// rfc1459 is a legacy mapping as defined here: https://modern.ircdocs.horse/#casemapping-parameter
|
|
||||||
CasemappingRFC1459
|
|
||||||
// rfc1459-strict is a legacy mapping as defined here: https://modern.ircdocs.horse/#casemapping-parameter
|
|
||||||
CasemappingRFC1459Strict
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errInvalidCharacter = errors.New("Invalid character")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cm *Casemapping) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {
|
|
||||||
var orig string
|
|
||||||
if err = unmarshal(&orig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result Casemapping
|
|
||||||
switch strings.ToLower(orig) {
|
|
||||||
case "ascii":
|
|
||||||
result = CasemappingASCII
|
|
||||||
case "precis", "rfc7613", "rfc8265":
|
|
||||||
result = CasemappingPRECIS
|
|
||||||
case "permissive", "fun":
|
|
||||||
result = CasemappingPermissive
|
|
||||||
case "rfc1459":
|
|
||||||
result = CasemappingRFC1459
|
|
||||||
case "rfc1459-strict":
|
|
||||||
result = CasemappingRFC1459Strict
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid casemapping value: %s", orig)
|
|
||||||
}
|
|
||||||
*cm = result
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPrintableASCII(str string) bool {
|
|
||||||
for i := 0; i < len(str); i++ {
|
|
||||||
// allow space here because it's technically printable;
|
|
||||||
// it will be disallowed later by CasefoldName/CasefoldChannel
|
|
||||||
chr := str[i]
|
|
||||||
if chr < ' ' || chr > '~' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func foldASCII(str string) (result string, err error) {
|
|
||||||
if !isPrintableASCII(str) {
|
|
||||||
return "", errInvalidCharacter
|
|
||||||
}
|
|
||||||
return strings.ToLower(str), nil
|
|
||||||
}
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
//go:build i18n
|
|
||||||
|
|
||||||
package i18n
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ergochat/confusables"
|
|
||||||
"golang.org/x/text/cases"
|
|
||||||
"golang.org/x/text/secure/precis"
|
|
||||||
"golang.org/x/text/unicode/norm"
|
|
||||||
"golang.org/x/text/width"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Enabled = true
|
|
||||||
|
|
||||||
// 1.x configurations don't have a server.casemapping field, but
|
|
||||||
// expect PRECIS. however, technically it's not this value that
|
|
||||||
// causes them to get PRECIS, it's that PRECIS is the zero value of
|
|
||||||
// Casemapping (so that's how the YAML deserializes when the field
|
|
||||||
// is missing).
|
|
||||||
DefaultCasemapping = CasemappingPRECIS
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// reviving the old ergonomadic nickname regex:
|
|
||||||
// in permissive mode, allow arbitrary letters, numbers, punctuation, and symbols
|
|
||||||
permissiveCharsRegex = regexp.MustCompile(`^[\pL\pN\pP\pS]*$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// String Errors
|
|
||||||
var (
|
|
||||||
errCouldNotStabilize = errors.New("Could not stabilize string while casefolding")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Each pass of PRECIS casefolding is a composition of idempotent operations,
|
|
||||||
// but not idempotent itself. Therefore, the spec says "do it four times and hope
|
|
||||||
// it converges" (lolwtf). Golang's PRECIS implementation has a "repeat" option,
|
|
||||||
// which provides this functionality, but unfortunately it's not exposed publicly.
|
|
||||||
func iterateFolding(profile *precis.Profile, oldStr string) (str string, err error) {
|
|
||||||
str = oldStr
|
|
||||||
// follow the stabilizing rules laid out here:
|
|
||||||
// https://tools.ietf.org/html/draft-ietf-precis-7564bis-10.html#section-7
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
str, err = profile.CompareKey(str)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if oldStr == str {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
oldStr = str
|
|
||||||
}
|
|
||||||
if oldStr != str {
|
|
||||||
return "", errCouldNotStabilize
|
|
||||||
}
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func foldPRECIS(str string) (result string, err error) {
|
|
||||||
return iterateFolding(precis.UsernameCaseMapped, str)
|
|
||||||
}
|
|
||||||
|
|
||||||
func foldPermissive(str string) (result string, err error) {
|
|
||||||
if !permissiveCharsRegex.MatchString(str) {
|
|
||||||
return "", errInvalidCharacter
|
|
||||||
}
|
|
||||||
// YOLO
|
|
||||||
str = norm.NFD.String(str)
|
|
||||||
str = cases.Fold().String(str)
|
|
||||||
str = norm.NFD.String(str)
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
rfc1459Replacer = strings.NewReplacer("[", "{", "]", "}", "\\", "|", "~", "^")
|
|
||||||
rfc1459StrictReplacer = strings.NewReplacer("[", "{", "]", "}", "\\", "|")
|
|
||||||
)
|
|
||||||
|
|
||||||
func foldRFC1459(str string, strict bool) (result string, err error) {
|
|
||||||
asciiFold, err := foldASCII(str)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
replacer := rfc1459Replacer
|
|
||||||
if strict {
|
|
||||||
replacer = rfc1459StrictReplacer
|
|
||||||
}
|
|
||||||
return replacer.Replace(asciiFold), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CasefoldWithSetting(str string, setting Casemapping) (string, error) {
|
|
||||||
switch setting {
|
|
||||||
default:
|
|
||||||
return foldPRECIS(str)
|
|
||||||
case CasemappingASCII:
|
|
||||||
return foldASCII(str)
|
|
||||||
case CasemappingPermissive:
|
|
||||||
return foldPermissive(str)
|
|
||||||
case CasemappingRFC1459:
|
|
||||||
return foldRFC1459(str, false)
|
|
||||||
case CasemappingRFC1459Strict:
|
|
||||||
return foldRFC1459(str, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skeleton produces a canonicalized identifier that tries to catch
|
|
||||||
// homoglyphic / confusable identifiers. It's a tweaked version of the TR39
|
|
||||||
// skeleton algorithm. We apply the skeleton algorithm first and only then casefold,
|
|
||||||
// because casefolding first would lose some information about visual confusability.
|
|
||||||
// This has the weird consequence that the skeleton is not a function of the
|
|
||||||
// casefolded identifier --- therefore it must always be computed
|
|
||||||
// from the original (unfolded) identifier and stored/tracked separately from the
|
|
||||||
// casefolded identifier.
|
|
||||||
func Skeleton(name string) (string, error) {
|
|
||||||
// XXX the confusables table includes some, but not all, fullwidth->standard
|
|
||||||
// mappings for latin characters. do a pass of explicit width folding,
|
|
||||||
// same as PRECIS:
|
|
||||||
name = width.Fold.String(name)
|
|
||||||
|
|
||||||
name = confusables.SkeletonTweaked(name)
|
|
||||||
|
|
||||||
// internationalized lowercasing for skeletons; this is much more lenient than
|
|
||||||
// Casefold. In particular, skeletons are expected to mix scripts (which may
|
|
||||||
// violate the bidi rule). We also don't care if they contain runes
|
|
||||||
// that are disallowed by PRECIS, because every identifier must independently
|
|
||||||
// pass PRECIS --- we are just further canonicalizing the skeleton.
|
|
||||||
return cases.Fold().String(name), nil
|
|
||||||
}
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
//go:build i18n
|
|
||||||
|
|
||||||
package i18n
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func validFoldTester(first, second string, equal bool, folder func(string) (string, error), t *testing.T) {
|
|
||||||
firstFolded, err := folder(first)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
secondFolded, err := folder(second)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
foundEqual := firstFolded == secondFolded
|
|
||||||
if foundEqual != equal {
|
|
||||||
t.Errorf("%s and %s: expected equality %t, but got %t", first, second, equal, foundEqual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFoldPermissive(t *testing.T) {
|
|
||||||
tester := func(first, second string, equal bool) {
|
|
||||||
validFoldTester(first, second, equal, foldPermissive, t)
|
|
||||||
}
|
|
||||||
tester("SHIVARAM", "shivaram", true)
|
|
||||||
tester("shIvaram", "shivaraM", true)
|
|
||||||
tester("shivaram", "DAN-", false)
|
|
||||||
tester("dolph🐬n", "DOLPH🐬n", true)
|
|
||||||
tester("dolph🐬n", "dolph💻n", false)
|
|
||||||
tester("9FRONT", "9front", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFoldPermissiveInvalid(t *testing.T) {
|
|
||||||
_, err := foldPermissive("a\tb")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("whitespace should be invalid in identifiers")
|
|
||||||
}
|
|
||||||
_, err = foldPermissive("a\x00b")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("the null byte should be invalid in identifiers")
|
|
||||||
}
|
|
||||||
_, err = foldPermissive("a b")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("space should be invalid in identifiers")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFoldPermissiveNormalization(t *testing.T) {
|
|
||||||
tester := func(first, second string, equal bool) {
|
|
||||||
validFoldTester(first, second, equal, foldPermissive, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case folding should work on non-ASCII letters
|
|
||||||
tester("Ω", "ω", true) // Greek capital/small omega
|
|
||||||
tester("Ñoño", "ñoño", true) // Spanish precomposed tilde-n, upper vs lower
|
|
||||||
tester("中文", "中文", true) // CJK (no case distinction)
|
|
||||||
tester("中文", "English", false) // different scripts, not equal
|
|
||||||
|
|
||||||
// NFC-encoded input: "É" (U+00C9) and "é" (U+00E9) should fold equal
|
|
||||||
// NFD normalization before case folding ensures composed chars are handled
|
|
||||||
tester("\u00c9l\u00e8ve", "\u00e9l\u00e8ve", true) // Élève vs élève
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFoldASCII(t *testing.T) {
|
|
||||||
tester := func(first, second string, equal bool) {
|
|
||||||
validFoldTester(first, second, equal, foldASCII, t)
|
|
||||||
}
|
|
||||||
tester("shivaram", "SHIVARAM", true)
|
|
||||||
tester("X|Y", "x|y", true)
|
|
||||||
tester("a != b", "A != B", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFoldASCIIInvalid(t *testing.T) {
|
|
||||||
_, err := foldASCII("\x01")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("control characters should be invalid in identifiers")
|
|
||||||
}
|
|
||||||
_, err = foldASCII("\x7F")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("control characters should be invalid in identifiers")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFoldRFC1459(t *testing.T) {
|
|
||||||
folder := func(str string) (string, error) {
|
|
||||||
return foldRFC1459(str, false)
|
|
||||||
}
|
|
||||||
tester := func(first, second string, equal bool) {
|
|
||||||
validFoldTester(first, second, equal, folder, t)
|
|
||||||
}
|
|
||||||
tester("shivaram", "SHIVARAM", true)
|
|
||||||
tester("shivaram[a]", "shivaram{a}", true)
|
|
||||||
tester("shivaram\\a]", "shivaram{a}", false)
|
|
||||||
tester("shivaram\\a]", "shivaram|a}", true)
|
|
||||||
tester("shivaram~a]", "shivaram^a}", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFoldRFC1459Strict(t *testing.T) {
|
|
||||||
folder := func(str string) (string, error) {
|
|
||||||
return foldRFC1459(str, true)
|
|
||||||
}
|
|
||||||
tester := func(first, second string, equal bool) {
|
|
||||||
validFoldTester(first, second, equal, folder, t)
|
|
||||||
}
|
|
||||||
tester("shivaram", "SHIVARAM", true)
|
|
||||||
tester("shivaram[a]", "shivaram{a}", true)
|
|
||||||
tester("shivaram\\a]", "shivaram{a}", false)
|
|
||||||
tester("shivaram\\a]", "shivaram|a}", true)
|
|
||||||
tester("shivaram~a]", "shivaram^a}", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSkeleton(t *testing.T) {
|
|
||||||
skeleton := func(str string) string {
|
|
||||||
skel, err := Skeleton(str)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
return skel
|
|
||||||
}
|
|
||||||
|
|
||||||
if skeleton("warning") == skeleton("waming") {
|
|
||||||
t.Errorf("Oragono shouldn't consider rn confusable with m")
|
|
||||||
}
|
|
||||||
|
|
||||||
if skeleton("Phi|ip") != "philip" {
|
|
||||||
t.Errorf("but we still consider pipe confusable with l")
|
|
||||||
}
|
|
||||||
|
|
||||||
if skeleton("smt") != skeleton("smt") {
|
|
||||||
t.Errorf("fullwidth characters should skeletonize to plain old ascii characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
if skeleton("SMT") != skeleton("smt") {
|
|
||||||
t.Errorf("after skeletonizing, we should casefold")
|
|
||||||
}
|
|
||||||
|
|
||||||
if skeleton("smt") != skeleton("smt") {
|
|
||||||
t.Errorf("our friend lover successfully tricked the skeleton algorithm!")
|
|
||||||
}
|
|
||||||
|
|
||||||
if skeleton("еvan") != "evan" {
|
|
||||||
t.Errorf("we must protect against cyrillic homoglyph attacks")
|
|
||||||
}
|
|
||||||
|
|
||||||
if skeleton("еmily") != skeleton("emily") {
|
|
||||||
t.Errorf("we must protect against cyrillic homoglyph attacks")
|
|
||||||
}
|
|
||||||
|
|
||||||
if skeleton("РОТАТО") != "potato" {
|
|
||||||
t.Errorf("we must protect against cyrillic homoglyph attacks")
|
|
||||||
}
|
|
||||||
|
|
||||||
// should not raise an error:
|
|
||||||
skeleton("けらんぐ")
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
//go:build !i18n
|
|
||||||
|
|
||||||
package i18n
|
|
||||||
|
|
||||||
const (
|
|
||||||
Enabled = false
|
|
||||||
|
|
||||||
DefaultCasemapping = CasemappingASCII
|
|
||||||
)
|
|
||||||
|
|
||||||
func CasefoldWithSetting(str string, setting Casemapping) (string, error) {
|
|
||||||
return foldASCII(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Skeleton(str string) (string, error) {
|
|
||||||
// identity function is fine because we independently case-normalize in Casefold
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
@ -99,13 +99,8 @@ func (nl *NetListener) serve() {
|
|||||||
// hand off the connection
|
// hand off the connection
|
||||||
wConn, ok := conn.(*utils.WrappedConn)
|
wConn, ok := conn.(*utils.WrappedConn)
|
||||||
if ok {
|
if ok {
|
||||||
if wConn.ProxyError == nil {
|
confirmProxyData(wConn, "", "", "", nl.server.Config())
|
||||||
confirmProxyData(wConn, "", "", "", nl.server.Config())
|
go nl.server.RunClient(NewIRCStreamConn(wConn))
|
||||||
go nl.server.RunClient(NewIRCStreamConn(wConn))
|
|
||||||
} else {
|
|
||||||
nl.server.logger.Error("internal", "PROXY protocol error", nl.addr, wConn.ProxyError.Error())
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
nl.server.logger.Error("internal", "invalid connection type", nl.addr)
|
nl.server.logger.Error("internal", "invalid connection type", nl.addr)
|
||||||
}
|
}
|
||||||
@ -190,13 +185,6 @@ func (wl *WSListener) handle(w http.ResponseWriter, r *http.Request) {
|
|||||||
conn.Close()
|
conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if wConn.ProxyError != nil {
|
|
||||||
// actually the connection is likely corrupted, so probably Upgrade()
|
|
||||||
// would have already failed
|
|
||||||
wl.server.logger.Error("internal", "PROXY protocol error on websocket", wl.addr, wConn.ProxyError.Error())
|
|
||||||
conn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmProxyData(wConn, remoteAddr, xff, xfp, config)
|
confirmProxyData(wConn, remoteAddr, xff, xfp, config)
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@ type MessageCache struct {
|
|||||||
|
|
||||||
func addAllTags(msg *ircmsg.Message, tags map[string]string, serverTime time.Time, msgid, accountName string, isBot bool) {
|
func addAllTags(msg *ircmsg.Message, tags map[string]string, serverTime time.Time, msgid, accountName string, isBot bool) {
|
||||||
msg.UpdateTags(tags)
|
msg.UpdateTags(tags)
|
||||||
msg.SetTag("time", serverTime.Format(utils.IRCv3TimestampFormat))
|
msg.SetTag("time", serverTime.Format(IRCv3TimestampFormat))
|
||||||
if accountName != "*" {
|
if accountName != "*" {
|
||||||
msg.SetTag("account", accountName)
|
msg.SetTag("account", accountName)
|
||||||
}
|
}
|
||||||
|
|||||||
174
irc/metadata.go
174
irc/metadata.go
@ -1,174 +0,0 @@
|
|||||||
package irc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"iter"
|
|
||||||
"maps"
|
|
||||||
"regexp"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/caps"
|
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// metadata key + value need to be relayable on a single IRC RPL_KEYVALUE line
|
|
||||||
maxCombinedMetadataLenBytes = 350
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errMetadataTooManySubs = errors.New("too many subscriptions")
|
|
||||||
errMetadataNotFound = errors.New("key not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
type MetadataHaver interface {
|
|
||||||
SetMetadata(key string, value string, limit int) (updated bool, err error)
|
|
||||||
GetMetadata(key string) (string, bool)
|
|
||||||
DeleteMetadata(key string) (updated bool)
|
|
||||||
ListMetadata() map[string]string
|
|
||||||
ClearMetadata() map[string]string
|
|
||||||
CountMetadata() int
|
|
||||||
}
|
|
||||||
|
|
||||||
func notifySubscribers(server *Server, session *Session, targetObj MetadataHaver, targetName, key, value string, set bool) {
|
|
||||||
var recipientSessions iter.Seq[*Session]
|
|
||||||
|
|
||||||
switch target := targetObj.(type) {
|
|
||||||
case *Client:
|
|
||||||
// TODO this case is expensive and might warrant rate-limiting
|
|
||||||
friends := target.FriendsMonitors(caps.Metadata)
|
|
||||||
// broadcast metadata update to other connected sessions
|
|
||||||
for _, s := range target.Sessions() {
|
|
||||||
friends.Add(s)
|
|
||||||
}
|
|
||||||
recipientSessions = maps.Keys(friends)
|
|
||||||
case *Channel:
|
|
||||||
recipientSessions = target.sessionsWithCaps(caps.Metadata)
|
|
||||||
default:
|
|
||||||
return // impossible
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastMetadataUpdate(server, recipientSessions, session, targetName, key, value, set)
|
|
||||||
}
|
|
||||||
|
|
||||||
func broadcastMetadataUpdate(server *Server, sessions iter.Seq[*Session], originator *Session, target, key, value string, set bool) {
|
|
||||||
for s := range sessions {
|
|
||||||
// don't notify the session that made the change
|
|
||||||
if s == originator || !s.isSubscribedTo(key) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if set {
|
|
||||||
s.Send(nil, server.name, "METADATA", target, key, "*", value)
|
|
||||||
} else {
|
|
||||||
s.Send(nil, server.name, "METADATA", target, key, "*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncClientMetadata(server *Server, rb *ResponseBuffer, target *Client) {
|
|
||||||
batchId := rb.StartNestedBatch("metadata", target.Nick())
|
|
||||||
defer rb.EndNestedBatch(batchId)
|
|
||||||
|
|
||||||
subs := rb.session.MetadataSubscriptions()
|
|
||||||
values := target.ListMetadata()
|
|
||||||
for k, v := range values {
|
|
||||||
if subs.Has(k) {
|
|
||||||
visibility := "*"
|
|
||||||
rb.Add(nil, server.name, "METADATA", target.Nick(), k, visibility, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) {
|
|
||||||
batchId := rb.StartNestedBatch("metadata", channel.Name())
|
|
||||||
defer rb.EndNestedBatch(batchId)
|
|
||||||
|
|
||||||
subs := rb.session.MetadataSubscriptions()
|
|
||||||
chname := channel.Name()
|
|
||||||
|
|
||||||
values := channel.ListMetadata()
|
|
||||||
for k, v := range values {
|
|
||||||
if subs.Has(k) {
|
|
||||||
visibility := "*"
|
|
||||||
rb.Add(nil, server.name, "METADATA", chname, k, visibility, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, client := range channel.Members() {
|
|
||||||
values := client.ListMetadata()
|
|
||||||
for k, v := range values {
|
|
||||||
if subs.Has(k) {
|
|
||||||
visibility := "*"
|
|
||||||
rb.Add(nil, server.name, "METADATA", client.Nick(), k, visibility, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func playMetadataList(rb *ResponseBuffer, nick, target string, values map[string]string) {
|
|
||||||
batchId := rb.StartNestedBatch("metadata", target)
|
|
||||||
defer rb.EndNestedBatch(batchId)
|
|
||||||
|
|
||||||
for key, val := range values {
|
|
||||||
visibility := "*"
|
|
||||||
rb.Add(nil, rb.session.client.server.name, RPL_KEYVALUE, nick, target, key, visibility, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func playMetadataVerbBatch(rb *ResponseBuffer, target string, values map[string]string) {
|
|
||||||
batchId := rb.StartNestedBatch("metadata", target)
|
|
||||||
defer rb.EndNestedBatch(batchId)
|
|
||||||
|
|
||||||
for key, val := range values {
|
|
||||||
visibility := "*"
|
|
||||||
rb.Add(nil, rb.session.client.server.name, "METADATA", target, key, visibility, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var validMetadataKeyRegexp = regexp.MustCompile("^[a-z0-9_./-]+$")
|
|
||||||
|
|
||||||
func metadataKeyIsEvil(key string) bool {
|
|
||||||
return !validMetadataKeyRegexp.MatchString(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func metadataValueIsEvil(config *Config, key, value string) (failMsg string) {
|
|
||||||
if !globalUtf8EnforcementSetting && !utf8.ValidString(value) {
|
|
||||||
return `METADATA values must be UTF-8`
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(key)+len(value) > maxCombinedMetadataLenBytes ||
|
|
||||||
(config.Metadata.MaxValueBytes > 0 && len(value) > config.Metadata.MaxValueBytes) {
|
|
||||||
|
|
||||||
return `Value is too long`
|
|
||||||
}
|
|
||||||
|
|
||||||
return "" // success
|
|
||||||
}
|
|
||||||
|
|
||||||
func metadataCanIEditThisKey(client *Client, targetObj MetadataHaver, key string) bool {
|
|
||||||
// no key-specific logic as yet
|
|
||||||
return metadataCanIEditThisTarget(client, targetObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
func metadataCanIEditThisTarget(client *Client, targetObj MetadataHaver) bool {
|
|
||||||
switch target := targetObj.(type) {
|
|
||||||
case *Client:
|
|
||||||
return client == target || client.HasRoleCapabs("metadata")
|
|
||||||
case *Channel:
|
|
||||||
return target.ClientIsAtLeast(client, modes.Operator) || client.HasRoleCapabs("metadata")
|
|
||||||
default:
|
|
||||||
return false // impossible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func metadataCanISeeThisTarget(client *Client, targetObj MetadataHaver) bool {
|
|
||||||
switch target := targetObj.(type) {
|
|
||||||
case *Client:
|
|
||||||
return true
|
|
||||||
case *Channel:
|
|
||||||
return target.hasClient(client) || client.HasRoleCapabs("metadata")
|
|
||||||
default:
|
|
||||||
return false // impossible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
package irc
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestKeyCheck(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
input string
|
|
||||||
isEvil bool
|
|
||||||
}{
|
|
||||||
{"ImNormalButIHaveCaps", true},
|
|
||||||
{"imnormalandidonthavecaps", false},
|
|
||||||
{"ergo.chat/vendor-extension", false},
|
|
||||||
{"", true},
|
|
||||||
{":imevil", true},
|
|
||||||
{"im:evil", true},
|
|
||||||
{"key£with$not%allowed^chars", true},
|
|
||||||
{"key.thats_completely/normal-and.fine", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range cases {
|
|
||||||
if metadataKeyIsEvil(c.input) != c.isEvil {
|
|
||||||
t.Errorf("%s should have returned %v. but it didn't. so that's not great", c.input, c.isEvil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -251,11 +251,9 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
|||||||
switch change.Op {
|
switch change.Op {
|
||||||
case modes.Add:
|
case modes.Add:
|
||||||
val, err := strconv.Atoi(change.Arg)
|
val, err := strconv.Atoi(change.Arg)
|
||||||
if err == nil && val > 0 {
|
if err == nil {
|
||||||
channel.setUserLimit(val)
|
channel.setUserLimit(val)
|
||||||
applied = append(applied, change)
|
applied = append(applied, change)
|
||||||
} else {
|
|
||||||
rb.Add(nil, client.server.name, ERR_INVALIDMODEPARAM, details.nick, chname, string(change.Mode), utils.SafeErrorParam(change.Arg), client.t("+l user limit value must be an integer between 1 and 2147483647, expressed in base 10"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case modes.Remove:
|
case modes.Remove:
|
||||||
|
|||||||
@ -28,21 +28,17 @@ func (mm *MonitorManager) Initialize() {
|
|||||||
|
|
||||||
// AddMonitors adds clients using extended-monitor monitoring `client`'s nick to the passed user set.
|
// AddMonitors adds clients using extended-monitor monitoring `client`'s nick to the passed user set.
|
||||||
func (manager *MonitorManager) AddMonitors(users utils.HashSet[*Session], cfnick string, capabs ...caps.Capability) {
|
func (manager *MonitorManager) AddMonitors(users utils.HashSet[*Session], cfnick string, capabs ...caps.Capability) {
|
||||||
// technically, we should check extended-monitor here, but it's not really necessary
|
|
||||||
// since clients will ignore AWAY, ACCOUNT, CHGHOST, and SETNAME for users
|
|
||||||
// they're not tracking
|
|
||||||
|
|
||||||
manager.RLock()
|
manager.RLock()
|
||||||
defer manager.RUnlock()
|
defer manager.RUnlock()
|
||||||
for session := range manager.watchedby[cfnick] {
|
for session := range manager.watchedby[cfnick] {
|
||||||
if session.capabilities.HasAll(capabs...) {
|
if session.capabilities.Has(caps.ExtendedMonitor) && session.capabilities.HasAll(capabs...) {
|
||||||
users.Add(session)
|
users.Add(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlertAbout alerts everyone monitoring `client`'s nick that `client` is now {on,off}line.
|
// AlertAbout alerts everyone monitoring `client`'s nick that `client` is now {on,off}line.
|
||||||
func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool, client *Client) {
|
func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool) {
|
||||||
var watchers []*Session
|
var watchers []*Session
|
||||||
// safely copy the list of clients watching our nick
|
// safely copy the list of clients watching our nick
|
||||||
manager.RLock()
|
manager.RLock()
|
||||||
@ -56,21 +52,8 @@ func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool, clie
|
|||||||
command = RPL_MONONLINE
|
command = RPL_MONONLINE
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadata map[string]string
|
|
||||||
if online && client != nil {
|
|
||||||
metadata = client.ListMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, session := range watchers {
|
for _, session := range watchers {
|
||||||
session.Send(nil, session.client.server.name, command, session.client.Nick(), nick)
|
session.Send(nil, session.client.server.name, command, session.client.Nick(), nick)
|
||||||
|
|
||||||
if metadata != nil && session.capabilities.Has(caps.Metadata) {
|
|
||||||
for key := range session.MetadataSubscriptions() {
|
|
||||||
if val, ok := metadata[key]; ok {
|
|
||||||
session.Send(nil, client.server.name, "METADATA", nick, key, "*", val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,12 +7,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// maximum length in bytes of any message target (nickname or channel name) in its
|
|
||||||
// canonicalized (i.e., casefolded) state:
|
|
||||||
MaxTargetLength = 64
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// these are intended to be written directly into the config file:
|
// these are intended to be written directly into the config file:
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
//go:build mysql
|
|
||||||
|
|
||||||
// Copyright (c) 2020 Shivaram Lingamneni
|
// Copyright (c) 2020 Shivaram Lingamneni
|
||||||
// released under the MIT license
|
// released under the MIT license
|
||||||
|
|
||||||
@ -9,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
@ -24,9 +23,14 @@ import (
|
|||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDisallowed = errors.New("disallowed")
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Enabled is true when MySQL support is compiled in
|
// maximum length in bytes of any message target (nickname or channel name) in its
|
||||||
Enabled = true
|
// canonicalized (i.e., casefolded) state:
|
||||||
|
MaxTargetLength = 64
|
||||||
|
|
||||||
// latest schema of the db
|
// latest schema of the db
|
||||||
latestDbSchema = "2"
|
latestDbSchema = "2"
|
||||||
@ -60,16 +64,10 @@ type MySQL struct {
|
|||||||
trackAccountMessages atomic.Uint32
|
trackAccountMessages atomic.Uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ history.Database = (*MySQL)(nil)
|
func (mysql *MySQL) Initialize(logger *logger.Manager, config Config) {
|
||||||
|
|
||||||
func NewMySQLDatabase(logger *logger.Manager, config Config) (*MySQL, error) {
|
|
||||||
var mysql MySQL
|
|
||||||
|
|
||||||
mysql.logger = logger
|
mysql.logger = logger
|
||||||
mysql.wakeForgetter = make(chan e, 1)
|
mysql.wakeForgetter = make(chan e, 1)
|
||||||
mysql.SetConfig(config)
|
mysql.SetConfig(config)
|
||||||
|
|
||||||
return &mysql, mysql.open()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) SetConfig(config Config) {
|
func (mysql *MySQL) SetConfig(config Config) {
|
||||||
@ -91,7 +89,7 @@ func (mysql *MySQL) getExpireTime() (expireTime time.Duration) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MySQL) open() (err error) {
|
func (m *MySQL) Open() (err error) {
|
||||||
var address string
|
var address string
|
||||||
if m.config.SocketPath != "" {
|
if m.config.SocketPath != "" {
|
||||||
address = fmt.Sprintf("unix(%s)", m.config.SocketPath)
|
address = fmt.Sprintf("unix(%s)", m.config.SocketPath)
|
||||||
@ -130,7 +128,7 @@ func (m *MySQL) open() (err error) {
|
|||||||
|
|
||||||
func (mysql *MySQL) fixSchemas() (err error) {
|
func (mysql *MySQL) fixSchemas() (err error) {
|
||||||
_, err = mysql.db.Exec(`CREATE TABLE IF NOT EXISTS metadata (
|
_, err = mysql.db.Exec(`CREATE TABLE IF NOT EXISTS metadata (
|
||||||
key_name VARCHAR(32) PRIMARY KEY,
|
key_name VARCHAR(32) primary key,
|
||||||
value VARCHAR(32) NOT NULL
|
value VARCHAR(32) NOT NULL
|
||||||
) CHARSET=ascii COLLATE=ascii_bin;`)
|
) CHARSET=ascii COLLATE=ascii_bin;`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -138,17 +136,17 @@ func (mysql *MySQL) fixSchemas() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var schema string
|
var schema string
|
||||||
err = mysql.db.QueryRow(`SELECT value FROM metadata WHERE key_name = ?;`, keySchemaVersion).Scan(&schema)
|
err = mysql.db.QueryRow(`select value from metadata where key_name = ?;`, keySchemaVersion).Scan(&schema)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
err = mysql.createTables()
|
err = mysql.createTables()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = mysql.db.Exec(`INSERT INTO metadata (key_name, value) VALUES (?, ?);`, keySchemaVersion, latestDbSchema)
|
_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaVersion, latestDbSchema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = mysql.db.Exec(`INSERT INTO metadata (key_name, value) VALUES (?, ?);`, keySchemaMinorVersion, latestDbMinorVersion)
|
_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaMinorVersion, latestDbMinorVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -161,7 +159,7 @@ func (mysql *MySQL) fixSchemas() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var minorVersion string
|
var minorVersion string
|
||||||
err = mysql.db.QueryRow(`SELECT value FROM metadata WHERE key_name = ?;`, keySchemaMinorVersion).Scan(&minorVersion)
|
err = mysql.db.QueryRow(`select value from metadata where key_name = ?;`, keySchemaMinorVersion).Scan(&minorVersion)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
// XXX for now, the only minor version upgrade is the account tracking tables
|
// XXX for now, the only minor version upgrade is the account tracking tables
|
||||||
err = mysql.createComplianceTables()
|
err = mysql.createComplianceTables()
|
||||||
@ -172,7 +170,7 @@ func (mysql *MySQL) fixSchemas() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = mysql.db.Exec(`INSERT INTO metadata (key_name, value) VALUES (?, ?);`, keySchemaMinorVersion, latestDbMinorVersion)
|
_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaMinorVersion, latestDbMinorVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -182,15 +180,13 @@ func (mysql *MySQL) fixSchemas() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = mysql.db.Exec(`UPDATE metadata SET value = ? WHERE key_name = ?;`, latestDbMinorVersion, keySchemaMinorVersion)
|
_, err = mysql.db.Exec(`update metadata set value = ? where key_name = ?;`, latestDbMinorVersion, keySchemaMinorVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if err == nil && minorVersion != latestDbMinorVersion {
|
} else if err == nil && minorVersion != latestDbMinorVersion {
|
||||||
// TODO: if minorVersion < latestDbMinorVersion, upgrade,
|
// TODO: if minorVersion < latestDbMinorVersion, upgrade,
|
||||||
// if latestDbMinorVersion < minorVersion, ignore because backwards compatible
|
// if latestDbMinorVersion < minorVersion, ignore because backwards compatible
|
||||||
} else if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -420,7 +416,7 @@ func (mysql *MySQL) deleteCorrespondents(ctx context.Context, threshold int64) {
|
|||||||
} else {
|
} else {
|
||||||
count, err := result.RowsAffected()
|
count, err := result.RowsAffected()
|
||||||
if !mysql.logError("error deleting correspondents", err) {
|
if !mysql.logError("error deleting correspondents", err) {
|
||||||
mysql.logger.Debug("mysql", fmt.Sprintf("deleted %d correspondents entries", count))
|
mysql.logger.Debug(fmt.Sprintf("deleted %d correspondents entries", count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -627,46 +623,40 @@ func (mysql *MySQL) AddChannelItem(target string, item history.Item, account str
|
|||||||
|
|
||||||
func (mysql *MySQL) insertSequenceEntry(ctx context.Context, target string, messageTime int64, id int64) (err error) {
|
func (mysql *MySQL) insertSequenceEntry(ctx context.Context, target string, messageTime int64, id int64) (err error) {
|
||||||
_, err = mysql.insertSequence.ExecContext(ctx, target, messageTime, id)
|
_, err = mysql.insertSequence.ExecContext(ctx, target, messageTime, id)
|
||||||
if err != nil {
|
mysql.logError("could not insert sequence entry", err)
|
||||||
return fmt.Errorf("could not insert sequence entry: %w", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) insertConversationEntry(ctx context.Context, target, correspondent string, messageTime int64, id int64) (err error) {
|
func (mysql *MySQL) insertConversationEntry(ctx context.Context, target, correspondent string, messageTime int64, id int64) (err error) {
|
||||||
_, err = mysql.insertConversation.ExecContext(ctx, target, correspondent, messageTime, id)
|
_, err = mysql.insertConversation.ExecContext(ctx, target, correspondent, messageTime, id)
|
||||||
if err != nil {
|
mysql.logError("could not insert conversations entry", err)
|
||||||
return fmt.Errorf("could not insert conversations entry: %w", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) insertCorrespondentsEntry(ctx context.Context, target, correspondent string, messageTime int64, historyId int64) (err error) {
|
func (mysql *MySQL) insertCorrespondentsEntry(ctx context.Context, target, correspondent string, messageTime int64, historyId int64) (err error) {
|
||||||
_, err = mysql.insertCorrespondent.ExecContext(ctx, target, correspondent, messageTime, messageTime)
|
_, err = mysql.insertCorrespondent.ExecContext(ctx, target, correspondent, messageTime, messageTime)
|
||||||
if err != nil {
|
mysql.logError("could not insert conversations entry", err)
|
||||||
return fmt.Errorf("could not insert correspondents entry: %w", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) insertBase(ctx context.Context, item history.Item) (id int64, err error) {
|
func (mysql *MySQL) insertBase(ctx context.Context, item history.Item) (id int64, err error) {
|
||||||
value, err := history.MarshalItem(&item)
|
value, err := marshalItem(&item)
|
||||||
if err != nil {
|
if mysql.logError("could not marshal item", err) {
|
||||||
return 0, fmt.Errorf("could not marshal item: %w", err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msgidBytes, err := utils.DecodeSecretToken(item.Message.Msgid)
|
msgidBytes, err := decodeMsgid(item.Message.Msgid)
|
||||||
if err != nil {
|
if mysql.logError("could not decode msgid", err) {
|
||||||
return 0, fmt.Errorf("could not decode msgid: %w", err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := mysql.insertHistory.ExecContext(ctx, value, msgidBytes)
|
result, err := mysql.insertHistory.ExecContext(ctx, value, msgidBytes)
|
||||||
if err != nil {
|
if mysql.logError("could not insert item", err) {
|
||||||
return 0, fmt.Errorf("could not insert item: %w", err)
|
return
|
||||||
}
|
}
|
||||||
id, err = result.LastInsertId()
|
id, err = result.LastInsertId()
|
||||||
if err != nil {
|
if mysql.logError("could not insert item", err) {
|
||||||
return 0, fmt.Errorf("could not insert item: %w", err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -677,9 +667,7 @@ func (mysql *MySQL) insertAccountMessageEntry(ctx context.Context, id int64, acc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = mysql.insertAccountMessage.ExecContext(ctx, id, account)
|
_, err = mysql.insertAccountMessage.ExecContext(ctx, id, account)
|
||||||
if err != nil {
|
mysql.logError("could not insert account-message entry", err)
|
||||||
return fmt.Errorf("could not insert account-message entry: %w", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -747,25 +735,20 @@ func (mysql *MySQL) DeleteMsgid(msgid, accountName string) (err error) {
|
|||||||
|
|
||||||
_, id, data, err := mysql.lookupMsgid(ctx, msgid, true)
|
_, id, data, err := mysql.lookupMsgid(ctx, msgid, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return history.ErrNotFound
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if accountName != "*" {
|
if accountName != "*" {
|
||||||
var item history.Item
|
var item history.Item
|
||||||
err = history.UnmarshalItem(data, &item)
|
err = unmarshalItem(data, &item)
|
||||||
// delete if the entry is corrupt
|
// delete if the entry is corrupt
|
||||||
if err == nil && item.AccountName != accountName {
|
if err == nil && item.AccountName != accountName {
|
||||||
return history.ErrDisallowed
|
return ErrDisallowed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mysql.deleteHistoryIDs(ctx, []uint64{id})
|
err = mysql.deleteHistoryIDs(ctx, []uint64{id})
|
||||||
if err != nil {
|
mysql.logError("couldn't delete msgid", err)
|
||||||
return fmt.Errorf("couldn't delete msgid: %w", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -801,7 +784,7 @@ func (mysql *MySQL) Export(account string, writer io.Writer) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = history.UnmarshalItem(blob, &item)
|
err = unmarshalItem(blob, &item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -829,11 +812,8 @@ func (mysql *MySQL) Export(account string, writer io.Writer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) lookupMsgid(ctx context.Context, msgid string, includeData bool) (result time.Time, id uint64, data []byte, err error) {
|
func (mysql *MySQL) lookupMsgid(ctx context.Context, msgid string, includeData bool) (result time.Time, id uint64, data []byte, err error) {
|
||||||
decoded, err := utils.DecodeSecretToken(msgid)
|
decoded, err := decodeMsgid(msgid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// use sql.ErrNoRows internally for consistency, translate to history.ErrNotFound
|
|
||||||
// at the package boundary if necessary
|
|
||||||
err = sql.ErrNoRows
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cols := `sequence.nanotime, conversations.nanotime`
|
cols := `sequence.nanotime, conversations.nanotime`
|
||||||
@ -851,10 +831,10 @@ func (mysql *MySQL) lookupMsgid(ctx context.Context, msgid string, includeData b
|
|||||||
} else {
|
} else {
|
||||||
err = row.Scan(&nanoSeq, &nanoConv, &id, &data)
|
err = row.Scan(&nanoSeq, &nanoConv, &id, &data)
|
||||||
}
|
}
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
mysql.logError("could not resolve msgid to time", err)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != sql.ErrNoRows {
|
|
||||||
err = fmt.Errorf("could not resolve msgid to time: %w", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nanotime := extractNanotime(nanoSeq, nanoConv)
|
nanotime := extractNanotime(nanoSeq, nanoConv)
|
||||||
@ -877,8 +857,8 @@ func extractNanotime(seq, conv sql.NullInt64) (result int64) {
|
|||||||
|
|
||||||
func (mysql *MySQL) selectItems(ctx context.Context, query string, args ...interface{}) (results []history.Item, err error) {
|
func (mysql *MySQL) selectItems(ctx context.Context, query string, args ...interface{}) (results []history.Item, err error) {
|
||||||
rows, err := mysql.db.QueryContext(ctx, query, args...)
|
rows, err := mysql.db.QueryContext(ctx, query, args...)
|
||||||
if err != nil {
|
if mysql.logError("could not select history items", err) {
|
||||||
return nil, fmt.Errorf("could not select history items: %w", err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@ -887,12 +867,12 @@ func (mysql *MySQL) selectItems(ctx context.Context, query string, args ...inter
|
|||||||
var blob []byte
|
var blob []byte
|
||||||
var item history.Item
|
var item history.Item
|
||||||
err = rows.Scan(&blob)
|
err = rows.Scan(&blob)
|
||||||
if err != nil {
|
if mysql.logError("could not scan history item", err) {
|
||||||
return nil, fmt.Errorf("could not scan history item: %w", err)
|
return
|
||||||
}
|
}
|
||||||
err = history.UnmarshalItem(blob, &item)
|
err = unmarshalItem(blob, &item)
|
||||||
if err != nil {
|
if mysql.logError("could not unmarshal history item", err) {
|
||||||
return nil, fmt.Errorf("could not unmarshal history item: %w", err)
|
return
|
||||||
}
|
}
|
||||||
results = append(results, item)
|
results = append(results, item)
|
||||||
}
|
}
|
||||||
@ -969,7 +949,7 @@ func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target strin
|
|||||||
|
|
||||||
rows, err := mysql.db.QueryContext(ctx, query, args...)
|
rows, err := mysql.db.QueryContext(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not query correspondents: %w", err)
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var correspondent string
|
var correspondent string
|
||||||
@ -977,7 +957,7 @@ func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target strin
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = rows.Scan(&correspondent, &nanotime)
|
err = rows.Scan(&correspondent, &nanotime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not scan correspondents: %w", err)
|
return
|
||||||
}
|
}
|
||||||
results = append(results, history.TargetListing{
|
results = append(results, history.TargetListing{
|
||||||
CfName: correspondent,
|
CfName: correspondent,
|
||||||
@ -992,19 +972,6 @@ func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target strin
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) ListCorrespondents(cftarget string, start, end time.Time, limit int) (results []history.TargetListing, err error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// TODO accept msgids here?
|
|
||||||
|
|
||||||
results, err = mysql.listCorrespondentsInternal(ctx, cftarget, start, end, time.Time{}, limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not read correspondents: %w", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mysql *MySQL) ListChannels(cfchannels []string) (results []history.TargetListing, err error) {
|
func (mysql *MySQL) ListChannels(cfchannels []string) (results []history.TargetListing, err error) {
|
||||||
if mysql.db == nil {
|
if mysql.db == nil {
|
||||||
return
|
return
|
||||||
@ -1018,7 +985,7 @@ func (mysql *MySQL) ListChannels(cfchannels []string) (results []history.TargetL
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var queryBuf strings.Builder
|
var queryBuf strings.Builder
|
||||||
args := make([]interface{}, 0, len(cfchannels))
|
args := make([]interface{}, 0, len(results))
|
||||||
// https://dev.mysql.com/doc/refman/8.0/en/group-by-optimization.html
|
// https://dev.mysql.com/doc/refman/8.0/en/group-by-optimization.html
|
||||||
// this should be a "loose index scan"
|
// this should be a "loose index scan"
|
||||||
queryBuf.WriteString(`SELECT sequence.target, MAX(sequence.nanotime) FROM sequence
|
queryBuf.WriteString(`SELECT sequence.target, MAX(sequence.nanotime) FROM sequence
|
||||||
@ -1033,8 +1000,8 @@ func (mysql *MySQL) ListChannels(cfchannels []string) (results []history.TargetL
|
|||||||
queryBuf.WriteString(") GROUP BY sequence.target;")
|
queryBuf.WriteString(") GROUP BY sequence.target;")
|
||||||
|
|
||||||
rows, err := mysql.db.QueryContext(ctx, queryBuf.String(), args...)
|
rows, err := mysql.db.QueryContext(ctx, queryBuf.String(), args...)
|
||||||
if err != nil {
|
if mysql.logError("could not query channel listings", err) {
|
||||||
return nil, fmt.Errorf("could not query channel listings: %w", err)
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
@ -1042,8 +1009,8 @@ func (mysql *MySQL) ListChannels(cfchannels []string) (results []history.TargetL
|
|||||||
var nanotime int64
|
var nanotime int64
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = rows.Scan(&target, &nanotime)
|
err = rows.Scan(&target, &nanotime)
|
||||||
if err != nil {
|
if mysql.logError("could not scan channel listings", err) {
|
||||||
return nil, fmt.Errorf("could not scan channel listings: %w", err)
|
return
|
||||||
}
|
}
|
||||||
results = append(results, history.TargetListing{
|
results = append(results, history.TargetListing{
|
||||||
CfName: target,
|
CfName: target,
|
||||||
@ -1053,13 +1020,12 @@ func (mysql *MySQL) ListChannels(cfchannels []string) (results []history.TargetL
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mysql *MySQL) Close() (err error) {
|
func (mysql *MySQL) Close() {
|
||||||
// closing the database will close our prepared statements as well
|
// closing the database will close our prepared statements as well
|
||||||
if mysql.db != nil {
|
if mysql.db != nil {
|
||||||
err = mysql.db.Close()
|
mysql.db.Close()
|
||||||
}
|
}
|
||||||
mysql.db = nil
|
mysql.db = nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// implements history.Sequence, emulating a single history buffer (for a channel,
|
// implements history.Sequence, emulating a single history buffer (for a channel,
|
||||||
@ -1106,6 +1072,19 @@ func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (result
|
|||||||
return history.GenericAround(s, start, limit)
|
return history.GenericAround(s, start, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (seq *mySQLHistorySequence) ListCorrespondents(start, end history.Selector, limit int) (results []history.TargetListing, err error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), seq.mysql.getTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// TODO accept msgids here?
|
||||||
|
startTime := start.Time
|
||||||
|
endTime := end.Time
|
||||||
|
|
||||||
|
results, err = seq.mysql.listCorrespondentsInternal(ctx, seq.target, startTime, endTime, seq.cutoff, limit)
|
||||||
|
seq.mysql.logError("could not read correspondents", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (seq *mySQLHistorySequence) Cutoff() time.Time {
|
func (seq *mySQLHistorySequence) Cutoff() time.Time {
|
||||||
return seq.cutoff
|
return seq.cutoff
|
||||||
}
|
}
|
||||||
|
|||||||
23
irc/mysql/serialization.go
Normal file
23
irc/mysql/serialization.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/history"
|
||||||
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 123 / '{' is the magic number that means JSON;
|
||||||
|
// if we want to do a binary encoding later, we just have to add different magic version numbers
|
||||||
|
|
||||||
|
func marshalItem(item *history.Item) (result []byte, err error) {
|
||||||
|
return json.Marshal(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalItem(data []byte, result *history.Item) (err error) {
|
||||||
|
return json.Unmarshal(data, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMsgid(msgid string) ([]byte, error) {
|
||||||
|
return utils.B32Encoder.DecodeString(msgid)
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
//go:build !mysql
|
|
||||||
|
|
||||||
package mysql
|
|
||||||
|
|
||||||
// Copyright (c) 2020 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/history"
|
|
||||||
"github.com/ergochat/ergo/irc/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Enabled is false when MySQL support is not compiled in
|
|
||||||
const Enabled = false
|
|
||||||
|
|
||||||
// MySQL is a stub implementation when the mysql build tag is not present
|
|
||||||
type MySQL struct {
|
|
||||||
history.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMySQLDatabase returns an error when MySQL support is not compiled in
|
|
||||||
func NewMySQLDatabase(logger *logger.Manager, config Config) (*MySQL, error) {
|
|
||||||
return nil, errors.New("MySQL support not enabled in this build. Rebuild with `make build_full` to enable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetConfig is a no-op for the stub implementation
|
|
||||||
func (m *MySQL) SetConfig(config Config) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
@ -128,11 +128,9 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
|||||||
}
|
}
|
||||||
|
|
||||||
newCfnick := target.NickCasefolded()
|
newCfnick := target.NickCasefolded()
|
||||||
// send MONITOR updates only for nick changes, not for new connection registration;
|
if newCfnick != details.nickCasefolded {
|
||||||
// defer MONITOR for new connection registration until pre-registration metadata is applied
|
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false)
|
||||||
if hadNick && newCfnick != details.nickCasefolded {
|
client.server.monitorManager.AlertAbout(assignedNickname, newCfnick, true)
|
||||||
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false, nil)
|
|
||||||
client.server.monitorManager.AlertAbout(assignedNickname, newCfnick, true, target)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1055,10 +1055,10 @@ func nsSaregisterHandler(service *ircService, server *Server, client *Client, co
|
|||||||
var failCode string
|
var failCode string
|
||||||
if err == errAccountAlreadyRegistered || err == errAccountAlreadyVerified {
|
if err == errAccountAlreadyRegistered || err == errAccountAlreadyVerified {
|
||||||
errMsg = client.t("Account already exists")
|
errMsg = client.t("Account already exists")
|
||||||
failCode = "ACCOUNT_EXISTS"
|
failCode = "USERNAME_EXISTS"
|
||||||
} else if err == errNameReserved {
|
} else if err == errNameReserved {
|
||||||
errMsg = client.t(err.Error())
|
errMsg = client.t(err.Error())
|
||||||
failCode = "ACCOUNT_EXISTS"
|
failCode = "USERNAME_EXISTS"
|
||||||
} else if err == errAccountBadPassphrase {
|
} else if err == errAccountBadPassphrase {
|
||||||
errMsg = client.t("Passphrase contains forbidden characters or is otherwise invalid")
|
errMsg = client.t("Passphrase contains forbidden characters or is otherwise invalid")
|
||||||
failCode = "INVALID_PASSWORD"
|
failCode = "INVALID_PASSWORD"
|
||||||
|
|||||||
@ -79,8 +79,8 @@ const (
|
|||||||
RPL_WHOISACTUALLY = "338"
|
RPL_WHOISACTUALLY = "338"
|
||||||
RPL_INVITING = "341"
|
RPL_INVITING = "341"
|
||||||
RPL_SUMMONING = "342"
|
RPL_SUMMONING = "342"
|
||||||
RPL_INVEXLIST = "346"
|
RPL_INVITELIST = "346"
|
||||||
RPL_ENDOFINVEXLIST = "347"
|
RPL_ENDOFINVITELIST = "347"
|
||||||
RPL_EXCEPTLIST = "348"
|
RPL_EXCEPTLIST = "348"
|
||||||
RPL_ENDOFEXCEPTLIST = "349"
|
RPL_ENDOFEXCEPTLIST = "349"
|
||||||
RPL_VERSION = "351"
|
RPL_VERSION = "351"
|
||||||
@ -183,13 +183,6 @@ const (
|
|||||||
RPL_MONLIST = "732"
|
RPL_MONLIST = "732"
|
||||||
RPL_ENDOFMONLIST = "733"
|
RPL_ENDOFMONLIST = "733"
|
||||||
ERR_MONLISTFULL = "734"
|
ERR_MONLISTFULL = "734"
|
||||||
RPL_WHOISKEYVALUE = "760" // metadata numerics
|
|
||||||
RPL_KEYVALUE = "761"
|
|
||||||
RPL_KEYNOTSET = "766"
|
|
||||||
RPL_METADATASUBOK = "770"
|
|
||||||
RPL_METADATAUNSUBOK = "771"
|
|
||||||
RPL_METADATASUBS = "772"
|
|
||||||
RPL_METADATASYNCLATER = "774" // end metadata numerics
|
|
||||||
RPL_LOGGEDIN = "900"
|
RPL_LOGGEDIN = "900"
|
||||||
RPL_LOGGEDOUT = "901"
|
RPL_LOGGEDOUT = "901"
|
||||||
ERR_NICKLOCKED = "902"
|
ERR_NICKLOCKED = "902"
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
MinCost = bcrypt.MinCost
|
MinCost = bcrypt.MinCost
|
||||||
MaxCost = bcrypt.MaxCost
|
|
||||||
DefaultCost = 12 // ballpark: 250 msec on a modern Intel CPU
|
DefaultCost = 12 // ballpark: 250 msec on a modern Intel CPU
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
// Copyright (c) 2020 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package postgresql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// maximum length in bytes of any message target (nickname or channel name) in its
|
|
||||||
// canonicalized (i.e., casefolded) state:
|
|
||||||
MaxTargetLength = 64
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
// these are intended to be written directly into the config file:
|
|
||||||
Enabled bool
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
SocketPath string `yaml:"socket-path"`
|
|
||||||
User string
|
|
||||||
Password string
|
|
||||||
HistoryDatabase string `yaml:"history-database"`
|
|
||||||
Timeout time.Duration
|
|
||||||
MaxConns int `yaml:"max-conns"`
|
|
||||||
ConnMaxLifetime time.Duration `yaml:"conn-max-lifetime"`
|
|
||||||
// PostgreSQL-specific configuration:
|
|
||||||
ApplicationName string `yaml:"application-name"` // shown in pg_stat_activity
|
|
||||||
ConnectTimeout time.Duration `yaml:"connect-timeout"` // timeout for establishing connections
|
|
||||||
// PostgreSQL SSL/TLS configuration:
|
|
||||||
SSLMode string `yaml:"ssl-mode"` // disable, require, verify-ca, verify-full
|
|
||||||
SSLCert string `yaml:"ssl-cert"` // client certificate path
|
|
||||||
SSLKey string `yaml:"ssl-key"` // client key path
|
|
||||||
SSLRootCert string `yaml:"ssl-root-cert"` // CA certificate path
|
|
||||||
URI string `yaml:"uri"` // libpq postgresql:// URI overriding the above
|
|
||||||
|
|
||||||
// XXX these are copied from elsewhere in the config:
|
|
||||||
ExpireTime time.Duration
|
|
||||||
TrackAccountMessages bool
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
//go:build postgres
|
|
||||||
|
|
||||||
// Copyright (c) 2020 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package postgresql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testBuildURI(t *testing.T, config Config, expected string) {
|
|
||||||
t.Helper()
|
|
||||||
uri, err := config.buildURI()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if uri != expected {
|
|
||||||
t.Errorf("got %q, want %q", uri, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildURITCP(t *testing.T) {
|
|
||||||
testBuildURI(t, Config{
|
|
||||||
Host: "db.example.com",
|
|
||||||
Port: 5432,
|
|
||||||
User: "ergo",
|
|
||||||
Password: "secret",
|
|
||||||
HistoryDatabase: "ergo_history",
|
|
||||||
}, "postgresql://ergo:secret@db.example.com:5432/ergo_history?sslmode=disable")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildURIDefaultPort(t *testing.T) {
|
|
||||||
testBuildURI(t, Config{
|
|
||||||
Host: "localhost",
|
|
||||||
HistoryDatabase: "ergo_history",
|
|
||||||
}, "postgresql://localhost:5432/ergo_history?sslmode=disable")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildURIDefaultHost(t *testing.T) {
|
|
||||||
testBuildURI(t, Config{
|
|
||||||
HistoryDatabase: "ergo_history",
|
|
||||||
}, "postgresql://localhost:5432/ergo_history?sslmode=disable")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildURISSLMode(t *testing.T) {
|
|
||||||
testBuildURI(t, Config{
|
|
||||||
Host: "db.example.com",
|
|
||||||
Port: 5432,
|
|
||||||
HistoryDatabase: "ergo_history",
|
|
||||||
SSLMode: "verify-full",
|
|
||||||
SSLCert: "/etc/ssl/client.crt",
|
|
||||||
SSLKey: "/etc/ssl/client.key",
|
|
||||||
SSLRootCert: "/etc/ssl/ca.crt",
|
|
||||||
}, "postgresql://db.example.com:5432/ergo_history?sslcert=%2Fetc%2Fssl%2Fclient.crt&sslkey=%2Fetc%2Fssl%2Fclient.key&sslmode=verify-full&sslrootcert=%2Fetc%2Fssl%2Fca.crt")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildURIUnixSocket(t *testing.T) {
|
|
||||||
testBuildURI(t, Config{
|
|
||||||
SocketPath: "/var/run/postgresql",
|
|
||||||
User: "ergo",
|
|
||||||
Password: "secret",
|
|
||||||
HistoryDatabase: "ergo_history",
|
|
||||||
}, "postgresql://ergo:secret@/ergo_history?host=%2Fvar%2Frun%2Fpostgresql")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildURISpecialCharsInPassword(t *testing.T) {
|
|
||||||
testBuildURI(t, Config{
|
|
||||||
Host: "db.example.com",
|
|
||||||
Port: 5432,
|
|
||||||
User: "ergo",
|
|
||||||
Password: "p@ss:w/ord?#&=",
|
|
||||||
HistoryDatabase: "ergo_history",
|
|
||||||
}, "postgresql://ergo:p%40ss%3Aw%2Ford%3F%23&=@db.example.com:5432/ergo_history?sslmode=disable")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildURIOptionalParams(t *testing.T) {
|
|
||||||
testBuildURI(t, Config{
|
|
||||||
Host: "db.example.com",
|
|
||||||
Port: 5433,
|
|
||||||
HistoryDatabase: "ergo_history",
|
|
||||||
ApplicationName: "ergo",
|
|
||||||
ConnectTimeout: 30 * time.Second,
|
|
||||||
}, "postgresql://db.example.com:5433/ergo_history?application_name=ergo&connect_timeout=30&sslmode=disable")
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,31 +0,0 @@
|
|||||||
//go:build !postgresql
|
|
||||||
|
|
||||||
// Copyright (c) 2020 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package postgresql
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/history"
|
|
||||||
"github.com/ergochat/ergo/irc/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Enabled is false when PostgreSQL support is not compiled in
|
|
||||||
const Enabled = false
|
|
||||||
|
|
||||||
// PostgreSQL is a stub implementation when the postgres build tag is not present
|
|
||||||
type PostgreSQL struct {
|
|
||||||
history.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPostgreSQLDatabase returns an error when PostgreSQL support is not compiled in
|
|
||||||
func NewPostgreSQLDatabase(logger *logger.Manager, config Config) (*PostgreSQL, error) {
|
|
||||||
return nil, errors.New("PostgreSQL support not enabled in this build. Rebuild with `make build_full` to enable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetConfig is a no-op for the stub implementation
|
|
||||||
func (pg *PostgreSQL) SetConfig(config Config) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
@ -34,9 +34,7 @@ import (
|
|||||||
"github.com/ergochat/ergo/irc/logger"
|
"github.com/ergochat/ergo/irc/logger"
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/mysql"
|
"github.com/ergochat/ergo/irc/mysql"
|
||||||
"github.com/ergochat/ergo/irc/postgresql"
|
|
||||||
"github.com/ergochat/ergo/irc/sno"
|
"github.com/ergochat/ergo/irc/sno"
|
||||||
"github.com/ergochat/ergo/irc/sqlite"
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
"github.com/ergochat/ergo/irc/webpush"
|
"github.com/ergochat/ergo/irc/webpush"
|
||||||
)
|
)
|
||||||
@ -92,10 +90,7 @@ type Server struct {
|
|||||||
snomasks SnoManager
|
snomasks SnoManager
|
||||||
store *buntdb.DB
|
store *buntdb.DB
|
||||||
dstore datastore.Datastore
|
dstore datastore.Datastore
|
||||||
mysqlHistoryDB *mysql.MySQL
|
historyDB mysql.MySQL
|
||||||
postgresHistoryDB *postgresql.PostgreSQL
|
|
||||||
sqliteHistoryDB *sqlite.SQLite
|
|
||||||
historyDB history.Database
|
|
||||||
torLimiter connection_limits.TorLimiter
|
torLimiter connection_limits.TorLimiter
|
||||||
whoWas WhoWasList
|
whoWas WhoWasList
|
||||||
stats Stats
|
stats Stats
|
||||||
@ -158,6 +153,7 @@ func (server *Server) Shutdown() {
|
|||||||
sdnotify.Stopping()
|
sdnotify.Stopping()
|
||||||
server.logger.Info("server", "Stopping server")
|
server.logger.Info("server", "Stopping server")
|
||||||
|
|
||||||
|
//TODO(dan): Make sure we disallow new nicks
|
||||||
for _, client := range server.clients.AllClients() {
|
for _, client := range server.clients.AllClients() {
|
||||||
client.Notice("Server is shutting down")
|
client.Notice("Server is shutting down")
|
||||||
}
|
}
|
||||||
@ -166,12 +162,10 @@ func (server *Server) Shutdown() {
|
|||||||
server.performAlwaysOnMaintenance(false, true)
|
server.performAlwaysOnMaintenance(false, true)
|
||||||
|
|
||||||
if err := server.store.Close(); err != nil {
|
if err := server.store.Close(); err != nil {
|
||||||
server.logger.Error("shutdown", "Could not close datastore", err.Error())
|
server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.historyDB.Close(); err != nil {
|
server.historyDB.Close()
|
||||||
server.logger.Error("shutdown", "Could not close history database", err.Error())
|
|
||||||
}
|
|
||||||
server.logger.Info("server", fmt.Sprintf("%s exiting", Ver))
|
server.logger.Info("server", fmt.Sprintf("%s exiting", Ver))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,10 +428,6 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
|||||||
c.SetMode(defaultMode, true)
|
c.SetMode(defaultMode, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.applyPreregMetadata(session)
|
|
||||||
|
|
||||||
c.server.monitorManager.AlertAbout(c.Nick(), c.NickCasefolded(), true, c)
|
|
||||||
|
|
||||||
// this is not a reattach, so if the client is always-on, this is the first time
|
// this is not a reattach, so if the client is always-on, this is the first time
|
||||||
// the Client object was created during the current server uptime. mark dirty in
|
// the Client object was created during the current server uptime. mark dirty in
|
||||||
// order to persist the realname and the user modes:
|
// order to persist the realname and the user modes:
|
||||||
@ -506,9 +496,6 @@ func (server *Server) playRegistrationBurst(session *Session) {
|
|||||||
if !(rb.session.capabilities.Has(caps.ExtendedISupport) && rb.session.isupportSentPrereg) {
|
if !(rb.session.capabilities.Has(caps.ExtendedISupport) && rb.session.isupportSentPrereg) {
|
||||||
server.RplISupport(c, rb)
|
server.RplISupport(c, rb)
|
||||||
}
|
}
|
||||||
if session.capabilities.Has(caps.Metadata) {
|
|
||||||
playMetadataVerbBatch(rb, d.nick, c.ListMetadata())
|
|
||||||
}
|
|
||||||
if d.account != "" && session.capabilities.Has(caps.Persistence) {
|
if d.account != "" && session.capabilities.Has(caps.Persistence) {
|
||||||
reportPersistenceStatus(c, rb, false)
|
reportPersistenceStatus(c, rb, false)
|
||||||
}
|
}
|
||||||
@ -643,6 +630,7 @@ func (client *Client) getWhoisOf(target *Client, hasPrivs bool, rb *ResponseBuff
|
|||||||
if target.HasMode(modes.Bot) {
|
if target.HasMode(modes.Bot) {
|
||||||
rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, fmt.Sprintf(ircfmt.Unescape(client.t("is a $bBot$b on %s")), client.server.Config().Network.Name))
|
rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, fmt.Sprintf(ircfmt.Unescape(client.t("is a $bBot$b on %s")), client.server.Config().Network.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
if client == target || oper.HasRoleCapab("ban") {
|
if client == target || oper.HasRoleCapab("ban") {
|
||||||
for _, session := range target.Sessions() {
|
for _, session := range target.Sessions() {
|
||||||
if session.certfp != "" {
|
if session.certfp != "" {
|
||||||
@ -654,11 +642,6 @@ func (client *Client) getWhoisOf(target *Client, hasPrivs bool, rb *ResponseBuff
|
|||||||
if away, awayMessage := target.Away(); away {
|
if away, awayMessage := target.Away(); away {
|
||||||
rb.Add(nil, client.server.name, RPL_AWAY, cnick, tnick, awayMessage)
|
rb.Add(nil, client.server.name, RPL_AWAY, cnick, tnick, awayMessage)
|
||||||
}
|
}
|
||||||
if rb.session.capabilities.Has(caps.Metadata) {
|
|
||||||
for key, value := range target.ListMetadata() {
|
|
||||||
rb.Add(nil, client.server.name, RPL_WHOISKEYVALUE, cnick, tnick, key, "*", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// rehash reloads the config and applies the changes from the config file.
|
// rehash reloads the config and applies the changes from the config file.
|
||||||
@ -702,9 +685,6 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
|||||||
globalCasemappingSetting = config.Server.Casemapping
|
globalCasemappingSetting = config.Server.Casemapping
|
||||||
globalUtf8EnforcementSetting = config.Server.EnforceUtf8
|
globalUtf8EnforcementSetting = config.Server.EnforceUtf8
|
||||||
MaxLineLen = config.Server.MaxLineLen
|
MaxLineLen = config.Server.MaxLineLen
|
||||||
RegisterTimeout = config.Server.IdleTimeouts.Registration
|
|
||||||
PingTimeout = config.Server.IdleTimeouts.Ping
|
|
||||||
DisconnectTimeout = config.Server.IdleTimeouts.Disconnect
|
|
||||||
} else {
|
} else {
|
||||||
// enforce configs that can't be changed after launch:
|
// enforce configs that can't be changed after launch:
|
||||||
if server.name != config.Server.Name {
|
if server.name != config.Server.Name {
|
||||||
@ -730,8 +710,6 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
|||||||
return fmt.Errorf("Cannot enable MySQL after launching the server, rehash aborted")
|
return fmt.Errorf("Cannot enable MySQL after launching the server, rehash aborted")
|
||||||
} else if oldConfig.Server.MaxLineLen != config.Server.MaxLineLen {
|
} else if oldConfig.Server.MaxLineLen != config.Server.MaxLineLen {
|
||||||
return fmt.Errorf("Cannot change max-line-len after launching the server, rehash aborted")
|
return fmt.Errorf("Cannot change max-line-len after launching the server, rehash aborted")
|
||||||
} else if oldConfig.Server.IdleTimeouts != config.Server.IdleTimeouts {
|
|
||||||
return fmt.Errorf("Cannot change idle-timeouts after launching the server, rehash aborted")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -810,20 +788,8 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if config.Datastore.MySQL.Enabled && server.mysqlHistoryDB != nil {
|
if config.Datastore.MySQL.Enabled && config.Datastore.MySQL != oldConfig.Datastore.MySQL {
|
||||||
if config.Datastore.MySQL != oldConfig.Datastore.MySQL {
|
server.historyDB.SetConfig(config.Datastore.MySQL)
|
||||||
server.mysqlHistoryDB.SetConfig(config.Datastore.MySQL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.Datastore.PostgreSQL.Enabled && server.postgresHistoryDB != nil {
|
|
||||||
if config.Datastore.PostgreSQL != oldConfig.Datastore.PostgreSQL {
|
|
||||||
server.postgresHistoryDB.SetConfig(config.Datastore.PostgreSQL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.Datastore.SQLite.Enabled && server.sqliteHistoryDB != nil {
|
|
||||||
if config.Datastore.SQLite != oldConfig.Datastore.SQLite {
|
|
||||||
server.sqliteHistoryDB.SetConfig(config.Datastore.SQLite)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -930,9 +896,6 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
|||||||
if config.Accounts.RequireSasl.Enabled && config.Accounts.Registration.Enabled {
|
if config.Accounts.RequireSasl.Enabled && config.Accounts.Registration.Enabled {
|
||||||
server.logger.Warning("server", "Warning: although require-sasl is enabled, users can still register accounts. If your server is not intended to be public, you must set accounts.registration.enabled to false.")
|
server.logger.Warning("server", "Warning: although require-sasl is enabled, users can still register accounts. If your server is not intended to be public, you must set accounts.registration.enabled to false.")
|
||||||
}
|
}
|
||||||
if config.History.Enabled && config.History.ChathistoryMax == 0 {
|
|
||||||
server.logger.Warning("server", "Warning: for history to work correctly, you must set history.chathistory-maxmessages (see default.yaml for a recommendation).")
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1033,28 +996,12 @@ func (server *Server) loadFromDatastore(config *Config) (err error) {
|
|||||||
server.accounts.Initialize(server)
|
server.accounts.Initialize(server)
|
||||||
|
|
||||||
if config.Datastore.MySQL.Enabled {
|
if config.Datastore.MySQL.Enabled {
|
||||||
server.mysqlHistoryDB, err = mysql.NewMySQLDatabase(server.logger, config.Datastore.MySQL)
|
server.historyDB.Initialize(server.logger, config.Datastore.MySQL)
|
||||||
|
err = server.historyDB.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.logger.Error("internal", "could not connect to mysql", err.Error())
|
server.logger.Error("internal", "could not connect to mysql", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
server.historyDB = server.mysqlHistoryDB
|
|
||||||
} else if config.Datastore.PostgreSQL.Enabled {
|
|
||||||
server.postgresHistoryDB, err = postgresql.NewPostgreSQLDatabase(server.logger, config.Datastore.PostgreSQL)
|
|
||||||
if err != nil {
|
|
||||||
server.logger.Error("internal", "could not connect to postgresql", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
server.historyDB = server.postgresHistoryDB
|
|
||||||
} else if config.Datastore.SQLite.Enabled {
|
|
||||||
server.sqliteHistoryDB, err = sqlite.NewSQLiteDatabase(server.logger, config.Datastore.SQLite)
|
|
||||||
if err != nil {
|
|
||||||
server.logger.Error("internal", "could not open sqlite database", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
server.historyDB = server.sqliteHistoryDB
|
|
||||||
} else {
|
|
||||||
server.historyDB = history.NewNoopDatabase()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -1119,6 +1066,10 @@ func (server *Server) setupListeners(config *Config) (err error) {
|
|||||||
// we may already know the channel we're querying, or we may have
|
// we may already know the channel we're querying, or we may have
|
||||||
// to look it up via a string query. This function is responsible for
|
// to look it up via a string query. This function is responsible for
|
||||||
// privilege checking.
|
// privilege checking.
|
||||||
|
// XXX: call this with providedChannel==nil and query=="" to get a sequence
|
||||||
|
// suitable for ListCorrespondents (i.e., this function is still used to
|
||||||
|
// decide whether the ringbuf or mysql is authoritative about the client's
|
||||||
|
// message history).
|
||||||
func (server *Server) GetHistorySequence(providedChannel *Channel, client *Client, query string) (channel *Channel, sequence history.Sequence, err error) {
|
func (server *Server) GetHistorySequence(providedChannel *Channel, client *Client, query string) (channel *Channel, sequence history.Sequence, err error) {
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
// 4 cases: {persistent, ephemeral} x {normal, conversation}
|
// 4 cases: {persistent, ephemeral} x {normal, conversation}
|
||||||
@ -1270,7 +1221,7 @@ func (server *Server) DeleteMessage(target, msgid, accountName string) (err erro
|
|||||||
return item.Message.Msgid == msgid && (accountName == "*" || item.AccountName == accountName)
|
return item.Message.Msgid == msgid && (accountName == "*" || item.AccountName == accountName)
|
||||||
})
|
})
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
err = history.ErrNotFound
|
err = errNoop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
// Copyright (c) 2020 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package sqlite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// maximum length in bytes of any message target (nickname or channel name) in its
|
|
||||||
// canonicalized (i.e., casefolded) state:
|
|
||||||
MaxTargetLength = 64
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
// these are intended to be written directly into the config file:
|
|
||||||
Enabled bool
|
|
||||||
DatabasePath string `yaml:"database-path"`
|
|
||||||
BusyTimeout time.Duration `yaml:"busy-timeout"`
|
|
||||||
MaxConns int `yaml:"max-conns"`
|
|
||||||
|
|
||||||
// XXX these are copied from elsewhere in the config:
|
|
||||||
ExpireTime time.Duration
|
|
||||||
TrackAccountMessages bool
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,34 +0,0 @@
|
|||||||
//go:build !sqlite || !(linux || darwin || freebsd || windows)
|
|
||||||
|
|
||||||
// Copyright (c) 2020 Shivaram Lingamneni
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
// Package sqlite provides a stub implementation when SQLite support is not enabled.
|
|
||||||
// To enable SQLite support, build with: make build_full
|
|
||||||
// This stub prevents the binary from including the large modernc.org/sqlite driver dependencies.
|
|
||||||
package sqlite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/history"
|
|
||||||
"github.com/ergochat/ergo/irc/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Enabled is false when SQLite support is not compiled in
|
|
||||||
const Enabled = false
|
|
||||||
|
|
||||||
// SQLite is a stub implementation when the sqlite build tag is not present
|
|
||||||
type SQLite struct {
|
|
||||||
history.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSQLiteDatabase returns an error when SQLite support is not compiled in
|
|
||||||
func NewSQLiteDatabase(logger *logger.Manager, config Config) (*SQLite, error) {
|
|
||||||
return nil, errors.New("SQLite support not enabled in this build. Rebuild with `make build_full` to enable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetConfig is a no-op for the stub implementation
|
|
||||||
func (s *SQLite) SetConfig(config Config) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
144
irc/strings.go
144
irc/strings.go
@ -7,9 +7,15 @@ package irc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/i18n"
|
"github.com/ergochat/confusables"
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/secure/precis"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
"golang.org/x/text/width"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,20 +38,87 @@ const (
|
|||||||
disfavoredNameCharacters = `<>'";#`
|
disfavoredNameCharacters = `<>'";#`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// reviving the old ergonomadic nickname regex:
|
||||||
|
// in permissive mode, allow arbitrary letters, numbers, punctuation, and symbols
|
||||||
|
permissiveCharsRegex = regexp.MustCompile(`^[\pL\pN\pP\pS]*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Casemapping uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
// "precis" is the default / zero value:
|
||||||
|
// casefolding/validation: PRECIS + ircd restrictions (like no *)
|
||||||
|
// confusables detection: standard skeleton algorithm
|
||||||
|
CasemappingPRECIS Casemapping = iota
|
||||||
|
// "ascii" is the traditional ircd behavior:
|
||||||
|
// casefolding/validation: must be pure ASCII and follow ircd restrictions, ASCII lowercasing
|
||||||
|
// confusables detection: none
|
||||||
|
CasemappingASCII
|
||||||
|
// "permissive" is an insecure mode:
|
||||||
|
// casefolding/validation: arbitrary unicodes that follow ircd restrictions, unicode casefolding
|
||||||
|
// confusables detection: standard skeleton algorithm (which may be ineffective
|
||||||
|
// over the larger set of permitted identifiers)
|
||||||
|
CasemappingPermissive
|
||||||
|
// rfc1459 is a legacy mapping as defined here: https://modern.ircdocs.horse/#casemapping-parameter
|
||||||
|
CasemappingRFC1459
|
||||||
|
// rfc1459-strict is a legacy mapping as defined here: https://modern.ircdocs.horse/#casemapping-parameter
|
||||||
|
CasemappingRFC1459Strict
|
||||||
|
)
|
||||||
|
|
||||||
// XXX this is a global variable without explicit synchronization.
|
// XXX this is a global variable without explicit synchronization.
|
||||||
// it gets set during the initial Server.applyConfig and cannot be changed by rehash:
|
// it gets set during the initial Server.applyConfig and cannot be changed by rehash:
|
||||||
// this happens-before all IRC connections and all casefolding operations.
|
// this happens-before all IRC connections and all casefolding operations.
|
||||||
var globalCasemappingSetting i18n.Casemapping = i18n.DefaultCasemapping
|
var globalCasemappingSetting Casemapping = CasemappingPRECIS
|
||||||
|
|
||||||
// XXX analogous unsynchronized global variable controlling utf8 validation
|
// XXX analogous unsynchronized global variable controlling utf8 validation
|
||||||
// if this is off, you get the traditional IRC behavior (relaying any valid RFC1459
|
// if this is off, you get the traditional IRC behavior (relaying any valid RFC1459
|
||||||
// octets), and websocket listeners are disabled.
|
// octets) and invalid utf8 messages are silently dropped for websocket clients only.
|
||||||
// if this is on, invalid utf8 inputs get a FAIL reply.
|
// if this is on, invalid utf8 inputs get a FAIL reply.
|
||||||
var globalUtf8EnforcementSetting bool
|
var globalUtf8EnforcementSetting bool
|
||||||
|
|
||||||
|
// Each pass of PRECIS casefolding is a composition of idempotent operations,
|
||||||
|
// but not idempotent itself. Therefore, the spec says "do it four times and hope
|
||||||
|
// it converges" (lolwtf). Golang's PRECIS implementation has a "repeat" option,
|
||||||
|
// which provides this functionality, but unfortunately it's not exposed publicly.
|
||||||
|
func iterateFolding(profile *precis.Profile, oldStr string) (str string, err error) {
|
||||||
|
str = oldStr
|
||||||
|
// follow the stabilizing rules laid out here:
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-precis-7564bis-10.html#section-7
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
str, err = profile.CompareKey(str)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if oldStr == str {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
oldStr = str
|
||||||
|
}
|
||||||
|
if oldStr != str {
|
||||||
|
return "", errCouldNotStabilize
|
||||||
|
}
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Casefold returns a casefolded string, without doing any name or channel character checks.
|
// Casefold returns a casefolded string, without doing any name or channel character checks.
|
||||||
func Casefold(str string) (string, error) {
|
func Casefold(str string) (string, error) {
|
||||||
return i18n.CasefoldWithSetting(str, globalCasemappingSetting)
|
return casefoldWithSetting(str, globalCasemappingSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
func casefoldWithSetting(str string, setting Casemapping) (string, error) {
|
||||||
|
switch setting {
|
||||||
|
default:
|
||||||
|
return iterateFolding(precis.UsernameCaseMapped, str)
|
||||||
|
case CasemappingASCII:
|
||||||
|
return foldASCII(str)
|
||||||
|
case CasemappingPermissive:
|
||||||
|
return foldPermissive(str)
|
||||||
|
case CasemappingRFC1459:
|
||||||
|
return foldRFC1459(str, false)
|
||||||
|
case CasemappingRFC1459Strict:
|
||||||
|
return foldRFC1459(str, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CasefoldChannel returns a casefolded version of a channel name.
|
// CasefoldChannel returns a casefolded version of a channel name.
|
||||||
@ -138,17 +211,39 @@ func isIdent(name string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skeleton produces a canonicalized identifier that tries to catch
|
// Skeleton produces a canonicalized identifier that tries to catch
|
||||||
// homoglyphic / confusable identifiers.
|
// homoglyphic / confusable identifiers. It's a tweaked version of the TR39
|
||||||
|
// skeleton algorithm. We apply the skeleton algorithm first and only then casefold,
|
||||||
|
// because casefolding first would lose some information about visual confusability.
|
||||||
|
// This has the weird consequence that the skeleton is not a function of the
|
||||||
|
// casefolded identifier --- therefore it must always be computed
|
||||||
|
// from the original (unfolded) identifier and stored/tracked separately from the
|
||||||
|
// casefolded identifier.
|
||||||
func Skeleton(name string) (string, error) {
|
func Skeleton(name string) (string, error) {
|
||||||
switch globalCasemappingSetting {
|
switch globalCasemappingSetting {
|
||||||
default:
|
default:
|
||||||
return i18n.Skeleton(name)
|
return realSkeleton(name)
|
||||||
case i18n.CasemappingASCII, i18n.CasemappingRFC1459, i18n.CasemappingRFC1459Strict:
|
case CasemappingASCII, CasemappingRFC1459, CasemappingRFC1459Strict:
|
||||||
// identity function is fine because we independently case-normalize in Casefold
|
// identity function is fine because we independently case-normalize in Casefold
|
||||||
return name, nil
|
return name, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func realSkeleton(name string) (string, error) {
|
||||||
|
// XXX the confusables table includes some, but not all, fullwidth->standard
|
||||||
|
// mappings for latin characters. do a pass of explicit width folding,
|
||||||
|
// same as PRECIS:
|
||||||
|
name = width.Fold.String(name)
|
||||||
|
|
||||||
|
name = confusables.SkeletonTweaked(name)
|
||||||
|
|
||||||
|
// internationalized lowercasing for skeletons; this is much more lenient than
|
||||||
|
// Casefold. In particular, skeletons are expected to mix scripts (which may
|
||||||
|
// violate the bidi rule). We also don't care if they contain runes
|
||||||
|
// that are disallowed by PRECIS, because every identifier must independently
|
||||||
|
// pass PRECIS --- we are just further canonicalizing the skeleton.
|
||||||
|
return cases.Fold().String(name), nil
|
||||||
|
}
|
||||||
|
|
||||||
// maps a nickmask fragment to an expanded, casefolded wildcard:
|
// maps a nickmask fragment to an expanded, casefolded wildcard:
|
||||||
// Shivaram@good-fortune -> *!shivaram@good-fortune
|
// Shivaram@good-fortune -> *!shivaram@good-fortune
|
||||||
// EDMUND -> edmund!*@*
|
// EDMUND -> edmund!*@*
|
||||||
@ -208,6 +303,30 @@ func CanonicalizeMaskWildcard(userhost string) (expanded string, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func foldASCII(str string) (result string, err error) {
|
||||||
|
if !IsPrintableASCII(str) {
|
||||||
|
return "", errInvalidCharacter
|
||||||
|
}
|
||||||
|
return strings.ToLower(str), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
rfc1459Replacer = strings.NewReplacer("[", "{", "]", "}", "\\", "|", "~", "^")
|
||||||
|
rfc1459StrictReplacer = strings.NewReplacer("[", "{", "]", "}", "\\", "|")
|
||||||
|
)
|
||||||
|
|
||||||
|
func foldRFC1459(str string, strict bool) (result string, err error) {
|
||||||
|
asciiFold, err := foldASCII(str)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
replacer := rfc1459Replacer
|
||||||
|
if strict {
|
||||||
|
replacer = rfc1459StrictReplacer
|
||||||
|
}
|
||||||
|
return replacer.Replace(asciiFold), nil
|
||||||
|
}
|
||||||
|
|
||||||
func IsPrintableASCII(str string) bool {
|
func IsPrintableASCII(str string) bool {
|
||||||
for i := 0; i < len(str); i++ {
|
for i := 0; i < len(str); i++ {
|
||||||
// allow space here because it's technically printable;
|
// allow space here because it's technically printable;
|
||||||
@ -220,6 +339,17 @@ func IsPrintableASCII(str string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func foldPermissive(str string) (result string, err error) {
|
||||||
|
if !permissiveCharsRegex.MatchString(str) {
|
||||||
|
return "", errInvalidCharacter
|
||||||
|
}
|
||||||
|
// YOLO
|
||||||
|
str = norm.NFD.String(str)
|
||||||
|
str = cases.Fold().String(str)
|
||||||
|
str = norm.NFD.String(str)
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reduce, e.g., `alice!~u@host` to `alice`
|
// Reduce, e.g., `alice!~u@host` to `alice`
|
||||||
func NUHToNick(nuh string) (nick string) {
|
func NUHToNick(nuh string) (nick string) {
|
||||||
if idx := strings.IndexByte(nuh, '!'); idx != -1 {
|
if idx := strings.IndexByte(nuh, '!'); idx != -1 {
|
||||||
|
|||||||
@ -7,27 +7,13 @@ package irc
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/i18n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func TestCasefoldChannel(t *testing.T) {
|
||||||
asciiCasemappingOnly = []i18n.Casemapping{i18n.CasemappingASCII}
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCasefoldChannelAllCasemappings(t *testing.T) {
|
|
||||||
oldGlobalCasemapping := globalCasemappingSetting
|
|
||||||
t.Cleanup(func() {
|
|
||||||
globalCasemappingSetting = oldGlobalCasemapping
|
|
||||||
})
|
|
||||||
|
|
||||||
globalCasemappingSetting = i18n.CasemappingPRECIS
|
|
||||||
|
|
||||||
type channelTest struct {
|
type channelTest struct {
|
||||||
channel string
|
channel string
|
||||||
folded string
|
folded string
|
||||||
nonASCII bool
|
err bool
|
||||||
err bool
|
|
||||||
}
|
}
|
||||||
testCases := []channelTest{
|
testCases := []channelTest{
|
||||||
{
|
{
|
||||||
@ -63,20 +49,18 @@ func TestCasefoldChannelAllCasemappings(t *testing.T) {
|
|||||||
folded: "##ubuntu",
|
folded: "##ubuntu",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
channel: "#中文频道",
|
channel: "#中文频道",
|
||||||
folded: "#中文频道",
|
folded: "#中文频道",
|
||||||
nonASCII: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Hebrew; it's up to the client to display this right-to-left, including the #
|
// Hebrew; it's up to the client to display this right-to-left, including the #
|
||||||
channel: "#שלום",
|
channel: "#שלום",
|
||||||
folded: "#שלום",
|
folded: "#שלום",
|
||||||
nonASCII: true,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, errCase := range []string{
|
for _, errCase := range []string{
|
||||||
"", "#*starpower", "# NASA", "#interro?", "OOF#", "foo", "a b", "#a b",
|
"", "#*starpower", "# NASA", "#interro?", "OOF#", "foo",
|
||||||
// bidi violation mixing latin and hebrew characters:
|
// bidi violation mixing latin and hebrew characters:
|
||||||
"#shalomעליכם",
|
"#shalomעליכם",
|
||||||
"#tab\tcharacter", "#\t", "#carriage\rreturn",
|
"#tab\tcharacter", "#\t", "#carriage\rreturn",
|
||||||
@ -84,41 +68,25 @@ func TestCasefoldChannelAllCasemappings(t *testing.T) {
|
|||||||
testCases = append(testCases, channelTest{channel: errCase, err: true})
|
testCases = append(testCases, channelTest{channel: errCase, err: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't test permissive because it doesn't fail on bidi violations
|
for i, tt := range testCases {
|
||||||
casemappings := []i18n.Casemapping{i18n.CasemappingASCII, i18n.CasemappingPRECIS}
|
t.Run(fmt.Sprintf("case %d: %s", i, tt.channel), func(t *testing.T) {
|
||||||
if !i18n.Enabled {
|
res, err := CasefoldChannel(tt.channel)
|
||||||
casemappings = asciiCasemappingOnly // XXX allow testing this package with i18n compiled out
|
if tt.err && err == nil {
|
||||||
}
|
t.Errorf("expected error when casefolding [%s], but did not receive one", tt.channel)
|
||||||
|
return
|
||||||
for _, casemapping := range casemappings {
|
}
|
||||||
globalCasemappingSetting = casemapping
|
if !tt.err && err != nil {
|
||||||
|
t.Errorf("unexpected error while casefolding [%s]: %s", tt.channel, err.Error())
|
||||||
for i, tt := range testCases {
|
return
|
||||||
t.Run(fmt.Sprintf("case %d: %s", i, tt.channel), func(t *testing.T) {
|
}
|
||||||
res, err := CasefoldChannel(tt.channel)
|
if tt.folded != res {
|
||||||
errExpected := tt.err || (tt.nonASCII && (casemapping == i18n.CasemappingASCII || casemapping == i18n.CasemappingRFC1459Strict))
|
t.Errorf("expected [%v] to be [%v]", res, tt.folded)
|
||||||
if errExpected && err == nil {
|
}
|
||||||
t.Errorf("expected error when casefolding [%s] under casemapping %d, but did not receive one", tt.channel, casemapping)
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
if !errExpected && err != nil {
|
|
||||||
t.Errorf("unexpected error while casefolding [%s] under casemapping %d: %s", tt.channel, casemapping, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !errExpected && tt.folded != res {
|
|
||||||
t.Errorf("expected [%v] to be [%v] under casemapping %d", res, tt.folded, casemapping)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCasefoldNameAllCasemappings(t *testing.T) {
|
func TestCasefoldName(t *testing.T) {
|
||||||
oldGlobalCasemapping := globalCasemappingSetting
|
|
||||||
t.Cleanup(func() {
|
|
||||||
globalCasemappingSetting = oldGlobalCasemapping
|
|
||||||
})
|
|
||||||
|
|
||||||
type nameTest struct {
|
type nameTest struct {
|
||||||
name string
|
name string
|
||||||
folded string
|
folded string
|
||||||
@ -136,37 +104,28 @@ func TestCasefoldNameAllCasemappings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, errCase := range []string{
|
for _, errCase := range []string{
|
||||||
"", "#", "foo,bar", "star*man*junior", "lo7t?", "a b", "#a b",
|
"", "#", "foo,bar", "star*man*junior", "lo7t?",
|
||||||
"f.l", "excited!nick", "foo@bar", ":trail",
|
"f.l", "excited!nick", "foo@bar", ":trail",
|
||||||
"~o", "&o", "@o", "%h", "+v", "-m", "\t", "a\tb",
|
"~o", "&o", "@o", "%h", "+v", "-m", "\t", "a\tb",
|
||||||
} {
|
} {
|
||||||
testCases = append(testCases, nameTest{name: errCase, err: true})
|
testCases = append(testCases, nameTest{name: errCase, err: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
casemappings := []i18n.Casemapping{i18n.CasemappingASCII, i18n.CasemappingPRECIS, i18n.CasemappingPermissive, i18n.CasemappingRFC1459Strict}
|
for i, tt := range testCases {
|
||||||
if !i18n.Enabled {
|
t.Run(fmt.Sprintf("case %d: %s", i, tt.name), func(t *testing.T) {
|
||||||
casemappings = asciiCasemappingOnly // XXX allow testing this package with i18n compiled out
|
res, err := CasefoldName(tt.name)
|
||||||
}
|
if tt.err && err == nil {
|
||||||
|
t.Errorf("expected error when casefolding [%s], but did not receive one", tt.name)
|
||||||
for _, casemapping := range casemappings {
|
return
|
||||||
globalCasemappingSetting = casemapping
|
}
|
||||||
|
if !tt.err && err != nil {
|
||||||
for i, tt := range testCases {
|
t.Errorf("unexpected error while casefolding [%s]: %s", tt.name, err.Error())
|
||||||
t.Run(fmt.Sprintf("case %d: %s", i, tt.name), func(t *testing.T) {
|
return
|
||||||
res, err := CasefoldName(tt.name)
|
}
|
||||||
if tt.err && err == nil {
|
if tt.folded != res {
|
||||||
t.Errorf("expected error when casefolding [%s], but did not receive one", tt.name)
|
t.Errorf("expected [%v] to be [%v]", res, tt.folded)
|
||||||
return
|
}
|
||||||
}
|
})
|
||||||
if !tt.err && err != nil {
|
|
||||||
t.Errorf("unexpected error while casefolding [%s]: %s", tt.name, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tt.folded != res {
|
|
||||||
t.Errorf("expected [%v] to be [%v]", res, tt.folded)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +145,51 @@ func TestIsIdent(t *testing.T) {
|
|||||||
assertIdent("-dan56", false)
|
assertIdent("-dan56", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSkeleton(t *testing.T) {
|
||||||
|
skeleton := func(str string) string {
|
||||||
|
skel, err := Skeleton(str)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
return skel
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("warning") == skeleton("waming") {
|
||||||
|
t.Errorf("Oragono shouldn't consider rn confusable with m")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("Phi|ip") != "philip" {
|
||||||
|
t.Errorf("but we still consider pipe confusable with l")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("smt") != skeleton("smt") {
|
||||||
|
t.Errorf("fullwidth characters should skeletonize to plain old ascii characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("SMT") != skeleton("smt") {
|
||||||
|
t.Errorf("after skeletonizing, we should casefold")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("smt") != skeleton("smt") {
|
||||||
|
t.Errorf("our friend lover successfully tricked the skeleton algorithm!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("еvan") != "evan" {
|
||||||
|
t.Errorf("we must protect against cyrillic homoglyph attacks")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("еmily") != skeleton("emily") {
|
||||||
|
t.Errorf("we must protect against cyrillic homoglyph attacks")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("РОТАТО") != "potato" {
|
||||||
|
t.Errorf("we must protect against cyrillic homoglyph attacks")
|
||||||
|
}
|
||||||
|
|
||||||
|
// should not raise an error:
|
||||||
|
skeleton("けらんぐ")
|
||||||
|
}
|
||||||
|
|
||||||
func TestCanonicalizeMaskWildcard(t *testing.T) {
|
func TestCanonicalizeMaskWildcard(t *testing.T) {
|
||||||
tester := func(input, expected string, expectedErr error) {
|
tester := func(input, expected string, expectedErr error) {
|
||||||
out, err := CanonicalizeMaskWildcard(input)
|
out, err := CanonicalizeMaskWildcard(input)
|
||||||
@ -199,8 +203,10 @@ func TestCanonicalizeMaskWildcard(t *testing.T) {
|
|||||||
|
|
||||||
tester("shivaram", "shivaram!*@*", nil)
|
tester("shivaram", "shivaram!*@*", nil)
|
||||||
tester("slingamn!shivaram", "slingamn!shivaram@*", nil)
|
tester("slingamn!shivaram", "slingamn!shivaram@*", nil)
|
||||||
|
tester("ברוך", "ברוך!*@*", nil)
|
||||||
tester("hacker@monad.io", "*!hacker@monad.io", nil)
|
tester("hacker@monad.io", "*!hacker@monad.io", nil)
|
||||||
tester("Evan!hacker@monad.io", "evan!hacker@monad.io", nil)
|
tester("Evan!hacker@monad.io", "evan!hacker@monad.io", nil)
|
||||||
|
tester("РОТАТО!Potato", "ротато!potato@*", nil)
|
||||||
tester("tkadich*", "tkadich*!*@*", nil)
|
tester("tkadich*", "tkadich*!*@*", nil)
|
||||||
tester("SLINGAMN!*@*", "slingamn!*@*", nil)
|
tester("SLINGAMN!*@*", "slingamn!*@*", nil)
|
||||||
tester("slingamn!shivaram*", "slingamn!shivaram*@*", nil)
|
tester("slingamn!shivaram*", "slingamn!shivaram*@*", nil)
|
||||||
@ -214,9 +220,90 @@ func TestCanonicalizeMaskWildcard(t *testing.T) {
|
|||||||
tester(":shivaram", "", errInvalidCharacter)
|
tester(":shivaram", "", errInvalidCharacter)
|
||||||
tester("shivaram!us er@host", "", errInvalidCharacter)
|
tester("shivaram!us er@host", "", errInvalidCharacter)
|
||||||
tester("shivaram!user@ho st", "", errInvalidCharacter)
|
tester("shivaram!user@ho st", "", errInvalidCharacter)
|
||||||
|
}
|
||||||
|
|
||||||
if i18n.Enabled {
|
func validFoldTester(first, second string, equal bool, folder func(string) (string, error), t *testing.T) {
|
||||||
tester("ברוך", "ברוך!*@*", nil)
|
firstFolded, err := folder(first)
|
||||||
tester("РОТАТО!Potato", "ротато!potato@*", nil)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
secondFolded, err := folder(second)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
foundEqual := firstFolded == secondFolded
|
||||||
|
if foundEqual != equal {
|
||||||
|
t.Errorf("%s and %s: expected equality %t, but got %t", first, second, equal, foundEqual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFoldPermissive(t *testing.T) {
|
||||||
|
tester := func(first, second string, equal bool) {
|
||||||
|
validFoldTester(first, second, equal, foldPermissive, t)
|
||||||
|
}
|
||||||
|
tester("SHIVARAM", "shivaram", true)
|
||||||
|
tester("shIvaram", "shivaraM", true)
|
||||||
|
tester("shivaram", "DAN-", false)
|
||||||
|
tester("dolph🐬n", "DOLPH🐬n", true)
|
||||||
|
tester("dolph🐬n", "dolph💻n", false)
|
||||||
|
tester("9FRONT", "9front", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldPermissiveInvalid(t *testing.T) {
|
||||||
|
_, err := foldPermissive("a\tb")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("whitespace should be invalid in identifiers")
|
||||||
|
}
|
||||||
|
_, err = foldPermissive("a\x00b")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("the null byte should be invalid in identifiers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldASCII(t *testing.T) {
|
||||||
|
tester := func(first, second string, equal bool) {
|
||||||
|
validFoldTester(first, second, equal, foldASCII, t)
|
||||||
|
}
|
||||||
|
tester("shivaram", "SHIVARAM", true)
|
||||||
|
tester("X|Y", "x|y", true)
|
||||||
|
tester("a != b", "A != B", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldASCIIInvalid(t *testing.T) {
|
||||||
|
_, err := foldASCII("\x01")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("control characters should be invalid in identifiers")
|
||||||
|
}
|
||||||
|
_, err = foldASCII("\x7F")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("control characters should be invalid in identifiers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldRFC1459(t *testing.T) {
|
||||||
|
folder := func(str string) (string, error) {
|
||||||
|
return foldRFC1459(str, false)
|
||||||
|
}
|
||||||
|
tester := func(first, second string, equal bool) {
|
||||||
|
validFoldTester(first, second, equal, folder, t)
|
||||||
|
}
|
||||||
|
tester("shivaram", "SHIVARAM", true)
|
||||||
|
tester("shivaram[a]", "shivaram{a}", true)
|
||||||
|
tester("shivaram\\a]", "shivaram{a}", false)
|
||||||
|
tester("shivaram\\a]", "shivaram|a}", true)
|
||||||
|
tester("shivaram~a]", "shivaram^a}", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFoldRFC1459Strict(t *testing.T) {
|
||||||
|
folder := func(str string) (string, error) {
|
||||||
|
return foldRFC1459(str, true)
|
||||||
|
}
|
||||||
|
tester := func(first, second string, equal bool) {
|
||||||
|
validFoldTester(first, second, equal, folder, t)
|
||||||
|
}
|
||||||
|
tester("shivaram", "SHIVARAM", true)
|
||||||
|
tester("shivaram[a]", "shivaram{a}", true)
|
||||||
|
tester("shivaram\\a]", "shivaram{a}", false)
|
||||||
|
tester("shivaram\\a]", "shivaram|a}", true)
|
||||||
|
tester("shivaram~a]", "shivaram^a}", false)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import "iter"
|
|
||||||
|
|
||||||
func ChunkifyParams(params iter.Seq[string], maxChars int) [][]string {
|
|
||||||
var chunked [][]string
|
|
||||||
|
|
||||||
var acc []string
|
|
||||||
var length = 0
|
|
||||||
|
|
||||||
for p := range params {
|
|
||||||
length = length + len(p) + 1 // (accounting for the space)
|
|
||||||
|
|
||||||
if length > maxChars {
|
|
||||||
chunked = append(chunked, acc)
|
|
||||||
acc = []string{}
|
|
||||||
length = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
acc = append(acc, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(acc) != 0 {
|
|
||||||
chunked = append(chunked, acc)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chunked
|
|
||||||
}
|
|
||||||
@ -42,11 +42,6 @@ func GenerateSecretToken() string {
|
|||||||
return B32Encoder.EncodeToString(buf[:])
|
return B32Encoder.EncodeToString(buf[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// return a compact representation of a token generated by GenerateSecretToken()
|
|
||||||
func DecodeSecretToken(t string) ([]byte, error) {
|
|
||||||
return B32Encoder.DecodeString(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// securely check if a supplied token matches a stored token
|
// securely check if a supplied token matches a stored token
|
||||||
func SecretTokensMatch(storedToken string, suppliedToken string) bool {
|
func SecretTokensMatch(storedToken string, suppliedToken string) bool {
|
||||||
// XXX fix a potential gotcha: if the stored token is uninitialized,
|
// XXX fix a potential gotcha: if the stored token is uninitialized,
|
||||||
|
|||||||
@ -6,7 +6,6 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
@ -21,8 +20,24 @@ const (
|
|||||||
maxProxyLineLenV1 = 107
|
maxProxyLineLenV1 = 107
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XXX implement net.Error with a Temporary() method that returns true;
|
||||||
|
// otherwise, ErrBadProxyLine will cause (*http.Server).Serve() to exit
|
||||||
|
type proxyLineError struct{}
|
||||||
|
|
||||||
|
func (p *proxyLineError) Error() string {
|
||||||
|
return "invalid PROXY line"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *proxyLineError) Timeout() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *proxyLineError) Temporary() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrBadProxyLine = errors.New("invalid PROXY protocol line")
|
ErrBadProxyLine error = &proxyLineError{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListenerConfig is all the information about how to process
|
// ListenerConfig is all the information about how to process
|
||||||
@ -193,13 +208,12 @@ func parseProxyLineV2(line []byte) (ip net.IP, err error) {
|
|||||||
// configuration.
|
// configuration.
|
||||||
type WrappedConn struct {
|
type WrappedConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
ProxiedIP net.IP
|
ProxiedIP net.IP
|
||||||
ProxyError error
|
TLS bool
|
||||||
TLS bool
|
Tor bool
|
||||||
Tor bool
|
STSOnly bool
|
||||||
STSOnly bool
|
WebSocket bool
|
||||||
WebSocket bool
|
HideSTS bool
|
||||||
HideSTS bool
|
|
||||||
// Secure indicates whether we believe the connection between us and the client
|
// Secure indicates whether we believe the connection between us and the client
|
||||||
// was secure against interception and modification (including all proxies):
|
// was secure against interception and modification (including all proxies):
|
||||||
Secure bool
|
Secure bool
|
||||||
@ -242,7 +256,6 @@ func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var proxiedIP net.IP
|
var proxiedIP net.IP
|
||||||
var proxyError error
|
|
||||||
if config.RequireProxy {
|
if config.RequireProxy {
|
||||||
// this will occur synchronously on the goroutine calling Accept(),
|
// this will occur synchronously on the goroutine calling Accept(),
|
||||||
// but that's OK because this listener *requires* a PROXY line,
|
// but that's OK because this listener *requires* a PROXY line,
|
||||||
@ -252,7 +265,10 @@ func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
proxiedIP, err = ParseProxyLine(proxyLine)
|
proxiedIP, err = ParseProxyLine(proxyLine)
|
||||||
}
|
}
|
||||||
proxyError = err
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.TLSConfig != nil {
|
if config.TLSConfig != nil {
|
||||||
@ -260,14 +276,13 @@ func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &WrappedConn{
|
return &WrappedConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
ProxiedIP: proxiedIP,
|
ProxiedIP: proxiedIP,
|
||||||
ProxyError: proxyError,
|
TLS: config.TLSConfig != nil,
|
||||||
TLS: config.TLSConfig != nil,
|
Tor: config.Tor,
|
||||||
Tor: config.Tor,
|
STSOnly: config.STSOnly,
|
||||||
STSOnly: config.STSOnly,
|
WebSocket: config.WebSocket,
|
||||||
WebSocket: config.WebSocket,
|
HideSTS: config.HideSTS,
|
||||||
HideSTS: config.HideSTS,
|
|
||||||
// Secure will be set later by client code
|
// Secure will be set later by client code
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import "fmt"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// SemVer is the semantic version of Ergo.
|
// SemVer is the semantic version of Ergo.
|
||||||
SemVer = "2.19.0-unreleased"
|
SemVer = "2.16.0-rc1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ergochat/irc-go/ircmsg"
|
"github.com/ergochat/irc-go/ircmsg"
|
||||||
webpush "github.com/ergochat/webpush-go/v2"
|
webpush "github.com/ergochat/webpush-go/v2"
|
||||||
@ -96,21 +97,18 @@ func MakePushMessage(command, nuh, accountName, target string, msg utils.SplitMe
|
|||||||
} else {
|
} else {
|
||||||
messageForPush = msg.Split[0].Message
|
messageForPush = msg.Split[0].Message
|
||||||
}
|
}
|
||||||
|
return MakePushLine(msg.Time, accountName, nuh, command, target, messageForPush)
|
||||||
|
}
|
||||||
|
|
||||||
pushMessage := ircmsg.MakeMessage(nil, nuh, command, target, messageForPush)
|
// MakePushLine serializes an arbitrary IRC line as a web push message (the args are in
|
||||||
pushMessage.SetTag("time", msg.Time.Format(utils.IRCv3TimestampFormat))
|
// IRC syntax order)
|
||||||
pushMessage.SetTag("msgid", msg.Msgid)
|
func MakePushLine(time time.Time, accountName, source, command string, params ...string) ([]byte, error) {
|
||||||
|
pushMessage := ircmsg.MakeMessage(nil, source, command, params...)
|
||||||
|
pushMessage.SetTag("time", time.Format(utils.IRCv3TimestampFormat))
|
||||||
// "*" is canonical for the unset form of the unfolded account name, but check both:
|
// "*" is canonical for the unset form of the unfolded account name, but check both:
|
||||||
if accountName != "*" && accountName != "" {
|
if accountName != "*" && accountName != "" {
|
||||||
pushMessage.SetTag("account", accountName)
|
pushMessage.SetTag("account", accountName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return MakePushLine(pushMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakePushLine serializes an arbitrary IRC message as a web push message;
|
|
||||||
// we assume tags were already filtered.
|
|
||||||
func MakePushLine(pushMessage ircmsg.Message) ([]byte, error) {
|
|
||||||
if line, err := pushMessage.LineBytesStrict(false, 512); err == nil {
|
if line, err := pushMessage.LineBytesStrict(false, 512); err == nil {
|
||||||
// strip final \r\n
|
// strip final \r\n
|
||||||
return line[:len(line)-2], nil
|
return line[:len(line)-2], nil
|
||||||
|
|||||||
@ -11,13 +11,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildPushLine(t *testing.T) {
|
func TestBuildPushLine(t *testing.T) {
|
||||||
now := "2025-01-12T00:55:44.403Z"
|
now, err := time.Parse(utils.IRCv3TimestampFormat, "2025-01-12T00:55:44.403Z")
|
||||||
readTimestamp := "timestamp=2025-01-12T00:07:57.972Z"
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
markreadPushMessage := ircmsg.MakeMessage(nil, "ergo.test", "MARKREAD", "#ergo", readTimestamp)
|
line, err := MakePushLine(now, "*", "ergo.test", "MARKREAD", "#ergo", "timestamp=2025-01-12T00:07:57.972Z")
|
||||||
markreadPushMessage.SetTag("time", now)
|
|
||||||
|
|
||||||
line, err := MakePushLine(markreadPushMessage)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -218,7 +218,7 @@ func zncPlayPrivmsgsFromAll(client *Client, rb *ResponseBuffer, start, end time.
|
|||||||
// PRIVMSG *playback :list
|
// PRIVMSG *playback :list
|
||||||
func zncPlaybackListHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
|
func zncPlaybackListHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
limit := client.server.Config().History.ChathistoryMax
|
limit := client.server.Config().History.ChathistoryMax
|
||||||
correspondents, err := client.listTargets(time.Time{}, time.Time{}, limit)
|
correspondents, err := client.listTargets(history.Selector{}, history.Selector{}, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.server.logger.Error("internal", "couldn't get history for ZNC list", err.Error())
|
client.server.logger.Error("internal", "couldn't get history for ZNC list", err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
2
irctest
2
irctest
@ -1 +1 @@
|
|||||||
Subproject commit 17fac53c5cdfe78caecb601399512574f242cc85
|
Subproject commit e9e37f5438bd5f02656b89dab0cd40ef113edac6
|
||||||
@ -154,21 +154,6 @@ server:
|
|||||||
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
|
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
|
||||||
motd-formatting: true
|
motd-formatting: true
|
||||||
|
|
||||||
# send a configurable notice to clients immediately after they connect,
|
|
||||||
# as a way of detecting open proxies
|
|
||||||
#initial-notice: "*** Welcome to the Ergo IRC server"
|
|
||||||
|
|
||||||
# idle timeouts for inactive clients
|
|
||||||
idle-timeouts:
|
|
||||||
# give the client this long to complete connection registration (i.e. the initial
|
|
||||||
# IRC handshake, including capability negotiation and SASL)
|
|
||||||
registration: 60s
|
|
||||||
# if the client hasn't sent anything for this long, send them a PING
|
|
||||||
ping: 1m30s
|
|
||||||
# if the client hasn't sent anything for this long (including the PONG to the
|
|
||||||
# above PING), disconnect them
|
|
||||||
disconnect: 2m30s
|
|
||||||
|
|
||||||
# relaying using the RELAYMSG command
|
# relaying using the RELAYMSG command
|
||||||
relaymsg:
|
relaymsg:
|
||||||
# is relaymsg enabled at all?
|
# is relaymsg enabled at all?
|
||||||
@ -345,10 +330,6 @@ server:
|
|||||||
secure-nets:
|
secure-nets:
|
||||||
# - "10.0.0.0/8"
|
# - "10.0.0.0/8"
|
||||||
|
|
||||||
# allow attempts to OPER with a password at most this often. defaults to
|
|
||||||
# 10 seconds when unset.
|
|
||||||
oper-throttle: 10s
|
|
||||||
|
|
||||||
# Ergo will write files to disk under certain circumstances, e.g.,
|
# Ergo will write files to disk under certain circumstances, e.g.,
|
||||||
# CPU profiling or data export. by default, these files will be written
|
# CPU profiling or data export. by default, these files will be written
|
||||||
# to the working directory. set this to customize:
|
# to the working directory. set this to customize:
|
||||||
@ -513,7 +494,7 @@ accounts:
|
|||||||
# 1. these nicknames cannot be registered or reserved
|
# 1. these nicknames cannot be registered or reserved
|
||||||
# 2. if a client is automatically renamed by the server,
|
# 2. if a client is automatically renamed by the server,
|
||||||
# this is the template that will be used (e.g., Guest-nccj6rgmt97cg)
|
# this is the template that will be used (e.g., Guest-nccj6rgmt97cg)
|
||||||
# 3. if force-guest-format (see below) is enabled, clients without
|
# 3. if enforce-guest-format (see below) is enabled, clients without
|
||||||
# a registered account will have this template applied to their
|
# a registered account will have this template applied to their
|
||||||
# nicknames (e.g., 'katie' will become 'Guest-katie')
|
# nicknames (e.g., 'katie' will become 'Guest-katie')
|
||||||
guest-nickname-format: "Guest-*"
|
guest-nickname-format: "Guest-*"
|
||||||
@ -714,7 +695,6 @@ oper-classes:
|
|||||||
- "history" # modify or delete history messages
|
- "history" # modify or delete history messages
|
||||||
- "defcon" # use the DEFCON command (restrict server capabilities)
|
- "defcon" # use the DEFCON command (restrict server capabilities)
|
||||||
- "massmessage" # message all users on the server
|
- "massmessage" # message all users on the server
|
||||||
- "metadata" # modify arbitrary metadata on channels and users
|
|
||||||
|
|
||||||
# ircd operators
|
# ircd operators
|
||||||
opers:
|
opers:
|
||||||
@ -846,43 +826,6 @@ datastore:
|
|||||||
# this may be necessary to prevent middleware from closing your connections:
|
# this may be necessary to prevent middleware from closing your connections:
|
||||||
#conn-max-lifetime: 180s
|
#conn-max-lifetime: 180s
|
||||||
|
|
||||||
# connection information for PostgreSQL (currently only used for persistent history)
|
|
||||||
postgresql:
|
|
||||||
enabled: false
|
|
||||||
host: "localhost"
|
|
||||||
port: 5432
|
|
||||||
# if socket-path is set, it will be used instead of host:port
|
|
||||||
# PostgreSQL uses the socket directory, not the socket file path
|
|
||||||
#socket-path: "/var/run/postgresql"
|
|
||||||
# PostgreSQL SSL/TLS configuration:
|
|
||||||
ssl-mode: "disable" # options: disable, require, verify-ca, verify-full
|
|
||||||
#ssl-cert: "/path/to/client-cert.pem"
|
|
||||||
#ssl-key: "/path/to/client-key.pem"
|
|
||||||
#ssl-root-cert: "/path/to/ca-cert.pem"
|
|
||||||
user: "ergo"
|
|
||||||
password: "hunter2"
|
|
||||||
history-database: "ergo_history"
|
|
||||||
# uri takes a postgresql:// (libpq) URI, overriding the above parameters if present:
|
|
||||||
# uri: "postgresql://ergo:hunter2@localhost/ergo_history"
|
|
||||||
timeout: 3s
|
|
||||||
max-conns: 4
|
|
||||||
# this may be necessary to prevent middleware from closing your connections:
|
|
||||||
#conn-max-lifetime: 180s
|
|
||||||
# application name shown in pg_stat_activity for operational visibility:
|
|
||||||
#application-name: "ergo"
|
|
||||||
# timeout for establishing initial connections to PostgreSQL:
|
|
||||||
#connect-timeout: 10s
|
|
||||||
|
|
||||||
# connection information for SQLite (currently only used for persistent history)
|
|
||||||
sqlite:
|
|
||||||
enabled: false
|
|
||||||
# path to the SQLite database file
|
|
||||||
database-path: "ergo_history.db"
|
|
||||||
# timeout when waiting for write lock
|
|
||||||
busy-timeout: 5s
|
|
||||||
# maximum concurrent connections
|
|
||||||
max-conns: 1
|
|
||||||
|
|
||||||
# languages config
|
# languages config
|
||||||
languages:
|
languages:
|
||||||
# whether to load languages
|
# whether to load languages
|
||||||
@ -1015,12 +958,10 @@ history:
|
|||||||
# in your country and the countries of your users.
|
# in your country and the countries of your users.
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# if the in-memory backend is enabled for a channel, how many channel-specific events
|
# how many channel-specific events (messages, joins, parts) should be tracked per channel?
|
||||||
# (messages, joins, parts) should be retained?
|
|
||||||
channel-length: 2048
|
channel-length: 2048
|
||||||
|
|
||||||
# if the in-memory backend is enabled for a user, how many direct messages
|
# how many direct messages and notices should be tracked per user?
|
||||||
# and notices should be retained?
|
|
||||||
client-length: 256
|
client-length: 256
|
||||||
|
|
||||||
# how long should we try to preserve messages?
|
# how long should we try to preserve messages?
|
||||||
@ -1117,22 +1058,6 @@ history:
|
|||||||
# e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
# e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
||||||
allow-environment-overrides: true
|
allow-environment-overrides: true
|
||||||
|
|
||||||
# metadata support for setting key/value data on channels and nicknames.
|
|
||||||
metadata:
|
|
||||||
# can clients store metadata?
|
|
||||||
enabled: true
|
|
||||||
# if this is true, only server operators with the `metadata` capability can edit metadata:
|
|
||||||
operator-only-modification: false
|
|
||||||
# how many keys can a client subscribe to?
|
|
||||||
max-subs: 100
|
|
||||||
# how many keys can be stored per entity?
|
|
||||||
max-keys: 100
|
|
||||||
# rate limiting for client metadata updates, which are expensive to process
|
|
||||||
client-throttle:
|
|
||||||
enabled: true
|
|
||||||
duration: 2m
|
|
||||||
max-attempts: 10
|
|
||||||
|
|
||||||
# experimental support for mobile push notifications
|
# experimental support for mobile push notifications
|
||||||
# see the manual for potential security, privacy, and performance implications.
|
# see the manual for potential security, privacy, and performance implications.
|
||||||
# DO NOT enable if you are running a Tor or I2P hidden service (i.e. one
|
# DO NOT enable if you are running a Tor or I2P hidden service (i.e. one
|
||||||
|
|||||||
27
vendor/filippo.io/edwards25519/LICENSE
generated
vendored
27
vendor/filippo.io/edwards25519/LICENSE
generated
vendored
@ -1,27 +0,0 @@
|
|||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
14
vendor/filippo.io/edwards25519/README.md
generated
vendored
14
vendor/filippo.io/edwards25519/README.md
generated
vendored
@ -1,14 +0,0 @@
|
|||||||
# filippo.io/edwards25519
|
|
||||||
|
|
||||||
```
|
|
||||||
import "filippo.io/edwards25519"
|
|
||||||
```
|
|
||||||
|
|
||||||
This library implements the edwards25519 elliptic curve, exposing the necessary APIs to build a wide array of higher-level primitives.
|
|
||||||
Read the docs at [pkg.go.dev/filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519).
|
|
||||||
|
|
||||||
The code is originally derived from Adam Langley's internal implementation in the Go standard library, and includes George Tankersley's [performance improvements](https://golang.org/cl/71950). It was then further developed by Henry de Valence for use in ristretto255, and was finally [merged back into the Go standard library](https://golang.org/cl/276272) as of Go 1.17. It now tracks the upstream codebase and extends it with additional functionality.
|
|
||||||
|
|
||||||
Most users don't need this package, and should instead use `crypto/ed25519` for signatures, `golang.org/x/crypto/curve25519` for Diffie-Hellman, or `github.com/gtank/ristretto255` for prime order group logic. However, for anyone currently using a fork of `crypto/internal/edwards25519`/`crypto/ed25519/internal/edwards25519` or `github.com/agl/edwards25519`, this package should be a safer, faster, and more powerful alternative.
|
|
||||||
|
|
||||||
Since this package is meant to curb proliferation of edwards25519 implementations in the Go ecosystem, it welcomes requests for new APIs or reviewable performance improvements.
|
|
||||||
20
vendor/filippo.io/edwards25519/doc.go
generated
vendored
20
vendor/filippo.io/edwards25519/doc.go
generated
vendored
@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package edwards25519 implements group logic for the twisted Edwards curve
|
|
||||||
//
|
|
||||||
// -x^2 + y^2 = 1 + -(121665/121666)*x^2*y^2
|
|
||||||
//
|
|
||||||
// This is better known as the Edwards curve equivalent to Curve25519, and is
|
|
||||||
// the curve used by the Ed25519 signature scheme.
|
|
||||||
//
|
|
||||||
// Most users don't need this package, and should instead use crypto/ed25519 for
|
|
||||||
// signatures, golang.org/x/crypto/curve25519 for Diffie-Hellman, or
|
|
||||||
// github.com/gtank/ristretto255 for prime order group logic.
|
|
||||||
//
|
|
||||||
// However, developers who do need to interact with low-level edwards25519
|
|
||||||
// operations can use this package, which is an extended version of
|
|
||||||
// crypto/internal/edwards25519 from the standard library repackaged as
|
|
||||||
// an importable module.
|
|
||||||
package edwards25519
|
|
||||||
427
vendor/filippo.io/edwards25519/edwards25519.go
generated
vendored
427
vendor/filippo.io/edwards25519/edwards25519.go
generated
vendored
@ -1,427 +0,0 @@
|
|||||||
// Copyright (c) 2017 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package edwards25519
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"filippo.io/edwards25519/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Point types.
|
|
||||||
|
|
||||||
type projP1xP1 struct {
|
|
||||||
X, Y, Z, T field.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
type projP2 struct {
|
|
||||||
X, Y, Z field.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Point represents a point on the edwards25519 curve.
|
|
||||||
//
|
|
||||||
// This type works similarly to math/big.Int, and all arguments and receivers
|
|
||||||
// are allowed to alias.
|
|
||||||
//
|
|
||||||
// The zero value is NOT valid, and it may be used only as a receiver.
|
|
||||||
type Point struct {
|
|
||||||
// Make the type not comparable (i.e. used with == or as a map key), as
|
|
||||||
// equivalent points can be represented by different Go values.
|
|
||||||
_ incomparable
|
|
||||||
|
|
||||||
// The point is internally represented in extended coordinates (X, Y, Z, T)
|
|
||||||
// where x = X/Z, y = Y/Z, and xy = T/Z per https://eprint.iacr.org/2008/522.
|
|
||||||
x, y, z, t field.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
type incomparable [0]func()
|
|
||||||
|
|
||||||
func checkInitialized(points ...*Point) {
|
|
||||||
for _, p := range points {
|
|
||||||
if p.x == (field.Element{}) && p.y == (field.Element{}) {
|
|
||||||
panic("edwards25519: use of uninitialized Point")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type projCached struct {
|
|
||||||
YplusX, YminusX, Z, T2d field.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
type affineCached struct {
|
|
||||||
YplusX, YminusX, T2d field.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constructors.
|
|
||||||
|
|
||||||
func (v *projP2) Zero() *projP2 {
|
|
||||||
v.X.Zero()
|
|
||||||
v.Y.One()
|
|
||||||
v.Z.One()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// identity is the point at infinity.
|
|
||||||
var identity, _ = new(Point).SetBytes([]byte{
|
|
||||||
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
|
||||||
|
|
||||||
// NewIdentityPoint returns a new Point set to the identity.
|
|
||||||
func NewIdentityPoint() *Point {
|
|
||||||
return new(Point).Set(identity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generator is the canonical curve basepoint. See TestGenerator for the
|
|
||||||
// correspondence of this encoding with the values in RFC 8032.
|
|
||||||
var generator, _ = new(Point).SetBytes([]byte{
|
|
||||||
0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
|
||||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
|
||||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
|
||||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66})
|
|
||||||
|
|
||||||
// NewGeneratorPoint returns a new Point set to the canonical generator.
|
|
||||||
func NewGeneratorPoint() *Point {
|
|
||||||
return new(Point).Set(generator)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *projCached) Zero() *projCached {
|
|
||||||
v.YplusX.One()
|
|
||||||
v.YminusX.One()
|
|
||||||
v.Z.One()
|
|
||||||
v.T2d.Zero()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *affineCached) Zero() *affineCached {
|
|
||||||
v.YplusX.One()
|
|
||||||
v.YminusX.One()
|
|
||||||
v.T2d.Zero()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assignments.
|
|
||||||
|
|
||||||
// Set sets v = u, and returns v.
|
|
||||||
func (v *Point) Set(u *Point) *Point {
|
|
||||||
*v = *u
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encoding.
|
|
||||||
|
|
||||||
// Bytes returns the canonical 32-byte encoding of v, according to RFC 8032,
|
|
||||||
// Section 5.1.2.
|
|
||||||
func (v *Point) Bytes() []byte {
|
|
||||||
// This function is outlined to make the allocations inline in the caller
|
|
||||||
// rather than happen on the heap.
|
|
||||||
var buf [32]byte
|
|
||||||
return v.bytes(&buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Point) bytes(buf *[32]byte) []byte {
|
|
||||||
checkInitialized(v)
|
|
||||||
|
|
||||||
var zInv, x, y field.Element
|
|
||||||
zInv.Invert(&v.z) // zInv = 1 / Z
|
|
||||||
x.Multiply(&v.x, &zInv) // x = X / Z
|
|
||||||
y.Multiply(&v.y, &zInv) // y = Y / Z
|
|
||||||
|
|
||||||
out := copyFieldElement(buf, &y)
|
|
||||||
out[31] |= byte(x.IsNegative() << 7)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
var feOne = new(field.Element).One()
|
|
||||||
|
|
||||||
// SetBytes sets v = x, where x is a 32-byte encoding of v. If x does not
|
|
||||||
// represent a valid point on the curve, SetBytes returns nil and an error and
|
|
||||||
// the receiver is unchanged. Otherwise, SetBytes returns v.
|
|
||||||
//
|
|
||||||
// Note that SetBytes accepts all non-canonical encodings of valid points.
|
|
||||||
// That is, it follows decoding rules that match most implementations in
|
|
||||||
// the ecosystem rather than RFC 8032.
|
|
||||||
func (v *Point) SetBytes(x []byte) (*Point, error) {
|
|
||||||
// Specifically, the non-canonical encodings that are accepted are
|
|
||||||
// 1) the ones where the field element is not reduced (see the
|
|
||||||
// (*field.Element).SetBytes docs) and
|
|
||||||
// 2) the ones where the x-coordinate is zero and the sign bit is set.
|
|
||||||
//
|
|
||||||
// Read more at https://hdevalence.ca/blog/2020-10-04-its-25519am,
|
|
||||||
// specifically the "Canonical A, R" section.
|
|
||||||
|
|
||||||
y, err := new(field.Element).SetBytes(x)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("edwards25519: invalid point encoding length")
|
|
||||||
}
|
|
||||||
|
|
||||||
// -x² + y² = 1 + dx²y²
|
|
||||||
// x² + dx²y² = x²(dy² + 1) = y² - 1
|
|
||||||
// x² = (y² - 1) / (dy² + 1)
|
|
||||||
|
|
||||||
// u = y² - 1
|
|
||||||
y2 := new(field.Element).Square(y)
|
|
||||||
u := new(field.Element).Subtract(y2, feOne)
|
|
||||||
|
|
||||||
// v = dy² + 1
|
|
||||||
vv := new(field.Element).Multiply(y2, d)
|
|
||||||
vv = vv.Add(vv, feOne)
|
|
||||||
|
|
||||||
// x = +√(u/v)
|
|
||||||
xx, wasSquare := new(field.Element).SqrtRatio(u, vv)
|
|
||||||
if wasSquare == 0 {
|
|
||||||
return nil, errors.New("edwards25519: invalid point encoding")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select the negative square root if the sign bit is set.
|
|
||||||
xxNeg := new(field.Element).Negate(xx)
|
|
||||||
xx = xx.Select(xxNeg, xx, int(x[31]>>7))
|
|
||||||
|
|
||||||
v.x.Set(xx)
|
|
||||||
v.y.Set(y)
|
|
||||||
v.z.One()
|
|
||||||
v.t.Multiply(xx, y) // xy = T / Z
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFieldElement(buf *[32]byte, v *field.Element) []byte {
|
|
||||||
copy(buf[:], v.Bytes())
|
|
||||||
return buf[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conversions.
|
|
||||||
|
|
||||||
func (v *projP2) FromP1xP1(p *projP1xP1) *projP2 {
|
|
||||||
v.X.Multiply(&p.X, &p.T)
|
|
||||||
v.Y.Multiply(&p.Y, &p.Z)
|
|
||||||
v.Z.Multiply(&p.Z, &p.T)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *projP2) FromP3(p *Point) *projP2 {
|
|
||||||
v.X.Set(&p.x)
|
|
||||||
v.Y.Set(&p.y)
|
|
||||||
v.Z.Set(&p.z)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Point) fromP1xP1(p *projP1xP1) *Point {
|
|
||||||
v.x.Multiply(&p.X, &p.T)
|
|
||||||
v.y.Multiply(&p.Y, &p.Z)
|
|
||||||
v.z.Multiply(&p.Z, &p.T)
|
|
||||||
v.t.Multiply(&p.X, &p.Y)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Point) fromP2(p *projP2) *Point {
|
|
||||||
v.x.Multiply(&p.X, &p.Z)
|
|
||||||
v.y.Multiply(&p.Y, &p.Z)
|
|
||||||
v.z.Square(&p.Z)
|
|
||||||
v.t.Multiply(&p.X, &p.Y)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// d is a constant in the curve equation.
|
|
||||||
var d, _ = new(field.Element).SetBytes([]byte{
|
|
||||||
0xa3, 0x78, 0x59, 0x13, 0xca, 0x4d, 0xeb, 0x75,
|
|
||||||
0xab, 0xd8, 0x41, 0x41, 0x4d, 0x0a, 0x70, 0x00,
|
|
||||||
0x98, 0xe8, 0x79, 0x77, 0x79, 0x40, 0xc7, 0x8c,
|
|
||||||
0x73, 0xfe, 0x6f, 0x2b, 0xee, 0x6c, 0x03, 0x52})
|
|
||||||
var d2 = new(field.Element).Add(d, d)
|
|
||||||
|
|
||||||
func (v *projCached) FromP3(p *Point) *projCached {
|
|
||||||
v.YplusX.Add(&p.y, &p.x)
|
|
||||||
v.YminusX.Subtract(&p.y, &p.x)
|
|
||||||
v.Z.Set(&p.z)
|
|
||||||
v.T2d.Multiply(&p.t, d2)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *affineCached) FromP3(p *Point) *affineCached {
|
|
||||||
v.YplusX.Add(&p.y, &p.x)
|
|
||||||
v.YminusX.Subtract(&p.y, &p.x)
|
|
||||||
v.T2d.Multiply(&p.t, d2)
|
|
||||||
|
|
||||||
var invZ field.Element
|
|
||||||
invZ.Invert(&p.z)
|
|
||||||
v.YplusX.Multiply(&v.YplusX, &invZ)
|
|
||||||
v.YminusX.Multiply(&v.YminusX, &invZ)
|
|
||||||
v.T2d.Multiply(&v.T2d, &invZ)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// (Re)addition and subtraction.
|
|
||||||
|
|
||||||
// Add sets v = p + q, and returns v.
|
|
||||||
func (v *Point) Add(p, q *Point) *Point {
|
|
||||||
checkInitialized(p, q)
|
|
||||||
qCached := new(projCached).FromP3(q)
|
|
||||||
result := new(projP1xP1).Add(p, qCached)
|
|
||||||
return v.fromP1xP1(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtract sets v = p - q, and returns v.
|
|
||||||
func (v *Point) Subtract(p, q *Point) *Point {
|
|
||||||
checkInitialized(p, q)
|
|
||||||
qCached := new(projCached).FromP3(q)
|
|
||||||
result := new(projP1xP1).Sub(p, qCached)
|
|
||||||
return v.fromP1xP1(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *projP1xP1) Add(p *Point, q *projCached) *projP1xP1 {
|
|
||||||
var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
|
|
||||||
|
|
||||||
YplusX.Add(&p.y, &p.x)
|
|
||||||
YminusX.Subtract(&p.y, &p.x)
|
|
||||||
|
|
||||||
PP.Multiply(&YplusX, &q.YplusX)
|
|
||||||
MM.Multiply(&YminusX, &q.YminusX)
|
|
||||||
TT2d.Multiply(&p.t, &q.T2d)
|
|
||||||
ZZ2.Multiply(&p.z, &q.Z)
|
|
||||||
|
|
||||||
ZZ2.Add(&ZZ2, &ZZ2)
|
|
||||||
|
|
||||||
v.X.Subtract(&PP, &MM)
|
|
||||||
v.Y.Add(&PP, &MM)
|
|
||||||
v.Z.Add(&ZZ2, &TT2d)
|
|
||||||
v.T.Subtract(&ZZ2, &TT2d)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *projP1xP1) Sub(p *Point, q *projCached) *projP1xP1 {
|
|
||||||
var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
|
|
||||||
|
|
||||||
YplusX.Add(&p.y, &p.x)
|
|
||||||
YminusX.Subtract(&p.y, &p.x)
|
|
||||||
|
|
||||||
PP.Multiply(&YplusX, &q.YminusX) // flipped sign
|
|
||||||
MM.Multiply(&YminusX, &q.YplusX) // flipped sign
|
|
||||||
TT2d.Multiply(&p.t, &q.T2d)
|
|
||||||
ZZ2.Multiply(&p.z, &q.Z)
|
|
||||||
|
|
||||||
ZZ2.Add(&ZZ2, &ZZ2)
|
|
||||||
|
|
||||||
v.X.Subtract(&PP, &MM)
|
|
||||||
v.Y.Add(&PP, &MM)
|
|
||||||
v.Z.Subtract(&ZZ2, &TT2d) // flipped sign
|
|
||||||
v.T.Add(&ZZ2, &TT2d) // flipped sign
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *projP1xP1) AddAffine(p *Point, q *affineCached) *projP1xP1 {
|
|
||||||
var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
|
|
||||||
|
|
||||||
YplusX.Add(&p.y, &p.x)
|
|
||||||
YminusX.Subtract(&p.y, &p.x)
|
|
||||||
|
|
||||||
PP.Multiply(&YplusX, &q.YplusX)
|
|
||||||
MM.Multiply(&YminusX, &q.YminusX)
|
|
||||||
TT2d.Multiply(&p.t, &q.T2d)
|
|
||||||
|
|
||||||
Z2.Add(&p.z, &p.z)
|
|
||||||
|
|
||||||
v.X.Subtract(&PP, &MM)
|
|
||||||
v.Y.Add(&PP, &MM)
|
|
||||||
v.Z.Add(&Z2, &TT2d)
|
|
||||||
v.T.Subtract(&Z2, &TT2d)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *projP1xP1) SubAffine(p *Point, q *affineCached) *projP1xP1 {
|
|
||||||
var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
|
|
||||||
|
|
||||||
YplusX.Add(&p.y, &p.x)
|
|
||||||
YminusX.Subtract(&p.y, &p.x)
|
|
||||||
|
|
||||||
PP.Multiply(&YplusX, &q.YminusX) // flipped sign
|
|
||||||
MM.Multiply(&YminusX, &q.YplusX) // flipped sign
|
|
||||||
TT2d.Multiply(&p.t, &q.T2d)
|
|
||||||
|
|
||||||
Z2.Add(&p.z, &p.z)
|
|
||||||
|
|
||||||
v.X.Subtract(&PP, &MM)
|
|
||||||
v.Y.Add(&PP, &MM)
|
|
||||||
v.Z.Subtract(&Z2, &TT2d) // flipped sign
|
|
||||||
v.T.Add(&Z2, &TT2d) // flipped sign
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Doubling.
|
|
||||||
|
|
||||||
func (v *projP1xP1) Double(p *projP2) *projP1xP1 {
|
|
||||||
var XX, YY, ZZ2, XplusYsq field.Element
|
|
||||||
|
|
||||||
XX.Square(&p.X)
|
|
||||||
YY.Square(&p.Y)
|
|
||||||
ZZ2.Square(&p.Z)
|
|
||||||
ZZ2.Add(&ZZ2, &ZZ2)
|
|
||||||
XplusYsq.Add(&p.X, &p.Y)
|
|
||||||
XplusYsq.Square(&XplusYsq)
|
|
||||||
|
|
||||||
v.Y.Add(&YY, &XX)
|
|
||||||
v.Z.Subtract(&YY, &XX)
|
|
||||||
|
|
||||||
v.X.Subtract(&XplusYsq, &v.Y)
|
|
||||||
v.T.Subtract(&ZZ2, &v.Z)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Negation.
|
|
||||||
|
|
||||||
// Negate sets v = -p, and returns v.
|
|
||||||
func (v *Point) Negate(p *Point) *Point {
|
|
||||||
checkInitialized(p)
|
|
||||||
v.x.Negate(&p.x)
|
|
||||||
v.y.Set(&p.y)
|
|
||||||
v.z.Set(&p.z)
|
|
||||||
v.t.Negate(&p.t)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equal returns 1 if v is equivalent to u, and 0 otherwise.
|
|
||||||
func (v *Point) Equal(u *Point) int {
|
|
||||||
checkInitialized(v, u)
|
|
||||||
|
|
||||||
var t1, t2, t3, t4 field.Element
|
|
||||||
t1.Multiply(&v.x, &u.z)
|
|
||||||
t2.Multiply(&u.x, &v.z)
|
|
||||||
t3.Multiply(&v.y, &u.z)
|
|
||||||
t4.Multiply(&u.y, &v.z)
|
|
||||||
|
|
||||||
return t1.Equal(&t2) & t3.Equal(&t4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constant-time operations
|
|
||||||
|
|
||||||
// Select sets v to a if cond == 1 and to b if cond == 0.
|
|
||||||
func (v *projCached) Select(a, b *projCached, cond int) *projCached {
|
|
||||||
v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
|
|
||||||
v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
|
|
||||||
v.Z.Select(&a.Z, &b.Z, cond)
|
|
||||||
v.T2d.Select(&a.T2d, &b.T2d, cond)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select sets v to a if cond == 1 and to b if cond == 0.
|
|
||||||
func (v *affineCached) Select(a, b *affineCached, cond int) *affineCached {
|
|
||||||
v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
|
|
||||||
v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
|
|
||||||
v.T2d.Select(&a.T2d, &b.T2d, cond)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
|
|
||||||
func (v *projCached) CondNeg(cond int) *projCached {
|
|
||||||
v.YplusX.Swap(&v.YminusX, cond)
|
|
||||||
v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
|
|
||||||
func (v *affineCached) CondNeg(cond int) *affineCached {
|
|
||||||
v.YplusX.Swap(&v.YminusX, cond)
|
|
||||||
v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
350
vendor/filippo.io/edwards25519/extra.go
generated
vendored
350
vendor/filippo.io/edwards25519/extra.go
generated
vendored
@ -1,350 +0,0 @@
|
|||||||
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package edwards25519
|
|
||||||
|
|
||||||
// This file contains additional functionality that is not included in the
|
|
||||||
// upstream crypto/internal/edwards25519 package.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"filippo.io/edwards25519/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExtendedCoordinates returns v in extended coordinates (X:Y:Z:T) where
|
|
||||||
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
|
|
||||||
func (v *Point) ExtendedCoordinates() (X, Y, Z, T *field.Element) {
|
|
||||||
// This function is outlined to make the allocations inline in the caller
|
|
||||||
// rather than happen on the heap. Don't change the style without making
|
|
||||||
// sure it doesn't increase the inliner cost.
|
|
||||||
var e [4]field.Element
|
|
||||||
X, Y, Z, T = v.extendedCoordinates(&e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Point) extendedCoordinates(e *[4]field.Element) (X, Y, Z, T *field.Element) {
|
|
||||||
checkInitialized(v)
|
|
||||||
X = e[0].Set(&v.x)
|
|
||||||
Y = e[1].Set(&v.y)
|
|
||||||
Z = e[2].Set(&v.z)
|
|
||||||
T = e[3].Set(&v.t)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetExtendedCoordinates sets v = (X:Y:Z:T) in extended coordinates where
|
|
||||||
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
|
|
||||||
//
|
|
||||||
// If the coordinates are invalid or don't represent a valid point on the curve,
|
|
||||||
// SetExtendedCoordinates returns nil and an error and the receiver is
|
|
||||||
// unchanged. Otherwise, SetExtendedCoordinates returns v.
|
|
||||||
func (v *Point) SetExtendedCoordinates(X, Y, Z, T *field.Element) (*Point, error) {
|
|
||||||
if !isOnCurve(X, Y, Z, T) {
|
|
||||||
return nil, errors.New("edwards25519: invalid point coordinates")
|
|
||||||
}
|
|
||||||
v.x.Set(X)
|
|
||||||
v.y.Set(Y)
|
|
||||||
v.z.Set(Z)
|
|
||||||
v.t.Set(T)
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOnCurve(X, Y, Z, T *field.Element) bool {
|
|
||||||
var lhs, rhs field.Element
|
|
||||||
XX := new(field.Element).Square(X)
|
|
||||||
YY := new(field.Element).Square(Y)
|
|
||||||
ZZ := new(field.Element).Square(Z)
|
|
||||||
TT := new(field.Element).Square(T)
|
|
||||||
// -x² + y² = 1 + dx²y²
|
|
||||||
// -(X/Z)² + (Y/Z)² = 1 + d(T/Z)²
|
|
||||||
// -X² + Y² = Z² + dT²
|
|
||||||
lhs.Subtract(YY, XX)
|
|
||||||
rhs.Multiply(d, TT).Add(&rhs, ZZ)
|
|
||||||
if lhs.Equal(&rhs) != 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// xy = T/Z
|
|
||||||
// XY/Z² = T/Z
|
|
||||||
// XY = TZ
|
|
||||||
lhs.Multiply(X, Y)
|
|
||||||
rhs.Multiply(T, Z)
|
|
||||||
return lhs.Equal(&rhs) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// BytesMontgomery converts v to a point on the birationally-equivalent
|
|
||||||
// Curve25519 Montgomery curve, and returns its canonical 32 bytes encoding
|
|
||||||
// according to RFC 7748.
|
|
||||||
//
|
|
||||||
// Note that BytesMontgomery only encodes the u-coordinate, so v and -v encode
|
|
||||||
// to the same value. If v is the identity point, BytesMontgomery returns 32
|
|
||||||
// zero bytes, analogously to the X25519 function.
|
|
||||||
//
|
|
||||||
// The lack of an inverse operation (such as SetMontgomeryBytes) is deliberate:
|
|
||||||
// while every valid edwards25519 point has a unique u-coordinate Montgomery
|
|
||||||
// encoding, X25519 accepts inputs on the quadratic twist, which don't correspond
|
|
||||||
// to any edwards25519 point, and every other X25519 input corresponds to two
|
|
||||||
// edwards25519 points.
|
|
||||||
func (v *Point) BytesMontgomery() []byte {
|
|
||||||
// This function is outlined to make the allocations inline in the caller
|
|
||||||
// rather than happen on the heap.
|
|
||||||
var buf [32]byte
|
|
||||||
return v.bytesMontgomery(&buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Point) bytesMontgomery(buf *[32]byte) []byte {
|
|
||||||
checkInitialized(v)
|
|
||||||
|
|
||||||
// RFC 7748, Section 4.1 provides the bilinear map to calculate the
|
|
||||||
// Montgomery u-coordinate
|
|
||||||
//
|
|
||||||
// u = (1 + y) / (1 - y)
|
|
||||||
//
|
|
||||||
// where y = Y / Z.
|
|
||||||
|
|
||||||
var y, recip, u field.Element
|
|
||||||
|
|
||||||
y.Multiply(&v.y, y.Invert(&v.z)) // y = Y / Z
|
|
||||||
recip.Invert(recip.Subtract(feOne, &y)) // r = 1/(1 - y)
|
|
||||||
u.Multiply(u.Add(feOne, &y), &recip) // u = (1 + y)*r
|
|
||||||
|
|
||||||
return copyFieldElement(buf, &u)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MultByCofactor sets v = 8 * p, and returns v.
|
|
||||||
func (v *Point) MultByCofactor(p *Point) *Point {
|
|
||||||
checkInitialized(p)
|
|
||||||
result := projP1xP1{}
|
|
||||||
pp := (&projP2{}).FromP3(p)
|
|
||||||
result.Double(pp)
|
|
||||||
pp.FromP1xP1(&result)
|
|
||||||
result.Double(pp)
|
|
||||||
pp.FromP1xP1(&result)
|
|
||||||
result.Double(pp)
|
|
||||||
return v.fromP1xP1(&result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given k > 0, set s = s**(2*i).
|
|
||||||
func (s *Scalar) pow2k(k int) {
|
|
||||||
for i := 0; i < k; i++ {
|
|
||||||
s.Multiply(s, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invert sets s to the inverse of a nonzero scalar v, and returns s.
|
|
||||||
//
|
|
||||||
// If t is zero, Invert returns zero.
|
|
||||||
func (s *Scalar) Invert(t *Scalar) *Scalar {
|
|
||||||
// Uses a hardcoded sliding window of width 4.
|
|
||||||
var table [8]Scalar
|
|
||||||
var tt Scalar
|
|
||||||
tt.Multiply(t, t)
|
|
||||||
table[0] = *t
|
|
||||||
for i := 0; i < 7; i++ {
|
|
||||||
table[i+1].Multiply(&table[i], &tt)
|
|
||||||
}
|
|
||||||
// Now table = [t**1, t**3, t**5, t**7, t**9, t**11, t**13, t**15]
|
|
||||||
// so t**k = t[k/2] for odd k
|
|
||||||
|
|
||||||
// To compute the sliding window digits, use the following Sage script:
|
|
||||||
|
|
||||||
// sage: import itertools
|
|
||||||
// sage: def sliding_window(w,k):
|
|
||||||
// ....: digits = []
|
|
||||||
// ....: while k > 0:
|
|
||||||
// ....: if k % 2 == 1:
|
|
||||||
// ....: kmod = k % (2**w)
|
|
||||||
// ....: digits.append(kmod)
|
|
||||||
// ....: k = k - kmod
|
|
||||||
// ....: else:
|
|
||||||
// ....: digits.append(0)
|
|
||||||
// ....: k = k // 2
|
|
||||||
// ....: return digits
|
|
||||||
|
|
||||||
// Now we can compute s roughly as follows:
|
|
||||||
|
|
||||||
// sage: s = 1
|
|
||||||
// sage: for coeff in reversed(sliding_window(4,l-2)):
|
|
||||||
// ....: s = s*s
|
|
||||||
// ....: if coeff > 0 :
|
|
||||||
// ....: s = s*t**coeff
|
|
||||||
|
|
||||||
// This works on one bit at a time, with many runs of zeros.
|
|
||||||
// The digits can be collapsed into [(count, coeff)] as follows:
|
|
||||||
|
|
||||||
// sage: [(len(list(group)),d) for d,group in itertools.groupby(sliding_window(4,l-2))]
|
|
||||||
|
|
||||||
// Entries of the form (k, 0) turn into pow2k(k)
|
|
||||||
// Entries of the form (1, coeff) turn into a squaring and then a table lookup.
|
|
||||||
// We can fold the squaring into the previous pow2k(k) as pow2k(k+1).
|
|
||||||
|
|
||||||
*s = table[1/2]
|
|
||||||
s.pow2k(127 + 1)
|
|
||||||
s.Multiply(s, &table[1/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[9/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[11/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[13/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[15/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[7/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[15/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[5/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[1/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[15/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[15/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[7/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[3/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[11/2])
|
|
||||||
s.pow2k(5 + 1)
|
|
||||||
s.Multiply(s, &table[11/2])
|
|
||||||
s.pow2k(9 + 1)
|
|
||||||
s.Multiply(s, &table[9/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[3/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[3/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[3/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[9/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[7/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[3/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[13/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[7/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[9/2])
|
|
||||||
s.pow2k(3 + 1)
|
|
||||||
s.Multiply(s, &table[15/2])
|
|
||||||
s.pow2k(4 + 1)
|
|
||||||
s.Multiply(s, &table[11/2])
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// MultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
|
|
||||||
//
|
|
||||||
// Execution time depends only on the lengths of the two slices, which must match.
|
|
||||||
func (v *Point) MultiScalarMult(scalars []*Scalar, points []*Point) *Point {
|
|
||||||
if len(scalars) != len(points) {
|
|
||||||
panic("edwards25519: called MultiScalarMult with different size inputs")
|
|
||||||
}
|
|
||||||
checkInitialized(points...)
|
|
||||||
|
|
||||||
// Proceed as in the single-base case, but share doublings
|
|
||||||
// between each point in the multiscalar equation.
|
|
||||||
|
|
||||||
// Build lookup tables for each point
|
|
||||||
tables := make([]projLookupTable, len(points))
|
|
||||||
for i := range tables {
|
|
||||||
tables[i].FromP3(points[i])
|
|
||||||
}
|
|
||||||
// Compute signed radix-16 digits for each scalar
|
|
||||||
digits := make([][64]int8, len(scalars))
|
|
||||||
for i := range digits {
|
|
||||||
digits[i] = scalars[i].signedRadix16()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap first loop iteration to save computing 16*identity
|
|
||||||
multiple := &projCached{}
|
|
||||||
tmp1 := &projP1xP1{}
|
|
||||||
tmp2 := &projP2{}
|
|
||||||
// Lookup-and-add the appropriate multiple of each input point
|
|
||||||
v.Set(NewIdentityPoint())
|
|
||||||
for j := range tables {
|
|
||||||
tables[j].SelectInto(multiple, digits[j][63])
|
|
||||||
tmp1.Add(v, multiple) // tmp1 = v + x_(j,63)*Q in P1xP1 coords
|
|
||||||
v.fromP1xP1(tmp1) // update v
|
|
||||||
}
|
|
||||||
tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
|
|
||||||
for i := 62; i >= 0; i-- {
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 2*(prev) in P1xP1 coords
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = 2*(prev) in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 4*(prev) in P1xP1 coords
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = 4*(prev) in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 8*(prev) in P1xP1 coords
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = 8*(prev) in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 16*(prev) in P1xP1 coords
|
|
||||||
v.fromP1xP1(tmp1) // v = 16*(prev) in P3 coords
|
|
||||||
// Lookup-and-add the appropriate multiple of each input point
|
|
||||||
for j := range tables {
|
|
||||||
tables[j].SelectInto(multiple, digits[j][i])
|
|
||||||
tmp1.Add(v, multiple) // tmp1 = v + x_(j,i)*Q in P1xP1 coords
|
|
||||||
v.fromP1xP1(tmp1) // update v
|
|
||||||
}
|
|
||||||
tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// VarTimeMultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
|
|
||||||
//
|
|
||||||
// Execution time depends on the inputs.
|
|
||||||
func (v *Point) VarTimeMultiScalarMult(scalars []*Scalar, points []*Point) *Point {
|
|
||||||
if len(scalars) != len(points) {
|
|
||||||
panic("edwards25519: called VarTimeMultiScalarMult with different size inputs")
|
|
||||||
}
|
|
||||||
checkInitialized(points...)
|
|
||||||
|
|
||||||
// Generalize double-base NAF computation to arbitrary sizes.
|
|
||||||
// Here all the points are dynamic, so we only use the smaller
|
|
||||||
// tables.
|
|
||||||
|
|
||||||
// Build lookup tables for each point
|
|
||||||
tables := make([]nafLookupTable5, len(points))
|
|
||||||
for i := range tables {
|
|
||||||
tables[i].FromP3(points[i])
|
|
||||||
}
|
|
||||||
// Compute a NAF for each scalar
|
|
||||||
nafs := make([][256]int8, len(scalars))
|
|
||||||
for i := range nafs {
|
|
||||||
nafs[i] = scalars[i].nonAdjacentForm(5)
|
|
||||||
}
|
|
||||||
|
|
||||||
multiple := &projCached{}
|
|
||||||
tmp1 := &projP1xP1{}
|
|
||||||
tmp2 := &projP2{}
|
|
||||||
tmp2.Zero()
|
|
||||||
|
|
||||||
// Move from high to low bits, doubling the accumulator
|
|
||||||
// at each iteration and checking whether there is a nonzero
|
|
||||||
// coefficient to look up a multiple of.
|
|
||||||
//
|
|
||||||
// Skip trying to find the first nonzero coefficent, because
|
|
||||||
// searching might be more work than a few extra doublings.
|
|
||||||
for i := 255; i >= 0; i-- {
|
|
||||||
tmp1.Double(tmp2)
|
|
||||||
|
|
||||||
for j := range nafs {
|
|
||||||
if nafs[j][i] > 0 {
|
|
||||||
v.fromP1xP1(tmp1)
|
|
||||||
tables[j].SelectInto(multiple, nafs[j][i])
|
|
||||||
tmp1.Add(v, multiple)
|
|
||||||
} else if nafs[j][i] < 0 {
|
|
||||||
v.fromP1xP1(tmp1)
|
|
||||||
tables[j].SelectInto(multiple, -nafs[j][i])
|
|
||||||
tmp1.Sub(v, multiple)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp2.FromP1xP1(tmp1)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.fromP2(tmp2)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
420
vendor/filippo.io/edwards25519/field/fe.go
generated
vendored
420
vendor/filippo.io/edwards25519/field/fe.go
generated
vendored
@ -1,420 +0,0 @@
|
|||||||
// Copyright (c) 2017 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package field implements fast arithmetic modulo 2^255-19.
|
|
||||||
package field
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"math/bits"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Element represents an element of the field GF(2^255-19). Note that this
|
|
||||||
// is not a cryptographically secure group, and should only be used to interact
|
|
||||||
// with edwards25519.Point coordinates.
|
|
||||||
//
|
|
||||||
// This type works similarly to math/big.Int, and all arguments and receivers
|
|
||||||
// are allowed to alias.
|
|
||||||
//
|
|
||||||
// The zero value is a valid zero element.
|
|
||||||
type Element struct {
|
|
||||||
// An element t represents the integer
|
|
||||||
// t.l0 + t.l1*2^51 + t.l2*2^102 + t.l3*2^153 + t.l4*2^204
|
|
||||||
//
|
|
||||||
// Between operations, all limbs are expected to be lower than 2^52.
|
|
||||||
l0 uint64
|
|
||||||
l1 uint64
|
|
||||||
l2 uint64
|
|
||||||
l3 uint64
|
|
||||||
l4 uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
const maskLow51Bits uint64 = (1 << 51) - 1
|
|
||||||
|
|
||||||
var feZero = &Element{0, 0, 0, 0, 0}
|
|
||||||
|
|
||||||
// Zero sets v = 0, and returns v.
|
|
||||||
func (v *Element) Zero() *Element {
|
|
||||||
*v = *feZero
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
var feOne = &Element{1, 0, 0, 0, 0}
|
|
||||||
|
|
||||||
// One sets v = 1, and returns v.
|
|
||||||
func (v *Element) One() *Element {
|
|
||||||
*v = *feOne
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// reduce reduces v modulo 2^255 - 19 and returns it.
|
|
||||||
func (v *Element) reduce() *Element {
|
|
||||||
v.carryPropagate()
|
|
||||||
|
|
||||||
// After the light reduction we now have a field element representation
|
|
||||||
// v < 2^255 + 2^13 * 19, but need v < 2^255 - 19.
|
|
||||||
|
|
||||||
// If v >= 2^255 - 19, then v + 19 >= 2^255, which would overflow 2^255 - 1,
|
|
||||||
// generating a carry. That is, c will be 0 if v < 2^255 - 19, and 1 otherwise.
|
|
||||||
c := (v.l0 + 19) >> 51
|
|
||||||
c = (v.l1 + c) >> 51
|
|
||||||
c = (v.l2 + c) >> 51
|
|
||||||
c = (v.l3 + c) >> 51
|
|
||||||
c = (v.l4 + c) >> 51
|
|
||||||
|
|
||||||
// If v < 2^255 - 19 and c = 0, this will be a no-op. Otherwise, it's
|
|
||||||
// effectively applying the reduction identity to the carry.
|
|
||||||
v.l0 += 19 * c
|
|
||||||
|
|
||||||
v.l1 += v.l0 >> 51
|
|
||||||
v.l0 = v.l0 & maskLow51Bits
|
|
||||||
v.l2 += v.l1 >> 51
|
|
||||||
v.l1 = v.l1 & maskLow51Bits
|
|
||||||
v.l3 += v.l2 >> 51
|
|
||||||
v.l2 = v.l2 & maskLow51Bits
|
|
||||||
v.l4 += v.l3 >> 51
|
|
||||||
v.l3 = v.l3 & maskLow51Bits
|
|
||||||
// no additional carry
|
|
||||||
v.l4 = v.l4 & maskLow51Bits
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add sets v = a + b, and returns v.
|
|
||||||
func (v *Element) Add(a, b *Element) *Element {
|
|
||||||
v.l0 = a.l0 + b.l0
|
|
||||||
v.l1 = a.l1 + b.l1
|
|
||||||
v.l2 = a.l2 + b.l2
|
|
||||||
v.l3 = a.l3 + b.l3
|
|
||||||
v.l4 = a.l4 + b.l4
|
|
||||||
// Using the generic implementation here is actually faster than the
|
|
||||||
// assembly. Probably because the body of this function is so simple that
|
|
||||||
// the compiler can figure out better optimizations by inlining the carry
|
|
||||||
// propagation.
|
|
||||||
return v.carryPropagateGeneric()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtract sets v = a - b, and returns v.
|
|
||||||
func (v *Element) Subtract(a, b *Element) *Element {
|
|
||||||
// We first add 2 * p, to guarantee the subtraction won't underflow, and
|
|
||||||
// then subtract b (which can be up to 2^255 + 2^13 * 19).
|
|
||||||
v.l0 = (a.l0 + 0xFFFFFFFFFFFDA) - b.l0
|
|
||||||
v.l1 = (a.l1 + 0xFFFFFFFFFFFFE) - b.l1
|
|
||||||
v.l2 = (a.l2 + 0xFFFFFFFFFFFFE) - b.l2
|
|
||||||
v.l3 = (a.l3 + 0xFFFFFFFFFFFFE) - b.l3
|
|
||||||
v.l4 = (a.l4 + 0xFFFFFFFFFFFFE) - b.l4
|
|
||||||
return v.carryPropagate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Negate sets v = -a, and returns v.
|
|
||||||
func (v *Element) Negate(a *Element) *Element {
|
|
||||||
return v.Subtract(feZero, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invert sets v = 1/z mod p, and returns v.
|
|
||||||
//
|
|
||||||
// If z == 0, Invert returns v = 0.
|
|
||||||
func (v *Element) Invert(z *Element) *Element {
|
|
||||||
// Inversion is implemented as exponentiation with exponent p − 2. It uses the
|
|
||||||
// same sequence of 255 squarings and 11 multiplications as [Curve25519].
|
|
||||||
var z2, z9, z11, z2_5_0, z2_10_0, z2_20_0, z2_50_0, z2_100_0, t Element
|
|
||||||
|
|
||||||
z2.Square(z) // 2
|
|
||||||
t.Square(&z2) // 4
|
|
||||||
t.Square(&t) // 8
|
|
||||||
z9.Multiply(&t, z) // 9
|
|
||||||
z11.Multiply(&z9, &z2) // 11
|
|
||||||
t.Square(&z11) // 22
|
|
||||||
z2_5_0.Multiply(&t, &z9) // 31 = 2^5 - 2^0
|
|
||||||
|
|
||||||
t.Square(&z2_5_0) // 2^6 - 2^1
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
t.Square(&t) // 2^10 - 2^5
|
|
||||||
}
|
|
||||||
z2_10_0.Multiply(&t, &z2_5_0) // 2^10 - 2^0
|
|
||||||
|
|
||||||
t.Square(&z2_10_0) // 2^11 - 2^1
|
|
||||||
for i := 0; i < 9; i++ {
|
|
||||||
t.Square(&t) // 2^20 - 2^10
|
|
||||||
}
|
|
||||||
z2_20_0.Multiply(&t, &z2_10_0) // 2^20 - 2^0
|
|
||||||
|
|
||||||
t.Square(&z2_20_0) // 2^21 - 2^1
|
|
||||||
for i := 0; i < 19; i++ {
|
|
||||||
t.Square(&t) // 2^40 - 2^20
|
|
||||||
}
|
|
||||||
t.Multiply(&t, &z2_20_0) // 2^40 - 2^0
|
|
||||||
|
|
||||||
t.Square(&t) // 2^41 - 2^1
|
|
||||||
for i := 0; i < 9; i++ {
|
|
||||||
t.Square(&t) // 2^50 - 2^10
|
|
||||||
}
|
|
||||||
z2_50_0.Multiply(&t, &z2_10_0) // 2^50 - 2^0
|
|
||||||
|
|
||||||
t.Square(&z2_50_0) // 2^51 - 2^1
|
|
||||||
for i := 0; i < 49; i++ {
|
|
||||||
t.Square(&t) // 2^100 - 2^50
|
|
||||||
}
|
|
||||||
z2_100_0.Multiply(&t, &z2_50_0) // 2^100 - 2^0
|
|
||||||
|
|
||||||
t.Square(&z2_100_0) // 2^101 - 2^1
|
|
||||||
for i := 0; i < 99; i++ {
|
|
||||||
t.Square(&t) // 2^200 - 2^100
|
|
||||||
}
|
|
||||||
t.Multiply(&t, &z2_100_0) // 2^200 - 2^0
|
|
||||||
|
|
||||||
t.Square(&t) // 2^201 - 2^1
|
|
||||||
for i := 0; i < 49; i++ {
|
|
||||||
t.Square(&t) // 2^250 - 2^50
|
|
||||||
}
|
|
||||||
t.Multiply(&t, &z2_50_0) // 2^250 - 2^0
|
|
||||||
|
|
||||||
t.Square(&t) // 2^251 - 2^1
|
|
||||||
t.Square(&t) // 2^252 - 2^2
|
|
||||||
t.Square(&t) // 2^253 - 2^3
|
|
||||||
t.Square(&t) // 2^254 - 2^4
|
|
||||||
t.Square(&t) // 2^255 - 2^5
|
|
||||||
|
|
||||||
return v.Multiply(&t, &z11) // 2^255 - 21
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set sets v = a, and returns v.
|
|
||||||
func (v *Element) Set(a *Element) *Element {
|
|
||||||
*v = *a
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBytes sets v to x, where x is a 32-byte little-endian encoding. If x is
|
|
||||||
// not of the right length, SetBytes returns nil and an error, and the
|
|
||||||
// receiver is unchanged.
|
|
||||||
//
|
|
||||||
// Consistent with RFC 7748, the most significant bit (the high bit of the
|
|
||||||
// last byte) is ignored, and non-canonical values (2^255-19 through 2^255-1)
|
|
||||||
// are accepted. Note that this is laxer than specified by RFC 8032, but
|
|
||||||
// consistent with most Ed25519 implementations.
|
|
||||||
func (v *Element) SetBytes(x []byte) (*Element, error) {
|
|
||||||
if len(x) != 32 {
|
|
||||||
return nil, errors.New("edwards25519: invalid field element input size")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bits 0:51 (bytes 0:8, bits 0:64, shift 0, mask 51).
|
|
||||||
v.l0 = binary.LittleEndian.Uint64(x[0:8])
|
|
||||||
v.l0 &= maskLow51Bits
|
|
||||||
// Bits 51:102 (bytes 6:14, bits 48:112, shift 3, mask 51).
|
|
||||||
v.l1 = binary.LittleEndian.Uint64(x[6:14]) >> 3
|
|
||||||
v.l1 &= maskLow51Bits
|
|
||||||
// Bits 102:153 (bytes 12:20, bits 96:160, shift 6, mask 51).
|
|
||||||
v.l2 = binary.LittleEndian.Uint64(x[12:20]) >> 6
|
|
||||||
v.l2 &= maskLow51Bits
|
|
||||||
// Bits 153:204 (bytes 19:27, bits 152:216, shift 1, mask 51).
|
|
||||||
v.l3 = binary.LittleEndian.Uint64(x[19:27]) >> 1
|
|
||||||
v.l3 &= maskLow51Bits
|
|
||||||
// Bits 204:255 (bytes 24:32, bits 192:256, shift 12, mask 51).
|
|
||||||
// Note: not bytes 25:33, shift 4, to avoid overread.
|
|
||||||
v.l4 = binary.LittleEndian.Uint64(x[24:32]) >> 12
|
|
||||||
v.l4 &= maskLow51Bits
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the canonical 32-byte little-endian encoding of v.
|
|
||||||
func (v *Element) Bytes() []byte {
|
|
||||||
// This function is outlined to make the allocations inline in the caller
|
|
||||||
// rather than happen on the heap.
|
|
||||||
var out [32]byte
|
|
||||||
return v.bytes(&out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Element) bytes(out *[32]byte) []byte {
|
|
||||||
t := *v
|
|
||||||
t.reduce()
|
|
||||||
|
|
||||||
var buf [8]byte
|
|
||||||
for i, l := range [5]uint64{t.l0, t.l1, t.l2, t.l3, t.l4} {
|
|
||||||
bitsOffset := i * 51
|
|
||||||
binary.LittleEndian.PutUint64(buf[:], l<<uint(bitsOffset%8))
|
|
||||||
for i, bb := range buf {
|
|
||||||
off := bitsOffset/8 + i
|
|
||||||
if off >= len(out) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
out[off] |= bb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equal returns 1 if v and u are equal, and 0 otherwise.
|
|
||||||
func (v *Element) Equal(u *Element) int {
|
|
||||||
sa, sv := u.Bytes(), v.Bytes()
|
|
||||||
return subtle.ConstantTimeCompare(sa, sv)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mask64Bits returns 0xffffffff if cond is 1, and 0 otherwise.
|
|
||||||
func mask64Bits(cond int) uint64 { return ^(uint64(cond) - 1) }
|
|
||||||
|
|
||||||
// Select sets v to a if cond == 1, and to b if cond == 0.
|
|
||||||
func (v *Element) Select(a, b *Element, cond int) *Element {
|
|
||||||
m := mask64Bits(cond)
|
|
||||||
v.l0 = (m & a.l0) | (^m & b.l0)
|
|
||||||
v.l1 = (m & a.l1) | (^m & b.l1)
|
|
||||||
v.l2 = (m & a.l2) | (^m & b.l2)
|
|
||||||
v.l3 = (m & a.l3) | (^m & b.l3)
|
|
||||||
v.l4 = (m & a.l4) | (^m & b.l4)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap swaps v and u if cond == 1 or leaves them unchanged if cond == 0, and returns v.
|
|
||||||
func (v *Element) Swap(u *Element, cond int) {
|
|
||||||
m := mask64Bits(cond)
|
|
||||||
t := m & (v.l0 ^ u.l0)
|
|
||||||
v.l0 ^= t
|
|
||||||
u.l0 ^= t
|
|
||||||
t = m & (v.l1 ^ u.l1)
|
|
||||||
v.l1 ^= t
|
|
||||||
u.l1 ^= t
|
|
||||||
t = m & (v.l2 ^ u.l2)
|
|
||||||
v.l2 ^= t
|
|
||||||
u.l2 ^= t
|
|
||||||
t = m & (v.l3 ^ u.l3)
|
|
||||||
v.l3 ^= t
|
|
||||||
u.l3 ^= t
|
|
||||||
t = m & (v.l4 ^ u.l4)
|
|
||||||
v.l4 ^= t
|
|
||||||
u.l4 ^= t
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNegative returns 1 if v is negative, and 0 otherwise.
|
|
||||||
func (v *Element) IsNegative() int {
|
|
||||||
return int(v.Bytes()[0] & 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Absolute sets v to |u|, and returns v.
|
|
||||||
func (v *Element) Absolute(u *Element) *Element {
|
|
||||||
return v.Select(new(Element).Negate(u), u, u.IsNegative())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiply sets v = x * y, and returns v.
|
|
||||||
func (v *Element) Multiply(x, y *Element) *Element {
|
|
||||||
feMul(v, x, y)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Square sets v = x * x, and returns v.
|
|
||||||
func (v *Element) Square(x *Element) *Element {
|
|
||||||
feSquare(v, x)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mult32 sets v = x * y, and returns v.
|
|
||||||
func (v *Element) Mult32(x *Element, y uint32) *Element {
|
|
||||||
x0lo, x0hi := mul51(x.l0, y)
|
|
||||||
x1lo, x1hi := mul51(x.l1, y)
|
|
||||||
x2lo, x2hi := mul51(x.l2, y)
|
|
||||||
x3lo, x3hi := mul51(x.l3, y)
|
|
||||||
x4lo, x4hi := mul51(x.l4, y)
|
|
||||||
v.l0 = x0lo + 19*x4hi // carried over per the reduction identity
|
|
||||||
v.l1 = x1lo + x0hi
|
|
||||||
v.l2 = x2lo + x1hi
|
|
||||||
v.l3 = x3lo + x2hi
|
|
||||||
v.l4 = x4lo + x3hi
|
|
||||||
// The hi portions are going to be only 32 bits, plus any previous excess,
|
|
||||||
// so we can skip the carry propagation.
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// mul51 returns lo + hi * 2⁵¹ = a * b.
|
|
||||||
func mul51(a uint64, b uint32) (lo uint64, hi uint64) {
|
|
||||||
mh, ml := bits.Mul64(a, uint64(b))
|
|
||||||
lo = ml & maskLow51Bits
|
|
||||||
hi = (mh << 13) | (ml >> 51)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pow22523 set v = x^((p-5)/8), and returns v. (p-5)/8 is 2^252-3.
|
|
||||||
func (v *Element) Pow22523(x *Element) *Element {
|
|
||||||
var t0, t1, t2 Element
|
|
||||||
|
|
||||||
t0.Square(x) // x^2
|
|
||||||
t1.Square(&t0) // x^4
|
|
||||||
t1.Square(&t1) // x^8
|
|
||||||
t1.Multiply(x, &t1) // x^9
|
|
||||||
t0.Multiply(&t0, &t1) // x^11
|
|
||||||
t0.Square(&t0) // x^22
|
|
||||||
t0.Multiply(&t1, &t0) // x^31
|
|
||||||
t1.Square(&t0) // x^62
|
|
||||||
for i := 1; i < 5; i++ { // x^992
|
|
||||||
t1.Square(&t1)
|
|
||||||
}
|
|
||||||
t0.Multiply(&t1, &t0) // x^1023 -> 1023 = 2^10 - 1
|
|
||||||
t1.Square(&t0) // 2^11 - 2
|
|
||||||
for i := 1; i < 10; i++ { // 2^20 - 2^10
|
|
||||||
t1.Square(&t1)
|
|
||||||
}
|
|
||||||
t1.Multiply(&t1, &t0) // 2^20 - 1
|
|
||||||
t2.Square(&t1) // 2^21 - 2
|
|
||||||
for i := 1; i < 20; i++ { // 2^40 - 2^20
|
|
||||||
t2.Square(&t2)
|
|
||||||
}
|
|
||||||
t1.Multiply(&t2, &t1) // 2^40 - 1
|
|
||||||
t1.Square(&t1) // 2^41 - 2
|
|
||||||
for i := 1; i < 10; i++ { // 2^50 - 2^10
|
|
||||||
t1.Square(&t1)
|
|
||||||
}
|
|
||||||
t0.Multiply(&t1, &t0) // 2^50 - 1
|
|
||||||
t1.Square(&t0) // 2^51 - 2
|
|
||||||
for i := 1; i < 50; i++ { // 2^100 - 2^50
|
|
||||||
t1.Square(&t1)
|
|
||||||
}
|
|
||||||
t1.Multiply(&t1, &t0) // 2^100 - 1
|
|
||||||
t2.Square(&t1) // 2^101 - 2
|
|
||||||
for i := 1; i < 100; i++ { // 2^200 - 2^100
|
|
||||||
t2.Square(&t2)
|
|
||||||
}
|
|
||||||
t1.Multiply(&t2, &t1) // 2^200 - 1
|
|
||||||
t1.Square(&t1) // 2^201 - 2
|
|
||||||
for i := 1; i < 50; i++ { // 2^250 - 2^50
|
|
||||||
t1.Square(&t1)
|
|
||||||
}
|
|
||||||
t0.Multiply(&t1, &t0) // 2^250 - 1
|
|
||||||
t0.Square(&t0) // 2^251 - 2
|
|
||||||
t0.Square(&t0) // 2^252 - 4
|
|
||||||
return v.Multiply(&t0, x) // 2^252 - 3 -> x^(2^252-3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sqrtM1 is 2^((p-1)/4), which squared is equal to -1 by Euler's Criterion.
|
|
||||||
var sqrtM1 = &Element{1718705420411056, 234908883556509,
|
|
||||||
2233514472574048, 2117202627021982, 765476049583133}
|
|
||||||
|
|
||||||
// SqrtRatio sets r to the non-negative square root of the ratio of u and v.
|
|
||||||
//
|
|
||||||
// If u/v is square, SqrtRatio returns r and 1. If u/v is not square, SqrtRatio
|
|
||||||
// sets r according to Section 4.3 of draft-irtf-cfrg-ristretto255-decaf448-00,
|
|
||||||
// and returns r and 0.
|
|
||||||
func (r *Element) SqrtRatio(u, v *Element) (R *Element, wasSquare int) {
|
|
||||||
t0 := new(Element)
|
|
||||||
|
|
||||||
// r = (u * v3) * (u * v7)^((p-5)/8)
|
|
||||||
v2 := new(Element).Square(v)
|
|
||||||
uv3 := new(Element).Multiply(u, t0.Multiply(v2, v))
|
|
||||||
uv7 := new(Element).Multiply(uv3, t0.Square(v2))
|
|
||||||
rr := new(Element).Multiply(uv3, t0.Pow22523(uv7))
|
|
||||||
|
|
||||||
check := new(Element).Multiply(v, t0.Square(rr)) // check = v * r^2
|
|
||||||
|
|
||||||
uNeg := new(Element).Negate(u)
|
|
||||||
correctSignSqrt := check.Equal(u)
|
|
||||||
flippedSignSqrt := check.Equal(uNeg)
|
|
||||||
flippedSignSqrtI := check.Equal(t0.Multiply(uNeg, sqrtM1))
|
|
||||||
|
|
||||||
rPrime := new(Element).Multiply(rr, sqrtM1) // r_prime = SQRT_M1 * r
|
|
||||||
// r = CT_SELECT(r_prime IF flipped_sign_sqrt | flipped_sign_sqrt_i ELSE r)
|
|
||||||
rr.Select(rPrime, rr, flippedSignSqrt|flippedSignSqrtI)
|
|
||||||
|
|
||||||
r.Absolute(rr) // Choose the nonnegative square root.
|
|
||||||
return r, correctSignSqrt | flippedSignSqrt
|
|
||||||
}
|
|
||||||
16
vendor/filippo.io/edwards25519/field/fe_amd64.go
generated
vendored
16
vendor/filippo.io/edwards25519/field/fe_amd64.go
generated
vendored
@ -1,16 +0,0 @@
|
|||||||
// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT.
|
|
||||||
|
|
||||||
//go:build amd64 && gc && !purego
|
|
||||||
// +build amd64,gc,!purego
|
|
||||||
|
|
||||||
package field
|
|
||||||
|
|
||||||
// feMul sets out = a * b. It works like feMulGeneric.
|
|
||||||
//
|
|
||||||
//go:noescape
|
|
||||||
func feMul(out *Element, a *Element, b *Element)
|
|
||||||
|
|
||||||
// feSquare sets out = a * a. It works like feSquareGeneric.
|
|
||||||
//
|
|
||||||
//go:noescape
|
|
||||||
func feSquare(out *Element, a *Element)
|
|
||||||
379
vendor/filippo.io/edwards25519/field/fe_amd64.s
generated
vendored
379
vendor/filippo.io/edwards25519/field/fe_amd64.s
generated
vendored
@ -1,379 +0,0 @@
|
|||||||
// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT.
|
|
||||||
|
|
||||||
//go:build amd64 && gc && !purego
|
|
||||||
// +build amd64,gc,!purego
|
|
||||||
|
|
||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
// func feMul(out *Element, a *Element, b *Element)
|
|
||||||
TEXT ·feMul(SB), NOSPLIT, $0-24
|
|
||||||
MOVQ a+8(FP), CX
|
|
||||||
MOVQ b+16(FP), BX
|
|
||||||
|
|
||||||
// r0 = a0×b0
|
|
||||||
MOVQ (CX), AX
|
|
||||||
MULQ (BX)
|
|
||||||
MOVQ AX, DI
|
|
||||||
MOVQ DX, SI
|
|
||||||
|
|
||||||
// r0 += 19×a1×b4
|
|
||||||
MOVQ 8(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 32(BX)
|
|
||||||
ADDQ AX, DI
|
|
||||||
ADCQ DX, SI
|
|
||||||
|
|
||||||
// r0 += 19×a2×b3
|
|
||||||
MOVQ 16(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 24(BX)
|
|
||||||
ADDQ AX, DI
|
|
||||||
ADCQ DX, SI
|
|
||||||
|
|
||||||
// r0 += 19×a3×b2
|
|
||||||
MOVQ 24(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 16(BX)
|
|
||||||
ADDQ AX, DI
|
|
||||||
ADCQ DX, SI
|
|
||||||
|
|
||||||
// r0 += 19×a4×b1
|
|
||||||
MOVQ 32(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 8(BX)
|
|
||||||
ADDQ AX, DI
|
|
||||||
ADCQ DX, SI
|
|
||||||
|
|
||||||
// r1 = a0×b1
|
|
||||||
MOVQ (CX), AX
|
|
||||||
MULQ 8(BX)
|
|
||||||
MOVQ AX, R9
|
|
||||||
MOVQ DX, R8
|
|
||||||
|
|
||||||
// r1 += a1×b0
|
|
||||||
MOVQ 8(CX), AX
|
|
||||||
MULQ (BX)
|
|
||||||
ADDQ AX, R9
|
|
||||||
ADCQ DX, R8
|
|
||||||
|
|
||||||
// r1 += 19×a2×b4
|
|
||||||
MOVQ 16(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 32(BX)
|
|
||||||
ADDQ AX, R9
|
|
||||||
ADCQ DX, R8
|
|
||||||
|
|
||||||
// r1 += 19×a3×b3
|
|
||||||
MOVQ 24(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 24(BX)
|
|
||||||
ADDQ AX, R9
|
|
||||||
ADCQ DX, R8
|
|
||||||
|
|
||||||
// r1 += 19×a4×b2
|
|
||||||
MOVQ 32(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 16(BX)
|
|
||||||
ADDQ AX, R9
|
|
||||||
ADCQ DX, R8
|
|
||||||
|
|
||||||
// r2 = a0×b2
|
|
||||||
MOVQ (CX), AX
|
|
||||||
MULQ 16(BX)
|
|
||||||
MOVQ AX, R11
|
|
||||||
MOVQ DX, R10
|
|
||||||
|
|
||||||
// r2 += a1×b1
|
|
||||||
MOVQ 8(CX), AX
|
|
||||||
MULQ 8(BX)
|
|
||||||
ADDQ AX, R11
|
|
||||||
ADCQ DX, R10
|
|
||||||
|
|
||||||
// r2 += a2×b0
|
|
||||||
MOVQ 16(CX), AX
|
|
||||||
MULQ (BX)
|
|
||||||
ADDQ AX, R11
|
|
||||||
ADCQ DX, R10
|
|
||||||
|
|
||||||
// r2 += 19×a3×b4
|
|
||||||
MOVQ 24(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 32(BX)
|
|
||||||
ADDQ AX, R11
|
|
||||||
ADCQ DX, R10
|
|
||||||
|
|
||||||
// r2 += 19×a4×b3
|
|
||||||
MOVQ 32(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 24(BX)
|
|
||||||
ADDQ AX, R11
|
|
||||||
ADCQ DX, R10
|
|
||||||
|
|
||||||
// r3 = a0×b3
|
|
||||||
MOVQ (CX), AX
|
|
||||||
MULQ 24(BX)
|
|
||||||
MOVQ AX, R13
|
|
||||||
MOVQ DX, R12
|
|
||||||
|
|
||||||
// r3 += a1×b2
|
|
||||||
MOVQ 8(CX), AX
|
|
||||||
MULQ 16(BX)
|
|
||||||
ADDQ AX, R13
|
|
||||||
ADCQ DX, R12
|
|
||||||
|
|
||||||
// r3 += a2×b1
|
|
||||||
MOVQ 16(CX), AX
|
|
||||||
MULQ 8(BX)
|
|
||||||
ADDQ AX, R13
|
|
||||||
ADCQ DX, R12
|
|
||||||
|
|
||||||
// r3 += a3×b0
|
|
||||||
MOVQ 24(CX), AX
|
|
||||||
MULQ (BX)
|
|
||||||
ADDQ AX, R13
|
|
||||||
ADCQ DX, R12
|
|
||||||
|
|
||||||
// r3 += 19×a4×b4
|
|
||||||
MOVQ 32(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 32(BX)
|
|
||||||
ADDQ AX, R13
|
|
||||||
ADCQ DX, R12
|
|
||||||
|
|
||||||
// r4 = a0×b4
|
|
||||||
MOVQ (CX), AX
|
|
||||||
MULQ 32(BX)
|
|
||||||
MOVQ AX, R15
|
|
||||||
MOVQ DX, R14
|
|
||||||
|
|
||||||
// r4 += a1×b3
|
|
||||||
MOVQ 8(CX), AX
|
|
||||||
MULQ 24(BX)
|
|
||||||
ADDQ AX, R15
|
|
||||||
ADCQ DX, R14
|
|
||||||
|
|
||||||
// r4 += a2×b2
|
|
||||||
MOVQ 16(CX), AX
|
|
||||||
MULQ 16(BX)
|
|
||||||
ADDQ AX, R15
|
|
||||||
ADCQ DX, R14
|
|
||||||
|
|
||||||
// r4 += a3×b1
|
|
||||||
MOVQ 24(CX), AX
|
|
||||||
MULQ 8(BX)
|
|
||||||
ADDQ AX, R15
|
|
||||||
ADCQ DX, R14
|
|
||||||
|
|
||||||
// r4 += a4×b0
|
|
||||||
MOVQ 32(CX), AX
|
|
||||||
MULQ (BX)
|
|
||||||
ADDQ AX, R15
|
|
||||||
ADCQ DX, R14
|
|
||||||
|
|
||||||
// First reduction chain
|
|
||||||
MOVQ $0x0007ffffffffffff, AX
|
|
||||||
SHLQ $0x0d, DI, SI
|
|
||||||
SHLQ $0x0d, R9, R8
|
|
||||||
SHLQ $0x0d, R11, R10
|
|
||||||
SHLQ $0x0d, R13, R12
|
|
||||||
SHLQ $0x0d, R15, R14
|
|
||||||
ANDQ AX, DI
|
|
||||||
IMUL3Q $0x13, R14, R14
|
|
||||||
ADDQ R14, DI
|
|
||||||
ANDQ AX, R9
|
|
||||||
ADDQ SI, R9
|
|
||||||
ANDQ AX, R11
|
|
||||||
ADDQ R8, R11
|
|
||||||
ANDQ AX, R13
|
|
||||||
ADDQ R10, R13
|
|
||||||
ANDQ AX, R15
|
|
||||||
ADDQ R12, R15
|
|
||||||
|
|
||||||
// Second reduction chain (carryPropagate)
|
|
||||||
MOVQ DI, SI
|
|
||||||
SHRQ $0x33, SI
|
|
||||||
MOVQ R9, R8
|
|
||||||
SHRQ $0x33, R8
|
|
||||||
MOVQ R11, R10
|
|
||||||
SHRQ $0x33, R10
|
|
||||||
MOVQ R13, R12
|
|
||||||
SHRQ $0x33, R12
|
|
||||||
MOVQ R15, R14
|
|
||||||
SHRQ $0x33, R14
|
|
||||||
ANDQ AX, DI
|
|
||||||
IMUL3Q $0x13, R14, R14
|
|
||||||
ADDQ R14, DI
|
|
||||||
ANDQ AX, R9
|
|
||||||
ADDQ SI, R9
|
|
||||||
ANDQ AX, R11
|
|
||||||
ADDQ R8, R11
|
|
||||||
ANDQ AX, R13
|
|
||||||
ADDQ R10, R13
|
|
||||||
ANDQ AX, R15
|
|
||||||
ADDQ R12, R15
|
|
||||||
|
|
||||||
// Store output
|
|
||||||
MOVQ out+0(FP), AX
|
|
||||||
MOVQ DI, (AX)
|
|
||||||
MOVQ R9, 8(AX)
|
|
||||||
MOVQ R11, 16(AX)
|
|
||||||
MOVQ R13, 24(AX)
|
|
||||||
MOVQ R15, 32(AX)
|
|
||||||
RET
|
|
||||||
|
|
||||||
// func feSquare(out *Element, a *Element)
|
|
||||||
TEXT ·feSquare(SB), NOSPLIT, $0-16
|
|
||||||
MOVQ a+8(FP), CX
|
|
||||||
|
|
||||||
// r0 = l0×l0
|
|
||||||
MOVQ (CX), AX
|
|
||||||
MULQ (CX)
|
|
||||||
MOVQ AX, SI
|
|
||||||
MOVQ DX, BX
|
|
||||||
|
|
||||||
// r0 += 38×l1×l4
|
|
||||||
MOVQ 8(CX), AX
|
|
||||||
IMUL3Q $0x26, AX, AX
|
|
||||||
MULQ 32(CX)
|
|
||||||
ADDQ AX, SI
|
|
||||||
ADCQ DX, BX
|
|
||||||
|
|
||||||
// r0 += 38×l2×l3
|
|
||||||
MOVQ 16(CX), AX
|
|
||||||
IMUL3Q $0x26, AX, AX
|
|
||||||
MULQ 24(CX)
|
|
||||||
ADDQ AX, SI
|
|
||||||
ADCQ DX, BX
|
|
||||||
|
|
||||||
// r1 = 2×l0×l1
|
|
||||||
MOVQ (CX), AX
|
|
||||||
SHLQ $0x01, AX
|
|
||||||
MULQ 8(CX)
|
|
||||||
MOVQ AX, R8
|
|
||||||
MOVQ DX, DI
|
|
||||||
|
|
||||||
// r1 += 38×l2×l4
|
|
||||||
MOVQ 16(CX), AX
|
|
||||||
IMUL3Q $0x26, AX, AX
|
|
||||||
MULQ 32(CX)
|
|
||||||
ADDQ AX, R8
|
|
||||||
ADCQ DX, DI
|
|
||||||
|
|
||||||
// r1 += 19×l3×l3
|
|
||||||
MOVQ 24(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 24(CX)
|
|
||||||
ADDQ AX, R8
|
|
||||||
ADCQ DX, DI
|
|
||||||
|
|
||||||
// r2 = 2×l0×l2
|
|
||||||
MOVQ (CX), AX
|
|
||||||
SHLQ $0x01, AX
|
|
||||||
MULQ 16(CX)
|
|
||||||
MOVQ AX, R10
|
|
||||||
MOVQ DX, R9
|
|
||||||
|
|
||||||
// r2 += l1×l1
|
|
||||||
MOVQ 8(CX), AX
|
|
||||||
MULQ 8(CX)
|
|
||||||
ADDQ AX, R10
|
|
||||||
ADCQ DX, R9
|
|
||||||
|
|
||||||
// r2 += 38×l3×l4
|
|
||||||
MOVQ 24(CX), AX
|
|
||||||
IMUL3Q $0x26, AX, AX
|
|
||||||
MULQ 32(CX)
|
|
||||||
ADDQ AX, R10
|
|
||||||
ADCQ DX, R9
|
|
||||||
|
|
||||||
// r3 = 2×l0×l3
|
|
||||||
MOVQ (CX), AX
|
|
||||||
SHLQ $0x01, AX
|
|
||||||
MULQ 24(CX)
|
|
||||||
MOVQ AX, R12
|
|
||||||
MOVQ DX, R11
|
|
||||||
|
|
||||||
// r3 += 2×l1×l2
|
|
||||||
MOVQ 8(CX), AX
|
|
||||||
IMUL3Q $0x02, AX, AX
|
|
||||||
MULQ 16(CX)
|
|
||||||
ADDQ AX, R12
|
|
||||||
ADCQ DX, R11
|
|
||||||
|
|
||||||
// r3 += 19×l4×l4
|
|
||||||
MOVQ 32(CX), AX
|
|
||||||
IMUL3Q $0x13, AX, AX
|
|
||||||
MULQ 32(CX)
|
|
||||||
ADDQ AX, R12
|
|
||||||
ADCQ DX, R11
|
|
||||||
|
|
||||||
// r4 = 2×l0×l4
|
|
||||||
MOVQ (CX), AX
|
|
||||||
SHLQ $0x01, AX
|
|
||||||
MULQ 32(CX)
|
|
||||||
MOVQ AX, R14
|
|
||||||
MOVQ DX, R13
|
|
||||||
|
|
||||||
// r4 += 2×l1×l3
|
|
||||||
MOVQ 8(CX), AX
|
|
||||||
IMUL3Q $0x02, AX, AX
|
|
||||||
MULQ 24(CX)
|
|
||||||
ADDQ AX, R14
|
|
||||||
ADCQ DX, R13
|
|
||||||
|
|
||||||
// r4 += l2×l2
|
|
||||||
MOVQ 16(CX), AX
|
|
||||||
MULQ 16(CX)
|
|
||||||
ADDQ AX, R14
|
|
||||||
ADCQ DX, R13
|
|
||||||
|
|
||||||
// First reduction chain
|
|
||||||
MOVQ $0x0007ffffffffffff, AX
|
|
||||||
SHLQ $0x0d, SI, BX
|
|
||||||
SHLQ $0x0d, R8, DI
|
|
||||||
SHLQ $0x0d, R10, R9
|
|
||||||
SHLQ $0x0d, R12, R11
|
|
||||||
SHLQ $0x0d, R14, R13
|
|
||||||
ANDQ AX, SI
|
|
||||||
IMUL3Q $0x13, R13, R13
|
|
||||||
ADDQ R13, SI
|
|
||||||
ANDQ AX, R8
|
|
||||||
ADDQ BX, R8
|
|
||||||
ANDQ AX, R10
|
|
||||||
ADDQ DI, R10
|
|
||||||
ANDQ AX, R12
|
|
||||||
ADDQ R9, R12
|
|
||||||
ANDQ AX, R14
|
|
||||||
ADDQ R11, R14
|
|
||||||
|
|
||||||
// Second reduction chain (carryPropagate)
|
|
||||||
MOVQ SI, BX
|
|
||||||
SHRQ $0x33, BX
|
|
||||||
MOVQ R8, DI
|
|
||||||
SHRQ $0x33, DI
|
|
||||||
MOVQ R10, R9
|
|
||||||
SHRQ $0x33, R9
|
|
||||||
MOVQ R12, R11
|
|
||||||
SHRQ $0x33, R11
|
|
||||||
MOVQ R14, R13
|
|
||||||
SHRQ $0x33, R13
|
|
||||||
ANDQ AX, SI
|
|
||||||
IMUL3Q $0x13, R13, R13
|
|
||||||
ADDQ R13, SI
|
|
||||||
ANDQ AX, R8
|
|
||||||
ADDQ BX, R8
|
|
||||||
ANDQ AX, R10
|
|
||||||
ADDQ DI, R10
|
|
||||||
ANDQ AX, R12
|
|
||||||
ADDQ R9, R12
|
|
||||||
ANDQ AX, R14
|
|
||||||
ADDQ R11, R14
|
|
||||||
|
|
||||||
// Store output
|
|
||||||
MOVQ out+0(FP), AX
|
|
||||||
MOVQ SI, (AX)
|
|
||||||
MOVQ R8, 8(AX)
|
|
||||||
MOVQ R10, 16(AX)
|
|
||||||
MOVQ R12, 24(AX)
|
|
||||||
MOVQ R14, 32(AX)
|
|
||||||
RET
|
|
||||||
12
vendor/filippo.io/edwards25519/field/fe_amd64_noasm.go
generated
vendored
12
vendor/filippo.io/edwards25519/field/fe_amd64_noasm.go
generated
vendored
@ -1,12 +0,0 @@
|
|||||||
// Copyright (c) 2019 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build !amd64 || !gc || purego
|
|
||||||
// +build !amd64 !gc purego
|
|
||||||
|
|
||||||
package field
|
|
||||||
|
|
||||||
func feMul(v, x, y *Element) { feMulGeneric(v, x, y) }
|
|
||||||
|
|
||||||
func feSquare(v, x *Element) { feSquareGeneric(v, x) }
|
|
||||||
16
vendor/filippo.io/edwards25519/field/fe_arm64.go
generated
vendored
16
vendor/filippo.io/edwards25519/field/fe_arm64.go
generated
vendored
@ -1,16 +0,0 @@
|
|||||||
// Copyright (c) 2020 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build arm64 && gc && !purego
|
|
||||||
// +build arm64,gc,!purego
|
|
||||||
|
|
||||||
package field
|
|
||||||
|
|
||||||
//go:noescape
|
|
||||||
func carryPropagate(v *Element)
|
|
||||||
|
|
||||||
func (v *Element) carryPropagate() *Element {
|
|
||||||
carryPropagate(v)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
42
vendor/filippo.io/edwards25519/field/fe_arm64.s
generated
vendored
42
vendor/filippo.io/edwards25519/field/fe_arm64.s
generated
vendored
@ -1,42 +0,0 @@
|
|||||||
// Copyright (c) 2020 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build arm64 && gc && !purego
|
|
||||||
|
|
||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
// carryPropagate works exactly like carryPropagateGeneric and uses the
|
|
||||||
// same AND, ADD, and LSR+MADD instructions emitted by the compiler, but
|
|
||||||
// avoids loading R0-R4 twice and uses LDP and STP.
|
|
||||||
//
|
|
||||||
// See https://golang.org/issues/43145 for the main compiler issue.
|
|
||||||
//
|
|
||||||
// func carryPropagate(v *Element)
|
|
||||||
TEXT ·carryPropagate(SB),NOFRAME|NOSPLIT,$0-8
|
|
||||||
MOVD v+0(FP), R20
|
|
||||||
|
|
||||||
LDP 0(R20), (R0, R1)
|
|
||||||
LDP 16(R20), (R2, R3)
|
|
||||||
MOVD 32(R20), R4
|
|
||||||
|
|
||||||
AND $0x7ffffffffffff, R0, R10
|
|
||||||
AND $0x7ffffffffffff, R1, R11
|
|
||||||
AND $0x7ffffffffffff, R2, R12
|
|
||||||
AND $0x7ffffffffffff, R3, R13
|
|
||||||
AND $0x7ffffffffffff, R4, R14
|
|
||||||
|
|
||||||
ADD R0>>51, R11, R11
|
|
||||||
ADD R1>>51, R12, R12
|
|
||||||
ADD R2>>51, R13, R13
|
|
||||||
ADD R3>>51, R14, R14
|
|
||||||
// R4>>51 * 19 + R10 -> R10
|
|
||||||
LSR $51, R4, R21
|
|
||||||
MOVD $19, R22
|
|
||||||
MADD R22, R10, R21, R10
|
|
||||||
|
|
||||||
STP (R10, R11), 0(R20)
|
|
||||||
STP (R12, R13), 16(R20)
|
|
||||||
MOVD R14, 32(R20)
|
|
||||||
|
|
||||||
RET
|
|
||||||
12
vendor/filippo.io/edwards25519/field/fe_arm64_noasm.go
generated
vendored
12
vendor/filippo.io/edwards25519/field/fe_arm64_noasm.go
generated
vendored
@ -1,12 +0,0 @@
|
|||||||
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build !arm64 || !gc || purego
|
|
||||||
// +build !arm64 !gc purego
|
|
||||||
|
|
||||||
package field
|
|
||||||
|
|
||||||
func (v *Element) carryPropagate() *Element {
|
|
||||||
return v.carryPropagateGeneric()
|
|
||||||
}
|
|
||||||
50
vendor/filippo.io/edwards25519/field/fe_extra.go
generated
vendored
50
vendor/filippo.io/edwards25519/field/fe_extra.go
generated
vendored
@ -1,50 +0,0 @@
|
|||||||
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package field
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
// This file contains additional functionality that is not included in the
|
|
||||||
// upstream crypto/ed25519/edwards25519/field package.
|
|
||||||
|
|
||||||
// SetWideBytes sets v to x, where x is a 64-byte little-endian encoding, which
|
|
||||||
// is reduced modulo the field order. If x is not of the right length,
|
|
||||||
// SetWideBytes returns nil and an error, and the receiver is unchanged.
|
|
||||||
//
|
|
||||||
// SetWideBytes is not necessary to select a uniformly distributed value, and is
|
|
||||||
// only provided for compatibility: SetBytes can be used instead as the chance
|
|
||||||
// of bias is less than 2⁻²⁵⁰.
|
|
||||||
func (v *Element) SetWideBytes(x []byte) (*Element, error) {
|
|
||||||
if len(x) != 64 {
|
|
||||||
return nil, errors.New("edwards25519: invalid SetWideBytes input size")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split the 64 bytes into two elements, and extract the most significant
|
|
||||||
// bit of each, which is ignored by SetBytes.
|
|
||||||
lo, _ := new(Element).SetBytes(x[:32])
|
|
||||||
loMSB := uint64(x[31] >> 7)
|
|
||||||
hi, _ := new(Element).SetBytes(x[32:])
|
|
||||||
hiMSB := uint64(x[63] >> 7)
|
|
||||||
|
|
||||||
// The output we want is
|
|
||||||
//
|
|
||||||
// v = lo + loMSB * 2²⁵⁵ + hi * 2²⁵⁶ + hiMSB * 2⁵¹¹
|
|
||||||
//
|
|
||||||
// which applying the reduction identity comes out to
|
|
||||||
//
|
|
||||||
// v = lo + loMSB * 19 + hi * 2 * 19 + hiMSB * 2 * 19²
|
|
||||||
//
|
|
||||||
// l0 will be the sum of a 52 bits value (lo.l0), plus a 5 bits value
|
|
||||||
// (loMSB * 19), a 6 bits value (hi.l0 * 2 * 19), and a 10 bits value
|
|
||||||
// (hiMSB * 2 * 19²), so it fits in a uint64.
|
|
||||||
|
|
||||||
v.l0 = lo.l0 + loMSB*19 + hi.l0*2*19 + hiMSB*2*19*19
|
|
||||||
v.l1 = lo.l1 + hi.l1*2*19
|
|
||||||
v.l2 = lo.l2 + hi.l2*2*19
|
|
||||||
v.l3 = lo.l3 + hi.l3*2*19
|
|
||||||
v.l4 = lo.l4 + hi.l4*2*19
|
|
||||||
|
|
||||||
return v.carryPropagate(), nil
|
|
||||||
}
|
|
||||||
266
vendor/filippo.io/edwards25519/field/fe_generic.go
generated
vendored
266
vendor/filippo.io/edwards25519/field/fe_generic.go
generated
vendored
@ -1,266 +0,0 @@
|
|||||||
// Copyright (c) 2017 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package field
|
|
||||||
|
|
||||||
import "math/bits"
|
|
||||||
|
|
||||||
// uint128 holds a 128-bit number as two 64-bit limbs, for use with the
|
|
||||||
// bits.Mul64 and bits.Add64 intrinsics.
|
|
||||||
type uint128 struct {
|
|
||||||
lo, hi uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// mul64 returns a * b.
|
|
||||||
func mul64(a, b uint64) uint128 {
|
|
||||||
hi, lo := bits.Mul64(a, b)
|
|
||||||
return uint128{lo, hi}
|
|
||||||
}
|
|
||||||
|
|
||||||
// addMul64 returns v + a * b.
|
|
||||||
func addMul64(v uint128, a, b uint64) uint128 {
|
|
||||||
hi, lo := bits.Mul64(a, b)
|
|
||||||
lo, c := bits.Add64(lo, v.lo, 0)
|
|
||||||
hi, _ = bits.Add64(hi, v.hi, c)
|
|
||||||
return uint128{lo, hi}
|
|
||||||
}
|
|
||||||
|
|
||||||
// shiftRightBy51 returns a >> 51. a is assumed to be at most 115 bits.
|
|
||||||
func shiftRightBy51(a uint128) uint64 {
|
|
||||||
return (a.hi << (64 - 51)) | (a.lo >> 51)
|
|
||||||
}
|
|
||||||
|
|
||||||
func feMulGeneric(v, a, b *Element) {
|
|
||||||
a0 := a.l0
|
|
||||||
a1 := a.l1
|
|
||||||
a2 := a.l2
|
|
||||||
a3 := a.l3
|
|
||||||
a4 := a.l4
|
|
||||||
|
|
||||||
b0 := b.l0
|
|
||||||
b1 := b.l1
|
|
||||||
b2 := b.l2
|
|
||||||
b3 := b.l3
|
|
||||||
b4 := b.l4
|
|
||||||
|
|
||||||
// Limb multiplication works like pen-and-paper columnar multiplication, but
|
|
||||||
// with 51-bit limbs instead of digits.
|
|
||||||
//
|
|
||||||
// a4 a3 a2 a1 a0 x
|
|
||||||
// b4 b3 b2 b1 b0 =
|
|
||||||
// ------------------------
|
|
||||||
// a4b0 a3b0 a2b0 a1b0 a0b0 +
|
|
||||||
// a4b1 a3b1 a2b1 a1b1 a0b1 +
|
|
||||||
// a4b2 a3b2 a2b2 a1b2 a0b2 +
|
|
||||||
// a4b3 a3b3 a2b3 a1b3 a0b3 +
|
|
||||||
// a4b4 a3b4 a2b4 a1b4 a0b4 =
|
|
||||||
// ----------------------------------------------
|
|
||||||
// r8 r7 r6 r5 r4 r3 r2 r1 r0
|
|
||||||
//
|
|
||||||
// We can then use the reduction identity (a * 2²⁵⁵ + b = a * 19 + b) to
|
|
||||||
// reduce the limbs that would overflow 255 bits. r5 * 2²⁵⁵ becomes 19 * r5,
|
|
||||||
// r6 * 2³⁰⁶ becomes 19 * r6 * 2⁵¹, etc.
|
|
||||||
//
|
|
||||||
// Reduction can be carried out simultaneously to multiplication. For
|
|
||||||
// example, we do not compute r5: whenever the result of a multiplication
|
|
||||||
// belongs to r5, like a1b4, we multiply it by 19 and add the result to r0.
|
|
||||||
//
|
|
||||||
// a4b0 a3b0 a2b0 a1b0 a0b0 +
|
|
||||||
// a3b1 a2b1 a1b1 a0b1 19×a4b1 +
|
|
||||||
// a2b2 a1b2 a0b2 19×a4b2 19×a3b2 +
|
|
||||||
// a1b3 a0b3 19×a4b3 19×a3b3 19×a2b3 +
|
|
||||||
// a0b4 19×a4b4 19×a3b4 19×a2b4 19×a1b4 =
|
|
||||||
// --------------------------------------
|
|
||||||
// r4 r3 r2 r1 r0
|
|
||||||
//
|
|
||||||
// Finally we add up the columns into wide, overlapping limbs.
|
|
||||||
|
|
||||||
a1_19 := a1 * 19
|
|
||||||
a2_19 := a2 * 19
|
|
||||||
a3_19 := a3 * 19
|
|
||||||
a4_19 := a4 * 19
|
|
||||||
|
|
||||||
// r0 = a0×b0 + 19×(a1×b4 + a2×b3 + a3×b2 + a4×b1)
|
|
||||||
r0 := mul64(a0, b0)
|
|
||||||
r0 = addMul64(r0, a1_19, b4)
|
|
||||||
r0 = addMul64(r0, a2_19, b3)
|
|
||||||
r0 = addMul64(r0, a3_19, b2)
|
|
||||||
r0 = addMul64(r0, a4_19, b1)
|
|
||||||
|
|
||||||
// r1 = a0×b1 + a1×b0 + 19×(a2×b4 + a3×b3 + a4×b2)
|
|
||||||
r1 := mul64(a0, b1)
|
|
||||||
r1 = addMul64(r1, a1, b0)
|
|
||||||
r1 = addMul64(r1, a2_19, b4)
|
|
||||||
r1 = addMul64(r1, a3_19, b3)
|
|
||||||
r1 = addMul64(r1, a4_19, b2)
|
|
||||||
|
|
||||||
// r2 = a0×b2 + a1×b1 + a2×b0 + 19×(a3×b4 + a4×b3)
|
|
||||||
r2 := mul64(a0, b2)
|
|
||||||
r2 = addMul64(r2, a1, b1)
|
|
||||||
r2 = addMul64(r2, a2, b0)
|
|
||||||
r2 = addMul64(r2, a3_19, b4)
|
|
||||||
r2 = addMul64(r2, a4_19, b3)
|
|
||||||
|
|
||||||
// r3 = a0×b3 + a1×b2 + a2×b1 + a3×b0 + 19×a4×b4
|
|
||||||
r3 := mul64(a0, b3)
|
|
||||||
r3 = addMul64(r3, a1, b2)
|
|
||||||
r3 = addMul64(r3, a2, b1)
|
|
||||||
r3 = addMul64(r3, a3, b0)
|
|
||||||
r3 = addMul64(r3, a4_19, b4)
|
|
||||||
|
|
||||||
// r4 = a0×b4 + a1×b3 + a2×b2 + a3×b1 + a4×b0
|
|
||||||
r4 := mul64(a0, b4)
|
|
||||||
r4 = addMul64(r4, a1, b3)
|
|
||||||
r4 = addMul64(r4, a2, b2)
|
|
||||||
r4 = addMul64(r4, a3, b1)
|
|
||||||
r4 = addMul64(r4, a4, b0)
|
|
||||||
|
|
||||||
// After the multiplication, we need to reduce (carry) the five coefficients
|
|
||||||
// to obtain a result with limbs that are at most slightly larger than 2⁵¹,
|
|
||||||
// to respect the Element invariant.
|
|
||||||
//
|
|
||||||
// Overall, the reduction works the same as carryPropagate, except with
|
|
||||||
// wider inputs: we take the carry for each coefficient by shifting it right
|
|
||||||
// by 51, and add it to the limb above it. The top carry is multiplied by 19
|
|
||||||
// according to the reduction identity and added to the lowest limb.
|
|
||||||
//
|
|
||||||
// The largest coefficient (r0) will be at most 111 bits, which guarantees
|
|
||||||
// that all carries are at most 111 - 51 = 60 bits, which fits in a uint64.
|
|
||||||
//
|
|
||||||
// r0 = a0×b0 + 19×(a1×b4 + a2×b3 + a3×b2 + a4×b1)
|
|
||||||
// r0 < 2⁵²×2⁵² + 19×(2⁵²×2⁵² + 2⁵²×2⁵² + 2⁵²×2⁵² + 2⁵²×2⁵²)
|
|
||||||
// r0 < (1 + 19 × 4) × 2⁵² × 2⁵²
|
|
||||||
// r0 < 2⁷ × 2⁵² × 2⁵²
|
|
||||||
// r0 < 2¹¹¹
|
|
||||||
//
|
|
||||||
// Moreover, the top coefficient (r4) is at most 107 bits, so c4 is at most
|
|
||||||
// 56 bits, and c4 * 19 is at most 61 bits, which again fits in a uint64 and
|
|
||||||
// allows us to easily apply the reduction identity.
|
|
||||||
//
|
|
||||||
// r4 = a0×b4 + a1×b3 + a2×b2 + a3×b1 + a4×b0
|
|
||||||
// r4 < 5 × 2⁵² × 2⁵²
|
|
||||||
// r4 < 2¹⁰⁷
|
|
||||||
//
|
|
||||||
|
|
||||||
c0 := shiftRightBy51(r0)
|
|
||||||
c1 := shiftRightBy51(r1)
|
|
||||||
c2 := shiftRightBy51(r2)
|
|
||||||
c3 := shiftRightBy51(r3)
|
|
||||||
c4 := shiftRightBy51(r4)
|
|
||||||
|
|
||||||
rr0 := r0.lo&maskLow51Bits + c4*19
|
|
||||||
rr1 := r1.lo&maskLow51Bits + c0
|
|
||||||
rr2 := r2.lo&maskLow51Bits + c1
|
|
||||||
rr3 := r3.lo&maskLow51Bits + c2
|
|
||||||
rr4 := r4.lo&maskLow51Bits + c3
|
|
||||||
|
|
||||||
// Now all coefficients fit into 64-bit registers but are still too large to
|
|
||||||
// be passed around as an Element. We therefore do one last carry chain,
|
|
||||||
// where the carries will be small enough to fit in the wiggle room above 2⁵¹.
|
|
||||||
*v = Element{rr0, rr1, rr2, rr3, rr4}
|
|
||||||
v.carryPropagate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func feSquareGeneric(v, a *Element) {
|
|
||||||
l0 := a.l0
|
|
||||||
l1 := a.l1
|
|
||||||
l2 := a.l2
|
|
||||||
l3 := a.l3
|
|
||||||
l4 := a.l4
|
|
||||||
|
|
||||||
// Squaring works precisely like multiplication above, but thanks to its
|
|
||||||
// symmetry we get to group a few terms together.
|
|
||||||
//
|
|
||||||
// l4 l3 l2 l1 l0 x
|
|
||||||
// l4 l3 l2 l1 l0 =
|
|
||||||
// ------------------------
|
|
||||||
// l4l0 l3l0 l2l0 l1l0 l0l0 +
|
|
||||||
// l4l1 l3l1 l2l1 l1l1 l0l1 +
|
|
||||||
// l4l2 l3l2 l2l2 l1l2 l0l2 +
|
|
||||||
// l4l3 l3l3 l2l3 l1l3 l0l3 +
|
|
||||||
// l4l4 l3l4 l2l4 l1l4 l0l4 =
|
|
||||||
// ----------------------------------------------
|
|
||||||
// r8 r7 r6 r5 r4 r3 r2 r1 r0
|
|
||||||
//
|
|
||||||
// l4l0 l3l0 l2l0 l1l0 l0l0 +
|
|
||||||
// l3l1 l2l1 l1l1 l0l1 19×l4l1 +
|
|
||||||
// l2l2 l1l2 l0l2 19×l4l2 19×l3l2 +
|
|
||||||
// l1l3 l0l3 19×l4l3 19×l3l3 19×l2l3 +
|
|
||||||
// l0l4 19×l4l4 19×l3l4 19×l2l4 19×l1l4 =
|
|
||||||
// --------------------------------------
|
|
||||||
// r4 r3 r2 r1 r0
|
|
||||||
//
|
|
||||||
// With precomputed 2×, 19×, and 2×19× terms, we can compute each limb with
|
|
||||||
// only three Mul64 and four Add64, instead of five and eight.
|
|
||||||
|
|
||||||
l0_2 := l0 * 2
|
|
||||||
l1_2 := l1 * 2
|
|
||||||
|
|
||||||
l1_38 := l1 * 38
|
|
||||||
l2_38 := l2 * 38
|
|
||||||
l3_38 := l3 * 38
|
|
||||||
|
|
||||||
l3_19 := l3 * 19
|
|
||||||
l4_19 := l4 * 19
|
|
||||||
|
|
||||||
// r0 = l0×l0 + 19×(l1×l4 + l2×l3 + l3×l2 + l4×l1) = l0×l0 + 19×2×(l1×l4 + l2×l3)
|
|
||||||
r0 := mul64(l0, l0)
|
|
||||||
r0 = addMul64(r0, l1_38, l4)
|
|
||||||
r0 = addMul64(r0, l2_38, l3)
|
|
||||||
|
|
||||||
// r1 = l0×l1 + l1×l0 + 19×(l2×l4 + l3×l3 + l4×l2) = 2×l0×l1 + 19×2×l2×l4 + 19×l3×l3
|
|
||||||
r1 := mul64(l0_2, l1)
|
|
||||||
r1 = addMul64(r1, l2_38, l4)
|
|
||||||
r1 = addMul64(r1, l3_19, l3)
|
|
||||||
|
|
||||||
// r2 = l0×l2 + l1×l1 + l2×l0 + 19×(l3×l4 + l4×l3) = 2×l0×l2 + l1×l1 + 19×2×l3×l4
|
|
||||||
r2 := mul64(l0_2, l2)
|
|
||||||
r2 = addMul64(r2, l1, l1)
|
|
||||||
r2 = addMul64(r2, l3_38, l4)
|
|
||||||
|
|
||||||
// r3 = l0×l3 + l1×l2 + l2×l1 + l3×l0 + 19×l4×l4 = 2×l0×l3 + 2×l1×l2 + 19×l4×l4
|
|
||||||
r3 := mul64(l0_2, l3)
|
|
||||||
r3 = addMul64(r3, l1_2, l2)
|
|
||||||
r3 = addMul64(r3, l4_19, l4)
|
|
||||||
|
|
||||||
// r4 = l0×l4 + l1×l3 + l2×l2 + l3×l1 + l4×l0 = 2×l0×l4 + 2×l1×l3 + l2×l2
|
|
||||||
r4 := mul64(l0_2, l4)
|
|
||||||
r4 = addMul64(r4, l1_2, l3)
|
|
||||||
r4 = addMul64(r4, l2, l2)
|
|
||||||
|
|
||||||
c0 := shiftRightBy51(r0)
|
|
||||||
c1 := shiftRightBy51(r1)
|
|
||||||
c2 := shiftRightBy51(r2)
|
|
||||||
c3 := shiftRightBy51(r3)
|
|
||||||
c4 := shiftRightBy51(r4)
|
|
||||||
|
|
||||||
rr0 := r0.lo&maskLow51Bits + c4*19
|
|
||||||
rr1 := r1.lo&maskLow51Bits + c0
|
|
||||||
rr2 := r2.lo&maskLow51Bits + c1
|
|
||||||
rr3 := r3.lo&maskLow51Bits + c2
|
|
||||||
rr4 := r4.lo&maskLow51Bits + c3
|
|
||||||
|
|
||||||
*v = Element{rr0, rr1, rr2, rr3, rr4}
|
|
||||||
v.carryPropagate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// carryPropagateGeneric brings the limbs below 52 bits by applying the reduction
|
|
||||||
// identity (a * 2²⁵⁵ + b = a * 19 + b) to the l4 carry.
|
|
||||||
func (v *Element) carryPropagateGeneric() *Element {
|
|
||||||
c0 := v.l0 >> 51
|
|
||||||
c1 := v.l1 >> 51
|
|
||||||
c2 := v.l2 >> 51
|
|
||||||
c3 := v.l3 >> 51
|
|
||||||
c4 := v.l4 >> 51
|
|
||||||
|
|
||||||
// c4 is at most 64 - 51 = 13 bits, so c4*19 is at most 18 bits, and
|
|
||||||
// the final l0 will be at most 52 bits. Similarly for the rest.
|
|
||||||
v.l0 = v.l0&maskLow51Bits + c4*19
|
|
||||||
v.l1 = v.l1&maskLow51Bits + c0
|
|
||||||
v.l2 = v.l2&maskLow51Bits + c1
|
|
||||||
v.l3 = v.l3&maskLow51Bits + c2
|
|
||||||
v.l4 = v.l4&maskLow51Bits + c3
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
343
vendor/filippo.io/edwards25519/scalar.go
generated
vendored
343
vendor/filippo.io/edwards25519/scalar.go
generated
vendored
@ -1,343 +0,0 @@
|
|||||||
// Copyright (c) 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package edwards25519
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Scalar is an integer modulo
|
|
||||||
//
|
|
||||||
// l = 2^252 + 27742317777372353535851937790883648493
|
|
||||||
//
|
|
||||||
// which is the prime order of the edwards25519 group.
|
|
||||||
//
|
|
||||||
// This type works similarly to math/big.Int, and all arguments and
|
|
||||||
// receivers are allowed to alias.
|
|
||||||
//
|
|
||||||
// The zero value is a valid zero element.
|
|
||||||
type Scalar struct {
|
|
||||||
// s is the scalar in the Montgomery domain, in the format of the
|
|
||||||
// fiat-crypto implementation.
|
|
||||||
s fiatScalarMontgomeryDomainFieldElement
|
|
||||||
}
|
|
||||||
|
|
||||||
// The field implementation in scalar_fiat.go is generated by the fiat-crypto
|
|
||||||
// project (https://github.com/mit-plv/fiat-crypto) at version v0.0.9 (23d2dbc)
|
|
||||||
// from a formally verified model.
|
|
||||||
//
|
|
||||||
// fiat-crypto code comes under the following license.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2015-2020 The fiat-crypto Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// Redistribution and use in source and binary forms, with or without
|
|
||||||
// modification, are permitted provided that the following conditions are
|
|
||||||
// met:
|
|
||||||
//
|
|
||||||
// 1. Redistributions of source code must retain the above copyright
|
|
||||||
// notice, this list of conditions and the following disclaimer.
|
|
||||||
//
|
|
||||||
// THIS SOFTWARE IS PROVIDED BY the fiat-crypto authors "AS IS"
|
|
||||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Berkeley Software Design,
|
|
||||||
// Inc. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
||||||
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
||||||
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
||||||
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
||||||
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
//
|
|
||||||
|
|
||||||
// NewScalar returns a new zero Scalar.
|
|
||||||
func NewScalar() *Scalar {
|
|
||||||
return &Scalar{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MultiplyAdd sets s = x * y + z mod l, and returns s. It is equivalent to
|
|
||||||
// using Multiply and then Add.
|
|
||||||
func (s *Scalar) MultiplyAdd(x, y, z *Scalar) *Scalar {
|
|
||||||
// Make a copy of z in case it aliases s.
|
|
||||||
zCopy := new(Scalar).Set(z)
|
|
||||||
return s.Multiply(x, y).Add(s, zCopy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add sets s = x + y mod l, and returns s.
|
|
||||||
func (s *Scalar) Add(x, y *Scalar) *Scalar {
|
|
||||||
// s = 1 * x + y mod l
|
|
||||||
fiatScalarAdd(&s.s, &x.s, &y.s)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtract sets s = x - y mod l, and returns s.
|
|
||||||
func (s *Scalar) Subtract(x, y *Scalar) *Scalar {
|
|
||||||
// s = -1 * y + x mod l
|
|
||||||
fiatScalarSub(&s.s, &x.s, &y.s)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Negate sets s = -x mod l, and returns s.
|
|
||||||
func (s *Scalar) Negate(x *Scalar) *Scalar {
|
|
||||||
// s = -1 * x + 0 mod l
|
|
||||||
fiatScalarOpp(&s.s, &x.s)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiply sets s = x * y mod l, and returns s.
|
|
||||||
func (s *Scalar) Multiply(x, y *Scalar) *Scalar {
|
|
||||||
// s = x * y + 0 mod l
|
|
||||||
fiatScalarMul(&s.s, &x.s, &y.s)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set sets s = x, and returns s.
|
|
||||||
func (s *Scalar) Set(x *Scalar) *Scalar {
|
|
||||||
*s = *x
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUniformBytes sets s = x mod l, where x is a 64-byte little-endian integer.
|
|
||||||
// If x is not of the right length, SetUniformBytes returns nil and an error,
|
|
||||||
// and the receiver is unchanged.
|
|
||||||
//
|
|
||||||
// SetUniformBytes can be used to set s to a uniformly distributed value given
|
|
||||||
// 64 uniformly distributed random bytes.
|
|
||||||
func (s *Scalar) SetUniformBytes(x []byte) (*Scalar, error) {
|
|
||||||
if len(x) != 64 {
|
|
||||||
return nil, errors.New("edwards25519: invalid SetUniformBytes input length")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have a value x of 512 bits, but our fiatScalarFromBytes function
|
|
||||||
// expects an input lower than l, which is a little over 252 bits.
|
|
||||||
//
|
|
||||||
// Instead of writing a reduction function that operates on wider inputs, we
|
|
||||||
// can interpret x as the sum of three shorter values a, b, and c.
|
|
||||||
//
|
|
||||||
// x = a + b * 2^168 + c * 2^336 mod l
|
|
||||||
//
|
|
||||||
// We then precompute 2^168 and 2^336 modulo l, and perform the reduction
|
|
||||||
// with two multiplications and two additions.
|
|
||||||
|
|
||||||
s.setShortBytes(x[:21])
|
|
||||||
t := new(Scalar).setShortBytes(x[21:42])
|
|
||||||
s.Add(s, t.Multiply(t, scalarTwo168))
|
|
||||||
t.setShortBytes(x[42:])
|
|
||||||
s.Add(s, t.Multiply(t, scalarTwo336))
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// scalarTwo168 and scalarTwo336 are 2^168 and 2^336 modulo l, encoded as a
|
|
||||||
// fiatScalarMontgomeryDomainFieldElement, which is a little-endian 4-limb value
|
|
||||||
// in the 2^256 Montgomery domain.
|
|
||||||
var scalarTwo168 = &Scalar{s: [4]uint64{0x5b8ab432eac74798, 0x38afddd6de59d5d7,
|
|
||||||
0xa2c131b399411b7c, 0x6329a7ed9ce5a30}}
|
|
||||||
var scalarTwo336 = &Scalar{s: [4]uint64{0xbd3d108e2b35ecc5, 0x5c3a3718bdf9c90b,
|
|
||||||
0x63aa97a331b4f2ee, 0x3d217f5be65cb5c}}
|
|
||||||
|
|
||||||
// setShortBytes sets s = x mod l, where x is a little-endian integer shorter
|
|
||||||
// than 32 bytes.
|
|
||||||
func (s *Scalar) setShortBytes(x []byte) *Scalar {
|
|
||||||
if len(x) >= 32 {
|
|
||||||
panic("edwards25519: internal error: setShortBytes called with a long string")
|
|
||||||
}
|
|
||||||
var buf [32]byte
|
|
||||||
copy(buf[:], x)
|
|
||||||
fiatScalarFromBytes((*[4]uint64)(&s.s), &buf)
|
|
||||||
fiatScalarToMontgomery(&s.s, (*fiatScalarNonMontgomeryDomainFieldElement)(&s.s))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCanonicalBytes sets s = x, where x is a 32-byte little-endian encoding of
|
|
||||||
// s, and returns s. If x is not a canonical encoding of s, SetCanonicalBytes
|
|
||||||
// returns nil and an error, and the receiver is unchanged.
|
|
||||||
func (s *Scalar) SetCanonicalBytes(x []byte) (*Scalar, error) {
|
|
||||||
if len(x) != 32 {
|
|
||||||
return nil, errors.New("invalid scalar length")
|
|
||||||
}
|
|
||||||
if !isReduced(x) {
|
|
||||||
return nil, errors.New("invalid scalar encoding")
|
|
||||||
}
|
|
||||||
|
|
||||||
fiatScalarFromBytes((*[4]uint64)(&s.s), (*[32]byte)(x))
|
|
||||||
fiatScalarToMontgomery(&s.s, (*fiatScalarNonMontgomeryDomainFieldElement)(&s.s))
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// scalarMinusOneBytes is l - 1 in little endian.
|
|
||||||
var scalarMinusOneBytes = [32]byte{236, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16}
|
|
||||||
|
|
||||||
// isReduced returns whether the given scalar in 32-byte little endian encoded
|
|
||||||
// form is reduced modulo l.
|
|
||||||
func isReduced(s []byte) bool {
|
|
||||||
if len(s) != 32 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := len(s) - 1; i >= 0; i-- {
|
|
||||||
switch {
|
|
||||||
case s[i] > scalarMinusOneBytes[i]:
|
|
||||||
return false
|
|
||||||
case s[i] < scalarMinusOneBytes[i]:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBytesWithClamping applies the buffer pruning described in RFC 8032,
|
|
||||||
// Section 5.1.5 (also known as clamping) and sets s to the result. The input
|
|
||||||
// must be 32 bytes, and it is not modified. If x is not of the right length,
|
|
||||||
// SetBytesWithClamping returns nil and an error, and the receiver is unchanged.
|
|
||||||
//
|
|
||||||
// Note that since Scalar values are always reduced modulo the prime order of
|
|
||||||
// the curve, the resulting value will not preserve any of the cofactor-clearing
|
|
||||||
// properties that clamping is meant to provide. It will however work as
|
|
||||||
// expected as long as it is applied to points on the prime order subgroup, like
|
|
||||||
// in Ed25519. In fact, it is lost to history why RFC 8032 adopted the
|
|
||||||
// irrelevant RFC 7748 clamping, but it is now required for compatibility.
|
|
||||||
func (s *Scalar) SetBytesWithClamping(x []byte) (*Scalar, error) {
|
|
||||||
// The description above omits the purpose of the high bits of the clamping
|
|
||||||
// for brevity, but those are also lost to reductions, and are also
|
|
||||||
// irrelevant to edwards25519 as they protect against a specific
|
|
||||||
// implementation bug that was once observed in a generic Montgomery ladder.
|
|
||||||
if len(x) != 32 {
|
|
||||||
return nil, errors.New("edwards25519: invalid SetBytesWithClamping input length")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to use the wide reduction from SetUniformBytes, since clamping
|
|
||||||
// sets the 2^254 bit, making the value higher than the order.
|
|
||||||
var wideBytes [64]byte
|
|
||||||
copy(wideBytes[:], x[:])
|
|
||||||
wideBytes[0] &= 248
|
|
||||||
wideBytes[31] &= 63
|
|
||||||
wideBytes[31] |= 64
|
|
||||||
return s.SetUniformBytes(wideBytes[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the canonical 32-byte little-endian encoding of s.
|
|
||||||
func (s *Scalar) Bytes() []byte {
|
|
||||||
// This function is outlined to make the allocations inline in the caller
|
|
||||||
// rather than happen on the heap.
|
|
||||||
var encoded [32]byte
|
|
||||||
return s.bytes(&encoded)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scalar) bytes(out *[32]byte) []byte {
|
|
||||||
var ss fiatScalarNonMontgomeryDomainFieldElement
|
|
||||||
fiatScalarFromMontgomery(&ss, &s.s)
|
|
||||||
fiatScalarToBytes(out, (*[4]uint64)(&ss))
|
|
||||||
return out[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equal returns 1 if s and t are equal, and 0 otherwise.
|
|
||||||
func (s *Scalar) Equal(t *Scalar) int {
|
|
||||||
var diff fiatScalarMontgomeryDomainFieldElement
|
|
||||||
fiatScalarSub(&diff, &s.s, &t.s)
|
|
||||||
var nonzero uint64
|
|
||||||
fiatScalarNonzero(&nonzero, (*[4]uint64)(&diff))
|
|
||||||
nonzero |= nonzero >> 32
|
|
||||||
nonzero |= nonzero >> 16
|
|
||||||
nonzero |= nonzero >> 8
|
|
||||||
nonzero |= nonzero >> 4
|
|
||||||
nonzero |= nonzero >> 2
|
|
||||||
nonzero |= nonzero >> 1
|
|
||||||
return int(^nonzero) & 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// nonAdjacentForm computes a width-w non-adjacent form for this scalar.
|
|
||||||
//
|
|
||||||
// w must be between 2 and 8, or nonAdjacentForm will panic.
|
|
||||||
func (s *Scalar) nonAdjacentForm(w uint) [256]int8 {
|
|
||||||
// This implementation is adapted from the one
|
|
||||||
// in curve25519-dalek and is documented there:
|
|
||||||
// https://github.com/dalek-cryptography/curve25519-dalek/blob/f630041af28e9a405255f98a8a93adca18e4315b/src/scalar.rs#L800-L871
|
|
||||||
b := s.Bytes()
|
|
||||||
if b[31] > 127 {
|
|
||||||
panic("scalar has high bit set illegally")
|
|
||||||
}
|
|
||||||
if w < 2 {
|
|
||||||
panic("w must be at least 2 by the definition of NAF")
|
|
||||||
} else if w > 8 {
|
|
||||||
panic("NAF digits must fit in int8")
|
|
||||||
}
|
|
||||||
|
|
||||||
var naf [256]int8
|
|
||||||
var digits [5]uint64
|
|
||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
digits[i] = binary.LittleEndian.Uint64(b[i*8:])
|
|
||||||
}
|
|
||||||
|
|
||||||
width := uint64(1 << w)
|
|
||||||
windowMask := uint64(width - 1)
|
|
||||||
|
|
||||||
pos := uint(0)
|
|
||||||
carry := uint64(0)
|
|
||||||
for pos < 256 {
|
|
||||||
indexU64 := pos / 64
|
|
||||||
indexBit := pos % 64
|
|
||||||
var bitBuf uint64
|
|
||||||
if indexBit < 64-w {
|
|
||||||
// This window's bits are contained in a single u64
|
|
||||||
bitBuf = digits[indexU64] >> indexBit
|
|
||||||
} else {
|
|
||||||
// Combine the current 64 bits with bits from the next 64
|
|
||||||
bitBuf = (digits[indexU64] >> indexBit) | (digits[1+indexU64] << (64 - indexBit))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add carry into the current window
|
|
||||||
window := carry + (bitBuf & windowMask)
|
|
||||||
|
|
||||||
if window&1 == 0 {
|
|
||||||
// If the window value is even, preserve the carry and continue.
|
|
||||||
// Why is the carry preserved?
|
|
||||||
// If carry == 0 and window & 1 == 0,
|
|
||||||
// then the next carry should be 0
|
|
||||||
// If carry == 1 and window & 1 == 0,
|
|
||||||
// then bit_buf & 1 == 1 so the next carry should be 1
|
|
||||||
pos += 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if window < width/2 {
|
|
||||||
carry = 0
|
|
||||||
naf[pos] = int8(window)
|
|
||||||
} else {
|
|
||||||
carry = 1
|
|
||||||
naf[pos] = int8(window) - int8(width)
|
|
||||||
}
|
|
||||||
|
|
||||||
pos += w
|
|
||||||
}
|
|
||||||
return naf
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Scalar) signedRadix16() [64]int8 {
|
|
||||||
b := s.Bytes()
|
|
||||||
if b[31] > 127 {
|
|
||||||
panic("scalar has high bit set illegally")
|
|
||||||
}
|
|
||||||
|
|
||||||
var digits [64]int8
|
|
||||||
|
|
||||||
// Compute unsigned radix-16 digits:
|
|
||||||
for i := 0; i < 32; i++ {
|
|
||||||
digits[2*i] = int8(b[i] & 15)
|
|
||||||
digits[2*i+1] = int8((b[i] >> 4) & 15)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recenter coefficients:
|
|
||||||
for i := 0; i < 63; i++ {
|
|
||||||
carry := (digits[i] + 8) >> 4
|
|
||||||
digits[i] -= carry << 4
|
|
||||||
digits[i+1] += carry
|
|
||||||
}
|
|
||||||
|
|
||||||
return digits
|
|
||||||
}
|
|
||||||
1147
vendor/filippo.io/edwards25519/scalar_fiat.go
generated
vendored
1147
vendor/filippo.io/edwards25519/scalar_fiat.go
generated
vendored
File diff suppressed because it is too large
Load Diff
214
vendor/filippo.io/edwards25519/scalarmult.go
generated
vendored
214
vendor/filippo.io/edwards25519/scalarmult.go
generated
vendored
@ -1,214 +0,0 @@
|
|||||||
// Copyright (c) 2019 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package edwards25519
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
// basepointTable is a set of 32 affineLookupTables, where table i is generated
|
|
||||||
// from 256i * basepoint. It is precomputed the first time it's used.
|
|
||||||
func basepointTable() *[32]affineLookupTable {
|
|
||||||
basepointTablePrecomp.initOnce.Do(func() {
|
|
||||||
p := NewGeneratorPoint()
|
|
||||||
for i := 0; i < 32; i++ {
|
|
||||||
basepointTablePrecomp.table[i].FromP3(p)
|
|
||||||
for j := 0; j < 8; j++ {
|
|
||||||
p.Add(p, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return &basepointTablePrecomp.table
|
|
||||||
}
|
|
||||||
|
|
||||||
var basepointTablePrecomp struct {
|
|
||||||
table [32]affineLookupTable
|
|
||||||
initOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScalarBaseMult sets v = x * B, where B is the canonical generator, and
|
|
||||||
// returns v.
|
|
||||||
//
|
|
||||||
// The scalar multiplication is done in constant time.
|
|
||||||
func (v *Point) ScalarBaseMult(x *Scalar) *Point {
|
|
||||||
basepointTable := basepointTable()
|
|
||||||
|
|
||||||
// Write x = sum(x_i * 16^i) so x*B = sum( B*x_i*16^i )
|
|
||||||
// as described in the Ed25519 paper
|
|
||||||
//
|
|
||||||
// Group even and odd coefficients
|
|
||||||
// x*B = x_0*16^0*B + x_2*16^2*B + ... + x_62*16^62*B
|
|
||||||
// + x_1*16^1*B + x_3*16^3*B + ... + x_63*16^63*B
|
|
||||||
// x*B = x_0*16^0*B + x_2*16^2*B + ... + x_62*16^62*B
|
|
||||||
// + 16*( x_1*16^0*B + x_3*16^2*B + ... + x_63*16^62*B)
|
|
||||||
//
|
|
||||||
// We use a lookup table for each i to get x_i*16^(2*i)*B
|
|
||||||
// and do four doublings to multiply by 16.
|
|
||||||
digits := x.signedRadix16()
|
|
||||||
|
|
||||||
multiple := &affineCached{}
|
|
||||||
tmp1 := &projP1xP1{}
|
|
||||||
tmp2 := &projP2{}
|
|
||||||
|
|
||||||
// Accumulate the odd components first
|
|
||||||
v.Set(NewIdentityPoint())
|
|
||||||
for i := 1; i < 64; i += 2 {
|
|
||||||
basepointTable[i/2].SelectInto(multiple, digits[i])
|
|
||||||
tmp1.AddAffine(v, multiple)
|
|
||||||
v.fromP1xP1(tmp1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiply by 16
|
|
||||||
tmp2.FromP3(v) // tmp2 = v in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 2*v in P1xP1 coords
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = 2*v in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 4*v in P1xP1 coords
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = 4*v in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 8*v in P1xP1 coords
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = 8*v in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 16*v in P1xP1 coords
|
|
||||||
v.fromP1xP1(tmp1) // now v = 16*(odd components)
|
|
||||||
|
|
||||||
// Accumulate the even components
|
|
||||||
for i := 0; i < 64; i += 2 {
|
|
||||||
basepointTable[i/2].SelectInto(multiple, digits[i])
|
|
||||||
tmp1.AddAffine(v, multiple)
|
|
||||||
v.fromP1xP1(tmp1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScalarMult sets v = x * q, and returns v.
|
|
||||||
//
|
|
||||||
// The scalar multiplication is done in constant time.
|
|
||||||
func (v *Point) ScalarMult(x *Scalar, q *Point) *Point {
|
|
||||||
checkInitialized(q)
|
|
||||||
|
|
||||||
var table projLookupTable
|
|
||||||
table.FromP3(q)
|
|
||||||
|
|
||||||
// Write x = sum(x_i * 16^i)
|
|
||||||
// so x*Q = sum( Q*x_i*16^i )
|
|
||||||
// = Q*x_0 + 16*(Q*x_1 + 16*( ... + Q*x_63) ... )
|
|
||||||
// <------compute inside out---------
|
|
||||||
//
|
|
||||||
// We use the lookup table to get the x_i*Q values
|
|
||||||
// and do four doublings to compute 16*Q
|
|
||||||
digits := x.signedRadix16()
|
|
||||||
|
|
||||||
// Unwrap first loop iteration to save computing 16*identity
|
|
||||||
multiple := &projCached{}
|
|
||||||
tmp1 := &projP1xP1{}
|
|
||||||
tmp2 := &projP2{}
|
|
||||||
table.SelectInto(multiple, digits[63])
|
|
||||||
|
|
||||||
v.Set(NewIdentityPoint())
|
|
||||||
tmp1.Add(v, multiple) // tmp1 = x_63*Q in P1xP1 coords
|
|
||||||
for i := 62; i >= 0; i-- {
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = (prev) in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 2*(prev) in P1xP1 coords
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = 2*(prev) in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 4*(prev) in P1xP1 coords
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = 4*(prev) in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 8*(prev) in P1xP1 coords
|
|
||||||
tmp2.FromP1xP1(tmp1) // tmp2 = 8*(prev) in P2 coords
|
|
||||||
tmp1.Double(tmp2) // tmp1 = 16*(prev) in P1xP1 coords
|
|
||||||
v.fromP1xP1(tmp1) // v = 16*(prev) in P3 coords
|
|
||||||
table.SelectInto(multiple, digits[i])
|
|
||||||
tmp1.Add(v, multiple) // tmp1 = x_i*Q + 16*(prev) in P1xP1 coords
|
|
||||||
}
|
|
||||||
v.fromP1xP1(tmp1)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// basepointNafTable is the nafLookupTable8 for the basepoint.
|
|
||||||
// It is precomputed the first time it's used.
|
|
||||||
func basepointNafTable() *nafLookupTable8 {
|
|
||||||
basepointNafTablePrecomp.initOnce.Do(func() {
|
|
||||||
basepointNafTablePrecomp.table.FromP3(NewGeneratorPoint())
|
|
||||||
})
|
|
||||||
return &basepointNafTablePrecomp.table
|
|
||||||
}
|
|
||||||
|
|
||||||
var basepointNafTablePrecomp struct {
|
|
||||||
table nafLookupTable8
|
|
||||||
initOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
// VarTimeDoubleScalarBaseMult sets v = a * A + b * B, where B is the canonical
|
|
||||||
// generator, and returns v.
|
|
||||||
//
|
|
||||||
// Execution time depends on the inputs.
|
|
||||||
func (v *Point) VarTimeDoubleScalarBaseMult(a *Scalar, A *Point, b *Scalar) *Point {
|
|
||||||
checkInitialized(A)
|
|
||||||
|
|
||||||
// Similarly to the single variable-base approach, we compute
|
|
||||||
// digits and use them with a lookup table. However, because
|
|
||||||
// we are allowed to do variable-time operations, we don't
|
|
||||||
// need constant-time lookups or constant-time digit
|
|
||||||
// computations.
|
|
||||||
//
|
|
||||||
// So we use a non-adjacent form of some width w instead of
|
|
||||||
// radix 16. This is like a binary representation (one digit
|
|
||||||
// for each binary place) but we allow the digits to grow in
|
|
||||||
// magnitude up to 2^{w-1} so that the nonzero digits are as
|
|
||||||
// sparse as possible. Intuitively, this "condenses" the
|
|
||||||
// "mass" of the scalar onto sparse coefficients (meaning
|
|
||||||
// fewer additions).
|
|
||||||
|
|
||||||
basepointNafTable := basepointNafTable()
|
|
||||||
var aTable nafLookupTable5
|
|
||||||
aTable.FromP3(A)
|
|
||||||
// Because the basepoint is fixed, we can use a wider NAF
|
|
||||||
// corresponding to a bigger table.
|
|
||||||
aNaf := a.nonAdjacentForm(5)
|
|
||||||
bNaf := b.nonAdjacentForm(8)
|
|
||||||
|
|
||||||
// Find the first nonzero coefficient.
|
|
||||||
i := 255
|
|
||||||
for j := i; j >= 0; j-- {
|
|
||||||
if aNaf[j] != 0 || bNaf[j] != 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
multA := &projCached{}
|
|
||||||
multB := &affineCached{}
|
|
||||||
tmp1 := &projP1xP1{}
|
|
||||||
tmp2 := &projP2{}
|
|
||||||
tmp2.Zero()
|
|
||||||
|
|
||||||
// Move from high to low bits, doubling the accumulator
|
|
||||||
// at each iteration and checking whether there is a nonzero
|
|
||||||
// coefficient to look up a multiple of.
|
|
||||||
for ; i >= 0; i-- {
|
|
||||||
tmp1.Double(tmp2)
|
|
||||||
|
|
||||||
// Only update v if we have a nonzero coeff to add in.
|
|
||||||
if aNaf[i] > 0 {
|
|
||||||
v.fromP1xP1(tmp1)
|
|
||||||
aTable.SelectInto(multA, aNaf[i])
|
|
||||||
tmp1.Add(v, multA)
|
|
||||||
} else if aNaf[i] < 0 {
|
|
||||||
v.fromP1xP1(tmp1)
|
|
||||||
aTable.SelectInto(multA, -aNaf[i])
|
|
||||||
tmp1.Sub(v, multA)
|
|
||||||
}
|
|
||||||
|
|
||||||
if bNaf[i] > 0 {
|
|
||||||
v.fromP1xP1(tmp1)
|
|
||||||
basepointNafTable.SelectInto(multB, bNaf[i])
|
|
||||||
tmp1.AddAffine(v, multB)
|
|
||||||
} else if bNaf[i] < 0 {
|
|
||||||
v.fromP1xP1(tmp1)
|
|
||||||
basepointNafTable.SelectInto(multB, -bNaf[i])
|
|
||||||
tmp1.SubAffine(v, multB)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp2.FromP1xP1(tmp1)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.fromP2(tmp2)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
129
vendor/filippo.io/edwards25519/tables.go
generated
vendored
129
vendor/filippo.io/edwards25519/tables.go
generated
vendored
@ -1,129 +0,0 @@
|
|||||||
// Copyright (c) 2019 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package edwards25519
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/subtle"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A dynamic lookup table for variable-base, constant-time scalar muls.
|
|
||||||
type projLookupTable struct {
|
|
||||||
points [8]projCached
|
|
||||||
}
|
|
||||||
|
|
||||||
// A precomputed lookup table for fixed-base, constant-time scalar muls.
|
|
||||||
type affineLookupTable struct {
|
|
||||||
points [8]affineCached
|
|
||||||
}
|
|
||||||
|
|
||||||
// A dynamic lookup table for variable-base, variable-time scalar muls.
|
|
||||||
type nafLookupTable5 struct {
|
|
||||||
points [8]projCached
|
|
||||||
}
|
|
||||||
|
|
||||||
// A precomputed lookup table for fixed-base, variable-time scalar muls.
|
|
||||||
type nafLookupTable8 struct {
|
|
||||||
points [64]affineCached
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constructors.
|
|
||||||
|
|
||||||
// Builds a lookup table at runtime. Fast.
|
|
||||||
func (v *projLookupTable) FromP3(q *Point) {
|
|
||||||
// Goal: v.points[i] = (i+1)*Q, i.e., Q, 2Q, ..., 8Q
|
|
||||||
// This allows lookup of -8Q, ..., -Q, 0, Q, ..., 8Q
|
|
||||||
v.points[0].FromP3(q)
|
|
||||||
tmpP3 := Point{}
|
|
||||||
tmpP1xP1 := projP1xP1{}
|
|
||||||
for i := 0; i < 7; i++ {
|
|
||||||
// Compute (i+1)*Q as Q + i*Q and convert to a projCached
|
|
||||||
// This is needlessly complicated because the API has explicit
|
|
||||||
// receivers instead of creating stack objects and relying on RVO
|
|
||||||
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.Add(q, &v.points[i])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is not optimised for speed; fixed-base tables should be precomputed.
|
|
||||||
func (v *affineLookupTable) FromP3(q *Point) {
|
|
||||||
// Goal: v.points[i] = (i+1)*Q, i.e., Q, 2Q, ..., 8Q
|
|
||||||
// This allows lookup of -8Q, ..., -Q, 0, Q, ..., 8Q
|
|
||||||
v.points[0].FromP3(q)
|
|
||||||
tmpP3 := Point{}
|
|
||||||
tmpP1xP1 := projP1xP1{}
|
|
||||||
for i := 0; i < 7; i++ {
|
|
||||||
// Compute (i+1)*Q as Q + i*Q and convert to affineCached
|
|
||||||
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.AddAffine(q, &v.points[i])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builds a lookup table at runtime. Fast.
|
|
||||||
func (v *nafLookupTable5) FromP3(q *Point) {
|
|
||||||
// Goal: v.points[i] = (2*i+1)*Q, i.e., Q, 3Q, 5Q, ..., 15Q
|
|
||||||
// This allows lookup of -15Q, ..., -3Q, -Q, 0, Q, 3Q, ..., 15Q
|
|
||||||
v.points[0].FromP3(q)
|
|
||||||
q2 := Point{}
|
|
||||||
q2.Add(q, q)
|
|
||||||
tmpP3 := Point{}
|
|
||||||
tmpP1xP1 := projP1xP1{}
|
|
||||||
for i := 0; i < 7; i++ {
|
|
||||||
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.Add(&q2, &v.points[i])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is not optimised for speed; fixed-base tables should be precomputed.
|
|
||||||
func (v *nafLookupTable8) FromP3(q *Point) {
|
|
||||||
v.points[0].FromP3(q)
|
|
||||||
q2 := Point{}
|
|
||||||
q2.Add(q, q)
|
|
||||||
tmpP3 := Point{}
|
|
||||||
tmpP1xP1 := projP1xP1{}
|
|
||||||
for i := 0; i < 63; i++ {
|
|
||||||
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.AddAffine(&q2, &v.points[i])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selectors.
|
|
||||||
|
|
||||||
// Set dest to x*Q, where -8 <= x <= 8, in constant time.
|
|
||||||
func (v *projLookupTable) SelectInto(dest *projCached, x int8) {
|
|
||||||
// Compute xabs = |x|
|
|
||||||
xmask := x >> 7
|
|
||||||
xabs := uint8((x + xmask) ^ xmask)
|
|
||||||
|
|
||||||
dest.Zero()
|
|
||||||
for j := 1; j <= 8; j++ {
|
|
||||||
// Set dest = j*Q if |x| = j
|
|
||||||
cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
|
|
||||||
dest.Select(&v.points[j-1], dest, cond)
|
|
||||||
}
|
|
||||||
// Now dest = |x|*Q, conditionally negate to get x*Q
|
|
||||||
dest.CondNeg(int(xmask & 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set dest to x*Q, where -8 <= x <= 8, in constant time.
|
|
||||||
func (v *affineLookupTable) SelectInto(dest *affineCached, x int8) {
|
|
||||||
// Compute xabs = |x|
|
|
||||||
xmask := x >> 7
|
|
||||||
xabs := uint8((x + xmask) ^ xmask)
|
|
||||||
|
|
||||||
dest.Zero()
|
|
||||||
for j := 1; j <= 8; j++ {
|
|
||||||
// Set dest = j*Q if |x| = j
|
|
||||||
cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
|
|
||||||
dest.Select(&v.points[j-1], dest, cond)
|
|
||||||
}
|
|
||||||
// Now dest = |x|*Q, conditionally negate to get x*Q
|
|
||||||
dest.CondNeg(int(xmask & 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given odd x with 0 < x < 2^4, return x*Q (in variable time).
|
|
||||||
func (v *nafLookupTable5) SelectInto(dest *projCached, x int8) {
|
|
||||||
*dest = v.points[x/2]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given odd x with 0 < x < 2^7, return x*Q (in variable time).
|
|
||||||
func (v *nafLookupTable8) SelectInto(dest *affineCached, x int8) {
|
|
||||||
*dest = v.points[x/2]
|
|
||||||
}
|
|
||||||
21
vendor/github.com/dustin/go-humanize/.travis.yml
generated
vendored
21
vendor/github.com/dustin/go-humanize/.travis.yml
generated
vendored
@ -1,21 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
language: go
|
|
||||||
go_import_path: github.com/dustin/go-humanize
|
|
||||||
go:
|
|
||||||
- 1.13.x
|
|
||||||
- 1.14.x
|
|
||||||
- 1.15.x
|
|
||||||
- 1.16.x
|
|
||||||
- stable
|
|
||||||
- master
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- go: master
|
|
||||||
fast_finish: true
|
|
||||||
install:
|
|
||||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
|
||||||
script:
|
|
||||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
|
||||||
- go vet .
|
|
||||||
- go install -v -race ./...
|
|
||||||
- go test -v -race ./...
|
|
||||||
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
@ -1,21 +0,0 @@
|
|||||||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
<http://www.opensource.org/licenses/mit-license.php>
|
|
||||||
124
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
124
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
@ -1,124 +0,0 @@
|
|||||||
# Humane Units [](https://travis-ci.org/dustin/go-humanize) [](https://godoc.org/github.com/dustin/go-humanize)
|
|
||||||
|
|
||||||
Just a few functions for helping humanize times and sizes.
|
|
||||||
|
|
||||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
|
||||||
`"github.com/dustin/go-humanize"`, use it as `humanize`.
|
|
||||||
|
|
||||||
See [godoc](https://pkg.go.dev/github.com/dustin/go-humanize) for
|
|
||||||
complete documentation.
|
|
||||||
|
|
||||||
## Sizes
|
|
||||||
|
|
||||||
This lets you take numbers like `82854982` and convert them to useful
|
|
||||||
strings like, `83 MB` or `79 MiB` (whichever you prefer).
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Times
|
|
||||||
|
|
||||||
This lets you take a `time.Time` and spit it out in relative terms.
|
|
||||||
For example, `12 seconds ago` or `3 days from now`.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
|
|
||||||
```
|
|
||||||
|
|
||||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
|
||||||
conversation one day. It's pretty neat.
|
|
||||||
|
|
||||||
## Ordinals
|
|
||||||
|
|
||||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
|
||||||
to label ordinals.
|
|
||||||
|
|
||||||
0 -> 0th
|
|
||||||
1 -> 1st
|
|
||||||
2 -> 2nd
|
|
||||||
3 -> 3rd
|
|
||||||
4 -> 4th
|
|
||||||
[...]
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Commas
|
|
||||||
|
|
||||||
Want to shove commas into numbers? Be my guest.
|
|
||||||
|
|
||||||
0 -> 0
|
|
||||||
100 -> 100
|
|
||||||
1000 -> 1,000
|
|
||||||
1000000000 -> 1,000,000,000
|
|
||||||
-100000 -> -100,000
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ftoa
|
|
||||||
|
|
||||||
Nicer float64 formatter that removes trailing zeros.
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("%f", 2.24) // 2.240000
|
|
||||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
|
||||||
fmt.Printf("%f", 2.0) // 2.000000
|
|
||||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
|
||||||
```
|
|
||||||
|
|
||||||
## SI notation
|
|
||||||
|
|
||||||
Format numbers with [SI notation][sinotation].
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
humanize.SI(0.00000000223, "M") // 2.23 nM
|
|
||||||
```
|
|
||||||
|
|
||||||
## English-specific functions
|
|
||||||
|
|
||||||
The following functions are in the `humanize/english` subpackage.
|
|
||||||
|
|
||||||
### Plurals
|
|
||||||
|
|
||||||
Simple English pluralization
|
|
||||||
|
|
||||||
```go
|
|
||||||
english.PluralWord(1, "object", "") // object
|
|
||||||
english.PluralWord(42, "object", "") // objects
|
|
||||||
english.PluralWord(2, "bus", "") // buses
|
|
||||||
english.PluralWord(99, "locus", "loci") // loci
|
|
||||||
|
|
||||||
english.Plural(1, "object", "") // 1 object
|
|
||||||
english.Plural(42, "object", "") // 42 objects
|
|
||||||
english.Plural(2, "bus", "") // 2 buses
|
|
||||||
english.Plural(99, "locus", "loci") // 99 loci
|
|
||||||
```
|
|
||||||
|
|
||||||
### Word series
|
|
||||||
|
|
||||||
Format comma-separated words lists with conjuctions:
|
|
||||||
|
|
||||||
```go
|
|
||||||
english.WordSeries([]string{"foo"}, "and") // foo
|
|
||||||
english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar
|
|
||||||
english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz
|
|
||||||
|
|
||||||
english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz
|
|
||||||
```
|
|
||||||
|
|
||||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
|
||||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
|
||||||
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
@ -1,31 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
)
|
|
||||||
|
|
||||||
// order of magnitude (to a max order)
|
|
||||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
|
||||||
mag := 0
|
|
||||||
m := &big.Int{}
|
|
||||||
for n.Cmp(b) >= 0 {
|
|
||||||
n.DivMod(n, b, m)
|
|
||||||
mag++
|
|
||||||
if mag == maxmag && maxmag >= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
|
||||||
}
|
|
||||||
|
|
||||||
// total order of magnitude
|
|
||||||
// (same as above, but with no upper limit)
|
|
||||||
func oom(n, b *big.Int) (float64, int) {
|
|
||||||
mag := 0
|
|
||||||
m := &big.Int{}
|
|
||||||
for n.Cmp(b) >= 0 {
|
|
||||||
n.DivMod(n, b, m)
|
|
||||||
mag++
|
|
||||||
}
|
|
||||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
|
||||||
}
|
|
||||||
189
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
189
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
@ -1,189 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
bigIECExp = big.NewInt(1024)
|
|
||||||
|
|
||||||
// BigByte is one byte in bit.Ints
|
|
||||||
BigByte = big.NewInt(1)
|
|
||||||
// BigKiByte is 1,024 bytes in bit.Ints
|
|
||||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
|
||||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
|
||||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
|
||||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
|
||||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
|
||||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
|
||||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
|
||||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
|
||||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
|
||||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
|
||||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
|
||||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
|
||||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
|
||||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
|
||||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
|
||||||
// BigRiByte is 1,024 y bytes in bit.Ints
|
|
||||||
BigRiByte = (&big.Int{}).Mul(BigYiByte, bigIECExp)
|
|
||||||
// BigQiByte is 1,024 r bytes in bit.Ints
|
|
||||||
BigQiByte = (&big.Int{}).Mul(BigRiByte, bigIECExp)
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
bigSIExp = big.NewInt(1000)
|
|
||||||
|
|
||||||
// BigSIByte is one SI byte in big.Ints
|
|
||||||
BigSIByte = big.NewInt(1)
|
|
||||||
// BigKByte is 1,000 SI bytes in big.Ints
|
|
||||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
|
||||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
|
||||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
|
||||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
|
||||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
|
||||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
|
||||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
|
||||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
|
||||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
|
||||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
|
||||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
|
||||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
|
||||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
|
||||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
|
||||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
|
||||||
// BigRByte is 1,000 SI y bytes in big.Ints
|
|
||||||
BigRByte = (&big.Int{}).Mul(BigYByte, bigSIExp)
|
|
||||||
// BigQByte is 1,000 SI r bytes in big.Ints
|
|
||||||
BigQByte = (&big.Int{}).Mul(BigRByte, bigSIExp)
|
|
||||||
)
|
|
||||||
|
|
||||||
var bigBytesSizeTable = map[string]*big.Int{
|
|
||||||
"b": BigByte,
|
|
||||||
"kib": BigKiByte,
|
|
||||||
"kb": BigKByte,
|
|
||||||
"mib": BigMiByte,
|
|
||||||
"mb": BigMByte,
|
|
||||||
"gib": BigGiByte,
|
|
||||||
"gb": BigGByte,
|
|
||||||
"tib": BigTiByte,
|
|
||||||
"tb": BigTByte,
|
|
||||||
"pib": BigPiByte,
|
|
||||||
"pb": BigPByte,
|
|
||||||
"eib": BigEiByte,
|
|
||||||
"eb": BigEByte,
|
|
||||||
"zib": BigZiByte,
|
|
||||||
"zb": BigZByte,
|
|
||||||
"yib": BigYiByte,
|
|
||||||
"yb": BigYByte,
|
|
||||||
"rib": BigRiByte,
|
|
||||||
"rb": BigRByte,
|
|
||||||
"qib": BigQiByte,
|
|
||||||
"qb": BigQByte,
|
|
||||||
// Without suffix
|
|
||||||
"": BigByte,
|
|
||||||
"ki": BigKiByte,
|
|
||||||
"k": BigKByte,
|
|
||||||
"mi": BigMiByte,
|
|
||||||
"m": BigMByte,
|
|
||||||
"gi": BigGiByte,
|
|
||||||
"g": BigGByte,
|
|
||||||
"ti": BigTiByte,
|
|
||||||
"t": BigTByte,
|
|
||||||
"pi": BigPiByte,
|
|
||||||
"p": BigPByte,
|
|
||||||
"ei": BigEiByte,
|
|
||||||
"e": BigEByte,
|
|
||||||
"z": BigZByte,
|
|
||||||
"zi": BigZiByte,
|
|
||||||
"y": BigYByte,
|
|
||||||
"yi": BigYiByte,
|
|
||||||
"r": BigRByte,
|
|
||||||
"ri": BigRiByte,
|
|
||||||
"q": BigQByte,
|
|
||||||
"qi": BigQiByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
var ten = big.NewInt(10)
|
|
||||||
|
|
||||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
|
||||||
if s.Cmp(ten) < 0 {
|
|
||||||
return fmt.Sprintf("%d B", s)
|
|
||||||
}
|
|
||||||
c := (&big.Int{}).Set(s)
|
|
||||||
val, mag := oomm(c, base, len(sizes)-1)
|
|
||||||
suffix := sizes[mag]
|
|
||||||
f := "%.0f %s"
|
|
||||||
if val < 10 {
|
|
||||||
f = "%.1f %s"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(f, val, suffix)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// BigBytes produces a human readable representation of an SI size.
|
|
||||||
//
|
|
||||||
// See also: ParseBigBytes.
|
|
||||||
//
|
|
||||||
// BigBytes(82854982) -> 83 MB
|
|
||||||
func BigBytes(s *big.Int) string {
|
|
||||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "RB", "QB"}
|
|
||||||
return humanateBigBytes(s, bigSIExp, sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BigIBytes produces a human readable representation of an IEC size.
|
|
||||||
//
|
|
||||||
// See also: ParseBigBytes.
|
|
||||||
//
|
|
||||||
// BigIBytes(82854982) -> 79 MiB
|
|
||||||
func BigIBytes(s *big.Int) string {
|
|
||||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB"}
|
|
||||||
return humanateBigBytes(s, bigIECExp, sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseBigBytes parses a string representation of bytes into the number
|
|
||||||
// of bytes it represents.
|
|
||||||
//
|
|
||||||
// See also: BigBytes, BigIBytes.
|
|
||||||
//
|
|
||||||
// ParseBigBytes("42 MB") -> 42000000, nil
|
|
||||||
// ParseBigBytes("42 mib") -> 44040192, nil
|
|
||||||
func ParseBigBytes(s string) (*big.Int, error) {
|
|
||||||
lastDigit := 0
|
|
||||||
hasComma := false
|
|
||||||
for _, r := range s {
|
|
||||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if r == ',' {
|
|
||||||
hasComma = true
|
|
||||||
}
|
|
||||||
lastDigit++
|
|
||||||
}
|
|
||||||
|
|
||||||
num := s[:lastDigit]
|
|
||||||
if hasComma {
|
|
||||||
num = strings.Replace(num, ",", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val := &big.Rat{}
|
|
||||||
_, err := fmt.Sscanf(num, "%f", val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
|
||||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
|
||||||
mv := (&big.Rat{}).SetInt(m)
|
|
||||||
val.Mul(val, mv)
|
|
||||||
rv := &big.Int{}
|
|
||||||
rv.Div(val.Num(), val.Denom())
|
|
||||||
return rv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
|
||||||
}
|
|
||||||
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
@ -1,143 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IEC Sizes.
|
|
||||||
// kibis of bits
|
|
||||||
const (
|
|
||||||
Byte = 1 << (iota * 10)
|
|
||||||
KiByte
|
|
||||||
MiByte
|
|
||||||
GiByte
|
|
||||||
TiByte
|
|
||||||
PiByte
|
|
||||||
EiByte
|
|
||||||
)
|
|
||||||
|
|
||||||
// SI Sizes.
|
|
||||||
const (
|
|
||||||
IByte = 1
|
|
||||||
KByte = IByte * 1000
|
|
||||||
MByte = KByte * 1000
|
|
||||||
GByte = MByte * 1000
|
|
||||||
TByte = GByte * 1000
|
|
||||||
PByte = TByte * 1000
|
|
||||||
EByte = PByte * 1000
|
|
||||||
)
|
|
||||||
|
|
||||||
var bytesSizeTable = map[string]uint64{
|
|
||||||
"b": Byte,
|
|
||||||
"kib": KiByte,
|
|
||||||
"kb": KByte,
|
|
||||||
"mib": MiByte,
|
|
||||||
"mb": MByte,
|
|
||||||
"gib": GiByte,
|
|
||||||
"gb": GByte,
|
|
||||||
"tib": TiByte,
|
|
||||||
"tb": TByte,
|
|
||||||
"pib": PiByte,
|
|
||||||
"pb": PByte,
|
|
||||||
"eib": EiByte,
|
|
||||||
"eb": EByte,
|
|
||||||
// Without suffix
|
|
||||||
"": Byte,
|
|
||||||
"ki": KiByte,
|
|
||||||
"k": KByte,
|
|
||||||
"mi": MiByte,
|
|
||||||
"m": MByte,
|
|
||||||
"gi": GiByte,
|
|
||||||
"g": GByte,
|
|
||||||
"ti": TiByte,
|
|
||||||
"t": TByte,
|
|
||||||
"pi": PiByte,
|
|
||||||
"p": PByte,
|
|
||||||
"ei": EiByte,
|
|
||||||
"e": EByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
func logn(n, b float64) float64 {
|
|
||||||
return math.Log(n) / math.Log(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
|
||||||
if s < 10 {
|
|
||||||
return fmt.Sprintf("%d B", s)
|
|
||||||
}
|
|
||||||
e := math.Floor(logn(float64(s), base))
|
|
||||||
suffix := sizes[int(e)]
|
|
||||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
|
||||||
f := "%.0f %s"
|
|
||||||
if val < 10 {
|
|
||||||
f = "%.1f %s"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(f, val, suffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes produces a human readable representation of an SI size.
|
|
||||||
//
|
|
||||||
// See also: ParseBytes.
|
|
||||||
//
|
|
||||||
// Bytes(82854982) -> 83 MB
|
|
||||||
func Bytes(s uint64) string {
|
|
||||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
|
||||||
return humanateBytes(s, 1000, sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IBytes produces a human readable representation of an IEC size.
|
|
||||||
//
|
|
||||||
// See also: ParseBytes.
|
|
||||||
//
|
|
||||||
// IBytes(82854982) -> 79 MiB
|
|
||||||
func IBytes(s uint64) string {
|
|
||||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
|
||||||
return humanateBytes(s, 1024, sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseBytes parses a string representation of bytes into the number
|
|
||||||
// of bytes it represents.
|
|
||||||
//
|
|
||||||
// See Also: Bytes, IBytes.
|
|
||||||
//
|
|
||||||
// ParseBytes("42 MB") -> 42000000, nil
|
|
||||||
// ParseBytes("42 mib") -> 44040192, nil
|
|
||||||
func ParseBytes(s string) (uint64, error) {
|
|
||||||
lastDigit := 0
|
|
||||||
hasComma := false
|
|
||||||
for _, r := range s {
|
|
||||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if r == ',' {
|
|
||||||
hasComma = true
|
|
||||||
}
|
|
||||||
lastDigit++
|
|
||||||
}
|
|
||||||
|
|
||||||
num := s[:lastDigit]
|
|
||||||
if hasComma {
|
|
||||||
num = strings.Replace(num, ",", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := strconv.ParseFloat(num, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
|
||||||
if m, ok := bytesSizeTable[extra]; ok {
|
|
||||||
f *= float64(m)
|
|
||||||
if f >= math.MaxUint64 {
|
|
||||||
return 0, fmt.Errorf("too large: %v", s)
|
|
||||||
}
|
|
||||||
return uint64(f), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
|
||||||
}
|
|
||||||
116
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
116
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
@ -1,116 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Comma produces a string form of the given number in base 10 with
|
|
||||||
// commas after every three orders of magnitude.
|
|
||||||
//
|
|
||||||
// e.g. Comma(834142) -> 834,142
|
|
||||||
func Comma(v int64) string {
|
|
||||||
sign := ""
|
|
||||||
|
|
||||||
// Min int64 can't be negated to a usable value, so it has to be special cased.
|
|
||||||
if v == math.MinInt64 {
|
|
||||||
return "-9,223,372,036,854,775,808"
|
|
||||||
}
|
|
||||||
|
|
||||||
if v < 0 {
|
|
||||||
sign = "-"
|
|
||||||
v = 0 - v
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := []string{"", "", "", "", "", "", ""}
|
|
||||||
j := len(parts) - 1
|
|
||||||
|
|
||||||
for v > 999 {
|
|
||||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
|
||||||
switch len(parts[j]) {
|
|
||||||
case 2:
|
|
||||||
parts[j] = "0" + parts[j]
|
|
||||||
case 1:
|
|
||||||
parts[j] = "00" + parts[j]
|
|
||||||
}
|
|
||||||
v = v / 1000
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
parts[j] = strconv.Itoa(int(v))
|
|
||||||
return sign + strings.Join(parts[j:], ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commaf produces a string form of the given number in base 10 with
|
|
||||||
// commas after every three orders of magnitude.
|
|
||||||
//
|
|
||||||
// e.g. Commaf(834142.32) -> 834,142.32
|
|
||||||
func Commaf(v float64) string {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
if v < 0 {
|
|
||||||
buf.Write([]byte{'-'})
|
|
||||||
v = 0 - v
|
|
||||||
}
|
|
||||||
|
|
||||||
comma := []byte{','}
|
|
||||||
|
|
||||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
|
||||||
pos := 0
|
|
||||||
if len(parts[0])%3 != 0 {
|
|
||||||
pos += len(parts[0]) % 3
|
|
||||||
buf.WriteString(parts[0][:pos])
|
|
||||||
buf.Write(comma)
|
|
||||||
}
|
|
||||||
for ; pos < len(parts[0]); pos += 3 {
|
|
||||||
buf.WriteString(parts[0][pos : pos+3])
|
|
||||||
buf.Write(comma)
|
|
||||||
}
|
|
||||||
buf.Truncate(buf.Len() - 1)
|
|
||||||
|
|
||||||
if len(parts) > 1 {
|
|
||||||
buf.Write([]byte{'.'})
|
|
||||||
buf.WriteString(parts[1])
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommafWithDigits works like the Commaf but limits the resulting
|
|
||||||
// string to the given number of decimal places.
|
|
||||||
//
|
|
||||||
// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3
|
|
||||||
func CommafWithDigits(f float64, decimals int) string {
|
|
||||||
return stripTrailingDigits(Commaf(f), decimals)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BigComma produces a string form of the given big.Int in base 10
|
|
||||||
// with commas after every three orders of magnitude.
|
|
||||||
func BigComma(b *big.Int) string {
|
|
||||||
sign := ""
|
|
||||||
if b.Sign() < 0 {
|
|
||||||
sign = "-"
|
|
||||||
b.Abs(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
athousand := big.NewInt(1000)
|
|
||||||
c := (&big.Int{}).Set(b)
|
|
||||||
_, m := oom(c, athousand)
|
|
||||||
parts := make([]string, m+1)
|
|
||||||
j := len(parts) - 1
|
|
||||||
|
|
||||||
mod := &big.Int{}
|
|
||||||
for b.Cmp(athousand) >= 0 {
|
|
||||||
b.DivMod(b, athousand, mod)
|
|
||||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
|
||||||
switch len(parts[j]) {
|
|
||||||
case 2:
|
|
||||||
parts[j] = "0" + parts[j]
|
|
||||||
case 1:
|
|
||||||
parts[j] = "00" + parts[j]
|
|
||||||
}
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
|
||||||
return sign + strings.Join(parts[j:], ",")
|
|
||||||
}
|
|
||||||
41
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
41
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
@ -1,41 +0,0 @@
|
|||||||
//go:build go1.6
|
|
||||||
// +build go1.6
|
|
||||||
|
|
||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BigCommaf produces a string form of the given big.Float in base 10
|
|
||||||
// with commas after every three orders of magnitude.
|
|
||||||
func BigCommaf(v *big.Float) string {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
if v.Sign() < 0 {
|
|
||||||
buf.Write([]byte{'-'})
|
|
||||||
v.Abs(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
comma := []byte{','}
|
|
||||||
|
|
||||||
parts := strings.Split(v.Text('f', -1), ".")
|
|
||||||
pos := 0
|
|
||||||
if len(parts[0])%3 != 0 {
|
|
||||||
pos += len(parts[0]) % 3
|
|
||||||
buf.WriteString(parts[0][:pos])
|
|
||||||
buf.Write(comma)
|
|
||||||
}
|
|
||||||
for ; pos < len(parts[0]); pos += 3 {
|
|
||||||
buf.WriteString(parts[0][pos : pos+3])
|
|
||||||
buf.Write(comma)
|
|
||||||
}
|
|
||||||
buf.Truncate(buf.Len() - 1)
|
|
||||||
|
|
||||||
if len(parts) > 1 {
|
|
||||||
buf.Write([]byte{'.'})
|
|
||||||
buf.WriteString(parts[1])
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
49
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
49
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
@ -1,49 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func stripTrailingZeros(s string) string {
|
|
||||||
if !strings.ContainsRune(s, '.') {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
offset := len(s) - 1
|
|
||||||
for offset > 0 {
|
|
||||||
if s[offset] == '.' {
|
|
||||||
offset--
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if s[offset] != '0' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
offset--
|
|
||||||
}
|
|
||||||
return s[:offset+1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripTrailingDigits(s string, digits int) string {
|
|
||||||
if i := strings.Index(s, "."); i >= 0 {
|
|
||||||
if digits <= 0 {
|
|
||||||
return s[:i]
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
if i+digits >= len(s) {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s[:i+digits]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ftoa converts a float to a string with no trailing zeros.
|
|
||||||
func Ftoa(num float64) string {
|
|
||||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
|
||||||
}
|
|
||||||
|
|
||||||
// FtoaWithDigits converts a float to a string but limits the resulting string
|
|
||||||
// to the given number of decimal places, and no trailing zeros.
|
|
||||||
func FtoaWithDigits(num float64, digits int) string {
|
|
||||||
return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits))
|
|
||||||
}
|
|
||||||
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
|
||||||
|
|
||||||
Durations can be turned into strings such as "3 days ago", numbers
|
|
||||||
representing sizes like 82854982 into useful strings like, "83 MB" or
|
|
||||||
"79 MiB" (whichever you prefer).
|
|
||||||
*/
|
|
||||||
package humanize
|
|
||||||
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