Compare commits

...

10 Commits

Author SHA1 Message Date
PhatPhuckDave
0539fa6621 Update 2024-07-22 19:50:03 +02:00
a9979def99 Fix issues with multiple sessions and shared timer 2023-12-12 09:22:35 +01:00
19ae430779 Fix session tracking and logging in center.js 2023-12-12 09:14:47 +01:00
c873628abc Refactor splitToParts function to use slice
instead of substr
2023-12-11 21:50:18 +01:00
ba0d9bf430 Add tests
Sponsored by codiumai
2023-12-05 14:58:30 +01:00
f8a8365f90 Minor cleanup 2023-12-04 13:01:57 +01:00
06c7f42bf4 Refactor center 2023-12-04 10:17:14 +01:00
927ac2edc9 Add longsms support to client 2023-12-04 09:14:13 +01:00
f7976d3738 Minor refactor 2023-12-04 08:51:46 +01:00
0799dc6a4d Add error message for bind failed 2023-12-04 08:50:55 +01:00
15 changed files with 3117 additions and 918 deletions

View File

@@ -1,26 +1,24 @@
module.exports = {
"env": {
"node": true,
"commonjs": true,
"es2021": true
},
"extends": "eslint:recommended",
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
}
}
env: {
node: true,
commonjs: true,
es2021: true,
"jest/globals": true,
},
extends: "eslint:recommended",
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parserOptions: {
ecmaVersion: "latest",
},
rules: {},
};

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
node_modules
out
.idea
.vscode

10
.vscode/settings.json vendored
View File

@@ -1,10 +0,0 @@
{
"sqltools.connections": [
{
"previewLimit": 50,
"driver": "SQLite",
"name": "1",
"database": "E:\\tmp\\output.db"
}
]
}

View File

