Have ServerConfig and co. do validation of the config in-house

This also causes the components to produce a ValidatedServerConfig for use by other components.
This commit is contained in:
Travis Ralston 2019-05-02 22:57:49 -06:00
parent e8a94ca3cf
commit 636cb8a5cc
4 changed files with 238 additions and 74 deletions

View file

@ -35,3 +35,8 @@ limitations under the License.
.mx_ServerConfig_help:link { .mx_ServerConfig_help:link {
opacity: 0.8; opacity: 0.8;
} }
.mx_ServerConfig_error {
display: block;
color: $warning-color;
}

View file

@ -18,9 +18,15 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector';
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
// TODO: TravisR - Can this extend ServerConfig for most things?
/* /*
* Configure the Modular server name. * Configure the Modular server name.
* *
@ -31,65 +37,87 @@ export default class ModularServerConfig extends React.PureComponent {
static propTypes = { static propTypes = {
onServerConfigChange: PropTypes.func, onServerConfigChange: PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults) // The current configuration that the user is expecting to change.
// they are used if the user has not overridden them with a custom URL. serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
// This component always uses the default IS URL and doesn't allow it
// to be changed. We still receive it as a prop here to simplify
// consumers by still passing the IS URL via onServerConfigChange.
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
} };
static defaultProps = { static defaultProps = {
onServerConfigChange: function() {}, onServerConfigChange: function() {},
customHsUrl: "", customHsUrl: "",
delayTimeMs: 0, delayTimeMs: 0,
} };
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
hsUrl: props.customHsUrl, busy: false,
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
}; };
} }
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl) return; if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({ this.setState({
hsUrl: newProps.customHsUrl, hsUrl,
}); isUrl,
this.props.onServerConfigChange({ busy: true,
hsUrl: newProps.customHsUrl, errorText: "",
isUrl: this.props.defaultIsUrl,
}); });
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
}
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
} }
onHomeserverBlur = (ev) => { onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({ this.validateServer();
hsUrl: this.state.hsUrl,
isUrl: this.props.defaultIsUrl,
});
}); });
} };
onHomeserverChange = (ev) => { onHomeserverChange = (ev) => {
const hsUrl = ev.target.value; const hsUrl = ev.target.value;
this.setState({ hsUrl }); this.setState({ hsUrl });
} };
_waitThenInvoke(existingTimeoutId, fn) { _waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) { if (existingTimeoutId) {
@ -116,7 +144,7 @@ export default class ModularServerConfig extends React.PureComponent {
<div className="mx_ServerConfig_fields"> <div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl" <Field id="mx_ServerConfig_hsUrl"
label={_t("Server Name")} label={_t("Server Name")}
placeholder={this.props.defaultHsUrl} placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl} value={this.state.hsUrl}
onBlur={this.onHomeserverBlur} onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange} onChange={this.onHomeserverChange}

View file

@ -20,6 +20,9 @@ import PropTypes from 'prop-types';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
/* /*
* A pure UI component which displays the HS and IS to use. * A pure UI component which displays the HS and IS to use.
@ -29,80 +32,97 @@ export default class ServerConfig extends React.PureComponent {
static propTypes = { static propTypes = {
onServerConfigChange: PropTypes.func, onServerConfigChange: PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults) // The current configuration that the user is expecting to change.
// they are used if the user has not overridden them with a custom URL. serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
} };
static defaultProps = { static defaultProps = {
onServerConfigChange: function() {}, onServerConfigChange: function() {},
customHsUrl: "",
customIsUrl: "",
delayTimeMs: 0, delayTimeMs: 0,
} };
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
hsUrl: props.customHsUrl, busy: false,
isUrl: props.customIsUrl, errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
}; };
} }
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl && if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.customIsUrl === this.state.isUrl) return; newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
return this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
}
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({ this.setState({
hsUrl: newProps.customHsUrl, hsUrl,
isUrl: newProps.customIsUrl, isUrl,
}); busy: true,
this.props.onServerConfigChange({ errorText: "",
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
}); });
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
}
} }
onHomeserverBlur = (ev) => { onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({ this.validateServer();
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
});
}); });
} };
onHomeserverChange = (ev) => { onHomeserverChange = (ev) => {
const hsUrl = ev.target.value; const hsUrl = ev.target.value;
this.setState({ hsUrl }); this.setState({ hsUrl });
} };
onIdentityServerBlur = (ev) => { onIdentityServerBlur = (ev) => {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => { this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
this.props.onServerConfigChange({ this.validateServer();
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
});
}); });
} };
onIdentityServerChange = (ev) => { onIdentityServerChange = (ev) => {
const isUrl = ev.target.value; const isUrl = ev.target.value;
this.setState({ isUrl }); this.setState({ isUrl });
} };
_waitThenInvoke(existingTimeoutId, fn) { _waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) { if (existingTimeoutId) {
@ -114,11 +134,15 @@ export default class ServerConfig extends React.PureComponent {
showHelpPopup = () => { showHelpPopup = () => {
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog'); const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
} };
render() { render() {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
const errorText = this.state.errorText
? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
: null;
return ( return (
<div className="mx_ServerConfig"> <div className="mx_ServerConfig">
<h3>{_t("Other servers")}</h3> <h3>{_t("Other servers")}</h3>
@ -127,20 +151,23 @@ export default class ServerConfig extends React.PureComponent {
{ sub } { sub }
</a>, </a>,
})} })}
{errorText}
<div className="mx_ServerConfig_fields"> <div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl" <Field id="mx_ServerConfig_hsUrl"
label={_t("Homeserver URL")} label={_t("Homeserver URL")}
placeholder={this.props.defaultHsUrl} placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl} value={this.state.hsUrl}
onBlur={this.onHomeserverBlur} onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange} onChange={this.onHomeserverChange}
disabled={this.state.busy}
/> />
<Field id="mx_ServerConfig_isUrl" <Field id="mx_ServerConfig_isUrl"
label={_t("Identity Server URL")} label={_t("Identity Server URL")}
placeholder={this.props.defaultIsUrl} placeholder={this.props.serverConfig.isUrl}
value={this.state.isUrl} value={this.state.isUrl}
onBlur={this.onIdentityServerBlur} onBlur={this.onIdentityServerBlur}
onChange={this.onIdentityServerChange} onChange={this.onIdentityServerChange}
disabled={this.state.busy}
/> />
</div> </div>
</div> </div>

View file

@ -0,0 +1,104 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {AutoDiscovery} from "matrix-js-sdk";
import {_td, newTranslatableError} from "../languageHandler";
import {makeType} from "./TypeUtils";
import SdkConfig from "../SdkConfig";
export class ValidatedServerConfig {
hsUrl: string;
hsName: string;
hsNameIsDifferent: string;
isUrl: string;
identityEnabled: boolean;
}
export default class AutoDiscoveryUtils {
static async validateServerConfigWithStaticUrls(homeserverUrl: string, identityUrl: string): ValidatedServerConfig {
if (!homeserverUrl) {
throw newTranslatableError(_td("No homeserver URL provided"));
}
const wellknownConfig = {
"m.homeserver": {
base_url: homeserverUrl,
},
"m.identity_server": {
base_url: identityUrl,
},
};
const result = await AutoDiscovery.fromDiscoveryConfig(wellknownConfig);
const url = new URL(homeserverUrl);
const serverName = url.hostname;
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result);
}
static async validateServerName(serverName: string): ValidatedServerConfig {
const result = await AutoDiscovery.findClientConfig(serverName);
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result);
}
static buildValidatedConfigFromDiscovery(serverName: string, discoveryResult): ValidatedServerConfig {
if (!discoveryResult || !discoveryResult["m.homeserver"]) {
// This shouldn't happen without major misconfiguration, so we'll log a bit of information
// in the log so we can find this bit of codee but otherwise tell teh user "it broke".
console.error("Ended up in a state of not knowing which homeserver to connect to.");
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
}
const hsResult = discoveryResult['m.homeserver'];
if (hsResult.state !== AutoDiscovery.SUCCESS) {
if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error) !== -1) {
throw newTranslatableError(hsResult.error);
}
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
}
const isResult = discoveryResult['m.identity_server'];
let preferredIdentityUrl = "https://vector.im";
if (isResult && isResult.state === AutoDiscovery.SUCCESS) {
preferredIdentityUrl = isResult["base_url"];
} else if (isResult && isResult.state !== AutoDiscovery.PROMPT) {
console.error("Error determining preferred identity server URL:", isResult);
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
}
const preferredHomeserverUrl = hsResult["base_url"];
let preferredHomeserverName = serverName ? serverName : hsResult["server_name"];
const url = new URL(preferredHomeserverUrl);
if (!preferredHomeserverName) preferredHomeserverName = url.hostname;
// It should have been set by now, so check it
if (!preferredHomeserverName) {
console.error("Failed to parse homeserver name from homeserver URL");
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
}
return makeType(ValidatedServerConfig, {
hsUrl: preferredHomeserverUrl,
hsName: preferredHomeserverName,
hsNameIsDifferent: url.hostname !== preferredHomeserverName,
isUrl: preferredIdentityUrl,
identityEnabled: !SdkConfig.get()['disable_identity_server'],
});
}
}