diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 0000000..8f2598f --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,9 @@ +{ + "ts-node": { + "compilerOptions": { + "esModuleInterop": true, + "module": "commonjs", + "target": "es2021" + } + } +} diff --git a/package-lock.json b/package-lock.json index c71cd46..3401a1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dcmjs-dimse", - "version": "0.1.29", + "version": "0.1.30", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "dcmjs-dimse", - "version": "0.1.29", + "version": "0.1.30", "license": "MIT", "dependencies": { "async-eventemitter": "^0.2.4", @@ -17,12 +17,12 @@ }, "devDependencies": { "@types/async-eventemitter": "^0.2.4", - "@types/node": "^22.7.5", + "@types/node": "^22.8.1", "c8": "^9.1.0", "chai": "^4.3.10", "docdash": "^2.0.2", "eslint": "^8.57.0", - "jsdoc": "^4.0.3", + "jsdoc": "^4.0.4", "mocha": "^10.7.3", "mock-fs": "^5.4.0", "nodemon": "^3.1.7", @@ -33,7 +33,7 @@ "terser-webpack-plugin": "^5.3.10", "ts-node": "^10.9.2", "tsd": "^0.31.2", - "typescript": "^5.1.3", + "typescript": "^5.6.3", "webpack": "^5.95.0", "webpack-cli": "^5.1.4" }, @@ -639,12 +639,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "22.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", + "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", "dev": true, "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/node-forge": { @@ -2901,9 +2901,9 @@ } }, "node_modules/jsdoc": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", - "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz", + "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==", "dev": true, "dependencies": { "@babel/parser": "^7.20.15", @@ -6606,12 +6606,12 @@ "dev": true }, "@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "22.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", + "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", "dev": true, "requires": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "@types/node-forge": { @@ -8287,9 +8287,9 @@ } }, "jsdoc": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", - "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz", + "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==", "dev": true, "requires": { "@babel/parser": "^7.20.15", diff --git a/package.json b/package.json index 756ed64..7f87bbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dcmjs-dimse", - "version": "0.1.29", + "version": "0.1.30", "description": "DICOM DIMSE implementation for Node.js using dcmjs", "main": "build/dcmjs-dimse.min.js", "module": "build/dcmjs-dimse.min.js", @@ -51,12 +51,12 @@ }, "devDependencies": { "@types/async-eventemitter": "^0.2.4", - "@types/node": "^22.7.5", + "@types/node": "^22.8.1", "c8": "^9.1.0", "chai": "^4.3.10", "docdash": "^2.0.2", "eslint": "^8.57.0", - "jsdoc": "^4.0.3", + "jsdoc": "^4.0.4", "mocha": "^10.7.3", "mock-fs": "^5.4.0", "nodemon": "^3.1.7", diff --git a/src/Constants.js b/src/Constants.js index 690a2b3..a5660c5 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -1,3 +1,20 @@ +//#region RawPduType +/** + * Raw PDU types. + * @constant {Object} + */ +const RawPduType = { + AAbort: 0x07, + AAssociateAC: 0x02, + AAssociateRJ: 0x03, + AAssociateRQ: 0x01, + AReleaseRP: 0x06, + AReleaseRQ: 0x05, + PDataTF: 0x04, +}; +Object.freeze(RawPduType); +//#endregion + //#region CommandFieldType /** * Command field types. @@ -350,6 +367,7 @@ module.exports = { DefaultImplementation, PresentationContextResult, Priority, + RawPduType, RejectReason, RejectResult, RejectSource, diff --git a/src/Network.js b/src/Network.js index 72d4ae3..27c1ce0 100644 --- a/src/Network.js +++ b/src/Network.js @@ -10,7 +10,12 @@ const { Pdv, RawPdu, } = require('./Pdu'); -const { CommandFieldType, Status, TranscodableTransferSyntaxes } = require('./Constants'); +const { + CommandFieldType, + RawPduType, + Status, + TranscodableTransferSyntaxes, +} = require('./Constants'); const { CCancelRequest, CEchoRequest, @@ -406,7 +411,7 @@ class Network extends AsyncEventEmitter { raw.readPdu(); switch (raw.getType()) { - case 0x01: { + case RawPduType.AAssociateRQ: { this.association = new Association(); const pdu = new AAssociateRQ(this.association); pdu.read(raw); @@ -415,7 +420,7 @@ class Network extends AsyncEventEmitter { this.emit('associationRequested', this.association); break; } - case 0x02: { + case RawPduType.AAssociateAC: { const pdu = new AAssociateAC(this.association); pdu.read(raw); this.logId = this.association.getCalledAeTitle(); @@ -423,7 +428,7 @@ class Network extends AsyncEventEmitter { this.emit('associationAccepted', this.association); break; } - case 0x03: { + case RawPduType.AAssociateRJ: { const pdu = new AAssociateRJ(); pdu.read(raw); log.info(`${this.logId} <- Association reject ${pdu.toString()}`); @@ -434,27 +439,27 @@ class Network extends AsyncEventEmitter { }); break; } - case 0x04: { + case RawPduType.PDataTF: { const pdu = new PDataTF(); pdu.read(raw); this._processPDataTf(pdu); break; } - case 0x05: { + case RawPduType.AReleaseRQ: { const pdu = new AReleaseRQ(); pdu.read(raw); log.info(`${this.logId} <- Association release request`); this.emit('associationReleaseRequested'); break; } - case 0x06: { + case RawPduType.AReleaseRP: { const pdu = new AReleaseRP(); pdu.read(raw); log.info(`${this.logId} <- Association release response`); this.emit('associationReleaseResponse'); break; } - case 0x07: { + case RawPduType.AAbort: { const pdu = new AAbort(); pdu.read(raw); log.info(`${this.logId} <- Association abort ${pdu.toString()}`); @@ -704,6 +709,12 @@ class Network extends AsyncEventEmitter { this.lastPduTime = Date.now(); this._processPdu(data); }); + pduAccumulator.on('error', (err) => { + this._reset(); + const error = `${this.logId} -> Connection error: ${err.message}`; + log.error(error); + this.emit('networkError', new Error(error)); + }); this.socket.on('data', (data) => { pduAccumulator.accumulate(data); this.statistics.addBytesReceived(data.length); @@ -816,7 +827,21 @@ class PduAccumulator extends AsyncEventEmitter { } const buffer = SmartBuffer.fromBuffer(data, 'ascii'); - buffer.readUInt8(); + const pduType = buffer.readUInt8(); + if ( + pduType !== RawPduType.AAssociateRQ && + pduType !== RawPduType.AAssociateAC && + pduType !== RawPduType.AAssociateRJ && + pduType !== RawPduType.PDataTF && + pduType !== RawPduType.AReleaseRQ && + pduType !== RawPduType.AReleaseRP && + pduType !== RawPduType.AAbort + ) { + this.emit('error', new Error(`Unknown PDU type: ${pduType}`)); + + return undefined; + } + buffer.readUInt8(); const pduLength = buffer.readUInt32BE(); let dataNeeded = data.length - 6; diff --git a/src/Pdu.js b/src/Pdu.js index a91820f..5c7bab7 100644 --- a/src/Pdu.js +++ b/src/Pdu.js @@ -1,6 +1,7 @@ const { AbortReason, AbortSource, + RawPduType, RejectReason, RejectResult, RejectSource, @@ -365,7 +366,7 @@ class AAssociateRQ { * @returns {RawPdu} PDU. */ write() { - const pdu = new RawPdu(0x01); + const pdu = new RawPdu(RawPduType.AAssociateRQ); pdu.writeUInt16('Version', 0x0001); pdu.writeByteMultiple('Reserved', 0x00, 2); @@ -608,7 +609,7 @@ class AAssociateAC { * @returns {RawPdu} PDU. */ write() { - const pdu = new RawPdu(0x02); + const pdu = new RawPdu(RawPduType.AAssociateAC); pdu.writeUInt16('Version', 0x0001); pdu.writeByteMultiple('Reserved', 0x00, 2); @@ -864,7 +865,7 @@ class AAssociateRJ { * @returns {RawPdu} PDU. */ write() { - const pdu = new RawPdu(0x03); + const pdu = new RawPdu(RawPduType.AAssociateRJ); pdu.writeByte('Reserved', 0x00); pdu.writeByte('Result', this.result); @@ -1001,7 +1002,7 @@ class AReleaseRQ { * @returns {RawPdu} PDU. */ write() { - const pdu = new RawPdu(0x05); + const pdu = new RawPdu(RawPduType.AReleaseRQ); pdu.writeUInt32('Reserved', 0x00000000); @@ -1041,7 +1042,7 @@ class AReleaseRP { * @returns {RawPdu} PDU buffer. */ write() { - const pdu = new RawPdu(0x06); + const pdu = new RawPdu(RawPduType.AReleaseRP); pdu.writeUInt32('Reserved', 0x00000000); @@ -1104,7 +1105,7 @@ class AAbort { * @returns {RawPdu} PDU. */ write() { - const pdu = new RawPdu(0x07); + const pdu = new RawPdu(RawPduType.AAbort); pdu.writeByte('Reserved', 0x00); pdu.writeByte('Reserved', 0x00); @@ -1356,7 +1357,7 @@ class PDataTF { * @returns {RawPdu} PDU. */ write() { - const pdu = new RawPdu(0x04); + const pdu = new RawPdu(RawPduType.PDataTF); this.pdvs.forEach((pdv) => { pdv.write(pdu); }); diff --git a/src/Server.js b/src/Server.js index a86d2c4..1970f6d 100644 --- a/src/Server.js +++ b/src/Server.js @@ -341,6 +341,10 @@ class Server extends AsyncEventEmitter { client.on('close', () => { this.statistics.addFromOtherStatistics(client.getStatistics()); }); + client.on('networkError', (err) => { + socket.end(); + this.emit('networkError', err); + }); this.clients.push(client); this.clients = this.clients.filter((item) => item.connected); diff --git a/src/index.js b/src/index.js index bafec46..6fca83a 100644 --- a/src/index.js +++ b/src/index.js @@ -31,6 +31,7 @@ const { CommandFieldType, PresentationContextResult, Priority, + RawPduType, RejectReason, RejectResult, RejectSource, @@ -95,6 +96,7 @@ const constants = { CommandFieldType, PresentationContextResult, Priority, + RawPduType, RejectReason, RejectResult, RejectSource, diff --git a/src/version.js b/src/version.js index d7f4f02..a417f7e 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -module.exports = '0.1.29'; +module.exports = '0.1.30'; diff --git a/test/Network.test.js b/test/Network.test.js index e1ab1ea..eaf99e4 100644 --- a/test/Network.test.js +++ b/test/Network.test.js @@ -30,6 +30,7 @@ const Client = require('./../src/Client'); const Dataset = require('./../src/Dataset'); const log = require('./../src/log'); +const http = require('http'); const selfSigned = require('selfsigned'); const chai = require('chai'); const expect = chai.expect; @@ -713,4 +714,19 @@ describe('Network', () => { }); client.send('127.0.0.1', 2112, 'CALLINGAET', 'CALLEDAET'); }); + + it('should reject non-DIMSE communication (HTTP)', () => { + let error = false; + + const server = new Server(AbortingScp); + server.on('networkError', () => { + error = true; + server.close(); + }); + server.listen(2113); + + http.get('http://localhost:2113').on('error', () => { + expect(error).to.be.true; + }); + }); }); diff --git a/test/Pdus.test.js b/test/Pdus.test.js index 24c43ae..53e081c 100644 --- a/test/Pdus.test.js +++ b/test/Pdus.test.js @@ -10,6 +10,7 @@ const { } = require('../src/Pdu'); const { PresentationContextResult, + RawPduType, SopClass, TransferSyntax, UserIdentityType, @@ -149,7 +150,7 @@ describe('PDU', () => { const command = false; const last = true; - const pdu = new RawPdu(0x01); + const pdu = new RawPdu(RawPduType.AAssociateRQ); const pdv1 = new Pdv(pcId, data, command, last); pdv1.write(pdu);