From 0030ba7015c8e2626d739abe6cdad77ea0a4f27c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 18 Oct 2018 16:42:54 -0600 Subject: [PATCH 1/5] Support .well-known discovery Fixes https://github.com/vector-im/riot-web/issues/7253 --- src/components/structures/login/Login.js | 152 +++++++++++++++++++- src/components/views/login/PasswordLogin.js | 9 +- src/components/views/login/ServerConfig.js | 17 +++ src/i18n/strings/en_EN.json | 2 + 4 files changed, 173 insertions(+), 7 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 45f523f141..2ab922c827 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -26,6 +26,7 @@ import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; +import request from 'browser-request'; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -74,6 +75,11 @@ module.exports = React.createClass({ phoneCountry: null, phoneNumber: "", currentFlow: "m.login.password", + + // .well-known discovery + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: "", }; }, @@ -102,6 +108,15 @@ 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.discoveredHsUrl) { + console.log("Rewriting username because the homeserver was discovered"); + username = username.substring(1).split(":")[0]; + } + this.setState({ busy: true, errorText: null, @@ -218,8 +233,12 @@ module.exports = React.createClass({ }).done(); }, - onUsernameChanged: function(username) { + onUsernameChanged: function(username, endOfInput) { this.setState({ username: username }); + if (username[0] === "@" && endOfInput) { + const serverName = username.split(':').slice(1).join(':'); + this._tryWellKnownDiscovery(serverName); + } }, onPhoneCountryChanged: function(phoneCountry) { @@ -257,6 +276,125 @@ module.exports = React.createClass({ }); }, + _tryWellKnownDiscovery: async function(serverName) { + if (!serverName.trim()) { + // Nothing to discover + this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""}); + return; + } + + 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 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; + } + + // XXX: We don't verify the identity server URL because sydent doesn't register + // the route we need. + + // 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; @@ -418,6 +556,8 @@ module.exports = React.createClass({ const ServerConfig = sdk.getComponent("login.ServerConfig"); const loader = this.state.busy ?
: null; + const errorText = this.state.discoveryError || this.state.errorText; + let loginAsGuestJsx; if (this.props.enableGuest) { loginAsGuestJsx = @@ -432,8 +572,8 @@ module.exports = React.createClass({ if (!SdkConfig.get()['disable_custom_urls']) { serverConfig = { _t('Sign in') } { loader }; } else { - if (!this.state.errorText) { + if (!errorText) { header =

{ _t('Sign in to get started') } { loader }

; } } let errorTextSection; - if (this.state.errorText) { + if (errorText) { errorTextSection = (
- { this.state.errorText } + { errorText }
); } diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index a0e5ab0ddb..0e3aed1187 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -53,6 +53,7 @@ class PasswordLogin extends React.Component { this.onSubmitForm = this.onSubmitForm.bind(this); this.onUsernameChanged = this.onUsernameChanged.bind(this); + this.onUsernameBlur = this.onUsernameBlur.bind(this); this.onLoginTypeChange = this.onLoginTypeChange.bind(this); this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this); this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this); @@ -121,7 +122,11 @@ class PasswordLogin extends React.Component { onUsernameChanged(ev) { this.setState({username: ev.target.value}); - this.props.onUsernameChanged(ev.target.value); + this.props.onUsernameChanged(ev.target.value, false); + } + + onUsernameBlur(ev) { + this.props.onUsernameChanged(this.state.username, true); } onLoginTypeChange(loginType) { @@ -167,6 +172,7 @@ class PasswordLogin extends React.Component { type="text" name="username" // make it a little easier for browser's remember-password onChange={this.onUsernameChanged} + onBlur={this.onUsernameBlur} placeholder="joe@example.com" value={this.state.username} autoFocus @@ -182,6 +188,7 @@ class PasswordLogin extends React.Component { type="text" name="username" // make it a little easier for browser's remember-password onChange={this.onUsernameChanged} + onBlur={this.onUsernameBlur} placeholder={SdkConfig.get().disable_custom_urls ? _t("Username on %(hs)s", { hs: this.props.hsUrl.replace(/^https?:\/\//, ''), diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index a6944ec20a..2f04011273 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -70,6 +70,23 @@ module.exports = React.createClass({ }; }, + componentWillReceiveProps: function(newProps) { + if (newProps.customHsUrl === this.state.hs_url && + newProps.customIsUrl === this.state.is_url) return; + + this.setState({ + hs_url: newProps.customHsUrl, + is_url: newProps.customIsUrl, + configVisible: !newProps.withToggleButton || + (newProps.customHsUrl !== newProps.defaultHsUrl) || + (newProps.customIsUrl !== newProps.defaultIsUrl), + }); + this.props.onServerConfigChange({ + hsUrl: newProps.customHsUrl, + isUrl: newProps.customIsUrl, + }); + }, + onHomeserverChanged: function(ev) { this.setState({hs_url: ev.target.value}, function() { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e42d3ce434..be0963656d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1205,6 +1205,8 @@ "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", "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", "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.", From 71a97170d79fdf1f5941f4cb6c742b05fb5cc7f1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 18 Oct 2018 17:06:47 -0600 Subject: [PATCH 2/5] Linting --- src/components/structures/login/Login.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 2ab922c827..6a33e9369b 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -384,7 +384,7 @@ module.exports = React.createClass({ try { resolve(JSON.parse(body)); - }catch (e) { + } catch (e) { console.error(e); if (e.name === "SyntaxError") { reject({wkAction: "FAIL_PROMPT", wkError: "Invalid JSON"}); From 4cfefe4c3c58bca6a19ed12b06fa8df86fcd875d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Nov 2018 14:13:56 -0700 Subject: [PATCH 3/5] Introduce an onUsernameBlur and fix hostname parsing --- src/components/structures/login/Login.js | 20 +++++++++++++++++--- src/components/views/login/PasswordLogin.js | 5 +++-- src/i18n/strings/en_EN.json | 1 + 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 67d9bb7d38..fae7cc37df 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -232,11 +232,24 @@ module.exports = React.createClass({ }).done(); }, - onUsernameChanged: function(username, endOfInput) { + onUsernameChanged: function(username) { this.setState({ username: username }); - if (username[0] === "@" && endOfInput) { + }, + + onUsernameBlur: function(username) { + this.setState({ username: username }); + if (username[0] === "@") { const serverName = username.split(':').slice(1).join(':'); - this._tryWellKnownDiscovery(serverName); + try { + // we have to append 'https://' to make the URL constructor happy + // otherwise we get things like 'protocol: matrix.org, pathname: 8448' + const url = new URL("https://" + serverName); + this._tryWellKnownDiscovery(url.hostname); + } catch (e) { + console.error("Problem parsing URL or unhandled error doing .well-known discovery"); + console.error(e); + this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); + } } }, @@ -531,6 +544,7 @@ module.exports = React.createClass({ initialPhoneCountry={this.state.phoneCountry} initialPhoneNumber={this.state.phoneNumber} onUsernameChanged={this.onUsernameChanged} + onUsernameBlur={this.onUsernameBlur} onPhoneCountryChanged={this.onPhoneCountryChanged} onPhoneNumberChanged={this.onPhoneNumberChanged} onForgotPasswordClick={this.props.onForgotPasswordClick} diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 0e3aed1187..6a5577fb62 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -30,6 +30,7 @@ class PasswordLogin extends React.Component { static defaultProps = { onError: function() {}, onUsernameChanged: function() {}, + onUsernameBlur: function() {}, onPasswordChanged: function() {}, onPhoneCountryChanged: function() {}, onPhoneNumberChanged: function() {}, @@ -122,11 +123,11 @@ class PasswordLogin extends React.Component { onUsernameChanged(ev) { this.setState({username: ev.target.value}); - this.props.onUsernameChanged(ev.target.value, false); + this.props.onUsernameChanged(ev.target.value); } onUsernameBlur(ev) { - this.props.onUsernameChanged(this.state.username, true); + this.props.onUsernameBlur(this.state.username); } onLoginTypeChange(loginType) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index eb0a7fb1db..f9980f5645 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1288,6 +1288,7 @@ "Incorrect username and/or password.": "Incorrect username and/or password.", "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "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", From 5600a9d02adc61f9420234a4090d9342743443db Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Nov 2018 14:14:08 -0700 Subject: [PATCH 4/5] Validate the identity server --- src/components/structures/login/Login.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index fae7cc37df..6002f058f4 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -115,11 +115,6 @@ module.exports = React.createClass({ // discovery went wrong if (this.state.discoveryError) return; - if (this.state.discoveredHsUrl) { - console.log("Rewriting username because the homeserver was discovered"); - username = username.substring(1).split(":")[0]; - } - this.setState({ busy: true, errorText: null, @@ -327,16 +322,13 @@ module.exports = React.createClass({ return; } - // XXX: We don't verify the identity server URL because sydent doesn't register - // the route we need. - - // 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; - // } + 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: ""}); From 015af7d771673074161f7ab64aa413b02b176ebf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Nov 2018 13:41:34 -0700 Subject: [PATCH 5/5] Use sensible logging --- src/components/structures/login/Login.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 6002f058f4..bd18699dd1 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -241,8 +241,7 @@ module.exports = React.createClass({ const url = new URL("https://" + serverName); this._tryWellKnownDiscovery(url.hostname); } catch (e) { - console.error("Problem parsing URL or unhandled error doing .well-known discovery"); - console.error(e); + console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); } }