mirror of https://github.com/vector-im/riot-web
Merge pull request #2227 from matrix-org/travis/well-known
Support .well-known discoverypull/21833/head
commit
e3f2e69087
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?:\/\//, ''),
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>.",
|
||||||
|
|
Loading…
Reference in New Issue