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 = { module.exports = {
"env": { env: {
"node": true, node: true,
"commonjs": true, commonjs: true,
"es2021": true es2021: true,
}, "jest/globals": true,
"extends": "eslint:recommended", },
"overrides": [ extends: "eslint:recommended",
{ overrides: [
"env": { {
"node": true env: {
}, node: true,
"files": [ },
".eslintrc.{js,cjs}" files: [".eslintrc.{js,cjs}"],
], parserOptions: {
"parserOptions": { sourceType: "script",
"sourceType": "script" },
} },
} ],
], parserOptions: {
"parserOptions": { ecmaVersion: "latest",
"ecmaVersion": "latest" },
}, rules: {},
"rules": { };
}
}

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
node_modules node_modules
out 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"
}
]
}

224
README.md
View File

@@ -1,112 +1,112 @@
# SMPP_CLI # SMPP_CLI
### A small application used to create SMPP clients or centers. Serves as both the SMSC and ESME. ### A small application used to create SMPP clients or centers. Serves as both the SMSC and ESME.
(Taken from the help page) (Taken from the help page)
--- ---
``` ```
CLI SMPP (Client) CLI SMPP (Client)
Options Options
--help Display this usage guide. --help Display this usage guide.
-h, --host string The host (IP) to connect to. -h, --host string The host (IP) to connect to.
-p, --port number The port to connect to. -p, --port number The port to connect to.
-s, --systemid string SMPP related login info. -s, --systemid string SMPP related login info.
-w, --password string SMPP related login info. -w, --password string SMPP related login info.
--sessions number Number of sessions to start, defaults to 1. --sessions number Number of sessions to start, defaults to 1.
--messagecount number Number of messages to send; Optional, defaults to 1. --messagecount number Number of messages to send; Optional, defaults to 1.
--window number Defines the amount of messages that are allowed to be --window number Defines the amount of messages that are allowed to be
'in flight'. The client no longer waits for a 'in flight'. The client no longer waits for a
response before sending the next message for up to response before sending the next message for up to
<window> messages. Defaults to 100. <window> messages. Defaults to 100.
--windowsleep number Defines the amount time (in ms) waited between --windowsleep number Defines the amount time (in ms) waited between
retrying in the case of full window. Defaults to 100. retrying in the case of full window. Defaults to 100.
--mps number Number of messages to send per second --mps number Number of messages to send per second
--source string Source field of the sent messages. --source string Source field of the sent messages.
--destination string Destination field of the sent messages. --destination string Destination field of the sent messages.
--message string Text content of the sent messages. --message string Text content of the sent messages.
--debug Display all traffic to and from the client; Debug --debug Display all traffic to and from the client; Debug
mode. mode.
``` ```
--- ---
``` ```
CLI SMPP (Center) CLI SMPP (Center)
Options Options
--help Display this usage guide. --help Display this usage guide.
-p, --port number The port to connect to. -p, --port number The port to connect to.
-s, --systemid string SMPP related login info. -s, --systemid string SMPP related login info.
-w, --password string SMPP related login info. -w, --password string SMPP related login info.
--dr Whether or not to send Delivery Reports. --dr Whether or not to send Delivery Reports.
--sessions number Maximum number of client sessions to accept, defaults --sessions number Maximum number of client sessions to accept, defaults
to 8. to 8.
--messagecount number Number of messages to send; Optional, defaults to 0. --messagecount number Number of messages to send; Optional, defaults to 0.
--window number Defines the amount of messages that are allowed to be --window number Defines the amount of messages that are allowed to be
'in flight'. The client no longer waits for a 'in flight'. The client no longer waits for a
response before sending the next message for up to response before sending the next message for up to
<window> messages. Defaults to 100. <window> messages. Defaults to 100.
--windowsleep number Defines the amount time (in ms) waited between --windowsleep number Defines the amount time (in ms) waited between
retrying in the case of full window. Defaults to 100. retrying in the case of full window. Defaults to 100.
--mps number Number of messages to send per second --mps number Number of messages to send per second
--source string Source field of the sent messages. --source string Source field of the sent messages.
--destination string Destination field of the sent messages. --destination string Destination field of the sent messages.
--message string Text content of the sent messages. --message string Text content of the sent messages.
--debug Display all traffic to and from the center; Debug --debug Display all traffic to and from the center; Debug
mode. mode.
``` ```
--- ---
#### Center example usage: #### Center example usage:
``` ```
./center-win.exe \ ./center-win.exe \
--port 7001 \ --port 7001 \
--systemid test \ --systemid test \
--password test \ --password test \
--sessions 10 --sessions 10
``` ```
Running this command will spawn an SMPP center (SMSC) which will: Running this command will spawn an SMPP center (SMSC) which will:
- Start listening on 7001 - Start listening on 7001
- Accept clients with test:test credentials - Accept clients with test:test credentials
- Allow up to a maximum of 10 sessions - Allow up to a maximum of 10 sessions
#### Client example usage: #### Client example usage:
``` ```
./client-win.exe \ ./client-win.exe \
--host localhost \ --host localhost \
--port 7001 \ --port 7001 \
--systemid test \ --systemid test \
--password test \ --password test \
--window 5 \ --window 5 \
--windowsleep 100 \ --windowsleep 100 \
--messagecount 10000 \ --messagecount 10000 \
--mps 10 \ --mps 10 \
--sessions 10 --sessions 10
``` ```
Running this command will spawn an SMPP client (ESME) which will: Running this command will spawn an SMPP client (ESME) which will:
- Try to connect 10 sessions to localhost:7001 - Try to connect 10 sessions to localhost:7001
- Once connected try to bind using test:test - Once connected try to bind using test:test
- Once bound send 10000 messages at a rate of 10 per second and a window size of 5 - Once bound send 10000 messages at a rate of 10 per second and a window size of 5
- If the window is filled during sending the session will wait 100ms before attempting send again - If the window is filled during sending the session will wait 100ms before attempting send again
--- ---
--- ---
#### Center example usage (sending): #### Center example usage (sending):
``` ```
./center-win.exe \ ./center-win.exe \
--port 7001 \ --port 7001 \
--systemid test \ --systemid test \
--password test \ --password test \
--window 5 \ --window 5 \
--windowsleep 100 \ --windowsleep 100 \
--messagecount 10000 \ --messagecount 10000 \
--mps 10 \ --mps 10 \
--sessions 10 --sessions 10
``` ```
Running this command will spawn an SMPP center (SMSC) which will: Running this command will spawn an SMPP center (SMSC) which will:
- Start listening on 7001 - Start listening on 7001
- Accept clients with test:test credentials - Accept clients with test:test credentials
- Allow up to a maximum of 10 sessions - Allow up to a maximum of 10 sessions
- Once a client is connected start sending 10000 messages at a rate of 10 per second with a window size of 5 - Once a client is connected start sending 10000 messages at a rate of 10 per second with a window size of 5
- If the window is filled during sending the session will wait 100ms before attempting send again - If the window is filled during sending the session will wait 100ms before attempting send again
--- ---

