Validate default homeserver config before loading the app
Implements the process described here: https://github.com/vector-im/riot-web/issues/9290#issuecomment-481966910 The expectation is that later layers (like the react-sdk) will make use of the `validated_discovery_config` option instead of interpreting the config themselves. We intentionally block the UI from loading here to avoid races between discovery and the app loading.pull/9496/head
							parent
							
								
									9cd4ac1df4
								
							
						
					
					
						commit
						f08491cee8
					
				
							
								
								
									
										38
									
								
								README.md
								
								
								
								
							
							
						
						
									
										38
									
								
								README.md
								
								
								
								
							|  | @ -109,25 +109,29 @@ 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*: 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: | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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.", | ||||
|     "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", | ||||
|     "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" | ||||
| } | ||||
|  |  | |||
|  | @ -45,6 +45,8 @@ 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} from 'matrix-react-sdk/lib/languageHandler'; | ||||
| import {AutoDiscovery} from "matrix-js-sdk/lib/autodiscovery"; | ||||
| 
 | ||||
| import url from 'url'; | ||||
| 
 | ||||
|  | @ -341,22 +343,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 +445,141 @@ async function loadLanguage() { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| async function verifyServerConfig() { | ||||
|     console.log("Verifying homeserver configuration"); | ||||
| 
 | ||||
|     // Errors which can be returned by .well-known lookups. If autodiscovery fails for unexpected reasons,
 | ||||
|     // the last thing we want is "missing-translation|en:Your error here". The actual strings are also defined
 | ||||
|     // in the react-sdk, so we don't need them here.
 | ||||
|     const discoveryErrors = [ | ||||
|         "Invalid homeserver discovery response", | ||||
|         "Failed to get autodiscovery configuration from server", | ||||
|         "Invalid base_url for m.homeserver", | ||||
|         "Homeserver URL does not appear to be a valid Matrix homeserver", | ||||
|         "Invalid identity server discovery response", | ||||
|         "Invalid base_url for m.identity_server", | ||||
|         "Identity server URL does not appear to be a valid identity server", | ||||
|         "General failure", | ||||
|     ]; | ||||
| 
 | ||||
|     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 (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); | ||||
|     } | ||||
| 
 | ||||
|     if (!result || !result["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 = result['m.homeserver']; | ||||
|     if (hsResult.state !== AutoDiscovery.SUCCESS) { | ||||
|         if (discoveryErrors.indexOf(hsResult.error) !== -1) { | ||||
|             throw newTranslatableError(hsResult.error); | ||||
|         } | ||||
|         throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); | ||||
|     } | ||||
| 
 | ||||
|     const isResult = result['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")); | ||||
|     } | ||||
| 
 | ||||
|     const isServerNameDifferentFromUrl = url.hostname !== preferredHomeserverName; | ||||
| 
 | ||||
|     console.log("Using homeserver config:", { | ||||
|         isServerNameDifferentFromUrl, | ||||
|         preferredHomeserverName, | ||||
|         preferredHomeserverUrl, | ||||
|         preferredIdentityUrl, | ||||
|     }); | ||||
| 
 | ||||
|     // Build our own discovery result for distribution within the app
 | ||||
|     const configResult = { | ||||
|         "m.homeserver": { | ||||
|             "base_url": preferredHomeserverUrl, | ||||
|             "server_name": preferredHomeserverName, | ||||
|             "server_name_different": isServerNameDifferentFromUrl, | ||||
|         }, | ||||
|         "m.identity_server": { | ||||
|             "base_url": preferredIdentityUrl, | ||||
|             "enabled": !SdkConfig.get()['disable_identity_server'], | ||||
|         }, | ||||
|     }; | ||||
| 
 | ||||
|     // Copy over any other keys that may be of interest
 | ||||
|     for (const key of Object.keys(result)) { | ||||
|         if (key === "m.homeserver" || key === "m.identity_server") continue; | ||||
|         configResult[key] = JSON.parse(JSON.stringify(result[key])); // deep clone
 | ||||
|     } | ||||
| 
 | ||||
|     // 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_discovery_config": configResult}); | ||||
| 
 | ||||
|     return SdkConfig.get(); | ||||
| } | ||||
| 
 | ||||
| // Helper function to provide English errors in logs, but present translated
 | ||||
| // errors to users.
 | ||||
| function newTranslatableError(message) { | ||||
|     const error = new Error(message); | ||||
|     error.translatedMessage = _t(message); | ||||
|     return error; | ||||
| } | ||||
| 
 | ||||
| loadApp(); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston