extract platform class to put all platform specific code in

This commit is contained in:
Bruno Windels 2020-10-26 15:44:11 +01:00
parent 46fd769dda
commit 03351d2e1f
32 changed files with 309 additions and 275 deletions

View file

@ -20,14 +20,15 @@
</script>
<script id="main" type="module">
import {main} from "./src/main.js";
main(document.body, {
import {Platform} from "./src/platform/web/Platform.js";
main(new Platform(document.body, {
worker: "src/worker.js",
olm: {
wasm: "lib/olm/olm.wasm",
legacyBundle: "lib/olm/olm_legacy.js",
wasmBundle: "lib/olm/olm.js",
}
});
}));
</script>
</body>
</html>

View file

@ -45,12 +45,12 @@ import flexbugsFixes from "postcss-flexbugs-fixes";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectDir = path.join(__dirname, "../");
const cssSrcDir = path.join(projectDir, "src/ui/web/css/");
const cssSrcDir = path.join(projectDir, "src/platform/web/ui/css/");
const program = new commander.Command();
program
const parameters = new commander.Command();
parameters
.option("--modern-only", "don't make a legacy build")
program.parse(process.argv);
parameters.parse(process.argv);
async function build({modernOnly}) {
// get version number
@ -70,10 +70,13 @@ async function build({modernOnly}) {
// copy olm assets
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
assets.addSubMap(olmAssets);
await assets.write(`hydrogen.js`, await buildJs("src/main.js"));
await assets.write(`hydrogen.js`, await buildJs("src/main.js", ["src/platform/web/Platform.js"]));
if (!modernOnly) {
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy("src/main.js", ['src/legacy-polyfill.js', 'src/legacy-extras.js']));
await assets.write(`worker.js`, await buildJsLegacy("src/worker.js", ['src/worker-polyfill.js']));
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy("src/main.js", [
'src/platform/web/legacy-polyfill.js',
'src/platform/web/LegacyPlatform.js'
]));
await assets.write(`worker.js`, await buildJsLegacy("src/platform/web/worker/main.js", ['src/platform/web/worker/polyfill.js']));
}
// creates the directories where the theme css bundles are placed in,
// and writes to assets, so the build bundles can translate them, so do it first
@ -82,7 +85,7 @@ async function build({modernOnly}) {
await buildManifest(assets);
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html
assets.addToHashForAll("index.html", devHtml);
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8");
let swSource = await fs.readFile(path.join(projectDir, "src/platform/web/service-worker.template.js"), "utf8");
assets.addToHashForAll("sw.js", swSource);
const globalHash = assets.hashForAll();
@ -148,12 +151,12 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
}
});
const mainScripts = [
`<script type="module">import {main} from "./${assets.resolve(`hydrogen.js`)}"; main(document.body, ${pathsJSON});</script>`
`<script type="module">import {main, Platform} from "./${assets.resolve(`hydrogen.js`)}"; main(new Platform(document.body, ${pathsJSON}));</script>`
];
if (!modernOnly) {
mainScripts.push(
`<script type="text/javascript" nomodule src="${assets.resolve(`hydrogen-legacy.js`)}"></script>`,
`<script type="text/javascript" nomodule>hydrogenBundle.main(document.body, ${pathsJSON}, hydrogenBundle.legacyExtras);</script>`
`<script type="text/javascript" nomodule>hydrogen.main(new hydrogen.Platform(document.body, ${pathsJSON}));</script>`
);
}
doc("script#main").replaceWith(mainScripts.join(""));
@ -168,16 +171,16 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
await assets.writeUnhashed("index.html", doc.html());
}
async function buildJs(inputFile) {
async function buildJs(mainFile, extraFiles = []) {
// create js bundle
const bundle = await rollup({
input: inputFile,
input: extraFiles.concat(mainFile),
plugins: [removeJsComments({comments: "none"})]
});
const {output} = await bundle.generate({
format: 'es',
// TODO: can remove this?
name: `hydrogenBundle`
name: `hydrogen`
});
const code = output[0].code;
return code;
@ -214,7 +217,7 @@ async function buildJsLegacy(mainFile, extraFiles = []) {
const bundle = await rollup(rollupConfig);
const {output} = await bundle.generate({
format: 'iife',
name: `hydrogenBundle`
name: `hydrogen`
});
const code = output[0].code;
return code;
@ -460,4 +463,4 @@ class AssetMap {
}
}
build(program).catch(err => console.error(err));
build(parameters).catch(err => console.error(err));

View file

@ -23,10 +23,7 @@ import {ViewModel} from "./ViewModel.js";
export class RootViewModel extends ViewModel {
constructor(options) {
super(options);
const {createSessionContainer, sessionInfoStorage, storageFactory} = options;
this._createSessionContainer = createSessionContainer;
this._sessionInfoStorage = sessionInfoStorage;
this._storageFactory = storageFactory;
this._createSessionContainer = options.createSessionContainer;
this._error = null;
this._sessionPickerViewModel = null;
this._sessionLoadViewModel = null;
@ -90,10 +87,7 @@ export class RootViewModel extends ViewModel {
async _showPicker() {
this._setSection(() => {
this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions({
sessionInfoStorage: this._sessionInfoStorage,
storageFactory: this._storageFactory,
}));
this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions());
});
try {
await this._sessionPickerViewModel.load();
@ -125,11 +119,7 @@ export class RootViewModel extends ViewModel {
_showSession(sessionContainer) {
this._setSection(() => {
this._sessionViewModel = new SessionViewModel(this.childOptions({
sessionContainer,
updateService: this.getOption("updateService"),
estimateStorageUsage: this.getOption("estimateStorageUsage"),
}));
this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer}));
this._sessionViewModel.start();
});
}

View file

@ -130,9 +130,6 @@ class SessionItemViewModel extends ViewModel {
export class SessionPickerViewModel extends ViewModel {
constructor(options) {
super(options);
const {storageFactory, sessionInfoStorage} = options;
this._storageFactory = storageFactory;
this._sessionInfoStorage = sessionInfoStorage;
this._sessions = new SortedArray((s1, s2) => s1.id.localeCompare(s2.id));
this._loadViewModel = null;
this._error = null;
@ -140,7 +137,7 @@ export class SessionPickerViewModel extends ViewModel {
// this loads all the sessions
async load() {
const sessions = await this._sessionInfoStorage.getAll();
const sessions = await this.platform.sessionInfoStorage.getAll();
this._sessions.setManyUnsorted(sessions.map(s => {
return new SessionItemViewModel(this.childOptions({sessionInfo: s}), this);
}));
@ -152,8 +149,8 @@ export class SessionPickerViewModel extends ViewModel {
}
async _exportData(id) {
const sessionInfo = await this._sessionInfoStorage.get(id);
const stores = await this._storageFactory.export(id);
const sessionInfo = await this.platform.sessionInfoStorage.get(id);
const stores = await this.platform.storageFactory.export(id);
const data = {sessionInfo, stores};
return data;
}
@ -164,8 +161,8 @@ export class SessionPickerViewModel extends ViewModel {
const {sessionInfo} = data;
sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`;
sessionInfo.id = this._createSessionContainer().createNewSessionId();
await this._storageFactory.import(sessionInfo.id, data.stores);
await this._sessionInfoStorage.add(sessionInfo);
await this.platform.storageFactory.import(sessionInfo.id, data.stores);
await this.platform.sessionInfoStorage.add(sessionInfo);
this._sessions.set(new SessionItemViewModel(sessionInfo, this));
} catch (err) {
alert(err.message);
@ -175,13 +172,13 @@ export class SessionPickerViewModel extends ViewModel {
async delete(id) {
const idx = this._sessions.array.findIndex(s => s.id === id);
await this._sessionInfoStorage.delete(id);
await this._storageFactory.delete(id);
await this.platform.sessionInfoStorage.delete(id);
await this.platform.storageFactory.delete(id);
this._sessions.remove(idx);
}
async clear(id) {
await this._storageFactory.delete(id);
await this.platform.storageFactory.delete(id);
}
get sessions() {

View file

@ -30,8 +30,8 @@ export class ViewModel extends EventEmitter {
}
childOptions(explicitOptions) {
const {navigation, urlCreator, clock} = this._options;
return Object.assign({navigation, urlCreator, clock}, explicitOptions);
const {navigation, urlCreator, platform} = this._options;
return Object.assign({navigation, urlCreator, platform}, explicitOptions);
}
// makes it easier to pass through dependencies of a sub-view model
@ -100,8 +100,12 @@ export class ViewModel extends EventEmitter {
}
}
get platform() {
return this._options.platform;
}
get clock() {
return this._options.clock;
return this._options.platform.clock;
}
/**

View file

@ -188,9 +188,7 @@ export class SessionViewModel extends ViewModel {
}
if (settingsOpen) {
this._settingsViewModel = this.track(new SettingsViewModel(this.childOptions({
updateService: this.getOption("updateService"),
session: this._sessionContainer.session,
estimateStorageUsage: this.getOption("estimateStorageUsage")
})));
this._settingsViewModel.load();
}

View file

@ -43,7 +43,7 @@ export class TimelineViewModel extends ViewModel {
// once we support sending messages we could do
// timeline.entries.concat(timeline.pendingEvents)
// for an ObservableList that also contains local echos
this._tiles = new TilesCollection(timeline.entries, tilesCreator({room, ownUserId, clock: this.clock}));
this._tiles = new TilesCollection(timeline.entries, tilesCreator({room, ownUserId, platform: this.platform}));
}
async load() {

View file

@ -21,7 +21,6 @@ export class MessageTile extends SimpleTile {
constructor(options) {
super(options);
this._mediaRepository = options.mediaRepository;
this._clock = options.clock;
this._isOwn = this._entry.sender === options.ownUserId;
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
this._isContinuation = false;
@ -88,8 +87,8 @@ export class MessageTile extends SimpleTile {
let isContinuation = false;
if (prev && prev instanceof MessageTile && prev.sender === this.sender) {
// timestamp is null for pending events
const myTimestamp = this._entry.timestamp || this._clock.now();
const otherTimestamp = prev._entry.timestamp || this._clock.now();
const myTimestamp = this._entry.timestamp || this.clock.now();
const otherTimestamp = prev._entry.timestamp || this.clock.now();
// other message was sent less than 5min ago
isContinuation = (myTimestamp - otherTimestamp) < (5 * 60 * 1000);
}

View file

@ -23,9 +23,9 @@ import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
export function tilesCreator({room, ownUserId, clock}) {
export function tilesCreator({room, ownUserId, platform}) {
return function tilesCreator(entry, emitUpdate) {
const options = {entry, emitUpdate, ownUserId, clock,
const options = {entry, emitUpdate, ownUserId, platform,
mediaRepository: room.mediaRepository};
if (entry.isGap) {
return new GapTile(options, room);

View file

@ -35,12 +35,11 @@ export class SettingsViewModel extends ViewModel {
this._session = session;
this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session})));
this._closeUrl = this.urlCreator.urlUntilSegment("session");
this._estimateStorageUsage = options.estimateStorageUsage;
this._estimate = null;
}
async load() {
this._estimate = await this._estimateStorageUsage();
this._estimate = await this.platform.estimateStorageUsage();
this.emitChange("");
}
@ -61,18 +60,19 @@ export class SettingsViewModel extends ViewModel {
}
get version() {
if (this._updateService) {
return `${this._updateService.version} (${this._updateService.buildHash})`;
const {updateService} = this.platform;
if (updateService) {
return `${updateService.version} (${updateService.buildHash})`;
}
return this.i18n`development version`;
}
checkForUpdate() {
this._updateService?.checkForUpdate();
this.platform.updateService?.checkForUpdate();
}
get showUpdateButton() {
return !!this._updateService;
return !!this.platform.updateService;
}
get sessionBackupViewModel() {

View file

@ -1,6 +0,0 @@
import aesjs from "../lib/aes-js/index.js";
import {hkdf} from "./utils/crypto/hkdf.js";
// these are run-time dependencies that are only needed for the legacy bundle.
// they are exported here and passed into main to make them available to the app.
export const legacyExtras = {crypto:{aesjs, hkdf}};

View file

@ -16,82 +16,14 @@ limitations under the License.
*/
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js";
import {createFetchRequest} from "./matrix/net/request/fetch.js";
import {xhrRequest} from "./matrix/net/request/xhr.js";
import {SessionContainer} from "./matrix/SessionContainer.js";
import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js";
import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js";
import {RootViewModel} from "./domain/RootViewModel.js";
import {createNavigation, createRouter} from "./domain/navigation/index.js";
import {RootView} from "./platform/web/ui/RootView.js";
import {Clock} from "./platform/web/dom/Clock.js";
import {ServiceWorkerHandler} from "./platform/web/dom/ServiceWorkerHandler.js";
import {History} from "./platform/web/dom/History.js";
import {OnlineStatus} from "./platform/web/dom/OnlineStatus.js";
import {CryptoDriver} from "./platform/web/dom/CryptoDriver.js";
import {estimateStorageUsage} from "./platform/web/dom/StorageEstimate.js";
import {WorkerPool} from "./utils/WorkerPool.js";
import {OlmWorker} from "./matrix/e2ee/OlmWorker.js";
function addScript(src) {
return new Promise(function (resolve, reject) {
var s = document.createElement("script");
s.setAttribute("src", src );
s.onload=resolve;
s.onerror=reject;
document.body.appendChild(s);
});
}
async function loadOlm(olmPaths) {
// make crypto.getRandomValues available without
// a prefix on IE11, needed by olm to work
if (window.msCrypto && !window.crypto) {
window.crypto = window.msCrypto;
}
if (olmPaths) {
if (window.WebAssembly) {
await addScript(olmPaths.wasmBundle);
await window.Olm.init({locateFile: () => olmPaths.wasm});
} else {
await addScript(olmPaths.legacyBundle);
await window.Olm.init();
}
return window.Olm;
}
return null;
}
// make path relative to basePath,
// assuming it and basePath are relative to document
function relPath(path, basePath) {
const idx = basePath.lastIndexOf("/");
const dir = idx === -1 ? "" : basePath.slice(0, idx);
const dirCount = dir.length ? dir.split("/").length : 0;
return "../".repeat(dirCount) + path;
}
async function loadOlmWorker(paths) {
const workerPool = new WorkerPool(paths.worker, 4);
await workerPool.init();
const path = relPath(paths.olm.legacyBundle, paths.worker);
await workerPool.sendAll({type: "load_olm", path});
const olmWorker = new OlmWorker(workerPool);
return olmWorker;
}
// Don't use a default export here, as we use multiple entries during legacy build,
// which does not support default exports,
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
export async function main(container, paths, legacyExtras) {
export async function main(platform) {
try {
// TODO: add .legacy to .hydrogen (container) in (legacy)platform.createAndMountRootView; and use .hydrogen:not(.legacy) if needed for modern stuff
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
if (isIE11) {
document.body.className += " ie11";
} else {
document.body.className += " not-ie11";
}
// to replay:
// const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json();
// const replay = new ReplayRequester(fetchLog, {delay: false});
@ -101,61 +33,25 @@ export async function main(container, paths, legacyExtras) {
// const recorder = new RecordRequester(createFetchRequest(clock.createTimeout));
// const request = recorder.request;
// window.getBrawlFetchLog = () => recorder.log();
const clock = new Clock();
let request;
if (typeof fetch === "function") {
request = createFetchRequest(clock.createTimeout);
} else {
request = xhrRequest;
}
const navigation = createNavigation();
const sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
let serviceWorkerHandler;
if (paths.serviceWorker && "serviceWorker" in navigator) {
serviceWorkerHandler = new ServiceWorkerHandler({navigation});
serviceWorkerHandler.registerAndStart(paths.serviceWorker);
}
const storageFactory = new StorageFactory(serviceWorkerHandler);
const olmPromise = loadOlm(paths.olm);
// if wasm is not supported, we'll want
// to run some olm operations in a worker (mainly for IE11)
let workerPromise;
if (!window.WebAssembly) {
workerPromise = loadOlmWorker(paths);
}
const urlRouter = createRouter({navigation, history: new History()});
platform.setNavigation(navigation);
const urlRouter = createRouter({navigation, history: platform.history});
urlRouter.attach();
const olmPromise = platform.loadOlm();
const workerPromise = platform.loadOlmWorker();
const vm = new RootViewModel({
createSessionContainer: () => {
return new SessionContainer({
random: Math.random,
onlineStatus: new OnlineStatus(),
storageFactory,
sessionInfoStorage,
request,
clock,
cryptoDriver: new CryptoDriver(legacyExtras?.crypto),
olmPromise,
workerPromise,
});
return new SessionContainer({platform, olmPromise, workerPromise});
},
sessionInfoStorage,
storageFactory,
clock,
platform,
// the only public interface of the router is to create urls,
// so we call it that in the view models
urlCreator: urlRouter,
navigation,
updateService: serviceWorkerHandler,
estimateStorageUsage
});
window.__hydrogenViewModel = vm;
await vm.load();
// TODO: replace with platform.createAndMountRootView(vm, container);
const view = new RootView(vm);
container.appendChild(view.mount());
platform.createAndMountRootView(vm);
} catch(err) {
console.error(`${err.message}:\n${err.stack}`);
}

View file

@ -41,8 +41,8 @@ const PICKLE_KEY = "DEFAULT_KEY";
export class Session {
// sessionInfo contains deviceId, userId and homeServer
constructor({clock, storage, hsApi, sessionInfo, olm, olmWorker, cryptoDriver, mediaRepository}) {
this._clock = clock;
constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) {
this._platform = platform;
this._storage = storage;
this._hsApi = hsApi;
this._mediaRepository = mediaRepository;
@ -61,7 +61,6 @@ export class Session {
this._megolmDecryption = null;
this._getSyncToken = () => this.syncToken;
this._olmWorker = olmWorker;
this._cryptoDriver = cryptoDriver;
this._sessionBackup = null;
this._hasSecretStorageKey = new ObservableValue(null);
@ -106,7 +105,7 @@ export class Session {
pickleKey: PICKLE_KEY,
olm: this._olm,
storage: this._storage,
now: this._clock.now,
now: this._platform.clock.now,
ownUserId: this._user.id,
senderKeyLock
});
@ -115,7 +114,7 @@ export class Session {
pickleKey: PICKLE_KEY,
olm: this._olm,
storage: this._storage,
now: this._clock.now,
now: this._platform.clock.now,
ownUserId: this._user.id,
olmUtil: this._olmUtil,
senderKeyLock
@ -125,7 +124,7 @@ export class Session {
pickleKey: PICKLE_KEY,
olm: this._olm,
storage: this._storage,
now: this._clock.now,
now: this._platform.clock.now,
ownDeviceId: this._sessionInfo.deviceId,
});
this._megolmDecryption = new MegOlmDecryption({
@ -166,7 +165,7 @@ export class Session {
this.needsSessionBackup.set(true)
}
},
clock: this._clock
clock: this._platform.clock
});
}
@ -185,7 +184,7 @@ export class Session {
if (this._sessionBackup) {
return false;
}
const key = await ssssKeyFromCredential(type, credential, this._storage, this._cryptoDriver, this._olm);
const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform.crypto, this._olm);
// and create session backup, which needs to read from accountData
const readTxn = this._storage.readTxn([
this._storage.storeNames.accountData,
@ -207,7 +206,7 @@ export class Session {
}
async _createSessionBackup(ssssKey, txn) {
const secretStorage = new SecretStorage({key: ssssKey, cryptoDriver: this._cryptoDriver});
const secretStorage = new SecretStorage({key: ssssKey, crypto: this._platform.crypto});
this._sessionBackup = await SessionBackup.fromSecretStorage({olm: this._olm, secretStorage, hsApi: this._hsApi, txn});
if (this._sessionBackup) {
for (const room of this._rooms.values()) {
@ -363,7 +362,7 @@ export class Session {
pendingEvents,
user: this._user,
createRoomEncryption: this._createRoomEncryption,
clock: this._clock
clock: this._platform.clock
});
this._rooms.add(roomId, room);
return room;

View file

@ -44,13 +44,8 @@ export const LoginFailure = createEnum(
);
export class SessionContainer {
constructor({clock, random, onlineStatus, request, storageFactory, sessionInfoStorage, olmPromise, workerPromise, cryptoDriver}) {
this._random = random;
this._clock = clock;
this._onlineStatus = onlineStatus;
this._request = request;
this._storageFactory = storageFactory;
this._sessionInfoStorage = sessionInfoStorage;
constructor({platform, olmPromise, workerPromise}) {
this._platform = platform;
this._sessionStartedByReconnector = false;
this._status = new ObservableValue(LoadStatus.NotLoading);
this._error = null;
@ -63,11 +58,10 @@ export class SessionContainer {
this._requestScheduler = null;
this._olmPromise = olmPromise;
this._workerPromise = workerPromise;
this._cryptoDriver = cryptoDriver;
}
createNewSessionId() {
return (Math.floor(this._random() * Number.MAX_SAFE_INTEGER)).toString();
return (Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)).toString();
}
get sessionId() {
@ -80,7 +74,7 @@ export class SessionContainer {
}
this._status.set(LoadStatus.Loading);
try {
const sessionInfo = await this._sessionInfoStorage.get(sessionId);
const sessionInfo = await this._platform.sessionInfoStorage.get(sessionId);
if (!sessionInfo) {
throw new Error("Invalid session id: " + sessionId);
}
@ -96,9 +90,11 @@ export class SessionContainer {
return;
}
this._status.set(LoadStatus.Login);
const clock = this._platform.clock;
let sessionInfo;
try {
const hsApi = new HomeServerApi({homeServer, request: this._request, createTimeout: this._clock.createTimeout});
const request = this._platform.request;
const hsApi = new HomeServerApi({homeServer, request, createTimeout: clock.createTimeout});
const loginData = await hsApi.passwordLogin(username, password, "Hydrogen").response();
const sessionId = this.createNewSessionId();
sessionInfo = {
@ -107,7 +103,7 @@ export class SessionContainer {
userId: loginData.user_id,
homeServer: homeServer,
accessToken: loginData.access_token,
lastUsed: this._clock.now()
lastUsed: clock.now()
};
await this._sessionInfoStorage.add(sessionInfo);
} catch (err) {
@ -139,22 +135,23 @@ export class SessionContainer {
}
async _loadSessionInfo(sessionInfo, isNewLogin) {
const clock = this._platform.clock;
this._sessionStartedByReconnector = false;
this._status.set(LoadStatus.Loading);
this._reconnector = new Reconnector({
onlineStatus: this._onlineStatus,
retryDelay: new ExponentialRetryDelay(this._clock.createTimeout),
createMeasure: this._clock.createMeasure
onlineStatus: this._platform.onlineStatus,
retryDelay: new ExponentialRetryDelay(clock.createTimeout),
createMeasure: clock.createMeasure
});
const hsApi = new HomeServerApi({
homeServer: sessionInfo.homeServer,
accessToken: sessionInfo.accessToken,
request: this._request,
request: this._platform.request,
reconnector: this._reconnector,
createTimeout: this._clock.createTimeout
createTimeout: clock.createTimeout
});
this._sessionId = sessionInfo.id;
this._storage = await this._storageFactory.create(sessionInfo.id);
this._storage = await this._platform.storageFactory.create(sessionInfo.id);
// no need to pass access token to session
const filteredSessionInfo = {
deviceId: sessionInfo.deviceId,
@ -166,16 +163,15 @@ export class SessionContainer {
if (this._workerPromise) {
olmWorker = await this._workerPromise;
}
this._requestScheduler = new RequestScheduler({hsApi, clock: this._clock});
this._requestScheduler = new RequestScheduler({hsApi, clock});
this._requestScheduler.start();
this._session = new Session({
storage: this._storage,
sessionInfo: filteredSessionInfo,
hsApi: this._requestScheduler.hsApi,
olm,
clock: this._clock,
olmWorker,
cryptoDriver: this._cryptoDriver,
platform: this._platform,
mediaRepository: new MediaRepository(sessionInfo.homeServer)
});
await this._session.load();
@ -293,8 +289,8 @@ export class SessionContainer {
// if one fails, don't block the other from trying
// also, run in parallel
await Promise.all([
this._storageFactory.delete(this._sessionId),
this._sessionInfoStorage.delete(this._sessionId),
this._platform.storageFactory.delete(this._sessionId),
this._platform.sessionInfoStorage.delete(this._sessionId),
]);
this._sessionId = null;
}

View file

@ -26,25 +26,3 @@ export function encodeQueryParams(queryParams) {
})
.join("&");
}
export function addCacheBuster(urlStr, random = Math.random) {
// XHR doesn't have a good way to disable cache,
// so add a random query param
// see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
if (urlStr.includes("?")) {
urlStr = urlStr + "&";
} else {
urlStr = urlStr + "?";
}
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
}
export function tests() {
return {
"add cache buster": assert => {
const random = () => 0.5;
assert.equal(addCacheBuster("http://foo", random), "http://foo?_cacheBuster=4503599627370496");
assert.equal(addCacheBuster("http://foo?bar=baz", random), "http://foo?bar=baz&_cacheBuster=4503599627370496");
}
}
}

View file

@ -17,9 +17,9 @@ limitations under the License.
import base64 from "../../../lib/base64-arraybuffer/index.js";
export class SecretStorage {
constructor({key, cryptoDriver}) {
constructor({key, crypto}) {
this._key = key;
this._cryptoDriver = cryptoDriver;
this._crypto = crypto;
}
async readSecret(name, txn) {
@ -44,7 +44,7 @@ export class SecretStorage {
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
// now derive the aes and mac key from the 4s key
const hkdfKey = await this._cryptoDriver.derive.hkdf(
const hkdfKey = await this._crypto.derive.hkdf(
this._key.binaryKey,
new Uint8Array(8).buffer, //zero salt
textEncoder.encode(type), // info
@ -56,7 +56,7 @@ export class SecretStorage {
const ciphertextBytes = base64.decode(encryptedData.ciphertext);
const isVerified = await this._cryptoDriver.hmac.verify(
const isVerified = await this._crypto.hmac.verify(
hmacKey, base64.decode(encryptedData.mac),
ciphertextBytes, "SHA-256");
@ -64,7 +64,7 @@ export class SecretStorage {
throw new Error("Bad MAC");
}
const plaintextBytes = await this._cryptoDriver.aes.decrypt(
const plaintextBytes = await this._crypto.aes.decrypt(
aesKey, base64.decode(encryptedData.iv), ciphertextBytes);
return textDecoder.decode(plaintextBytes);

View file

@ -47,14 +47,14 @@ export async function readKey(txn) {
return new Key(new KeyDescription(keyData.id, keyAccountData), keyData.binaryKey);
}
export async function keyFromCredential(type, credential, storage, cryptoDriver, olm) {
export async function keyFromCredential(type, credential, storage, crypto, olm) {
const keyDescription = await readDefaultKeyDescription(storage);
if (!keyDescription) {
throw new Error("Could not find a default secret storage key in account data");
}
let key;
if (type === "phrase") {
key = await keyFromPassphrase(keyDescription, credential, cryptoDriver);
key = await keyFromPassphrase(keyDescription, credential, crypto);
} else if (type === "key") {
key = keyFromRecoveryKey(olm, keyDescription, credential);
} else {

View file

@ -22,10 +22,10 @@ const DEFAULT_BITSIZE = 256;
/**
* @param {KeyDescription} keyDescription
* @param {string} passphrase
* @param {CryptoDriver} cryptoDriver
* @param {Crypto} crypto
* @return {Key}
*/
export async function keyFromPassphrase(keyDescription, passphrase, cryptoDriver) {
export async function keyFromPassphrase(keyDescription, passphrase, crypto) {
const {passphraseParams} = keyDescription;
if (!passphraseParams) {
throw new Error("not a passphrase key");
@ -35,7 +35,7 @@ export async function keyFromPassphrase(keyDescription, passphrase, cryptoDriver
}
// TODO: we should we move this to platform specific code
const textEncoder = new TextEncoder();
const keyBits = await cryptoDriver.derive.pbkdf2(
const keyBits = await crypto.derive.pbkdf2(
textEncoder.encode(passphrase),
passphraseParams.iterations || DEFAULT_ITERATIONS,
// salt is just a random string, not encoded in any way

View file

@ -0,0 +1,7 @@
import aesjs from "../../../lib/aes-js/index.js";
import {hkdf} from "../../utils/crypto/hkdf.js";
import {Platform as ModernPlatform} from "./Platform.js";
export function Platform(container, paths) {
return new ModernPlatform(container, paths, {aesjs, hkdf});
}

View file

@ -0,0 +1,130 @@
/*
Copyright 2020 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 {createFetchRequest} from "./dom/request/fetch.js";
import {xhrRequest} from "./dom/request/xhr.js";
import {StorageFactory} from "../../matrix/storage/idb/StorageFactory.js";
import {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionInfoStorage.js";
import {OlmWorker} from "../../matrix/e2ee/OlmWorker.js";
import {RootView} from "./ui/RootView.js";
import {Clock} from "./dom/Clock.js";
import {ServiceWorkerHandler} from "./dom/ServiceWorkerHandler.js";
import {History} from "./dom/History.js";
import {OnlineStatus} from "./dom/OnlineStatus.js";
import {Crypto} from "./dom/Crypto.js";
import {estimateStorageUsage} from "./dom/StorageEstimate.js";
import {WorkerPool} from "./dom/WorkerPool.js";
function addScript(src) {
return new Promise(function (resolve, reject) {
var s = document.createElement("script");
s.setAttribute("src", src );
s.onload=resolve;
s.onerror=reject;
document.body.appendChild(s);
});
}
async function loadOlm(olmPaths) {
// make crypto.getRandomValues available without
// a prefix on IE11, needed by olm to work
if (window.msCrypto && !window.crypto) {
window.crypto = window.msCrypto;
}
if (olmPaths) {
if (window.WebAssembly) {
await addScript(olmPaths.wasmBundle);
await window.Olm.init({locateFile: () => olmPaths.wasm});
} else {
await addScript(olmPaths.legacyBundle);
await window.Olm.init();
}
return window.Olm;
}
return null;
}
// make path relative to basePath,
// assuming it and basePath are relative to document
function relPath(path, basePath) {
const idx = basePath.lastIndexOf("/");
const dir = idx === -1 ? "" : basePath.slice(0, idx);
const dirCount = dir.length ? dir.split("/").length : 0;
return "../".repeat(dirCount) + path;
}
async function loadOlmWorker(paths) {
const workerPool = new WorkerPool(paths.worker, 4);
await workerPool.init();
const path = relPath(paths.olm.legacyBundle, paths.worker);
await workerPool.sendAll({type: "load_olm", path});
const olmWorker = new OlmWorker(workerPool);
return olmWorker;
}
export class Platform {
constructor(container, paths, cryptoExtras = null) {
this._paths = paths;
this._container = container;
this.clock = new Clock();
this.history = new History();
this.onlineStatus = new OnlineStatus();
this._serviceWorkerHandler = null;
if (paths.serviceWorker && "serviceWorker" in navigator) {
this._serviceWorkerHandler = new ServiceWorkerHandler();
this._serviceWorkerHandler.registerAndStart(paths.serviceWorker);
}
this.crypto = new Crypto(cryptoExtras);
this.storageFactory = new StorageFactory(this._serviceWorkerHandler);
this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
this.estimateStorageUsage = estimateStorageUsage;
this.random = Math.random;
if (typeof fetch === "function") {
this.request = createFetchRequest(this.clock.createTimeout);
} else {
this.request = xhrRequest;
}
}
get updateService() {
return this._serviceWorkerHandler;
}
loadOlm() {
return loadOlm(this._paths.olm);
}
async loadOlmWorker() {
if (!window.WebAssembly) {
return await loadOlmWorker(this._paths);
}
}
createAndMountRootView(vm) {
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
if (isIE11) {
this._container.className += " legacy";
}
window.__hydrogenViewModel = vm;
const view = new RootView(vm);
this._container.appendChild(view.mount());
}
setNavigation(navigation) {
this._serviceWorkerHandler?.setNavigation(navigation);
}
}

View file

@ -26,7 +26,7 @@ function subtleCryptoResult(promiseOrOp, method) {
}
}
class CryptoHMACDriver {
class HMACCrypto {
constructor(subtleCrypto) {
this._subtleCrypto = subtleCrypto;
}
@ -80,10 +80,10 @@ class CryptoHMACDriver {
}
}
class CryptoDeriveDriver {
constructor(subtleCrypto, cryptoDriver, cryptoExtras) {
class DeriveCrypto {
constructor(subtleCrypto, crypto, cryptoExtras) {
this._subtleCrypto = subtleCrypto;
this._cryptoDriver = cryptoDriver;
this._crypto = crypto;
this._cryptoExtras = cryptoExtras;
}
/**
@ -130,7 +130,7 @@ class CryptoDeriveDriver {
*/
async hkdf(key, salt, info, hash, length) {
if (!this._subtleCrypto.deriveBits) {
return this._cryptoExtras.hkdf(this._cryptoDriver, key, salt, info, hash, length);
return this._cryptoExtras.hkdf(this._crypto, key, salt, info, hash, length);
}
const hkdfkey = await subtleCryptoResult(this._subtleCrypto.importKey(
'raw',
@ -152,7 +152,7 @@ class CryptoDeriveDriver {
}
}
class CryptoAESDriver {
class AESCrypto {
constructor(subtleCrypto) {
this._subtleCrypto = subtleCrypto;
}
@ -196,7 +196,7 @@ class CryptoAESDriver {
}
class CryptoLegacyAESDriver {
class AESLegacyCrypto {
constructor(aesjs) {
this._aesjs = aesjs;
}
@ -221,20 +221,20 @@ function hashName(name) {
return name;
}
export class CryptoDriver {
export class Crypto {
constructor(cryptoExtras) {
const crypto = window.crypto || window.msCrypto;
const subtleCrypto = crypto.subtle || crypto.webkitSubtle;
this._subtleCrypto = subtleCrypto;
// not exactly guaranteeing AES-CTR support
// but in practice IE11 doesn't have this
if (!subtleCrypto.deriveBits && cryptoExtras.aesjs) {
this.aes = new CryptoLegacyAESDriver(cryptoExtras.aesjs);
if (!subtleCrypto.deriveBits && cryptoExtras?.aesjs) {
this.aes = new AESLegacyCrypto(cryptoExtras.aesjs);
} else {
this.aes = new CryptoAESDriver(subtleCrypto);
this.aes = new AESCrypto(subtleCrypto);
}
this.hmac = new CryptoHMACDriver(subtleCrypto);
this.derive = new CryptoDeriveDriver(subtleCrypto, this, cryptoExtras);
this.hmac = new HMACCrypto(subtleCrypto);
this.derive = new DeriveCrypto(subtleCrypto, this, cryptoExtras);
}
/**

View file

@ -19,15 +19,19 @@ limitations under the License.
// - UpdateService (see checkForUpdate method, and should also emit events rather than showing confirm dialog here)
// - ConcurrentAccessBlocker (see preventConcurrentSessionAccess method)
export class ServiceWorkerHandler {
constructor({navigation}) {
constructor() {
this._waitingForReply = new Map();
this._messageIdCounter = 0;
this._navigation = null;
this._registration = null;
this._navigation = navigation;
this._registrationPromise = null;
this._currentController = null;
}
setNavigation(navigation) {
this._navigation = navigation;
}
registerAndStart(path) {
this._registrationPromise = (async () => {
navigator.serviceWorker.addEventListener("message", this);
@ -61,7 +65,7 @@ export class ServiceWorkerHandler {
}
_closeSessionIfNeeded(sessionId) {
const currentSession = this._navigation.path.get("session");
const currentSession = this._navigation?.path.get("session");
if (sessionId && currentSession?.value === sessionId) {
return new Promise(resolve => {
const unsubscribe = this._navigation.pathObservable.subscribe(path => {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {AbortError} from "./error.js";
import {AbortError} from "../../../utils/error.js";
class WorkerState {
constructor(worker) {

View file

@ -0,0 +1,38 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 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.
*/
export function addCacheBuster(urlStr, random = Math.random) {
// XHR doesn't have a good way to disable cache,
// so add a random query param
// see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
if (urlStr.includes("?")) {
urlStr = urlStr + "&";
} else {
urlStr = urlStr + "?";
}
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
}
export function tests() {
return {
"add cache buster": assert => {
const random = () => 0.5;
assert.equal(addCacheBuster("http://foo", random), "http://foo?_cacheBuster=4503599627370496");
assert.equal(addCacheBuster("http://foo?bar=baz", random), "http://foo?bar=baz&_cacheBuster=4503599627370496");
}
}
}

View file

@ -18,9 +18,9 @@ limitations under the License.
import {
AbortError,
ConnectionError
} from "../../error.js";
import {abortOnTimeout} from "../timeout.js";
import {addCacheBuster} from "../common.js";
} from "../../../../matrix/error.js";
import {abortOnTimeout} from "./timeout.js";
import {addCacheBuster} from "./common.js";
class RequestResult {
constructor(promise, controller) {

View file

@ -18,7 +18,7 @@ limitations under the License.
import {
AbortError,
ConnectionError
} from "../error.js";
} from "../../../../matrix/error.js";
export function abortOnTimeout(createTimeout, timeoutAmount, requestResult, responsePromise) {

View file

@ -17,8 +17,8 @@ limitations under the License.
import {
AbortError,
ConnectionError
} from "../../error.js";
import {addCacheBuster} from "../common.js";
} from "../../../../matrix/error.js";
import {addCacheBuster} from "./common.js";
class RequestResult {
constructor(promise, xhr) {

View file

@ -15,8 +15,8 @@ limitations under the License.
*/
// polyfills needed for IE11
import Promise from "../lib/es6-promise/index.js";
import {checkNeedsSyncPromise} from "./matrix/storage/idb/utils.js";
import Promise from "../../../lib/es6-promise/index.js";
import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils.js";
if (typeof window.Promise === "undefined") {
window.Promise = Promise;

View file

@ -32,7 +32,7 @@ limitations under the License.
}
}
.not-ie11 .spinner circle {
.hydrogen:not(.legacy) .spinner circle {
transform-origin: 50% 50%;
animation-name: spinner;
animation-duration: 2s;
@ -45,35 +45,35 @@ limitations under the License.
stroke-linecap: butt;
}
.ie11 .spinner {
.hydrogen.legacy .spinner {
display: inline-block;
position: relative;
}
.ie11 .spinner div {
.hydrogen.legacy .spinner div {
box-sizing: border-box;
display: block;
position: absolute;
padding: 2px;
border: 2px solid currentcolor;
border-radius: 50%;
animation: ie-spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
animation: legacy-spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: currentcolor transparent transparent transparent;
width: var(--size);
height: var(--size);
}
.ie11 .spinner div:nth-child(1) {
.hydrogen.legacy .spinner div:nth-child(1) {
animation-delay: -0.45s;
}
.ie11 .spinner div:nth-child(2) {
.hydrogen.legacy .spinner div:nth-child(2) {
animation-delay: -0.3s;
}
.ie11 .spinner div:nth-child(3) {
.hydrogen.legacy .spinner div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes ie-spinner {
@keyframes legacy-spinner {
0% {
transform: rotate(0deg);
}

View file

@ -19,7 +19,7 @@ limitations under the License.
// just enough to run olm, have promises and async/await
// load this first just in case anything else depends on it
import Promise from "../lib/es6-promise/index.js";
import Promise from "../../../../lib/es6-promise/index.js";
// not calling checkNeedsSyncPromise from here as we don't do any idb in the worker,
// mainly because IE doesn't handle multiple concurrent connections well
self.Promise = Promise;