diff --git a/README.md b/README.md index da23c94f..e3e5dca2 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,40 @@ Users can authenticate and authorize the application to access data via [OpenID ## Configuration +### Server Configuration + The app can be configured via a `public/config/{name}.js` JavaScript configuration file (see for example the default `public/config/local.js`). Please refer to the [AppConfig.d.ts](src/AppConfig.d.ts) file for configuration options. The configuration can be changed at build-time using the `REACT_APP_CONFIG` environment variable. +### Handling Mixed Content and HTTPS + +When deploying SLIM with HTTPS, you may encounter mixed content scenarios where your PACS/VNA server returns HTTP URLs in its responses. This commonly occurs when: + +- The PACS server populates bulkdataURI fields with internal HTTP URLs +- Your viewer is running on HTTPS but needs to communicate with services that respond with HTTP URLs +- You're using a reverse proxy that terminates SSL + +To handle these scenarios, SLIM provides the `upgradeInsecureRequests` option in the server configuration: + +```js +window.config = { + servers: [{ + id: "local", + url: "https://your-server.com/dcm4chee-arc/aets/MYAET/rs", + upgradeInsecureRequests: true // Enable automatic HTTP -> HTTPS upgrade + }] +} +``` + +When `upgradeInsecureRequests` is set to `true` and at least one of your URLs (service URL, QIDO, WADO, or STOW prefixes) uses HTTPS, the viewer will automatically: + +1. Add the `Content-Security-Policy: upgrade-insecure-requests` header to requests +2. Attempt to upgrade any HTTP responses to HTTPS + +This feature was implemented in response to [issue #159](https://github.com/ImagingDataCommons/slim/issues/159) where PACS servers would return HTTP bulkdata URIs even when accessed via HTTPS. + ## Deployment Download the latest release from [github.com/imagingdatacommons/slim/releases](https://github.com/imagingdatacommons/slim/releases) and then run the following commands to install build dependencies and build the app: @@ -223,7 +252,6 @@ Create an [OIDC client ID for web application](https://developers.google.com/ide Note that Google's OIDC implementation does currently not yet support the authorization code grant type with PKCE challenge for private clients. For the time being, the legacy implicit grand type has to be used. - ## Development To install requirements and run the app for local development, run the following commands: diff --git a/src/AppConfig.d.ts b/src/AppConfig.d.ts index 469e892a..2c6df033 100644 --- a/src/AppConfig.d.ts +++ b/src/AppConfig.d.ts @@ -65,6 +65,7 @@ export interface ServerSettings { retry?: RetryRequestSettings errorMessages?: ErrorMessageSettings[] storageClasses?: string[] + upgradeInsecureRequests?: boolean } export interface OidcSettings { diff --git a/src/DicomWebManager.ts b/src/DicomWebManager.ts index 36ce938f..c7bdb011 100644 --- a/src/DicomWebManager.ts +++ b/src/DicomWebManager.ts @@ -58,9 +58,20 @@ export default class DicomWebManager implements dwc.api.DICOMwebClient { ) ) } + + const hasHttpsUrl = (url?: string): boolean => url?.startsWith('https') ?? false + const clientSettings: dwc.api.DICOMwebClientOptions = { url: serviceUrl } + + const shouldUpgradeInsecure = serverSettings.upgradeInsecureRequests === true && [ + serviceUrl, + serverSettings.qidoPathPrefix, + serverSettings.wadoPathPrefix, + serverSettings.stowPathPrefix + ].some(hasHttpsUrl) + if (serverSettings.qidoPathPrefix !== undefined) { clientSettings.qidoURLPrefix = serverSettings.qidoPathPrefix } @@ -70,6 +81,14 @@ export default class DicomWebManager implements dwc.api.DICOMwebClient { if (serverSettings.stowPathPrefix !== undefined) { clientSettings.stowURLPrefix = serverSettings.stowPathPrefix } + + if (shouldUpgradeInsecure) { + clientSettings.headers = { + ...clientSettings.headers, + 'Content-Security-Policy': 'upgrade-insecure-requests' + } + } + if (serverSettings.retry !== undefined) { clientSettings.requestHooks = [getXHRRetryHook(serverSettings.retry)] } diff --git a/types/dicomweb-client/index.d.ts b/types/dicomweb-client/index.d.ts index 02dfac16..07299346 100644 --- a/types/dicomweb-client/index.d.ts +++ b/types/dicomweb-client/index.d.ts @@ -10,12 +10,13 @@ declare module 'dicomweb-client' { export type DICOMwebClientRequestHook = (request: XMLHttpRequest, metadata: DICOMwebClientRequestHookMetadata) => XMLHttpRequest export interface DICOMwebClientOptions { - url: string|undefined + url: string | undefined qidoURLPrefix?: string wadoURLPrefix?: string stowURLPrefix?: string headers?: { Authorization?: string + 'Content-Security-Policy'?: string } requestHooks?: DICOMwebClientRequestHook[] errorInterceptor?: (request: DICOMwebClientError) => void @@ -133,49 +134,49 @@ declare module 'dicomweb-client' { export type Dataset = ArrayBuffer export interface DICOMwebClient { - headers: {[key: string]: string} + headers: { [key: string]: string } baseURL: string // STOW-RS - storeInstances (options: StoreInstancesOptions): Promise + storeInstances(options: StoreInstancesOptions): Promise // QIDO-RS - searchForStudies ( + searchForStudies( options: SearchForStudiesOptions ): Promise - searchForSeries ( + searchForSeries( options: SearchForSeriesOptions ): Promise - searchForInstances ( + searchForInstances( options: SearchForInstancesOptions ): Promise // WADO-RS - retrieveStudyMetadata ( + retrieveStudyMetadata( options: RetrieveStudyMetadataOptions ): Promise - retrieveSeriesMetadata ( + retrieveSeriesMetadata( options: RetrieveSeriesMetadataOptions ): Promise - retrieveInstanceMetadata ( + retrieveInstanceMetadata( options: RetrieveInstanceMetadataOptions ): Promise - retrieveInstance ( + retrieveInstance( options: RetrieveInstanceOptions ): Promise - retrieveInstanceFrames ( + retrieveInstanceFrames( options: RetrieveInstanceFramesOptions ): Promise - retrieveInstanceRendered ( + retrieveInstanceRendered( options: RetrieveInstanceRenderedOptions ): Promise - retrieveInstanceFramesRendered ( + retrieveInstanceFramesRendered( options: RetrieveInstanceFramesRenderedOptions ): Promise - retrieveBulkData ( + retrieveBulkData( options: RetrieveBulkDataOptions ): Promise } export class DICOMwebClient implements DICOMwebClient { - constructor (options: DICOMwebClientOptions) + constructor(options: DICOMwebClientOptions) } export interface MetadataElement {