Compare commits
10 Commits
9326923b72
...
0539fa6621
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0539fa6621 | ||
a9979def99 | |||
19ae430779 | |||
c873628abc | |||
ba0d9bf430 | |||
f8a8365f90 | |||
06c7f42bf4 | |||
927ac2edc9 | |||
f7976d3738 | |||
0799dc6a4d |
48
.eslintrc.js
48
.eslintrc.js
@@ -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
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
node_modules
|
||||
out
|
||||
.idea
|
||||
.vscode
|
||||
|
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"previewLimit": 50,
|
||||
"driver": "SQLite",
|
||||
"name": "1",
|
||||
"database": "E:\\tmp\\output.db"
|
||||
}
|
||||
]
|
||||
}
|
63
center.js
63
center.js
@@ -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}`);
|
||||
|
@@ -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 };
|
||||
|
46
client.js
46
client.js
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -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
2040
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
94
tests/utils.test.js
Normal file
94
tests/utils.test.js
Normal 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);
|
||||
});
|
||||
});
|
89
utils.js
89
utils.js
@@ -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 };
|
||||
|
Reference in New Issue
Block a user