Merge pull request #2227 from matrix-org/travis/well-known

Support .well-known discovery
pull/21833/head
Travis Ralston 2018-11-27 13:51:44 -07:00 committed by GitHub
commit e3f2e69087
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 178 additions and 5 deletions

View File

@ -26,6 +26,7 @@ import Login from '../../../Login';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import request from 'browser-request';
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -74,6 +75,11 @@ module.exports = React.createClass({
phoneCountry: null, phoneCountry: null,
phoneNumber: "", phoneNumber: "",
currentFlow: "m.login.password", currentFlow: "m.login.password",
// .well-known discovery
discoveredHsUrl: "",
discoveredIsUrl: "",
discoveryError: "",
}; };
}, },
@ -105,6 +111,10 @@ module.exports = React.createClass({
}, },
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
// Prevent people from submitting their password when homeserver
// discovery went wrong
if (this.state.discoveryError) return;
this.setState({ this.setState({
busy: true, busy: true,
errorText: null, errorText: null,
@ -221,6 +231,22 @@ module.exports = React.createClass({
this.setState({ username: username }); this.setState({ username: username });
}, },
onUsernameBlur: function(username) {
this.setState({ username: username });
if (username[0] === "@") {
const serverName = username.split(':').slice(1).join(':');
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:", e);
this.setState({discoveryError: _t("Failed to perform homeserver discovery")});
}
}
},
onPhoneCountryChanged: function(phoneCountry) { onPhoneCountryChanged: function(phoneCountry) {
this.setState({ phoneCountry: phoneCountry }); this.setState({ phoneCountry: phoneCountry });
}, },
@ -256,6 +282,122 @@ 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;
}
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) { _initLoginLogic: function(hsUrl, isUrl) {
const self = this; const self = this;
hsUrl = hsUrl || this.state.enteredHomeserverUrl; hsUrl = hsUrl || this.state.enteredHomeserverUrl;
@ -393,6 +535,7 @@ module.exports = React.createClass({
initialPhoneCountry={this.state.phoneCountry} initialPhoneCountry={this.state.phoneCountry}
initialPhoneNumber={this.state.phoneNumber} initialPhoneNumber={this.state.phoneNumber}
onUsernameChanged={this.onUsernameChanged} onUsernameChanged={this.onUsernameChanged}
onUsernameBlur={this.onUsernameBlur}
onPhoneCountryChanged={this.onPhoneCountryChanged} onPhoneCountryChanged={this.onPhoneCountryChanged}
onPhoneNumberChanged={this.onPhoneNumberChanged} onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick} onForgotPasswordClick={this.props.onForgotPasswordClick}
@ -416,6 +559,8 @@ module.exports = React.createClass({
const ServerConfig = sdk.getComponent("login.ServerConfig"); const ServerConfig = sdk.getComponent("login.ServerConfig");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null; const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
const errorText = this.state.discoveryError || this.state.errorText;
let loginAsGuestJsx; let loginAsGuestJsx;
if (this.props.enableGuest) { if (this.props.enableGuest) {
loginAsGuestJsx = loginAsGuestJsx =
@ -430,8 +575,8 @@ module.exports = React.createClass({
if (!SdkConfig.get()['disable_custom_urls']) { if (!SdkConfig.get()['disable_custom_urls']) {
serverConfig = <ServerConfig ref="serverConfig" serverConfig = <ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
customHsUrl={this.props.customHsUrl} customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
customIsUrl={this.props.customIsUrl} customIsUrl={this.state.discoveredIsUrl ||this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl} defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl} defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
@ -443,16 +588,16 @@ module.exports = React.createClass({
if (theme !== "status") { if (theme !== "status") {
header = <h2>{ _t('Sign in') } { loader }</h2>; header = <h2>{ _t('Sign in') } { loader }</h2>;
} else { } else {
if (!this.state.errorText) { if (!errorText) {
header = <h2>{ _t('Sign in to get started') } { loader }</h2>; header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
} }
} }
let errorTextSection; let errorTextSection;
if (this.state.errorText) { if (errorText) {
errorTextSection = ( errorTextSection = (
<div className="mx_Login_error"> <div className="mx_Login_error">
{ this.state.errorText } { errorText }
</div> </div>
); );
} }

View File

@ -30,6 +30,7 @@ class PasswordLogin extends React.Component {
static defaultProps = { static defaultProps = {
onError: function() {}, onError: function() {},
onUsernameChanged: function() {}, onUsernameChanged: function() {},
onUsernameBlur: function() {},
onPasswordChanged: function() {}, onPasswordChanged: function() {},
onPhoneCountryChanged: function() {}, onPhoneCountryChanged: function() {},
onPhoneNumberChanged: function() {}, onPhoneNumberChanged: function() {},
@ -53,6 +54,7 @@ class PasswordLogin extends React.Component {
this.onSubmitForm = this.onSubmitForm.bind(this); this.onSubmitForm = this.onSubmitForm.bind(this);
this.onUsernameChanged = this.onUsernameChanged.bind(this); this.onUsernameChanged = this.onUsernameChanged.bind(this);
this.onUsernameBlur = this.onUsernameBlur.bind(this);
this.onLoginTypeChange = this.onLoginTypeChange.bind(this); this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this); this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this); this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
@ -124,6 +126,10 @@ class PasswordLogin extends React.Component {
this.props.onUsernameChanged(ev.target.value); this.props.onUsernameChanged(ev.target.value);
} }
onUsernameBlur(ev) {
this.props.onUsernameBlur(this.state.username);
}
onLoginTypeChange(loginType) { onLoginTypeChange(loginType) {
this.props.onError(null); // send a null error to clear any error messages this.props.onError(null); // send a null error to clear any error messages
this.setState({ this.setState({
@ -167,6 +173,7 @@ class PasswordLogin extends React.Component {
type="text" type="text"
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
placeholder="joe@example.com" placeholder="joe@example.com"
value={this.state.username} value={this.state.username}
autoFocus autoFocus
@ -182,6 +189,7 @@ class PasswordLogin extends React.Component {
type="text" type="text"
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
placeholder={SdkConfig.get().disable_custom_urls ? placeholder={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", { _t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''), 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) { onHomeserverChanged: function(ev) {
this.setState({hs_url: ev.target.value}, function() { this.setState({hs_url: ev.target.value}, function() {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {

View File

@ -1288,7 +1288,10 @@
"Incorrect username and/or password.": "Incorrect username and/or password.", "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.", "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.", "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", "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.", "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.", "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>.", "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>.",