This commit is contained in:
PhatPhuckDave
2024-07-22 19:50:03 +02:00
parent a9979def99
commit 0539fa6621
12 changed files with 1068 additions and 1076 deletions

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
--- ---

472
center.js
View File

@@ -1,236 +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, sendPdu } = 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: Currently bars are broken
// A major rework will need to happen before bars are able to play nice with multiple sessions // 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 // 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(sessions, 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; 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}`);
const pdu = new smpp.PDU("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,
}); });
if (sessionPointer >= sessions.length) { if (sessionPointer >= sessions.length) {
sessionPointer = 0; sessionPointer = 0;
} }
sendPdu(sessions[sessionPointer++], pdu, sessionLogger, options.longsms) sendPdu(sessions[sessionPointer++], pdu, sessionLogger, options.longsms)
.then((resp) => { .then((resp) => {
inFlight--; inFlight--;
sessionLogger.info(`Received response with id ${resp.message_id}`); sessionLogger.info(`Received response with id ${resp.message_id}`);
success++; success++;
}) })
.catch((resp) => { .catch((resp) => {
inFlight--; inFlight--;
sessionLogger.warn(`Message failed with id ${resp.message_id}`); sessionLogger.warn(`Message failed with id ${resp.message_id}`);
failed++; failed++;
}); });
sent++; sent++;
inFlight++; inFlight++;
} else { } else {
sessionLogger.warn( sessionLogger.warn(
`${inFlight}/${options.window} messages pending, waiting for a reply before sending more` `${inFlight}/${options.window} messages pending, waiting for a reply before sending more`
); );
sendTimer.clearInterval(); sendTimer.clearInterval();
setTimeout(() => startInterval(sessions, sessionLogger), options.windowsleep); setTimeout(() => startInterval(sessions, sessionLogger), options.windowsleep);
} }
}, },
"", "",
`${1 / options.mps} s` `${1 / options.mps} s`
); );
} }
logger.info(`Staring server on port ${options.port}...`); logger.info(`Staring server on port ${options.port}...`);
let sessionid = 1; let sessionid = 1;
let messageid = 0; let messageid = 0;
const sessions = []; const sessions = [];
const server = smpp.createServer( const server = smpp.createServer(
{ {
debug: options.debug, debug: options.debug,
}, },
function (session) { function (session) {
const id = sessionid++; const id = sessionid++;
const sessionLogger = createSessionLogger(options, id); const sessionLogger = createSessionLogger(options, id);
const rxMetrics = metricManager.AddMetrics(`Session-${id}-RX`); const rxMetrics = metricManager.AddMetrics(`Session-${id}-RX`);
const txMetrics = metricManager.AddMetrics(`Session-${id}-TX`); const txMetrics = metricManager.AddMetrics(`Session-${id}-TX`);
session.on("bind_transceiver", function (pdu) { session.on("bind_transceiver", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) { if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessions.push(session); sessions.push(session);
sessionLogger.info(`Client connected, currently: ${sessions.length}`); sessionLogger.info(`Client connected, currently: ${sessions.length}`);
session.send(pdu.response()); session.send(pdu.response());
startInterval(sessions, sessionLogger); startInterval(sessions, sessionLogger);
} else { } else {
sessionLogger.warn( sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
); );
pdu.response({ pdu.response({
command_status: smpp.ESME_RBINDFAIL, command_status: smpp.ESME_RBINDFAIL,
}); });
session.close(); session.close();
} }
}); });
session.on("bind_transmitter", function (pdu) { session.on("bind_transmitter", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) { if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.info("Client connected"); sessionLogger.info("Client connected");
session.send(pdu.response()); session.send(pdu.response());
startInterval(session, sessionLogger, rxMetrics); startInterval(session, sessionLogger, rxMetrics);
} else { } else {
sessionLogger.warn( sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
); );
pdu.response({ pdu.response({
command_status: smpp.ESME_RBINDFAIL, command_status: smpp.ESME_RBINDFAIL,
}); });
session.close(); session.close();
} }
}); });
session.on("bind_receiver", function (pdu) { session.on("bind_receiver", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) { if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.info("Client connected"); sessionLogger.info("Client connected");
session.send(pdu.response()); session.send(pdu.response());
startInterval(session, sessionLogger); startInterval(session, sessionLogger);
} else { } else {
sessionLogger.warn( sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')` `Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
); );
pdu.response({ pdu.response({
command_status: smpp.ESME_RBINDFAIL, command_status: smpp.ESME_RBINDFAIL,
}); });
session.close(); session.close();
} }
}); });
session.on("enquire_link", function (pdu) { session.on("enquire_link", function (pdu) {
session.send(pdu.response()); session.send(pdu.response());
}); });
session.on("submit_sm", async function (pdu) { session.on("submit_sm", async function (pdu) {
if (!options.dr) { if (!options.dr) {
sessionLogger.info("Replying to incoming submit_sm"); sessionLogger.info("Replying to incoming submit_sm");
if (options.bars) { if (options.bars) {
rxMetrics.AddEvent(); rxMetrics.AddEvent();
} }
// setTimeout(() => { // setTimeout(() => {
// session.send(pdu.response()); // session.send(pdu.response());
// }, 200); // }, 200);
session.send(pdu.response()); session.send(pdu.response());
return; return;
} }
sessionLogger.info("Generating DR for incoming submit_sm"); sessionLogger.info("Generating DR for incoming submit_sm");
let response = pdu.response(); let response = pdu.response();
let smppid = messageid++; let smppid = messageid++;
if (options.randid) { if (options.randid) {
smppid = crypto.randomBytes(8).toString("hex"); smppid = crypto.randomBytes(8).toString("hex");
} }
response.message_id = smppid.toString(16); response.message_id = smppid.toString(16);
session.send(response); session.send(response);
let drMessage = ""; let drMessage = "";
let date = new Date() let date = new Date()
.toISOString() .toISOString()
.replace(/T/, "") .replace(/T/, "")
.replace(/\..+/, "") .replace(/\..+/, "")
.replace(/-/g, "") .replace(/-/g, "")
.replace(/:/g, "") .replace(/:/g, "")
.substring(2, 12); .substring(2, 12);
drMessage += "id:" + response.message_id + " "; drMessage += "id:" + response.message_id + " ";
drMessage += "sub:001 "; drMessage += "sub:001 ";
drMessage += "dlvrd:001 "; drMessage += "dlvrd:001 ";
drMessage += "submit date:" + date + " "; drMessage += "submit date:" + date + " ";
drMessage += "done date:" + date + " "; drMessage += "done date:" + date + " ";
drMessage += "stat:DELIVRD "; drMessage += "stat:DELIVRD ";
drMessage += "err:000 "; drMessage += "err:000 ";
drMessage += "text:"; drMessage += "text:";
const DRPdu = { const DRPdu = {
source_addr: pdu.destination_addr, source_addr: pdu.destination_addr,
destination_addr: pdu.source_addr, destination_addr: pdu.source_addr,
short_message: drMessage, short_message: drMessage,
esm_class: 4, esm_class: 4,
}; };
sessionLogger.info(`Generated DR as ${drMessage}`); sessionLogger.info(`Generated DR as ${drMessage}`);
session.deliver_sm(DRPdu); session.deliver_sm(DRPdu);
if (txMetrics) { if (txMetrics) {
txMetrics.AddEvent(); txMetrics.AddEvent();
} }
}); });
session.on("close", function () { session.on("close", function () {
sessions.splice(sessions.indexOf(session), 1); sessions.splice(sessions.indexOf(session), 1);
sessionLogger.warn(`Session closed, now ${sessions.length}`); sessionLogger.warn(`Session closed, now ${sessions.length}`);
session.close(); session.close();
if (sessions.length === 0) { if (sessions.length === 0) {
sessionLogger.info("No more sessions, stopping sending timer"); sessionLogger.info("No more sessions, stopping sending timer");
sendTimer.clearInterval(); sendTimer.clearInterval();
} }
}); });
session.on("error", function (err) { session.on("error", function (err) {
sessionLogger.error(`Fatal error ${err}`); sessionLogger.error(`Fatal error ${err}`);
session.close(); session.close();
}); });
} }
); );
server.on("error", function (err) { server.on("error", function (err) {
logger.error(`Fatal server error ${err}`); logger.error(`Fatal server error ${err}`);
server.close(); server.close();
process.exit(1); process.exit(1);
}); });
server.listen(options.port); server.listen(options.port);
logger.info(`SMPP Server listening on ${options.port}`); logger.info(`SMPP Server listening on ${options.port}`);

