diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx index 30b8932a5..fc2ad3626 100644 --- a/extensions/default/src/ViewerLayout/index.tsx +++ b/extensions/default/src/ViewerLayout/index.tsx @@ -46,13 +46,27 @@ function ViewerLayout({ const onClickReturnButton = () => { const { pathname } = location; const dataSourceIdx = pathname.indexOf('/', 1); - const search = - dataSourceIdx === -1 - ? undefined - : `datasources=${pathname.substring(dataSourceIdx + 1)}`; + // const search = + // dataSourceIdx === -1 + // ? undefined + // : `datasources=${pathname.substring(dataSourceIdx + 1)}`; + + // Todo: Handle parameters in a better way. + const query = new URLSearchParams(window.location.search); + const configUrl = query.get('configUrl'); + + const searchQuery = new URLSearchParams(); + if (dataSourceIdx !== -1) { + searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1)); + } + + if (configUrl) { + searchQuery.append('configUrl', configUrl); + } + navigate({ pathname: '/', - search, + search: decodeURIComponent(searchQuery.toString()), }); }; diff --git a/platform/docs/docs/configuration/configurationFiles.md b/platform/docs/docs/configuration/configurationFiles.md index d8cf8ded6..141b2c005 100644 --- a/platform/docs/docs/configuration/configurationFiles.md +++ b/platform/docs/docs/configuration/configurationFiles.md @@ -116,7 +116,24 @@ Here are a list of some options available: if auth headers are used, a preflight request is required. - `maxNumRequests`: The maximum number of requests to allow in parallel. It is an object with keys of `interaction`, `thumbnail`, and `prefetch`. You can specify a specific number for each type. - `showLoadingIndicator`: (default to true), if set to false, the loading indicator will not be shown when navigating between studies. - +- `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.
+Points to consider while using `dangerouslyUseDynamicConfig`:
+ - User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`. + - Regex helps to avoid easy exploit. Dafault is `/.*/`. Setup your own regex to choose a specific source of configuration only. + - System administrators can return `cross-origin: same-origin` with OHIF files to disallow any loading from other origin. It will block read access to resources loaded from a different origin to avoid potential attack vector. + - Example config: + ```js + dangerouslyUseDynamicConfig: { + enabled: false, + regex: /.*/ + } + ``` + > Example 1, to allow numbers and letters in an absolute or sub-path only.
+`regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/`
+Example 2, to restricts to either hosptial.com or othersite.com.
+`regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/`
+Example usage:
+`http://localhost:3000/?configUrl=http://localhost:3000/config/example.json`
diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index 757bdc344..ff93f31a1 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -25,6 +25,16 @@ window.config = { }, // filterQueryParam: false, defaultDataSourceName: 'dicomweb', + /* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */ + // dangerouslyUseDynamicConfig: { + // enabled: true, + // // regex will ensure valid configuration source and default is /.*/ which matches any character. To use this, setup your own regex to choose a specific source of configuration only. + // // Example 1, to allow numbers and letters in an absolute or sub-path only. + // // regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/ + // // Example 2, to restricts to either hosptial.com or othersite.com. + // // regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/ + // regex: /.*/, + // }, dataSources: [ { friendlyName: 'dcmjs DICOMWeb Server', diff --git a/platform/viewer/src/index.js b/platform/viewer/src/index.js index 71d48e2ef..6a6c064bf 100644 --- a/platform/viewer/src/index.js +++ b/platform/viewer/src/index.js @@ -20,20 +20,28 @@ import { modes as defaultModes, extensions as defaultExtensions, } from './pluginImports'; +import loadDynamicConfig from './loadDynamicConfig'; -/** - * Combine our appConfiguration with installed extensions and modes. - * In the future appConfiguration may contain modes added at runtime. - * */ -const appProps = { - config: window ? window.config : {}, - defaultExtensions, - defaultModes, -}; +loadDynamicConfig(window.config).then(config_json => { + // Reset Dynamic config if defined + if (config_json !== null) { + window.config = config_json; + } -/** Create App */ -const app = React.createElement(App, appProps, null); -/** Render */ -ReactDOM.render(app, document.getElementById('root')); + /** + * Combine our appConfiguration with installed extensions and modes. + * In the future appConfiguration may contain modes added at runtime. + * */ + const appProps = { + config: window ? window.config : {}, + defaultExtensions, + defaultModes, + }; + + /** Create App */ + const app = React.createElement(App, appProps, null); + /** Render */ + ReactDOM.render(app, document.getElementById('root')); +}); export { history }; diff --git a/platform/viewer/src/loadDynamicConfig.js b/platform/viewer/src/loadDynamicConfig.js new file mode 100644 index 000000000..f646eefef --- /dev/null +++ b/platform/viewer/src/loadDynamicConfig.js @@ -0,0 +1,23 @@ +export default async config => { + const useDynamicConfig = config.dangerouslyUseDynamicConfig; + + // Check if dangerouslyUseDynamicConfig enabled + if (useDynamicConfig?.enabled) { + // If enabled then get configUrl query-string + let query = new URLSearchParams(window.location.search); + let configUrl = query.get('configUrl'); + + if (configUrl) { + // validate regex + const regex = useDynamicConfig.regex; + + if (configUrl.match(regex)) { + const response = await fetch(configUrl); + return response.json(); + } else { + return null; + } + } + } + return null; +}; diff --git a/platform/viewer/src/routes/DataSourceWrapper.tsx b/platform/viewer/src/routes/DataSourceWrapper.tsx index d5357875e..026bba14c 100644 --- a/platform/viewer/src/routes/DataSourceWrapper.tsx +++ b/platform/viewer/src/routes/DataSourceWrapper.tsx @@ -170,6 +170,7 @@ function _getQueryFilterValues(query, queryLimit) { // Offset... offset: Math.floor((pageNumber * resultsPerPage) / queryLimit) * (queryLimit - 1), + config: query.get('configUrl'), }; // patientName: good diff --git a/platform/viewer/src/routes/WorkList/WorkList.tsx b/platform/viewer/src/routes/WorkList/WorkList.tsx index 7cbc57cf1..ee3cc0aac 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.tsx +++ b/platform/viewer/src/routes/WorkList/WorkList.tsx @@ -345,12 +345,17 @@ function WorkList({ // mode.routeName // mode.routes[x].path // Don't specify default data source, and it should just be picked up... (this may not currently be the case) - // How do we know which params to pass? Today, it's just StudyInstanceUIDs + // How do we know which params to pass? Today, it's just StudyInstanceUIDs and configUrl if exists + const query = new URLSearchParams(); + if (filterValues.configUrl) { + query.append('configUrl', filterValues.configUrl); + } + query.append('StudyInstanceUIDs', studyInstanceUid); return (