463
center.js
View File

@@ -1,227 +1,236 @@
const smpp = require("smpp"); const smpp = require("smpp");
const commandLineArgs = require("command-line-args"); const commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage"); const commandLineUsage = require("command-line-usage");
const NanoTimer = require("nanotimer"); const NanoTimer = require("nanotimer");
const { createBaseLogger, createSessionLogger } = require("./logger"); const { createBaseLogger, createSessionLogger } = require("./logger");
const { verifyDefaults, verifyExists } = require("./utils"); const { verifyDefaults, verifyExists, sendPdu } = require("./utils");
const { centerOptions } = require("./cliOptions"); const { centerOptions } = require("./cliOptions");
const crypto = require("crypto"); const crypto = require("crypto");
const { MetricManager } = require("./metrics/metricManager"); const { MetricManager } = require("./metrics/metricManager");
const options = commandLineArgs(centerOptions); const options = commandLineArgs(centerOptions);
const logger = createBaseLogger(options); const logger = createBaseLogger(options);
if (options.help) { if (options.help) {
const usage = commandLineUsage([ const usage = commandLineUsage([
{ {
header: "CLI SMPP (Center)", header: "CLI SMPP (Center)",
}, },
{ {
header: "Options", header: "Options",
optionList: centerOptions, optionList: centerOptions,
}, },
{ {
content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}", content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}",
}, },
]); ]);
console.log(usage); console.log(usage);
process.exit(0); process.exit(0);
} }
verifyDefaults(options, centerOptions);
verifyDefaults(options, centerOptions); verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger);
verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger); verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger);
verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger); verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger);
verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger);
let inFlight = 0;
let inFlight = 0; let sent = 0;
let sent = 0; let success = 0;
let success = 0; let failed = 0;
let failed = 0; const sendTimer = new NanoTimer();
const sendTimer = new NanoTimer(); const metricManager = new MetricManager(options);
const metricManager = new MetricManager(options);
// TODO: Currently bars are broken
// TODO: Fix issue where a client disconnecting does not stop this timer // A major rework will need to happen before bars are able to play nice with multiple sessions
// TODO: Fix issue where only one session is being utilized because they all share the same timer // TODO: Maybe add only receiver and only transmitter modes instead of transciever
// 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
// Instead just use the same timer but make a pool of connections; That way both problems will be solved function startInterval(sessions, sessionLogger, rxMetrics) {
function startInterval(session, sessionLogger, rxMetrics) { if (!options.messagecount > 0) {
if (!options.messagecount > 0) { sessionLogger.info("No messages to send");
sessionLogger.info("No messages to send"); return;
return; }
} let sessionPointer = 0;
sendTimer.setInterval( sendTimer.setInterval(
async () => { async () => {
if (sent >= options.messagecount) { if (sent >= options.messagecount) {
sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`); sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`);
sendTimer.clearInterval(); sendTimer.clearInterval();
} else if (inFlight < options.window) { } else if (inFlight < options.window) {
sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`); sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`);
session.deliver_sm( const pdu = new smpp.PDU("deliver_sm", {
{ source_addr: options.source,
source_addr: options.source, destination_addr: options.destination,
destination_addr: options.destination, short_message: options.message,
short_message: options.message, });
},
function (pdu) { if (sessionPointer >= sessions.length) {
inFlight--; sessionPointer = 0;
if (pdu.command_status === 0) { }
sessionLogger.info(`Received response with id ${pdu.message_id}`); sendPdu(sessions[sessionPointer++], pdu, sessionLogger, options.longsms)
success++; .then((resp) => {
} else { inFlight--;
sessionLogger.warn(`Message failed with id ${pdu.message_id}`); sessionLogger.info(`Received response with id ${resp.message_id}`);
failed++; success++;
} })
} .catch((resp) => {
); inFlight--;
rxMetrics.AddEvent(); sessionLogger.warn(`Message failed with id ${resp.message_id}`);
sent++; failed++;
inFlight++; });
} else { sent++;
sessionLogger.warn( inFlight++;
`${inFlight}/${options.window} messages pending, waiting for a reply before sending more` } else {
); sessionLogger.warn(
sendTimer.clearInterval(); `${inFlight}/${options.window} messages pending, waiting for a reply before sending more`
setTimeout(() => startInterval(session, sessionLogger), options.windowsleep); );
} sendTimer.clearInterval();
}, setTimeout(() => startInterval(sessions, sessionLogger), options.windowsleep);
"", }
`${1 / options.mps} s` },
); "",
} `${1 / options.mps} s`
);
logger.info(`Staring server on port ${options.port}...`); }
let sessionid = 1;
let messageid = 0; logger.info(`Staring server on port ${options.port}...`);
const server = smpp.createServer( let sessionid = 1;
{ let messageid = 0;
debug: options.debug, const sessions = [];
}, const server = smpp.createServer(
function (session) { {
const id = sessionid++; debug: options.debug,
const sessionLogger = createSessionLogger(options, id); },
const rxMetrics = metricManager.AddMetrics(`Session-${id}-RX`); function (session) {
const txMetrics = metricManager.AddMetrics(`Session-${id}-TX`); const id = sessionid++;
const sessionLogger = createSessionLogger(options, id);
session.on("bind_transceiver", function (pdu) { const rxMetrics = metricManager.AddMetrics(`Session-${id}-RX`);
if (pdu.system_id === options.systemid && pdu.password === options.password) { const txMetrics = metricManager.AddMetrics(`Session-${id}-TX`);
sessionLogger.info("Client connected");
session.send(pdu.response()); session.on("bind_transceiver", function (pdu) {
startInterval(session, sessionLogger); if (pdu.system_id === options.systemid && pdu.password === options.password) {
} else { sessions.push(session);
sessionLogger.warn( sessionLogger.info(`Client connected, currently: ${sessions.length}`);
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` session.send(pdu.response());
); startInterval(sessions, sessionLogger);
pdu.response({ } else {
command_status: smpp.ESME_RBINDFAIL, sessionLogger.warn(
}); `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
session.close(); );
} pdu.response({
}); command_status: smpp.ESME_RBINDFAIL,
session.on("bind_transmitter", function (pdu) { });
if (pdu.system_id === options.systemid && pdu.password === options.password) { session.close();
sessionLogger.info("Client connected"); }
session.send(pdu.response()); });
startInterval(session, sessionLogger); session.on("bind_transmitter", function (pdu) {
} else { if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.warn( sessionLogger.info("Client connected");
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` session.send(pdu.response());
); startInterval(session, sessionLogger, rxMetrics);
pdu.response({ } else {
command_status: smpp.ESME_RBINDFAIL, sessionLogger.warn(
}); `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
session.close(); );
} pdu.response({
}); command_status: smpp.ESME_RBINDFAIL,
session.on("bind_receiver", function (pdu) { });
if (pdu.system_id === options.systemid && pdu.password === options.password) { session.close();
sessionLogger.info("Client connected"); }
session.send(pdu.response()); });
startInterval(session, sessionLogger); session.on("bind_receiver", function (pdu) {
} else { if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.warn( sessionLogger.info("Client connected");
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` session.send(pdu.response());
); startInterval(session, sessionLogger);
pdu.response({ } else {
command_status: smpp.ESME_RBINDFAIL, sessionLogger.warn(
}); `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
session.close(); );
} pdu.response({
}); command_status: smpp.ESME_RBINDFAIL,
session.on("enquire_link", function (pdu) { });
session.send(pdu.response()); session.close();
}); }
session.on("submit_sm", async function (pdu) { });
if (!options.dr) { session.on("enquire_link", function (pdu) {
sessionLogger.info("Replying to incoming submit_sm"); session.send(pdu.response());
if (options.bars) { });
rxMetrics.AddEvent(); session.on("submit_sm", async function (pdu) {
} if (!options.dr) {
// setTimeout(() => { sessionLogger.info("Replying to incoming submit_sm");
// session.send(pdu.response()); if (options.bars) {
// }, 200); rxMetrics.AddEvent();
session.send(pdu.response()); }
return; // setTimeout(() => {
} // session.send(pdu.response());
// }, 200);
sessionLogger.info("Generating DR for incoming submit_sm"); session.send(pdu.response());
let response = pdu.response(); return;
}
let smppid = messageid++;
if (options.randid) { sessionLogger.info("Generating DR for incoming submit_sm");
smppid = crypto.randomBytes(8).toString("hex"); let response = pdu.response();
}
let smppid = messageid++;
response.message_id = smppid.toString(16); if (options.randid) {
session.send(response); smppid = crypto.randomBytes(8).toString("hex");
}
let drMessage = "";
let date = new Date() response.message_id = smppid.toString(16);
.toISOString() session.send(response);
.replace(/T/, "")
.replace(/\..+/, "") let drMessage = "";
.replace(/-/g, "") let date = new Date()
.replace(/:/g, "") .toISOString()
.substring(2, 12); .replace(/T/, "")
.replace(/\..+/, "")
drMessage += "id:" + response.message_id + " "; .replace(/-/g, "")
drMessage += "sub:001 "; .replace(/:/g, "")
drMessage += "dlvrd:001 "; .substring(2, 12);
drMessage += "submit date:" + date + " ";
drMessage += "done date:" + date + " "; drMessage += "id:" + response.message_id + " ";
drMessage += "stat:DELIVRD "; drMessage += "sub:001 ";
drMessage += "err:000 "; drMessage += "dlvrd:001 ";
drMessage += "text:"; drMessage += "submit date:" + date + " ";
drMessage += "done date:" + date + " ";
const DRPdu = { drMessage += "stat:DELIVRD ";
source_addr: pdu.destination_addr, drMessage += "err:000 ";
destination_addr: pdu.source_addr, drMessage += "text:";
short_message: drMessage,
esm_class: 4, const DRPdu = {
}; source_addr: pdu.destination_addr,
sessionLogger.info(`Generated DR as ${drMessage}`); destination_addr: pdu.source_addr,
session.deliver_sm(DRPdu); short_message: drMessage,
if (txMetrics) { esm_class: 4,
txMetrics.AddEvent(); };
} sessionLogger.info(`Generated DR as ${drMessage}`);
}); session.deliver_sm(DRPdu);
if (txMetrics) {
session.on("close", function () { txMetrics.AddEvent();
sessionLogger.warn(`Session closed`); }
session.close(); });
});
session.on("error", function (err) { session.on("close", function () {
sessionLogger.error(`Fatal error ${err}`); sessions.splice(sessions.indexOf(session), 1);
session.close(); sessionLogger.warn(`Session closed, now ${sessions.length}`);
}); session.close();
} if (sessions.length === 0) {
); sessionLogger.info("No more sessions, stopping sending timer");
sendTimer.clearInterval();
server.on("error", function (err) { }
logger.error(`Fatal server error ${err}`); });
server.close(); session.on("error", function (err) {
process.exit(1); sessionLogger.error(`Fatal error ${err}`);
}); session.close();
});
server.listen(options.port); }
logger.info(`SMPP Server listening on ${options.port}`); );
server.on("error", function (err) {
logger.error(`Fatal server error ${err}`);
server.close();
process.exit(1);
});
server.listen(options.port);
logger.info(`SMPP Server listening on ${options.port}`);

