mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-11-20 03:25:52 +01:00
Use authenticated media endpoints when possible
This commit is contained in:
parent
53b8a2f4d0
commit
c67f8c3582
@ -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,
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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;
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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({homeserver, platform}: {homeserver:string, platform: Platform}) {
|
||||
this._homeserver = homeserver;
|
||||
this._platform = platform;
|
||||
constructor(params: Params) {
|
||||
this.homeserver = params.homeserver;
|
||||
this.platform = params.platform;
|
||||
this.generateMediaUrl(params.serverVersions);
|
||||
}
|
||||
|
||||
mxcUrlThumbnail(url: string, width: number, height: number, method: "crop" | "scale"): string | undefined {
|
||||
const parts = this._parseMxcUrl(url);
|
||||
/**
|
||||
* 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);
|
||||
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/
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user