diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52497aa6..8bda7153 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+5.2.1 (2024-02-21)
+==================
+
+Bugfixes
+--------
+
+- Fix Atom feeds being repeated in rooms once after an upgrade. ([\#901](https://github.com/matrix-org/matrix-hookshot/issues/901))
+
+
5.2.0 (2024-02-21)
==================
diff --git a/Cargo.lock b/Cargo.lock
index 4d2eea7f..4889bca6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -78,6 +78,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -111,6 +117,12 @@ version = "1.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
[[package]]
name = "bytes"
version = "1.5.0"
@@ -138,6 +150,12 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
[[package]]
name = "contrast"
version = "0.1.0"
@@ -228,6 +246,17 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "der"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
[[package]]
name = "derive_builder"
version = "0.12.0"
@@ -266,6 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
+ "const-oid",
"crypto-common",
]
@@ -417,9 +447,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "h2"
-version = "0.3.24"
+version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
+checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
@@ -608,6 +638,9 @@ name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin",
+]
[[package]]
name = "libc"
@@ -625,6 +658,12 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
@@ -669,9 +708,10 @@ dependencies = [
[[package]]
name = "matrix-hookshot"
-version = "5.2.0"
+version = "5.2.1"
dependencies = [
"atom_syndication",
+ "base64ct",
"contrast",
"hex",
"md-5",
@@ -681,6 +721,7 @@ dependencies = [
"rand",
"reqwest",
"rgb",
+ "rsa",
"rss",
"ruma",
"serde",
@@ -722,9 +763,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "0.8.10"
+version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"wasi",
@@ -821,6 +862,43 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.18"
@@ -828,6 +906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
+ "libm",
]
[[package]]
@@ -922,6 +1001,15 @@ dependencies = [
"windows-targets 0.48.5",
]
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -1020,6 +1108,27 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.30"
@@ -1193,6 +1302,26 @@ dependencies = [
"bytemuck",
]
+[[package]]
+name = "rsa"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "rss"
version = "2.0.7"
@@ -1450,6 +1579,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -1481,6 +1620,22 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
[[package]]
name = "string_cache"
version = "0.8.7"
@@ -1513,6 +1668,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
[[package]]
name = "syn"
version = "1.0.109"
@@ -2046,3 +2207,9 @@ dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
diff --git a/Cargo.toml b/Cargo.toml
index 7c70ab98..0711159f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "matrix-hookshot"
-version = "5.2.0"
+version = "5.2.1"
edition = "2021"
[lib]
@@ -22,6 +22,7 @@ atom_syndication = "0.12"
ruma = { version = "0.9", features = ["events", "html"] }
reqwest = "0.11"
rand = "0.8.5"
-
+rsa = "0.9.6"
+base64ct = { version = "1.6.0", features = ["alloc"] }
[build-dependencies]
napi-build = "2"
diff --git a/Dockerfile b/Dockerfile
index fa087a3b..101af29c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -45,6 +45,8 @@ COPY --from=builder /src/lib ./
COPY --from=builder /src/public ./public
COPY --from=builder /src/assets ./assets
+ENV NODE_ENV="production"
+
VOLUME /data
EXPOSE 9993
EXPOSE 7775
diff --git a/changelog.d/901.bugfix b/changelog.d/901.bugfix
deleted file mode 100644
index 6bc32020..00000000
--- a/changelog.d/901.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix Atom feeds being repeated in rooms once after an upgrade.
diff --git a/changelog.d/902.removal b/changelog.d/902.removal
new file mode 100644
index 00000000..05f7d124
--- /dev/null
+++ b/changelog.d/902.removal
@@ -0,0 +1,3 @@
+The cache/queue configuration has been changed in this release. The `queue.monolithic` option has been deprecated, in place of a dedicated `cache`
+config section. Check the ["Cache configuration" section](https://matrix-org.github.io/matrix-hookshot/latest/setup.html#cache-configuration) for
+more information on how to configure Hookshot caches.
\ No newline at end of file
diff --git a/changelog.d/904.misc b/changelog.d/904.misc
new file mode 100644
index 00000000..30e92ca5
--- /dev/null
+++ b/changelog.d/904.misc
@@ -0,0 +1 @@
+Switch expressjs to production mode for improved performance.
diff --git a/changelog.d/915.misc b/changelog.d/915.misc
new file mode 100644
index 00000000..7261adb4
--- /dev/null
+++ b/changelog.d/915.misc
@@ -0,0 +1 @@
+Track which key was used to encrypt secrets in storage, and encrypt/decrypt secrets in Rust.
diff --git a/config.sample.yml b/config.sample.yml
index 7182a9bc..00201678 100644
--- a/config.sample.yml
+++ b/config.sample.yml
@@ -10,7 +10,7 @@ bridge:
passFile:
# A passkey used to encrypt tokens stored inside the bridge.
# Run openssl genpkey -out passkey.pem -outform PEM -algorithm RSA -pkeyopt rsa_keygen_bits:4096 to generate
- passkey.pem
+ ./passkey.pem
logging:
# Logging settings. You can have a severity debug,info,warn,error
level: info
@@ -134,12 +134,15 @@ listeners:
# # (Optional) Prometheus metrics support
# enabled: true
+#cache:
+# # (Optional) Cache options for large scale deployments.
+# # For encryption to work, this must be configured.
+# redisUri: redis://localhost:6379
+
#queue:
-# # (Optional) Message queue / cache configuration options for large scale deployments.
-# # For encryption to work, must be set to monolithic mode and have a host & port specified.
-# monolithic: true
-# port: 6379
-# host: localhost
+# # (Optional) Message queue configuration options for large scale deployments.
+# # For encryption to work, this must not be configured.
+# redisUri: redis://localhost:6379
#widgets:
# # (Optional) EXPERIMENTAL support for complimentary widgets
diff --git a/docs/advanced/encryption.md b/docs/advanced/encryption.md
index 21c334cc..4133f6be 100644
--- a/docs/advanced/encryption.md
+++ b/docs/advanced/encryption.md
@@ -13,7 +13,7 @@ Hookshot supports end-to-bridge encryption via [MSC3202](https://github.com/matr
In order for Hookshot to use encryption, it must be configured as follows:
- The `experimentalEncryption.storagePath` setting must point to a directory that Hookshot has permissions to write files into. If running with Docker, this path should be within a volume (for persistency). Hookshot uses this directory for its crypto store (i.e. long-lived state relating to its encryption keys).
- Once a crypto store has been initialized, its files must not be modified, and Hookshot cannot be configured to use another crypto store of the same type as one it has used before. If a crypto store's files get lost or corrupted, Hookshot may fail to start up, or may be unable to decrypt command messages. To fix such issues, stop Hookshot, then reset its crypto store by running `yarn start:resetcrypto`.
-- [Redis](./workers.md) must be enabled. Note that worker mode is not yet supported with encryption, so `queue.monolithic` must be set to `true`.
+- [Redis](./workers.md) must be enabled. Note that worker mode is not yet supported with encryption, so `queue` MUST **NOT be configured**.
If you ever reset your homeserver's state, ensure you also reset Hookshot's encryption state. This includes clearing the `experimentalEncryption.storagePath` directory and all worker state stored in your redis instance. Otherwise, Hookshot may fail on start up with registration errors.
diff --git a/docs/advanced/workers.md b/docs/advanced/workers.md
index c4b59e07..1decc97c 100644
--- a/docs/advanced/workers.md
+++ b/docs/advanced/workers.md
@@ -11,18 +11,19 @@ This feature is experimental and should only be used when you are reachin
You must first have a working Redis instance somewhere which can talk between processes. For example, in Docker you can run:
-`docker run --name github-bridge-redis -p 6379:6379 -d redis`.
+`docker run --name redis-host -p 6379:6379 -d redis`.
The processes should all share the same config, which should contain the correct config to enable Redis:
```yaml
queue:
- monolithic: false
- port: 6379
- host: github-bridge-redis
+ redisUri: "redis://redis-host:6379"
+cache:
+ redisUri: "redis://redis-host:6379"
```
-Note that if [encryption](./encryption.md) is enabled, `queue.monolithic` must be set to `true`, as worker mode is not yet supported with encryption.
+Note that if [encryption](./encryption.md) is enabled, you MUST enable the `cache` config but NOT the `queue` config. Workers require persistent
+storage in Redis, but cannot make use of worker-mode queues.
Once that is done, you can simply start the processes by name using yarn:
```
diff --git a/docs/setup.md b/docs/setup.md
index 8b930192..004c2f71 100644
--- a/docs/setup.md
+++ b/docs/setup.md
@@ -27,7 +27,12 @@ cd matrix-hookshot
yarn # or npm i
```
-Starting the bridge (after configuring it), is a matter of running `yarn start`.
+Starting the bridge (after configuring it), is a matter of setting the `NODE_ENV` environment variable to `production` or `development`, depending if you want [better performance or more verbose logging](https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production), and then running it:
+
+
+```bash
+NODE_ENV=production yarn start
+```
## Installation via Docker
@@ -222,6 +227,20 @@ Please note that the appservice HTTP listener is configured separately
in the upstream library. See this issue for details.
+### Cache configuration
+
+You can optionally enable a Redis-backed cache for Hookshot. This is generally a good thing to enable if you can
+afford to, as it will generally improve startup times. Some features such as resuming RSS/Atom feeds between restarts
+is also only possible with a external cache.
+
+To enable, simply set:
+
+```yaml
+cache:
+ redisUri: "redis://redis-host:3679"
+```
+
+
### Services configuration
You will need to configure some services. Each service has its own documentation file inside the setup subdirectory.
diff --git a/helm/hookshot/values.yaml b/helm/hookshot/values.yaml
index a4b1302b..6a8e0c21 100644
--- a/helm/hookshot/values.yaml
+++ b/helm/hookshot/values.yaml
@@ -3,7 +3,6 @@
#
# -- Number of replicas to deploy. Consequences of using multiple Hookshot replicas currently unknown.
replicaCount: 1
-
image:
# -- Repository to pull hookshot image from
repository: halfshot/matrix-hookshot
@@ -11,16 +10,12 @@ image:
pullPolicy: IfNotPresent
# -- Image tag to pull. Defaults to chart's appVersion value as set in Chart.yaml
tag:
-
# -- List of names of k8s secrets to be used as ImagePullSecrets for the pod
imagePullSecrets: []
-
# -- Name override for helm chart
nameOverride: ""
-
# -- Full name override for helm chart
fullnameOverride: ""
-
serviceAccount:
# -- Specifies whether a service account should be created
create: true
@@ -28,10 +23,8 @@ serviceAccount:
annotations: {}
# -- The name of the service account to use. If not set and create is true, a name is generated using the fullname template
name: ""
-
# -- Extra annotations for Hookshot pod
podAnnotations: {}
-
# -- Pod security context settings
podSecurityContext: {}
# fsGroup: 2000
@@ -54,7 +47,6 @@ service:
annotations: {}
# -- Extra labels for service
labels: {}
-
webhook:
# -- Webhook port as configured in container
port: 9000
@@ -64,7 +56,6 @@ service:
appservice:
# -- Appservice port as configured in container
port: 9002
-
ingress:
webhook:
# -- Enable ingress for webhook
@@ -77,7 +68,6 @@ ingress:
hosts: []
# -- TLS configuration for webhook ingress
tls: []
-
appservice:
# -- Enable ingress for appservice
enabled: false
@@ -89,7 +79,6 @@ ingress:
hosts: []
# -- TLS configuration for appservice ingress
tls: []
-
# -- Pod resource requests / limits
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
@@ -105,189 +94,40 @@ resources: {}
autoscaling:
enabled: false
-
# -- Node selector parameters
nodeSelector: {}
# -- Tolerations for deployment
tolerations: []
-
# -- Affinity settings for deployment
affinity: {}
-
hookshot:
# -- Name of existing ConfigMap with valid Hookshot configuration
existingConfigMap:
-
# -- Raw Hookshot configuration. Gets templated into a YAML file and then loaded unless an existingConfigMap is specified.
config:
+ # This is an example configuration file
bridge:
# Basic homeserver configuration
- #
domain: example.com
url: http://localhost:8008
mediaUrl: https://example.com
port: 9993
bindAddress: 127.0.0.1
- # github:
- # (Optional) Configure this to enable GitHub support
- #
- # auth:
- # Authentication for the GitHub App.
- #
- # id: 123
- # privateKeyFile: github-key.pem
- # webhook:
- # Webhook settings for the GitHub app.
- #
- # secret: secrettoken
- # oauth:
- # (Optional) Settings for allowing users to sign in via OAuth.
- #
- # client_id: foo
- # client_secret: bar
- # redirect_uri: https://example.com/bridge_oauth/
- # defaultOptions:
- # (Optional) Default options for GitHub connections.
- #
- # showIssueRoomLink: false
- # hotlinkIssues:
- # prefix: "#"
- # userIdPrefix: _github_
- # (Optional) Prefix used when creating ghost users for GitHub accounts.
- #
- # gitlab:
- # (Optional) Configure this to enable GitLab support
- #
- # instances:
- # gitlab.com:
- # url: https://gitlab.com
- # webhook:
- # secret: secrettoken
- # publicUrl: https://example.com/hookshot/
- # userIdPrefix: _gitlab_
- # (Optional) Prefix used when creating ghost users for GitLab accounts.
- #
- # figma:
- # (Optional) Configure this to enable Figma support
- #
- # publicUrl: https://example.com/hookshot/
- # instances:
- # your-instance:
- # teamId: your-team-id
- # accessToken: your-personal-access-token
- # passcode: your-webhook-passcode
- # jira:
- # (Optional) Configure this to enable Jira support. Only specify `url` if you are using a On Premise install (i.e. not atlassian.com)
- #
- # webhook:
- # Webhook settings for JIRA
- #
- # secret: secrettoken
- # oauth:
- # (Optional) OAuth settings for connecting users to JIRA. See documentation for more information
- #
- # client_id: foo
- # client_secret: bar
- # redirect_uri: https://example.com/bridge_oauth/
- generic:
- # (Optional) Support for generic webhook events.
- #'allowJsTransformationFunctions' will allow users to write short transformation snippets in code, and thus is unsafe in untrusted environments
- #
- #
- enabled: false
- enableHttpGet: false
- urlPrefix: https://example.com/webhook/
- userIdPrefix: _webhooks_
- allowJsTransformationFunctions: false
- waitForComplete: false
- feeds:
- # (Optional) Configure this to enable RSS/Atom feed support
- #
- enabled: false
- pollIntervalSeconds: 600
- pollTimeoutSeconds: 30
- # provisioning:
- # (Optional) Provisioning API for integration managers
- #
- # secret: "!secretToken"
passFile: passkey.pem
# A passkey used to encrypt tokens stored inside the bridge.
# Run openssl genpkey -out passkey.pem -outform PEM -algorithm RSA -pkeyopt rsa_keygen_bits:4096 to generate
- #
- # bot:
- # (Optional) Define profile information for the bot user
- #
- # displayname: Hookshot Bot
- # avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
- # serviceBots:
- # (Optional) Define additional bot users for specific services
- #
- # - localpart: feeds
- # displayname: Feeds
- # avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
- # prefix: "!feeds"
- # service: feeds
- metrics:
- # (Optional) Prometheus metrics support
- #
- enabled: true
- # queue:
- # (Optional) Message queue / cache configuration options for large scale deployments.
- # For encryption to work, must be set to monolithic mode and have a host & port specified.
- #
- # monolithic: true
- # port: 6379
- # host: localhost
logging:
- # (Optional) Logging settings. You can have a severity debug,info,warn,error
- #
+ # Logging settings. You can have a severity debug,info,warn,error
level: info
colorize: true
json: false
timestampFormat: HH:mm:ss:SSS
- # widgets:
- # (Optional) EXPERIMENTAL support for complimentary widgets
- #
- # addToAdminRooms: false
- # disallowedIpRanges:
- # - 127.0.0.0/8
- # - 10.0.0.0/8
- # - 172.16.0.0/12
- # - 192.168.0.0/16
- # - 100.64.0.0/10
- # - 192.0.0.0/24
- # - 169.254.0.0/16
- # - 192.88.99.0/24
- # - 198.18.0.0/15
- # - 192.0.2.0/24
- # - 198.51.100.0/24
- # - 203.0.113.0/24
- # - 224.0.0.0/4
- # - ::1/128
- # - fe80::/10
- # - fc00::/7
- # - 2001:db8::/32
- # - ff00::/8
- # - fec0::/10
- # roomSetupWidget:
- # addOnInvite: false
- # publicUrl: https://example.com/widgetapi/v1/static/
- # branding:
- # widgetTitle: Hookshot Configuration
- # permissions:
- # (Optional) Permissions for using the bridge. See docs/setup.md#permissions for help
- #
- # - actor: example.com
- # services:
- # - service: "*"
- # level: admin
listeners:
- # (Optional) HTTP Listener configuration.
+ # HTTP Listener configuration.
# Bind resource endpoints to ports and addresses.
# 'port' must be specified. Each listener must listen on a unique port.
# 'bindAddress' will default to '127.0.0.1' if not specified, which may not be suited to Docker environments.
# 'resources' may be any of webhooks, widgets, metrics, provisioning
- #
- port: 9000
bindAddress: 0.0.0.0
resources:
@@ -302,6 +142,153 @@ hookshot:
resources:
- widgets
registration:
+ #github:
+ # # (Optional) Configure this to enable GitHub support
+ # auth:
+ # # Authentication for the GitHub App.
+ # id: 123
+ # privateKeyFile: github-key.pem
+ # webhook:
+ # # Webhook settings for the GitHub app.
+ # secret: secrettoken
+ # oauth:
+ # # (Optional) Settings for allowing users to sign in via OAuth.
+ # client_id: foo
+ # client_secret: bar
+ # redirect_uri: https://example.com/oauth/
+ # defaultOptions:
+ # # (Optional) Default options for GitHub connections.
+ # showIssueRoomLink: false
+ # hotlinkIssues:
+ # prefix: "#"
+ # userIdPrefix:
+ # # (Optional) Prefix used when creating ghost users for GitHub accounts.
+ # _github_
+
+ #gitlab:
+ # # (Optional) Configure this to enable GitLab support
+ # instances:
+ # gitlab.com:
+ # url: https://gitlab.com
+ # webhook:
+ # secret: secrettoken
+ # publicUrl: https://example.com/hookshot/
+ # userIdPrefix:
+ # # (Optional) Prefix used when creating ghost users for GitLab accounts.
+ # _gitlab_
+ # commentDebounceMs:
+ # # (Optional) Aggregate comments by waiting this many miliseconds before posting them to Matrix. Defaults to 5000 (5 seconds)
+ # 5000
+
+ #figma:
+ # # (Optional) Configure this to enable Figma support
+ # publicUrl: https://example.com/hookshot/
+ # instances:
+ # your-instance:
+ # teamId: your-team-id
+ # accessToken: your-personal-access-token
+ # passcode: your-webhook-passcode
+
+ #jira:
+ # # (Optional) Configure this to enable Jira support. Only specify `url` if you are using a On Premise install (i.e. not atlassian.com)
+ # webhook:
+ # # Webhook settings for JIRA
+ # secret: secrettoken
+ # oauth:
+ # # (Optional) OAuth settings for connecting users to JIRA. See documentation for more information
+ # client_id: foo
+ # client_secret: bar
+ # redirect_uri: https://example.com/oauth/
+
+ #generic:
+ # # (Optional) Support for generic webhook events.
+ # #'allowJsTransformationFunctions' will allow users to write short transformation snippets in code, and thus is unsafe in untrusted environments
+
+ # enabled: false
+ # enableHttpGet: false
+ # urlPrefix: https://example.com/webhook/
+ # userIdPrefix: _webhooks_
+ # allowJsTransformationFunctions: false
+ # waitForComplete: false
+
+ #feeds:
+ # # (Optional) Configure this to enable RSS/Atom feed support
+ # enabled: false
+ # pollConcurrency: 4
+ # pollIntervalSeconds: 600
+ # pollTimeoutSeconds: 30
+
+ #provisioning:
+ # # (Optional) Provisioning API for integration managers
+ # secret: "!secretToken"
+
+ #bot:
+ # # (Optional) Define profile information for the bot user
+ # displayname: Hookshot Bot
+ # avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
+
+ #serviceBots:
+ # # (Optional) Define additional bot users for specific services
+ # - localpart: feeds
+ # displayname: Feeds
+ # avatar: ./assets/feeds_avatar.png
+ # prefix: "!feeds"
+ # service: feeds
+
+ #metrics:
+ # # (Optional) Prometheus metrics support
+ # enabled: true
+
+ #cache:
+ # # (Optional) Cache options for large scale deployments.
+ # # For encryption to work, this must be configured.
+ # redisUri: redis://localhost:6379
+
+ #queue:
+ # # (Optional) Message queue configuration options for large scale deployments.
+ # # For encryption to work, this must not be configured.
+ # redisUri: redis://localhost:6379
+
+ #widgets:
+ # # (Optional) EXPERIMENTAL support for complimentary widgets
+ # addToAdminRooms: false
+ # disallowedIpRanges:
+ # - 127.0.0.0/8
+ # - 10.0.0.0/8
+ # - 172.16.0.0/12
+ # - 192.168.0.0/16
+ # - 100.64.0.0/10
+ # - 192.0.0.0/24
+ # - 169.254.0.0/16
+ # - 192.88.99.0/24
+ # - 198.18.0.0/15
+ # - 192.0.2.0/24
+ # - 198.51.100.0/24
+ # - 203.0.113.0/24
+ # - 224.0.0.0/4
+ # - ::1/128
+ # - fe80::/10
+ # - fc00::/7
+ # - 2001:db8::/32
+ # - ff00::/8
+ # - fec0::/10
+ # roomSetupWidget:
+ # addOnInvite: false
+ # publicUrl: https://example.com/widgetapi/v1/static/
+ # branding:
+ # widgetTitle: Hookshot Configuration
+
+ #sentry:
+ # # (Optional) Configure Sentry error reporting
+ # dsn: https://examplePublicKey@o0.ingest.sentry.io/0
+ # environment: production
+
+ #permissions:
+ # # (Optional) Permissions for using the bridge. See docs/setup.md#permissions for help
+ # - actor: example.com
+ # services:
+ # - service: "*"
+ # level: admin
id: matrix-hookshot
as_token: ""
hs_token: ""
diff --git a/package.json b/package.json
index d0c6010d..23a1ff14 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-hookshot",
- "version": "5.2.0",
+ "version": "5.2.1",
"description": "A bridge between Matrix and multiple project management services, such as GitHub, GitLab and JIRA.",
"main": "lib/app.js",
"repository": "https://github.com/matrix-org/matrix-hookshot",
@@ -115,6 +115,6 @@
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.3",
- "vite": "^5.0.12"
+ "vite": "^5.0.13"
}
}
diff --git a/scripts/build-app.sh b/scripts/build-app.sh
index 6a8fdce4..9b25f720 100755
--- a/scripts/build-app.sh
+++ b/scripts/build-app.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# exit when any command fails
set -e
diff --git a/spec/basic.spec.ts b/spec/basic.spec.ts
index ec36ddf0..e375a18a 100644
--- a/spec/basic.spec.ts
+++ b/spec/basic.spec.ts
@@ -19,12 +19,12 @@ describe('Basic test setup', () => {
const user = testEnv.getUser('user');
const roomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid] });
await user.waitForRoomJoin({sender: testEnv.botMxid, roomId });
- await user.sendText(roomId, "!hookshot help");
- const msg = await user.waitForRoomEvent({
+ const msg = user.waitForRoomEvent({
eventType: 'm.room.message', sender: testEnv.botMxid, roomId
});
+ await user.sendText(roomId, "!hookshot help");
// Expect help text.
- expect(msg.data.content.body).to.include('!hookshot help` - This help text\n');
+ expect((await msg).data.content.body).to.include('!hookshot help` - This help text\n');
});
// TODO: Move test to it's own generic connections file.
diff --git a/spec/util/e2e-test.ts b/spec/util/e2e-test.ts
index 20cf38c2..28631077 100644
--- a/spec/util/e2e-test.ts
+++ b/spec/util/e2e-test.ts
@@ -203,9 +203,6 @@ export class E2ETestEnv {
logging: {
level: 'info',
},
- queue: {
- monolithic: true,
- },
// Always enable webhooks so that hookshot starts.
generic: {
enabled: true,
diff --git a/src/AdminRoom.ts b/src/AdminRoom.ts
index efa2c130..69155163 100644
--- a/src/AdminRoom.ts
+++ b/src/AdminRoom.ts
@@ -16,7 +16,7 @@ import { Intent } from "matrix-bot-sdk";
import { JiraBotCommands } from "./jira/AdminCommands";
import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters";
import { ProjectsListResponseData } from "./github/Types";
-import { UserTokenStore } from "./UserTokenStore";
+import { UserTokenStore } from "./tokens/UserTokenStore";
import { Logger } from "matrix-appservice-bridge";
import markdown from "markdown-it";
type ProjectsListForRepoResponseData = Endpoints["GET /repos/{owner}/{repo}/projects"]["response"];
diff --git a/src/AdminRoomCommandHandler.ts b/src/AdminRoomCommandHandler.ts
index 10102e7e..6840729e 100644
--- a/src/AdminRoomCommandHandler.ts
+++ b/src/AdminRoomCommandHandler.ts
@@ -1,7 +1,7 @@
import EventEmitter from "events";
import { Intent } from "matrix-bot-sdk";
import { BridgeConfig } from "./config/Config";
-import { UserTokenStore } from "./UserTokenStore";
+import { UserTokenStore } from "./tokens/UserTokenStore";
export enum Category {
diff --git a/src/App/BridgeApp.ts b/src/App/BridgeApp.ts
index b07b9323..24ec7a8d 100644
--- a/src/App/BridgeApp.ts
+++ b/src/App/BridgeApp.ts
@@ -10,6 +10,7 @@ import BotUsersManager from "../Managers/BotUsersManager";
import * as Sentry from '@sentry/node';
import { GenericHookConnection } from "../Connections";
import { installRequestFunction } from "../Request";
+import { UserTokenStore } from "../tokens/UserTokenStore";
Logger.configure({console: "info"});
const log = new Logger("App");
@@ -29,7 +30,7 @@ export async function start(config: BridgeConfig, registration: IAppserviceRegis
const {appservice, storage} = getAppservice(config, registration);
- if (config.queue.monolithic) {
+ if (!config.queue) {
const matrixSender = new MatrixSender(config, appservice);
matrixSender.listen();
const userNotificationWatcher = new UserNotificationWatcher(config);
@@ -53,7 +54,8 @@ export async function start(config: BridgeConfig, registration: IAppserviceRegis
const botUsersManager = new BotUsersManager(config, appservice);
- const bridgeApp = new Bridge(config, listener, appservice, storage, botUsersManager);
+ const tokenStore = await UserTokenStore.fromKeyPath(config.passFile , appservice.botIntent, config);
+ const bridgeApp = new Bridge(config, tokenStore, listener, appservice, storage, botUsersManager);
process.once("SIGTERM", () => {
log.error("Got SIGTERM");
diff --git a/src/Bridge.ts b/src/Bridge.ts
index c9251745..0e6daed4 100644
--- a/src/Bridge.ts
+++ b/src/Bridge.ts
@@ -23,7 +23,7 @@ import { NotificationsEnableEvent, NotificationsDisableEvent, Webhooks } from ".
import { GitHubOAuthToken, GitHubOAuthTokenResponse, ProjectsGetResponseData } from "./github/Types";
import { retry } from "./PromiseUtil";
import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher";
-import { UserTokenStore } from "./UserTokenStore";
+import { UserTokenStore } from "./tokens/UserTokenStore";
import * as GitHubWebhookTypes from "@octokit/webhooks-types";
import { Logger } from "matrix-appservice-bridge";
import { Provisioner } from "./provisioning/provisioner";
@@ -49,11 +49,9 @@ export class Bridge {
private readonly queue: MessageQueue;
private readonly commentProcessor: CommentProcessor;
private readonly notifProcessor: NotificationProcessor;
- private readonly tokenStore: UserTokenStore;
private connectionManager?: ConnectionManager;
private github?: GithubInstance;
private adminRooms: Map = new Map();
- private widgetApi?: BridgeWidgetApi;
private feedReader?: FeedReader;
private provisioningApi?: Provisioner;
private replyProcessor = new RichRepliesPreprocessor(true);
@@ -62,6 +60,7 @@ export class Bridge {
constructor(
private config: BridgeConfig,
+ private readonly tokenStore: UserTokenStore,
private readonly listener: ListenerService,
private readonly as: Appservice,
private readonly storage: IBridgeStorageProvider,
@@ -71,8 +70,6 @@ export class Bridge {
this.messageClient = new MessageSenderClient(this.queue);
this.commentProcessor = new CommentProcessor(this.as, this.config.bridge.mediaUrl || this.config.bridge.url);
this.notifProcessor = new NotificationProcessor(this.storage, this.messageClient);
- this.tokenStore = new UserTokenStore(this.config.passFile || "./passkey.pem", this.as.botIntent, this.config);
- this.tokenStore.on("onNewToken", this.onTokenUpdated.bind(this));
// Legacy routes, to be removed.
this.as.expressAppInstance.get("/live", (_, res) => res.send({ok: true}));
@@ -87,8 +84,8 @@ export class Bridge {
}
public async start() {
+ this.tokenStore.on("onNewToken", this.onTokenUpdated.bind(this));
log.info('Starting up');
- await this.tokenStore.load();
await this.storage.connect?.();
await this.queue.connect?.();
@@ -755,7 +752,7 @@ export class Bridge {
if (apps.length > 1) {
throw Error('You may only bind `widgets` to one listener.');
}
- this.widgetApi = new BridgeWidgetApi(
+ new BridgeWidgetApi(
this.adminRooms,
this.config,
this.storage,
diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts
index c475aa77..891f6c08 100644
--- a/src/ConnectionManager.ts
+++ b/src/ConnectionManager.ts
@@ -17,7 +17,7 @@ import { IBridgeStorageProvider } from "./Stores/StorageProvider";
import { JiraProject, JiraVersion } from "./jira/Types";
import { Logger } from "matrix-appservice-bridge";
import { MessageSenderClient } from "./MatrixSender";
-import { UserTokenStore } from "./UserTokenStore";
+import { UserTokenStore } from "./tokens/UserTokenStore";
import BotUsersManager from "./Managers/BotUsersManager";
import { retry, retryMatrixErrorFilter } from "./PromiseUtil";
import Metrics from "./Metrics";
diff --git a/src/Connections/GithubDiscussion.ts b/src/Connections/GithubDiscussion.ts
index 5529118c..d3787d1d 100644
--- a/src/Connections/GithubDiscussion.ts
+++ b/src/Connections/GithubDiscussion.ts
@@ -1,6 +1,6 @@
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import { CommentProcessor } from "../CommentProcessor";
import { MessageSenderClient } from "../MatrixSender";
import { ensureUserIsInRoom, getIntentForUser } from "../IntentUtils";
diff --git a/src/Connections/GithubIssue.ts b/src/Connections/GithubIssue.ts
index 449349d4..4c0a728d 100644
--- a/src/Connections/GithubIssue.ts
+++ b/src/Connections/GithubIssue.ts
@@ -2,7 +2,7 @@ import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnectio
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
import { MatrixMessageContent, MatrixEvent } from "../MatrixEvent";
import markdown from "markdown-it";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import { Logger } from "matrix-appservice-bridge";
import { CommentProcessor } from "../CommentProcessor";
import { MessageSenderClient } from "../MatrixSender";
diff --git a/src/Connections/GithubRepo.ts b/src/Connections/GithubRepo.ts
index 6e77191d..dc4e452d 100644
--- a/src/Connections/GithubRepo.ts
+++ b/src/Connections/GithubRepo.ts
@@ -13,7 +13,7 @@ import { MatrixMessageContent, MatrixEvent, MatrixReactionContent } from "../Mat
import { MessageSenderClient } from "../MatrixSender";
import { CommandError, NotLoggedInError } from "../errors";
import { ReposGetResponseData } from "../github/Types";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import axios, { AxiosError } from "axios";
import { emojify } from "node-emoji";
import { Logger } from "matrix-appservice-bridge";
diff --git a/src/Connections/GitlabIssue.ts b/src/Connections/GitlabIssue.ts
index 6a19cdd6..7d2df87d 100644
--- a/src/Connections/GitlabIssue.ts
+++ b/src/Connections/GitlabIssue.ts
@@ -1,7 +1,7 @@
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
import { MatrixMessageContent, MatrixEvent } from "../MatrixEvent";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import { Logger } from "matrix-appservice-bridge";
import { CommentProcessor } from "../CommentProcessor";
import { MessageSenderClient } from "../MatrixSender";
diff --git a/src/Connections/GitlabRepo.ts b/src/Connections/GitlabRepo.ts
index c3936a4c..29e7f3e1 100644
--- a/src/Connections/GitlabRepo.ts
+++ b/src/Connections/GitlabRepo.ts
@@ -1,4 +1,4 @@
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
import { BotCommands, botCommand, compileBotCommands } from "../BotCommands";
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
diff --git a/src/Connections/IConnection.ts b/src/Connections/IConnection.ts
index 9fe63a9a..c01c8878 100644
--- a/src/Connections/IConnection.ts
+++ b/src/Connections/IConnection.ts
@@ -3,7 +3,7 @@ import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types";
import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api";
import { Appservice, Intent, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk";
import { BridgeConfig, BridgePermissionLevel } from "../config/Config";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import { CommentProcessor } from "../CommentProcessor";
import { MessageSenderClient } from "../MatrixSender";
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
diff --git a/src/Connections/JiraProject.ts b/src/Connections/JiraProject.ts
index a1aa2acd..05aabec1 100644
--- a/src/Connections/JiraProject.ts
+++ b/src/Connections/JiraProject.ts
@@ -9,7 +9,7 @@ import { JiraProject, JiraVersion } from "../jira/Types";
import { botCommand, BotCommands, compileBotCommands } from "../BotCommands";
import { MatrixMessageContent } from "../MatrixEvent";
import { CommandConnection } from "./CommandConnection";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import { CommandError, NotLoggedInError } from "../errors";
import { ApiError, ErrCode } from "../api";
import JiraApi from "jira-client";
diff --git a/src/Gitlab/GrantChecker.ts b/src/Gitlab/GrantChecker.ts
index 66bd7bea..de759fc6 100644
--- a/src/Gitlab/GrantChecker.ts
+++ b/src/Gitlab/GrantChecker.ts
@@ -3,7 +3,7 @@ import { Appservice } from "matrix-bot-sdk";
import { BridgeConfigGitLab } from "../config/Config";
import { GitLabRepoConnection } from "../Connections";
import { GrantChecker } from "../grants/GrantCheck";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
const log = new Logger('GitLabGrantChecker');
diff --git a/src/ListenerService.ts b/src/ListenerService.ts
index 32ef379f..62f2168e 100644
--- a/src/ListenerService.ts
+++ b/src/ListenerService.ts
@@ -30,6 +30,7 @@ export class ListenerService {
}
for (const listenerConfig of config) {
const app = expressApp();
+ app.set('x-powered-by', false);
app.use(Handlers.requestHandler());
this.listeners.push({
config: listenerConfig,
diff --git a/src/MessageQueue/MessageQueue.ts b/src/MessageQueue/MessageQueue.ts
index 476e6d86..03dc1d17 100644
--- a/src/MessageQueue/MessageQueue.ts
+++ b/src/MessageQueue/MessageQueue.ts
@@ -1,4 +1,4 @@
-import { BridgeConfigQueue } from "../config/Config";
+import { BridgeConfigQueue } from "../config/sections";
import { LocalMQ } from "./LocalMQ";
import { RedisMQ } from "./RedisQueue";
import { MessageQueue } from "./Types";
@@ -6,8 +6,8 @@ import { MessageQueue } from "./Types";
const staticLocalMq = new LocalMQ();
let staticRedisMq: RedisMQ|null = null;
-export function createMessageQueue(config: BridgeConfigQueue): MessageQueue {
- if (config.monolithic) {
+export function createMessageQueue(config?: BridgeConfigQueue): MessageQueue {
+ if (!config) {
return staticLocalMq;
}
if (staticRedisMq === null) {
diff --git a/src/MessageQueue/RedisQueue.ts b/src/MessageQueue/RedisQueue.ts
index 2fbda2f3..0800da89 100644
--- a/src/MessageQueue/RedisQueue.ts
+++ b/src/MessageQueue/RedisQueue.ts
@@ -1,7 +1,7 @@
import { MessageQueue, MessageQueueMessage, DEFAULT_RES_TIMEOUT, MessageQueueMessageOut } from "./Types";
import { Redis, default as redis } from "ioredis";
-import { BridgeConfigQueue } from "../config/Config";
+import { BridgeConfigQueue } from "../config/sections/queue";
import { EventEmitter } from "events";
import { Logger } from "matrix-appservice-bridge";
import { randomUUID } from 'node:crypto';
@@ -22,9 +22,10 @@ export class RedisMQ extends EventEmitter implements MessageQueue {
private myUuid: string;
constructor(config: BridgeConfigQueue) {
super();
- this.redisSub = new redis(config.port ?? 6379, config.host ?? "localhost");
- this.redisPub = new redis(config.port ?? 6379, config.host ?? "localhost");
- this.redis = new redis(config.port ?? 6379, config.host ?? "localhost");
+ const uri = 'redisUri' in config ? config.redisUri : `redis://${config.host ?? 'localhost'}:${config.port ?? 6379}`;
+ this.redisSub = new redis(uri);
+ this.redisPub = new redis(uri);
+ this.redis = new redis(uri);
this.myUuid = randomUUID();
this.redisSub.on("pmessage", (_: string, channel: string, message: string) => {
const msg = JSON.parse(message) as MessageQueueMessageOut;
diff --git a/src/Stores/RedisStorageProvider.ts b/src/Stores/RedisStorageProvider.ts
index 12c42a79..49fac70f 100644
--- a/src/Stores/RedisStorageProvider.ts
+++ b/src/Stores/RedisStorageProvider.ts
@@ -6,6 +6,7 @@ import { IBridgeStorageProvider, MAX_FEED_ITEMS } from "./StorageProvider";
import { IFilterInfo, IStorageProvider } from "matrix-bot-sdk";
import { ProvisionSession } from "matrix-appservice-bridge";
import { SerializedGitlabDiscussionThreads } from "../Gitlab/Types";
+import { BridgeConfigCache } from "../config/sections";
const BOT_SYNC_TOKEN_KEY = "bot.sync_token.";
const BOT_FILTER_KEY = "bot.filter.";
@@ -68,8 +69,8 @@ export class RedisStorageContextualProvider implements IStorageProvider {
export class RedisStorageProvider extends RedisStorageContextualProvider implements IBridgeStorageProvider {
- constructor(host: string, port: number, contextSuffix = '') {
- super(new redis(port, host), contextSuffix);
+ constructor(cacheConfig: BridgeConfigCache, contextSuffix = '') {
+ super(new redis(cacheConfig.redisUri), contextSuffix);
this.redis.expire(COMPLETED_TRANSACTIONS_KEY, COMPLETED_TRANSACTIONS_EXPIRE_AFTER).catch((ex) => {
log.warn("Failed to set expiry time on as.completed_transactions", ex);
});
diff --git a/src/Widgets/BridgeWidgetApi.ts b/src/Widgets/BridgeWidgetApi.ts
index 38555789..0962d5d0 100644
--- a/src/Widgets/BridgeWidgetApi.ts
+++ b/src/Widgets/BridgeWidgetApi.ts
@@ -11,7 +11,7 @@ import BotUsersManager, {BotUser} from "../Managers/BotUsersManager";
import { assertUserPermissionsInRoom, GetConnectionsResponseItem } from "../provisioning/api";
import { Appservice, PowerLevelsEvent } from "matrix-bot-sdk";
import { GithubInstance } from '../github/GithubInstance';
-import { AllowedTokenTypes, TokenType, UserTokenStore } from '../UserTokenStore';
+import { AllowedTokenTypes, TokenType, UserTokenStore } from '../tokens/UserTokenStore';
const log = new Logger("BridgeWidgetApi");
diff --git a/src/appservice.ts b/src/appservice.ts
index 2bd97742..9fbd5f34 100644
--- a/src/appservice.ts
+++ b/src/appservice.ts
@@ -9,9 +9,9 @@ const log = new Logger("Appservice");
export function getAppservice(config: BridgeConfig, registration: IAppserviceRegistration) {
let storage: IBridgeStorageProvider;
- if (config.queue.host && config.queue.port) {
- log.info(`Initialising Redis storage (on ${config.queue.host}:${config.queue.port})`);
- storage = new RedisStorageProvider(config.queue.host, config.queue.port);
+ if (config.cache) {
+ log.info(`Initialising Redis storage`);
+ storage = new RedisStorageProvider(config.cache);
} else {
log.info('Initialising memory storage');
storage = new MemoryStorageProvider();
diff --git a/src/config/Config.ts b/src/config/Config.ts
index 374c295a..1926d91a 100644
--- a/src/config/Config.ts
+++ b/src/config/Config.ts
@@ -10,6 +10,8 @@ import { ConfigError } from "../errors";
import { ApiError, ErrCode } from "../api";
import { GithubInstance, GITHUB_CLOUD_URL } from "../github/GithubInstance";
import { Logger } from "matrix-appservice-bridge";
+import { BridgeConfigCache } from "./sections/cache";
+import { BridgeConfigQueue } from "./sections";
const log = new Logger("Config");
@@ -407,12 +409,6 @@ interface BridgeConfigWebhook {
bindAddress?: string;
}
-export interface BridgeConfigQueue {
- monolithic: boolean;
- port?: number;
- host?: string;
-}
-
export interface BridgeConfigLogging {
level: "debug"|"info"|"warn"|"error"|"trace";
json?: boolean;
@@ -454,40 +450,45 @@ export interface BridgeConfigSentry {
environment?: string;
}
+
export interface BridgeConfigRoot {
bot?: BridgeConfigBot;
- serviceBots?: BridgeConfigServiceBot[];
bridge: BridgeConfigBridge;
+ cache?: BridgeConfigCache;
experimentalEncryption?: BridgeConfigEncryption;
- figma?: BridgeConfigFigma;
feeds?: BridgeConfigFeedsYAML;
+ figma?: BridgeConfigFigma;
generic?: BridgeGenericWebhooksConfigYAML;
github?: BridgeConfigGitHubYAML;
gitlab?: BridgeConfigGitLabYAML;
+ jira?: BridgeConfigJiraYAML;
+ listeners?: BridgeConfigListener[];
+ logging: BridgeConfigLogging;
+ metrics?: BridgeConfigMetrics;
+ passFile: string;
permissions?: BridgeConfigActorPermission[];
provisioning?: BridgeConfigProvisioning;
- jira?: BridgeConfigJiraYAML;
- logging: BridgeConfigLogging;
- passFile: string;
- queue: BridgeConfigQueue;
+ queue?: BridgeConfigQueue;
+ sentry?: BridgeConfigSentry;
+ serviceBots?: BridgeConfigServiceBot[];
webhook?: BridgeConfigWebhook;
widgets?: BridgeWidgetConfigYAML;
- metrics?: BridgeConfigMetrics;
- listeners?: BridgeConfigListener[];
- sentry?: BridgeConfigSentry;
}
export class BridgeConfig {
@configKey("Basic homeserver configuration")
public readonly bridge: BridgeConfigBridge;
+ @configKey(`Cache options for large scale deployments.
+ For encryption to work, this must be configured.`, true)
+ public readonly cache?: BridgeConfigCache;
@configKey(`Configuration for encryption support in the bridge.
If omitted, encryption support will be disabled.
This feature is HIGHLY EXPERIMENTAL AND SUBJECT TO CHANGE.
For more details, see https://github.com/matrix-org/matrix-hookshot/issues/594.`, true)
public readonly encryption?: BridgeConfigEncryption;
- @configKey(`Message queue / cache configuration options for large scale deployments.
- For encryption to work, must be set to monolithic mode and have a host & port specified.`, true)
- public readonly queue: BridgeConfigQueue;
+ @configKey(`Message queue configuration options for large scale deployments.
+ For encryption to work, this must not be configured.`, true)
+ public readonly queue?: Omit;
@configKey("Logging settings. You can have a severity debug,info,warn,error")
public readonly logging: BridgeConfigLogging;
@configKey(`Permissions for using the bridge. See docs/setup.md#permissions for help`, true)
@@ -549,13 +550,38 @@ export class BridgeConfig {
this.generic = configData.generic && new BridgeConfigGenericWebhooks(configData.generic);
this.feeds = configData.feeds && new BridgeConfigFeeds(configData.feeds);
this.provisioning = configData.provisioning;
- this.passFile = configData.passFile;
+ this.passFile = configData.passFile ?? "./passkey.pem";
this.bot = configData.bot;
this.serviceBots = configData.serviceBots;
this.metrics = configData.metrics;
- this.queue = configData.queue || {
- monolithic: true,
- };
+
+ // TODO: Formalize env support
+ if (env?.CFG_QUEUE_MONOLITHIC && ["false", "off", "no"].includes(env.CFG_QUEUE_MONOLITHIC)) {
+ if (!env?.CFG_QUEUE_HOST) {
+ throw new ConfigError("env:CFG_QUEUE_HOST", "CFG_QUEUE_MONOLITHIC was defined but host was not");
+ }
+ configData.queue = {
+ monolithic: false,
+ host: env?.CFG_QUEUE_HOST,
+ port: env?.CFG_QUEUE_POST ? parseInt(env?.CFG_QUEUE_POST, 10) : undefined,
+ }
+ }
+
+ this.cache = configData.cache;
+ this.queue = configData.queue;
+
+ if (configData.queue?.monolithic !== undefined) {
+ log.warn("The `queue.monolithic` config option is deprecated. Instead, configure the `cache` section.");
+ this.cache = {
+ redisUri: 'redisUri' in configData.queue ? configData.queue.redisUri
+ : `redis://${configData.queue.host ?? 'localhost'}:${configData.queue.port ?? 6379}`
+ };
+ // If monolithic, disable the redis queue.
+ if (configData.queue.monolithic === true) {
+ this.queue = undefined;
+ }
+ }
+
this.encryption = configData.experimentalEncryption;
@@ -589,13 +615,6 @@ export class BridgeConfig {
throw Error("Config is not valid: At least one of GitHub, GitLab, JIRA, Figma, feeds or generic hooks must be configured");
}
- // TODO: Formalize env support
- if (env?.CFG_QUEUE_MONOLITHIC && ["false", "off", "no"].includes(env.CFG_QUEUE_MONOLITHIC)) {
- this.queue.monolithic = false;
- this.queue.host = env?.CFG_QUEUE_HOST;
- this.queue.port = env?.CFG_QUEUE_POST ? parseInt(env?.CFG_QUEUE_POST, 10) : undefined;
- }
-
if ('goNebMigrator' in configData) {
log.warn(`The GoNEB migrator has been removed from this release. You should remove the 'goNebMigrator' from your config.`);
}
@@ -677,12 +696,12 @@ Please back up your crypto store at ${this.encryption.storagePath},
remove "useLegacySledStore" from your configuration file, and restart Hookshot.
`);
}
- if (!this.queue.monolithic) {
- throw new ConfigError("queue.monolithic", "Encryption is not supported in worker mode yet.");
+ if (!this.cache) {
+ throw new ConfigError("cache", "Encryption requires the Redis cache to be enabled.");
}
- if (!this.queue.port) {
- throw new ConfigError("queue.port", "You must enable redis support for encryption to work.");
+ if (this.queue) {
+ throw new ConfigError("queue", "Encryption does not support message queues.");
}
}
diff --git a/src/config/Defaults.ts b/src/config/Defaults.ts
index 70522e8e..2ea9e1f5 100644
--- a/src/config/Defaults.ts
+++ b/src/config/Defaults.ts
@@ -16,9 +16,10 @@ export const DefaultConfigRoot: BridgeConfigRoot = {
bindAddress: "127.0.0.1",
},
queue: {
- monolithic: true,
- port: 6379,
- host: "localhost",
+ redisUri: "redis://localhost:6379",
+ },
+ cache: {
+ redisUri: "redis://localhost:6379",
},
logging: {
level: "info",
@@ -33,7 +34,7 @@ export const DefaultConfigRoot: BridgeConfigRoot = {
level: "admin"
}],
}],
- passFile: "passkey.pem",
+ passFile: "./passkey.pem",
widgets: {
publicUrl: `${hookshotWebhooksUrl}/widgetapi/v1/static`,
addToAdminRooms: false,
diff --git a/src/config/sections/cache.ts b/src/config/sections/cache.ts
new file mode 100644
index 00000000..2da69c73
--- /dev/null
+++ b/src/config/sections/cache.ts
@@ -0,0 +1,7 @@
+export interface BridgeConfigCache {
+ /**
+ * A redis URI string
+ * @example `redis://user:password@host:port/dbnum`
+ */
+ redisUri: string;
+}
\ No newline at end of file
diff --git a/src/config/sections/index.ts b/src/config/sections/index.ts
new file mode 100644
index 00000000..e4fb0283
--- /dev/null
+++ b/src/config/sections/index.ts
@@ -0,0 +1,2 @@
+export * from "./cache";
+export * from "./queue";
\ No newline at end of file
diff --git a/src/config/sections/queue.ts b/src/config/sections/queue.ts
new file mode 100644
index 00000000..605450e7
--- /dev/null
+++ b/src/config/sections/queue.ts
@@ -0,0 +1,22 @@
+/**
+ * Configuration for the message queue.
+ */
+interface BridgeConfigQueueBase {
+ /**
+ * Controls whether the queue config is used just for the cache (monolithic),
+ * or the message queue as well.
+ * @deprecated Use the `cache` config instead to control this seperately.
+ */
+ monolithic?: boolean;
+}
+
+interface BridgeConfigQueueUri extends BridgeConfigQueueBase {
+ redisUri: string;
+}
+
+interface BridgeConfigQueueLegacyOptions extends BridgeConfigQueueBase {
+ port?: number;
+ host?: string;
+}
+
+export type BridgeConfigQueue = BridgeConfigQueueUri|BridgeConfigQueueLegacyOptions
\ No newline at end of file
diff --git a/src/format_util.rs b/src/format_util.rs
index 2146381c..dc11b7ff 100644
--- a/src/format_util.rs
+++ b/src/format_util.rs
@@ -1,7 +1,7 @@
use crate::github::types::*;
use crate::jira;
use crate::jira::types::{JiraIssue, JiraIssueLight, JiraIssueMessageBody, JiraIssueSimpleItem};
-use contrast;
+use contrast::contrast;
use md5::{Digest, Md5};
use napi::bindgen_prelude::*;
use napi_derive::napi;
@@ -87,12 +87,11 @@ pub fn format_labels(array: Vec) -> Result(color_rgb, RGB::new(0, 0, 0)) > 4.5 {
- "#000000"
- } else {
- "#FFFFFF"
- };
+ let contrast_color = if contrast::(color_rgb, RGB::new(0, 0, 0)) > 4.5 {
+ "#000000"
+ } else {
+ "#FFFFFF"
+ };
write!(html, " data-mx-color=\"{}\"", contrast_color).unwrap();
}
if let Some(description) = label.description {
diff --git a/src/github/GrantChecker.ts b/src/github/GrantChecker.ts
index a1497f30..e3eb27ee 100644
--- a/src/github/GrantChecker.ts
+++ b/src/github/GrantChecker.ts
@@ -1,7 +1,7 @@
import { Appservice } from "matrix-bot-sdk";
import { GitHubRepoConnection } from "../Connections";
import { GrantChecker } from "../grants/GrantCheck";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import { Logger } from 'matrix-appservice-bridge';
const log = new Logger('GitHubGrantChecker');
diff --git a/src/github/Router.ts b/src/github/Router.ts
index 94ba2ce9..fa05c57b 100644
--- a/src/github/Router.ts
+++ b/src/github/Router.ts
@@ -1,7 +1,7 @@
import { Router, Request, Response, NextFunction } from "express";
import { BridgeConfigGitHub } from "../config/Config";
import { ApiError, ErrCode } from "../api";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import { Logger } from "matrix-appservice-bridge";
import { GithubInstance } from "./GithubInstance";
import { NAMELESS_ORG_PLACEHOLDER } from "./Types";
diff --git a/src/jira/GrantChecker.ts b/src/jira/GrantChecker.ts
index 211fc821..1fc19b14 100644
--- a/src/jira/GrantChecker.ts
+++ b/src/jira/GrantChecker.ts
@@ -1,7 +1,7 @@
import { Appservice } from "matrix-bot-sdk";
import { JiraProjectConnection } from "../Connections";
import { GrantChecker } from "../grants/GrantCheck";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
interface JiraGrantConnectionId{
url: string;
diff --git a/src/jira/Router.ts b/src/jira/Router.ts
index 2bd6815b..7771e5f7 100644
--- a/src/jira/Router.ts
+++ b/src/jira/Router.ts
@@ -1,7 +1,7 @@
import { BridgeConfigJira } from "../config/Config";
import { MessageQueue } from "../MessageQueue";
import { Router, Request, Response, NextFunction, json } from "express";
-import { UserTokenStore } from "../UserTokenStore";
+import { UserTokenStore } from "../tokens/UserTokenStore";
import { Logger } from "matrix-appservice-bridge";
import { ApiError, ErrCode } from "../api";
import { JiraOAuthRequestCloud, JiraOAuthRequestOnPrem, JiraOAuthRequestResult } from "./OAuth";
diff --git a/src/lib.rs b/src/lib.rs
index 3a15bd65..4e4e1bfe 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,6 +3,7 @@ pub mod feeds;
pub mod format_util;
pub mod github;
pub mod jira;
+pub mod tokens;
pub mod util;
#[macro_use]
diff --git a/src/UserTokenStore.ts b/src/tokens/UserTokenStore.ts
similarity index 83%
rename from src/UserTokenStore.ts
rename to src/tokens/UserTokenStore.ts
index afd6520d..7ea2ce3b 100644
--- a/src/UserTokenStore.ts
+++ b/src/tokens/UserTokenStore.ts
@@ -1,22 +1,22 @@
-import { GithubInstance } from "./github/GithubInstance";
-import { GitLabClient } from "./Gitlab/Client";
+import { GithubInstance } from "../github/GithubInstance";
+import { GitLabClient } from "../Gitlab/Client";
import { Intent } from "matrix-bot-sdk";
import { promises as fs } from "fs";
-import { publicEncrypt, privateDecrypt } from "crypto";
import { Logger } from "matrix-appservice-bridge";
-import { isJiraCloudInstance, JiraClient } from "./jira/Client";
-import { JiraStoredToken } from "./jira/Types";
-import { BridgeConfig, BridgeConfigJira, BridgeConfigJiraOnPremOAuth, BridgePermissionLevel } from "./config/Config";
+import { isJiraCloudInstance, JiraClient } from "../jira/Client";
+import { JiraStoredToken } from "../jira/Types";
+import { BridgeConfig, BridgeConfigJira, BridgeConfigJiraOnPremOAuth, BridgePermissionLevel } from "../config/Config";
import { randomUUID } from 'node:crypto';
-import { GitHubOAuthToken } from "./github/Types";
-import { ApiError, ErrCode } from "./api";
-import { JiraOAuth } from "./jira/OAuth";
-import { JiraCloudOAuth } from "./jira/oauth/CloudOAuth";
-import { JiraOnPremOAuth } from "./jira/oauth/OnPremOAuth";
-import { JiraOnPremClient } from "./jira/client/OnPremClient";
-import { JiraCloudClient } from "./jira/client/CloudClient";
-import { TokenError, TokenErrorCode } from "./errors";
+import { GitHubOAuthToken } from "../github/Types";
+import { ApiError, ErrCode } from "../api";
+import { JiraOAuth } from "../jira/OAuth";
+import { JiraCloudOAuth } from "../jira/oauth/CloudOAuth";
+import { JiraOnPremOAuth } from "../jira/oauth/OnPremOAuth";
+import { JiraOnPremClient } from "../jira/client/OnPremClient";
+import { JiraCloudClient } from "../jira/client/CloudClient";
+import { TokenError, TokenErrorCode } from "../errors";
import { TypedEmitter } from "tiny-typed-emitter";
+import { hashId, TokenEncryption } from "../libRs";
const ACCOUNT_DATA_TYPE = "uk.half-shot.matrix-hookshot.github.password-store:";
const ACCOUNT_DATA_GITLAB_TYPE = "uk.half-shot.matrix-hookshot.gitlab.password-store:";
@@ -31,6 +31,8 @@ export const AllowedTokenTypes = ["github", "gitlab", "jira"];
interface StoredTokenData {
encrypted: string|string[];
+ keyId: string;
+ algorithm: 'rsa';
instance?: string;
}
@@ -51,20 +53,29 @@ function tokenKey(type: TokenType, userId: string, legacy = false, instanceUrl?:
return `${legacy ? LEGACY_ACCOUNT_DATA_GITLAB_TYPE : ACCOUNT_DATA_GITLAB_TYPE}${instanceUrl}${userId}`;
}
-const MAX_TOKEN_PART_SIZE = 128;
const OAUTH_TIMEOUT_MS = 1000 * 60 * 30;
interface Emitter {
onNewToken: (type: TokenType, userId: string, token: string, instanceUrl?: string) => void,
}
export class UserTokenStore extends TypedEmitter {
- private key!: Buffer;
+
+ public static async fromKeyPath(keyPath: string, intent: Intent, config: BridgeConfig) {
+ log.info(`Loading token key file ${keyPath}`);
+ const key = await fs.readFile(keyPath);
+ return new UserTokenStore(key, intent, config);
+ }
+
private oauthSessionStore: Map = new Map();
private userTokens: Map;
public readonly jiraOAuth?: JiraOAuth;
- constructor(private keyPath: string, private intent: Intent, private config: BridgeConfig) {
+ private tokenEncryption: TokenEncryption;
+ private readonly keyId: string;
+ constructor(key: Buffer, private readonly intent: Intent, private readonly config: BridgeConfig) {
super();
+ this.tokenEncryption = new TokenEncryption(key);
this.userTokens = new Map();
+ this.keyId = hashId(key.toString('utf-8'));
if (config.jira?.oauth) {
if ("client_id" in config.jira.oauth) {
this.jiraOAuth = new JiraCloudOAuth(config.jira.oauth);
@@ -76,11 +87,6 @@ export class UserTokenStore extends TypedEmitter {
}
}
- public async load() {
- log.info(`Loading token key file ${this.keyPath}`);
- this.key = await fs.readFile(this.keyPath);
- }
-
public stop() {
for (const session of this.oauthSessionStore.values()) {
clearTimeout(session.timeout);
@@ -92,21 +98,16 @@ export class UserTokenStore extends TypedEmitter {
throw new ApiError('User does not have permission to log in to service', ErrCode.ForbiddenUser);
}
const key = tokenKey(type, userId, false, instanceUrl);
- const tokenParts: string[] = [];
- let tokenSource = token;
- while (tokenSource && tokenSource.length > 0) {
- const part = tokenSource.slice(0, MAX_TOKEN_PART_SIZE);
- tokenSource = tokenSource.substring(MAX_TOKEN_PART_SIZE);
- tokenParts.push(publicEncrypt(this.key, Buffer.from(part)).toString("base64"));
- }
+ const tokenParts: string[] = this.tokenEncryption.encrypt(token);
const data: StoredTokenData = {
encrypted: tokenParts,
+ keyId: this.keyId,
+ algorithm: "rsa",
instance: instanceUrl,
};
await this.intent.underlyingClient.setAccountData(key, data);
this.userTokens.set(key, token);
log.info(`Stored new ${type} token for ${userId}`);
- log.debug(`Stored`, data);
this.emit("onNewToken", type, userId, token, instanceUrl);
}
@@ -146,8 +147,19 @@ export class UserTokenStore extends TypedEmitter {
if (!obj || "deleted" in obj) {
return null;
}
+ // For legacy we just assume it's the current configured key.
+ const algorithm = obj.algorithm ?? "rsa";
+ const keyId = obj.keyId ?? this.keyId;
+
+ if (algorithm !== 'rsa') {
+ throw new Error(`Algorithm for stored data is '${algorithm}', but we only support RSA`);
+ }
+ if (keyId !== this.keyId) {
+ throw new Error(`Stored data was encrypted with a different key to the one currently configured`);
+ }
+
const encryptedParts = typeof obj.encrypted === "string" ? [obj.encrypted] : obj.encrypted;
- const token = encryptedParts.map((t) => privateDecrypt(this.key, Buffer.from(t, "base64")).toString("utf-8")).join("");
+ const token = this.tokenEncryption.decrypt(encryptedParts);
this.userTokens.set(key, token);
return token;
} catch (ex) {
diff --git a/src/tokens/mod.rs b/src/tokens/mod.rs
new file mode 100644
index 00000000..6aa0cc17
--- /dev/null
+++ b/src/tokens/mod.rs
@@ -0,0 +1,116 @@
+use std::string::FromUtf8Error;
+
+use base64ct::{Base64, Encoding};
+use napi::bindgen_prelude::Buffer;
+use napi::Error;
+use rsa::pkcs8::DecodePrivateKey;
+use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};
+
+static MAX_TOKEN_PART_SIZE: usize = 128;
+
+struct TokenEncryption {
+ pub private_key: RsaPrivateKey,
+ pub public_key: RsaPublicKey,
+}
+
+#[derive(Debug)]
+#[allow(dead_code)]
+enum TokenEncryptionError {
+ FromUtf8(FromUtf8Error),
+ PrivateKey(rsa::pkcs8::Error),
+}
+
+#[derive(Debug)]
+#[allow(dead_code)]
+enum DecryptError {
+ Base64(base64ct::Error),
+ Decryption(rsa::Error),
+ FromUtf8(FromUtf8Error),
+}
+
+impl TokenEncryption {
+ pub fn new(private_key_data: Vec) -> Result {
+ let data = String::from_utf8(private_key_data).map_err(TokenEncryptionError::FromUtf8)?;
+ let private_key = RsaPrivateKey::from_pkcs8_pem(data.as_str())
+ .map_err(TokenEncryptionError::PrivateKey)?;
+ let public_key = private_key.to_public_key();
+ Ok(TokenEncryption {
+ private_key,
+ public_key,
+ })
+ }
+}
+
+#[napi(js_name = "TokenEncryption")]
+pub struct JsTokenEncryption {
+ inner: TokenEncryption,
+}
+
+#[napi]
+impl JsTokenEncryption {
+ #[napi(constructor)]
+ pub fn new(private_key_data: Buffer) -> Result {
+ let buf: Vec = private_key_data.into();
+ match TokenEncryption::new(buf) {
+ Ok(inner) => Ok(JsTokenEncryption { inner }),
+ Err(err) => Err(Error::new(
+ napi::Status::GenericFailure,
+ format!("Error reading private key: {:?}", err).to_string(),
+ )),
+ }
+ }
+
+ #[napi]
+ pub fn decrypt(&self, parts: Vec) -> Result {
+ let mut result = String::new();
+
+ for v in parts {
+ match self.decrypt_value(v) {
+ Ok(new_value) => {
+ result += &new_value;
+ Ok(())
+ }
+ Err(err) => Err(Error::new(
+ napi::Status::GenericFailure,
+ format!("Could not decrypt string: {:?}", err).to_string(),
+ )),
+ }?
+ }
+ Ok(result)
+ }
+
+ fn decrypt_value(&self, value: String) -> Result {
+ let raw_value = Base64::decode_vec(&value).map_err(DecryptError::Base64)?;
+ let decrypted_value = self
+ .inner
+ .private_key
+ .decrypt(Pkcs1v15Encrypt, &raw_value)
+ .map_err(DecryptError::Decryption)?;
+ let utf8_value = String::from_utf8(decrypted_value).map_err(DecryptError::FromUtf8)?;
+ Ok(utf8_value)
+ }
+
+ #[napi]
+ pub fn encrypt(&self, input: String) -> Result, Error> {
+ let mut rng = rand::thread_rng();
+ let mut parts: Vec = Vec::new();
+ for part in input.into_bytes().chunks(MAX_TOKEN_PART_SIZE) {
+ match self
+ .inner
+ .public_key
+ .encrypt(&mut rng, Pkcs1v15Encrypt, part)
+ {
+ Ok(encrypted) => {
+ let b64 = Base64::encode_string(encrypted.as_slice());
+ parts.push(b64);
+ Ok(())
+ }
+ Err(err) => Err(Error::new(
+ napi::Status::GenericFailure,
+ format!("Could not encrypt string: {:?}", err).to_string(),
+ )),
+ }?
+ }
+ Ok(parts)
+ }
+}
diff --git a/tests/AdminRoomTest.ts b/tests/AdminRoomTest.ts
index ee171e17..658c0cfc 100644
--- a/tests/AdminRoomTest.ts
+++ b/tests/AdminRoomTest.ts
@@ -4,7 +4,7 @@ import { AdminRoom } from "../src/AdminRoom";
import { DefaultConfig } from "../src/config/Defaults";
import { ConnectionManager } from "../src/ConnectionManager";
import { NotifFilter } from "../src/NotificationFilters";
-import { UserTokenStore } from "../src/UserTokenStore";
+import { UserTokenStore } from "../src/tokens/UserTokenStore";
import { IntentMock } from "./utils/IntentMock";
const ROOM_ID = "!foo:bar";
@@ -14,9 +14,8 @@ function createAdminRoom(data: any = {admin_user: "@admin:bar"}): [AdminRoom, In
if (!data.admin_user) {
data.admin_user = "@admin:bar";
}
- const tokenStore = new UserTokenStore("notapath", intent, DefaultConfig);
- return [new AdminRoom(ROOM_ID, data, NotifFilter.getDefaultContent(), intent, tokenStore, DefaultConfig, {} as ConnectionManager), intent];
-}
+ return [new AdminRoom(ROOM_ID, data, NotifFilter.getDefaultContent(), intent, {} as UserTokenStore, DefaultConfig, {} as ConnectionManager), intent];
+}
describe("AdminRoom", () => {
it("will present help text", async () => {
diff --git a/tests/MessageQueueTest.ts b/tests/MessageQueueTest.ts
index a3fa0d63..f2cd6e45 100644
--- a/tests/MessageQueueTest.ts
+++ b/tests/MessageQueueTest.ts
@@ -1,9 +1,7 @@
import { expect } from "chai";
import { createMessageQueue } from "../src/MessageQueue/MessageQueue";
-const mq = createMessageQueue({
- monolithic: true,
-});
+const mq = createMessageQueue();
describe("MessageQueueTest", () => {
describe("LocalMq", () => {
diff --git a/tests/config/config.ts b/tests/config/config.ts
new file mode 100644
index 00000000..22a1a752
--- /dev/null
+++ b/tests/config/config.ts
@@ -0,0 +1,76 @@
+import { BridgeConfig } from "../../src/config/Config";
+import { DefaultConfigRoot } from "../../src/config/Defaults";
+import { expect } from "chai";
+
+
+describe("Config/BridgeConfig", () => {
+ describe("will handle the legacy queue.monolitihc option", () => {
+ it("with no parameters", () => {
+ const config = new BridgeConfig({ ...DefaultConfigRoot, queue: {
+ monolithic: true
+ }});
+ expect(config.queue).to.be.undefined;
+ expect(config.cache?.redisUri).to.equal("redis://localhost:6379");
+ });
+ it("with a host parameter", () => {
+ const config = new BridgeConfig({ ...DefaultConfigRoot, queue: {
+ monolithic: true,
+ host: 'bark'
+ }});
+ expect(config.queue).to.be.undefined;
+ expect(config.cache?.redisUri).to.equal("redis://bark:6379");
+ });
+ it("with a port parameter", () => {
+ const config = new BridgeConfig({ ...DefaultConfigRoot, queue: {
+ monolithic: true,
+ port: 6379,
+ }});
+ expect(config.queue).to.be.undefined;
+ expect(config.cache?.redisUri).to.equal("redis://localhost:6379");
+ });
+ it("with a host and port parameter", () => {
+ const config = new BridgeConfig({ ...DefaultConfigRoot, queue: {
+ monolithic: true,
+ host: 'bark',
+ port: 6379,
+ }});
+ expect(config.queue).to.be.undefined;
+ expect(config.cache?.redisUri).to.equal("redis://bark:6379");
+ });
+ it("with monolithic disabled", () => {
+ const config = new BridgeConfig({ ...DefaultConfigRoot, queue: {
+ monolithic: false
+ }});
+ expect(config.queue).to.deep.equal({
+ monolithic: false,
+ });
+ expect(config.cache?.redisUri).to.equal("redis://localhost:6379");
+ });
+ });
+ describe("will handle the queue option", () => {
+ it("with redisUri", () => {
+ const config = new BridgeConfig({ ...DefaultConfigRoot, queue: {
+ redisUri: "redis://localhost:6379"
+ }, cache: undefined});
+ expect(config.queue).to.deep.equal({
+ redisUri: "redis://localhost:6379"
+ });
+ expect(config.cache).to.be.undefined;
+ });
+ });
+ describe("will handle the cache option", () => {
+ it("with redisUri", () => {
+ const config = new BridgeConfig({
+ ...DefaultConfigRoot,
+ cache: {
+ redisUri: "redis://localhost:6379"
+ },
+ queue: undefined,
+ });
+ expect(config.cache).to.deep.equal({
+ redisUri: "redis://localhost:6379"
+ });
+ expect(config.queue).to.be.undefined;
+ });
+ });
+})
\ No newline at end of file
diff --git a/tests/connections/GithubRepoTest.ts b/tests/connections/GithubRepoTest.ts
index e686f2a5..7359e55b 100644
--- a/tests/connections/GithubRepoTest.ts
+++ b/tests/connections/GithubRepoTest.ts
@@ -1,7 +1,7 @@
import { GitHubRepoConnection, GitHubRepoConnectionState } from "../../src/Connections/GithubRepo"
import { GithubInstance } from "../../src/github/GithubInstance";
import { createMessageQueue } from "../../src/MessageQueue";
-import { UserTokenStore } from "../../src/UserTokenStore";
+import { UserTokenStore } from "../../src/tokens/UserTokenStore";
import { DefaultConfig } from "../../src/config/Defaults";
import { AppserviceMock } from "../utils/AppserviceMock";
import { ApiError, ErrCode, ValidatorApiError } from "../../src/api";
@@ -37,9 +37,7 @@ const GITHUB_ISSUE_CREATED_PAYLOAD = {
};
function createConnection(state: Record = {}, isExistingState=false) {
- const mq = createMessageQueue({
- monolithic: true
- });
+ const mq = createMessageQueue();
mq.subscribe('*');
const as = AppserviceMock.create();
const intent = as.getIntentForUserId('@github:example.test');
diff --git a/tests/connections/GitlabRepoTest.ts b/tests/connections/GitlabRepoTest.ts
index ee87d765..af438e61 100644
--- a/tests/connections/GitlabRepoTest.ts
+++ b/tests/connections/GitlabRepoTest.ts
@@ -1,5 +1,5 @@
import { createMessageQueue } from "../../src/MessageQueue";
-import { UserTokenStore } from "../../src/UserTokenStore";
+import { UserTokenStore } from "../../src/tokens/UserTokenStore";
import { AppserviceMock } from "../utils/AppserviceMock";
import { ApiError, ErrCode, ValidatorApiError } from "../../src/api";
import { GitLabRepoConnection, GitLabRepoConnectionState } from "../../src/Connections";
@@ -56,9 +56,7 @@ const GITLAB_MR_COMMENT = {
const COMMENT_DEBOUNCE_MS = 25;
function createConnection(state: Record = {}, isExistingState=false): { connection: GitLabRepoConnection, intent: IntentMock } {
- const mq = createMessageQueue({
- monolithic: true
- });
+ const mq = createMessageQueue();
mq.subscribe('*');
const as = AppserviceMock.create();
const intent = as.getIntentForUserId('@gitlab:example.test');
diff --git a/tests/tokens/tokenencryption.spec.ts b/tests/tokens/tokenencryption.spec.ts
new file mode 100644
index 00000000..bd969447
--- /dev/null
+++ b/tests/tokens/tokenencryption.spec.ts
@@ -0,0 +1,48 @@
+import { TokenEncryption } from "../../src/libRs";
+import { RSAKeyPairOptions, generateKeyPair } from "node:crypto";
+import { expect } from "chai";
+
+describe("TokenEncryption", () => {
+ let keyPromise: Promise;
+ async function createTokenEncryption() {
+ return new TokenEncryption(await keyPromise);
+ }
+
+ before('generate RSA key', () => {
+ // Generate this once since it will take an age.
+ keyPromise = new Promise((resolve, reject) => generateKeyPair("rsa", {
+ // Deliberately shorter length to speed up test
+ modulusLength: 2048,
+ privateKeyEncoding: {
+ type: "pkcs8",
+ format: "pem",
+ },
+ publicKeyEncoding: {
+ format: "pem",
+ type: "pkcs1",
+ }
+ } satisfies RSAKeyPairOptions<"pem", "pem">, (err, _, privateKey) => {
+ if (err) { reject(err) } else { resolve(Buffer.from(privateKey)) }
+ }));
+ }, );
+ it('should be able to encrypt a string into a single part', async() => {
+ const tokenEncryption = await createTokenEncryption();
+ const result = tokenEncryption.encrypt('hello world');
+ expect(result).to.have.lengthOf(1);
+ });
+ it('should be able to decrypt from a single part into a string', async() => {
+ const tokenEncryption = await createTokenEncryption();
+ const value = tokenEncryption.encrypt('hello world');
+ const result = tokenEncryption.decrypt(value);
+ expect(result).to.equal('hello world');
+ });
+ it('should be able to decrypt from many parts into string', async() => {
+ const plaintext = 'This is a very long string that needs to be encoded into multiple parts in order for us to store it properly. This ' +
+ ' should end up as multiple encrypted values in base64.';
+ const tokenEncryption = await createTokenEncryption();
+ const value = tokenEncryption.encrypt(plaintext);
+ expect(value).to.have.lengthOf(2);
+ const result = tokenEncryption.decrypt(value);
+ expect(result).to.equal(plaintext);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index e0992acc..d80212e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4135,9 +4135,9 @@ fn.name@1.x.x:
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
follow-redirects@^1.14.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0:
- version "1.15.4"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
- integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
+ version "1.15.6"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
+ integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
for-each@^0.3.3:
version "0.3.3"
@@ -7139,9 +7139,9 @@ safe-stable-stringify@^2.3.1:
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sanitize-html@^2.11.0, sanitize-html@^2.8.0:
- version "2.11.0"
- resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.11.0.tgz#9a6434ee8fcaeddc740d8ae7cd5dd71d3981f8f6"
- integrity sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==
+ version "2.12.1"
+ resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.12.1.tgz#280a0f5c37305222921f6f9d605be1f6558914c7"
+ integrity sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==
dependencies:
deepmerge "^4.2.2"
escape-string-regexp "^4.0.0"
@@ -8000,10 +8000,10 @@ vite-plugin-magical-svg@^1.1.1:
svgo "^3.1.0"
xml2js "^0.6.2"
-vite@^5.0.12:
- version "5.0.12"
- resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.12.tgz#8a2ffd4da36c132aec4adafe05d7adde38333c47"
- integrity sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==
+vite@^5.0.13:
+ version "5.0.13"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.13.tgz#605865b0e482506163e3f04f91665238f3be8cf1"
+ integrity sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==
dependencies:
esbuild "^0.19.3"
postcss "^8.4.32"