Merge pull request #9779 from vector-im/travis/feature/wellknown2

Validate homeserver configuration prior to loading the app
This commit is contained in:
Travis Ralston 2019-05-23 10:56:19 -06:00 committed by GitHub
commit afdaca0515
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 281 additions and 48 deletions

View File

@ -109,25 +109,31 @@ You can configure the app by copying `config.sample.json` to
For a good example, see https://riot.im/develop/config.json. For a good example, see https://riot.im/develop/config.json.
1. `default_server_name` sets the default server name to use for authentication. 1. `default_server_config` sets the default homeserver and identity server URL for
This will trigger Riot to ask Riot to use. The object is the same as returned by [https://<server_name>/.well-known/matrix/client](https://matrix.org/docs/spec/client_server/latest.html#get-well-known-matrix-client),
`https://<server_name>/.well-known/matrix/client` for the homeserver and with added support for a `server_name` under the `m.homeserver` section to display
identity server URLs to use. This is the recommended approach for setting a a custom homeserver name. Alternatively, the config can contain a `default_server_name`
default server. However, it is also possible to use the following to directly instead which is where Riot will go to get that same object - see the `.well-known`
configure each of the URLs: link above for more information. Note that the `default_server_name` is used to get
* `default_hs_url` sets the default homeserver URL. a complete server configuration whereas the `server_name` in the `default_server_config`
* `default_is_url` sets the default identity server URL (this is the server used is for display purposes only.
for verifying third party identifiers like email addresses). If this is blank, * *Note*: The URLs can also be individually specified as `default_hs_url` and
registering with an email address, adding an email address to your account, `default_is_url`, however these are deprecated. They are maintained for backwards
or inviting users via email address will not work. Matrix identity servers are compatibility with older configurations. `default_is_url` is respected only
very simple web services which map third party identifiers (currently only email if `default_hs_url` is used.
addresses) to matrix IDs: see http://matrix.org/docs/spec/identity_service/unstable.html * The identity server is used for verifying third party identifiers like emails
for more details. Currently the only public matrix identity servers are https://matrix.org and phone numbers. It is not used to store your password or account information.
and https://vector.im. In the future, identity servers will be decentralised. If not provided, the identity server defaults to vector.im unless `disable_identity_server`
* Riot will report an error if you accidentally configure both `default_server_name` _and_ `default_hs_url` since it's unclear which should take priority. is set to true in the config. Currently the only two public identity servers
are https://matrix.org and https://vector.im, however in future identity servers
will be decentralised.
* Riot will fail to load if a mix of `default_server_config`, `default_server_name`, or
`default_hs_url` is specified. When multiple sources are specified, it is unclear
which should take priority and therefore the application cannot continue.
1. `features`: Lookup of optional features that may be `enable`d, `disable`d, or exposed to the user 1. `features`: Lookup of optional features that may be `enable`d, `disable`d, or exposed to the user
in the `labs` section of settings. The available optional experimental features vary from in the `labs` section of settings. The available optional experimental features vary from
release to release. release to release. Some of the available features are described in the Labs Feature section
of this README.
1. `brand`: String to pass to your homeserver when configuring email notifications, to let the 1. `brand`: String to pass to your homeserver when configuring email notifications, to let the
homeserver know what email template to use when talking to you. homeserver know what email template to use when talking to you.
1. `branding`: Configures various branding and logo details, such as: 1. `branding`: Configures various branding and logo details, such as:

View File

@ -1,6 +1,14 @@
{ {
"default_hs_url": "https://matrix.org", "default_server_config": {
"default_is_url": "https://vector.im", "m.homeserver": {
"base_url": "https://matrix.org",
"server_name": "matrix.org"
},
"m.identity_server": {
"base_url": "https://vector.im"
}
},
"disable_identity_server": false,
"disable_custom_urls": false, "disable_custom_urls": false,
"disable_guests": false, "disable_guests": false,
"disable_login_language_selector": false, "disable_login_language_selector": false,

View File

@ -1,4 +1,7 @@
{ {
"Unexpected error preparing the app. See console for details.": "Unexpected error preparing the app. See console for details.",
"Invalid configuration: can only specify one of default_server_config, default_server_name, or default_hs_url.": "Invalid configuration: can only specify one of default_server_config, default_server_name, or default_hs_url.",
"Invalid configuration: no default server specified.": "Invalid configuration: no default server specified.",
"Riot Desktop on %(platformName)s": "Riot Desktop on %(platformName)s", "Riot Desktop on %(platformName)s": "Riot Desktop on %(platformName)s",
"Unknown device": "Unknown device", "Unknown device": "Unknown device",
"%(appName)s via %(browserName)s on %(osName)s": "%(appName)s via %(browserName)s on %(osName)s", "%(appName)s via %(browserName)s on %(osName)s": "%(appName)s via %(browserName)s on %(osName)s",
@ -15,7 +18,5 @@
"Need help?": "Need help?", "Need help?": "Need help?",
"Chat with Riot Bot": "Chat with Riot Bot", "Chat with Riot Bot": "Chat with Riot Bot",
"Explore rooms": "Explore rooms", "Explore rooms": "Explore rooms",
"Room Directory": "Room Directory", "Room Directory": "Room Directory"
"Search the room directory": "Search the room directory",
"Get started with some tips from Riot Bot!": "Get started with some tips from Riot Bot!"
} }

View File

@ -1,7 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -45,6 +45,9 @@ import VectorConferenceHandler from 'matrix-react-sdk/lib/VectorConferenceHandle
import Promise from 'bluebird'; import Promise from 'bluebird';
import request from 'browser-request'; import request from 'browser-request';
import * as languageHandler from 'matrix-react-sdk/lib/languageHandler'; import * as languageHandler from 'matrix-react-sdk/lib/languageHandler';
import {_t, _td, newTranslatableError} from 'matrix-react-sdk/lib/languageHandler';
import AutoDiscoveryUtils from 'matrix-react-sdk/lib/utils/AutoDiscoveryUtils';
import {AutoDiscovery} from "matrix-js-sdk/lib/autodiscovery";
import url from 'url'; import url from 'url';
@ -341,22 +344,37 @@ async function loadApp() {
const platform = PlatformPeg.get(); const platform = PlatformPeg.get();
platform.startUpdater(); platform.startUpdater();
const MatrixChat = sdk.getComponent('structures.MatrixChat'); // Don't bother loading the app until the config is verified
window.matrixChat = ReactDOM.render( verifyServerConfig().then((newConfig) => {
<MatrixChat const MatrixChat = sdk.getComponent('structures.MatrixChat');
onNewScreen={onNewScreen} window.matrixChat = ReactDOM.render(
makeRegistrationUrl={makeRegistrationUrl} <MatrixChat
ConferenceHandler={VectorConferenceHandler} onNewScreen={onNewScreen}
config={configJson} makeRegistrationUrl={makeRegistrationUrl}
realQueryParams={params} ConferenceHandler={VectorConferenceHandler}
startingFragmentQueryParams={fragparts.params} config={newConfig}
enableGuest={!configJson.disable_guests} realQueryParams={params}
onTokenLoginCompleted={onTokenLoginCompleted} startingFragmentQueryParams={fragparts.params}
initialScreenAfterLogin={getScreenFromLocation(window.location)} enableGuest={!configJson.disable_guests}
defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()} onTokenLoginCompleted={onTokenLoginCompleted}
/>, initialScreenAfterLogin={getScreenFromLocation(window.location)}
document.getElementById('matrixchat'), defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()}
); />,
document.getElementById('matrixchat'),
);
}).catch(err => {
console.error(err);
const errorMessage = err.translatedMessage
|| _t("Unexpected error preparing the app. See console for details.");
// Like the compatibility page, AWOOOOOGA at the user
const GenericErrorPage = sdk.getComponent("structures.GenericErrorPage");
window.matrixChat = ReactDOM.render(
<GenericErrorPage message={errorMessage} />,
document.getElementById('matrixchat'),
);
});
} else { } else {
console.error("Browser is missing required features."); console.error("Browser is missing required features.");
// take to a different landing page to AWOOOOOGA at the user // take to a different landing page to AWOOOOOGA at the user
@ -428,4 +446,68 @@ async function loadLanguage() {
} }
} }
async function verifyServerConfig() {
console.log("Verifying homeserver configuration");
// Note: the query string may include is_url and hs_url - we only respect these in the
// context of email validation. Because we don't respect them otherwise, we do not need
// to parse or consider them here.
const config = SdkConfig.get();
let wkConfig = config['default_server_config']; // overwritten later under some conditions
const serverName = config['default_server_name'];
const hsUrl = config['default_hs_url'];
const isUrl = config['default_is_url'];
const incompatibleOptions = [wkConfig, serverName, hsUrl].filter(i => !!i);
if (incompatibleOptions.length > 1) {
throw newTranslatableError(_td(
"Invalid configuration: can only specify one of default_server_config, default_server_name, " +
"or default_hs_url.",
));
}
if (incompatibleOptions.length < 1) {
throw newTranslatableError(_td("Invalid configuration: no default server specified."));
}
if (hsUrl) {
console.log("Config uses a default_hs_url - constructing a default_server_config using this information");
wkConfig = {
"m.homeserver": {
"base_url": hsUrl,
},
};
if (isUrl) {
wkConfig["m.identity_server"] = {
"base_url": isUrl,
};
}
}
let result = null;
if (wkConfig) {
console.log("Config uses a default_server_config - validating object");
result = await AutoDiscovery.fromDiscoveryConfig(wkConfig);
}
if (serverName) {
console.log("Config uses a default_server_name - doing .well-known lookup");
result = await AutoDiscovery.findClientConfig(serverName);
}
const validatedConfig = AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result);
validatedConfig.isDefault = true;
// Just in case we ever have to debug this
console.log("Using homeserver config:", validatedConfig);
// Add the newly built config to the actual config for use by the app
console.log("Updating SdkConfig with validated discovery information");
SdkConfig.add({"validated_server_config": validatedConfig});
return SdkConfig.get();
}
loadApp(); loadApp();

