Fixes for using OHIF Viewer against https site over DICOMWeb

This commit is contained in:
Erik Ziegler 2017-03-22 10:20:30 +01:00
parent 143d65b8c8
commit 7ec1c60bc5
12 changed files with 179 additions and 64 deletions

View File

@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { OHIF } from 'meteor/ohif:core'; import { OHIF } from 'meteor/ohif:core';
import { cornerstoneWADOImageLoader } from 'meteor/ohif:cornerstone'; import { cornerstoneWADOImageLoader } from 'meteor/ohif:cornerstone';
@ -19,4 +20,15 @@ Meteor.startup(function() {
}; };
cornerstoneWADOImageLoader.webWorkerManager.initialize(config); 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);
}
}
});
}); });

View File

@ -1,6 +1,6 @@
const ASCII = 'ascii'; 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) { function getMultipartContentInfo(headers) {
@ -120,24 +120,37 @@ function parseResponse(headers, data) {
} }
function makeRequest(geturl, options, callback) { function makeRequest(geturl, options, callback) {
const headers = 'multipart/related; type=application/octet-stream';
let parsedUrl = url.parse(geturl); const parsed = url.parse(geturl);
let requestOpt = { let requestOpt = {
hostname: parsedUrl.hostname, hostname: parsed.hostname,
port: parsedUrl.port,
headers: { headers: {
Accept: 'multipart/related; type=application/octet-stream' Accept: headers
}, },
path: parsedUrl.path, path: parsed.path,
method: 'GET' 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) { if (options.auth) {
requestOpt.auth = options.auth; requestOpt.auth = options.auth;
} }
let req = http.request(requestOpt, function(resp) { let req = requester(requestOpt, function(resp) {
let data = []; let data = [];
@ -168,6 +181,7 @@ function makeRequest(geturl, options, callback) {
const makeRequestSync = Meteor.wrapAsync(makeRequest); const makeRequestSync = Meteor.wrapAsync(makeRequest);
// TODO: Unify this stuff with the getJSON code
DICOMWeb.getBulkData = function(geturl, options) { DICOMWeb.getBulkData = function(geturl, options) {
if (options.logRequests) { if (options.logRequests) {

View File

@ -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) { function makeRequest(geturl, options, callback) {
var parsed = url.parse(geturl), jsonHeaders = ['application/json', 'application/dicom+json']; const parsed = url.parse(geturl);
const jsonHeaders = ['application/json', 'application/dicom+json'];
var requestOpt = { let requestOpt = {
hostname: parsed.hostname, hostname: parsed.hostname,
port: parsed.port,
headers: { headers: {
Accept: 'application/json' Accept: 'application/json'
}, },
path: parsed.path, path: parsed.path,
method: 'GET' 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) { if (options.auth) {
requestOpt.auth = 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]; const contentType = (resp.headers['content-type'] || '').split(';')[0];
if (jsonHeaders.indexOf(contentType) == -1) { if (jsonHeaders.indexOf(contentType) == -1) {
const errorMessage = `We only support json but "${contentType}" was sent by the server`; const errorMessage = `We only support json but "${contentType}" was sent by the server`;
callback(new Error(errorMessage), null); callback(new Error(errorMessage), null);
return; return;
} }
var output = ''; let output = '';
resp.setEncoding('utf8'); resp.setEncoding('utf8');
resp.on('data', function(chunk){ resp.on('data', function(chunk){
output += chunk; output += chunk;
@ -36,7 +52,7 @@ function makeRequest(geturl, options, callback) {
}); });
req.end(); req.end();
} }
var makeRequestSync = Meteor.wrapAsync(makeRequest); const makeRequestSync = Meteor.wrapAsync(makeRequest);
DICOMWeb.getJSON = function(geturl, options) { DICOMWeb.getJSON = function(geturl, options) {
if (options.logRequests) { if (options.logRequests) {
@ -47,7 +63,7 @@ DICOMWeb.getJSON = function(geturl, options) {
console.time(geturl); console.time(geturl);
} }
var result = makeRequestSync(geturl, options); const result = makeRequestSync(geturl, options);
if (options.logTiming) { if (options.logTiming) {
console.timeEnd(geturl); console.timeEnd(geturl);

View File

@ -135,7 +135,8 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
// Retrieve the actual data over WADO-URI // Retrieve the actual data over WADO-URI
var server = getCurrentServer(); 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); series.instances.push(instanceSummary);
}); });

View File

@ -101,7 +101,8 @@ function getPaletteColor(server, instance, tag, lutDescriptor) {
const lut = []; const lut = [];
const numLutEntries = lutDescriptor[0]; const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2]; 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++) { for (var i = 0; i < numLutEntries; i++) {
if(bits === 16) { if(bits === 16) {
@ -296,8 +297,8 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
contrastBolusAgent: DICOMWeb.getString(instance['00180010']), contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance), radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri, baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server.requestOptions), wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri), wadorsuri: WADOProxy.convertURL(wadorsuri, server),
imageRendering: server.imageRendering, imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering thumbnailRendering: server.thumbnailRendering
}; };

View File

@ -9,8 +9,6 @@ export function getWADORSImageUrl(instance, frame) {
frame = (frame || 0) + 1; frame = (frame || 0) + 1;
// Replaces /frame/1 by /frame/{frame} // 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}`); wadorsuri = wadorsuri.replace(/(%2Fframes%2F)(\d+)/, `$1${frame}`);
return wadorsuri; return wadorsuri;

View File

@ -13,11 +13,10 @@ Package.onUse(function(api) {
api.use('ohif:core'); api.use('ohif:core');
api.addFiles('server/namespace.js'); api.addFiles('server/namespace.js', 'server');
// Server-only
api.addFiles('server/initialize.js', 'server'); api.addFiles('server/initialize.js', 'server');
api.addFiles('server/routes.js', 'server'); api.addFiles('server/routes.js', 'server');
api.addFiles('server/convertURL.js', 'server');
// Global exports // Global exports
api.export('WADOProxy'); api.export('WADOProxy');

View File

@ -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}`;
}

View File

@ -1,10 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { OHIF } from 'meteor/ohif:core'; import { OHIF } from 'meteor/ohif:core';
Settings = Object.assign({ Settings = Object.assign({
uri : OHIF.utils.absoluteUrl("/__wado_proxy"), uri : OHIF.utils.absoluteUrl("/__wado_proxy"),
enabled: true enabled: true
}, (Meteor.settings && Meteor.settings.proxy) ? Meteor.settings.proxy : {}); }, (Meteor.settings && Meteor.settings.proxy) ? Meteor.settings.proxy : {});
http = require("http");
url = require("url");
querystring = require("querystring");

View File

@ -1,12 +1 @@
WADOProxy = {}; 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});
}

View File

@ -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'; 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 // Setup a Route using Iron Router to avoid Cross-origin resource sharing
// (CORS) errors. We only handle this route on the Server. // (CORS) errors. We only handle this route on the Server.
Router.route(Settings.uri.replace(OHIF.utils.absoluteUrl(), ''), function() { Router.route(Settings.uri.replace(OHIF.utils.absoluteUrl(), ''), function() {
const request = this.request; const request = this.request;
const response = this.response; const response = this.response;
const params = this.params; 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 // If no Web Access to DICOM Objects (WADO) Service URL is provided
// return an error for the request. // return an error for the request.
if (!params.query.url) { const wadoUrl = params.query.url;
if (!wadoUrl) {
response.writeHead(500); response.writeHead(500);
response.end('Error: No WADO URL was provided.\n'); response.end('Error: No WADO URL was provided.\n');
return; 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 // 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 // Create an object to hold the information required
// for the request to the PACS. // for the request to the PACS.
let options = { let options = {
headers: {}, headers: {},
method: request.method, method: request.method,
hostname: wadoUrl.hostname, hostname: parsed.hostname,
port: wadoUrl.port, //maybe null path: parsed.path
path: wadoUrl.path,
}; };
if (request.headers['referer']) { let requester;
options.headers.referer = request.headers['referer']; 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 (parsed.port) {
options.port = parsed.port;
} }
if (request.headers['accept']) {
options.headers['accept'] = request.headers['accept']; 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, // Retrieve the authorization user:password string for the PACS,
// if one is required, and include it in the request to the PACS. // if one is required, and include it in the request to the PACS.
//const wadoAuth = 'orthanc:orthanc'; if (requestOpt.auth) {
if (requestOptions.auth) { options.auth = requestOpt.auth;
options.auth = requestOptions.auth;
} }
// Use Node's HTTP API to send a request to the PACS // 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 // When we receive data from the PACS, stream it as the
// response to the original request. // response to the original request.
// console.log(`Got response: ${proxyResponse.statusCode}`); // console.log(`Got response: ${proxyResponse.statusCode}`);
response.writeHead(proxyResponse.statusCode, proxyResponse.headers); response.writeHead(proxyResponse.statusCode, proxyResponse.headers);
if (requestOpt.logTiming) {
console.timeEnd(request.url);
}
return proxyResponse.pipe(response, {end: true}); return proxyResponse.pipe(response, {end: true});
}); });
// If our request to the PACS fails, log the error message // If our request to the PACS fails, log the error message
proxyRequest.on('error', (error) => { proxyRequest.on('error', error => {
response.writeHead(500); response.writeHead(500);
response.end(`Error: Problem with request to PACS: ${error.message}\n`); response.end(`Error: Problem with request to PACS: ${error.message}\n`);
}); });

View File

@ -29,5 +29,8 @@
"displaySetNavigationMultipleViewports": true, "displaySetNavigationMultipleViewports": true,
"autoPositionMeasurementsTextCallOuts": "TRLB" "autoPositionMeasurementsTextCallOuts": "TRLB"
} }
},
"proxy": {
"enabled": true
} }
} }