Skip to content

Commit

Permalink
Merge pull request #1177 from element-hq/midhun/support-authenticated…
Browse files Browse the repository at this point in the history
…-media

Add support for authenticated media
  • Loading branch information
MidhunSureshR authored Aug 19, 2024
2 parents 841ed25 + c67f8c3 commit 27c7225
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 81 deletions.
77 changes: 54 additions & 23 deletions src/matrix/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ export class Client {
this._reconnector = new Reconnector({
onlineStatus: this._platform.onlineStatus,
retryDelay: new ExponentialRetryDelay(clock.createTimeout),
createMeasure: clock.createMeasure
createMeasure: clock.createMeasure,
});
const hsApi = new HomeServerApi({
homeserver: sessionInfo.homeServer,
Expand All @@ -261,7 +261,10 @@ export class Client {
reconnector: this._reconnector,
});
this._sessionId = sessionInfo.id;
this._storage = await this._platform.storageFactory.create(sessionInfo.id, log);
this._storage = await this._platform.storageFactory.create(
sessionInfo.id,
log
);
// no need to pass access token to session
const filteredSessionInfo = {
id: sessionInfo.id,
Expand All @@ -275,11 +278,16 @@ export class Client {
if (this._workerPromise) {
olmWorker = await this._workerPromise;
}
this._requestScheduler = new RequestScheduler({hsApi, clock});
this._requestScheduler = new RequestScheduler({ hsApi, clock });
this._requestScheduler.start();

const lastVersionsResponse = await hsApi
.versions({ timeout: 10000, log })
.response();
const mediaRepository = new MediaRepository({
homeserver: sessionInfo.homeServer,
platform: this._platform,
serverVersions: lastVersionsResponse.versions,
});
this._session = new Session({
storage: this._storage,
Expand All @@ -289,32 +297,54 @@ export class Client {
olmWorker,
mediaRepository,
platform: this._platform,
features: this._features
features: this._features,
});
await this._session.load(log);
if (dehydratedDevice) {
await log.wrap("dehydrateIdentity", log => this._session.dehydrateIdentity(dehydratedDevice, log));
await this._session.setupDehydratedDevice(dehydratedDevice.key, log);
await log.wrap("dehydrateIdentity", (log) =>
this._session.dehydrateIdentity(dehydratedDevice, log)
);
await this._session.setupDehydratedDevice(
dehydratedDevice.key,
log
);
} else if (!this._session.hasIdentity) {
this._status.set(LoadStatus.SessionSetup);
await log.wrap("createIdentity", log => this._session.createIdentity(log));
await log.wrap("createIdentity", (log) =>
this._session.createIdentity(log)
);
}

this._sync = new Sync({hsApi: this._requestScheduler.hsApi, storage: this._storage, session: this._session, logger: this._platform.logger});
// notify sync and session when back online
this._reconnectSubscription = this._reconnector.connectionStatus.subscribe(state => {
if (state === ConnectionStatus.Online) {
this._platform.logger.runDetached("reconnect", async log => {
// needs to happen before sync and session or it would abort all requests
this._requestScheduler.start();
this._sync.start();
this._sessionStartedByReconnector = true;
const d = dehydratedDevice;
dehydratedDevice = undefined;
await log.wrap("session start", log => this._session.start(this._reconnector.lastVersionsResponse, d, log));
});
}
this._sync = new Sync({
hsApi: this._requestScheduler.hsApi,
storage: this._storage,
session: this._session,
logger: this._platform.logger,
});
// notify sync and session when back online
this._reconnectSubscription =
this._reconnector.connectionStatus.subscribe((state) => {
if (state === ConnectionStatus.Online) {
this._platform.logger.runDetached(
"reconnect",
async (log) => {
// needs to happen before sync and session or it would abort all requests
this._requestScheduler.start();
this._sync.start();
this._sessionStartedByReconnector = true;
const d = dehydratedDevice;
dehydratedDevice = undefined;
await log.wrap("session start", (log) =>
this._session.start(
this._reconnector.lastVersionsResponse,
d,
log
)
);
}
);
}
});
await log.wrap("wait first sync", () => this._waitForFirstSync());
if (this._isDisposed) {
return;
Expand All @@ -326,14 +356,15 @@ export class Client {
// started to session, so check first
// to prevent an extra /versions request
if (!this._sessionStartedByReconnector) {
const lastVersionsResponse = await hsApi.versions({timeout: 10000, log}).response();
if (this._isDisposed) {
return;
}
const d = dehydratedDevice;
dehydratedDevice = undefined;
// log as ref as we don't want to await it
await log.wrap("session start", log => this._session.start(lastVersionsResponse, d, log));
await log.wrap("session start", (log) =>
this._session.start(lastVersionsResponse, d, log)
);
}
}

Expand Down
170 changes: 143 additions & 27 deletions src/matrix/net/MediaRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,67 +14,183 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import {encodeQueryParams} from "./common";
import {decryptAttachment} from "../e2ee/attachment.js";
import {Platform} from "../../platform/web/Platform.js";
import {BlobHandle} from "../../platform/web/dom/BlobHandle.js";
import type {Attachment, EncryptedFile} from "./types/response";
import { encodeQueryParams } from "./common";
import { decryptAttachment } from "../e2ee/attachment.js";
import { Platform } from "../../platform/web/Platform.js";
import { BlobHandle } from "../../platform/web/dom/BlobHandle.js";
import type {
Attachment,
EncryptedFile,
VersionResponse,
} from "./types/response";

type ServerVersions = VersionResponse["versions"];

type Params = {
homeserver: string;
platform: Platform;
serverVersions: ServerVersions;
};

export class MediaRepository {
private readonly _homeserver: string;
private readonly _platform: Platform;
private readonly homeserver: string;
private readonly platform: Platform;
// Depends on whether the server supports authenticated media
private mediaUrlPart: string;

constructor(params: Params) {
this.homeserver = params.homeserver;
this.platform = params.platform;
this.generateMediaUrl(params.serverVersions);
}

constructor({homeserver, platform}: {homeserver:string, platform: Platform}) {
this._homeserver = homeserver;
this._platform = platform;
/**
* Calculate and store the correct media endpoint depending
* on whether the homeserver supports authenticated media (MSC3916)
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/3916
* @param serverVersions List of supported spec versions
*/
private generateMediaUrl(serverVersions: ServerVersions) {
const VERSION_WITH_AUTHENTICATION = "v1.11";
if (serverVersions.includes(VERSION_WITH_AUTHENTICATION)) {
this.mediaUrlPart = "_matrix/client/v1/media";
} else {
this.mediaUrlPart = "_matrix/media/v3";
}
}

mxcUrlThumbnail(url: string, width: number, height: number, method: "crop" | "scale"): string | undefined {
const parts = this._parseMxcUrl(url);
mxcUrlThumbnail(
url: string,
width: number,
height: number,
method: "crop" | "scale"
): string | undefined {
const parts = this.parseMxcUrl(url);
if (parts) {
const [serverName, mediaId] = parts;
const httpUrl = `${this._homeserver}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
return httpUrl + "?" + encodeQueryParams({width: Math.round(width), height: Math.round(height), method});
const httpUrl = `${this.homeserver}/${
this.mediaUrlPart
}/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(
mediaId
)}`;
return (
httpUrl +
"?" +
encodeQueryParams({
width: Math.round(width),
height: Math.round(height),
method,
})
);
}
return undefined;
}

mxcUrl(url: string): string | undefined {
const parts = this._parseMxcUrl(url);
const parts = this.parseMxcUrl(url);
if (parts) {
const [serverName, mediaId] = parts;
return `${this._homeserver}/_matrix/media/r0/download/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
return `${this.homeserver}/${
this.mediaUrlPart
}/download/${encodeURIComponent(serverName)}/${encodeURIComponent(
mediaId
)}`;
}
return undefined;
}

private _parseMxcUrl(url: string): string[] | undefined {
private parseMxcUrl(url: string): string[] | undefined {
const prefix = "mxc://";
if (url.startsWith(prefix)) {
return url.substr(prefix.length).split("/", 2);
return url.slice(prefix.length).split("/", 2);
} else {
return undefined;
}
}

async downloadEncryptedFile(fileEntry: EncryptedFile, cache: boolean = false): Promise<BlobHandle> {
async downloadEncryptedFile(
fileEntry: EncryptedFile,
cache: boolean = false
): Promise<BlobHandle> {
const url = this.mxcUrl(fileEntry.url);
const {body: encryptedBuffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response();
const decryptedBuffer = await decryptAttachment(this._platform, encryptedBuffer, fileEntry);
return this._platform.createBlob(decryptedBuffer, fileEntry.mimetype);
const { body: encryptedBuffer } = await this.platform
.request(url, { method: "GET", format: "buffer", cache })
.response();
const decryptedBuffer = await decryptAttachment(
this.platform,
encryptedBuffer,
fileEntry
);
return this.platform.createBlob(decryptedBuffer, fileEntry.mimetype);
}

async downloadPlaintextFile(mxcUrl: string, mimetype: string, cache: boolean = false): Promise<BlobHandle> {
async downloadPlaintextFile(
mxcUrl: string,
mimetype: string,
cache: boolean = false
): Promise<BlobHandle> {
const url = this.mxcUrl(mxcUrl);
const {body: buffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response();
return this._platform.createBlob(buffer, mimetype);
const { body: buffer } = await this.platform
.request(url, { method: "GET", format: "buffer", cache })
.response();
return this.platform.createBlob(buffer, mimetype);
}

async downloadAttachment(content: Attachment, cache: boolean = false): Promise<BlobHandle> {
async downloadAttachment(
content: Attachment,
cache: boolean = false
): Promise<BlobHandle> {
if (content.file) {
return this.downloadEncryptedFile(content.file, cache);
} else {
return this.downloadPlaintextFile(content.url!, content.info?.mimetype, cache);
return this.downloadPlaintextFile(
content.url!,
content.info?.mimetype,
cache
);
}
}
}

export function tests() {
return {
"Uses correct endpoint when server supports authenticated media": (
assert
) => {
const homeserver = "matrix.org";
const platform = {};
// Is it enough to check if v1.11 is present?
// or do we check if maxVersion > v1.11
const serverVersions = ["v1.1", "v1.11", "v1.10"];
const mediaRepository = new MediaRepository({
homeserver,
platform,
serverVersions,
});

const mxcUrl = "mxc://matrix.org/foobartest";
assert.match(
mediaRepository.mxcUrl(mxcUrl),
/_matrix\/client\/v1\/media/
);
},

"Uses correct endpoint when server does not supports authenticated media":
(assert) => {
const homeserver = "matrix.org";
const platform = {};
const serverVersions = ["v1.1", "v1.11", "v1.10"];
const mediaRepository = new MediaRepository({
homeserver,
platform,
serverVersions,
});

const mxcUrl = "mxc://matrix.org/foobartest";
assert.match(
mediaRepository.mxcUrl(mxcUrl),
/_matrix\/client\/v1\/media/
);
},
};
}
8 changes: 6 additions & 2 deletions src/platform/web/Platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,13 @@ export class Platform {
this.onlineStatus = new OnlineStatus();
this.timeFormatter = new TimeFormatter();
this._serviceWorkerHandler = null;
this.sessionInfoStorage = new SessionInfoStorage(
"hydrogen_sessions_v1"
);
if (assetPaths.serviceWorker && "serviceWorker" in navigator) {
this._serviceWorkerHandler = new ServiceWorkerHandler();
this._serviceWorkerHandler = new ServiceWorkerHandler(
this.sessionInfoStorage
);
this._serviceWorkerHandler.registerAndStart(assetPaths.serviceWorker);
}
this.notificationService = undefined;
Expand All @@ -156,7 +161,6 @@ export class Platform {
this.crypto = new Crypto(cryptoExtras);
}
this.storageFactory = new StorageFactory(this._serviceWorkerHandler);
this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
this.estimateStorageUsage = estimateStorageUsage;
if (typeof fetch === "function") {
this.request = createFetchRequest(this.clock.createTimeout, this._serviceWorkerHandler);
Expand Down
Loading

0 comments on commit 27c7225

Please sign in to comment.