View File

@@ -1,156 +1,166 @@
const clientOptions = [ const clientOptions = [
{ name: "help", type: Boolean, description: "Display this usage guide." }, { name: "help", type: Boolean, description: "Display this usage guide." },
{ name: "host", alias: "h", type: String, description: "The host (IP) to connect to." }, { name: "host", alias: "h", type: String, description: "The host (IP) to connect to." },
{ name: "port", alias: "p", type: Number, description: "The port to connect to." }, { name: "port", alias: "p", type: Number, description: "The port to connect to." },
{ name: "systemid", alias: "s", type: String, description: "SMPP related login info." }, { name: "systemid", alias: "s", type: String, description: "SMPP related login info." },
{ name: "password", alias: "w", type: String, description: "SMPP related login info." }, { name: "password", alias: "w", type: String, description: "SMPP related login info." },
{ name: "sessions", type: Number, description: "Number of sessions to start, defaults to 1.", defaultOption: 1 }, { name: "sessions", type: Number, description: "Number of sessions to start, defaults to 1.", defaultOption: 1 },
{ {
name: "messagecount", name: "messagecount",
type: Number, type: Number,
description: "Number of messages to send; Optional, defaults to 1.", description: "Number of messages to send; Optional, defaults to 1.",
defaultOption: 1, defaultOption: 1,
}, },
{ {
name: "window", name: "window",
type: Number, type: Number,
description: description:
"Defines the amount of messages that are allowed to be 'in flight'. The client no longer waits for a response before sending the next message for up to <window> messages. Defaults to 100.", "Defines the amount of messages that are allowed to be 'in flight'. The client no longer waits for a response before sending the next message for up to <window> messages. Defaults to 100.",
defaultOption: 100, defaultOption: 100,
}, },
{ {
name: "windowsleep", name: "windowsleep",
type: Number, type: Number,
description: description:
"Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.", "Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.",
defaultOption: 100, defaultOption: 100,
}, },
{ {
name: "mps", name: "mps",
type: Number, type: Number,
description: "Number of messages to send per second", description: "Number of messages to send per second",
defaultOption: 999999, defaultOption: 999999,
}, },
{ {
name: "source", name: "source",
type: String, type: String,
description: "Source field of the sent messages.", description: "Source field of the sent messages.",
defaultOption: "smppDebugClient", defaultOption: "smppDebugClient",
}, },
{ {
name: "destination", name: "destination",
type: String, type: String,
description: "Destination field of the sent messages.", description: "Destination field of the sent messages.",
defaultOption: "smpp", defaultOption: "smpp",
}, },
{ {
name: "message", name: "message",
type: String, type: String,
description: "Text content of the sent messages.", description: "Text content of the sent messages.",
defaultOption: "smpp debug message", defaultOption: "smpp debug message",
}, },
{ name: "debug", type: Boolean, description: "Display all traffic to and from the client; Debug mode." }, { name: "debug", type: Boolean, description: "Display all traffic to and from the client; Debug mode." },
{ name: "logs", type: Boolean, description: "Write logs (to stdout), defaults to true." }, { name: "logs", type: Boolean, description: "Write logs (to stdout), defaults to true." },
{ {
name: "bars", name: "bars",
type: Boolean, type: Boolean,
description: "Display TX and RX bars. Can be used with logs (although it will make a mess)." description: "Display TX and RX bars. Can be used with logs (although it will make a mess)."
}, },
{ {
name: "metricsinterval", name: "metricsinterval",
type: Number, type: Number,
defaultOption: 5, defaultOption: 5,
description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5." description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5."
}, },
{ {
name: "defaultmaxrate", name: "defaultmaxrate",
type: Number, type: Number,
defaultOption: 1000, defaultOption: 1000,
description: "Default max rate for metrics/bars." description: "Default max rate for metrics/bars."
}, },
]; {
name: "longsms",
const centerOptions = [ type: Boolean,
{ name: "help", type: Boolean, description: "Display this usage guide." }, description: "Split messages into multiple parts. Applies only if message is too big for one packet."
{ name: "port", alias: "p", type: Number, description: "The port to connect to." }, },
{ name: "systemid", alias: "s", type: String, description: "SMPP related login info." }, ];
{ name: "password", alias: "w", type: String, description: "SMPP related login info." },
{ name: "dr", type: Boolean, description: "Whether or not to send Delivery Reports.", defaultOption: false }, const centerOptions = [
{ { name: "help", type: Boolean, description: "Display this usage guide." },
name: "randid", { name: "port", alias: "p", type: Number, description: "The port to connect to." },
type: Boolean, { name: "systemid", alias: "s", type: String, description: "SMPP related login info." },
description: "SMPP ID generation is entirely random instead of sequential.", { name: "password", alias: "w", type: String, description: "SMPP related login info." },
defaultOption: false, { name: "dr", type: Boolean, description: "Whether or not to send Delivery Reports.", defaultOption: false },
}, {
{ name: "randid",
name: "sessions", type: Boolean,
type: Number, description: "SMPP ID generation is entirely random instead of sequential.",
description: "Maximum number of client sessions to accept, defaults to 8.", defaultOption: false,
defaultOption: 8, },
}, {
{ name: "sessions",
name: "messagecount", type: Number,
type: Number, description: "Maximum number of client sessions to accept, defaults to 8.",
description: "Number of messages to send; Optional, defaults to 0.", defaultOption: 8,
defaultOption: 0, },
}, {
{ name: "messagecount",
name: "window", type: Number,
type: Number, description: "Number of messages to send; Optional, defaults to 0.",
description: defaultOption: 0,
"Defines the amount of messages that are allowed to be 'in flight'. The client no longer waits for a response before sending the next message for up to <window> messages. Defaults to 100.", },
defaultOption: 100, {
}, name: "window",
{ type: Number,
name: "windowsleep", description:
type: Number, "Defines the amount of messages that are allowed to be 'in flight'. The client no longer waits for a response before sending the next message for up to <window> messages. Defaults to 100.",
description: defaultOption: 100,
"Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.", },
defaultOption: 100, {
}, name: "windowsleep",
{ type: Number,
name: "mps", description:
type: Number, "Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.",
description: "Number of messages to send per second", defaultOption: 100,
defaultOption: 999999, },
}, {
{ name: "mps",
name: "source", type: Number,
type: String, description: "Number of messages to send per second",
description: "Source field of the sent messages.", defaultOption: 999999,
defaultOption: "smppDebugClient", },
}, {
{ name: "source",
name: "destination", type: String,
type: String, description: "Source field of the sent messages.",
description: "Destination field of the sent messages.", defaultOption: "smppDebugClient",
defaultOption: "smpp", },
}, {
{ name: "destination",
name: "message", type: String,
type: String, description: "Destination field of the sent messages.",
description: "Text content of the sent messages.", defaultOption: "smpp",
defaultOption: "smpp debug message", },
}, {
{ name: "debug", type: Boolean, description: "Display all traffic to and from the center; Debug mode." }, name: "message",
{ name: "logs", type: Boolean, description: "Write logs (to stdout), defaults to true." }, type: String,
{ description: "Text content of the sent messages.",
name: "bars", defaultOption: "smpp debug message",
type: Boolean, },
description: "Display TX and RX bars. Can be used with logs (although it will make a mess)." { name: "debug", type: Boolean, description: "Display all traffic to and from the center; Debug mode." },
}, { name: "logs", type: Boolean, description: "Write logs (to stdout), defaults to true." },
{ {
name: "metricsinterval", name: "bars",
type: Number, type: Boolean,
defaultOption: 5, description: "Display TX and RX bars. Can be used with logs (although it will make a mess)."
description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5." },
}, {
{ name: "metricsinterval",
name: "defaultmaxrate", type: Number,
type: Number, defaultOption: 5,
defaultOption: 1000, description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5."
description: "Default max rate for metrics/bars." },
}, {
]; name: "defaultmaxrate",
type: Number,
module.exports = { clientOptions, 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 };

324
client.js
View File

@@ -1,160 +1,164 @@
const smpp = require("smpp"); const smpp = require("smpp");
const commandLineArgs = require("command-line-args"); const commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage"); const commandLineUsage = require("command-line-usage");
const NanoTimer = require("nanotimer"); const NanoTimer = require("nanotimer");
const { createBaseLogger, createSessionLogger } = require("./logger"); const { createBaseLogger, createSessionLogger } = require("./logger");
const { verifyDefaults, verifyExists } = require("./utils"); const { verifyDefaults, verifyExists, sendPdu } = require("./utils");
const { clientOptions } = require("./cliOptions"); const { clientOptions } = require("./cliOptions");
const { MetricManager } = require("./metrics/metricManager"); const { MetricManager } = require("./metrics/metricManager");
const options = commandLineArgs(clientOptions); const options = commandLineArgs(clientOptions);
const logger = createBaseLogger(options); const logger = createBaseLogger(options);
if (options.help) { if (options.help) {
const usage = commandLineUsage([ const usage = commandLineUsage([
{ {
header: "CLI SMPP (Client)", header: "CLI SMPP (Client)",
}, },
{ {
header: "Options", header: "Options",
optionList: clientOptions, optionList: clientOptions,
}, },
{ {
content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}", content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}",
}, },
]); ]);
console.log(usage); console.log(usage);
process.exit(0); process.exit(0);
} }
verifyDefaults(options, clientOptions); verifyDefaults(options, clientOptions);
verifyExists(options.host, "Host can not be undefined or empty! (--host)", logger); verifyExists(options.host, "Host can not be undefined or empty! (--host)", logger);
verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger); verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger);
verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger); verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger);
verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger); verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger);
let inFlight = 0; let inFlight = 0;
let sent = 0; let sent = 0;
let success = 0; let success = 0;
let failed = 0; let failed = 0;
const sendTimer = new NanoTimer(); const sendTimer = new NanoTimer();
const metricManager = new MetricManager(options); const metricManager = new MetricManager(options);
function startInterval(session, sessionLogger, metrics) { function startInterval(session, sessionLogger, metrics) {
if (!metrics.progress && options.bars === true) { if (!metrics.progress && options.bars === true) {
metrics.progress = metricManager.AddMetrics("Send progress", false); metrics.progress = metricManager.AddMetrics("Send progress", false);
metrics.progress.bar.total = options.messagecount; metrics.progress.bar.total = options.messagecount;
metrics.window = metricManager.AddMetrics("Send window", false); metrics.window = metricManager.AddMetrics("Send window", false);
metrics.window.bar.total = options.window; metrics.window.bar.total = options.window;
} }
sendTimer.setInterval( sendTimer.setInterval(
async () => { async () => {
if (sent >= options.messagecount) { if (sent >= options.messagecount) {
sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`); sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`);
sendTimer.clearInterval(); sendTimer.clearInterval();
} else if (inFlight < options.window) { } else if (inFlight < options.window) {
sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`); sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`);
if (options.bars) { if (options.bars) {
metrics.progress.bar.increment(); metrics.progress.bar.increment();
metrics.window.bar.increment(); metrics.window.bar.increment();
} }
session.submit_sm( const pdu = new smpp.PDU("submit_sm", {
{ source_addr: options.source,
source_addr: options.source, destination_addr: options.destination,
destination_addr: options.destination, short_message: options.message,
short_message: options.message, });
},
function (pdu) { sendPdu(session, pdu, sessionLogger, options.longsms)
if (metrics.window?.bar) { .then((resp) => {
metrics.window.bar.update(metrics.window.bar.value - 1); inFlight--;
} sessionLogger.info(`Received response with id ${resp.message_id}`);
inFlight--; success++;
if (pdu.command_status === 0) { })
sessionLogger.info(`Received response with id ${pdu.message_id}`); .catch((resp) => {
success++; inFlight--;
} else { sessionLogger.warn(`Message failed with id ${resp.message_id}`);
sessionLogger.warn(`Message failed with id ${pdu.message_id}`); failed++;
failed++; });
}
} if (metrics.txMetrics) {
); metrics.txMetrics.AddEvent();
if (metrics.txMetrics) { }
metrics.txMetrics.AddEvent(); sent++;
} inFlight++;
sent++; } else {
inFlight++; sessionLogger.warn(
} else { `${inFlight}/${options.window} messages pending, waiting for a reply before sending more`
sessionLogger.warn( );
`${inFlight}/${options.window} messages pending, waiting for a reply before sending more` sendTimer.clearInterval();
); setTimeout(() => startInterval(session, sessionLogger, metrics), options.windowsleep);
sendTimer.clearInterval(); }
setTimeout(() => startInterval(session, sessionLogger, metrics), options.windowsleep); },
} "",
}, `${1 / options.mps} s`
"", );
`${1 / options.mps} s` }
);
} for (let i = 0; i < options.sessions; i++) {
const sessionLogger = createSessionLogger(options, i);
for (let i = 0; i < options.sessions; i++) { sessionLogger.info(`Connecting to ${options.host}:${options.port}...`);
const sessionLogger = createSessionLogger(options, i); const session = smpp.connect(
sessionLogger.info(`Connecting to ${options.host}:${options.port}...`); {
const session = smpp.connect( url: `smpp://${options.host}:${options.port}`,
{ auto_enquire_link_period: 10000,
url: `smpp://${options.host}:${options.port}`, debug: options.debug,
auto_enquire_link_period: 10000, },
debug: options.debug, function () {
}, sessionLogger.info(
function () { `Connected, sending bind_transciever with systemId '${options.systemid}' and password '${options.password}'...`
sessionLogger.info( );
`Connected, sending bind_transciever with systemId '${options.systemid}' and password '${options.password}'...` session.on('close', function () {
); sessionLogger.error(`Session closed`);
session.bind_transceiver( process.exit(1);
{ });
system_id: options.systemid, session.bind_transceiver(
password: options.password, {
}, system_id: options.systemid,
function (pdu) { password: options.password,
if (pdu.command_status === 0) { },
sessionLogger.info( function (pdu) {
`Successfully bound, sending ${options.messagecount} messages '${options.source}'->'${options.destination}' ('${options.message}')` if (pdu.command_status === 0) {
); sessionLogger.info(
const rxMetrics = metricManager.AddMetrics(`Session-${i}-RX`); `Successfully bound, sending ${options.messagecount} messages '${options.source}'->'${options.destination}' ('${options.message}')`
const txMetrics = metricManager.AddMetrics(`Session-${i}-TX`); );
startInterval(session, sessionLogger, { const rxMetrics = metricManager.AddMetrics(`Session-${i}-RX`);
rxMetrics, const txMetrics = metricManager.AddMetrics(`Session-${i}-TX`);
txMetrics, startInterval(session, sessionLogger, {
}); rxMetrics,
// TODO: Add error message for invalid systemid and password txMetrics,
});
session.on("deliver_sm", function (pdu) {
if (rxMetrics) { session.on("deliver_sm", function (pdu) {
rxMetrics.AddEvent(); if (rxMetrics) {
} rxMetrics.AddEvent();
sessionLogger.info("Got deliver_sm, replying..."); }
// setTimeout(() => { sessionLogger.info("Got deliver_sm, replying...");
// session.send(pdu.response()); // setTimeout(() => {
// txMetrics.AddEvent(); // session.send(pdu.response());
// }, 200); // txMetrics.AddEvent();
session.send(pdu.response()); // }, 200);
if (txMetrics) { session.send(pdu.response());
txMetrics.AddEvent(); if (txMetrics) {
} txMetrics.AddEvent();
}); }
session.on("enquire_link", function (pdu) { });
session.send(pdu.response()); session.on("enquire_link", function (pdu) {
}); session.send(pdu.response());
session.on("close", function () { });
sessionLogger.error(`Session closed`); session.on("close", function () {
process.exit(1); sessionLogger.error(`Session closed`);
}); process.exit(1);
session.on("error", function (err) { });
sessionLogger.error(`Fatal error ${err}`); session.on("error", function (err) {
process.exit(1); sessionLogger.error(`Fatal error ${err}`);
}); process.exit(1);
} });
} } else {
); sessionLogger.error(`Failed to bind, status ${pdu.command_status}`);
} process.exit(1);
); }
} }
);
}
);
}

