Fixes for using OHIF Viewer against https site over DICOMWeb
This commit is contained in:
parent
143d65b8c8
commit
7ec1c60bc5
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
15
Packages/ohif-wadoproxy/server/convertURL.js
Normal file
15
Packages/ohif-wadoproxy/server/convertURL.js
Normal 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}`;
|
||||||
|
}
|
||||||
@ -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");
|
|
||||||
@ -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});
|
|
||||||
}
|
|
||||||
@ -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`);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -29,5 +29,8 @@
|
|||||||
"displaySetNavigationMultipleViewports": true,
|
"displaySetNavigationMultipleViewports": true,
|
||||||
"autoPositionMeasurementsTextCallOuts": "TRLB"
|
"autoPositionMeasurementsTextCallOuts": "TRLB"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"proxy": {
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user