mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 13:17:08 +00:00
Stabilize E2EE support (#989)
* Deprecate legacy sled store * Add e2ee test * Add support for e2ee testing in e2e environment * Tidy up redis support * Attempt to get test working * cleanup test * opportunistic lint * tiny bit of cleanup * remove ref * tweak to homerunner * switch to nightly images for Synapse (to test E2EE) * use nightly * newsfile. * Update bot sdk to support authenticated media (now that Synapse requires it) * fix typings * MatrixError * one more * Graduate the encryption property to stable. * update test config * Update encryption docs. * fix some old config bits
This commit is contained in:
parent
46b0004581
commit
052d42fca8
18
.github/workflows/main.yml
vendored
18
.github/workflows/main.yml
vendored
@ -92,7 +92,7 @@ jobs:
|
|||||||
homerunnersha: ${{ steps.gitsha.outputs.sha }}
|
homerunnersha: ${{ steps.gitsha.outputs.sha }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout matrix-org/complement
|
- name: Checkout matrix-org/complement
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/complement
|
repository: matrix-org/complement
|
||||||
- name: Get complement git sha
|
- name: Get complement git sha
|
||||||
@ -100,7 +100,7 @@ jobs:
|
|||||||
run: echo sha=`git rev-parse --short HEAD` >> "$GITHUB_OUTPUT"
|
run: echo sha=`git rev-parse --short HEAD` >> "$GITHUB_OUTPUT"
|
||||||
- name: Cache homerunner
|
- name: Cache homerunner
|
||||||
id: cached
|
id: cached
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: homerunner
|
path: homerunner
|
||||||
key: ${{ runner.os }}-homerunner-${{ steps.gitsha.outputs.sha }}
|
key: ${{ runner.os }}-homerunner-${{ steps.gitsha.outputs.sha }}
|
||||||
@ -125,23 +125,28 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- test
|
- test
|
||||||
- build-homerunner
|
- build-homerunner
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
steps:
|
steps:
|
||||||
- name: Install Complement Dependencies
|
- name: Install Complement Dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update && sudo apt-get install -y libolm3
|
sudo apt-get update && sudo apt-get install -y libolm3
|
||||||
- name: Load cached homerunner bin
|
- name: Load cached homerunner bin
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: homerunner
|
path: homerunner
|
||||||
key: ${{ runner.os }}-homerunner-${{ needs.build-synapse.outputs.homerunnersha }}
|
key: ${{ runner.os }}-homerunner-${{ needs.build-synapse.outputs.homerunnersha }}
|
||||||
fail-on-cache-miss: true # Shouldn't happen, we build this in the needs step.
|
fail-on-cache-miss: true # Shouldn't happen, we build this in the needs step.
|
||||||
- name: Checkout matrix-hookshot
|
- name: Checkout matrix-hookshot
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: matrix-hookshot
|
path: matrix-hookshot
|
||||||
# Setup node & run tests
|
# Setup node & run tests
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: matrix-hookshot/.node-version
|
node-version-file: matrix-hookshot/.node-version
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
@ -152,8 +157,9 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
env:
|
env:
|
||||||
HOMERUNNER_SPAWN_HS_TIMEOUT_SECS: 100
|
HOMERUNNER_SPAWN_HS_TIMEOUT_SECS: 100
|
||||||
HOMERUNNER_IMAGE: ghcr.io/element-hq/synapse/complement-synapse:latest
|
HOMERUNNER_IMAGE: ghcr.io/element-hq/synapse/complement-synapse:nightly
|
||||||
NODE_OPTIONS: --dns-result-order ipv4first
|
NODE_OPTIONS: --dns-result-order ipv4first
|
||||||
|
REDIS_DATABASE_URI: "redis://localhost:6379"
|
||||||
run: |
|
run: |
|
||||||
docker pull $HOMERUNNER_IMAGE
|
docker pull $HOMERUNNER_IMAGE
|
||||||
cd matrix-hookshot
|
cd matrix-hookshot
|
||||||
|
2
changelog.d/989.feature
Normal file
2
changelog.d/989.feature
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Support for E2E Encrypted rooms is now considered stable and can be enabled in production. Please see the [documentation](https://matrix-org.github.io/matrix-hookshot/latest/advanced/encryption.html)
|
||||||
|
on the requirements for enabling support.
|
@ -1,5 +1,11 @@
|
|||||||
# This is an example configuration file
|
# This is an example configuration file
|
||||||
|
|
||||||
|
logging:
|
||||||
|
# Logging settings. You can have a severity debug,info,warn,error
|
||||||
|
level: info
|
||||||
|
colorize: true
|
||||||
|
json: false
|
||||||
|
timestampFormat: HH:mm:ss:SSS
|
||||||
bridge:
|
bridge:
|
||||||
# Basic homeserver configuration
|
# Basic homeserver configuration
|
||||||
domain: example.com
|
domain: example.com
|
||||||
@ -11,12 +17,6 @@ passFile:
|
|||||||
# A passkey used to encrypt tokens stored inside the bridge.
|
# 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
|
# 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
|
|
||||||
colorize: true
|
|
||||||
json: false
|
|
||||||
timestampFormat: HH:mm:ss:SSS
|
|
||||||
listeners:
|
listeners:
|
||||||
# HTTP Listener configuration.
|
# HTTP Listener configuration.
|
||||||
# Bind resource endpoints to ports and addresses.
|
# Bind resource endpoints to ports and addresses.
|
||||||
@ -143,10 +143,12 @@ listeners:
|
|||||||
# # For encryption to work, this must be configured.
|
# # For encryption to work, this must be configured.
|
||||||
# redisUri: redis://localhost:6379
|
# redisUri: redis://localhost:6379
|
||||||
|
|
||||||
#queue:
|
#encryption:
|
||||||
# # (Optional) Message queue configuration options for large scale deployments.
|
# # (Optional) Configuration for encryption support in the bridge.
|
||||||
# # For encryption to work, this must not be configured.
|
# # If omitted, encryption support will be disabled.
|
||||||
# redisUri: redis://localhost:6379
|
# storagePath:
|
||||||
|
# # Path to the directory used to store encryption files. These files must be persist between restarts of the service.
|
||||||
|
# ./cryptostore
|
||||||
|
|
||||||
#widgets:
|
#widgets:
|
||||||
# # (Optional) EXPERIMENTAL support for complimentary widgets
|
# # (Optional) EXPERIMENTAL support for complimentary widgets
|
||||||
|
@ -1,27 +1,32 @@
|
|||||||
Encryption
|
Encryption
|
||||||
==========
|
==========
|
||||||
|
|
||||||
<section class="warning">
|
<section class="notice">
|
||||||
Encryption support is <strong>HIGHLY EXPERIMENTAL AND SUBJECT TO CHANGE</strong>. It should not be enabled for production workloads.
|
Support for encryption is considered stable, but the underlying specification changes are not yet.
|
||||||
For more details, see <a href="https://github.com/matrix-org/matrix-hookshot/issues/594">issue 594</a>.
|
|
||||||
|
Hookshot supports end-to-bridge encryption via [MSC3202](https://github.com/matrix-org/matrix-spec-proposals/pull/3202), and [MSC4203](https://github.com/matrix-org/matrix-spec-proposals/pull/4203). Hookshot needs to be configured against a a homeserver that supports these features, such as [Synapse](#running-with-synapse).
|
||||||
|
|
||||||
|
Please check with your homeserver implementation before reporting bugs against matrix-hookshot.
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
Hookshot supports end-to-bridge encryption via [MSC3202](https://github.com/matrix-org/matrix-spec-proposals/pull/3202). As such, encryption requires Hookshot to be connected to a homeserver that supports that MSC, such as [Synapse](#running-with-synapse).
|
|
||||||
|
|
||||||
## Enabling encryption in Hookshot
|
## Enabling encryption in Hookshot
|
||||||
|
|
||||||
In order for Hookshot to use encryption, it must be configured as follows:
|
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).
|
- The `encryption.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`.
|
- 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` MUST **NOT be configured**.
|
- [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.
|
If you ever reset your homeserver's state, ensure you also reset Hookshot's encryption state. This includes clearing the `storagePath` directory and all worker state stored in your redis instance. Otherwise, Hookshot may fail on start up with registration errors.
|
||||||
|
|
||||||
Also ensure that Hookshot's appservice registration file contains every line from `registration.sample.yml` that appears after the `If enabling encryption` comment. Note that changing the registration file may require restarting the homeserver that Hookshot is connected to.
|
Also ensure that Hookshot's appservice registration file contains every line from `registration.sample.yml` that appears after the `If enabling encryption` comment. Note that changing the registration file may require restarting the homeserver that Hookshot is connected to.
|
||||||
|
|
||||||
## Running with Synapse
|
## Running with Synapse
|
||||||
|
|
||||||
[Synapse](https://github.com/matrix-org/synapse/) has functional support for MSC3202 as of [v1.63.0](https://github.com/matrix-org/synapse/releases/tag/v1.63.0). To enable it, add the following section to Synapse's configuration file (typically named `homeserver.yaml`):
|
[Synapse](https://github.com/matrix-org/synapse/) has functional support for MSC3202 and MSC4203 as of [v1.63.0](https://github.com/matrix-org/synapse/releases/tag/v1.63.0). To enable it, add the following section to Synapse's configuration file (typically named `homeserver.yaml`):
|
||||||
|
|
||||||
|
You may notice that MSC2409 is not listed above. Due to the changes being split out from MSC2409, `msc2409_to_device_messages_enabled` refers to MSC4203.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
experimental_features:
|
experimental_features:
|
||||||
@ -29,3 +34,4 @@ experimental_features:
|
|||||||
msc3202_transaction_extensions: true
|
msc3202_transaction_extensions: true
|
||||||
msc2409_to_device_messages_enabled: true
|
msc2409_to_device_messages_enabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
"jira-client": "^8.2.2",
|
"jira-client": "^8.2.2",
|
||||||
"markdown-it": "^14.0.0",
|
"markdown-it": "^14.0.0",
|
||||||
"matrix-appservice-bridge": "^9.0.1",
|
"matrix-appservice-bridge": "^9.0.1",
|
||||||
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.0-specific-device-2",
|
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@v0.7.1-element.6",
|
||||||
"matrix-widget-api": "^1.6.0",
|
"matrix-widget-api": "^1.6.0",
|
||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
|
@ -26,37 +26,4 @@ describe('Basic test setup', () => {
|
|||||||
// Expect help text.
|
// Expect help text.
|
||||||
expect((await 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.
|
|
||||||
it('should be able to setup a webhook', async () => {
|
|
||||||
const user = testEnv.getUser('user');
|
|
||||||
const testRoomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid] });
|
|
||||||
await user.waitForRoomJoin({sender: testEnv.botMxid, roomId: testRoomId });
|
|
||||||
await user.setUserPowerLevel(testEnv.botMxid, testRoomId, 50);
|
|
||||||
await user.sendText(testRoomId, "!hookshot webhook test-webhook");
|
|
||||||
const inviteResponse = await user.waitForRoomInvite({sender: testEnv.botMxid});
|
|
||||||
await user.waitForRoomEvent<MessageEventContent>({
|
|
||||||
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId,
|
|
||||||
body: 'Room configured to bridge webhooks. See admin room for secret url.'
|
|
||||||
});
|
|
||||||
const webhookUrlMessage = user.waitForRoomEvent<MessageEventContent>({
|
|
||||||
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: inviteResponse.roomId
|
|
||||||
});
|
|
||||||
await user.joinRoom(inviteResponse.roomId);
|
|
||||||
const msgData = (await webhookUrlMessage).data.content.body;
|
|
||||||
const webhookUrl = msgData.split('\n')[2];
|
|
||||||
const webhookNotice = user.waitForRoomEvent<MessageEventContent>({
|
|
||||||
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId, body: 'Hello world!'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send a webhook
|
|
||||||
await fetch(webhookUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({text: 'Hello world!'})
|
|
||||||
});
|
|
||||||
|
|
||||||
// And await the notice.
|
|
||||||
await webhookNotice;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
66
spec/e2ee.spec.ts
Normal file
66
spec/e2ee.spec.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { MessageEventContent } from "matrix-bot-sdk";
|
||||||
|
import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test";
|
||||||
|
import { describe, it, beforeEach, afterEach } from "@jest/globals";
|
||||||
|
|
||||||
|
const CryptoRoomState = [{
|
||||||
|
content: {
|
||||||
|
"algorithm": "m.megolm.v1.aes-sha2"
|
||||||
|
},
|
||||||
|
state_key: "",
|
||||||
|
type: "m.room.encryption"
|
||||||
|
}];
|
||||||
|
|
||||||
|
describe('End-2-End Encryption support', () => {
|
||||||
|
let testEnv: E2ETestEnv;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
testEnv = await E2ETestEnv.createTestEnv({ matrixLocalparts: ['user'], enableE2EE: true });
|
||||||
|
await testEnv.setUp();
|
||||||
|
}, E2ESetupTestTimeout);
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return testEnv?.tearDown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to send the help command', async () => {
|
||||||
|
const user = testEnv.getUser('user');
|
||||||
|
const testRoomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid], initial_state: CryptoRoomState});
|
||||||
|
await user.setUserPowerLevel(testEnv.botMxid, testRoomId, 50);
|
||||||
|
await user.waitForRoomJoin({sender: testEnv.botMxid, roomId: testRoomId });
|
||||||
|
await user.sendText(testRoomId, "!hookshot help");
|
||||||
|
await user.waitForRoomEvent<MessageEventContent>({
|
||||||
|
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should send notices in an encrypted format', async () => {
|
||||||
|
const user = testEnv.getUser('user');
|
||||||
|
const testRoomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid], initial_state: CryptoRoomState});
|
||||||
|
await user.setUserPowerLevel(testEnv.botMxid, testRoomId, 50);
|
||||||
|
await user.waitForRoomJoin({sender: testEnv.botMxid, roomId: testRoomId });
|
||||||
|
await user.sendText(testRoomId, "!hookshot webhook test-webhook");
|
||||||
|
const inviteResponse = await user.waitForRoomInvite({sender: testEnv.botMxid});
|
||||||
|
await user.waitForEncryptedEvent<MessageEventContent>({
|
||||||
|
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId,
|
||||||
|
body: 'Room configured to bridge webhooks. See admin room for secret url.'
|
||||||
|
});
|
||||||
|
const webhookUrlMessage = user.waitForEncryptedEvent<MessageEventContent>({
|
||||||
|
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: inviteResponse.roomId
|
||||||
|
});
|
||||||
|
await user.joinRoom(inviteResponse.roomId);
|
||||||
|
const msgData = (await webhookUrlMessage).data.content.body;
|
||||||
|
const webhookUrl = msgData.split('\n')[2];
|
||||||
|
const webhookNotice = user.waitForEncryptedEvent<MessageEventContent>({
|
||||||
|
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId, body: 'Hello world!'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send a webhook
|
||||||
|
await fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({text: 'Hello world!'})
|
||||||
|
});
|
||||||
|
|
||||||
|
// And await the notice.
|
||||||
|
await webhookNotice;
|
||||||
|
});
|
||||||
|
});
|
@ -5,13 +5,24 @@ import { BridgeConfig, BridgeConfigRoot } from "../../src/config/Config";
|
|||||||
import { start } from "../../src/App/BridgeApp";
|
import { start } from "../../src/App/BridgeApp";
|
||||||
import { RSAKeyPairOptions, generateKeyPair } from "node:crypto";
|
import { RSAKeyPairOptions, generateKeyPair } from "node:crypto";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import Redis from "ioredis";
|
||||||
|
|
||||||
const WAIT_EVENT_TIMEOUT = 10000;
|
const WAIT_EVENT_TIMEOUT = 20000;
|
||||||
export const E2ESetupTestTimeout = 60000;
|
export const E2ESetupTestTimeout = 60000;
|
||||||
|
const REDIS_DATABASE_URI = process.env.HOOKSHOT_E2E_REDIS_DB_URI ?? "redis://localhost:6379";
|
||||||
|
|
||||||
interface Opts {
|
interface Opts {
|
||||||
matrixLocalparts?: string[];
|
matrixLocalparts?: string[];
|
||||||
config?: Partial<BridgeConfigRoot>,
|
config?: Partial<BridgeConfigRoot>,
|
||||||
|
enableE2EE?: boolean,
|
||||||
|
useRedis?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WaitForEventResponse<T extends object = Record<string, unknown>> {
|
||||||
|
roomId: string,
|
||||||
|
data: {
|
||||||
|
sender: string, type: string, state_key?: string, content: T, event_id: string,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class E2ETestMatrixClient extends MatrixClient {
|
export class E2ETestMatrixClient extends MatrixClient {
|
||||||
@ -55,13 +66,10 @@ export class E2ETestMatrixClient extends MatrixClient {
|
|||||||
}, `Timed out waiting for powerlevel from in ${roomId}`)
|
}, `Timed out waiting for powerlevel from in ${roomId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async waitForRoomEvent<T extends object = Record<string, unknown>>(
|
private async innerWaitForRoomEvent<T extends object = Record<string, unknown>>(
|
||||||
opts: {eventType: string, sender: string, roomId?: string, stateKey?: string, body?: string}
|
{eventType, sender, roomId, stateKey, eventId, body}: {eventType: string, sender: string, roomId?: string, stateKey?: string, body?: string, eventId?: string}, expectEncrypted: boolean,
|
||||||
): Promise<{roomId: string, data: {
|
): Promise<WaitForEventResponse<T>> {
|
||||||
sender: string, type: string, state_key?: string, content: T, event_id: string,
|
return this.waitForEvent(expectEncrypted ? 'room.decrypted_event' : 'room.event', (eventRoomId: string, eventData: {
|
||||||
}}> {
|
|
||||||
const {eventType, sender, roomId, stateKey} = opts;
|
|
||||||
return this.waitForEvent('room.event', (eventRoomId: string, eventData: {
|
|
||||||
sender: string, type: string, state_key?: string, content: T, event_id: string,
|
sender: string, type: string, state_key?: string, content: T, event_id: string,
|
||||||
}) => {
|
}) => {
|
||||||
if (eventData.sender !== sender) {
|
if (eventData.sender !== sender) {
|
||||||
@ -73,21 +81,36 @@ export class E2ETestMatrixClient extends MatrixClient {
|
|||||||
if (roomId && eventRoomId !== roomId) {
|
if (roomId && eventRoomId !== roomId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
if (eventId && eventData.event_id !== eventId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (stateKey !== undefined && eventData.state_key !== stateKey) {
|
if (stateKey !== undefined && eventData.state_key !== stateKey) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const body = 'body' in eventData.content && eventData.content.body;
|
const evtBody = 'body' in eventData.content && eventData.content.body;
|
||||||
if (opts.body && body !== opts.body) {
|
if (body && body !== evtBody) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
console.info(
|
console.info(
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
`${eventRoomId} ${eventData.event_id} ${eventData.type} ${eventData.sender} ${eventData.state_key ?? body ?? ''}`
|
`${eventRoomId} ${eventData.event_id} ${eventData.type} ${eventData.sender} ${eventData.state_key ?? evtBody ?? ''}`
|
||||||
);
|
);
|
||||||
return {roomId: eventRoomId, data: eventData};
|
return {roomId: eventRoomId, data: eventData};
|
||||||
}, `Timed out waiting for ${eventType} from ${sender} in ${roomId || "any room"}`)
|
}, `Timed out waiting for ${eventType} from ${sender} in ${roomId || "any room"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async waitForRoomEvent<T extends object = Record<string, unknown>>(
|
||||||
|
opts: Parameters<E2ETestMatrixClient["innerWaitForRoomEvent"]>[0]
|
||||||
|
): Promise<WaitForEventResponse<T>> {
|
||||||
|
return this.innerWaitForRoomEvent(opts, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async waitForEncryptedEvent<T extends object = Record<string, unknown>>(
|
||||||
|
opts: Parameters<E2ETestMatrixClient["innerWaitForRoomEvent"]>[0]
|
||||||
|
): Promise<WaitForEventResponse<T>> {
|
||||||
|
return this.innerWaitForRoomEvent(opts, true);
|
||||||
|
}
|
||||||
|
|
||||||
public async waitForRoomJoin(
|
public async waitForRoomJoin(
|
||||||
opts: {sender: string, roomId?: string}
|
opts: {sender: string, roomId?: string}
|
||||||
): Promise<{roomId: string, data: unknown}> {
|
): Promise<{roomId: string, data: unknown}> {
|
||||||
@ -173,10 +196,11 @@ export class E2ETestEnv {
|
|||||||
if (err) { reject(err) } else { resolve(privateKey) }
|
if (err) { reject(err) } else { resolve(privateKey) }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const dir = await mkdtemp('hookshot-int-test');
|
||||||
|
|
||||||
// Configure homeserver and bots
|
// Configure homeserver and bots
|
||||||
const [homeserver, dir, privateKey] = await Promise.all([
|
const [homeserver, privateKey] = await Promise.all([
|
||||||
createHS([...matrixLocalparts || []], workerID),
|
createHS([...matrixLocalparts || []], workerID, opts.enableE2EE ? path.join(dir, 'client-crypto') : undefined),
|
||||||
mkdtemp('hookshot-int-test'),
|
|
||||||
keyPromise,
|
keyPromise,
|
||||||
]);
|
]);
|
||||||
const keyPath = path.join(dir, 'key.pem');
|
const keyPath = path.join(dir, 'key.pem');
|
||||||
@ -193,6 +217,15 @@ export class E2ETestEnv {
|
|||||||
providedConfig.github.auth.privateKeyFile = keyPath;
|
providedConfig.github.auth.privateKeyFile = keyPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opts.useRedis = opts.enableE2EE || opts.useRedis;
|
||||||
|
|
||||||
|
let cacheConfig: BridgeConfigRoot["cache"]|undefined;
|
||||||
|
if (opts.useRedis) {
|
||||||
|
cacheConfig = {
|
||||||
|
redisUri: `${REDIS_DATABASE_URI}/${workerID}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const config = new BridgeConfig({
|
const config = new BridgeConfig({
|
||||||
bridge: {
|
bridge: {
|
||||||
domain: homeserver.domain,
|
domain: homeserver.domain,
|
||||||
@ -201,7 +234,7 @@ export class E2ETestEnv {
|
|||||||
bindAddress: '0.0.0.0',
|
bindAddress: '0.0.0.0',
|
||||||
},
|
},
|
||||||
logging: {
|
logging: {
|
||||||
level: 'info',
|
level: 'debug',
|
||||||
},
|
},
|
||||||
// Always enable webhooks so that hookshot starts.
|
// Always enable webhooks so that hookshot starts.
|
||||||
generic: {
|
generic: {
|
||||||
@ -214,6 +247,12 @@ export class E2ETestEnv {
|
|||||||
resources: ['webhooks'],
|
resources: ['webhooks'],
|
||||||
}],
|
}],
|
||||||
passFile: keyPath,
|
passFile: keyPath,
|
||||||
|
...(opts.enableE2EE ? {
|
||||||
|
encryption: {
|
||||||
|
storagePath: path.join(dir, 'crypto-store'),
|
||||||
|
}
|
||||||
|
} : undefined),
|
||||||
|
cache: cacheConfig,
|
||||||
...providedConfig,
|
...providedConfig,
|
||||||
});
|
});
|
||||||
const registration: IAppserviceRegistration = {
|
const registration: IAppserviceRegistration = {
|
||||||
@ -227,7 +266,8 @@ export class E2ETestEnv {
|
|||||||
}],
|
}],
|
||||||
rooms: [],
|
rooms: [],
|
||||||
aliases: [],
|
aliases: [],
|
||||||
}
|
},
|
||||||
|
"de.sorunome.msc2409.push_ephemeral": true
|
||||||
};
|
};
|
||||||
const app = await start(config, registration);
|
const app = await start(config, registration);
|
||||||
app.listener.finaliseListeners();
|
app.listener.finaliseListeners();
|
||||||
@ -255,6 +295,12 @@ export class E2ETestEnv {
|
|||||||
await this.app.bridgeApp.stop();
|
await this.app.bridgeApp.stop();
|
||||||
await this.app.listener.stop();
|
await this.app.listener.stop();
|
||||||
await this.app.storage.disconnect?.();
|
await this.app.storage.disconnect?.();
|
||||||
|
|
||||||
|
// Clear the redis DB.
|
||||||
|
if (this.config.cache?.redisUri) {
|
||||||
|
await new Redis(this.config.cache.redisUri).flushdb();
|
||||||
|
}
|
||||||
|
|
||||||
this.homeserver.users.forEach(u => u.client.stop());
|
this.homeserver.users.forEach(u => u.client.stop());
|
||||||
await destroyHS(this.homeserver.id);
|
await destroyHS(this.homeserver.id);
|
||||||
await rm(this.dir, { recursive: true });
|
await rm(this.dir, { recursive: true });
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { MatrixClient } from "matrix-bot-sdk";
|
import { MatrixClient, MemoryStorageProvider, RustSdkCryptoStorageProvider, RustSdkCryptoStoreType } from "matrix-bot-sdk";
|
||||||
import { createHash, createHmac, randomUUID } from "crypto";
|
import { createHash, createHmac, randomUUID } from "crypto";
|
||||||
import { Homerunner } from "homerunner-client";
|
import { Homerunner } from "homerunner-client";
|
||||||
import { E2ETestMatrixClient } from "./e2e-test";
|
import { E2ETestMatrixClient } from "./e2e-test";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
const HOMERUNNER_IMAGE = process.env.HOMERUNNER_IMAGE || 'ghcr.io/element-hq/synapse/complement-synapse:latest';
|
const HOMERUNNER_IMAGE = process.env.HOMERUNNER_IMAGE || 'ghcr.io/element-hq/synapse/complement-synapse:nightly';
|
||||||
export const DEFAULT_REGISTRATION_SHARED_SECRET = (
|
export const DEFAULT_REGISTRATION_SHARED_SECRET = (
|
||||||
process.env.REGISTRATION_SHARED_SECRET || 'complement'
|
process.env.REGISTRATION_SHARED_SECRET || 'complement'
|
||||||
);
|
);
|
||||||
@ -41,7 +42,7 @@ async function waitForHomerunner() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createHS(localparts: string[] = [], workerId: number): Promise<ComplementHomeServer> {
|
export async function createHS(localparts: string[] = [], workerId: number, cryptoRootPath?: string): Promise<ComplementHomeServer> {
|
||||||
await waitForHomerunner();
|
await waitForHomerunner();
|
||||||
|
|
||||||
const appPort = 49600 + workerId;
|
const appPort = 49600 + workerId;
|
||||||
@ -61,25 +62,35 @@ export async function createHS(localparts: string[] = [], workerId: number): Pro
|
|||||||
SenderLocalpart: 'hookshot',
|
SenderLocalpart: 'hookshot',
|
||||||
RateLimited: false,
|
RateLimited: false,
|
||||||
...{ASToken: asToken,
|
...{ASToken: asToken,
|
||||||
HSToken: hsToken},
|
HSToken: hsToken,
|
||||||
|
SendEphemeral: true,
|
||||||
|
EnableEncryption: true},
|
||||||
}]
|
}]
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const [homeserverName, homeserver] = Object.entries(blueprintResponse.homeservers)[0];
|
const [homeserverName, homeserver] = Object.entries(blueprintResponse.homeservers)[0];
|
||||||
// Skip AS user.
|
// Skip AS user.
|
||||||
const users = Object.entries(homeserver.AccessTokens)
|
const users = await Promise.all(Object.entries(homeserver.AccessTokens)
|
||||||
.filter(([_uId, accessToken]) => accessToken !== asToken)
|
.filter(([_uId, accessToken]) => accessToken !== asToken)
|
||||||
.map(([userId, accessToken]) => ({
|
.map(async ([userId, accessToken]) => {
|
||||||
userId: userId,
|
const cryptoStore = cryptoRootPath ? new RustSdkCryptoStorageProvider(path.join(cryptoRootPath, userId), RustSdkCryptoStoreType.Sqlite) : undefined;
|
||||||
accessToken,
|
const client = new E2ETestMatrixClient(homeserver.BaseURL, accessToken, new MemoryStorageProvider(), cryptoStore);
|
||||||
deviceId: homeserver.DeviceIDs[userId],
|
if (cryptoStore) {
|
||||||
client: new E2ETestMatrixClient(homeserver.BaseURL, accessToken),
|
await client.crypto.prepare();
|
||||||
})
|
}
|
||||||
);
|
// Start syncing proactively.
|
||||||
|
await client.start();
|
||||||
|
return {
|
||||||
|
userId: userId,
|
||||||
|
accessToken,
|
||||||
|
deviceId: homeserver.DeviceIDs[userId],
|
||||||
|
client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
|
||||||
// Start syncing proactively.
|
|
||||||
await Promise.all(users.map(u => u.client.start()));
|
|
||||||
return {
|
return {
|
||||||
users,
|
users,
|
||||||
id: blueprint,
|
id: blueprint,
|
||||||
@ -119,7 +130,7 @@ export async function registerUser(
|
|||||||
.update(password).update("\x00")
|
.update(password).update("\x00")
|
||||||
.update(user.admin ? 'admin' : 'notadmin')
|
.update(user.admin ? 'admin' : 'notadmin')
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
return await fetch(registerUrl, { method: "POST", body: JSON.stringify(
|
const req = await fetch(registerUrl, { method: "POST", body: JSON.stringify(
|
||||||
{
|
{
|
||||||
nonce,
|
nonce,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
@ -127,8 +138,10 @@ export async function registerUser(
|
|||||||
admin: user.admin,
|
admin: user.admin,
|
||||||
mac: hmac,
|
mac: hmac,
|
||||||
}
|
}
|
||||||
)}).then(res => res.json()).then(res => ({
|
)});
|
||||||
mxid: (res as {user_id: string}).user_id,
|
const res = await req.json() as {user_id: string, access_token: string};
|
||||||
client: new MatrixClient(homeserverUrl, (res as {access_token: string}).access_token),
|
return {
|
||||||
})).catch(err => { console.log(err.response.body); throw new Error(`Failed to register user: ${err}`); });
|
mxid: res.user_id,
|
||||||
|
client: new MatrixClient(homeserverUrl, res.access_token),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -171,13 +171,10 @@ export default class BotUsersManager {
|
|||||||
// Determine if an avatar update is needed
|
// Determine if an avatar update is needed
|
||||||
if (profile.avatar_url) {
|
if (profile.avatar_url) {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(
|
const res = await botUser.intent.underlyingClient.downloadContent(profile.avatar_url);
|
||||||
botUser.intent.underlyingClient.mxcToHttp(profile.avatar_url),
|
|
||||||
{ responseType: "arraybuffer" },
|
|
||||||
);
|
|
||||||
const currentAvatarImage = {
|
const currentAvatarImage = {
|
||||||
image: Buffer.from(res.data),
|
image: res.data,
|
||||||
contentType: res.headers["content-type"],
|
contentType: res.contentType,
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
currentAvatarImage.image.equals(avatarImage.image)
|
currentAvatarImage.image.equals(avatarImage.image)
|
||||||
|
@ -104,7 +104,7 @@ export class RedisStorageProvider extends RedisStorageContextualProvider impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addRegisteredUser(userId: string) {
|
public async addRegisteredUser(userId: string) {
|
||||||
this.redis.sadd(REGISTERED_USERS_KEY, [userId]);
|
await this.redis.sadd(REGISTERED_USERS_KEY, [userId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isUserRegistered(userId: string): Promise<boolean> {
|
public async isUserRegistered(userId: string): Promise<boolean> {
|
||||||
@ -112,7 +112,7 @@ export class RedisStorageProvider extends RedisStorageContextualProvider impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async setTransactionCompleted(transactionId: string) {
|
public async setTransactionCompleted(transactionId: string) {
|
||||||
this.redis.sadd(COMPLETED_TRANSACTIONS_KEY, [transactionId]);
|
await this.redis.sadd(COMPLETED_TRANSACTIONS_KEY, [transactionId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isTransactionCompleted(transactionId: string): Promise<boolean> {
|
public async isTransactionCompleted(transactionId: string): Promise<boolean> {
|
||||||
|
@ -45,7 +45,7 @@ export function getAppservice(config: BridgeConfig, registration: IAppserviceReg
|
|||||||
},
|
},
|
||||||
storage: storage,
|
storage: storage,
|
||||||
intentOptions: {
|
intentOptions: {
|
||||||
encryption: !!config.encryption,
|
encryption: !!cryptoStorage,
|
||||||
},
|
},
|
||||||
cryptoStorage: cryptoStorage,
|
cryptoStorage: cryptoStorage,
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ import { Logger } from "matrix-appservice-bridge";
|
|||||||
import { BridgeConfigCache } from "./sections/cache";
|
import { BridgeConfigCache } from "./sections/cache";
|
||||||
import { BridgeConfigGenericWebhooks, BridgeConfigQueue, BridgeGenericWebhooksConfigYAML } from "./sections";
|
import { BridgeConfigGenericWebhooks, BridgeConfigQueue, BridgeGenericWebhooksConfigYAML } from "./sections";
|
||||||
import { GenericHookServiceConfig } from "../Connections";
|
import { GenericHookServiceConfig } from "../Connections";
|
||||||
|
import { BridgeConfigEncryption } from "./sections/encryption";
|
||||||
|
|
||||||
const log = new Logger("Config");
|
const log = new Logger("Config");
|
||||||
|
|
||||||
@ -356,8 +357,6 @@ interface BridgeConfigBridge {
|
|||||||
mediaUrl?: string;
|
mediaUrl?: string;
|
||||||
port: number;
|
port: number;
|
||||||
bindAddress: string;
|
bindAddress: string;
|
||||||
// Removed
|
|
||||||
pantalaimon?: never;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BridgeConfigWebhook {
|
interface BridgeConfigWebhook {
|
||||||
@ -376,10 +375,7 @@ interface BridgeConfigBot {
|
|||||||
displayname?: string;
|
displayname?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
}
|
}
|
||||||
interface BridgeConfigEncryption {
|
|
||||||
storagePath: string;
|
|
||||||
useLegacySledStore: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BridgeConfigServiceBot {
|
export interface BridgeConfigServiceBot {
|
||||||
localpart: string;
|
localpart: string;
|
||||||
@ -415,7 +411,11 @@ export interface BridgeConfigRoot {
|
|||||||
bot?: BridgeConfigBot;
|
bot?: BridgeConfigBot;
|
||||||
bridge: BridgeConfigBridge;
|
bridge: BridgeConfigBridge;
|
||||||
cache?: BridgeConfigCache;
|
cache?: BridgeConfigCache;
|
||||||
experimentalEncryption?: BridgeConfigEncryption;
|
/**
|
||||||
|
* @deprecated Old, unsupported encryption propety.
|
||||||
|
*/
|
||||||
|
experimentalEncryption?: never;
|
||||||
|
encryption?: BridgeConfigEncryption;
|
||||||
feeds?: BridgeConfigFeedsYAML;
|
feeds?: BridgeConfigFeedsYAML;
|
||||||
figma?: BridgeConfigFigma;
|
figma?: BridgeConfigFigma;
|
||||||
generic?: BridgeGenericWebhooksConfigYAML;
|
generic?: BridgeGenericWebhooksConfigYAML;
|
||||||
@ -443,9 +443,7 @@ export class BridgeConfig {
|
|||||||
For encryption to work, this must be configured.`, true)
|
For encryption to work, this must be configured.`, true)
|
||||||
public readonly cache?: BridgeConfigCache;
|
public readonly cache?: BridgeConfigCache;
|
||||||
@configKey(`Configuration for encryption support in the bridge.
|
@configKey(`Configuration for encryption support in the bridge.
|
||||||
If omitted, encryption support will be disabled.
|
If omitted, encryption support will be disabled.`, true)
|
||||||
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;
|
public readonly encryption?: BridgeConfigEncryption;
|
||||||
@configKey(`Message queue configuration options for large scale deployments.
|
@configKey(`Message queue configuration options for large scale deployments.
|
||||||
For encryption to work, this must not be configured.`, true)
|
For encryption to work, this must not be configured.`, true)
|
||||||
@ -500,6 +498,9 @@ export class BridgeConfig {
|
|||||||
|
|
||||||
|
|
||||||
constructor(configData: BridgeConfigRoot, env?: {[key: string]: string|undefined}) {
|
constructor(configData: BridgeConfigRoot, env?: {[key: string]: string|undefined}) {
|
||||||
|
this.logging = configData.logging || {
|
||||||
|
level: "info",
|
||||||
|
}
|
||||||
this.bridge = configData.bridge;
|
this.bridge = configData.bridge;
|
||||||
assert.ok(this.bridge);
|
assert.ok(this.bridge);
|
||||||
this.github = configData.github && new BridgeConfigGitHub(configData.github);
|
this.github = configData.github && new BridgeConfigGitHub(configData.github);
|
||||||
@ -548,13 +549,11 @@ export class BridgeConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.encryption = configData.experimentalEncryption;
|
if (configData.experimentalEncryption) {
|
||||||
|
throw new ConfigError("experimentalEncryption", `This key is now called 'encryption'. Please adjust your config file.`)
|
||||||
|
|
||||||
this.logging = configData.logging || {
|
|
||||||
level: "info",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.encryption = configData.encryption && new BridgeConfigEncryption(configData.encryption, this.cache, this.queue);
|
||||||
this.widgets = configData.widgets && new BridgeWidgetConfig(configData.widgets);
|
this.widgets = configData.widgets && new BridgeWidgetConfig(configData.widgets);
|
||||||
this.sentry = configData.sentry;
|
this.sentry = configData.sentry;
|
||||||
|
|
||||||
@ -640,37 +639,6 @@ export class BridgeConfig {
|
|||||||
log.warn("The `widgets.openIdOverrides` config value SHOULD NOT be used in a production environment.")
|
log.warn("The `widgets.openIdOverrides` config value SHOULD NOT be used in a production environment.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.bridge.pantalaimon) {
|
|
||||||
throw new ConfigError("bridge.pantalaimon", "Pantalaimon support has been removed. Encrypted bridges should now use the `experimentalEncryption` config option");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.encryption) {
|
|
||||||
log.warn(`
|
|
||||||
You have enabled encryption support in the bridge. This feature is HIGHLY EXPERIMENTAL AND SUBJECT TO CHANGE.
|
|
||||||
For more details, see https://github.com/matrix-org/matrix-hookshot/issues/594.
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (!this.encryption.storagePath) {
|
|
||||||
throw new ConfigError("experimentalEncryption.storagePath", "The crypto storage path must not be empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.encryption.useLegacySledStore) {
|
|
||||||
throw new ConfigError(
|
|
||||||
"experimentalEncryption.useLegacySledStore", `
|
|
||||||
The Sled crypto store format is no longer supported.
|
|
||||||
Please back up your crypto store at ${this.encryption.storagePath},
|
|
||||||
remove "useLegacySledStore" from your configuration file, and restart Hookshot.
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
if (!this.cache) {
|
|
||||||
throw new ConfigError("cache", "Encryption requires the Redis cache to be enabled.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.queue) {
|
|
||||||
throw new ConfigError("queue", "Encryption does not support message queues.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.figma?.overrideUserId) {
|
if (this.figma?.overrideUserId) {
|
||||||
log.warn("The `figma.overrideUserId` config value is deprecated. A service bot should be configured instead.");
|
log.warn("The `figma.overrideUserId` config value is deprecated. A service bot should be configured instead.");
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,6 @@ export const DefaultConfigRoot: BridgeConfigRoot = {
|
|||||||
port: 9993,
|
port: 9993,
|
||||||
bindAddress: "127.0.0.1",
|
bindAddress: "127.0.0.1",
|
||||||
},
|
},
|
||||||
queue: {
|
|
||||||
redisUri: "redis://localhost:6379",
|
|
||||||
},
|
|
||||||
cache: {
|
cache: {
|
||||||
redisUri: "redis://localhost:6379",
|
redisUri: "redis://localhost:6379",
|
||||||
},
|
},
|
||||||
@ -154,6 +151,9 @@ export const DefaultConfigRoot: BridgeConfigRoot = {
|
|||||||
sentry: {
|
sentry: {
|
||||||
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
|
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
|
||||||
environment: "production"
|
environment: "production"
|
||||||
|
},
|
||||||
|
encryption: {
|
||||||
|
storagePath: "./cryptostore"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
27
src/config/sections/encryption.ts
Normal file
27
src/config/sections/encryption.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ConfigError } from "../../errors";
|
||||||
|
import { configKey } from "../Decorators";
|
||||||
|
import { BridgeConfigCache } from "./cache";
|
||||||
|
import { BridgeConfigQueue } from "./queue";
|
||||||
|
|
||||||
|
interface BridgeConfigEncryptionYAML {
|
||||||
|
storagePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BridgeConfigEncryption {
|
||||||
|
@configKey("Path to the directory used to store encryption files. These files must be persist between restarts of the service.")
|
||||||
|
public readonly storagePath: string;
|
||||||
|
|
||||||
|
constructor(config: BridgeConfigEncryptionYAML, cache: unknown|undefined, queue: unknown|undefined) {
|
||||||
|
if (typeof config.storagePath !== "string" || !config.storagePath) {
|
||||||
|
throw new ConfigError("encryption.storagePath", "The crypto storage path must not be empty.");
|
||||||
|
}
|
||||||
|
this.storagePath = config.storagePath;
|
||||||
|
|
||||||
|
if (!cache) {
|
||||||
|
throw new ConfigError("cache", "Encryption requires the Redis cache to be enabled.");
|
||||||
|
}
|
||||||
|
if (queue) {
|
||||||
|
throw new ConfigError("queue", "Encryption does not support message queues.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -60,7 +60,7 @@ fn parse_channel_to_js_result(channel: &Channel) -> JsRssChannel {
|
|||||||
.and_then(|i| i.permalink.then(|| i.value.to_string()))
|
.and_then(|i| i.permalink.then(|| i.value.to_string()))
|
||||||
}),
|
}),
|
||||||
id: item.guid().map(|f| f.value().to_string()),
|
id: item.guid().map(|f| f.value().to_string()),
|
||||||
id_is_permalink: item.guid().map_or(false, |f| f.is_permalink()),
|
id_is_permalink: item.guid().is_some_and(|f| f.is_permalink()),
|
||||||
pubdate: item.pub_date().map(String::from),
|
pubdate: item.pub_date().map(String::from),
|
||||||
summary: item.description().map(String::from),
|
summary: item.description().map(String::from),
|
||||||
author: item.author().map(String::from),
|
author: item.author().map(String::from),
|
||||||
@ -106,7 +106,7 @@ fn parse_feed_to_js_result(feed: &Feed) -> JsRssChannel {
|
|||||||
link: item
|
link: item
|
||||||
.links()
|
.links()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|l| l.mime_type.as_ref().map_or(false, |t| t == "text/html"))
|
.find(|l| l.mime_type.as_ref().is_some_and(|t| t == "text/html"))
|
||||||
.or_else(|| item.links().first())
|
.or_else(|| item.links().first())
|
||||||
.map(|f| f.href.clone()),
|
.map(|f| f.href.clone()),
|
||||||
id: Some(item.id.clone()),
|
id: Some(item.id.clone()),
|
||||||
|
@ -25,7 +25,7 @@ describe("IntentUtils", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
expect(roomId).to.equal(ROOM_ID);
|
expect(roomId).to.equal(ROOM_ID);
|
||||||
throw new MatrixError({ errcode: "M_FORBIDDEN", error: "Test forced error"}, 401)
|
throw new MatrixError({ errcode: "M_FORBIDDEN", error: "Test forced error"}, 401, { })
|
||||||
};
|
};
|
||||||
|
|
||||||
// This should invite the puppet user.
|
// This should invite the puppet user.
|
||||||
@ -46,7 +46,7 @@ describe("IntentUtils", () => {
|
|||||||
|
|
||||||
// This should fail the first time, then pass once we've tried to invite the user
|
// This should fail the first time, then pass once we've tried to invite the user
|
||||||
targetIntent.ensureJoined = () => {
|
targetIntent.ensureJoined = () => {
|
||||||
throw new MatrixError({ errcode: "FORCED_FAILURE", error: "Test forced error"}, 500)
|
throw new MatrixError({ errcode: "FORCED_FAILURE", error: "Test forced error"}, 500, { })
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
ensureUserIsInRoom(targetIntent, matrixClient, ROOM_ID);
|
ensureUserIsInRoom(targetIntent, matrixClient, ROOM_ID);
|
||||||
|
@ -38,9 +38,13 @@ describe("Config/BridgeConfig", () => {
|
|||||||
expect(config.cache?.redisUri).to.equal("redis://bark:6379");
|
expect(config.cache?.redisUri).to.equal("redis://bark:6379");
|
||||||
});
|
});
|
||||||
it("with monolithic disabled", () => {
|
it("with monolithic disabled", () => {
|
||||||
const config = new BridgeConfig({ ...DefaultConfigRoot, queue: {
|
const config = new BridgeConfig({
|
||||||
monolithic: false
|
...DefaultConfigRoot,
|
||||||
}});
|
encryption: undefined,
|
||||||
|
queue: {
|
||||||
|
monolithic: false
|
||||||
|
}
|
||||||
|
});
|
||||||
expect(config.queue).to.deep.equal({
|
expect(config.queue).to.deep.equal({
|
||||||
monolithic: false,
|
monolithic: false,
|
||||||
});
|
});
|
||||||
@ -49,9 +53,13 @@ describe("Config/BridgeConfig", () => {
|
|||||||
});
|
});
|
||||||
describe("will handle the queue option", () => {
|
describe("will handle the queue option", () => {
|
||||||
it("with redisUri", () => {
|
it("with redisUri", () => {
|
||||||
const config = new BridgeConfig({ ...DefaultConfigRoot, queue: {
|
const config = new BridgeConfig({ ...DefaultConfigRoot,
|
||||||
redisUri: "redis://localhost:6379"
|
encryption: undefined,
|
||||||
}, cache: undefined});
|
queue: {
|
||||||
|
redisUri: "redis://localhost:6379"
|
||||||
|
},
|
||||||
|
cache: undefined
|
||||||
|
});
|
||||||
expect(config.queue).to.deep.equal({
|
expect(config.queue).to.deep.equal({
|
||||||
redisUri: "redis://localhost:6379"
|
redisUri: "redis://localhost:6379"
|
||||||
});
|
});
|
||||||
|
@ -310,7 +310,7 @@ describe("GenericHookConnection", () => {
|
|||||||
return roomId;
|
return roomId;
|
||||||
}
|
}
|
||||||
expect(roomId).to.equal(ROOM_ID);
|
expect(roomId).to.equal(ROOM_ID);
|
||||||
throw new MatrixError({ errcode: "M_FORBIDDEN", error: "Test forced error"}, 401)
|
throw new MatrixError({ errcode: "M_FORBIDDEN", error: "Test forced error"}, 401, { })
|
||||||
};
|
};
|
||||||
|
|
||||||
// This should invite the puppet user.
|
// This should invite the puppet user.
|
||||||
@ -333,7 +333,7 @@ describe("GenericHookConnection", () => {
|
|||||||
|
|
||||||
// This should fail the first time, then pass once we've tried to invite the user
|
// This should fail the first time, then pass once we've tried to invite the user
|
||||||
intent.ensureJoined = () => {
|
intent.ensureJoined = () => {
|
||||||
throw new MatrixError({ errcode: "FORCED_FAILURE", error: "Test forced error"}, 500)
|
throw new MatrixError({ errcode: "FORCED_FAILURE", error: "Test forced error"}, 500, { })
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
// regression test covering https://github.com/matrix-org/matrix-hookshot/issues/625
|
// regression test covering https://github.com/matrix-org/matrix-hookshot/issues/625
|
||||||
|
@ -39,7 +39,7 @@ export class MatrixClientMock {
|
|||||||
throw new MatrixError({
|
throw new MatrixError({
|
||||||
errcode: 'M_NOT_FOUND',
|
errcode: 'M_NOT_FOUND',
|
||||||
error: 'Test error: No account data',
|
error: 'Test error: No account data',
|
||||||
}, 404);
|
}, 404, { });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRoomAccountData(key: string, roomId: string, value: string): Promise<void> {
|
async setRoomAccountData(key: string, roomId: string, value: string): Promise<void> {
|
||||||
|
30
yarn.lock
30
yarn.lock
@ -1027,14 +1027,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@lezer/common" "^1.0.0"
|
"@lezer/common" "^1.0.0"
|
||||||
|
|
||||||
"@matrix-org/matrix-sdk-crypto-nodejs@0.1.0-beta.11":
|
|
||||||
version "0.1.0-beta.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-nodejs/-/matrix-sdk-crypto-nodejs-0.1.0-beta.11.tgz#537cd7a7bbce1d9745b812a5a7ffa9a5944e146c"
|
|
||||||
integrity sha512-z5adcQo4o0UAry4zs6JHGxbTDlYTUMKUfpOpigmso65ETBDumbeTSQCWRw8UeUV7aCAyVoHARqDTol9SrauEFA==
|
|
||||||
dependencies:
|
|
||||||
https-proxy-agent "^5.0.1"
|
|
||||||
node-downloader-helper "^2.1.5"
|
|
||||||
|
|
||||||
"@matrix-org/matrix-sdk-crypto-nodejs@0.1.0-beta.6":
|
"@matrix-org/matrix-sdk-crypto-nodejs@0.1.0-beta.6":
|
||||||
version "0.1.0-beta.6"
|
version "0.1.0-beta.6"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-nodejs/-/matrix-sdk-crypto-nodejs-0.1.0-beta.6.tgz#0ecae51103ee3c107af0d6d0738f33eb7cc9857e"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-nodejs/-/matrix-sdk-crypto-nodejs-0.1.0-beta.6.tgz#0ecae51103ee3c107af0d6d0738f33eb7cc9857e"
|
||||||
@ -1043,6 +1035,14 @@
|
|||||||
https-proxy-agent "^5.0.1"
|
https-proxy-agent "^5.0.1"
|
||||||
node-downloader-helper "^2.1.5"
|
node-downloader-helper "^2.1.5"
|
||||||
|
|
||||||
|
"@matrix-org/matrix-sdk-crypto-nodejs@0.2.0-beta.1":
|
||||||
|
version "0.2.0-beta.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-nodejs/-/matrix-sdk-crypto-nodejs-0.2.0-beta.1.tgz#b696707ccfa944cfed3c96cf7e54799b0f1e3329"
|
||||||
|
integrity sha512-CgbOKORfD6dvYgQTPhfN73H1RbQknrFkMnRRwCIJMt15iL2AF1gEowgbrlGhkbG6gNng4CgPnKs1iHKCRrhvmA==
|
||||||
|
dependencies:
|
||||||
|
https-proxy-agent "^5.0.1"
|
||||||
|
node-downloader-helper "^2.1.5"
|
||||||
|
|
||||||
"@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.3.13":
|
"@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.3.13":
|
||||||
version "5.5.1"
|
version "5.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.5.1.tgz#cd480874a4ebb97010f488feb8204ac035a86332"
|
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.5.1.tgz#cd480874a4ebb97010f488feb8204ac035a86332"
|
||||||
@ -1933,7 +1933,7 @@
|
|||||||
"@types/range-parser" "*"
|
"@types/range-parser" "*"
|
||||||
"@types/send" "*"
|
"@types/send" "*"
|
||||||
|
|
||||||
"@types/express@^4.17.13", "@types/express@^4.17.14", "@types/express@^4.17.20":
|
"@types/express@^4.17.13", "@types/express@^4.17.14", "@types/express@^4.17.21":
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
|
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
|
||||||
integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
|
integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
|
||||||
@ -5949,13 +5949,13 @@ matrix-appservice@^2.0.0:
|
|||||||
request-promise "^4.2.6"
|
request-promise "^4.2.6"
|
||||||
sanitize-html "^2.8.0"
|
sanitize-html "^2.8.0"
|
||||||
|
|
||||||
"matrix-bot-sdk@npm:@vector-im/matrix-bot-sdk@^0.7.0-specific-device-2":
|
"matrix-bot-sdk@npm:@vector-im/matrix-bot-sdk@v0.7.1-element.6":
|
||||||
version "0.7.0-specific-device-2"
|
version "0.7.1-element.6"
|
||||||
resolved "https://registry.yarnpkg.com/@vector-im/matrix-bot-sdk/-/matrix-bot-sdk-0.7.0-specific-device-2.tgz#3682e14708979a6f24cc19f3103e3292ed08bbea"
|
resolved "https://registry.yarnpkg.com/@vector-im/matrix-bot-sdk/-/matrix-bot-sdk-0.7.1-element.6.tgz#d1f8a86d3bd60084d92d150f42a48b25199871e1"
|
||||||
integrity sha512-97h2tIlcK6/3wEuLN3x6/LM9TITVISnnbjUo/9nVbqkDvSQ2TNFURxfAqjUfFmgQwo0o3KnhvaS6dZITrBvj6A==
|
integrity sha512-0KfyTpQV5eyY4vPUZW89t7EZf1YF0UyFkyYqpsxL/6S7XIlbTMC4onod7vx/QpKC0lSREmwIiXx2JSjExP6CIw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@matrix-org/matrix-sdk-crypto-nodejs" "0.1.0-beta.11"
|
"@matrix-org/matrix-sdk-crypto-nodejs" "0.2.0-beta.1"
|
||||||
"@types/express" "^4.17.20"
|
"@types/express" "^4.17.21"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
async-lock "^1.4.0"
|
async-lock "^1.4.0"
|
||||||
chalk "4"
|
chalk "4"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user