View File

@@ -1,166 +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", name: "longsms",
type: Boolean, type: Boolean,
description: "Split messages into multiple parts. Applies only if message is too big for one packet." description: "Split messages into multiple parts. Applies only if message is too big for one packet."
}, },
]; ];
const centerOptions = [ const centerOptions = [
{ name: "help", type: Boolean, description: "Display this usage guide." }, { name: "help", type: Boolean, description: "Display this usage guide." },
{ 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: "dr", type: Boolean, description: "Whether or not to send Delivery Reports.", defaultOption: false }, { name: "dr", type: Boolean, description: "Whether or not to send Delivery Reports.", defaultOption: false },
{ {
name: "randid", name: "randid",
type: Boolean, type: Boolean,
description: "SMPP ID generation is entirely random instead of sequential.", description: "SMPP ID generation is entirely random instead of sequential.",
defaultOption: false, defaultOption: false,
}, },
{ {
name: "sessions", name: "sessions",
type: Number, type: Number,
description: "Maximum number of client sessions to accept, defaults to 8.", description: "Maximum number of client sessions to accept, defaults to 8.",
defaultOption: 8, defaultOption: 8,
}, },
{ {
name: "messagecount", name: "messagecount",
type: Number, type: Number,
description: "Number of messages to send; Optional, defaults to 0.", description: "Number of messages to send; Optional, defaults to 0.",
defaultOption: 0, defaultOption: 0,
}, },
{ {
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 center; Debug mode." }, { 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: "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", name: "longsms",
type: Boolean, type: Boolean,
description: "Split messages into multiple parts. Applies only if message is too big for one packet." description: "Split messages into multiple parts. Applies only if message is too big for one packet."
}, },
]; ];
module.exports = { clientOptions, centerOptions }; module.exports = { clientOptions, centerOptions };

