diff --git a/LesionTracker/client/config.js b/LesionTracker/client/config.js index dba72a935..44336c104 100644 --- a/LesionTracker/client/config.js +++ b/LesionTracker/client/config.js @@ -1,4 +1,5 @@ import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import { OHIF } from 'meteor/ohif:core'; import { cornerstoneWADOImageLoader } from 'meteor/ohif:cornerstone'; @@ -19,4 +20,15 @@ Meteor.startup(function() { }; cornerstoneWADOImageLoader.webWorkerManager.initialize(config); + + cornerstoneWADOImageLoader.configure({ + beforeSend: function(xhr) { + const userId = Meteor.userId(); + const loginToken = Accounts._storedLoginToken(); + if (userId && loginToken) { + xhr.setRequestHeader("x-user-id", userId); + xhr.setRequestHeader("x-auth-token", loginToken); + } + } + }); }); \ No newline at end of file diff --git a/Packages/ohif-dicom-services/server/DICOMWeb/getBulkData.js b/Packages/ohif-dicom-services/server/DICOMWeb/getBulkData.js index da30f7853..3e2f9b0ba 100644 --- a/Packages/ohif-dicom-services/server/DICOMWeb/getBulkData.js +++ b/Packages/ohif-dicom-services/server/DICOMWeb/getBulkData.js @@ -1,6 +1,6 @@ - const ASCII = 'ascii'; -const http = Npm.require('http'), url = Npm.require('url'); +const http = Npm.require('http') +const url = Npm.require('url'); function getMultipartContentInfo(headers) { @@ -120,24 +120,37 @@ function parseResponse(headers, data) { } function makeRequest(geturl, options, callback) { - - let parsedUrl = url.parse(geturl); + const headers = 'multipart/related; type=application/octet-stream'; + const parsed = url.parse(geturl); let requestOpt = { - hostname: parsedUrl.hostname, - port: parsedUrl.port, + hostname: parsed.hostname, headers: { - Accept: 'multipart/related; type=application/octet-stream' + Accept: headers }, - path: parsedUrl.path, - method: 'GET' + path: parsed.path, + method: 'GET', }; + let requester; + if (parsed.protocol === 'https:') { + requester = https.request; + + const allowUnauthorizedAgent = new https.Agent({ rejectUnauthorized: false }); + requestOpt.agent = allowUnauthorizedAgent + } else { + requester = http.request; + } + + if (parsed.port) { + requestOpt.port = parsed.port; + } + if (options.auth) { requestOpt.auth = options.auth; } - let req = http.request(requestOpt, function(resp) { + let req = requester(requestOpt, function(resp) { let data = []; @@ -168,6 +181,7 @@ function makeRequest(geturl, options, callback) { const makeRequestSync = Meteor.wrapAsync(makeRequest); +// TODO: Unify this stuff with the getJSON code DICOMWeb.getBulkData = function(geturl, options) { if (options.logRequests) { diff --git a/Packages/ohif-dicom-services/server/DICOMWeb/getJSON.js b/Packages/ohif-dicom-services/server/DICOMWeb/getJSON.js index a83b9f28d..62bb52541 100644 --- a/Packages/ohif-dicom-services/server/DICOMWeb/getJSON.js +++ b/Packages/ohif-dicom-services/server/DICOMWeb/getJSON.js @@ -1,31 +1,47 @@ -var http = Npm.require('http'), url = Npm.require('url'); +const http = Npm.require('http'); +const https = Npm.require('https'); +const url = Npm.require('url'); function makeRequest(geturl, options, callback) { - var parsed = url.parse(geturl), jsonHeaders = ['application/json', 'application/dicom+json']; - - var requestOpt = { + const parsed = url.parse(geturl); + const jsonHeaders = ['application/json', 'application/dicom+json']; + + let requestOpt = { hostname: parsed.hostname, - port: parsed.port, headers: { Accept: 'application/json' }, path: parsed.path, method: 'GET' }; + + let requester; + if (parsed.protocol === 'https:') { + requester = https.request; + + const allowUnauthorizedAgent = new https.Agent({ rejectUnauthorized: false }); + requestOpt.agent = allowUnauthorizedAgent + } else { + requester = http.request; + } + + if (parsed.port) { + requestOpt.port = parsed.port; + } + if (options.auth) { requestOpt.auth = options.auth; } - var req = http.request(requestOpt, function(resp) { + const req = requester(requestOpt, function(resp) { const contentType = (resp.headers['content-type'] || '').split(';')[0]; - if (jsonHeaders.indexOf(contentType) == -1) { const errorMessage = `We only support json but "${contentType}" was sent by the server`; callback(new Error(errorMessage), null); return; } - var output = ''; + let output = ''; resp.setEncoding('utf8'); resp.on('data', function(chunk){ output += chunk; @@ -36,7 +52,7 @@ function makeRequest(geturl, options, callback) { }); req.end(); } -var makeRequestSync = Meteor.wrapAsync(makeRequest); +const makeRequestSync = Meteor.wrapAsync(makeRequest); DICOMWeb.getJSON = function(geturl, options) { if (options.logRequests) { @@ -47,7 +63,7 @@ DICOMWeb.getJSON = function(geturl, options) { console.time(geturl); } - var result = makeRequestSync(geturl, options); + const result = makeRequestSync(geturl, options); if (options.logTiming) { console.timeEnd(geturl); diff --git a/Packages/ohif-study-list/server/services/dimse/retrieveMetadata.js b/Packages/ohif-study-list/server/services/dimse/retrieveMetadata.js index 3503f7246..be203f3a7 100755 --- a/Packages/ohif-study-list/server/services/dimse/retrieveMetadata.js +++ b/Packages/ohif-study-list/server/services/dimse/retrieveMetadata.js @@ -135,7 +135,8 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) { // Retrieve the actual data over WADO-URI var server = getCurrentServer(); - instanceSummary.wadouri = WADOProxy.convertURL(server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + '&contentType=application%2Fdicom', server.requestOptions); + const wadouri = `${server.wadoUriRoot}?requestType=WADO&studyUID=${studyInstanceUid}&seriesUID=${seriesInstanceUid}&objectUID=${sopInstanceUid}&contentType=application%2Fdicom`; + instanceSummary.wadouri = WADOProxy.convertURL(wadouri, server); series.instances.push(instanceSummary); }); diff --git a/Packages/ohif-study-list/server/services/wado/retrieveMetadata.js b/Packages/ohif-study-list/server/services/wado/retrieveMetadata.js index 8d3695c84..168b9a50e 100644 --- a/Packages/ohif-study-list/server/services/wado/retrieveMetadata.js +++ b/Packages/ohif-study-list/server/services/wado/retrieveMetadata.js @@ -101,7 +101,8 @@ function getPaletteColor(server, instance, tag, lutDescriptor) { const lut = []; const numLutEntries = lutDescriptor[0]; const bits = lutDescriptor[2]; - const data = DICOMWeb.getBulkData(instance[tag].BulkDataURI, server.requestOptions); + const uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server) + const data = DICOMWeb.getBulkData(uri); for (var i = 0; i < numLutEntries; i++) { if(bits === 16) { @@ -296,8 +297,8 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) { contrastBolusAgent: DICOMWeb.getString(instance['00180010']), radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance), baseWadoRsUri: baseWadoRsUri, - wadouri: WADOProxy.convertURL(wadouri, server.requestOptions), - wadorsuri: WADOProxy.convertURL(wadorsuri), + wadouri: WADOProxy.convertURL(wadouri, server), + wadorsuri: WADOProxy.convertURL(wadorsuri, server), imageRendering: server.imageRendering, thumbnailRendering: server.thumbnailRendering }; diff --git a/Packages/ohif-viewerbase/client/lib/getWADORSImageUrl.js b/Packages/ohif-viewerbase/client/lib/getWADORSImageUrl.js index c0df8fdf9..4f7b82dfb 100644 --- a/Packages/ohif-viewerbase/client/lib/getWADORSImageUrl.js +++ b/Packages/ohif-viewerbase/client/lib/getWADORSImageUrl.js @@ -9,8 +9,6 @@ export function getWADORSImageUrl(instance, frame) { frame = (frame || 0) + 1; // Replaces /frame/1 by /frame/{frame} - // TODO: Maybe should be better to export the WADOProxy to be able to use it on client - // Example: WADOProxy.convertURL(baseWadoRsUri + '/frame/' + frame) wadorsuri = wadorsuri.replace(/(%2Fframes%2F)(\d+)/, `$1${frame}`); return wadorsuri; diff --git a/Packages/ohif-wadoproxy/package.js b/Packages/ohif-wadoproxy/package.js index 8cb33f06d..689acc31a 100755 --- a/Packages/ohif-wadoproxy/package.js +++ b/Packages/ohif-wadoproxy/package.js @@ -13,11 +13,10 @@ Package.onUse(function(api) { api.use('ohif:core'); - api.addFiles('server/namespace.js'); - - // Server-only + api.addFiles('server/namespace.js', 'server'); api.addFiles('server/initialize.js', 'server'); api.addFiles('server/routes.js', 'server'); + api.addFiles('server/convertURL.js', 'server'); // Global exports api.export('WADOProxy'); diff --git a/Packages/ohif-wadoproxy/server/convertURL.js b/Packages/ohif-wadoproxy/server/convertURL.js new file mode 100644 index 000000000..1fdba4f66 --- /dev/null +++ b/Packages/ohif-wadoproxy/server/convertURL.js @@ -0,0 +1,15 @@ +const querystring = require("querystring"); + +WADOProxy.convertURL = (url, serverConfiguration) => { + if (!Settings.enabled) { + return url; + } + + if (!url) { + return null; + } + + const serverId = serverConfiguration._id; + const query = querystring.stringify({url, serverId}); + return `${Settings.uri}?${query}`; +} \ No newline at end of file diff --git a/Packages/ohif-wadoproxy/server/initialize.js b/Packages/ohif-wadoproxy/server/initialize.js index 401dca678..a79eb19dd 100755 --- a/Packages/ohif-wadoproxy/server/initialize.js +++ b/Packages/ohif-wadoproxy/server/initialize.js @@ -1,10 +1,7 @@ +import { Meteor } from 'meteor/meteor'; import { OHIF } from 'meteor/ohif:core'; Settings = Object.assign({ uri : OHIF.utils.absoluteUrl("/__wado_proxy"), enabled: true -}, (Meteor.settings && Meteor.settings.proxy) ? Meteor.settings.proxy : {}); - -http = require("http"); -url = require("url"); -querystring = require("querystring"); \ No newline at end of file +}, (Meteor.settings && Meteor.settings.proxy) ? Meteor.settings.proxy : {}); \ No newline at end of file diff --git a/Packages/ohif-wadoproxy/server/namespace.js b/Packages/ohif-wadoproxy/server/namespace.js index a9c3980bc..533560af9 100755 --- a/Packages/ohif-wadoproxy/server/namespace.js +++ b/Packages/ohif-wadoproxy/server/namespace.js @@ -1,12 +1 @@ -WADOProxy = {}; - -WADOProxy.convertURL = (wadoURL, requestOptions) => { - if (!Settings.enabled) { - return wadoURL; - } - if (!wadoURL) { - return null; - } - - return Settings.uri + '?' + querystring.stringify({url: wadoURL, options: requestOptions ? JSON.stringify(requestOptions) : null}); -} \ No newline at end of file +WADOProxy = {}; \ No newline at end of file diff --git a/Packages/ohif-wadoproxy/server/routes.js b/Packages/ohif-wadoproxy/server/routes.js index 18a96577d..79f94ad8f 100755 --- a/Packages/ohif-wadoproxy/server/routes.js +++ b/Packages/ohif-wadoproxy/server/routes.js @@ -1,62 +1,132 @@ +import { Meteor } from 'meteor/meteor'; +import { Router } from 'meteor/iron:router'; +import { Accounts } from 'meteor/accounts-base'; + import { OHIF } from 'meteor/ohif:core'; +const url = require("url"); +const http = require("http"); +const https = require("https"); +const querystring = require("querystring"); + +const doAuth = Meteor.users.find().count() ? true : false; + +const authenticateUser = request => { + // Only allow logged-in users to access this route + const userId = request.headers['x-user-id'] + const loginToken = request.headers['x-auth-token'] + if (!userId || !loginToken) { + return; + } + + const hashedToken = Accounts._hashLoginToken(loginToken); + + return Meteor.users.findOne({ + _id: userId, + 'services.resume.loginTokens.hashedToken': hashedToken + }); +} + // Setup a Route using Iron Router to avoid Cross-origin resource sharing // (CORS) errors. We only handle this route on the Server. Router.route(Settings.uri.replace(OHIF.utils.absoluteUrl(), ''), function() { const request = this.request; const response = this.response; const params = this.params; - const requestOptions = params.query.options ? JSON.parse(params.query.options) : {}; + + let user; + if (doAuth) { + user = authenticateUser(request); + if (!user) { + response.writeHead(401); + response.end('Error: You must be logged in to perform this action.\n'); + return; + } + } + + // TODO: Merge this with ohif-study-list? There is a circular dependency now... + const server = Servers.findOne(params.query.serverId); + if (!server) { + response.writeHead(500); + response.end('Error: No Server with the specified Server ID was found.\n'); + return; + } + + const requestOpt = server.requestOptions; // If no Web Access to DICOM Objects (WADO) Service URL is provided // return an error for the request. - if (!params.query.url) { + const wadoUrl = params.query.url; + if (!wadoUrl) { response.writeHead(500); response.end('Error: No WADO URL was provided.\n'); return; } + if (requestOpt.logRequests) { + console.log(request.url); + } + + if (requestOpt.logTiming) { + console.time(request.url); + } + // Use Node's URL parse to decode the query URL - const wadoUrl = url.parse(params.query.url); + const parsed = url.parse(wadoUrl); // Create an object to hold the information required // for the request to the PACS. let options = { headers: {}, method: request.method, - hostname: wadoUrl.hostname, - port: wadoUrl.port, //maybe null - path: wadoUrl.path, + hostname: parsed.hostname, + path: parsed.path }; - if (request.headers['referer']) { - options.headers.referer = request.headers['referer']; + let requester; + if (parsed.protocol === 'https:') { + requester = https.request; + + const allowUnauthorizedAgent = new https.Agent({ rejectUnauthorized: false }); + options.agent = allowUnauthorizedAgent + } else { + requester = http.request; } - if (request.headers['user-agent']) { - options.headers['user-agent'] = request.headers['user-agent']; - } - if (request.headers['accept']) { - options.headers['accept'] = request.headers['accept']; + + if (parsed.port) { + options.port = parsed.port; } + Object.keys(request.headers).forEach(entry => { + const value = request.headers[entry]; + if (entry) { + console.log(`${entry}: ${value}`); + options.headers[entry] = value; + } + }); + // Retrieve the authorization user:password string for the PACS, // if one is required, and include it in the request to the PACS. - //const wadoAuth = 'orthanc:orthanc'; - if (requestOptions.auth) { - options.auth = requestOptions.auth; + if (requestOpt.auth) { + options.auth = requestOpt.auth; } // Use Node's HTTP API to send a request to the PACS - const proxyRequest = http.request(options, (proxyResponse) => { + const proxyRequest = requester(options, proxyResponse => { // When we receive data from the PACS, stream it as the // response to the original request. // console.log(`Got response: ${proxyResponse.statusCode}`); response.writeHead(proxyResponse.statusCode, proxyResponse.headers); + + if (requestOpt.logTiming) { + console.timeEnd(request.url); + } + return proxyResponse.pipe(response, {end: true}); }); // If our request to the PACS fails, log the error message - proxyRequest.on('error', (error) => { + proxyRequest.on('error', error => { response.writeHead(500); response.end(`Error: Problem with request to PACS: ${error.message}\n`); }); diff --git a/config/orthancDICOMWeb.json b/config/orthancDICOMWeb.json index a241fa089..2c6619e64 100644 --- a/config/orthancDICOMWeb.json +++ b/config/orthancDICOMWeb.json @@ -29,5 +29,8 @@ "displaySetNavigationMultipleViewports": true, "autoPositionMeasurementsTextCallOuts": "TRLB" } + }, + "proxy": { + "enabled": true } }