You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-09 10:22:46 +03:00
Remove abandoned MSC3886, MSC3903, MSC3906 implementations (#4469)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
committed by
GitHub
parent
8b8ee91210
commit
23c4c9fd8a
@@ -1,276 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import {
|
||||
RendezvousChannel,
|
||||
RendezvousFailureListener,
|
||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||
RendezvousIntent,
|
||||
} from "./index.ts";
|
||||
import { MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client.ts";
|
||||
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature.ts";
|
||||
import { logger } from "../logger.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import { CrossSigningKey } from "../crypto-api/index.ts";
|
||||
import { Capabilities, Device, IGetLoginTokenCapability } from "../matrix.ts";
|
||||
|
||||
enum PayloadType {
|
||||
Start = "m.login.start",
|
||||
Finish = "m.login.finish",
|
||||
Progress = "m.login.progress",
|
||||
}
|
||||
|
||||
enum Outcome {
|
||||
Success = "success",
|
||||
Failure = "failure",
|
||||
Verified = "verified",
|
||||
Declined = "declined",
|
||||
Unsupported = "unsupported",
|
||||
}
|
||||
|
||||
export interface MSC3906RendezvousPayload {
|
||||
type: PayloadType;
|
||||
intent?: RendezvousIntent;
|
||||
outcome?: Outcome;
|
||||
device_id?: string;
|
||||
device_key?: string;
|
||||
verifying_device_id?: string;
|
||||
verifying_device_key?: string;
|
||||
master_key?: string;
|
||||
protocols?: string[];
|
||||
protocol?: string;
|
||||
login_token?: string;
|
||||
homeserver?: string;
|
||||
}
|
||||
|
||||
const LOGIN_TOKEN_PROTOCOL = new UnstableValue("login_token", "org.matrix.msc3906.login_token");
|
||||
|
||||
/**
|
||||
* Implements MSC3906 to allow a user to sign in on a new device using QR code.
|
||||
* This implementation only supports generating a QR code on a device that is already signed in.
|
||||
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
* MSC3886/MSC3903/MSC3906 are now closed and so this functionality will be removed in future.
|
||||
* However, we want to keep this implementation around for some time.
|
||||
* TODO: define an end-of-life date for this implementation.
|
||||
*/
|
||||
export class MSC3906Rendezvous {
|
||||
private newDeviceId?: string;
|
||||
private newDeviceKey?: string;
|
||||
private ourIntent: RendezvousIntent = RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE;
|
||||
private _code?: string;
|
||||
|
||||
/**
|
||||
* @param channel - The secure channel used for communication
|
||||
* @param client - The Matrix client in used on the device already logged in
|
||||
* @param onFailure - Callback for when the rendezvous fails
|
||||
*/
|
||||
public constructor(
|
||||
private channel: RendezvousChannel<MSC3906RendezvousPayload>,
|
||||
private client: MatrixClient,
|
||||
public onFailure?: RendezvousFailureListener,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet.
|
||||
*/
|
||||
public get code(): string | undefined {
|
||||
return this._code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the code including doing partial set up of the channel where required.
|
||||
*/
|
||||
public async generateCode(): Promise<void> {
|
||||
if (this._code) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent));
|
||||
}
|
||||
|
||||
public async startAfterShowingCode(): Promise<string | undefined> {
|
||||
const checksum = await this.channel.connect();
|
||||
|
||||
logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`);
|
||||
|
||||
// in stable and unstable r1 the availability is exposed as a capability
|
||||
let capabilities: Capabilities = {};
|
||||
try {
|
||||
capabilities = await this.client.getCapabilities();
|
||||
} catch {}
|
||||
// in r0 of MSC3882 the availability is exposed as a feature flag
|
||||
const features = await buildFeatureSupportMap(await this.client.getVersions());
|
||||
const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);
|
||||
|
||||
// determine available protocols
|
||||
if (!capability?.enabled && features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) {
|
||||
logger.info("Server doesn't support get_login_token");
|
||||
await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported });
|
||||
await this.cancel(RendezvousFailureReason.HomeserverLacksSupport);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this.send({ type: PayloadType.Progress, protocols: [LOGIN_TOKEN_PROTOCOL.name] });
|
||||
|
||||
logger.info("Waiting for other device to choose protocol");
|
||||
const { type, protocol, outcome } = await this.receive();
|
||||
|
||||
if (type === PayloadType.Finish) {
|
||||
// new device decided not to complete
|
||||
switch (outcome ?? "") {
|
||||
case "unsupported":
|
||||
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
break;
|
||||
default:
|
||||
await this.cancel(RendezvousFailureReason.Unknown);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (type !== PayloadType.Progress) {
|
||||
await this.cancel(RendezvousFailureReason.Unknown);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) {
|
||||
await this.cancel(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
private async receive(): Promise<MSC3906RendezvousPayload> {
|
||||
return (await this.channel.receive()) as MSC3906RendezvousPayload;
|
||||
}
|
||||
|
||||
private async send(payload: MSC3906RendezvousPayload): Promise<void> {
|
||||
await this.channel.send(payload);
|
||||
}
|
||||
|
||||
public async declineLoginOnExistingDevice(): Promise<void> {
|
||||
logger.info("User declined sign in");
|
||||
await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined });
|
||||
}
|
||||
|
||||
public async approveLoginOnExistingDevice(loginToken: string): Promise<string | undefined> {
|
||||
// eslint-disable-next-line camelcase
|
||||
await this.send({ type: PayloadType.Progress, login_token: loginToken, homeserver: this.client.baseUrl });
|
||||
|
||||
logger.info("Waiting for outcome");
|
||||
const res = await this.receive();
|
||||
if (!res) {
|
||||
return undefined;
|
||||
}
|
||||
const { outcome, device_id: deviceId, device_key: deviceKey } = res;
|
||||
|
||||
if (outcome !== "success") {
|
||||
throw new Error("Linking failed");
|
||||
}
|
||||
|
||||
this.newDeviceId = deviceId;
|
||||
this.newDeviceKey = deviceKey;
|
||||
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
private async verifyAndCrossSignDevice(deviceInfo: Device): Promise<void> {
|
||||
const crypto = this.client.getCrypto()!;
|
||||
|
||||
if (!this.newDeviceId) {
|
||||
throw new Error("No new device ID set");
|
||||
}
|
||||
|
||||
// check that keys received from the server for the new device match those received from the device itself
|
||||
if (deviceInfo.getFingerprint() !== this.newDeviceKey) {
|
||||
throw new Error(
|
||||
`New device has different keys than expected: ${this.newDeviceKey} vs ${deviceInfo.getFingerprint()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const userId = this.client.getSafeUserId();
|
||||
|
||||
// mark the device as verified locally + cross sign
|
||||
logger.info(`Marking device ${this.newDeviceId} as verified`);
|
||||
await crypto.setDeviceVerified(userId, this.newDeviceId, true);
|
||||
await crypto.crossSignDevice(this.newDeviceId);
|
||||
|
||||
const masterPublicKey = (await crypto.getCrossSigningKeyId(CrossSigningKey.Master)) ?? undefined;
|
||||
|
||||
const ourDeviceId = this.client.getDeviceId()!;
|
||||
const ourDeviceKey = (await crypto.getOwnDeviceKeys()).ed25519;
|
||||
|
||||
await this.send({
|
||||
type: PayloadType.Finish,
|
||||
outcome: Outcome.Verified,
|
||||
verifying_device_id: ourDeviceId,
|
||||
verifying_device_key: ourDeviceKey,
|
||||
master_key: masterPublicKey,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the device and cross-sign it.
|
||||
* @param timeout - time in milliseconds to wait for device to come online
|
||||
*/
|
||||
public async verifyNewDeviceOnExistingDevice(timeout = 10 * 1000): Promise<void> {
|
||||
if (!this.newDeviceId) {
|
||||
throw new Error("No new device to sign");
|
||||
}
|
||||
|
||||
if (!this.newDeviceKey) {
|
||||
logger.info("No new device key to sign");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const crypto = this.client.getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("Crypto not available on client");
|
||||
}
|
||||
|
||||
let deviceInfo = await this.getOwnDevice(this.newDeviceId);
|
||||
|
||||
if (!deviceInfo) {
|
||||
logger.info("Going to wait for new device to be online");
|
||||
await sleep(timeout);
|
||||
deviceInfo = await this.getOwnDevice(this.newDeviceId);
|
||||
}
|
||||
|
||||
if (deviceInfo) {
|
||||
await this.verifyAndCrossSignDevice(deviceInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Device not online within timeout");
|
||||
}
|
||||
|
||||
private async getOwnDevice(deviceId: string): Promise<Device | undefined> {
|
||||
const userId = this.client.getSafeUserId();
|
||||
const ownDeviceInfo = await this.client.getCrypto()!.getUserDeviceInfo([userId]);
|
||||
return ownDeviceInfo.get(userId)?.get(deviceId);
|
||||
}
|
||||
|
||||
public async cancel(reason: RendezvousFailureReason): Promise<void> {
|
||||
this.onFailure?.(reason);
|
||||
await this.channel.cancel(reason);
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
await this.channel.close();
|
||||
}
|
||||
}
|
@@ -16,20 +16,7 @@ limitations under the License.
|
||||
|
||||
export type RendezvousFailureListener = (reason: RendezvousFailureReason) => void;
|
||||
|
||||
export type RendezvousFailureReason =
|
||||
| LegacyRendezvousFailureReason
|
||||
| MSC4108FailureReason
|
||||
| ClientRendezvousFailureReason;
|
||||
|
||||
export enum LegacyRendezvousFailureReason {
|
||||
UserDeclined = "user_declined",
|
||||
Unknown = "unknown",
|
||||
Expired = "expired",
|
||||
UserCancelled = "user_cancelled",
|
||||
UnsupportedAlgorithm = "unsupported_algorithm",
|
||||
UnsupportedProtocol = "unsupported_protocol",
|
||||
HomeserverLacksSupport = "homeserver_lacks_support",
|
||||
}
|
||||
export type RendezvousFailureReason = MSC4108FailureReason | ClientRendezvousFailureReason;
|
||||
|
||||
export enum MSC4108FailureReason {
|
||||
AuthorizationExpired = "authorization_expired",
|
||||
|
@@ -1,261 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SAS } from "@matrix-org/olm";
|
||||
|
||||
import {
|
||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||
RendezvousChannel,
|
||||
RendezvousCode,
|
||||
RendezvousError,
|
||||
RendezvousIntent,
|
||||
RendezvousTransport,
|
||||
RendezvousTransportDetails,
|
||||
} from "../index.ts";
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../../base64.ts";
|
||||
import { generateDecimalSas } from "../../crypto/verification/SASDecimal.ts";
|
||||
import { UnstableValue } from "../../NamespacedValue.ts";
|
||||
|
||||
const ECDH_V2 = new UnstableValue(
|
||||
"m.rendezvous.v2.curve25519-aes-sha256",
|
||||
"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256",
|
||||
);
|
||||
|
||||
export interface ECDHv2RendezvousCode extends RendezvousCode {
|
||||
rendezvous: {
|
||||
transport: RendezvousTransportDetails;
|
||||
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
|
||||
key: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
|
||||
|
||||
export interface PlainTextPayload {
|
||||
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface EncryptedPayload {
|
||||
iv: string;
|
||||
ciphertext: string;
|
||||
}
|
||||
|
||||
async function importKey(key: Uint8Array): Promise<CryptoKey> {
|
||||
if (!globalThis.crypto.subtle) {
|
||||
throw new Error("Web Crypto is not available");
|
||||
}
|
||||
|
||||
const imported = globalThis.crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
||||
|
||||
return imported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903)
|
||||
* X25519/ECDH key agreement based secure rendezvous channel.
|
||||
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
* MSC3886/MSC3903/MSC3906 are now closed and so this functionality will be removed in future.
|
||||
* However, we want to keep this implementation around for some time.
|
||||
* TODO: define an end-of-life date for this implementation.
|
||||
*/
|
||||
export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
private olmSAS?: SAS;
|
||||
private ourPublicKey: Uint8Array;
|
||||
private aesKey?: CryptoKey;
|
||||
private connected = false;
|
||||
|
||||
public constructor(
|
||||
private transport: RendezvousTransport<MSC3903ECDHPayload>,
|
||||
private theirPublicKey?: Uint8Array,
|
||||
public onFailure?: (reason: RendezvousFailureReason) => void,
|
||||
) {
|
||||
this.olmSAS = new global.Olm.SAS();
|
||||
this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey());
|
||||
}
|
||||
|
||||
public async generateCode(intent: RendezvousIntent): Promise<ECDHv2RendezvousCode> {
|
||||
if (this.transport.ready) {
|
||||
throw new Error("Code already generated");
|
||||
}
|
||||
|
||||
await this.transport.send({ algorithm: ECDH_V2.name });
|
||||
|
||||
const rendezvous: ECDHv2RendezvousCode = {
|
||||
rendezvous: {
|
||||
algorithm: ECDH_V2.name,
|
||||
key: encodeUnpaddedBase64(this.ourPublicKey),
|
||||
transport: await this.transport.details(),
|
||||
},
|
||||
intent,
|
||||
};
|
||||
|
||||
return rendezvous;
|
||||
}
|
||||
|
||||
public async connect(): Promise<string> {
|
||||
if (this.connected) {
|
||||
throw new Error("Channel already connected");
|
||||
}
|
||||
|
||||
if (!this.olmSAS) {
|
||||
throw new Error("Channel closed");
|
||||
}
|
||||
|
||||
const isInitiator = !this.theirPublicKey;
|
||||
|
||||
if (isInitiator) {
|
||||
// wait for the other side to send us their public key
|
||||
const rawRes = await this.transport.receive();
|
||||
if (!rawRes) {
|
||||
throw new Error("No response from other device");
|
||||
}
|
||||
const res = rawRes as Partial<PlainTextPayload>;
|
||||
const { key, algorithm } = res;
|
||||
if (!algorithm || !ECDH_V2.matches(algorithm) || !key) {
|
||||
throw new RendezvousError(
|
||||
"Unsupported algorithm: " + algorithm,
|
||||
RendezvousFailureReason.UnsupportedAlgorithm,
|
||||
);
|
||||
}
|
||||
|
||||
this.theirPublicKey = decodeBase64(key);
|
||||
} else {
|
||||
// send our public key unencrypted
|
||||
await this.transport.send({
|
||||
algorithm: ECDH_V2.name,
|
||||
key: encodeUnpaddedBase64(this.ourPublicKey),
|
||||
});
|
||||
}
|
||||
|
||||
this.connected = true;
|
||||
|
||||
this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!));
|
||||
|
||||
const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!;
|
||||
const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey;
|
||||
let aesInfo = ECDH_V2.name;
|
||||
aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`;
|
||||
aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`;
|
||||
|
||||
const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32);
|
||||
|
||||
this.aesKey = await importKey(aesKeyBytes);
|
||||
|
||||
// blank the bytes out to make sure not kept in memory
|
||||
aesKeyBytes.fill(0);
|
||||
|
||||
const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5);
|
||||
return generateDecimalSas(Array.from(rawChecksum)).join("-");
|
||||
}
|
||||
|
||||
private async encrypt(data: T): Promise<MSC3903ECDHPayload> {
|
||||
if (!globalThis.crypto.subtle) {
|
||||
throw new Error("Web Crypto is not available");
|
||||
}
|
||||
|
||||
const iv = new Uint8Array(32);
|
||||
globalThis.crypto.getRandomValues(iv);
|
||||
|
||||
const encodedData = new TextEncoder().encode(JSON.stringify(data));
|
||||
|
||||
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
tagLength: 128,
|
||||
},
|
||||
this.aesKey as CryptoKey,
|
||||
encodedData,
|
||||
);
|
||||
|
||||
return {
|
||||
iv: encodeUnpaddedBase64(iv),
|
||||
ciphertext: encodeUnpaddedBase64(ciphertext),
|
||||
};
|
||||
}
|
||||
|
||||
public async send(payload: T): Promise<void> {
|
||||
if (!this.olmSAS) {
|
||||
throw new Error("Channel closed");
|
||||
}
|
||||
|
||||
if (!this.aesKey) {
|
||||
throw new Error("Shared secret not set up");
|
||||
}
|
||||
|
||||
return this.transport.send(await this.encrypt(payload));
|
||||
}
|
||||
|
||||
private async decrypt({ iv, ciphertext }: EncryptedPayload): Promise<Partial<T>> {
|
||||
if (!ciphertext || !iv) {
|
||||
throw new Error("Missing ciphertext and/or iv");
|
||||
}
|
||||
|
||||
const ciphertextBytes = decodeBase64(ciphertext);
|
||||
|
||||
if (!globalThis.crypto.subtle) {
|
||||
throw new Error("Web Crypto is not available");
|
||||
}
|
||||
|
||||
const plaintext = await globalThis.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: decodeBase64(iv),
|
||||
tagLength: 128,
|
||||
},
|
||||
this.aesKey as CryptoKey,
|
||||
ciphertextBytes,
|
||||
);
|
||||
|
||||
return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext)));
|
||||
}
|
||||
|
||||
public async receive(): Promise<Partial<T> | undefined> {
|
||||
if (!this.olmSAS) {
|
||||
throw new Error("Channel closed");
|
||||
}
|
||||
if (!this.aesKey) {
|
||||
throw new Error("Shared secret not set up");
|
||||
}
|
||||
|
||||
const rawData = await this.transport.receive();
|
||||
if (!rawData) {
|
||||
return undefined;
|
||||
}
|
||||
const data = rawData as Partial<EncryptedPayload>;
|
||||
if (data.ciphertext && data.iv) {
|
||||
return this.decrypt(data as EncryptedPayload);
|
||||
}
|
||||
|
||||
throw new Error("Data received but no ciphertext");
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
if (this.olmSAS) {
|
||||
this.olmSAS.free();
|
||||
this.olmSAS = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async cancel(reason: RendezvousFailureReason): Promise<void> {
|
||||
try {
|
||||
await this.transport.cancel(reason);
|
||||
} finally {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,8 +14,4 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated in favour of MSC4108-based implementation
|
||||
*/
|
||||
export * from "./MSC3903ECDHv2RendezvousChannel.ts";
|
||||
export * from "./MSC4108SecureChannel.ts";
|
||||
|
@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated in favour of MSC4108-based implementation
|
||||
*/
|
||||
export * from "./MSC3906Rendezvous.ts";
|
||||
export * from "./MSC4108SignInWithQR.ts";
|
||||
export type * from "./RendezvousChannel.ts";
|
||||
export type * from "./RendezvousCode.ts";
|
||||
|
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import { logger } from "../../logger.ts";
|
||||
import { sleep } from "../../utils.ts";
|
||||
import {
|
||||
RendezvousFailureListener,
|
||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||
RendezvousTransport,
|
||||
RendezvousTransportDetails,
|
||||
} from "../index.ts";
|
||||
import { MatrixClient } from "../../matrix.ts";
|
||||
import { ClientPrefix } from "../../http-api/index.ts";
|
||||
|
||||
const TYPE = new UnstableValue("http.v1", "org.matrix.msc3886.http.v1");
|
||||
|
||||
export interface MSC3886SimpleHttpRendezvousTransportDetails extends RendezvousTransportDetails {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886)
|
||||
* simple HTTP rendezvous protocol.
|
||||
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
* MSC3886/MSC3903/MSC3906 are now closed and so this functionality will be removed in future.
|
||||
* However, we want to keep this implementation around for some time.
|
||||
* TODO: define an end-of-life date for this implementation.
|
||||
*/
|
||||
export class MSC3886SimpleHttpRendezvousTransport<T extends {}> implements RendezvousTransport<T> {
|
||||
private uri?: string;
|
||||
private etag?: string;
|
||||
private expiresAt?: Date;
|
||||
private client: MatrixClient;
|
||||
private fallbackRzServer?: string;
|
||||
private fetchFn?: typeof global.fetch;
|
||||
private cancelled = false;
|
||||
private _ready = false;
|
||||
public onFailure?: RendezvousFailureListener;
|
||||
|
||||
public constructor({
|
||||
onFailure,
|
||||
client,
|
||||
fallbackRzServer,
|
||||
fetchFn,
|
||||
}: {
|
||||
fetchFn?: typeof global.fetch;
|
||||
onFailure?: RendezvousFailureListener;
|
||||
client: MatrixClient;
|
||||
fallbackRzServer?: string;
|
||||
}) {
|
||||
this.fetchFn = fetchFn;
|
||||
this.onFailure = onFailure;
|
||||
this.client = client;
|
||||
this.fallbackRzServer = fallbackRzServer;
|
||||
}
|
||||
|
||||
public get ready(): boolean {
|
||||
return this._ready;
|
||||
}
|
||||
|
||||
public async details(): Promise<MSC3886SimpleHttpRendezvousTransportDetails> {
|
||||
if (!this.uri) {
|
||||
throw new Error("Rendezvous not set up");
|
||||
}
|
||||
|
||||
return {
|
||||
type: TYPE.name,
|
||||
uri: this.uri,
|
||||
};
|
||||
}
|
||||
|
||||
private fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
|
||||
if (this.fetchFn) {
|
||||
return this.fetchFn(resource, options);
|
||||
}
|
||||
return global.fetch(resource, options);
|
||||
}
|
||||
|
||||
private async getPostEndpoint(): Promise<string | undefined> {
|
||||
try {
|
||||
if (await this.client.doesServerSupportUnstableFeature("org.matrix.msc3886")) {
|
||||
return `${this.client.baseUrl}${ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn("Failed to get unstable features", err);
|
||||
}
|
||||
|
||||
return this.fallbackRzServer;
|
||||
}
|
||||
|
||||
public async send(data: T): Promise<void> {
|
||||
if (this.cancelled) {
|
||||
return;
|
||||
}
|
||||
const method = this.uri ? "PUT" : "POST";
|
||||
const uri = this.uri ?? (await this.getPostEndpoint());
|
||||
|
||||
if (!uri) {
|
||||
throw new Error("Invalid rendezvous URI");
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { "content-type": "application/json" };
|
||||
if (this.etag) {
|
||||
headers["if-match"] = this.etag;
|
||||
}
|
||||
|
||||
const res = await this.fetch(uri, { method, headers, body: JSON.stringify(data) });
|
||||
if (res.status === 404) {
|
||||
return this.cancel(RendezvousFailureReason.Unknown);
|
||||
}
|
||||
this.etag = res.headers.get("etag") ?? undefined;
|
||||
|
||||
if (method === "POST") {
|
||||
const location = res.headers.get("location");
|
||||
if (!location) {
|
||||
throw new Error("No rendezvous URI given");
|
||||
}
|
||||
const expires = res.headers.get("expires");
|
||||
if (expires) {
|
||||
this.expiresAt = new Date(expires);
|
||||
}
|
||||
// we would usually expect the final `url` to be set by a proper fetch implementation.
|
||||
// however, if a polyfill based on XHR is used it won't be set, we we use existing URI as fallback
|
||||
const baseUrl = res.url ?? uri;
|
||||
// resolve location header which could be relative or absolute
|
||||
this.uri = new URL(location, `${baseUrl}${baseUrl.endsWith("/") ? "" : "/"}`).href;
|
||||
this._ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async receive(): Promise<Partial<T> | undefined> {
|
||||
if (!this.uri) {
|
||||
throw new Error("Rendezvous not set up");
|
||||
}
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
if (this.cancelled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.etag) {
|
||||
headers["if-none-match"] = this.etag;
|
||||
}
|
||||
const poll = await this.fetch(this.uri, { method: "GET", headers });
|
||||
|
||||
if (poll.status === 404) {
|
||||
this.cancel(RendezvousFailureReason.Unknown);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// rely on server expiring the channel rather than checking ourselves
|
||||
|
||||
if (poll.headers.get("content-type") !== "application/json") {
|
||||
this.etag = poll.headers.get("etag") ?? undefined;
|
||||
} else if (poll.status === 200) {
|
||||
this.etag = poll.headers.get("etag") ?? undefined;
|
||||
return poll.json();
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
public async cancel(reason: RendezvousFailureReason): Promise<void> {
|
||||
if (reason === RendezvousFailureReason.Unknown && this.expiresAt && this.expiresAt.getTime() < Date.now()) {
|
||||
reason = RendezvousFailureReason.Expired;
|
||||
}
|
||||
|
||||
this.cancelled = true;
|
||||
this._ready = false;
|
||||
this.onFailure?.(reason);
|
||||
|
||||
if (this.uri && reason === RendezvousFailureReason.UserDeclined) {
|
||||
try {
|
||||
await this.fetch(this.uri, { method: "DELETE" });
|
||||
} catch (e) {
|
||||
logger.warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,8 +14,4 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated in favour of MSC4108-based implementation
|
||||
*/
|
||||
export * from "./MSC3886SimpleHttpRendezvousTransport.ts";
|
||||
export * from "./MSC4108RendezvousSession.ts";
|
||||
|
Reference in New Issue
Block a user