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.
This commit is contained in:
Travis Ralston 2018-12-04 23:34:57 -07:00
parent c0ef2f7df3
commit 633be5061c
7 changed files with 166 additions and 118 deletions

View file

@ -180,6 +180,13 @@ limitations under the License.
margin-bottom: 12px; margin-bottom: 12px;
} }
.mx_Login_subtext {
display: block;
font-size: 0.8em;
text-align: center;
margin: 10px;
}
.mx_Login_type_container { .mx_Login_type_container {
display: flex; display: flex;
margin-bottom: 14px; margin-bottom: 14px;

View file

@ -48,6 +48,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import { startAnyRegistrationFlow } from "../../Registration.js"; import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils'; import { messageForSyncError } from '../../utils/ErrorUtils';
const AutoDiscovery = Matrix.AutoDiscovery;
// Disable warnings for now: we use deprecated bluebird functions // Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings. // and need to migrate, but they spam the console with warnings.
Promise.config({warnings: false}); Promise.config({warnings: false});
@ -181,6 +183,12 @@ export default React.createClass({
register_is_url: null, register_is_url: null,
register_id_sid: 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 // When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs // and disable it when there are no dialogs
hideToSRUsers: false, hideToSRUsers: false,
@ -199,6 +207,10 @@ export default React.createClass({
}; };
}, },
getDefaultServerName: function() {
return this.state.defaultServerName;
},
getCurrentHsUrl: function() { getCurrentHsUrl: function() {
if (this.state.register_hs_url) { if (this.state.register_hs_url) {
return this.state.register_hs_url; return this.state.register_hs_url;
@ -211,8 +223,10 @@ export default React.createClass({
} }
}, },
getDefaultHsUrl() { getDefaultHsUrl(defaultToMatrixDotOrg) {
return this.props.config.default_hs_url || "https://matrix.org"; defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg;
if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org";
return this.state.defaultHsUrl;
}, },
getFallbackHsUrl: function() { getFallbackHsUrl: function() {
@ -232,7 +246,7 @@ export default React.createClass({
}, },
getDefaultIsUrl() { getDefaultIsUrl() {
return this.props.config.default_is_url || "https://vector.im"; return this.state.defaultIsUrl || "https://vector.im";
}, },
componentWillMount: function() { componentWillMount: function() {
@ -282,6 +296,11 @@ export default React.createClass({
console.info(`Team token set to ${this._teamToken}`); 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` // Set a default HS with query param `hs_url`
const paramHs = this.props.startingFragmentQueryParams.hs_url; const paramHs = this.props.startingFragmentQueryParams.hs_url;
if (paramHs) { if (paramHs) {
@ -1732,6 +1751,21 @@ export default React.createClass({
this.setState(newState); 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) { _makeRegistrationUrl: function(params) {
if (this.props.startingFragmentQueryParams.referrer) { if (this.props.startingFragmentQueryParams.referrer) {
params.referrer = this.props.startingFragmentQueryParams.referrer; params.referrer = this.props.startingFragmentQueryParams.referrer;
@ -1820,6 +1854,8 @@ export default React.createClass({
idSid={this.state.register_id_sid} idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email} email={this.props.startingFragmentQueryParams.email}
referrer={this.props.startingFragmentQueryParams.referrer} referrer={this.props.startingFragmentQueryParams.referrer}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()} defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()} defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand} brand={this.props.config.brand}
@ -1842,6 +1878,8 @@ export default React.createClass({
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
return ( return (
<ForgotPassword <ForgotPassword
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()} defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()} defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}
@ -1858,6 +1896,8 @@ export default React.createClass({
<Login <Login
onLoggedIn={Lifecycle.setLoggedIn} onLoggedIn={Lifecycle.setLoggedIn}
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()} defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()} defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}

View file

@ -36,6 +36,14 @@ module.exports = React.createClass({
onLoginClick: PropTypes.func, onLoginClick: PropTypes.func,
onRegisterClick: PropTypes.func, onRegisterClick: PropTypes.func,
onComplete: PropTypes.func.isRequired, onComplete: PropTypes.func.isRequired,
// 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,
}, },
getInitialState: function() { getInitialState: function() {
@ -45,6 +53,7 @@ module.exports = React.createClass({
progress: null, progress: null,
password: null, password: null,
password2: null, password2: null,
errorText: null,
}; };
}, },
@ -81,6 +90,13 @@ module.exports = React.createClass({
onSubmitForm: function(ev) { onSubmitForm: function(ev) {
ev.preventDefault(); ev.preventDefault();
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
if (!this.state.email) { if (!this.state.email) {
this.showErrorDialog(_t('The email address linked to your account must be entered.')); this.showErrorDialog(_t('The email address linked to your account must be entered.'));
} else if (!this.state.password || !this.state.password2) { } else if (!this.state.password || !this.state.password2) {
@ -200,6 +216,12 @@ module.exports = React.createClass({
); );
} }
let errorText = null;
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
if (err) {
errorText = <div className="mx_Login_error">{ err }</div>;
}
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector'); const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
resetPasswordJsx = ( resetPasswordJsx = (
@ -230,6 +252,7 @@ module.exports = React.createClass({
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} /> <input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
</form> </form>
{ serverConfigSection } { serverConfigSection }
{ errorText }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{ _t('Return to login screen') } { _t('Return to login screen') }
</a> </a>

View file

@ -26,11 +26,17 @@ 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'; import { AutoDiscovery } from "matrix-js-sdk";
// 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]*$/;
// 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 * 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. // different home server without confusing users.
fallbackHsUrl: PropTypes.string, 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, defaultDeviceDisplayName: PropTypes.string,
// login shouldn't know or care how registration is done. // login shouldn't know or care how registration is done.
@ -113,7 +127,7 @@ module.exports = React.createClass({
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
// Prevent people from submitting their password when homeserver // Prevent people from submitting their password when homeserver
// discovery went wrong // discovery went wrong
if (this.state.discoveryError) return; if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
this.setState({ this.setState({
busy: true, busy: true,
@ -290,114 +304,43 @@ module.exports = React.createClass({
} }
try { try {
const wellknown = await this._getWellKnownObject(`https://${serverName}/.well-known/matrix/client`); const discovery = await AutoDiscovery.findClientConfig(serverName);
if (!wellknown["m.homeserver"]) { const state = discovery["m.homeserver"].state;
console.error("No m.homeserver key in well-known response"); if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
this.setState({discoveryError: _t("Invalid homeserver discovery response")}); this.setState({
return; 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) { } catch (e) {
console.error(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; 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;
@ -527,6 +470,9 @@ module.exports = React.createClass({
_renderPasswordStep: function() { _renderPasswordStep: function() {
const PasswordLogin = sdk.getComponent('login.PasswordLogin'); const PasswordLogin = sdk.getComponent('login.PasswordLogin');
const hsName = this.state.enteredHomeserverUrl === this.props.defaultHsUrl
? this.props.defaultServerName
: null;
return ( return (
<PasswordLogin <PasswordLogin
onSubmit={this.onPasswordLogin} onSubmit={this.onPasswordLogin}
@ -541,6 +487,7 @@ module.exports = React.createClass({
onForgotPasswordClick={this.props.onForgotPasswordClick} onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect} loginIncorrect={this.state.loginIncorrect}
hsUrl={this.state.enteredHomeserverUrl} hsUrl={this.state.enteredHomeserverUrl}
hsName={hsName}
/> />
); );
}, },
@ -559,7 +506,7 @@ 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; const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText;
let loginAsGuestJsx; let loginAsGuestJsx;
if (this.props.enableGuest) { if (this.props.enableGuest) {
@ -576,7 +523,7 @@ module.exports = React.createClass({
serverConfig = <ServerConfig ref="serverConfig" serverConfig = <ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl} customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
customIsUrl={this.state.discoveredIsUrl ||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}

View file

@ -57,6 +57,14 @@ module.exports = React.createClass({
}), }),
teamSelected: PropTypes.object, teamSelected: PropTypes.object,
// 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, defaultDeviceDisplayName: PropTypes.string,
// registration shouldn't know or care how login is done. // registration shouldn't know or care how login is done.
@ -170,6 +178,12 @@ module.exports = React.createClass({
}, },
onFormSubmit: function(formVals) { onFormSubmit: function(formVals) {
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
this.setState({ this.setState({
errorText: "", errorText: "",
busy: true, busy: true,
@ -441,12 +455,13 @@ module.exports = React.createClass({
let header; let header;
let errorText; let errorText;
// FIXME: remove hardcoded Status team tweaks at some point // FIXME: remove hardcoded Status team tweaks at some point
if (theme === 'status' && this.state.errorText) { const err = this.state.errorText || this.props.defaultServerDiscoveryError;
header = <div className="mx_Login_error">{ this.state.errorText }</div>; if (theme === 'status' && err) {
header = <div className="mx_Login_error">{ err }</div>;
} else { } else {
header = <h2>{ _t('Create an account') }</h2>; header = <h2>{ _t('Create an account') }</h2>;
if (this.state.errorText) { if (err) {
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>; errorText = <div className="mx_Login_error">{ err }</div>;
} }
} }

View file

@ -40,6 +40,7 @@ class PasswordLogin extends React.Component {
initialPassword: "", initialPassword: "",
loginIncorrect: false, loginIncorrect: false,
hsDomain: "", hsDomain: "",
hsName: null,
} }
constructor(props) { 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) { if (this.props.hsUrl) {
try { try {
const parsedHsUrl = new URL(this.props.hsUrl); 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) { } catch (e) {
// pass // ignore
} }
} }
@ -292,6 +304,7 @@ class PasswordLogin extends React.Component {
<div> <div>
<form onSubmit={this.onSubmitForm}> <form onSubmit={this.onSubmitForm}>
{ loginType } { loginType }
<span className="mx_Login_subtext">{ matrixIdSubtext }</span>
{ loginField } { loginField }
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password" <input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password" name="password"
@ -325,6 +338,7 @@ PasswordLogin.propTypes = {
onPhoneNumberChanged: PropTypes.func, onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func, onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool, loginIncorrect: PropTypes.bool,
hsName: PropTypes.string,
}; };
module.exports = PasswordLogin; module.exports = PasswordLogin;

View file

@ -686,6 +686,7 @@
"Mobile phone number": "Mobile phone number", "Mobile phone number": "Mobile phone number",
"Forgot your password?": "Forgot your password?", "Forgot your password?": "Forgot your password?",
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID", "%(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", "Sign in with": "Sign in with",
"Email address": "Email address", "Email address": "Email address",
"Sign in": "Sign in", "Sign in": "Sign in",
@ -831,7 +832,6 @@
"And %(count)s more...|other": "And %(count)s more...", "And %(count)s more...|other": "And %(count)s more...",
"ex. @bob:example.com": "ex. @bob:example.com", "ex. @bob:example.com": "ex. @bob:example.com",
"Add User": "Add User", "Add User": "Add User",
"Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID", "Matrix Room ID": "Matrix Room ID",
"email address": "email address", "email address": "email address",
"You have entered an invalid address.": "You have entered an invalid 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", "Confirm your new password": "Confirm your new password",
"Send Reset Email": "Send Reset Email", "Send Reset Email": "Send Reset Email",
"Create an account": "Create an account", "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.", "This Home Server does not support login using email address.": "This Home Server does not support login using email address.",
"Please <a>contact your service administrator</a> to continue using this service.": "Please <a>contact your service administrator</a> to continue using this service.", "Please <a>contact your service administrator</a> to continue using this service.": "Please <a>contact your service administrator</a> to continue using this service.",
"Incorrect username and/or password.": "Incorrect username and/or password.", "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.", "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", "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", "Unknown failure discovering homeserver": "Unknown failure discovering homeserver",
"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>.",