Support .well-known discovery

Fixes https://github.com/vector-im/riot-web/issues/7253
This commit is contained in:
Travis Ralston 2018-10-18 16:42:54 -06:00
parent b7c05d8dec
commit 0030ba7015
4 changed files with 173 additions and 7 deletions

View file

@ -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 ? <div className="mx_Login_loader"><Loader /></div> : 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 = <ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
customIsUrl={this.state.discoveredIsUrl ||this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
@ -445,16 +585,16 @@ module.exports = React.createClass({
if (theme !== "status") {
header = <h2>{ _t('Sign in') } { loader }</h2>;
} else {
if (!this.state.errorText) {
if (!errorText) {
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
}
}
let errorTextSection;
if (this.state.errorText) {
if (errorText) {
errorTextSection = (
<div className="mx_Login_error">
{ this.state.errorText }
{ errorText }
</div>
);
}

View file

@ -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?:\/\//, ''),

View file

@ -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() {

View file

@ -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 <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",