View File

@ -4,6 +4,11 @@
<style type="text/css"> <style type="text/css">
/* By default, hide the custom IS stuff - enabled in JS */
#custom_is, #is_url {
display: none;
}
body { body {
background: #c5e0f7; background: #c5e0f7;
background: -moz-linear-gradient(top, #c5e0f7 0%, #ffffff 100%); background: -moz-linear-gradient(top, #c5e0f7 0%, #ffffff 100%);
@ -109,6 +114,14 @@ body {
margin: 20px; margin: 20px;
} }
.mx_HomePage_errorContainer {
display: none; /* shown in JS if needed */
margin: 20px;
border: 1px solid red;
background-color: #ffb9b9;
padding: 5px;
}
.mx_HomePage_container h1, .mx_HomePage_container h1,
.mx_HomePage_container h2, .mx_HomePage_container h2,
.mx_HomePage_container h3, .mx_HomePage_container h3,
@ -152,6 +165,10 @@ body {
<body> <body>
<div class="mx_HomePage_errorContainer">
<!-- populated by JS if needed -->
</div>
<div class="mx_HomePage_container"> <div class="mx_HomePage_container">
<div class="mx_HomePage_col mx_HomePage_header"> <div class="mx_HomePage_col mx_HomePage_header">
<a href="https://riot.im"> <a href="https://riot.im">
@ -365,7 +382,9 @@ body {
<p>Launch the app, and enable <strong>Use custom server options (advanced)</strong>.</p> <p>Launch the app, and enable <strong>Use custom server options (advanced)</strong>.</p>
<p class="mx_Spacer">In the homeserver field, enter:</p> <p class="mx_Spacer">In the homeserver field, enter:</p>
<p><strong id="hs_url"></strong></p> <p><strong id="hs_url"></strong></p>
<p class="mx_Spacer"><em>Note: You shouldn&apos;t need to modify the identity server field, which by default is set to https://vector.im.</em></p> <p class="mx_Spacer" id="default_is"><em>Note: You shouldn&apos;t need to modify the identity server field, which by default is set to https://vector.im.</em></p>
<p class="mx_Spacer" id="custom_is">In the identity server field, enter:</p>
<p><strong id="is_url"></strong></p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,19 +6,102 @@ function onBackToRiotClick() {
window.location.href = '../'; window.location.href = '../';
} }
// NEVER pass user-controlled content to this function! Hardcoded strings only please.
function renderConfigError(message) {
const contactMsg = "If this is unexpected, please contact your system administrator " +
"or technical support representative.";
message = `<h2>Error loading Riot</h2><p>${message}</p><p>${contactMsg}</p>`;
const toHide = document.getElementsByClassName("mx_HomePage_container");
const errorContainers = document.getElementsByClassName("mx_HomePage_errorContainer");
for (const e of toHide) {
// We have to clear the content because .style.display='none'; doesn't work
// due to an !important in the CSS.
e.innerHTML = '';
}
for (const e of errorContainers) {
e.style.display = 'block';
e.innerHTML = message;
}
}
async function initPage() { async function initPage() {
document.getElementById('back_to_riot_button').onclick = onBackToRiotClick; document.getElementById('back_to_riot_button').onclick = onBackToRiotClick;
const config = await getVectorConfig('..'); let config = await getVectorConfig('..');
let hsUrl;
if (config && config['default_hs_url']) { // We manually parse the config similar to how validateServerConfig works because
hsUrl = config['default_hs_url']; // calling that function pulls in roughly 4mb of JS we don't use.
const wkConfig = config['default_server_config']; // overwritten later under some conditions
const serverName = config['default_server_name'];
const defaultHsUrl = config['default_hs_url'];
const defaultIsUrl = config['default_is_url'];
const incompatibleOptions = [wkConfig, serverName, defaultHsUrl].filter(i => !!i);
if (incompatibleOptions.length > 1) {
return renderConfigError(
"Invalid configuration: can only specify one of default_server_config, default_server_name, " +
"or default_hs_url.",
);
} }
if (incompatibleOptions.length < 1) {
return renderConfigError("Invalid configuration: no default server specified.");
}
let hsUrl = '';
let isUrl = '';
if (wkConfig && wkConfig['m.homeserver']) {
hsUrl = wkConfig['m.homeserver']['base_url'];
if (wkConfig['m.identity_server']) {
isUrl = wkConfig['m.identity_server']['base_url'];
}
}
if (serverName) {
// We also do our own minimal .well-known validation to avoid pulling in the js-sdk
try {
const result = await fetch(`https://${serverName}/.well-known/matrix/client`);
const wkConfig = await result.json();
if (wkConfig && wkConfig['m.homeserver']) {
hsUrl = wkConfig['m.homeserver']['base_url'];
if (wkConfig['m.identity_server']) {
isUrl = wkConfig['m.identity_server']['base_url'];
}
}
} catch (e) {
console.error(e);
return renderConfigError("Unable to fetch homeserver configuration");
}
}
if (defaultHsUrl) {
hsUrl = defaultHsUrl;
isUrl = defaultIsUrl;
}
if (!hsUrl) {
return renderConfigError("Unable to locate homeserver");
}
if (hsUrl && !hsUrl.endsWith('/')) hsUrl += '/'; if (hsUrl && !hsUrl.endsWith('/')) hsUrl += '/';
if (hsUrl && hsUrl !== 'https://matrix.org/') { if (isUrl && !isUrl.endsWith('/')) isUrl += '/';
if (hsUrl !== 'https://matrix.org/') {
document.getElementById('step2_container').style.display = 'block'; document.getElementById('step2_container').style.display = 'block';
document.getElementById('hs_url').innerHTML = hsUrl; document.getElementById('hs_url').innerText = hsUrl;
document.getElementById('step_login_header').innerHTML= 'Step 3: Register or Log in'; document.getElementById('step_login_header').innerHTML= 'Step 3: Register or Log in';
if (isUrl && isUrl !== "https://vector.im/") {
document.getElementById('default_is').style.display = 'none';
document.getElementById('custom_is').style.display = 'block';
document.getElementById('is_url').style.display = 'block';
document.getElementById('is_url').innerText = isUrl;
}
} }
} }

View File

@ -37,6 +37,8 @@ const ReactDOM = require('react-dom');
const ReactTestUtils = require('react-addons-test-utils'); const ReactTestUtils = require('react-addons-test-utils');
const expect = require('expect'); const expect = require('expect');
import Promise from 'bluebird'; import Promise from 'bluebird';
import {makeType} from "matrix-react-sdk/lib/utils/TypeUtils";
import {ValidatedServerConfig} from "matrix-react-sdk/lib/utils/AutoDiscoveryUtils";
const test_utils = require('../test-utils'); const test_utils = require('../test-utils');
const MockHttpBackend = require('matrix-mock-request'); const MockHttpBackend = require('matrix-mock-request');
@ -96,8 +98,20 @@ describe('joining a room', function() {
PlatformPeg.set(new WebPlatform()); PlatformPeg.set(new WebPlatform());
const config = {
validated_server_config: makeType(ValidatedServerConfig, {
hsUrl: HS_URL,
hsName: "TEST_ENVIRONMENT",
hsNameIsDifferent: false, // yes, we lie
isUrl: IS_URL,
identityEnabled: true,
}),
};
const mc = ( const mc = (
<MatrixChat config={{}} <MatrixChat
config={config}
serverConfig={config.validated_server_config}
makeRegistrationUrl={()=>{throw new Error("unimplemented");}} makeRegistrationUrl={()=>{throw new Error("unimplemented");}}
initialScreenAfterLogin={{ initialScreenAfterLogin={{
screen: 'directory', screen: 'directory',

View File

@ -39,6 +39,8 @@ import dis from 'matrix-react-sdk/lib/dispatcher';
import * as test_utils from '../test-utils'; import * as test_utils from '../test-utils';
import MockHttpBackend from 'matrix-mock-request'; import MockHttpBackend from 'matrix-mock-request';
import {parseQs, parseQsFromFragment} from '../../src/vector/url_utils'; import {parseQs, parseQsFromFragment} from '../../src/vector/url_utils';
import {makeType} from "matrix-react-sdk/lib/utils/TypeUtils";
import {ValidatedServerConfig} from "matrix-react-sdk/lib/utils/AutoDiscoveryUtils";
const DEFAULT_HS_URL='http://my_server'; const DEFAULT_HS_URL='http://my_server';
const DEFAULT_IS_URL='http://my_is'; const DEFAULT_IS_URL='http://my_is';
@ -146,6 +148,13 @@ describe('loading:', function() {
const config = Object.assign({ const config = Object.assign({
default_hs_url: DEFAULT_HS_URL, default_hs_url: DEFAULT_HS_URL,
default_is_url: DEFAULT_IS_URL, default_is_url: DEFAULT_IS_URL,
validated_server_config: makeType(ValidatedServerConfig, {
hsUrl: DEFAULT_HS_URL,
hsName: "TEST_ENVIRONMENT",
hsNameIsDifferent: false, // yes, we lie
isUrl: DEFAULT_IS_URL,
identityEnabled: true,
}),
embeddedPages: { embeddedPages: {
homeUrl: 'data:text/html;charset=utf-8;base64,PGh0bWw+PC9odG1sPg==', homeUrl: 'data:text/html;charset=utf-8;base64,PGh0bWw+PC9odG1sPg==',
}, },
@ -160,6 +169,7 @@ describe('loading:', function() {
<MatrixChat <MatrixChat
onNewScreen={onNewScreen} onNewScreen={onNewScreen}
config={config} config={config}
serverConfig={config.validated_server_config}
realQueryParams={params} realQueryParams={params}
startingFragmentQueryParams={fragParts.params} startingFragmentQueryParams={fragParts.params}
enableGuest={true} enableGuest={true}
@ -616,11 +626,21 @@ describe('loading:', function() {
// check that we have a Login component, send a 'user:pass' login, // check that we have a Login component, send a 'user:pass' login,
// and await the HTTP requests. // and await the HTTP requests.
function completeLogin(matrixChat) { async function completeLogin(matrixChat) {
// we expect a single <Login> component // we expect a single <Login> component
const login = ReactTestUtils.findRenderedComponentWithType( const login = ReactTestUtils.findRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.auth.Login')); matrixChat, sdk.getComponent('structures.auth.Login'));
// When we switch to the login component, it'll hit the login endpoint
// for proof of life and to get flows. We'll only give it one option.
httpBackend.when('GET', '/login')
.respond(200, {"flows": [{"type": "m.login.password"}]});
httpBackend.flush(); // We already would have tried the GET /login request
// Give the component some time to finish processing the login flows before
// continuing.
await Promise.delay(100);
httpBackend.when('POST', '/login').check(function(req) { httpBackend.when('POST', '/login').check(function(req) {
expect(req.data.type).toEqual('m.login.password'); expect(req.data.type).toEqual('m.login.password');
expect(req.data.identifier.type).toEqual('m.id.user'); expect(req.data.identifier.type).toEqual('m.id.user');