150
logger.js
View File

@@ -1,75 +1,75 @@
const { createLogger, format, transports } = require("winston"); const { createLogger, format, transports } = require("winston");
const { combine, timestamp, label, printf } = format; const { combine, timestamp, label, printf } = format;
const defaultFormat = printf(({ level, message, timestamp }) => { const defaultFormat = printf(({ level, message, timestamp }) => {
return `${timestamp} ${level}: ${message}`; return `${timestamp} ${level}: ${message}`;
}); });
const sessionFormat = printf(({ level, message, label, timestamp }) => { const sessionFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} [Session ${label}] ${level}: ${message}`; return `${timestamp} [Session ${label}] ${level}: ${message}`;
}); });
function createBaseLogger(options) { function createBaseLogger(options) {
const logger = createLogger({ const logger = createLogger({
format: combine(format.colorize({ all: true }), timestamp(), defaultFormat), format: combine(format.colorize({ all: true }), timestamp(), defaultFormat),
transports: [new transports.Console()], transports: [new transports.Console()],
}); });
const oldInfo = logger.info; const oldInfo = logger.info;
const oldWarn = logger.info; const oldWarn = logger.info;
const oldError = logger.error; const oldError = logger.error;
logger.info = function (input) { logger.info = function (input) {
if (!shouldLog(options)) { if (!shouldLog(options)) {
return; return;
} }
oldInfo(input); oldInfo(input);
}; };
logger.error = function (input) { logger.error = function (input) {
if (!shouldLog(options)) { if (!shouldLog(options)) {
return; return;
} }
oldError(input); oldError(input);
}; };
logger.warn = function (input) { logger.warn = function (input) {
if (!shouldLog(options)) { if (!shouldLog(options)) {
return; return;
} }
oldWarn(input); oldWarn(input);
}; };
return logger; return logger;
} }
function createSessionLogger(options, ilabel) { function createSessionLogger(options, ilabel) {
const logger = createLogger({ const logger = createLogger({
format: combine(label({ label: ilabel }), format.colorize({ all: true }), timestamp(), sessionFormat), format: combine(label({ label: ilabel }), format.colorize({ all: true }), timestamp(), sessionFormat),
transports: [new transports.Console()], transports: [new transports.Console()],
}); });
const oldInfo = logger.info; const oldInfo = logger.info;
const oldWarn = logger.info; const oldWarn = logger.info;
const oldError = logger.error; const oldError = logger.error;
logger.info = function (input) { logger.info = function (input) {
if (!shouldLog(options)) { if (!shouldLog(options)) {
return; return;
} }
oldInfo(input); oldInfo(input);
}; };
logger.error = function (input) { logger.error = function (input) {
if (!shouldLog(options)) { if (!shouldLog(options)) {
return; return;
} }
oldError(input); oldError(input);
}; };
logger.warn = function (input) { logger.warn = function (input) {
if (!shouldLog(options)) { if (!shouldLog(options)) {
return; return;
} }
oldWarn(input); oldWarn(input);
}; };
return logger; return logger;
} }
function shouldLog(options) { function shouldLog(options) {
return options.logs || !options.bars; return options.logs || !options.bars;
} }
module.exports = { createBaseLogger, createSessionLogger }; module.exports = { createBaseLogger, createSessionLogger };

View File

@@ -1,46 +1,46 @@
class CircularBuffer { class CircularBuffer {
constructor(size) { constructor(size) {
this.buffer = new Array(size); this.buffer = new Array(size);
this.size = size; this.size = size;
this.head = 0; this.head = 0;
this.tail = 0; this.tail = 0;
} }
push(item) { push(item) {
this.buffer[this.head] = item; this.buffer[this.head] = item;
this.head = (this.head + 1) % this.size; this.head = (this.head + 1) % this.size;
if (this.head === this.tail) { if (this.head === this.tail) {
this.tail = (this.tail + 1) % this.size; this.tail = (this.tail + 1) % this.size;
} }
} }
toArray() { toArray() {
const result = []; const result = [];
let current = this.tail; let current = this.tail;
for (let i = 0; i < this.size; i++) { for (let i = 0; i < this.size; i++) {
if (this.buffer[current] !== undefined) { if (this.buffer[current] !== undefined) {
result.push(this.buffer[current]); result.push(this.buffer[current]);
} }
current = (current + 1) % this.size; current = (current + 1) % this.size;
} }
return result; return result;
} }
toArrayRecent(n = 10) { toArrayRecent(n = 10) {
const result = []; const result = [];
const threshold = Date.now() - n * 1000; const threshold = Date.now() - n * 1000;
let current = (this.head - 1 + this.size) % this.size; let current = (this.head - 1 + this.size) % this.size;
while (current !== this.tail) { while (current !== this.tail) {
if (this.buffer[current] !== undefined && this.buffer[current].timestamp > threshold) { if (this.buffer[current] !== undefined && this.buffer[current].timestamp > threshold) {
result.push(this.buffer[current]); result.push(this.buffer[current]);
} else { } else {
break; break;
} }
current = (current - 1 + this.size) % this.size; current = (current - 1 + this.size) % this.size;
} }
return result; return result;
} }
} }
module.exports = { CircularBuffer }; module.exports = { CircularBuffer };

View File

@@ -1,30 +1,30 @@
const cliProgress = require("cli-progress"); const cliProgress = require("cli-progress");
const { Metric } = require("./metrics"); const { Metric } = require("./metrics");
class MetricManager { class MetricManager {
constructor(options) { constructor(options) {
this.options = options; this.options = options;
if (options.bars) { if (options.bars) {
this.metricBufferSize = 1000; this.metricBufferSize = 1000;
this.multibar = new cliProgress.MultiBar( this.multibar = new cliProgress.MultiBar(
{ {
clearOnComplete: false, clearOnComplete: false,
barCompleteChar: "\u2588", barCompleteChar: "\u2588",
barIncompleteChar: "\u2591", barIncompleteChar: "\u2591",
format: " {bar} | {name} | {value}/{total}", format: " {bar} | {name} | {value}/{total}",
}, },
cliProgress.Presets.shades_grey cliProgress.Presets.shades_grey
); );
setInterval(() => this.multibar.update(), 200); setInterval(() => this.multibar.update(), 200);
} }
} }
AddMetrics(name, refresh = true) { AddMetrics(name, refresh = true) {
if (this.options.bars) { if (this.options.bars) {
const metric = new Metric(name, this.multibar, this.metricBufferSize, this.options, refresh); const metric = new Metric(name, this.multibar, this.metricBufferSize, this.options, refresh);
return metric; return metric;
} }
} }
} }
module.exports = { MetricManager }; module.exports = { MetricManager };

View File

@@ -1,39 +1,39 @@
const { CircularBuffer } = require("./circularBuffer"); const { CircularBuffer } = require("./circularBuffer");
class Metric { class Metric {
constructor(barName, multibar, bufferSize, options, refresh = true) { constructor(barName, multibar, bufferSize, options, refresh = true) {
this.options = options; this.options = options;
this.multibar = multibar; this.multibar = multibar;
this.bar = multibar.create(0, 0); this.bar = multibar.create(0, 0);
this.bar.update(0, { name: barName }); this.bar.update(0, { name: barName });
this.maxRate = this.options.defaultmaxrate; this.maxRate = this.options.defaultmaxrate;
this.bar.total = this.maxRate; this.bar.total = this.maxRate;
this.buffer = new CircularBuffer(bufferSize); this.buffer = new CircularBuffer(bufferSize);
if (refresh) { if (refresh) {
setInterval(this.UpdateBar.bind(this), 100); setInterval(this.UpdateBar.bind(this), 100);
} }
} }
AddEvent() { AddEvent() {
const timestamp = Date.now(); const timestamp = Date.now();
this.buffer.push({ timestamp, count: 1 }); this.buffer.push({ timestamp, count: 1 });
} }
GetRate() { GetRate() {
const entries = this.buffer.toArrayRecent(this.options.metricsinterval); const entries = this.buffer.toArrayRecent(this.options.metricsinterval);
const totalRX = entries.reduce((sum, entry) => sum + entry.count, 0); const totalRX = entries.reduce((sum, entry) => sum + entry.count, 0);
return Math.round((totalRX / this.options.metricsinterval) * 100) / 100; return Math.round((totalRX / this.options.metricsinterval) * 100) / 100;
} }
UpdateBar() { UpdateBar() {
const eps = this.GetRate(); const eps = this.GetRate();
if (eps > this.maxRate) { if (eps > this.maxRate) {
this.bar.total = eps; this.bar.total = eps;
this.maxRate = eps; this.maxRate = eps;
} }
this.bar.update(eps); this.bar.update(eps);
} }
} }
module.exports = { Metric }; module.exports = { Metric };

View File

@@ -8,7 +8,8 @@
"build-windows-client": "nexe -i client.js -o out/client-windows -t windows-x86-18.18.2", "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-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-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": [], "keywords": [],
"author": "", "author": "",
@@ -22,6 +23,8 @@
"winston": "^3.11.0" "winston": "^3.11.0"
}, },
"devDependencies": { "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);
});
});

121
utils.js
View File

@@ -1,17 +1,104 @@
function verifyExists(value, err, logger) { const smpp = require("smpp");
if (!value) {
logger.error(err); function verifyExists(value, err, logger) {
process.exit(0); if (!value) {
} logger.error(err);
} process.exit(0);
function verifyDefaults(options, definitions) { }
for (const optionDefinition of definitions) { }
if (optionDefinition.defaultOption) { function verifyDefaults(options, definitions) {
if (!options[optionDefinition.name]) { for (const optionDefinition of definitions) {
options[optionDefinition.name] = optionDefinition.defaultOption; if (optionDefinition.defaultOption) {
} if (!options[optionDefinition.name]) {
} options[optionDefinition.name] = optionDefinition.defaultOption;
} }
} }
}
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 };