From 636cb8a5ccb00e91df624386af66c2c35f310c31 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 22:57:49 -0600 Subject: [PATCH] Have ServerConfig and co. do validation of the config in-house This also causes the components to produce a ValidatedServerConfig for use by other components. --- res/css/views/auth/_ServerConfig.scss | 5 + .../views/auth/ModularServerConfig.js | 92 ++++++++++----- src/components/views/auth/ServerConfig.js | 111 +++++++++++------- src/utils/AutoDiscoveryUtils.js | 104 ++++++++++++++++ 4 files changed, 238 insertions(+), 74 deletions(-) create mode 100644 src/utils/AutoDiscoveryUtils.js diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss index 79ad9e8238..fe96da2019 100644 --- a/res/css/views/auth/_ServerConfig.scss +++ b/res/css/views/auth/_ServerConfig.scss @@ -35,3 +35,8 @@ limitations under the License. .mx_ServerConfig_help:link { opacity: 0.8; } + +.mx_ServerConfig_error { + display: block; + color: $warning-color; +} diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index 9c6c4b01bf..ea22577dbd 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -18,9 +18,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import SdkConfig from "../../../SdkConfig"; +import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; +import * as ServerType from '../../views/auth/ServerTypeSelector'; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; +// TODO: TravisR - Can this extend ServerConfig for most things? + /* * Configure the Modular server name. * @@ -31,65 +37,87 @@ export default class ModularServerConfig extends React.PureComponent { static propTypes = { onServerConfigChange: PropTypes.func, - // default URLs are defined in config.json (or the hardcoded defaults) - // they are used if the user has not overridden them with a custom URL. - // In other words, if the custom URL is blank, the default is used. - defaultHsUrl: PropTypes.string, // e.g. https://matrix.org - - // This component always uses the default IS URL and doesn't allow it - // to be changed. We still receive it as a prop here to simplify - // consumers by still passing the IS URL via onServerConfigChange. - defaultIsUrl: PropTypes.string, // e.g. https://vector.im - - // custom URLs are explicitly provided by the user and override the - // default URLs. The user enters them via the component's input fields, - // which is reflected on these properties whenever on..UrlChanged fires. - // They are persisted in localStorage by MatrixClientPeg, and so can - // override the default URLs when the component initially loads. - customHsUrl: PropTypes.string, + // The current configuration that the user is expecting to change. + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, delayTimeMs: PropTypes.number, // time to wait before invoking onChanged - } + }; static defaultProps = { onServerConfigChange: function() {}, customHsUrl: "", delayTimeMs: 0, - } + }; constructor(props) { super(props); this.state = { - hsUrl: props.customHsUrl, + busy: false, + errorText: "", + hsUrl: props.serverConfig.hsUrl, + isUrl: props.serverConfig.isUrl, }; } componentWillReceiveProps(newProps) { - if (newProps.customHsUrl === this.state.hsUrl) return; + if (newProps.serverConfig.hsUrl === this.state.hsUrl && + newProps.serverConfig.isUrl === this.state.isUrl) return; + + this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); + } + + async validateAndApplyServer(hsUrl, isUrl) { + // Always try and use the defaults first + const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"]; + if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) { + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(defaultConfig); + return defaultConfig; + } this.setState({ - hsUrl: newProps.customHsUrl, - }); - this.props.onServerConfigChange({ - hsUrl: newProps.customHsUrl, - isUrl: this.props.defaultIsUrl, + hsUrl, + isUrl, + busy: true, + errorText: "", }); + + try { + const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(result); + return result; + } catch (e) { + console.error(e); + let message = _t("Unable to validate homeserver/identity server"); + if (e.translatedMessage) { + message = e.translatedMessage; + } + this.setState({ + busy: false, + errorText: message, + }); + } + } + + async validateServer() { + // TODO: Do we want to support .well-known lookups here? + // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to + // find their homeserver without demanding they use "https://matrix.org" + return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl); } onHomeserverBlur = (ev) => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.props.defaultIsUrl, - }); + this.validateServer(); }); - } + }; onHomeserverChange = (ev) => { const hsUrl = ev.target.value; this.setState({ hsUrl }); - } + }; _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { @@ -116,7 +144,7 @@ export default class ModularServerConfig extends React.PureComponent {
{ this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.state.isUrl, - }); + this.validateServer(); }); - } + }; onHomeserverChange = (ev) => { const hsUrl = ev.target.value; this.setState({ hsUrl }); - } + }; onIdentityServerBlur = (ev) => { this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.state.isUrl, - }); + this.validateServer(); }); - } + }; onIdentityServerChange = (ev) => { const isUrl = ev.target.value; this.setState({ isUrl }); - } + }; _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { @@ -114,11 +134,15 @@ export default class ServerConfig extends React.PureComponent { showHelpPopup = () => { const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog'); Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); - } + }; render() { const Field = sdk.getComponent('elements.Field'); + const errorText = this.state.errorText + ? {this.state.errorText} + : null; + return (

{_t("Other servers")}

@@ -127,20 +151,23 @@ export default class ServerConfig extends React.PureComponent { { sub } , })} + {errorText}
diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js new file mode 100644 index 0000000000..318c706136 --- /dev/null +++ b/src/utils/AutoDiscoveryUtils.js @@ -0,0 +1,104 @@ +/* +Copyright 2019 New Vector Ltd + +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 {AutoDiscovery} from "matrix-js-sdk"; +import {_td, newTranslatableError} from "../languageHandler"; +import {makeType} from "./TypeUtils"; +import SdkConfig from "../SdkConfig"; + +export class ValidatedServerConfig { + hsUrl: string; + hsName: string; + hsNameIsDifferent: string; + + isUrl: string; + identityEnabled: boolean; +} + +export default class AutoDiscoveryUtils { + static async validateServerConfigWithStaticUrls(homeserverUrl: string, identityUrl: string): ValidatedServerConfig { + if (!homeserverUrl) { + throw newTranslatableError(_td("No homeserver URL provided")); + } + + const wellknownConfig = { + "m.homeserver": { + base_url: homeserverUrl, + }, + "m.identity_server": { + base_url: identityUrl, + }, + }; + + const result = await AutoDiscovery.fromDiscoveryConfig(wellknownConfig); + + const url = new URL(homeserverUrl); + const serverName = url.hostname; + + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + } + + static async validateServerName(serverName: string): ValidatedServerConfig { + const result = await AutoDiscovery.findClientConfig(serverName); + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + } + + static buildValidatedConfigFromDiscovery(serverName: string, discoveryResult): ValidatedServerConfig { + if (!discoveryResult || !discoveryResult["m.homeserver"]) { + // This shouldn't happen without major misconfiguration, so we'll log a bit of information + // in the log so we can find this bit of codee but otherwise tell teh user "it broke". + console.error("Ended up in a state of not knowing which homeserver to connect to."); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const hsResult = discoveryResult['m.homeserver']; + if (hsResult.state !== AutoDiscovery.SUCCESS) { + if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error) !== -1) { + throw newTranslatableError(hsResult.error); + } + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const isResult = discoveryResult['m.identity_server']; + let preferredIdentityUrl = "https://vector.im"; + if (isResult && isResult.state === AutoDiscovery.SUCCESS) { + preferredIdentityUrl = isResult["base_url"]; + } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { + console.error("Error determining preferred identity server URL:", isResult); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const preferredHomeserverUrl = hsResult["base_url"]; + let preferredHomeserverName = serverName ? serverName : hsResult["server_name"]; + + const url = new URL(preferredHomeserverUrl); + if (!preferredHomeserverName) preferredHomeserverName = url.hostname; + + // It should have been set by now, so check it + if (!preferredHomeserverName) { + console.error("Failed to parse homeserver name from homeserver URL"); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + return makeType(ValidatedServerConfig, { + hsUrl: preferredHomeserverUrl, + hsName: preferredHomeserverName, + hsNameIsDifferent: url.hostname !== preferredHomeserverName, + isUrl: preferredIdentityUrl, + identityEnabled: !SdkConfig.get()['disable_identity_server'], + }); + } +}