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.
1. `default_server_name` sets the default server name to use for authentication.
This will trigger Riot to ask
`https://<server_name>/.well-known/matrix/client` for the homeserver and
identity server URLs to use. This is the recommended approach for setting a
default server. However, it is also possible to use the following to directly
configure each of the URLs:
* `default_hs_url` sets the default homeserver URL.
* `default_is_url` sets the default identity server URL (this is the server used
for verifying third party identifiers like email addresses). If this is blank,
registering with an email address, adding an email address to your account,
or inviting users via email address will not work. Matrix identity servers are
very simple web services which map third party identifiers (currently only email
addresses) to matrix IDs: see http://matrix.org/docs/spec/identity_service/unstable.html
for more details. Currently the only public matrix identity servers are https://matrix.org
and https://vector.im. In the future, identity servers will be decentralised.
* 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.
1. `default_server_config` sets the default homeserver and identity server URL for
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),
with added support for a `server_name` under the `m.homeserver` section to display
a custom homeserver name. Alternatively, the config can contain a `default_server_name`
instead which is where Riot will go to get that same object - see the `.well-known`
link above for more information. Note that the `default_server_name` is used to get
a complete server configuration whereas the `server_name` in the `default_server_config`
is for display purposes only.
* *Note*: The URLs can also be individually specified as `default_hs_url` and
`default_is_url`, however these are deprecated. They are maintained for backwards
compatibility with older configurations. `default_is_url` is respected only
if `default_hs_url` is used.
* The identity server is used for verifying third party identifiers like emails
and phone numbers. It is not used to store your password or account information.
If not provided, the identity server defaults to vector.im unless `disable_identity_server`
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
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
homeserver know what email template to use when talking to you.
1. `branding`: Configures various branding and logo details, such as:

View File

@ -1,6 +1,14 @@
{
"default_hs_url": "https://matrix.org",
"default_is_url": "https://vector.im",
"default_server_config": {
"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_guests": 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",
"Unknown device": "Unknown device",
"%(appName)s via %(browserName)s on %(osName)s": "%(appName)s via %(browserName)s on %(osName)s",
@ -15,7 +18,5 @@
"Need help?": "Need help?",
"Chat with Riot Bot": "Chat with Riot Bot",
"Explore rooms": "Explore rooms",
"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!"
"Room Directory": "Room Directory"
}

View File

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket 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");
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 request from 'browser-request';
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';
@ -341,22 +344,37 @@ async function loadApp() {
const platform = PlatformPeg.get();
platform.startUpdater();
const MatrixChat = sdk.getComponent('structures.MatrixChat');
window.matrixChat = ReactDOM.render(
<MatrixChat
onNewScreen={onNewScreen}
makeRegistrationUrl={makeRegistrationUrl}
ConferenceHandler={VectorConferenceHandler}
config={configJson}
realQueryParams={params}
startingFragmentQueryParams={fragparts.params}
enableGuest={!configJson.disable_guests}
onTokenLoginCompleted={onTokenLoginCompleted}
initialScreenAfterLogin={getScreenFromLocation(window.location)}
defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()}
/>,
document.getElementById('matrixchat'),
);
// Don't bother loading the app until the config is verified
verifyServerConfig().then((newConfig) => {
const MatrixChat = sdk.getComponent('structures.MatrixChat');
window.matrixChat = ReactDOM.render(
<MatrixChat
onNewScreen={onNewScreen}
makeRegistrationUrl={makeRegistrationUrl}
ConferenceHandler={VectorConferenceHandler}
config={newConfig}
realQueryParams={params}
startingFragmentQueryParams={fragparts.params}
enableGuest={!configJson.disable_guests}
onTokenLoginCompleted={onTokenLoginCompleted}
initialScreenAfterLogin={getScreenFromLocation(window.location)}
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 {
console.error("Browser is missing required features.");
// 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();

View File

@ -4,6 +4,11 @@
<style type="text/css">
/* By default, hide the custom IS stuff - enabled in JS */
#custom_is, #is_url {
display: none;
}
body {
background: #c5e0f7;
background: -moz-linear-gradient(top, #c5e0f7 0%, #ffffff 100%);
@ -109,6 +114,14 @@ body {
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 h2,
.mx_HomePage_container h3,
@ -152,6 +165,10 @@ body {
<body>
<div class="mx_HomePage_errorContainer">
<!-- populated by JS if needed -->
</div>
<div class="mx_HomePage_container">
<div class="mx_HomePage_col mx_HomePage_header">
<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 class="mx_Spacer">In the homeserver field, enter:</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>

View File

@ -6,19 +6,102 @@ function onBackToRiotClick() {
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() {
document.getElementById('back_to_riot_button').onclick = onBackToRiotClick;
const config = await getVectorConfig('..');
let hsUrl;
if (config && config['default_hs_url']) {
hsUrl = config['default_hs_url'];
let config = await getVectorConfig('..');
// We manually parse the config similar to how validateServerConfig works because
// 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 !== 'https://matrix.org/') {
if (isUrl && !isUrl.endsWith('/')) isUrl += '/';
if (hsUrl !== 'https://matrix.org/') {
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';
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 expect = require('expect');
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 MockHttpBackend = require('matrix-mock-request');
@ -96,8 +98,20 @@ describe('joining a room', function() {
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 = (
<MatrixChat config={{}}
<MatrixChat
config={config}
serverConfig={config.validated_server_config}
makeRegistrationUrl={()=>{throw new Error("unimplemented");}}
initialScreenAfterLogin={{
screen: 'directory',

View File

@ -39,6 +39,8 @@ import dis from 'matrix-react-sdk/lib/dispatcher';
import * as test_utils from '../test-utils';
import MockHttpBackend from 'matrix-mock-request';
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_IS_URL='http://my_is';
@ -146,6 +148,13 @@ describe('loading:', function() {
const config = Object.assign({
default_hs_url: DEFAULT_HS_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: {
homeUrl: 'data:text/html;charset=utf-8;base64,PGh0bWw+PC9odG1sPg==',
},
@ -160,6 +169,7 @@ describe('loading:', function() {
<MatrixChat
onNewScreen={onNewScreen}
config={config}
serverConfig={config.validated_server_config}
realQueryParams={params}
startingFragmentQueryParams={fragParts.params}
enableGuest={true}
@ -616,11 +626,21 @@ describe('loading:', function() {
// check that we have a Login component, send a 'user:pass' login,
// and await the HTTP requests.
function completeLogin(matrixChat) {
async function completeLogin(matrixChat) {
// we expect a single <Login> component
const login = ReactTestUtils.findRenderedComponentWithType(
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) {
expect(req.data.type).toEqual('m.login.password');
expect(req.data.identifier.type).toEqual('m.id.user');