feat(WADO) Allow WADO / QIDO requests to originate from client (#222)

This commit is contained in:
Erik Ziegler authored and GitHub committed 2018-07-17 13:03:36 +02:00
1 parent 92408d4c86
commit ebe0a40f3e
43 files changed
+519 -571

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
import makeRequest from "./makeRequest";
/**
* Make a request to the URL given a set of options.
* This function will add optional timing, request, and respone logging.
*
* @param {String} url
* @param {Object} options
* @return {Promise<*>}
*/
const get = async function get(url, options) {
if (options.logRequests) {
console.log(url);
}
if (options.logTiming) {
console.time(url);
}
const result = await makeRequest(url, options);
if (options.logTiming) {
console.timeEnd(url);
}
if (options.logResponses) {
console.info(result);
}
return result;
}
export default get;
@@ -0,0 +1,46 @@
/**
* Returns the specified element as a dicom attribute group/element.
*
* @param element - The group/element of the element (e.g. '00280009')
* @param [defaultValue] - The value to return if the element is not present
* @returns {*}
*/
const getAttribute = function(element, defaultValue) {
if (!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if (!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if (!element.Value.length) {
return defaultValue;
}
return convertToInt(element.Value);
};
function convertToInt(input) {
function padFour(input) {
var l = input.length;
if (l == 0) return '0000';
if (l == 1) return '000' + input;
if (l == 2) return '00' + input;
if (l == 3) return '0' + input;
return input;
}
var output = '';
for (var i = 0; i < input.length; i++) {
for (var j = 0; j < input[i].length; j++) {
output += padFour(input[i].charCodeAt(j).toString(16));
}
}
return parseInt(output, 16);
}
export default getAttribute;
@@ -0,0 +1,133 @@
import get from './get.js';
const ASCII = 'ascii';
function getMultipartContentInfo(headers) {
const multipartRegex = /^\s*multipart\/[^;]+/g;
const contentType = headers.get('content-type');
let dict = null;
if (typeof contentType === 'string' && multipartRegex.test(contentType)) {
let match;
let pairRegex = /;\s*([^=]+)=([^;\r\n]+)/g;
pairRegex.lastIndex = multipartRegex.lastIndex;
while ((match = pairRegex.exec(contentType)) !== null) {
let key = match[1]
let value = match[2];
if (dict === null) {
dict = {};
}
dict[key] = value;
}
}
return dict;
}
function parseContentHeader(data, offset) {
const endOfHeader = data.indexOf('\r\n\r\n', offset);
if (endOfHeader < 0 || endOfHeader <= offset || endOfHeader > data.length) {
throw new Error('End of content header cannot be determined...');
}
const header = {
start: offset,
end: endOfHeader + 4,
fields: {}
};
const headerRegex = /([\w\-]+):\s*([^\r\n]+)(?:\r\n)?/g;
const headerContentRegex = /\t([^\r\n]+)(?:\r\n)?/g;
const headerString = data.slice(offset, endOfHeader);
let match;
while ((match = headerRegex.exec(headerString)) !== null) {
const key = match[1].toLowerCase(), value = match[2];
while (headerString.charAt(headerRegex.lastIndex) === '\t') {
headerContentRegex.lastIndex = headerRegex.lastIndex;
if ((match = headerContentRegex.exec(headerString)) !== null) {
headerRegex.lastIndex = headerContentRegex.lastIndex;
value += ' ' + match[1];
continue;
}
break;
}
header.fields[key] = value;
}
return header;
}
function toBytes(input) {
const data = [];
for (let i = 0; i < input.length; i++){
data.push(input.charCodeAt(i));
}
return data;
}
function parseResponse(headers, data) {
const contentInfo = getMultipartContentInfo(headers);
if (!contentInfo || !contentInfo.boundary) {
throw new Error('Content boundary not specified...');
}
const boundary = contentInfo.boundary;
const delimiter = '--' + boundary + '\r\n';
let index = data.indexOf(delimiter, 0, ASCII);
if (index < 0) {
throw new Error('DICOMWeb: The specified boundary could not be found...');
}
let closingIndex
let closingDelimiter = '\r\n--' + boundary + '--';
index += delimiter.length;
if (data[index] !== 0x0D || data[index + 1] !== 0x0A) {
// multipart content headers are present, so let's parse them...
let contentHeader = parseContentHeader(data, index);
if (contentHeader && contentHeader.end > index) {
if (contentHeader.fields['content-length'] > 0) {
let endOfData = contentHeader.end + parseInt(contentHeader.fields['content-length'], 10);
if (endOfData === data.indexOf(closingDelimiter, endOfData)) {
// content-length is valid...
return data.slice(contentHeader.end, endOfData);
}
}
index = contentHeader.end;
}
} else {
// no headers, the content comes right after...
index += 2;
}
closingIndex = data.indexOf(closingDelimiter, index);
if (closingIndex < 0 || closingIndex < index) {
throw new Error('DICOMWeb: The end of the content could not be determined...')
}
return toBytes(data.slice(index, closingIndex));
}
const getBulkData = async function(url, options = {}) {
options.headers = options.headers || {};
options.headers.Accept = 'multipart/related; type=application/octet-stream';
const response = await get(url, options);
const data = await response.text();
return parseResponse(response.headers, data);
}
export default getBulkData;
@@ -0,0 +1,16 @@
import get from './get.js';
import getAttribute from "./getAttribute";
const getJSON = async function (url, options) {
options.headers = options.headers || {};
options.headers.Accept = 'application/json';
return get(url, options).then(response => {
if (!(response instanceof Response)) {
return '';
}
return response.json();
});
};
export default getJSON;
@@ -0,0 +1,23 @@
const getModalities = function(modality, modalitiesInStudy) {
var modalities = {};
if (modality) {
modalities = modality;
}
if (modalitiesInStudy) {
// Find vr in modalities
if (modalities.vr && modalities.vr === modalitiesInStudy.vr) {
for (var i = 0; i < modalitiesInStudy.Value.length; i++) {
var value = modalitiesInStudy.Value[i];
if (modalities.Value.indexOf(value) === -1) {
modalities.Value.push(value);
}
}
} else {
modalities = modalitiesInStudy;
}
}
return modalities;
};
export default getModalities;
@@ -0,0 +1,26 @@
/**
* Returns the Alphabetic version of a PN
*
* @param element - The group/element of the element (e.g. '00200013')
* @param [defaultValue] - The default value to return if the element is not found
* @returns {*}
*/
export default function getName(element, defaultValue) {
if (!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if (!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if (!element.Value.length) {
return defaultValue;
}
// Return the Alphabetic component group
if (element.Value[0].Alphabetic) {
return element.Value[0].Alphabetic;
}
// Orthanc does not return PN properly so this is a temporary workaround
return element.Value[0];
};
@@ -0,0 +1,21 @@
/**
* Returns the first string value as a Javascript Number
* @param element - The group/element of the element (e.g. '00200013')
* @param [defaultValue] - The default value to return if the element does not exist
* @returns {*}
*/
export default function getNumber(element, defaultValue) {
if (!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if (!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if (!element.Value.length) {
return defaultValue;
}
return parseFloat(element.Value[0]);
};
@@ -0,0 +1,23 @@
/**
* Returns the specified element as a string. Multi-valued elements will be separated by a backslash
*
* @param element - The group/element of the element (e.g. '00200013')
* @param [defaultValue] - The value to return if the element is not present
* @returns {*}
*/
export default function getString(element, defaultValue) {
if (!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if (!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if (!element.Value.length) {
return defaultValue;
}
// Join the array together separated by backslash
// NOTE: Orthanc does not correctly split values into an array so the join is a no-op
return element.Value.join('\\');
};
@@ -0,0 +1,23 @@
import get from './get.js';
import getAttribute from './getAttribute.js';
import getBulkData from './getBulkData.js';
import getJSON from './getJSON.js';
import getModalities from './getModalities.js';
import getName from './getName.js';
import getNumber from './getNumber.js';
import getString from './getString.js';
import makeRequest from './makeRequest.js';
const DICOMWeb = {
get,
getAttribute,
getBulkData,
getJSON,
getModalities,
getName,
getNumber,
getString,
makeRequest
};
export { DICOMWeb };
@@ -0,0 +1,60 @@
import { Meteor } from "meteor/meteor";
import URL from 'url-parse';
import 'isomorphic-fetch';
async function makeRequest(url, options) {
const parsed = new URL(url);
let requestOpt = {
method: 'GET',
headers: {}
};
// TODO: Clean this up
const accessToken = false //Meteor.user().services.keycloak.accessToken;
if (accessToken) {
requestOpt.headers = {
Authorization: `Bearer ${accessToken}`
};
} else if (requestOpt.auth) {
requestOpt.headers = {
Authorization: requestOpt.auth
};
}
if (options.headers) {
Object.keys(options.headers).forEach(key => {
requestOpt.headers[key] = options.headers[key];
});
}
return new Promise((resolve, reject) => {
// TODO: Kept getting weird build errors from RegExp
const isAbsolute = parsed.href.indexOf('http://') === 0 || parsed.href.indexOf('https://');
let url = parsed.href;
if (isAbsolute === false) {
url = Meteor.absoluteUrl(parsed.href);
}
fetch(url, requestOpt).then((response) => {
if (response.status >= 400) {
reject(new Error(response.status));
}
if (response.status === 204) {
resolve([]);
} else {
// TODO: Handle 204 no content
resolve(response);
}
}, (error) => {
console.error('There was an error in the DICOMWeb Server');
reject(new Error(error));
});
});
}
export default makeRequest;