Compare commits

..

10 Commits

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

View File

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

2
.gitignore vendored
View File

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

10
.vscode/settings.json vendored
View File

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

224
README.md
View File

@@ -1,112 +1,112 @@
# SMPP_CLI
### A small application used to create SMPP clients or centers. Serves as both the SMSC and ESME.
(Taken from the help page)
---
```
CLI SMPP (Client)
Options
--help Display this usage guide.
-h, --host string The host (IP) to connect to.
-p, --port number The port to connect to.
-s, --systemid string SMPP related login info.
-w, --password string SMPP related login info.
--sessions number Number of sessions to start, 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
'in flight'. The client no longer waits for a
response before sending the next message for up to
<window> messages. Defaults to 100.
--windowsleep number Defines the amount time (in ms) waited between
retrying in the case of full window. Defaults to 100.
--mps number Number of messages to send per second
--source string Source field of the sent messages.
--destination string Destination field of the sent messages.
--message string Text content of the sent messages.
--debug Display all traffic to and from the client; Debug
mode.
```
---
```
CLI SMPP (Center)
Options
--help Display this usage guide.
-p, --port number The port to connect to.
-s, --systemid string SMPP related login info.
-w, --password string SMPP related login info.
--dr Whether or not to send Delivery Reports.
--sessions number Maximum number of client sessions to accept, defaults
to 8.
--messagecount number Number of messages to send; Optional, defaults to 0.
--window 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.
--windowsleep number Defines the amount time (in ms) waited between
retrying in the case of full window. Defaults to 100.
--mps number Number of messages to send per second
--source string Source field of the sent messages.
--destination string Destination field of the sent messages.
--message string Text content of the sent messages.
--debug Display all traffic to and from the center; Debug
mode.
```
---
#### Center example usage:
```
./center-win.exe \
--port 7001 \
--systemid test \
--password test \
--sessions 10
```
Running this command will spawn an SMPP center (SMSC) which will:
- Start listening on 7001
- Accept clients with test:test credentials
- Allow up to a maximum of 10 sessions
#### Client example usage:
```
./client-win.exe \
--host localhost \
--port 7001 \
--systemid test \
--password test \
--window 5 \
--windowsleep 100 \
--messagecount 10000 \
--mps 10 \
--sessions 10
```
Running this command will spawn an SMPP client (ESME) which will:
- Try to connect 10 sessions to localhost:7001
- 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
- If the window is filled during sending the session will wait 100ms before attempting send again
---
---
#### Center example usage (sending):
```
./center-win.exe \
--port 7001 \
--systemid test \
--password test \
--window 5 \
--windowsleep 100 \
--messagecount 10000 \
--mps 10 \
--sessions 10
```
Running this command will spawn an SMPP center (SMSC) which will:
- Start listening on 7001
- Accept clients with test:test credentials
- 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
- If the window is filled during sending the session will wait 100ms before attempting send again
---
# SMPP_CLI
### A small application used to create SMPP clients or centers. Serves as both the SMSC and ESME.
(Taken from the help page)
---
```
CLI SMPP (Client)
Options
--help Display this usage guide.
-h, --host string The host (IP) to connect to.
-p, --port number The port to connect to.
-s, --systemid string SMPP related login info.
-w, --password string SMPP related login info.
--sessions number Number of sessions to start, 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
'in flight'. The client no longer waits for a
response before sending the next message for up to
<window> messages. Defaults to 100.
--windowsleep number Defines the amount time (in ms) waited between
retrying in the case of full window. Defaults to 100.
--mps number Number of messages to send per second
--source string Source field of the sent messages.
--destination string Destination field of the sent messages.
--message string Text content of the sent messages.
--debug Display all traffic to and from the client; Debug
mode.
```
---
```
CLI SMPP (Center)
Options
--help Display this usage guide.
-p, --port number The port to connect to.
-s, --systemid string SMPP related login info.
-w, --password string SMPP related login info.
--dr Whether or not to send Delivery Reports.
--sessions number Maximum number of client sessions to accept, defaults
to 8.
--messagecount number Number of messages to send; Optional, defaults to 0.
--window 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.
--windowsleep number Defines the amount time (in ms) waited between
retrying in the case of full window. Defaults to 100.
--mps number Number of messages to send per second
--source string Source field of the sent messages.
--destination string Destination field of the sent messages.
--message string Text content of the sent messages.
--debug Display all traffic to and from the center; Debug
mode.
```
---
#### Center example usage:
```
./center-win.exe \
--port 7001 \
--systemid test \
--password test \
--sessions 10
```
Running this command will spawn an SMPP center (SMSC) which will:
- Start listening on 7001
- Accept clients with test:test credentials
- Allow up to a maximum of 10 sessions
#### Client example usage:
```
./client-win.exe \
--host localhost \
--port 7001 \
--systemid test \
--password test \
--window 5 \
--windowsleep 100 \
--messagecount 10000 \
--mps 10 \
--sessions 10
```
Running this command will spawn an SMPP client (ESME) which will:
- Try to connect 10 sessions to localhost:7001
- 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
- If the window is filled during sending the session will wait 100ms before attempting send again
---
---
#### Center example usage (sending):
```
./center-win.exe \
--port 7001 \
--systemid test \
--password test \
--window 5 \
--windowsleep 100 \
--messagecount 10000 \
--mps 10 \
--sessions 10
```
Running this command will spawn an SMPP center (SMSC) which will:
- Start listening on 7001
- Accept clients with test:test credentials
- 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
- 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 commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage");
const NanoTimer = require("nanotimer");
const { createBaseLogger, createSessionLogger } = require("./logger");
const { verifyDefaults, verifyExists } = require("./utils");
const { centerOptions } = require("./cliOptions");
const crypto = require("crypto");
const { MetricManager } = require("./metrics/metricManager");
const options = commandLineArgs(centerOptions);
const logger = createBaseLogger(options);
if (options.help) {
const usage = commandLineUsage([
{
header: "CLI SMPP (Center)",
},
{
header: "Options",
optionList: centerOptions,
},
{
content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}",
},
]);
console.log(usage);
process.exit(0);
}
verifyDefaults(options, centerOptions);
verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger);
verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger);
verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger);
let inFlight = 0;
let sent = 0;
let success = 0;
let failed = 0;
const sendTimer = new NanoTimer();
const metricManager = new MetricManager(options);
// TODO: Fix issue where a client disconnecting does not stop this timer
// TODO: Fix issue where only one session is being utilized because they all share the same timer
// TODO: Maybe add only receiver and only transmitter modes instead of transciever
// Instead just use the same timer but make a pool of connections; That way both problems will be solved
function startInterval(session, sessionLogger, rxMetrics) {
if (!options.messagecount > 0) {
sessionLogger.info("No messages to send");
return;
}
sendTimer.setInterval(
async () => {
if (sent >= options.messagecount) {
sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`);
sendTimer.clearInterval();
} else if (inFlight < options.window) {
sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`);
session.deliver_sm(
{
source_addr: options.source,
destination_addr: options.destination,
short_message: options.message,
},
function (pdu) {
inFlight--;
if (pdu.command_status === 0) {
sessionLogger.info(`Received response with id ${pdu.message_id}`);
success++;
} else {
sessionLogger.warn(`Message failed with id ${pdu.message_id}`);
failed++;
}
}
);
rxMetrics.AddEvent();
sent++;
inFlight++;
} else {
sessionLogger.warn(
`${inFlight}/${options.window} messages pending, waiting for a reply before sending more`
);
sendTimer.clearInterval();
setTimeout(() => startInterval(session, sessionLogger), options.windowsleep);
}
},
"",
`${1 / options.mps} s`
);
}
logger.info(`Staring server on port ${options.port}...`);
let sessionid = 1;
let messageid = 0;
const server = smpp.createServer(
{
debug: options.debug,
},
function (session) {
const id = sessionid++;
const sessionLogger = createSessionLogger(options, id);
const rxMetrics = metricManager.AddMetrics(`Session-${id}-RX`);
const txMetrics = metricManager.AddMetrics(`Session-${id}-TX`);
session.on("bind_transceiver", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.info("Client connected");
session.send(pdu.response());
startInterval(session, sessionLogger);
} else {
sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
);
pdu.response({
command_status: smpp.ESME_RBINDFAIL,
});
session.close();
}
});
session.on("bind_transmitter", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.info("Client connected");
session.send(pdu.response());
startInterval(session, sessionLogger);
} else {
sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
);
pdu.response({
command_status: smpp.ESME_RBINDFAIL,
});
session.close();
}
});
session.on("bind_receiver", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.info("Client connected");
session.send(pdu.response());
startInterval(session, sessionLogger);
} else {
sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
);
pdu.response({
command_status: smpp.ESME_RBINDFAIL,
});
session.close();
}
});
session.on("enquire_link", function (pdu) {
session.send(pdu.response());
});
session.on("submit_sm", async function (pdu) {
if (!options.dr) {
sessionLogger.info("Replying to incoming submit_sm");
if (options.bars) {
rxMetrics.AddEvent();
}
// setTimeout(() => {
// session.send(pdu.response());
// }, 200);
session.send(pdu.response());
return;
}
sessionLogger.info("Generating DR for incoming submit_sm");
let response = pdu.response();
let smppid = messageid++;
if (options.randid) {
smppid = crypto.randomBytes(8).toString("hex");
}
response.message_id = smppid.toString(16);
session.send(response);
let drMessage = "";
let date = new Date()
.toISOString()
.replace(/T/, "")
.replace(/\..+/, "")
.replace(/-/g, "")
.replace(/:/g, "")
.substring(2, 12);
drMessage += "id:" + response.message_id + " ";
drMessage += "sub:001 ";
drMessage += "dlvrd:001 ";
drMessage += "submit date:" + date + " ";
drMessage += "done date:" + date + " ";
drMessage += "stat:DELIVRD ";
drMessage += "err:000 ";
drMessage += "text:";
const DRPdu = {
source_addr: pdu.destination_addr,
destination_addr: pdu.source_addr,
short_message: drMessage,
esm_class: 4,
};
sessionLogger.info(`Generated DR as ${drMessage}`);
session.deliver_sm(DRPdu);
if (txMetrics) {
txMetrics.AddEvent();
}
});
session.on("close", function () {
sessionLogger.warn(`Session closed`);
session.close();
});
session.on("error", function (err) {
sessionLogger.error(`Fatal error ${err}`);
session.close();
});
}
);
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}`);
const smpp = require("smpp");
const commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage");
const NanoTimer = require("nanotimer");
const { createBaseLogger, createSessionLogger } = require("./logger");
const { verifyDefaults, verifyExists, sendPdu } = require("./utils");
const { centerOptions } = require("./cliOptions");
const crypto = require("crypto");
const { MetricManager } = require("./metrics/metricManager");
const options = commandLineArgs(centerOptions);
const logger = createBaseLogger(options);
if (options.help) {
const usage = commandLineUsage([
{
header: "CLI SMPP (Center)",
},
{
header: "Options",
optionList: centerOptions,
},
{
content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}",
},
]);
console.log(usage);
process.exit(0);
}
verifyDefaults(options, centerOptions);
verifyExists(options.port, "Port can not be undefined or empty! (--port)", logger);
verifyExists(options.systemid, "SystemID can not be undefined or empty! (--systemid)", logger);
verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger);
let inFlight = 0;
let sent = 0;
let success = 0;
let failed = 0;
const sendTimer = new NanoTimer();
const metricManager = new MetricManager(options);
// TODO: Currently bars are broken
// A major rework will need to happen before bars are able to play nice with multiple sessions
// TODO: Maybe add only receiver and only transmitter modes instead of transciever
// Instead just use the same timer but make a pool of connections; That way both problems will be solved
function startInterval(sessions, sessionLogger, rxMetrics) {
if (!options.messagecount > 0) {
sessionLogger.info("No messages to send");
return;
}
let sessionPointer = 0;
sendTimer.setInterval(
async () => {
if (sent >= options.messagecount) {
sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`);
sendTimer.clearInterval();
} else if (inFlight < options.window) {
sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`);
const pdu = new smpp.PDU("deliver_sm", {
source_addr: options.source,
destination_addr: options.destination,
short_message: options.message,
});
if (sessionPointer >= sessions.length) {
sessionPointer = 0;
}
sendPdu(sessions[sessionPointer++], pdu, sessionLogger, options.longsms)
.then((resp) => {
inFlight--;
sessionLogger.info(`Received response with id ${resp.message_id}`);
success++;
})
.catch((resp) => {
inFlight--;
sessionLogger.warn(`Message failed with id ${resp.message_id}`);
failed++;
});
sent++;
inFlight++;
} else {
sessionLogger.warn(
`${inFlight}/${options.window} messages pending, waiting for a reply before sending more`
);
sendTimer.clearInterval();
setTimeout(() => startInterval(sessions, sessionLogger), options.windowsleep);
}
},
"",
`${1 / options.mps} s`
);
}
logger.info(`Staring server on port ${options.port}...`);
let sessionid = 1;
let messageid = 0;
const sessions = [];
const server = smpp.createServer(
{
debug: options.debug,
},
function (session) {
const id = sessionid++;
const sessionLogger = createSessionLogger(options, id);
const rxMetrics = metricManager.AddMetrics(`Session-${id}-RX`);
const txMetrics = metricManager.AddMetrics(`Session-${id}-TX`);
session.on("bind_transceiver", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessions.push(session);
sessionLogger.info(`Client connected, currently: ${sessions.length}`);
session.send(pdu.response());
startInterval(sessions, sessionLogger);
} else {
sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
);
pdu.response({
command_status: smpp.ESME_RBINDFAIL,
});
session.close();
}
});
session.on("bind_transmitter", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.info("Client connected");
session.send(pdu.response());
startInterval(session, sessionLogger, rxMetrics);
} else {
sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
);
pdu.response({
command_status: smpp.ESME_RBINDFAIL,
});
session.close();
}
});
session.on("bind_receiver", function (pdu) {
if (pdu.system_id === options.systemid && pdu.password === options.password) {
sessionLogger.info("Client connected");
session.send(pdu.response());
startInterval(session, sessionLogger);
} else {
sessionLogger.warn(
`Client tried to connect with incorrect login ('${pdu.system_id}' '${pdu.password}')`
);
pdu.response({
command_status: smpp.ESME_RBINDFAIL,
});
session.close();
}
});
session.on("enquire_link", function (pdu) {
session.send(pdu.response());
});
session.on("submit_sm", async function (pdu) {
if (!options.dr) {
sessionLogger.info("Replying to incoming submit_sm");
if (options.bars) {
rxMetrics.AddEvent();
}
// setTimeout(() => {
// session.send(pdu.response());
// }, 200);
session.send(pdu.response());
return;
}
sessionLogger.info("Generating DR for incoming submit_sm");
let response = pdu.response();
let smppid = messageid++;
if (options.randid) {
smppid = crypto.randomBytes(8).toString("hex");
}
response.message_id = smppid.toString(16);
session.send(response);
let drMessage = "";
let date = new Date()
.toISOString()
.replace(/T/, "")
.replace(/\..+/, "")
.replace(/-/g, "")
.replace(/:/g, "")
.substring(2, 12);
drMessage += "id:" + response.message_id + " ";
drMessage += "sub:001 ";
drMessage += "dlvrd:001 ";
drMessage += "submit date:" + date + " ";
drMessage += "done date:" + date + " ";
drMessage += "stat:DELIVRD ";
drMessage += "err:000 ";
drMessage += "text:";
const DRPdu = {
source_addr: pdu.destination_addr,
destination_addr: pdu.source_addr,
short_message: drMessage,
esm_class: 4,
};
sessionLogger.info(`Generated DR as ${drMessage}`);
session.deliver_sm(DRPdu);
if (txMetrics) {
txMetrics.AddEvent();
}
});
session.on("close", function () {
sessions.splice(sessions.indexOf(session), 1);
sessionLogger.warn(`Session closed, now ${sessions.length}`);
session.close();
if (sessions.length === 0) {
sessionLogger.info("No more sessions, stopping sending timer");
sendTimer.clearInterval();
}
});
session.on("error", function (err) {
sessionLogger.error(`Fatal error ${err}`);
session.close();
});
}
);
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 = [
{ name: "help", type: Boolean, description: "Display this usage guide." },
{ 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: "systemid", alias: "s", 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: "messagecount",
type: Number,
description: "Number of messages to send; Optional, defaults to 1.",
defaultOption: 1,
},
{
name: "window",
type: Number,
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.",
defaultOption: 100,
},
{
name: "windowsleep",
type: Number,
description:
"Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.",
defaultOption: 100,
},
{
name: "mps",
type: Number,
description: "Number of messages to send per second",
defaultOption: 999999,
},
{
name: "source",
type: String,
description: "Source field of the sent messages.",
defaultOption: "smppDebugClient",
},
{
name: "destination",
type: String,
description: "Destination field of the sent messages.",
defaultOption: "smpp",
},
{
name: "message",
type: String,
description: "Text content of the sent messages.",
defaultOption: "smpp debug message",
},
{ 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: "bars",
type: Boolean,
description: "Display TX and RX bars. Can be used with logs (although it will make a mess)."
},
{
name: "metricsinterval",
type: Number,
defaultOption: 5,
description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5."
},
{
name: "defaultmaxrate",
type: Number,
defaultOption: 1000,
description: "Default max rate for metrics/bars."
},
];
const centerOptions = [
{ name: "help", type: Boolean, description: "Display this usage guide." },
{ 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 },
{
name: "randid",
type: Boolean,
description: "SMPP ID generation is entirely random instead of sequential.",
defaultOption: false,
},
{
name: "sessions",
type: Number,
description: "Maximum number of client sessions to accept, defaults to 8.",
defaultOption: 8,
},
{
name: "messagecount",
type: Number,
description: "Number of messages to send; Optional, defaults to 0.",
defaultOption: 0,
},
{
name: "window",
type: Number,
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.",
defaultOption: 100,
},
{
name: "windowsleep",
type: Number,
description:
"Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.",
defaultOption: 100,
},
{
name: "mps",
type: Number,
description: "Number of messages to send per second",
defaultOption: 999999,
},
{
name: "source",
type: String,
description: "Source field of the sent messages.",
defaultOption: "smppDebugClient",
},
{
name: "destination",
type: String,
description: "Destination field of the sent messages.",
defaultOption: "smpp",
},
{
name: "message",
type: String,
description: "Text content of the sent messages.",
defaultOption: "smpp debug message",
},
{ 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: "bars",
type: Boolean,
description: "Display TX and RX bars. Can be used with logs (although it will make a mess)."
},
{
name: "metricsinterval",
type: Number,
defaultOption: 5,
description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5."
},
{
name: "defaultmaxrate",
type: Number,
defaultOption: 1000,
description: "Default max rate for metrics/bars."
},
];
module.exports = { clientOptions, centerOptions };
const clientOptions = [
{ name: "help", type: Boolean, description: "Display this usage guide." },
{ 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: "systemid", alias: "s", 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: "messagecount",
type: Number,
description: "Number of messages to send; Optional, defaults to 1.",
defaultOption: 1,
},
{
name: "window",
type: Number,
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.",
defaultOption: 100,
},
{
name: "windowsleep",
type: Number,
description:
"Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.",
defaultOption: 100,
},
{
name: "mps",
type: Number,
description: "Number of messages to send per second",
defaultOption: 999999,
},
{
name: "source",
type: String,
description: "Source field of the sent messages.",
defaultOption: "smppDebugClient",
},
{
name: "destination",
type: String,
description: "Destination field of the sent messages.",
defaultOption: "smpp",
},
{
name: "message",
type: String,
description: "Text content of the sent messages.",
defaultOption: "smpp debug message",
},
{ 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: "bars",
type: Boolean,
description: "Display TX and RX bars. Can be used with logs (although it will make a mess)."
},
{
name: "metricsinterval",
type: Number,
defaultOption: 5,
description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5."
},
{
name: "defaultmaxrate",
type: Number,
defaultOption: 1000,
description: "Default max rate for metrics/bars."
},
{
name: "longsms",
type: Boolean,
description: "Split messages into multiple parts. Applies only if message is too big for one packet."
},
];
const centerOptions = [
{ name: "help", type: Boolean, description: "Display this usage guide." },
{ 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 },
{
name: "randid",
type: Boolean,
description: "SMPP ID generation is entirely random instead of sequential.",
defaultOption: false,
},
{
name: "sessions",
type: Number,
description: "Maximum number of client sessions to accept, defaults to 8.",
defaultOption: 8,
},
{
name: "messagecount",
type: Number,
description: "Number of messages to send; Optional, defaults to 0.",
defaultOption: 0,
},
{
name: "window",
type: Number,
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.",
defaultOption: 100,
},
{
name: "windowsleep",
type: Number,
description:
"Defines the amount time (in ms) waited between retrying in the case of full window. Defaults to 100.",
defaultOption: 100,
},
{
name: "mps",
type: Number,
description: "Number of messages to send per second",
defaultOption: 999999,
},
{
name: "source",
type: String,
description: "Source field of the sent messages.",
defaultOption: "smppDebugClient",
},
{
name: "destination",
type: String,
description: "Destination field of the sent messages.",
defaultOption: "smpp",
},
{
name: "message",
type: String,
description: "Text content of the sent messages.",
defaultOption: "smpp debug message",
},
{ 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: "bars",
type: Boolean,
description: "Display TX and RX bars. Can be used with logs (although it will make a mess)."
},
{
name: "metricsinterval",
type: Number,
defaultOption: 5,
description: "Interval for measuring metrics. A value of 5 considers the packets within the last 5 seconds. Defaults to 5."
},
{
name: "defaultmaxrate",
type: Number,
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 commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage");
const NanoTimer = require("nanotimer");
const { createBaseLogger, createSessionLogger } = require("./logger");
const { verifyDefaults, verifyExists } = require("./utils");
const { clientOptions } = require("./cliOptions");
const { MetricManager } = require("./metrics/metricManager");
const options = commandLineArgs(clientOptions);
const logger = createBaseLogger(options);
if (options.help) {
const usage = commandLineUsage([
{
header: "CLI SMPP (Client)",
},
{
header: "Options",
optionList: clientOptions,
},
{
content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}",
},
]);
console.log(usage);
process.exit(0);
}
verifyDefaults(options, clientOptions);
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.systemid, "SystemID can not be undefined or empty! (--systemid)", logger);
verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger);
let inFlight = 0;
let sent = 0;
let success = 0;
let failed = 0;
const sendTimer = new NanoTimer();
const metricManager = new MetricManager(options);
function startInterval(session, sessionLogger, metrics) {
if (!metrics.progress && options.bars === true) {
metrics.progress = metricManager.AddMetrics("Send progress", false);
metrics.progress.bar.total = options.messagecount;
metrics.window = metricManager.AddMetrics("Send window", false);
metrics.window.bar.total = options.window;
}
sendTimer.setInterval(
async () => {
if (sent >= options.messagecount) {
sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`);
sendTimer.clearInterval();
} else if (inFlight < options.window) {
sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`);
if (options.bars) {
metrics.progress.bar.increment();
metrics.window.bar.increment();
}
session.submit_sm(
{
source_addr: options.source,
destination_addr: options.destination,
short_message: options.message,
},
function (pdu) {
if (metrics.window?.bar) {
metrics.window.bar.update(metrics.window.bar.value - 1);
}
inFlight--;
if (pdu.command_status === 0) {
sessionLogger.info(`Received response with id ${pdu.message_id}`);
success++;
} else {
sessionLogger.warn(`Message failed with id ${pdu.message_id}`);
failed++;
}
}
);
if (metrics.txMetrics) {
metrics.txMetrics.AddEvent();
}
sent++;
inFlight++;
} else {
sessionLogger.warn(
`${inFlight}/${options.window} messages pending, waiting for a reply before sending more`
);
sendTimer.clearInterval();
setTimeout(() => startInterval(session, sessionLogger, metrics), options.windowsleep);
}
},
"",
`${1 / options.mps} s`
);
}
for (let i = 0; i < options.sessions; i++) {
const sessionLogger = createSessionLogger(options, i);
sessionLogger.info(`Connecting to ${options.host}:${options.port}...`);
const session = smpp.connect(
{
url: `smpp://${options.host}:${options.port}`,
auto_enquire_link_period: 10000,
debug: options.debug,
},
function () {
sessionLogger.info(
`Connected, sending bind_transciever with systemId '${options.systemid}' and password '${options.password}'...`
);
session.bind_transceiver(
{
system_id: options.systemid,
password: options.password,
},
function (pdu) {
if (pdu.command_status === 0) {
sessionLogger.info(
`Successfully bound, sending ${options.messagecount} messages '${options.source}'->'${options.destination}' ('${options.message}')`
);
const rxMetrics = metricManager.AddMetrics(`Session-${i}-RX`);
const txMetrics = metricManager.AddMetrics(`Session-${i}-TX`);
startInterval(session, sessionLogger, {
rxMetrics,
txMetrics,
});
// TODO: Add error message for invalid systemid and password
session.on("deliver_sm", function (pdu) {
if (rxMetrics) {
rxMetrics.AddEvent();
}
sessionLogger.info("Got deliver_sm, replying...");
// setTimeout(() => {
// session.send(pdu.response());
// txMetrics.AddEvent();
// }, 200);
session.send(pdu.response());
if (txMetrics) {
txMetrics.AddEvent();
}
});
session.on("enquire_link", function (pdu) {
session.send(pdu.response());
});
session.on("close", function () {
sessionLogger.error(`Session closed`);
process.exit(1);
});
session.on("error", function (err) {
sessionLogger.error(`Fatal error ${err}`);
process.exit(1);
});
}
}
);
}
);
}
const smpp = require("smpp");
const commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage");
const NanoTimer = require("nanotimer");
const { createBaseLogger, createSessionLogger } = require("./logger");
const { verifyDefaults, verifyExists, sendPdu } = require("./utils");
const { clientOptions } = require("./cliOptions");
const { MetricManager } = require("./metrics/metricManager");
const options = commandLineArgs(clientOptions);
const logger = createBaseLogger(options);
if (options.help) {
const usage = commandLineUsage([
{
header: "CLI SMPP (Client)",
},
{
header: "Options",
optionList: clientOptions,
},
{
content: "Project home: {underline https://github.com/PhatDave/SMPP_CLI}",
},
]);
console.log(usage);
process.exit(0);
}
verifyDefaults(options, clientOptions);
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.systemid, "SystemID can not be undefined or empty! (--systemid)", logger);
verifyExists(options.password, "Password can not be undefined or empty! (--password)", logger);
let inFlight = 0;
let sent = 0;
let success = 0;
let failed = 0;
const sendTimer = new NanoTimer();
const metricManager = new MetricManager(options);
function startInterval(session, sessionLogger, metrics) {
if (!metrics.progress && options.bars === true) {
metrics.progress = metricManager.AddMetrics("Send progress", false);
metrics.progress.bar.total = options.messagecount;
metrics.window = metricManager.AddMetrics("Send window", false);
metrics.window.bar.total = options.window;
}
sendTimer.setInterval(
async () => {
if (sent >= options.messagecount) {
sessionLogger.info(`Finished sending messages success:${success}, failed:${failed}, idling...`);
sendTimer.clearInterval();
} else if (inFlight < options.window) {
sessionLogger.info(`Sending message ${sent + 1}/${options.messagecount}`);
if (options.bars) {
metrics.progress.bar.increment();
metrics.window.bar.increment();
}
const pdu = new smpp.PDU("submit_sm", {
source_addr: options.source,
destination_addr: options.destination,
short_message: options.message,
});
sendPdu(session, pdu, sessionLogger, options.longsms)
.then((resp) => {
inFlight--;
sessionLogger.info(`Received response with id ${resp.message_id}`);
success++;
})
.catch((resp) => {
inFlight--;
sessionLogger.warn(`Message failed with id ${resp.message_id}`);
failed++;
});
if (metrics.txMetrics) {
metrics.txMetrics.AddEvent();
}
sent++;
inFlight++;
} else {
sessionLogger.warn(
`${inFlight}/${options.window} messages pending, waiting for a reply before sending more`
);
sendTimer.clearInterval();
setTimeout(() => startInterval(session, sessionLogger, metrics), options.windowsleep);
}
},
"",
`${1 / options.mps} s`
);
}
for (let i = 0; i < options.sessions; i++) {
const sessionLogger = createSessionLogger(options, i);
sessionLogger.info(`Connecting to ${options.host}:${options.port}...`);
const session = smpp.connect(
{
url: `smpp://${options.host}:${options.port}`,
auto_enquire_link_period: 10000,
debug: options.debug,
},
function () {
sessionLogger.info(
`Connected, sending bind_transciever with systemId '${options.systemid}' and password '${options.password}'...`
);
session.on('close', function () {
sessionLogger.error(`Session closed`);
process.exit(1);
});
session.bind_transceiver(
{
system_id: options.systemid,
password: options.password,
},
function (pdu) {
if (pdu.command_status === 0) {
sessionLogger.info(
`Successfully bound, sending ${options.messagecount} messages '${options.source}'->'${options.destination}' ('${options.message}')`
);
const rxMetrics = metricManager.AddMetrics(`Session-${i}-RX`);
const txMetrics = metricManager.AddMetrics(`Session-${i}-TX`);
startInterval(session, sessionLogger, {
rxMetrics,
txMetrics,
});
session.on("deliver_sm", function (pdu) {
if (rxMetrics) {
rxMetrics.AddEvent();
}
sessionLogger.info("Got deliver_sm, replying...");
// setTimeout(() => {
// session.send(pdu.response());
// txMetrics.AddEvent();
// }, 200);
session.send(pdu.response());
if (txMetrics) {
txMetrics.AddEvent();
}
});
session.on("enquire_link", function (pdu) {
session.send(pdu.response());
});
session.on("close", function () {
sessionLogger.error(`Session closed`);
process.exit(1);
});
session.on("error", function (err) {
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 { combine, timestamp, label, printf } = format;
const defaultFormat = printf(({ level, message, timestamp }) => {
return `${timestamp} ${level}: ${message}`;
});
const sessionFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} [Session ${label}] ${level}: ${message}`;
});
function createBaseLogger(options) {
const logger = createLogger({
format: combine(format.colorize({ all: true }), timestamp(), defaultFormat),
transports: [new transports.Console()],
});
const oldInfo = logger.info;
const oldWarn = logger.info;
const oldError = logger.error;
logger.info = function (input) {
if (!shouldLog(options)) {
return;
}
oldInfo(input);
};
logger.error = function (input) {
if (!shouldLog(options)) {
return;
}
oldError(input);
};
logger.warn = function (input) {
if (!shouldLog(options)) {
return;
}
oldWarn(input);
};
return logger;
}
function createSessionLogger(options, ilabel) {
const logger = createLogger({
format: combine(label({ label: ilabel }), format.colorize({ all: true }), timestamp(), sessionFormat),
transports: [new transports.Console()],
});
const oldInfo = logger.info;
const oldWarn = logger.info;
const oldError = logger.error;
logger.info = function (input) {
if (!shouldLog(options)) {
return;
}
oldInfo(input);
};
logger.error = function (input) {
if (!shouldLog(options)) {
return;
}
oldError(input);
};
logger.warn = function (input) {
if (!shouldLog(options)) {
return;
}
oldWarn(input);
};
return logger;
}
function shouldLog(options) {
return options.logs || !options.bars;
}
module.exports = { createBaseLogger, createSessionLogger };
const { createLogger, format, transports } = require("winston");
const { combine, timestamp, label, printf } = format;
const defaultFormat = printf(({ level, message, timestamp }) => {
return `${timestamp} ${level}: ${message}`;
});
const sessionFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} [Session ${label}] ${level}: ${message}`;
});
function createBaseLogger(options) {
const logger = createLogger({
format: combine(format.colorize({ all: true }), timestamp(), defaultFormat),
transports: [new transports.Console()],
});
const oldInfo = logger.info;
const oldWarn = logger.info;
const oldError = logger.error;
logger.info = function (input) {
if (!shouldLog(options)) {
return;
}
oldInfo(input);
};
logger.error = function (input) {
if (!shouldLog(options)) {
return;
}
oldError(input);
};
logger.warn = function (input) {
if (!shouldLog(options)) {
return;
}
oldWarn(input);
};
return logger;
}
function createSessionLogger(options, ilabel) {
const logger = createLogger({
format: combine(label({ label: ilabel }), format.colorize({ all: true }), timestamp(), sessionFormat),
transports: [new transports.Console()],
});
const oldInfo = logger.info;
const oldWarn = logger.info;
const oldError = logger.error;
logger.info = function (input) {
if (!shouldLog(options)) {
return;
}
oldInfo(input);
};
logger.error = function (input) {
if (!shouldLog(options)) {
return;
}
oldError(input);
};
logger.warn = function (input) {
if (!shouldLog(options)) {
return;
}
oldWarn(input);
};
return logger;
}
function shouldLog(options) {
return options.logs || !options.bars;
}
module.exports = { createBaseLogger, createSessionLogger };

View File

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

View File

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

View File

@@ -1,39 +1,39 @@
const { CircularBuffer } = require("./circularBuffer");
class Metric {
constructor(barName, multibar, bufferSize, options, refresh = true) {
this.options = options;
this.multibar = multibar;
this.bar = multibar.create(0, 0);
this.bar.update(0, { name: barName });
this.maxRate = this.options.defaultmaxrate;
this.bar.total = this.maxRate;
this.buffer = new CircularBuffer(bufferSize);
if (refresh) {
setInterval(this.UpdateBar.bind(this), 100);
}
}
AddEvent() {
const timestamp = Date.now();
this.buffer.push({ timestamp, count: 1 });
}
GetRate() {
const entries = this.buffer.toArrayRecent(this.options.metricsinterval);
const totalRX = entries.reduce((sum, entry) => sum + entry.count, 0);
return Math.round((totalRX / this.options.metricsinterval) * 100) / 100;
}
UpdateBar() {
const eps = this.GetRate();
if (eps > this.maxRate) {
this.bar.total = eps;
this.maxRate = eps;
}
this.bar.update(eps);
}
}
module.exports = { Metric };
const { CircularBuffer } = require("./circularBuffer");
class Metric {
constructor(barName, multibar, bufferSize, options, refresh = true) {
this.options = options;
this.multibar = multibar;
this.bar = multibar.create(0, 0);
this.bar.update(0, { name: barName });
this.maxRate = this.options.defaultmaxrate;
this.bar.total = this.maxRate;
this.buffer = new CircularBuffer(bufferSize);
if (refresh) {
setInterval(this.UpdateBar.bind(this), 100);
}
}
AddEvent() {
const timestamp = Date.now();
this.buffer.push({ timestamp, count: 1 });
}
GetRate() {
const entries = this.buffer.toArrayRecent(this.options.metricsinterval);
const totalRX = entries.reduce((sum, entry) => sum + entry.count, 0);
return Math.round((totalRX / this.options.metricsinterval) * 100) / 100;
}
UpdateBar() {
const eps = this.GetRate();
if (eps > this.maxRate) {
this.bar.total = eps;
this.maxRate = eps;
}
this.bar.update(eps);
}
}
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-linux-server": "nexe -i server.js -o out/server-linux -t linux-x64-18.18.2",
"build-windows-server": "nexe -i server.js -o out/server-windows -t windows-x86-18.18.2",
"build": "sh build.sh"
"build": "sh build.sh",
"test": "jest"
},
"keywords": [],
"author": "",
@@ -22,6 +23,8 @@
"winston": "^3.11.0"
},
"devDependencies": {
"eslint": "^8.53.0"
"@types/jest": "^29.5.10",
"eslint": "^8.55.0",
"jest": "^29.7.0"
}
}

2040
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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

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

121
utils.js
View File

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