328
client.js
View File

@@ -1,164 +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, sendPdu } = 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();
} }
const pdu = new smpp.PDU("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,
}); });
sendPdu(session, pdu, sessionLogger, options.longsms) sendPdu(session, pdu, sessionLogger, options.longsms)
.then((resp) => { .then((resp) => {
inFlight--; inFlight--;
sessionLogger.info(`Received response with id ${resp.message_id}`); sessionLogger.info(`Received response with id ${resp.message_id}`);
success++; success++;
}) })
.catch((resp) => { .catch((resp) => {
inFlight--; inFlight--;
sessionLogger.warn(`Message failed with id ${resp.message_id}`); sessionLogger.warn(`Message failed with id ${resp.message_id}`);
failed++; failed++;
}); });
if (metrics.txMetrics) { if (metrics.txMetrics) {
metrics.txMetrics.AddEvent(); metrics.txMetrics.AddEvent();
} }
sent++; sent++;
inFlight++; inFlight++;
} else { } else {
sessionLogger.warn( sessionLogger.warn(
`${inFlight}/${options.window} messages pending, waiting for a reply before sending more` `${inFlight}/${options.window} messages pending, waiting for a reply before sending more`
); );
sendTimer.clearInterval(); sendTimer.clearInterval();
setTimeout(() => startInterval(session, sessionLogger, metrics), options.windowsleep); setTimeout(() => startInterval(session, sessionLogger, metrics), options.windowsleep);
} }
}, },
"", "",
`${1 / options.mps} s` `${1 / options.mps} s`
); );
} }
for (let i = 0; i < options.sessions; i++) { for (let i = 0; i < options.sessions; i++) {
const sessionLogger = createSessionLogger(options, i); const sessionLogger = createSessionLogger(options, i);
sessionLogger.info(`Connecting to ${options.host}:${options.port}...`); sessionLogger.info(`Connecting to ${options.host}:${options.port}...`);
const session = smpp.connect( const session = smpp.connect(
{ {
url: `smpp://${options.host}:${options.port}`, url: `smpp://${options.host}:${options.port}`,
auto_enquire_link_period: 10000, auto_enquire_link_period: 10000,
debug: options.debug, debug: options.debug,
}, },
function () { function () {
sessionLogger.info( sessionLogger.info(
`Connected, sending bind_transciever with systemId '${options.systemid}' and password '${options.password}'...` `Connected, sending bind_transciever with systemId '${options.systemid}' and password '${options.password}'...`
); );
session.on('close', function () { session.on('close', function () {
sessionLogger.error(`Session closed`); sessionLogger.error(`Session closed`);
process.exit(1); process.exit(1);
}); });
session.bind_transceiver( session.bind_transceiver(
{ {
system_id: options.systemid, system_id: options.systemid,
password: options.password, password: options.password,
}, },
function (pdu) { function (pdu) {
if (pdu.command_status === 0) { if (pdu.command_status === 0) {
sessionLogger.info( sessionLogger.info(
`Successfully bound, sending ${options.messagecount} messages '${options.source}'->'${options.destination}' ('${options.message}')` `Successfully bound, sending ${options.messagecount} messages '${options.source}'->'${options.destination}' ('${options.message}')`
); );
const rxMetrics = metricManager.AddMetrics(`Session-${i}-RX`); const rxMetrics = metricManager.AddMetrics(`Session-${i}-RX`);
const txMetrics = metricManager.AddMetrics(`Session-${i}-TX`); const txMetrics = metricManager.AddMetrics(`Session-${i}-TX`);
startInterval(session, sessionLogger, { startInterval(session, sessionLogger, {
rxMetrics, rxMetrics,
txMetrics, txMetrics,
}); });
session.on("deliver_sm", function (pdu) { session.on("deliver_sm", function (pdu) {
if (rxMetrics) { if (rxMetrics) {
rxMetrics.AddEvent(); rxMetrics.AddEvent();
} }
sessionLogger.info("Got deliver_sm, replying..."); sessionLogger.info("Got deliver_sm, replying...");
// setTimeout(() => { // setTimeout(() => {
// session.send(pdu.response()); // session.send(pdu.response());
// txMetrics.AddEvent(); // txMetrics.AddEvent();
// }, 200); // }, 200);
session.send(pdu.response()); session.send(pdu.response());
if (txMetrics) { if (txMetrics) {
txMetrics.AddEvent(); txMetrics.AddEvent();
} }
}); });
session.on("enquire_link", function (pdu) { session.on("enquire_link", function (pdu) {
session.send(pdu.response()); session.send(pdu.response());
}); });
session.on("close", function () { session.on("close", function () {
sessionLogger.error(`Session closed`); sessionLogger.error(`Session closed`);
process.exit(1); process.exit(1);
}); });
session.on("error", function (err) { session.on("error", function (err) {
sessionLogger.error(`Fatal error ${err}`); sessionLogger.error(`Fatal error ${err}`);
process.exit(1); process.exit(1);
}); });
} else { } else {
sessionLogger.error(`Failed to bind, status ${pdu.command_status}`); sessionLogger.error(`Failed to bind, status ${pdu.command_status}`);
process.exit(1); 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

@@ -1,94 +1,94 @@
const smpp = require("smpp"); const smpp = require("smpp");
const { splitToParts, verifyExists, getCharacterSizeForEncoding } = require("../utils"); const { splitToParts, verifyExists, getCharacterSizeForEncoding } = require("../utils");
describe("splitToParts", () => { 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) // 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. // 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", () => { 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", { const pdu = new smpp.PDU("deliver_sm", {
short_message: "test message", short_message: "test message",
}); });
const result = splitToParts(pdu); const result = splitToParts(pdu);
expect(result.length).toBe(1); expect(result.length).toBe(1);
}); });
// Given a pdu with short_message length greater than 160 chars, it should return an array with 2 pdus. // 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", () => { 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", { const pdu = new smpp.PDU("deliver_sm", {
short_message: "c".repeat(200), short_message: "c".repeat(200),
}); });
const result = splitToParts(pdu); const result = splitToParts(pdu);
expect(result.length).toBe(2); expect(result.length).toBe(2);
}); });
// Given a pdu with short_message length greater than 320 chars, it should return an array with 2 pdus. // 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", () => { 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", { const pdu = new smpp.PDU("deliver_sm", {
short_message: "c".repeat(400), short_message: "c".repeat(400),
}); });
const result = splitToParts(pdu); const result = splitToParts(pdu);
expect(result.length).toBe(3); expect(result.length).toBe(3);
}); });
// Given a pdu with short_message length equal to 0, it should return an empty array. // 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", () => { it("should return an empty array when short_message length is equal to 0", () => {
const pdu = new smpp.PDU("deliver_sm", { const pdu = new smpp.PDU("deliver_sm", {
short_message: "", short_message: "",
}); });
const result = splitToParts(pdu); const result = splitToParts(pdu);
expect(result.length).toBe(0); expect(result.length).toBe(0);
}); });
// Given a pdu with short_message length equal to 320, it should return an array with two pdus. // 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", () => { it("should return an array with two pdus when short_message length is equal to maxMessageSizeBits", () => {
const pdu = new smpp.PDU("deliver_sm", { const pdu = new smpp.PDU("deliver_sm", {
short_message: "c".repeat(320), short_message: "c".repeat(320),
}); });
const result = splitToParts(pdu); const result = splitToParts(pdu);
expect(result.length).toBe(2); expect(result.length).toBe(2);
}); });
}); });
describe("getCharacterSizeForEncoding", () => { describe("getCharacterSizeForEncoding", () => {
// Returns 7 when data_coding is 0 // Returns 7 when data_coding is 0
it("should return 7 when data_coding is 0", () => { it("should return 7 when data_coding is 0", () => {
const pdu = { data_coding: 0 }; const pdu = { data_coding: 0 };
const result = getCharacterSizeForEncoding(pdu); const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(7); expect(result).toBe(7);
}); });
// Returns 8 when data_coding is 1 // Returns 8 when data_coding is 1
it("should return 8 when data_coding is 1", () => { it("should return 8 when data_coding is 1", () => {
const pdu = { data_coding: 1 }; const pdu = { data_coding: 1 };
const result = getCharacterSizeForEncoding(pdu); const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(8); expect(result).toBe(8);
}); });
// Returns 16 when data_coding is 8 // Returns 16 when data_coding is 8
it("should return 16 when data_coding is 8", () => { it("should return 16 when data_coding is 8", () => {
const pdu = { data_coding: 8 }; const pdu = { data_coding: 8 };
const result = getCharacterSizeForEncoding(pdu); const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(16); expect(result).toBe(16);
}); });
// Returns 7 when data_coding is null // Returns 7 when data_coding is null
it("should return 0 when data_coding is null", () => { it("should return 0 when data_coding is null", () => {
const pdu = { data_coding: null }; const pdu = { data_coding: null };
const result = getCharacterSizeForEncoding(pdu); const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(7); expect(result).toBe(7);
}); });
// Returns 0 when data_coding is not a number // Returns 0 when data_coding is not a number
it("should return 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 pdu = { data_coding: "abc" };
const result = getCharacterSizeForEncoding(pdu); const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(0); expect(result).toBe(0);
}); });
// Returns 0 when data_coding is negative // Returns 0 when data_coding is negative
it("should return 0 when data_coding is negative", () => { it("should return 0 when data_coding is negative", () => {
const pdu = { data_coding: -1 }; const pdu = { data_coding: -1 };
const result = getCharacterSizeForEncoding(pdu); const result = getCharacterSizeForEncoding(pdu);
expect(result).toBe(0); expect(result).toBe(0);
}); });
}); });

208
utils.js
View File

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