From 3a5b7c1d0ed4939e9e6b23b621da7c83a3140f6d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 18:47:36 +0200 Subject: [PATCH 01/11] support well-known lookup --- src/matrix/SessionContainer.js | 22 +++++++++--- src/matrix/well-known.js | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/matrix/well-known.js diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index ab20a4eb..7233eeb0 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +Copyright 2020, 2021 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. @@ -15,6 +16,7 @@ limitations under the License. */ import {createEnum} from "../utils/enum.js"; +import {lookupHomeServer} from "./well-known.js"; import {AbortableOperation} from "../utils/AbortableOperation"; import {ObservableValue} from "../observable/ObservableValue.js"; import {HomeServerApi} from "./net/HomeServerApi.js"; @@ -36,6 +38,16 @@ function normalizeHomeserver(homeServer) { } } +function getRetryHomeServer(homeServer) { + const url = new URL(homeServer); + const {host} = url; + const dotCount = host.split(".").length - 1; + if (dotCount === 1) { + url.host = `www.${host}`; + return url.origin; + } +} + export const LoadStatus = createEnum( "NotLoading", "Login", @@ -108,7 +120,7 @@ export class SessionContainer { implements LoginMethod */ const flows = options.flows; - const result = {}; + const result = {homeServer}; for (const flow of flows) { if (flow.type === "m.login.password") { result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password}); @@ -124,11 +136,13 @@ export class SessionContainer { } queryLogin(homeServer) { - const normalizedHS = normalizeHomeserver(homeServer); - const hsApi = new HomeServerApi({homeServer: normalizedHS, request: this._platform.request}); return new AbortableOperation(async setAbortable => { + homeServer = await lookupHomeServer(homeServer, (url, options) => { + return setAbortable(this._platform.request(url, options)); + }); + const hsApi = new HomeServerApi({homeServer, request: this._platform.request}); const response = await setAbortable(hsApi.getLoginFlows()).response(); - return this._parseLoginOptions(response, normalizedHS); + return this._parseLoginOptions(response, homeServer); }); } diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js new file mode 100644 index 00000000..a33a734a --- /dev/null +++ b/src/matrix/well-known.js @@ -0,0 +1,64 @@ +/* +Copyright 2021 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. +*/ + +function normalizeHomeserver(homeServer) { + try { + return new URL(homeServer).origin; + } catch (err) { + return new URL(`https://${homeServer}`).origin; + } +} + +function getRetryHomeServer(homeServer) { + const url = new URL(homeServer); + const {host} = url; + const dotCount = host.split(".").length - 1; + if (dotCount === 1) { + url.host = `www.${host}`; + return url.origin; + } +} + +export async function lookupHomeServer(homeServer, request) { + homeServer = normalizeHomeserver(homeServer); + const requestOptions = {format: "json", timeout: 30000, method: "GET"}; + let wellKnownResponse = null; + while (!wellKnownResponse) { + try { + const wellKnownUrl = `${homeServer}/.well-known/matrix/client`; + wellKnownResponse = await request(wellKnownUrl, requestOptions).response(); + } catch (err) { + if (err.name === "ConnectionError") { + const retryHS = getRetryHomeServer(homeServer); + if (retryHS) { + homeServer = retryHS; + } else { + throw err; + } + } else { + throw err; + } + } + } + if (wellKnownResponse.status === 200) { + const {body} = wellKnownResponse; + const wellKnownHomeServer = body["m.homeserver"]?.["base_url"]; + if (typeof wellKnownHomeServer === "string") { + homeServer = normalizeHomeserver(wellKnownHomeServer); + } + } + return homeServer +} From c7b47bb8d66d23568fef2390983ef199763e621d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:26:39 +0200 Subject: [PATCH 02/11] fix homeServer misspelling to homeserver across the project --- src/domain/RootViewModel.js | 2 +- src/domain/login/LoginViewModel.js | 12 ++-- src/domain/login/PasswordLoginViewModel.js | 5 +- .../session/settings/SettingsViewModel.js | 2 +- src/matrix/Session.js | 4 +- src/matrix/SessionContainer.js | 50 ++++++----------- src/matrix/login/LoginMethod.js | 4 +- src/matrix/net/HomeServerApi.js | 6 +- src/matrix/net/MediaRepository.js | 8 +-- src/matrix/well-known.js | 56 +++++++++---------- src/platform/web/ui/login/LoginView.js | 4 +- src/platform/web/ui/view-gallery.html | 4 +- 12 files changed, 69 insertions(+), 88 deletions(-) diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index d6fdcfa0..d9949c77 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -105,7 +105,7 @@ export class RootViewModel extends ViewModel { _showLogin(loginToken) { this._setSection(() => { this._loginViewModel = new LoginViewModel(this.childOptions({ - defaultHomeServer: this.platform.config["defaultHomeServer"], + defaultHomeserver: this.platform.config["defaultHomeServer"], createSessionContainer: this._createSessionContainer, ready: sessionContainer => { // we don't want to load the session container again, diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index cd75ca87..d0cf9664 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -24,7 +24,7 @@ import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; export class LoginViewModel extends ViewModel { constructor(options) { super(options); - const {ready, defaultHomeServer, createSessionContainer, loginToken} = options; + const {ready, defaultHomeserver, createSessionContainer, loginToken} = options; this._createSessionContainer = createSessionContainer; this._ready = ready; this._loginToken = loginToken; @@ -35,7 +35,7 @@ export class LoginViewModel extends ViewModel { this._completeSSOLoginViewModel = null; this._loadViewModel = null; this._loadViewModelSubscription = null; - this._homeserver = defaultHomeServer; + this._homeserver = defaultHomeserver; this._errorMessage = ""; this._hideHomeserver = false; this._isBusy = false; @@ -71,7 +71,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("completeSSOLoginViewModel"); } else { - await this.queryHomeServer(); + await this.queryHomeserver(); } } @@ -156,7 +156,7 @@ export class LoginViewModel extends ViewModel { this.emitChange("disposeViewModels"); } - async setHomeServer(newHomeserver) { + async setHomeserver(newHomeserver) { this._homeserver = newHomeserver; // abort ongoing query, if any this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); @@ -174,13 +174,13 @@ export class LoginViewModel extends ViewModel { } } this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); - this.queryHomeServer(); + this.queryHomeserver(); } - async queryHomeServer() { this._errorMessage = ""; this.emitChange("errorMessage"); // if query is called before the typing timeout hits (e.g. field lost focus), cancel the timeout so we don't query again. + async queryHomeserver() { this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); // cancel ongoing query operation, if any this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js index f8d354a4..7ea239e6 100644 --- a/src/domain/login/PasswordLoginViewModel.js +++ b/src/domain/login/PasswordLoginViewModel.js @@ -43,15 +43,14 @@ export class PasswordLoginViewModel extends ViewModel { async login(username, password) { this._errorMessage = ""; this.emitChange("errorMessage"); - const loginMethod = this._loginOptions.password(username, password); - const status = await this._attemptLogin(loginMethod); + const status = await this._attemptLogin(this._loginOptions.password(username, password)); let error = ""; switch (status) { case LoginFailure.Credentials: error = this.i18n`Your username and/or password don't seem to be correct.`; break; case LoginFailure.Connection: - error = this.i18n`Can't connect to ${loginMethod.homeServer}.`; + error = this.i18n`Can't connect to ${this._loginOptions.homeserver}.`; break; case LoginFailure.Unknown: error = this.i18n`Something went wrong while checking your login and password.`; diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 221e39da..7120f5fb 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -151,7 +151,7 @@ export class SettingsViewModel extends ViewModel { this.pushNotifications.enabledOnServer = null; this.pushNotifications.serverError = null; try { - this.pushNotifications.enabledOnServer = await this._session.checkPusherEnabledOnHomeServer(); + this.pushNotifications.enabledOnServer = await this._session.checkPusherEnabledOnHomeserver(); this.emitChange("pushNotifications.enabledOnServer"); } catch (err) { this.pushNotifications.serverError = err; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 5a046117..63aece42 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -46,7 +46,7 @@ const PICKLE_KEY = "DEFAULT_KEY"; const PUSHER_KEY = "pusher"; export class Session { - // sessionInfo contains deviceId, userId and homeServer + // sessionInfo contains deviceId, userId and homeserver constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) { this._platform = platform; this._storage = storage; @@ -636,7 +636,7 @@ export class Session { return !!pusherData; } - async checkPusherEnabledOnHomeServer() { + async checkPusherEnabledOnHomeserver() { const readTxn = await this._storage.readTxn([this._storage.storeNames.session]); const pusherData = await readTxn.session.get(PUSHER_KEY); if (!pusherData) { diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 7233eeb0..f375fdd7 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -16,7 +16,7 @@ limitations under the License. */ import {createEnum} from "../utils/enum.js"; -import {lookupHomeServer} from "./well-known.js"; +import {lookupHomeserver} from "./well-known.js"; import {AbortableOperation} from "../utils/AbortableOperation"; import {ObservableValue} from "../observable/ObservableValue.js"; import {HomeServerApi} from "./net/HomeServerApi.js"; @@ -30,24 +30,6 @@ import {PasswordLoginMethod} from "./login/PasswordLoginMethod.js"; import {TokenLoginMethod} from "./login/TokenLoginMethod.js"; import {SSOLoginHelper} from "./login/SSOLoginHelper.js"; -function normalizeHomeserver(homeServer) { - try { - return new URL(homeServer).origin; - } catch (err) { - return new URL(`https://${homeServer}`).origin; - } -} - -function getRetryHomeServer(homeServer) { - const url = new URL(homeServer); - const {host} = url; - const dotCount = host.split(".").length - 1; - if (dotCount === 1) { - url.host = `www.${host}`; - return url.origin; - } -} - export const LoadStatus = createEnum( "NotLoading", "Login", @@ -66,7 +48,6 @@ export const LoginFailure = createEnum( "Unknown", ); - export class SessionContainer { constructor({platform, olmPromise, workerPromise}) { this._platform = platform; @@ -114,35 +95,35 @@ export class SessionContainer { }); } - _parseLoginOptions(options, homeServer) { + _parseLoginOptions(options, homeserver) { /* Take server response and return new object which has two props password and sso which implements LoginMethod */ const flows = options.flows; - const result = {homeServer}; + const result = {homeserver}; for (const flow of flows) { if (flow.type === "m.login.password") { - result.password = (username, password) => new PasswordLoginMethod({homeServer, username, password}); + result.password = (username, password) => new PasswordLoginMethod({homeserver, username, password}); } else if (flow.type === "m.login.sso" && flows.find(flow => flow.type === "m.login.token")) { - result.sso = new SSOLoginHelper(homeServer); + result.sso = new SSOLoginHelper(homeserver); } else if (flow.type === "m.login.token") { - result.token = loginToken => new TokenLoginMethod({homeServer, loginToken}); + result.token = loginToken => new TokenLoginMethod({homeserver, loginToken}); } } return result; } - queryLogin(homeServer) { + queryLogin(homeserver) { return new AbortableOperation(async setAbortable => { - homeServer = await lookupHomeServer(homeServer, (url, options) => { + homeserver = await lookupHomeserver(homeserver, (url, options) => { return setAbortable(this._platform.request(url, options)); }); - const hsApi = new HomeServerApi({homeServer, request: this._platform.request}); + const hsApi = new HomeServerApi({homeserver, request: this._platform.request}); const response = await setAbortable(hsApi.getLoginFlows()).response(); - return this._parseLoginOptions(response, homeServer); + return this._parseLoginOptions(response, homeserver); }); } @@ -160,14 +141,15 @@ export class SessionContainer { let sessionInfo; try { const request = this._platform.request; - const hsApi = new HomeServerApi({homeServer: loginMethod.homeServer, request}); + const hsApi = new HomeServerApi({homeserver: loginMethod.homeserver, request}); const loginData = await loginMethod.login(hsApi, "Hydrogen", log); const sessionId = this.createNewSessionId(); sessionInfo = { id: sessionId, deviceId: loginData.device_id, userId: loginData.user_id, - homeServer: loginMethod.homeServer, + homeServer: loginMethod.homeserver, // deprecate this over time + homeserver: loginMethod.homeserver, accessToken: loginData.access_token, lastUsed: clock.now() }; @@ -216,7 +198,7 @@ export class SessionContainer { createMeasure: clock.createMeasure }); const hsApi = new HomeServerApi({ - homeServer: sessionInfo.homeServer, + homeserver: sessionInfo.homeServer, accessToken: sessionInfo.accessToken, request: this._platform.request, reconnector: this._reconnector, @@ -228,7 +210,7 @@ export class SessionContainer { id: sessionInfo.id, deviceId: sessionInfo.deviceId, userId: sessionInfo.userId, - homeServer: sessionInfo.homeServer, + homeserver: sessionInfo.homeServer, }; const olm = await this._olmPromise; let olmWorker = null; @@ -238,7 +220,7 @@ export class SessionContainer { this._requestScheduler = new RequestScheduler({hsApi, clock}); this._requestScheduler.start(); const mediaRepository = new MediaRepository({ - homeServer: sessionInfo.homeServer, + homeserver: sessionInfo.homeServer, platform: this._platform, }); this._session = new Session({ diff --git a/src/matrix/login/LoginMethod.js b/src/matrix/login/LoginMethod.js index fee8e845..ece18871 100644 --- a/src/matrix/login/LoginMethod.js +++ b/src/matrix/login/LoginMethod.js @@ -15,8 +15,8 @@ limitations under the License. */ export class LoginMethod { - constructor({homeServer}) { - this.homeServer = homeServer; + constructor({homeserver}) { + this.homeserver = homeserver; } // eslint-disable-next-line no-unused-vars diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 8641e374..4b53b28b 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -19,10 +19,10 @@ import {encodeQueryParams, encodeBody} from "./common.js"; import {HomeServerRequest} from "./HomeServerRequest.js"; export class HomeServerApi { - constructor({homeServer, accessToken, request, reconnector}) { + constructor({homeserver, accessToken, request, reconnector}) { // store these both in a closure somehow so it's harder to get at in case of XSS? // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write - this._homeserver = homeServer; + this._homeserver = homeserver; this._accessToken = accessToken; this._requestFn = request; this._reconnector = reconnector; @@ -234,7 +234,7 @@ export function tests() { "superficial happy path for GET": async assert => { const hsApi = new HomeServerApi({ request: () => new MockRequest().respond(200, 42), - homeServer: "https://hs.tld" + homeserver: "https://hs.tld" }); const result = await hsApi._get("foo", null, null, null).response(); assert.strictEqual(result, 42); diff --git a/src/matrix/net/MediaRepository.js b/src/matrix/net/MediaRepository.js index f7e47cfd..3f718c85 100644 --- a/src/matrix/net/MediaRepository.js +++ b/src/matrix/net/MediaRepository.js @@ -18,8 +18,8 @@ import {encodeQueryParams} from "./common.js"; import {decryptAttachment} from "../e2ee/attachment.js"; export class MediaRepository { - constructor({homeServer, platform}) { - this._homeServer = homeServer; + constructor({homeserver, platform}) { + this._homeserver = homeserver; this._platform = platform; } @@ -27,7 +27,7 @@ export class MediaRepository { const parts = this._parseMxcUrl(url); if (parts) { const [serverName, mediaId] = parts; - const httpUrl = `${this._homeServer}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; + const httpUrl = `${this._homeserver}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; return httpUrl + "?" + encodeQueryParams({width: Math.round(width), height: Math.round(height), method}); } return null; @@ -37,7 +37,7 @@ export class MediaRepository { 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}/_matrix/media/r0/download/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; } else { return null; } diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js index a33a734a..1217c43e 100644 --- a/src/matrix/well-known.js +++ b/src/matrix/well-known.js @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -function normalizeHomeserver(homeServer) { +function normalizeHomeserver(homeserver) { try { - return new URL(homeServer).origin; + return new URL(homeserver).origin; } catch (err) { - return new URL(`https://${homeServer}`).origin; + return new URL(`https://${homeserver}`).origin; } } -function getRetryHomeServer(homeServer) { - const url = new URL(homeServer); +function getRetryHomeserver(homeserver) { + const url = new URL(homeserver); const {host} = url; const dotCount = host.split(".").length - 1; if (dotCount === 1) { @@ -32,33 +32,33 @@ function getRetryHomeServer(homeServer) { } } -export async function lookupHomeServer(homeServer, request) { - homeServer = normalizeHomeserver(homeServer); - const requestOptions = {format: "json", timeout: 30000, method: "GET"}; - let wellKnownResponse = null; - while (!wellKnownResponse) { - try { - const wellKnownUrl = `${homeServer}/.well-known/matrix/client`; - wellKnownResponse = await request(wellKnownUrl, requestOptions).response(); - } catch (err) { - if (err.name === "ConnectionError") { - const retryHS = getRetryHomeServer(homeServer); - if (retryHS) { - homeServer = retryHS; - } else { - throw err; - } +export async function lookupHomeserver(homeserver, request) { + homeserver = normalizeHomeserver(homeserver); + const requestOptions = {format: "json", timeout: 30000, method: "GET"}; + let wellKnownResponse = null; + while (!wellKnownResponse) { + try { + const wellKnownUrl = `${homeserver}/.well-known/matrix/client`; + wellKnownResponse = await request(wellKnownUrl, requestOptions).response(); + } catch (err) { + if (err.name === "ConnectionError") { + const retryHS = getRetryHomeserver(homeserver); + if (retryHS) { + homeserver = retryHS; } else { throw err; } + } else { + throw err; } } - if (wellKnownResponse.status === 200) { - const {body} = wellKnownResponse; - const wellKnownHomeServer = body["m.homeserver"]?.["base_url"]; - if (typeof wellKnownHomeServer === "string") { - homeServer = normalizeHomeserver(wellKnownHomeServer); - } + } + if (wellKnownResponse.status === 200) { + const {body} = wellKnownResponse; + const wellKnownHomeserver = body["m.homeserver"]?.["base_url"]; + if (typeof wellKnownHomeserver === "string") { + homeserver = normalizeHomeserver(wellKnownHomeserver); } - return homeServer + } + return homeserver; } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 498cb574..4e4f1061 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -43,8 +43,8 @@ export class LoginView extends TemplateView { placeholder: vm.i18n`Your matrix homeserver`, value: vm.homeserver, disabled, - onInput: () => vm.setHomeServer(event.target.value), - onChange: event => vm.queryHomeServer(), + onInput: event => vm.setHomeserver(event.target.value), + onChange: () => vm.queryHomeserver(), }), t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), ] diff --git a/src/platform/web/ui/view-gallery.html b/src/platform/web/ui/view-gallery.html index 7887d44f..675b2b5b 100644 --- a/src/platform/web/ui/view-gallery.html +++ b/src/platform/web/ui/view-gallery.html @@ -43,7 +43,7 @@ From e944dc5cac453facf7291d0db87927084acc56dd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:27:24 +0200 Subject: [PATCH 03/11] decrease typing timeout to 1s --- src/domain/login/LoginViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index d0cf9664..c634f7db 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -162,7 +162,7 @@ export class LoginViewModel extends ViewModel { this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); this.emitChange("isFetchingLoginOptions"); this.disposeTracked(this._abortHomeserverQueryTimeout); - const timeout = this.clock.createTimeout(2000); + const timeout = this.clock.createTimeout(1000); this._abortHomeserverQueryTimeout = this.track(() => timeout.abort()); try { await timeout.elapsed(); From e0d53e57badb4f407967a468e938818ced87d5b8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:28:21 +0200 Subject: [PATCH 04/11] clear everything when typing in homeserver field as it's not relevant anymore --- src/domain/login/LoginViewModel.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index c634f7db..72cde571 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -158,9 +158,14 @@ export class LoginViewModel extends ViewModel { async setHomeserver(newHomeserver) { this._homeserver = newHomeserver; - // abort ongoing query, if any + // clear everything set by queryHomeserver + this._loginOptions = null; + this._queriedHomeserver = null; + this._showError(""); + this._disposeViewModels(); this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); - this.emitChange("isFetchingLoginOptions"); + this.emitChange(); // multiple fields changing + // also clear the timeout if it is still running this.disposeTracked(this._abortHomeserverQueryTimeout); const timeout = this.clock.createTimeout(1000); this._abortHomeserverQueryTimeout = this.track(() => timeout.abort()); @@ -177,14 +182,16 @@ export class LoginViewModel extends ViewModel { this.queryHomeserver(); } - this._errorMessage = ""; - this.emitChange("errorMessage"); - // if query is called before the typing timeout hits (e.g. field lost focus), cancel the timeout so we don't query again. async queryHomeserver() { + // given that setHomeserver already clears everything set here, + // and that is the only way to change the homeserver, + // we don't need to reset things again here. + // However, clear things set by setHomeserver: + // if query is called before the typing timeout hits (e.g. field lost focus), + // cancel the timeout so we don't query again. this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); // cancel ongoing query operation, if any this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); - this._disposeViewModels(); try { const queryOperation = this._sessionContainer.queryLogin(this._homeserver); this._abortQueryOperation = this.track(() => queryOperation.abort()); @@ -209,7 +216,7 @@ export class LoginViewModel extends ViewModel { } } else { - this._showError("Could not query login methods supported by the homeserver"); + this._showError(`Could not query login methods supported by ${this.homeserver}`); } } From 3dbffdb417ab32695624563cb4a3d1fb79418723 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:28:43 +0200 Subject: [PATCH 05/11] don't requery the same homeserver we just did --- src/domain/login/LoginViewModel.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index 72cde571..d629241d 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -36,6 +36,7 @@ export class LoginViewModel extends ViewModel { this._loadViewModel = null; this._loadViewModelSubscription = null; this._homeserver = defaultHomeserver; + this._queriedHomeserver = null; this._errorMessage = ""; this._hideHomeserver = false; this._isBusy = false; @@ -183,6 +184,11 @@ export class LoginViewModel extends ViewModel { } async queryHomeserver() { + // don't repeat a query we've just done + if (this._homeserver === this._queriedHomeserver || this._homeserver === "") { + return; + } + this._queriedHomeserver = this._homeserver; // given that setHomeserver already clears everything set here, // and that is the only way to change the homeserver, // we don't need to reset things again here. From c0d3c950b02527d9f5d8c310d31cf2c7b08d342e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:28:59 +0200 Subject: [PATCH 06/11] show the homeserver after lookup --- src/domain/login/LoginViewModel.js | 2 ++ src/platform/web/ui/css/themes/element/theme.css | 6 ++++++ src/platform/web/ui/login/LoginView.js | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index d629241d..f2e9f0a5 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -49,6 +49,7 @@ export class LoginViewModel extends ViewModel { get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } get homeserver() { return this._homeserver; } + get forwardedHomeserver() { return this._loginOptions?.homeserver; } get errorMessage() { return this._errorMessage; } get showHomeserver() { return !this._hideHomeserver; } get loadViewModel() {return this._loadViewModel; } @@ -203,6 +204,7 @@ export class LoginViewModel extends ViewModel { this._abortQueryOperation = this.track(() => queryOperation.abort()); this.emitChange("isFetchingLoginOptions"); this._loginOptions = await queryOperation.result; + this.emitChange("forwardedHomeserver"); } catch (e) { if (e.name === "AbortError") { diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 08f1df06..1b0bc9e4 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -238,6 +238,12 @@ a.button-action { font-size: 1.5rem; } +.LoginView_forwardInfo { + font-size: 0.9em; + margin-left: 1em; + color: #777; +} + .CompleteSSOView_title { font-weight: 500; } diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 4e4f1061..fc2bf686 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -46,6 +46,10 @@ export class LoginView extends TemplateView { onInput: event => vm.setHomeserver(event.target.value), onChange: () => vm.queryHomeserver(), }), + t.p({className: { + LoginView_forwardInfo: true, + hidden: vm => !vm.forwardedHomeserver + }}, vm => vm.i18n`${vm.homeserver} forwards to ${vm.forwardedHomeserver}.`), t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), ] )), From 59605a2a57041ec6061a6ec86225a6b4ca9a0569 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:53:33 +0200 Subject: [PATCH 07/11] don't fail login on missing cors on well-known --- src/matrix/well-known.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js index 1217c43e..c29939c2 100644 --- a/src/matrix/well-known.js +++ b/src/matrix/well-known.js @@ -32,28 +32,35 @@ function getRetryHomeserver(homeserver) { } } -export async function lookupHomeserver(homeserver, request) { - homeserver = normalizeHomeserver(homeserver); +async function getWellKnownResponse(homeserver, request) { const requestOptions = {format: "json", timeout: 30000, method: "GET"}; let wellKnownResponse = null; while (!wellKnownResponse) { try { const wellKnownUrl = `${homeserver}/.well-known/matrix/client`; - wellKnownResponse = await request(wellKnownUrl, requestOptions).response(); + return await request(wellKnownUrl, requestOptions).response(); } catch (err) { if (err.name === "ConnectionError") { const retryHS = getRetryHomeserver(homeserver); if (retryHS) { homeserver = retryHS; } else { - throw err; + // don't fail lookup on a ConnectionError, + // there might be a missing CORS header on a 404 response or something, + // which won't be a problem necessarily with homeserver requests later on ... + return null; } } else { throw err; } } } - if (wellKnownResponse.status === 200) { +} + +export async function lookupHomeserver(homeserver, request) { + homeserver = normalizeHomeserver(homeserver); + const wellKnownResponse = await getWellKnownResponse(homeserver, request); + if (wellKnownResponse && wellKnownResponse.status === 200) { const {body} = wellKnownResponse; const wellKnownHomeserver = body["m.homeserver"]?.["base_url"]; if (typeof wellKnownHomeserver === "string") { From 160ae0b76762d24c645598f65d4eed87c7916ab0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 19:53:52 +0200 Subject: [PATCH 08/11] adjust to AbortableOperation api change --- src/domain/login/CompleteSSOLoginViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js index b7e04cc8..7821313a 100644 --- a/src/domain/login/CompleteSSOLoginViewModel.js +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -46,7 +46,7 @@ export class CompleteSSOLoginViewModel extends ViewModel { const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver"); let loginOptions; try { - loginOptions = await this._sessionContainer.queryLogin(homeserver); + loginOptions = await this._sessionContainer.queryLogin(homeserver).result; } catch (err) { this._showError(err.message); From d1412e1f425922a65ce73b17430669329ed9639a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 20:05:42 +0200 Subject: [PATCH 09/11] don't retry with www.{host}, as it's a minor security issue if www.host gets hacked (e.g. a bad wordpress plugin), it could spread to the matrix server running on a different host. --- src/matrix/well-known.js | 40 +++++++++++----------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js index c29939c2..00c91f27 100644 --- a/src/matrix/well-known.js +++ b/src/matrix/well-known.js @@ -22,37 +22,19 @@ function normalizeHomeserver(homeserver) { } } -function getRetryHomeserver(homeserver) { - const url = new URL(homeserver); - const {host} = url; - const dotCount = host.split(".").length - 1; - if (dotCount === 1) { - url.host = `www.${host}`; - return url.origin; - } -} - async function getWellKnownResponse(homeserver, request) { const requestOptions = {format: "json", timeout: 30000, method: "GET"}; - let wellKnownResponse = null; - while (!wellKnownResponse) { - try { - const wellKnownUrl = `${homeserver}/.well-known/matrix/client`; - return await request(wellKnownUrl, requestOptions).response(); - } catch (err) { - if (err.name === "ConnectionError") { - const retryHS = getRetryHomeserver(homeserver); - if (retryHS) { - homeserver = retryHS; - } else { - // don't fail lookup on a ConnectionError, - // there might be a missing CORS header on a 404 response or something, - // which won't be a problem necessarily with homeserver requests later on ... - return null; - } - } else { - throw err; - } + try { + const wellKnownUrl = `${homeserver}/.well-known/matrix/client`; + return await request(wellKnownUrl, requestOptions).response(); + } catch (err) { + if (err.name === "ConnectionError") { + // don't fail lookup on a ConnectionError, + // there might be a missing CORS header on a 404 response or something, + // which won't be a problem necessarily with homeserver requests later on ... + return null; + } else { + throw err; } } } From d31e442c168b6af4a07181bbe872c74fec497941 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 20:06:52 +0200 Subject: [PATCH 10/11] better text --- src/platform/web/ui/login/LoginView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index fc2bf686..8ed91357 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -49,7 +49,7 @@ export class LoginView extends TemplateView { t.p({className: { LoginView_forwardInfo: true, hidden: vm => !vm.forwardedHomeserver - }}, vm => vm.i18n`${vm.homeserver} forwards to ${vm.forwardedHomeserver}.`), + }}, vm => vm.i18n`You will connect to ${vm.forwardedHomeserver}.`), t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), ] )), From 65bd892d8f55bdc537d31a657359a5e26896d339 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Aug 2021 20:09:45 +0200 Subject: [PATCH 11/11] better naming --- src/domain/login/LoginViewModel.js | 4 ++-- src/platform/web/ui/login/LoginView.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js index f2e9f0a5..9cdf9290 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.js @@ -49,7 +49,7 @@ export class LoginViewModel extends ViewModel { get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } get homeserver() { return this._homeserver; } - get forwardedHomeserver() { return this._loginOptions?.homeserver; } + get resolvedHomeserver() { return this._loginOptions?.homeserver; } get errorMessage() { return this._errorMessage; } get showHomeserver() { return !this._hideHomeserver; } get loadViewModel() {return this._loadViewModel; } @@ -204,7 +204,7 @@ export class LoginViewModel extends ViewModel { this._abortQueryOperation = this.track(() => queryOperation.abort()); this.emitChange("isFetchingLoginOptions"); this._loginOptions = await queryOperation.result; - this.emitChange("forwardedHomeserver"); + this.emitChange("resolvedHomeserver"); } catch (e) { if (e.name === "AbortError") { diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 8ed91357..aa89ccca 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -48,8 +48,8 @@ export class LoginView extends TemplateView { }), t.p({className: { LoginView_forwardInfo: true, - hidden: vm => !vm.forwardedHomeserver - }}, vm => vm.i18n`You will connect to ${vm.forwardedHomeserver}.`), + hidden: vm => !vm.resolvedHomeserver + }}, vm => vm.i18n`You will connect to ${vm.resolvedHomeserver}.`), t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), ] )),