From 633be5061c0a197b2c8dab1c6d7641d8a37dfe51 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 4 Dec 2018 23:34:57 -0700 Subject: [PATCH] Introduce a default_server_name for aesthetics and rework .well-known Fixes https://github.com/vector-im/riot-web/issues/7724 The `default_server_name` from the config gets displayed in the "Login with my [server] matrix ID" dropdown when the default server is being used. At this point, we also discourage the use of the `default_hs_url` and `default_is_url` options because we do an implicit .well-known lookup to configure the client based on the `default_server_name`. If the URLs are still present in the config, we'll honour them and won't do a .well-known lookup when the URLs are mixed with the new server_name option. Users will be warned if the `default_server_name` does not match the `default_hs_url` if both are supplied. Users are additionally prevented from logging in, registering, and resetting their password if the implicit .well-known check fails - this is to prevent people from doing actions against the wrong homeserver. This relies on https://github.com/matrix-org/matrix-js-sdk/pull/799 as we now do auto discovery in two places. Instead of bringing the .well-known out to its own utility class in the react-sdk, we might as well drag it out to the js-sdk. --- res/css/structures/login/_Login.scss | 7 + src/components/structures/MatrixChat.js | 46 ++++- .../structures/login/ForgotPassword.js | 23 +++ src/components/structures/login/Login.js | 157 ++++++------------ .../structures/login/Registration.js | 23 ++- src/components/views/login/PasswordLogin.js | 20 ++- src/i18n/strings/en_EN.json | 8 +- 7 files changed, 166 insertions(+), 118 deletions(-) diff --git a/res/css/structures/login/_Login.scss b/res/css/structures/login/_Login.scss index 1264d2a30f..9b19c24b14 100644 --- a/res/css/structures/login/_Login.scss +++ b/res/css/structures/login/_Login.scss @@ -180,6 +180,13 @@ limitations under the License. margin-bottom: 12px; } +.mx_Login_subtext { + display: block; + font-size: 0.8em; + text-align: center; + margin: 10px; +} + .mx_Login_type_container { display: flex; margin-bottom: 14px; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4d7c71e3ef..dc3872664b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -48,6 +48,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; +const AutoDiscovery = Matrix.AutoDiscovery; + // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. Promise.config({warnings: false}); @@ -181,6 +183,12 @@ export default React.createClass({ register_is_url: null, register_id_sid: null, + // Parameters used for setting up the login/registration views + defaultServerName: this.props.config.default_server_name, + defaultHsUrl: this.props.config.default_hs_url, + defaultIsUrl: this.props.config.default_is_url, + defaultServerDiscoveryError: null, + // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: false, @@ -199,6 +207,10 @@ export default React.createClass({ }; }, + getDefaultServerName: function() { + return this.state.defaultServerName; + }, + getCurrentHsUrl: function() { if (this.state.register_hs_url) { return this.state.register_hs_url; @@ -211,8 +223,10 @@ export default React.createClass({ } }, - getDefaultHsUrl() { - return this.props.config.default_hs_url || "https://matrix.org"; + getDefaultHsUrl(defaultToMatrixDotOrg) { + defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg; + if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org"; + return this.state.defaultHsUrl; }, getFallbackHsUrl: function() { @@ -232,7 +246,7 @@ export default React.createClass({ }, getDefaultIsUrl() { - return this.props.config.default_is_url || "https://vector.im"; + return this.state.defaultIsUrl || "https://vector.im"; }, componentWillMount: function() { @@ -282,6 +296,11 @@ export default React.createClass({ console.info(`Team token set to ${this._teamToken}`); } + // Set up the default URLs (async) + if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { + this._tryDiscoverDefaultHomeserver(this.getDefaultServerName()); + } + // Set a default HS with query param `hs_url` const paramHs = this.props.startingFragmentQueryParams.hs_url; if (paramHs) { @@ -1732,6 +1751,21 @@ export default React.createClass({ this.setState(newState); }, + _tryDiscoverDefaultHomeserver: async function(serverName) { + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS) { + console.error("Failed to discover homeserver on startup:", discovery); + this.setState({defaultServerDiscoveryError: discovery["m.homeserver"].error}); + } else { + const hsUrl = discovery["m.homeserver"].base_url; + const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "https://vector.im"; + this.setState({defaultHsUrl: hsUrl, defaultIsUrl: isUrl}); + } + }, + _makeRegistrationUrl: function(params) { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; @@ -1820,6 +1854,8 @@ export default React.createClass({ idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} referrer={this.props.startingFragmentQueryParams.referrer} + defaultServerName={this.getDefaultServerName()} + defaultServerDiscoveryError={this.state.defaultServerDiscoveryError} defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} @@ -1842,6 +1878,8 @@ export default React.createClass({ const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); return ( { err }; + } + const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector'); resetPasswordJsx = ( @@ -230,6 +252,7 @@ module.exports = React.createClass({ { serverConfigSection } + { errorText } { _t('Return to login screen') } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index bd18699dd1..08e94e413a 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -26,11 +26,17 @@ import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import request from 'browser-request'; +import { AutoDiscovery } from "matrix-js-sdk"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; +// These are used in several places, and come from the js-sdk's autodiscovery +// stuff. We define them here so that they'll be picked up by i18n. +_td("Invalid homeserver discovery response"); +_td("Invalid identity server discovery response"); +_td("General failure"); + /** * A wire component which glues together login UI components and Login logic */ @@ -51,6 +57,14 @@ module.exports = React.createClass({ // different home server without confusing users. fallbackHsUrl: PropTypes.string, + // The default server name to use when the user hasn't specified + // one. This is used when displaying the defaultHsUrl in the UI. + defaultServerName: PropTypes.string, + + // An error passed along from higher up explaining that something + // went wrong when finding the defaultHsUrl. + defaultServerDiscoveryError: PropTypes.string, + defaultDeviceDisplayName: PropTypes.string, // login shouldn't know or care how registration is done. @@ -113,7 +127,7 @@ module.exports = React.createClass({ onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { // Prevent people from submitting their password when homeserver // discovery went wrong - if (this.state.discoveryError) return; + if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return; this.setState({ busy: true, @@ -290,114 +304,43 @@ module.exports = React.createClass({ } try { - const wellknown = await this._getWellKnownObject(`https://${serverName}/.well-known/matrix/client`); - if (!wellknown["m.homeserver"]) { - console.error("No m.homeserver key in well-known response"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) { + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: discovery["m.homeserver"].error, + }); + } else if (state === AutoDiscovery.PROMPT) { + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: "", + }); + } else if (state === AutoDiscovery.SUCCESS) { + this.setState({ + discoveredHsUrl: discovery["m.homeserver"].base_url, + discoveredIsUrl: + discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "", + discoveryError: "", + }); + } else { + console.warn("Unknown state for m.homeserver in discovery response: ", discovery); + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: _t("Unknown failure discovering homeserver"), + }); } - - const hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]); - if (!hsUrl) { - console.error("Invalid base_url for m.homeserver"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - - console.log("Verifying homeserver URL: " + hsUrl); - const hsVersions = await this._getWellKnownObject(`${hsUrl}/_matrix/client/versions`); - if (!hsVersions["versions"]) { - console.error("Invalid /versions response"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - - let isUrl = ""; - if (wellknown["m.identity_server"]) { - isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]); - if (!isUrl) { - console.error("Invalid base_url for m.identity_server"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - - console.log("Verifying identity server URL: " + isUrl); - const isResponse = await this._getWellKnownObject(`${isUrl}/_matrix/identity/api/v1`); - if (!isResponse) { - console.error("Invalid /api/v1 response"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - } - - this.setState({discoveredHsUrl: hsUrl, discoveredIsUrl: isUrl, discoveryError: ""}); } catch (e) { console.error(e); - if (e.wkAction) { - if (e.wkAction === "FAIL_ERROR" || e.wkAction === "FAIL_PROMPT") { - // We treat FAIL_ERROR and FAIL_PROMPT the same to avoid having the user - // submit their details to the wrong homeserver. In practice, the custom - // server options will show up to try and guide the user into entering - // the required information. - this.setState({discoveryError: _t("Cannot find homeserver")}); - return; - } else if (e.wkAction === "IGNORE") { - // Nothing to discover - this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""}); - return; - } - } - throw e; } }, - _sanitizeWellKnownUrl: function(url) { - if (!url) return false; - - const parser = document.createElement('a'); - parser.href = url; - - if (parser.protocol !== "http:" && parser.protocol !== "https:") return false; - if (!parser.hostname) return false; - - const port = parser.port ? `:${parser.port}` : ""; - const path = parser.pathname ? parser.pathname : ""; - let saferUrl = `${parser.protocol}//${parser.hostname}${port}${path}`; - if (saferUrl.endsWith("/")) saferUrl = saferUrl.substring(0, saferUrl.length - 1); - return saferUrl; - }, - - _getWellKnownObject: function(url) { - return new Promise(function(resolve, reject) { - request( - { method: "GET", url: url }, - (err, response, body) => { - if (err || response.status < 200 || response.status >= 300) { - let action = "FAIL_ERROR"; - if (response.status === 404) { - // We could just resolve with an empty object, but that - // causes a different series of branches when the m.homeserver - // bit of the JSON is missing. - action = "IGNORE"; - } - reject({err: err, response: response, wkAction: action}); - return; - } - - try { - resolve(JSON.parse(body)); - } catch (e) { - console.error(e); - if (e.name === "SyntaxError") { - reject({wkAction: "FAIL_PROMPT", wkError: "Invalid JSON"}); - } else throw e; - } - }, - ); - }); - }, - _initLoginLogic: function(hsUrl, isUrl) { const self = this; hsUrl = hsUrl || this.state.enteredHomeserverUrl; @@ -527,6 +470,9 @@ module.exports = React.createClass({ _renderPasswordStep: function() { const PasswordLogin = sdk.getComponent('login.PasswordLogin'); + const hsName = this.state.enteredHomeserverUrl === this.props.defaultHsUrl + ? this.props.defaultServerName + : null; return ( ); }, @@ -559,7 +506,7 @@ module.exports = React.createClass({ const ServerConfig = sdk.getComponent("login.ServerConfig"); const loader = this.state.busy ?
: null; - const errorText = this.state.discoveryError || this.state.errorText; + const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; let loginAsGuestJsx; if (this.props.enableGuest) { @@ -576,7 +523,7 @@ module.exports = React.createClass({ serverConfig = { this.state.errorText }; + const err = this.state.errorText || this.props.defaultServerDiscoveryError; + if (theme === 'status' && err) { + header =
{ err }
; } else { header =

{ _t('Create an account') }

; - if (this.state.errorText) { - errorText =
{ this.state.errorText }
; + if (err) { + errorText =
{ err }
; } } diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 6a5577fb62..582ccf94dd 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -40,6 +40,7 @@ class PasswordLogin extends React.Component { initialPassword: "", loginIncorrect: false, hsDomain: "", + hsName: null, } constructor(props) { @@ -250,13 +251,24 @@ class PasswordLogin extends React.Component { ); } - let matrixIdText = ''; + let matrixIdText = _t('Matrix ID'); + let matrixIdSubtext = null; + if (this.props.hsName) { + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName}); + } if (this.props.hsUrl) { try { const parsedHsUrl = new URL(this.props.hsUrl); - matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); + if (!this.props.hsName) { + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); + } else if (parsedHsUrl.hostname !== this.props.hsName) { + matrixIdSubtext = _t('%(serverName)s is located at %(homeserverUrl)s', { + serverName: this.props.hsName, + homeserverUrl: this.props.hsUrl, + }); + } } catch (e) { - // pass + // ignore } } @@ -292,6 +304,7 @@ class PasswordLogin extends React.Component {
{ loginType } + { matrixIdSubtext } { loginField } {this._passwordField = e;}} type="password" name="password" @@ -325,6 +338,7 @@ PasswordLogin.propTypes = { onPhoneNumberChanged: PropTypes.func, onPasswordChanged: PropTypes.func, loginIncorrect: PropTypes.bool, + hsName: PropTypes.string, }; module.exports = PasswordLogin; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e4aad2c55d..22764c2e77 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -686,6 +686,7 @@ "Mobile phone number": "Mobile phone number", "Forgot your password?": "Forgot your password?", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", + "%(serverName)s is located at %(homeserverUrl)s": "%(serverName)s is located at %(homeserverUrl)s", "Sign in with": "Sign in with", "Email address": "Email address", "Sign in": "Sign in", @@ -831,7 +832,6 @@ "And %(count)s more...|other": "And %(count)s more...", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", - "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", "You have entered an invalid address.": "You have entered an invalid address.", @@ -1283,6 +1283,9 @@ "Confirm your new password": "Confirm your new password", "Send Reset Email": "Send Reset Email", "Create an account": "Create an account", + "Invalid homeserver discovery response": "Invalid homeserver discovery response", + "Invalid identity server discovery response": "Invalid identity server discovery response", + "General failure": "General failure", "This Home Server does not support login using email address.": "This Home Server does not support login using email address.", "Please contact your service administrator to continue using this service.": "Please contact your service administrator to continue using this service.", "Incorrect username and/or password.": "Incorrect username and/or password.", @@ -1290,8 +1293,7 @@ "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "The phone number entered looks invalid": "The phone number entered looks invalid", - "Invalid homeserver discovery response": "Invalid homeserver discovery response", - "Cannot find homeserver": "Cannot find homeserver", + "Unknown failure discovering homeserver": "Unknown failure discovering homeserver", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.",