@@ -3,7 +3,7 @@ const commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage");
const NanoTimer = require("nanotimer");
const { createBaseLogger, createSessionLogger } = require("./logger");
const { verifyDefaults, verifyExists } = require("./utils");
const { verifyDefaults, verifyExists, sendPdu } = require("./utils");
const { centerOptions } = require("./cliOptions");
const crypto = require("crypto");
const { MetricManager } = require("./metrics/metricManager");
@@ -28,7 +28,6 @@ if (options.help) {
process.exit(0);
}
verifyDefaults(options, centerOptions);
verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger);
verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger);
@@ -41,15 +40,16 @@ let failed = 0;
const sendTimer = new NanoTimer();
const metricManager = new MetricManager(options);
// TODO: Fix issue where a client disconnecting does not stop this timer
// TODO: Fix issue where only one session is being utilized because they all share the same timer
// TODO: Currently bars are broken
// A major rework will need to happen before bars are able to play nice with multiple sessions
// TODO: Maybe add only receiver and only transmitter modes instead of transciever
// Instead just use the same timer but make a pool of connections; That way both problems will be solved
function startInterval(session, sessionLogger, rxMetrics) {
function startInterval(sessions, sessionLogger, rxMetrics) {
if (!options.messagecount > 0) {
sessionLogger.info("No messages to send");
return;
}
let sessionPointer = 0;
sendTimer.setInterval(
async () => {
if (sent >= options.messagecount) {
@@ -57,24 +57,26 @@ function startInterval(session, sessionLogger, rxMetrics) {
sendTimer.clearInterval();
} else if (inFlight < options.window) {
sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`);
session.deliver_sm(
{
source_addr: options.source,
destination_addr: options.destination,
short_message: options.message,
},
function (pdu) {
const pdu = new smpp.PDU("deliver_sm", {
source_addr: options.source,
destination_addr: options.destination,
short_message: options.message,
});
if (sessionPointer >= sessions.length) {
sessionPointer = 0;
}
sendPdu(sessions[sessionPointer++], pdu, sessionLogger, options.longsms)
.then((resp) => {
inFlight--;
if (pdu.command_status === 0) {
sessionLogger.info(`Received response with id ${pdu.message_id}`);
success++;
} else {
sessionLogger.warn(`Message failed with id ${pdu.message_id}`);
failed++;
}
}
);
rxMetrics.AddEvent();
sessionLogger.info(`Received response with id ${resp.message_id}`);
success++;
})
.catch((resp) => {
inFlight--;
sessionLogger.warn(`Message failed with id ${resp.message_id}`);
failed++;
});
sent++;
inFlight++;
} else {
@@ -82,7 +84,7 @@ function startInterval(session, sessionLogger, rxMetrics) {
`${inFlight}/${options.window} messages pending, waiting for a reply before sending more`
);
sendTimer.clearInterval();
setTimeout(() => startInterval(session, sessionLogger), options.windowsleep);
setTimeout(() => startInterval(sessions, sessionLogger), options.windowsleep);
}
},
"",
@@ -93,6 +95,7 @@ function startInterval(session, sessionLogger, rxMetrics) {
logger.info(`Staring server on port ${options.port}...`);
let sessionid = 1;
let messageid = 0;
const sessions = [];
const server = smpp.createServer(
{
debug: options.debug,
@@ -105,9 +108,10 @@ const server = smpp.createServer(
session.on("bind_transceiver", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.info("Client connected");
sessions.push(session);
sessionLogger.info(`Client connected, currently: ${sessions.length}`);
session.send(pdu.response());
startInterval(session, sessionLogger);
startInterval(sessions, sessionLogger);
} else {
sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
@@ -122,7 +126,7 @@ const server = smpp.createServer(
if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.info("Client connected");
session.send(pdu.response());
startInterval(session, sessionLogger);
startInterval(session, sessionLogger, rxMetrics);
} else {
sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
@@ -207,8 +211,13 @@ const server = smpp.createServer(
});
session.on("close", function () {
sessionLogger.warn(`Session closed`);
sessions.splice(sessions.indexOf(session), 1);
sessionLogger.warn(`Session closed, now ${sessions.length}`);
session.close();
if (sessions.length === 0) {
sessionLogger.info("No more sessions, stopping sending timer");
sendTimer.clearInterval();
}
});
session.on("error", function (err) {
sessionLogger.error(`Fatal error ${err}`);

View File

@@ -68,6 +68,11 @@ const clientOptions = [
defaultOption: 1000,
description: "Default max rate for metrics/bars."
},
{
name: "longsms",
type: Boolean,
description: "Split messages into multiple parts. Applies only if message is too big for one packet."
},
];
const centerOptions = [
@@ -151,6 +156,11 @@ const centerOptions = [
defaultOption: 1000,
description: "Default max rate for metrics/bars."
},
{
name: "longsms",
type: Boolean,
description: "Split messages into multiple parts. Applies only if message is too big for one packet."
},
];
module.exports = { clientOptions, centerOptions };

View File

@@ -3,7 +3,7 @@ const commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage");
const NanoTimer = require("nanotimer");
const { createBaseLogger, createSessionLogger } = require("./logger");
const { verifyDefaults, verifyExists } = require("./utils");
const { verifyDefaults, verifyExists, sendPdu } = require("./utils");
const { clientOptions } = require("./cliOptions");
const { MetricManager } = require("./metrics/metricManager");
@@ -58,26 +58,24 @@ function startInterval(session, sessionLogger, metrics) {
metrics.progress.bar.increment();
metrics.window.bar.increment();
}
session.submit_sm(
{
source_addr: options.source,
destination_addr: options.destination,
short_message: options.message,
},
function (pdu) {
if (metrics.window?.bar) {
metrics.window.bar.update(metrics.window.bar.value - 1);
}
const pdu = new smpp.PDU("submit_sm", {
source_addr: options.source,
destination_addr: options.destination,
short_message: options.message,
});
sendPdu(session, pdu, sessionLogger, options.longsms)
.then((resp) => {
inFlight--;
if (pdu.command_status === 0) {
sessionLogger.info(`Received response with id ${pdu.message_id}`);
success++;
} else {
sessionLogger.warn(`Message failed with id ${pdu.message_id}`);
failed++;
}
}
);
sessionLogger.info(`Received response with id ${resp.message_id}`);
success++;
})
.catch((resp) => {
inFlight--;
sessionLogger.warn(`Message failed with id ${resp.message_id}`);
failed++;
});
if (metrics.txMetrics) {
metrics.txMetrics.AddEvent();
}
@@ -109,6 +107,10 @@ for (let i = 0; i < options.sessions; i++) {
sessionLogger.info(
`Connected, sending bind_transciever with systemId '${options.systemid}' and password '${options.password}'...`
);
session.on('close', function () {
sessionLogger.error(`Session closed`);
process.exit(1);
});
session.bind_transceiver(
{
system_id: options.systemid,
@@ -125,7 +127,6 @@ for (let i = 0; i < options.sessions; i++) {
rxMetrics,
txMetrics,
});
// TODO: Add error message for invalid systemid and password
session.on("deliver_sm", function (pdu) {
if (rxMetrics) {
@@ -152,6 +153,9 @@ for (let i = 0; i < options.sessions; i++) {
sessionLogger.error(`Fatal error ${err}`);
process.exit(1);
});
} else {
sessionLogger.error(`Failed to bind, status ${pdu.command_status}`);
process.exit(1);
}
}
);

View File

@@ -8,7 +8,8 @@
"build-windows-client": "nexe -i client.js -o out/client-windows -t windows-x86-18.18.2",
"build-linux-server": "nexe -i server.js -o out/server-linux -t linux-x64-18.18.2",
"build-windows-server": "nexe -i server.js -o out/server-windows -t windows-x86-18.18.2",
"build": "sh build.sh"
"build": "sh build.sh",
"test": "jest"
},
"keywords": [],
"author": "",
@@ -22,6 +23,8 @@
"winston": "^3.11.0"
},
"devDependencies": {
"eslint": "^8.53.0"
"@types/jest": "^29.5.10",
"eslint": "^8.55.0",
"jest": "^29.7.0"
}
}

2040
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

94
tests/utils.test.js Normal file
View File

@@ -0,0 +1,94 @@
const smpp = require("smpp");
const { splitToParts, verifyExists, getCharacterSizeForEncoding } = require("../utils");
describe("splitToParts", () => {
// A pdu is expected to be one part if it has less than 160 characters and is encoded using GSM7 (data_coding = null or 0)
// Given a pdu with short_message length less than 160 chars, it should return an array with a single pdu.
it("should return an array with a single pdu when short_message length is less than or equal to maxMessageSizeBits", () => {
const pdu = new smpp.PDU("deliver_sm", {
short_message: "test message",
});
const result = splitToParts(pdu);
expect(result.length).toBe(1);
});
// Given a pdu with short_message length greater than 160 chars, it should return an array with 2 pdus.
it("should return an array with two pdus when short_message length is greater than maxMessageSizeBits and less than or equal to maxMessageSizeBits * 2", () => {
const pdu = new smpp.PDU("deliver_sm", {
short_message: "c".repeat(200),
});
const result = splitToParts(pdu);
expect(result.length).toBe(2);
});
// Given a pdu with short_message length greater than 320 chars, it should return an array with 2 pdus.
it("should return an array with three pdus when short_message length is greater than maxMessageSizeBits * 2 and less than or equal to maxMessageSizeBits * 3", () => {
const pdu = new smpp.PDU("deliver_sm", {
short_message: "c".repeat(400),
});
const result = splitToParts(pdu);
expect(result.length).toBe(3);
});
// Given a pdu with short_message length equal to 0, it should return an empty array.
it("should return an empty array when short_message length is equal to 0", () => {
const pdu = new smpp.PDU("deliver_sm", {
short_message: "",
});
const result = splitToParts(pdu);
expect(result.length).toBe(0);
});
// Given a pdu with short_message length equal to 320, it should return an array with two pdus.
it("should return an array with two pdus when short_message length is equal to maxMessageSizeBits", () => {
const pdu = new smpp.PDU("deliver_sm", {
short_message: "c".repeat(320),
});
const result = splitToParts(pdu);
expect(result.length).toBe(2);
});
});
describe("getCharacterSizeForEncoding", () => {
// Returns 7 when data_coding is 0
it("should return 7 when data_coding is 0", () => {
const pdu = { data_coding: 0 };
const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(7);
});
// Returns 8 when data_coding is 1
it("should return 8 when data_coding is 1", () => {
const pdu = { data_coding: 1 };
const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(8);
});
// Returns 16 when data_coding is 8
it("should return 16 when data_coding is 8", () => {
const pdu = { data_coding: 8 };
const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(16);
});
// Returns 7 when data_coding is null
it("should return 0 when data_coding is null", () => {
const pdu = { data_coding: null };
const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(7);
});
// Returns 0 when data_coding is not a number
it("should return 0 when data_coding is not a number", () => {
const pdu = { data_coding: "abc" };
const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(0);
});
// Returns 0 when data_coding is negative
it("should return 0 when data_coding is negative", () => {
const pdu = { data_coding: -1 };
const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(0);
});
});

View File

@@ -1,3 +1,5 @@
const smpp = require("smpp");
function verifyExists(value, err, logger) {
if (!value) {
logger.error(err);
@@ -14,4 +16,89 @@ function verifyDefaults(options, definitions) {
}
}
module.exports = { verifyDefaults, verifyExists };
function getCharacterSizeForEncoding(pdu) {
let encoding = pdu.data_coding;
if (!encoding) {
encoding = 0;
}
let characterSizeBits = 0;
switch (encoding) {
case 0:
characterSizeBits = 7;
break;
case 1:
characterSizeBits = 8;
break;
case 8:
characterSizeBits = 16;
break;
}
return characterSizeBits;
}
const maxMessageSizeBits = 1120;
function splitToParts(pdu) {
const charSize = getCharacterSizeForEncoding(pdu);
const maxMessageLength = maxMessageSizeBits / charSize;
const splitMessage = [];
const message = pdu.short_message;
const messageLength = message.length;
const messageCount = (messageLength / maxMessageLength) | 0;
for (let i = 0; i < messageCount; i++) {
splitMessage.push(message.slice(i * maxMessageLength, i * maxMessageLength + maxMessageLength));
}
const pdus = splitMessage.map((messagePart, index) => {
let udh = Buffer.from([0x05, 0x00, 0x03, this.iterator++, messageCount, index + 1]);
let partPdu = new smpp.PDU(pdu.command, { ...pdu });
partPdu.short_message = {
udh: udh,
message: messagePart,
};
return partPdu;
});
return pdus;
}
// TODO: Add "uselongsms" switch to options;
async function sendPdu(session, pdu, logger, uselongsms) {
return new Promise((resolve, reject) => {
if (uselongsms) {
const pdus = splitToParts(pdu);
logger.info(`Sending long sms of ${pdus.length} parts`);
const total = pdus.length;
let success = 0;
let failed = 0;
const promises = pdus.map((pdu) => {
return new Promise((resolve, reject) => {
session.send(pdu, (respPdu) => {
if (respPdu.command_status === 0) {
resolve(respPdu);
} else {
reject(respPdu);
}
});
});
});
Promise.all(promises)
.then((responses) => {
resolve(responses[0]);
})
.catch((error) => {
reject(error);
});
} else {
session.send(pdu, (respPdu) => {
if (respPdu.command_status === 0) {
resolve(respPdu);
} else {
reject(respPdu);
}
});
}
});
}
module.exports = { verifyDefaults, verifyExists, sendPdu, splitToParts, getCharacterSizeForEncoding };