var EventEmitter = Npm.require('events').EventEmitter; function time() { return Math.floor(Date.now() / 1000); } var DEFAULT_MAX_PACKAGE_SIZE = 32768; var DEFAULT_SOURCE_AE = "OHIFDCM"; var Envelope = function(conn, command, dataset) { EventEmitter.call(this); this.command = command; this.dataset = dataset; this.conn = conn; } util.inherits(Envelope, EventEmitter); Envelope.prototype.send = function() { return this; } Connection = function(socket, options) { EventEmitter.call(this); this.socket = socket; this.options = Object.assign({ hostAE: "", sourceAE: "OHIFDCM", maxPackageSize: 32768, idle: 60, reconnect: true, vr: { split: true } }, options); this.connected = false; this.started = null; this.lastReceived = null; this.associated = false; this.receiving = null; this.receiveLength = null; this.minRecv = null; this.pendingPDVs = null; this.server = null; //this.retrieveModel = RETRIEVE_MODEL_STUDY_ROOT; this.presentationContexts = []; this.transferSyntaxes = []; this.negotiatedContexts = {}; this.messages = {}; this.messageIdCounter = 0; this.services = []; this.lastCommand = null; this.lastSent = null; this.lastGets = []; this.findContext = C.SOP_STUDY_ROOT_FIND; //register hooks var o = this; this.socket.on("data", function(data) { o.received(data); }); this.socket.on("close", function(he) { o.closed(he); o.emit("close", he); }); this.socket.on("error", function(he) { o.error(he); }); this.socket.on("end", function() { if (o.intervalId) { clearInterval(o.intervalId); } if (o.server) { console.log("Closing server"); o.server.close(); } console.log('ended'); }) this.on("released", function() { this.released(); }); this.on('aborted', function() { this.released(); }) this.on('message', function(pdvs) { this.receivedMessage(pdvs); }); this.on("init", this.ready); //this.pause(); if (this.options.listenHost && this.options.listenPort) { this.server = net.createServer(); this.server.listen(this.options.listenPort, this.options.listenHost); this.server.on('listening', function() { console.log("listening on %j", this.address()); }); this.server.on('connection', function(socket) { }); } this.emit("init"); } util.inherits(Connection, EventEmitter); Connection.prototype.checkIdle = function() { var current = time(), idl = this.options.idle; if (!this.lastReceived && (current - this.started >= idl)) { this.idleClose(); } else if (this.lastReceived && (current - this.lastReceived >= idl)) { this.idleClose(); } else { //console.log('keep idling') } }; Connection.prototype.released = function() { this.socket.end(); }; Connection.prototype.idleClose = function() { console.log('Exceed idle time, closing connection'); this.release(); }; Connection.prototype.getSoureceAE = function() { return this.options.sourceAE; }; Connection.prototype.ready = function() { console.log("Connection established"); this.connected = true; this.started = time(); var o = this; this.intervalId = setInterval(function() { o.checkIdle(); }, 3000); //this.emit("init"); //this.startAssociationRequest(); }; Connection.prototype.resetReceive = function() { this.receiving = this.receiveLength = null; }; Connection.prototype.received = function(data) { var i = 0; do { data = this.process(data); } while (data !== null); this.lastReceived = time(); }; Connection.prototype.process = function(data) { //console.log("Data received"); if (this.receiving === null) { if (this.minRecv) { data = Buffer.concat([this.minRecv, data], this.minRecv.length + data.length); this.minRecv = null; } if (data.length < 6) { this.minRecv = data; return null; } var stream = new ReadStream(data); var type = stream.read(C.TYPE_UINT8); stream.increment(1); var len = stream.read(C.TYPE_UINT32), cmp = data.length - 6; if (len > cmp) { this.receiving = data; this.receiveLength = len; } else { var process = data, remaining = null; if (len < cmp) { process = data.slice(0, len + 6); remaining = data.slice(len + 6, cmp + 6); } this.resetReceive(); this.interpret(new ReadStream(process)); if (remaining) { return remaining; } } } else { var newData = Buffer.concat([this.receiving, data], this.receiving.length + data.length), pduLength = newData.length - 6; if (pduLength < this.receiveLength) { this.receiving = newData; } else { var remaining = null; if (pduLength > this.receiveLength) { remaining = newData.slice(this.receiveLength + 6, pduLength + 6); newData = newData.slice(0, this.receiveLength + 6); } this.resetReceive(); this.interpret(new ReadStream(newData)); if (remaining) { return remaining; } } } return null; }; Connection.prototype.interpret = function(stream) { var pdatas = [], size = stream.size(), o = this; while (stream.offset < size) { var pdu = pduByStream(stream); //console.log("Received PDU-TYPE " + pdu.type); if (pdu.is(C.ITEM_TYPE_PDU_ASSOCIATE_AC)) { pdu.presentationContextItems.forEach(function(ctx) { var requested = o.getContext(ctx.presentationContextID); if (!requested) { throw "Accepted presentation context not found"; } o.negotiatedContexts[ctx.presentationContextID] = { id: ctx.presentationContextID, transferSyntax: ctx.transferSyntaxesItems[0].transferSyntaxName, abstractSyntax: requested.abstractSyntax }; var notfound = false; o.services.forEach(function(service) { if (service.contextUID == requested.abstractSyntax) { service.contextID = ctx.presentationContextID; } }); }); //console.log('Accepted'); this.associated = true; this.emit('associated', pdu); } else if (pdu.is(C.ITEM_TYPE_PDU_RELEASE_RP)) { //console.log('Released'); this.associated = false; this.emit('released'); } else if (pdu.is(C.ITEM_TYPE_PDU_AABORT)) { //console.log('Aborted'); this.emit('aborted'); } else if (pdu.is(C.ITEM_TYPE_PDU_PDATA)) { pdatas.push(pdu); } } if (pdatas) { var pdvs = this.pendingPDVs ? this.pendingPDVs : []; pdatas.forEach(function(pdata) { pdvs = pdvs.concat(pdata.presentationDataValueItems); }); this.pendingPDVs = null; var i = 0, count = pdvs.length; while (i < count) { if (!pdvs[i].isLast) { var j = i + 1; while (j < count) { pdvs[i].messageStream.concat(pdvs[j].messageStream); if (pdvs[j++].isLast) { pdvs[i].isLast = true; break; } } if (pdvs[i].isLast) { this.emit('message', pdvs[i]); } else { this.pendingPDVs = [pdvs[i]]; } i = j; } else { this.emit('message', pdvs[i++]); } } } //this.release(); }; Connection.prototype.newMessageId = function() { return (++this.messageIdCounter) % 255; } Connection.prototype.closed = function(had_error) { this.connected = false; console.log("Connection closed", had_error); //this.destroy(); } Connection.prototype.error = function(err) { console.log("Error: ", err); } Connection.prototype.send = function(pdu, afterCbk) { //console.log('SEND PDU-TYPE: ', pdu.type); var toSend = pdu.buffer(); //console.log('send buffer', toSend.toString('hex')); this.socket.write(toSend, afterCbk ? afterCbk : function() { //console.log('Data written'); }); } Connection.prototype.getSyntax = function(contextId) { if (!this.negotiatedContexts[contextId]) return null; return this.negotiatedContexts[contextId].transferSyntax; } Connection.prototype.getContextByUID = function(uid) { for (var k in this.negotiatedContexts) { var ctx = this.negotiatedContexts[k]; if (ctx.abstractSyntax == uid) { return ctx; } } return null; } Connection.prototype.getContextId = function(contextId) { if (!this.negotiatedContexts[contextId]) return null; return this.negotiatedContexts[contextId].id; } Connection.prototype.getContext = function(id) { for (var k in this.presentationContexts) { var ctx = this.presentationContexts[k]; if (id == ctx.id) return ctx; } return null; } Connection.prototype.setPresentationContexts = function(uids) { var contexts = [], id = 0; uids.forEach(function(uid) { contexts.push({ id: ++id, abstractSyntax: uid, transferSyntaxes: [C.IMPLICIT_LITTLE_ENDIAN, C.EXPLICIT_LITTLE_ENDIAN, C.EXPLICIT_BIG_ENDIAN] }); }); this.presentationContexts = contexts; } Connection.prototype.verify = function() { this.setPresentationContexts([C.SOP_VERIFICATION]); this.startAssociationRequest(function() { //associated, we can release now this.release(); }); } Connection.prototype.release = function() { var releaseRQ = new ReleaseRQ(); this.send(releaseRQ); } Connection.prototype.addService = function(service) { service.setConnection(this); this.services.push(service); } Connection.prototype.receivedMessage = function(pdv) { var syntax = this.getSyntax(pdv.contextId), msg = readMessage(pdv.messageStream, pdv.type, syntax, this.options.vr); if (msg.isCommand()) { this.lastCommand = msg; if (msg.isResponse()) { if (msg.is(C.COMMAND_C_GET_RSP) || msg.is(C.COMMAND_C_MOVE_RSP)) { //console.log('remaining', msg.getNumOfRemainingSubOperations(), msg.getNumOfCompletedSubOperations()); } if (msg.failure()) { //console.log("message failed with status ", msg.getStatus().toString(16)); } if (msg.isFinal()) { var replyId = msg.respondedTo(); if (this.messages[replyId].listener) { this.messages[replyId].listener.emit('end', msg); /*if (this.messages[replyId].listener[1]) { this.messages[replyId].listener[1].call(this, msg); }*/ if (!msg.haveData()) delete this.messages[replyId]; } if (msg.is(C.COMMAND_C_GET_RSP)) { if (!msg.getNumOfRemainingSubOperations()) { if (this.lastGets && this.lastGets.length > 0) this.lastGets.shift(); } } } } else { /*if (msg.is(0x01)) { console.log('ae title ', msg.getValue(0x00001031)) }*/ } } else { if (!this.lastCommand) { throw "Only dataset?"; } else if (!this.lastCommand.haveData()) { throw "Last command didn't indicate presence of data"; } if (this.lastCommand.isResponse()) { var replyId = this.lastCommand.respondedTo(); if (this.messages[replyId].listener) { var flag = this.lastCommand.failure() ? true : false; this.messages[replyId].listener.emit("result", msg, flag); if (this.lastCommand.failure()) { delete this.messages[replyId]; } } } else { if (this.lastCommand.is(C.COMMAND_C_STORE_RQ)) { var moveMessageId = this.lastCommand.getMoveMessageId(), useId = moveMessageId; if (!moveMessageId) { //!! Going to deprecate now //kinda hacky but we know this c-store is came from a c-get if (this.lastGets.length > 0) { useId = this.lastGets[0]; } else { throw "Where does this c-store came from?"; } } else console.log('move ', moveMessageId); //this.storeResponse(useId, msg); } } } } Connection.prototype.storeResponse = function(messageId, msg) { var rq = this.messages[messageId]; if (rq.listener[2]) { var status = rq.listener[2].call(this, msg); if (status !== undefined && status !== null && rq.command.store) { //store ok, ready to send c-store-rsp var storeSr = rq.command.store, replyMessage = storeSr.replyWith(status); replyMessage.setAffectedSOPInstanceUID(this.lastCommand.getSOPInstanceUID()); replyMessage.setReplyMessageId(this.lastCommand.messageId); this.sendMessage(replyMessage, null, null, storeSr); } else { throw "Missing store status"; } } } Connection.prototype.sendMessage = function(context, command, dataset, listener) { var nContext = this.getContextByUID(context), syntax = nContext.transferSyntax, cid = nContext.id, messageId = this.newMessageId(), msgData = {}; /*if (listener) { if (typeof listener != 'object') { listener = [listener, null]; } msgData.listener = listener; }*/ msgData.listener = new Envelope(); var pdata = new PDataTF(), pdv = new PresentationDataValueItem(cid); command.setSyntax(C.IMPLICIT_LITTLE_ENDIAN); command.setContextId(context); command.setMessageId(messageId); if (dataset) command.setDataSetPresent(C.DATA_SET_PRESENT); this.lastSent = command; if (command.is(C.COMMAND_C_GET_RQ)) { this.lastGets.push(messageId); } pdv.setMessage(command); pdata.setPresentationDataValueItems([pdv]); msgData.command = command; this.messages[messageId] = msgData; /*var stream = new ReadStream(pdata.buffer()), np = pduByStream(stream), pdv = np.presentationDataValueItems[0]; var msg = readMessage(pdv.messageStream, pdv.type, C.IMPLICIT_LITTLE_ENDIAN); console.log(msg.isCommand()); return;*/ this.send(pdata); if (dataset) { dataset.setSyntax(syntax); var dsData = new PDataTF(), dPdv = new PresentationDataValueItem(cid); dPdv.setMessage(dataset); dsData.setPresentationDataValueItems([dPdv]); this.send(dsData); } return msgData.listener; }; Connection.prototype.associate = function(options, callback) { if (callback) { this.once('associated', callback); } if (this.associated) { this.emit('associated'); return; } if (options.contexts) { this.setPresentationContexts(options.contexts); } else { throw "No services attached"; } var associateRQ = new AssociateRQ(); associateRQ.setCalledAETitle(options.hostAE); var sourceAE = options.sourceAE ? options.sourceAE : DEFAULT_SOURCE_AE; associateRQ.setCallingAETitle(sourceAE); associateRQ.setApplicationContextItem(new ApplicationContextItem()); var contextItems = [] this.presentationContexts.forEach(function(context) { var contextItem = new PresentationContextItem(), syntaxes = []; context.transferSyntaxes.forEach(function(transferSyntax) { var transfer = new TransferSyntaxItem(); transfer.setTransferSyntaxName(transferSyntax); syntaxes.push(transfer); }); contextItem.setTransferSyntaxesItems(syntaxes); contextItem.setPresentationContextID(context.id); var abstractItem = new AbstractSyntaxItem(); abstractItem.setAbstractSyntaxName(context.abstractSyntax); contextItem.setAbstractSyntaxItem(abstractItem); contextItems.push(contextItem); }); associateRQ.setPresentationContextItems(contextItems); var maxLengthItem = new MaximumLengthItem(), classUIDItem = new ImplementationClassUIDItem(), versionItem = new ImplementationVersionNameItem(); classUIDItem.setImplementationClassUID(C.IMPLEM_UID); versionItem.setImplementationVersionName(C.IMPLEM_VERSION); var packageSize = options.maxPackageSize ? options.maxPackageSize : DEFAULT_MAX_PACKAGE_SIZE; maxLengthItem.setMaximumLengthReceived(packageSize); var userInfo = new UserInformationItem(); userInfo.setUserDataItems([maxLengthItem, classUIDItem, versionItem]); associateRQ.setUserInformationItem(userInfo); this.send(associateRQ); } Connection.prototype.wrapMessage = function(data) { if (data) { var datasetMessage = new DataSetMessage(); datasetMessage.setElements(data); return datasetMessage; } else return data; } Connection.prototype.setFindContext = function(ctx) { this.findContext = ctx; } Connection.prototype.find = function(params, callback) { return this.sendMessage(this.findContext, new CFindRQ(), this.wrapMessage(params), callback); } Connection.prototype.findPatients = function(params, callback) { var sendParams = Object.assign({ 0x00080052: C.QUERY_RETRIEVE_LEVEL_PATIENT, 0x00100010: "", 0x00100020: "", 0x00100030: "", 0x00100040: "", }, params); return this.find(sendParams, callback); } Connection.prototype.findStudies = function(params, callback) { var sendParams = Object.assign({ 0x00080052: C.QUERY_RETRIEVE_LEVEL_STUDY, 0x00080020: "", 0x00100010: "", 0x00080061: "", 0x0020000D: "" }, params); return this.find(sendParams, callback); } Connection.prototype.findSeries = function(params, callback) { var sendParams = Object.assign({ 0x00080052: C.QUERY_RETRIEVE_LEVEL_SERIES, 0x00080020: "", 0x0020000E: "", 0x0008103E: "", 0x0020000D: "" }, params); return this.find(sendParams, callback); } Connection.prototype.findInstances = function(params, callback) { var sendParams = Object.assign({ 0x00080052: C.QUERY_RETRIEVE_LEVEL_IMAGE, 0x00080020: "", 0x0020000E: "", 0x0008103E: "", 0x0020000D: "" }, params); return this.find(sendParams, callback); }