Comparing version 0.6.0-rc.0 to 0.6.0-rc.1
@@ -50,10 +50,10 @@ var iconv = require('iconv-lite'), | ||
write: function(value, buffer, offset) { | ||
if (!Buffer.isBuffer(value)) { | ||
value = Buffer.from(String(value), 'ascii'); | ||
} | ||
buffer.writeUInt8(value.length, offset++); | ||
if (typeof value == 'string') { | ||
value = Buffer.from(value, 'ascii'); | ||
} | ||
value.copy(buffer, offset); | ||
}, | ||
size: function(value) { | ||
return value.length + 1; | ||
return (value.length || String(value).length) + 1; | ||
}, | ||
@@ -71,4 +71,4 @@ default: '' | ||
write: function(value, buffer, offset) { | ||
if (typeof value == 'string') { | ||
value = Buffer.from(value, 'ascii'); | ||
if (!Buffer.isBuffer(value)) { | ||
value = Buffer.from(String(value), 'ascii'); | ||
} | ||
@@ -79,3 +79,3 @@ value.copy(buffer, offset); | ||
size: function(value) { | ||
return value.length + 1; | ||
return (value.length || String(value).length) + 1; | ||
}, | ||
@@ -82,0 +82,0 @@ default: '' |
253
lib/smpp.js
@@ -7,4 +7,14 @@ var net = require('net'), | ||
PDU = require('./pdu').PDU, | ||
EventEmitter = require('events').EventEmitter; | ||
EventEmitter = require('events').EventEmitter, | ||
proxy = require("findhit-proxywrap").proxy; | ||
var proxyTransport = proxy(net, { | ||
strict: false, | ||
ignoreStrictExceptions: true | ||
}); | ||
var proxyTlsTransport = proxy(tls, { | ||
strict: false, | ||
ignoreStrictExceptions: true | ||
}); | ||
function Session(options) { | ||
@@ -14,3 +24,3 @@ EventEmitter.call(this); | ||
var self = this; | ||
var transport = net; | ||
var clientTransport = net; | ||
var connectTimeout; | ||
@@ -22,3 +32,4 @@ this._extractPDUs = this._extractPDUs.bind(self); | ||
this.remoteAddress = null; | ||
this._proxyProtocolChecked = false; | ||
this.remotePort = null; | ||
this.proxyProtocolProxy = null; | ||
this._busy = false; | ||
@@ -39,17 +50,20 @@ this._callbacks = {}; | ||
if (options.tls) { | ||
transport = tls; | ||
clientTransport = tls; | ||
} | ||
connectTimeout = setTimeout(function() { | ||
if (self.socket) { | ||
var e = new Error("Timeout of " + options.connectTimeout + "ms while connecting to " + | ||
self.options.host + ":" + self.options.port); | ||
e.code = "ETIMEOUT"; | ||
e.timeout = options.connectTimeout; | ||
self.socket.destroy(e); | ||
} | ||
}, options.connectTimeout); | ||
this.socket = transport.connect(this.options); | ||
if (options.hasOwnProperty("connectTimeout") && options.connectTimeout>0) { | ||
connectTimeout = setTimeout(function () { | ||
if (self.socket) { | ||
var e = new Error("Timeout of " + options.connectTimeout + "ms while connecting to " + | ||
self.options.host + ":" + self.options.port); | ||
e.code = "ETIMEOUT"; | ||
e.timeout = options.connectTimeout; | ||
self.socket.destroy(e); | ||
} | ||
}, options.connectTimeout); | ||
} | ||
this.socket = clientTransport.connect(this.options); | ||
this.socket.on('connect', (function() { | ||
clearTimeout(connectTimeout); | ||
self.remoteAddress = self.socket.remoteAddress; | ||
self.remoteAddress = self.rootSocket().remoteAddress; | ||
self.remotePort = self.rootSocket().remotePort; | ||
self.debug("server.connected", "connected to server", {secure: options.tls}); | ||
@@ -67,6 +81,2 @@ self.emit('connect'); // @todo should emmit the session, but it would break BC | ||
} | ||
this.remoteAddress = this.socket.remoteAddress; | ||
this.socket.on('open', function() { | ||
self.debug("socket.open"); | ||
}); | ||
this.socket.on('readable', function() { | ||
@@ -103,2 +113,9 @@ if ( (self.socket.bytesRead - self._prevBytesRead) > 0 ) { | ||
}); | ||
this.rootSocket = (function() { | ||
if (self.socket._parent) return self.socket._parent; | ||
return self.socket; | ||
}); | ||
this.remoteAddress = this.rootSocket().remoteAddress; | ||
this.remotePort = this.rootSocket().remotePort; | ||
this.proxyProtocolProxy = this.rootSocket().proxyAddress ? { address: this.rootSocket().proxyAddress, port: this.rootSocket().proxyPort } : false; | ||
} | ||
@@ -133,2 +150,5 @@ | ||
} | ||
if (this.options.debugListener instanceof Function) { | ||
this.options.debugListener(type, msg, payload); | ||
} | ||
this.emit('debug', type, msg, payload); | ||
@@ -153,6 +173,2 @@ } | ||
try { | ||
if(this._mode === "server" && this.options.enable_proxy_protocol_detection && !this._proxyProtocolChecked) { | ||
this._handleProxyProtocolV1(); | ||
this._proxyProtocolChecked = true; | ||
} | ||
if(!this._command_length) { | ||
@@ -184,49 +200,3 @@ this._command_length = PDU.commandLength(this.socket); | ||
Session.prototype._handleProxyProtocolV1 = function() { | ||
var proxyBuffer = this.socket.read(6); | ||
if (!proxyBuffer) { | ||
return; | ||
} | ||
// Header specs: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt | ||
if (proxyBuffer.toString() === 'PROXY ') { | ||
var proxyProtocol = proxyBuffer.toString(); | ||
var char = null; | ||
var b = null; | ||
var prevChar = null; | ||
while (b = this.socket.read(1)) { | ||
char = b.toString('ascii'); | ||
proxyProtocol += char; | ||
if (char === "\n" && prevChar === "\r") break; | ||
if (proxyProtocol.length > 108) { | ||
this.debug("proxy_protocol.error", "Proxy protocol header cannot exceed 108 bytes", proxyProtocol); | ||
var e = new Error('Proxy protocol header cannot exceed 108 bytes'); | ||
e.code = "PROXY_PROTOCOL_HEADER_TOO_LARGE"; | ||
throw e; | ||
} | ||
prevChar = char; | ||
} | ||
this.debug("proxy_protocol.header.decoded", "Decoded", proxyProtocol.split("\r").join("").split("\n").join("")); | ||
var proxyProtocolAddressFound = null; | ||
if (proxyProtocol.indexOf('PROXY TCP') === 0) { // 'PROXY TCP(4|6) .+\r\n' | ||
var proxyProtocolParts = proxyProtocol.substring(10).split(' '); | ||
if (proxyProtocolParts.length>1) proxyProtocolAddressFound = proxyProtocolParts[1]; | ||
} | ||
if (proxyProtocolAddressFound) { | ||
this.remoteAddress = proxyProtocolAddressFound.toLowerCase(); | ||
this.debug("proxy_protocol.address.ok", "Found "+this.remoteAddress, this.remoteAddress); | ||
} else { | ||
this.debug("proxy_protocol.address.ko", "Not found"); | ||
} | ||
} else { | ||
this.debug("proxy_protocol.header.error","Header mismatch"); | ||
this.socket.unshift(proxyBuffer); | ||
} | ||
} | ||
Session.prototype.send = function(pdu, responseCallback, sendCallback) { | ||
@@ -313,4 +283,6 @@ if (!this.socket.writable) { | ||
function Server(options, listener) { | ||
var self = this; | ||
var self = this, | ||
transport; | ||
this.sessions = []; | ||
this.isProxiedServer = options.isProxiedServer == true; | ||
@@ -329,9 +301,37 @@ if (typeof options == 'function') { | ||
this.tls = options.key && options.cert; | ||
var transport = this.tls ? tls : net; | ||
options.tls = this.tls != null; // standarized option for the session on both client & server | ||
this.options = options; | ||
self.on("proxiedConnection", function(socket) { | ||
// The connection has successfully passed through the proxied server (event emitted by proxywrap) | ||
socket.proxiedConnection = true; | ||
}); | ||
// Fetch the right transport based on the current options | ||
if (this.isProxiedServer) { | ||
transport = this.tls ? proxyTlsTransport : proxyTransport; | ||
} else { | ||
transport = this.tls ? tls : net; | ||
} | ||
transport.Server.call(this, options, function(socket) { | ||
var session = new Session({socket: socket, enable_proxy_protocol_detection: self.options.enable_proxy_protocol_detection, debug: self.options.debug}); | ||
session.debug("client.connected", "client has connected"); | ||
session.server = self; | ||
var session = new Session({ | ||
socket: socket, | ||
tls: self.options.tls, | ||
debug: self.options.debug, | ||
debugListener: self.options.debugListener || null | ||
}); | ||
if (socket.savedEmit) { | ||
// Restore the saved emit to fix the proxywrap bug (on nodejs <=8) | ||
socket.emit = socket.savedEmit; | ||
socket.savedEmit = null; | ||
} | ||
session.debug("client.connected", "client has connected", { | ||
secure: self.options.tls, | ||
// Useful information for Proxy protocol debugging & testing | ||
proxiedServer: self.isProxiedServer, | ||
proxiedConnection: socket.proxiedConnection || (socket._parent ? socket._parent.proxiedConnection : false) || false, | ||
remoteAddress: session.remoteAddress, | ||
remotePort: session.remotePort, | ||
proxyProtocolProxy: session.proxyProtocolProxy, | ||
}); | ||
self.sessions.push(session); | ||
@@ -343,6 +343,22 @@ socket.on('close', function() { | ||
}); | ||
if (this.isProxiedServer) { | ||
// The proxied wrapper clears all connection listeners and adds their own. | ||
// A new listener is added in order to catch socket error on the wrapper. | ||
self.on("connection", function (socket) { | ||
socket.on("error", function (e) { | ||
self.emit("error", e); | ||
}); | ||
if (self.options.autoPrependBuffer) { | ||
// Allows to automatically prepend a buffer on the client socket. This feature is intended only for | ||
// testing purposes and it's used to inject client simulated headers (Proxy protocol) | ||
socket.unshift(self.options.autoPrependBuffer); | ||
} | ||
// There's a bug in the proxywrap server which tampers the emit method in nodejs <= 8 and makes the | ||
// socket unable to emit the events. As a simple fix, save the emit method so it can be restored later. | ||
socket.savedEmit = socket.emit; | ||
}); | ||
} | ||
} | ||
util.inherits(Server, net.Server); | ||
function SecureServer(options, listener) { | ||
@@ -352,16 +368,20 @@ Server.call(this, options, listener); | ||
function ProxyServer(options, listener) { | ||
options.isProxiedServer = true; | ||
Server.call(this, options, listener); | ||
} | ||
function ProxySecureServer(options, listener) { | ||
options.isProxiedServer = true; | ||
Server.call(this, options, listener); | ||
} | ||
// Standard servers without proxy protocol support | ||
util.inherits(Server, net.Server); | ||
util.inherits(SecureServer, tls.Server); | ||
SecureServer.prototype.listen = Server.prototype.listen = function() { | ||
var args = [this.tls ? 3550 : 2775]; | ||
if (typeof arguments[0] == 'function') { | ||
args[1] = arguments[0]; | ||
} else if (arguments.length > 0) { | ||
args = arguments; | ||
} | ||
// Servers with proxy protocol support | ||
util.inherits(ProxyServer, proxyTransport.Server); | ||
util.inherits(ProxySecureServer, proxyTlsTransport.Server); | ||
var transport = this.tls ? tls : net; | ||
return transport.Server.prototype.listen.apply(this, args); | ||
}; | ||
exports.createServer = function(options, listener) { | ||
@@ -376,39 +396,50 @@ if (typeof options == 'function') { | ||
if (options.key && options.cert) { | ||
return new SecureServer(options, listener); | ||
if (options.enable_proxy_protocol_detection) { | ||
return new ProxySecureServer(options, listener); | ||
} else { | ||
return new SecureServer(options, listener); | ||
} | ||
} else { | ||
if (options.enable_proxy_protocol_detection) { | ||
return new ProxyServer(options, listener); | ||
} else { | ||
return new Server(options, listener); | ||
} | ||
} | ||
return new Server(options, listener); | ||
}; | ||
exports.connect = exports.createSession = function(url, listener) { | ||
var options = {}; | ||
exports.connect = exports.createSession = function(options, listener) { | ||
var clientOptions = {}; | ||
if (arguments.length > 1 && typeof listener != 'function') { | ||
options = { | ||
host: url, | ||
clientOptions = { | ||
host: options, | ||
port: listener | ||
}; | ||
listener = arguments[3]; | ||
} else if (typeof url == 'string') { | ||
options = parse(url); | ||
options.host = options.slashes ? options.hostname : url; | ||
options.tls = options.protocol === 'ssmpp:'; | ||
} else if (typeof url == 'function') { | ||
listener = url; | ||
} else if (typeof options == 'string') { | ||
clientOptions = parse(options); | ||
clientOptions.host = clientOptions.slashes ? clientOptions.hostname : options; | ||
clientOptions.tls = clientOptions.protocol === 'ssmpp:'; | ||
} else if (typeof options == 'function') { | ||
listener = options; | ||
} else { | ||
options = url || {}; | ||
if (options.url) { | ||
url = parse(options.url); | ||
options.host = url.hostname; | ||
options.port = url.port; | ||
options.tls = url.protocol === 'ssmpp:'; | ||
clientOptions = options || {}; | ||
if (clientOptions.url) { | ||
options = parse(clientOptions.url); | ||
clientOptions.host = options.hostname; | ||
clientOptions.port = options.port; | ||
clientOptions.tls = options.protocol === 'ssmpp:'; | ||
} | ||
} | ||
options.port = options.port || (options.tls ? 3550 : 2775); | ||
options.debug = options.debug || false; | ||
options.connectTimeout = options.connectTimeout || 30000; | ||
if (clientOptions.tls && !clientOptions.hasOwnProperty("rejectUnauthorized")) { | ||
clientOptions.rejectUnauthorized = false; // Allow self signed certificates by default | ||
} | ||
clientOptions.port = clientOptions.port || (clientOptions.tls ? 3550 : 2775); | ||
clientOptions.debug = clientOptions.debug || false; | ||
clientOptions.connectTimeout = clientOptions.connectTimeout || 30000; | ||
var session = new Session(options); | ||
var session = new Session(clientOptions); | ||
if (listener) { | ||
session.on(options.tls ? 'secureConnect' : 'connect', function() { | ||
session.on(clientOptions.tls ? 'secureConnect' : 'connect', function() { | ||
listener(session); | ||
@@ -415,0 +446,0 @@ }); |
{ | ||
"name": "smpp", | ||
"version": "0.6.0-rc.0", | ||
"version": "0.6.0-rc.1", | ||
"description": "SMPP client and server implementation in node.js", | ||
@@ -34,2 +34,3 @@ "homepage": "https://github.com/farhadi/node-smpp", | ||
"dependencies": { | ||
"findhit-proxywrap": "^0.3.12", | ||
"iconv-lite": "0.x", | ||
@@ -36,0 +37,0 @@ "safer-buffer": ">= 2.1.2 < 3" |
129
README.md
@@ -1,3 +0,3 @@ | ||
node-smpp | ||
========= | ||
# node-smpp | ||
SMPP client and server implementation in node.js. | ||
@@ -10,4 +10,4 @@ | ||
Introduction | ||
------------ | ||
## Introduction | ||
This is a complete implementation of SMPP v5.0 in node.js, with support for | ||
@@ -25,9 +25,10 @@ custom commands and TLVs. | ||
Installation | ||
------------ | ||
## Installation | ||
npm install smpp | ||
``` | ||
npm install smpp | ||
``` | ||
Usage | ||
----- | ||
## Usage | ||
### Creating a SMPP session | ||
@@ -93,8 +94,11 @@ | ||
It's very important to listen for session errors, not listening for error events will terminate the program. | ||
It's very important to listen for session errors, not listening for error events | ||
will terminate the program. | ||
### Debug | ||
To enable a simple debug of ingoing/outgoing messages pass `debug: true` as server/client option. Debug is disabled by default. | ||
Alternatively, you can listen for the `debug` even and write your own implementation: | ||
To enable a simple debug of ingoing/outgoing messages pass `debug: true` as | ||
server/client option. Debug is disabled by default. | ||
Alternatively, you can listen for the `debug` event and write your own implementation: | ||
``` javascript | ||
@@ -106,5 +110,18 @@ session.on('debug', function(type, msg, payload) { | ||
### Handling client connection errors: | ||
In case of errors while trying to connect, an `error` event will be emitted by the session and the program will be terminated if it's not listened. This is how you should check for errors. | ||
A `debugListener` option is also supported: | ||
``` javascript | ||
var options = { | ||
debug: false, | ||
debugListener: function(type, msg, payload) { | ||
console.log({type: type, msg: msg, payload: payload}); | ||
} | ||
} | ||
``` | ||
### Handling client connection errors | ||
In case of errors while trying to connect, an `error` event will be emitted by the session | ||
and the program will be terminated if it's not listened. This is how you should check for | ||
errors. | ||
``` javascript | ||
@@ -123,22 +140,36 @@ session.on('error', function(e) { | ||
### Connection timeout: | ||
### Connection timeout | ||
By default the socket will be dropped after 30000 ms if it doesn't connect. A `connectTimeout` option can be sent when making connections with the server in order to change this setting. | ||
By default the socket will be dropped after 30000 ms if it doesn't connect. | ||
A `connectTimeout` option can be sent when making connections with the server in order | ||
to change this setting. | ||
### Proxy protocol (v1) support : | ||
[Proxy Protocol header specs](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) | ||
### Proxy protocol | ||
Pass `enable_proxy_protocol_detection: true` as server option. | ||
- Only Proxy protocol v1 is supported | ||
- `session.remote_addr` will contain the proxied source ip. | ||
- `session.socket.remote_addr` will contain the proxy ip. | ||
- Even with proxy protocol detection enabled the server will understand non-proxied requests. | ||
- Tests are provided to make sure TCP4, TCP6 & UNKNOWN proxy headers are handled correctly. Tests work by injecting fake proxy protocol headers upon establishing connection. | ||
[Proxy Protocol v1](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) is now | ||
supported as an _experimental_ feature, both for TCP4 and TCP6. | ||
Pass `enable_proxy_protocol_detection: true` as server option to enable proxy protocol | ||
detection, if the option is not provided the feature is completely ignored. | ||
- `session.remoteAddress` will contain the proxied source IP. | ||
- `session.proxyProtocolProxy` will contain the proxy IP. | ||
- Even with proxy protocol detection enabled the server will understand non-proxied | ||
requests. | ||
- Security: Proxy CIDRs validation is yet to be implemented. | ||
- Uses [findhit-proxywrap](https://www.npmjs.com/package/findhit-proxywrap), a third | ||
party library to wrap the net/tls implementations and decode the proxy-protocol | ||
- header before forwarding it to the standard implementations. | ||
Encodings | ||
--------- | ||
#### Compatibility issues | ||
- On proxied, non-tls connections (with Nodejs < v8): Proxywrap shows some misbehaviour | ||
with the way this library inherits from the net server, the socket looses the ability | ||
to emit events. As a minor-fix, the `socket.emit` method is backed up and restored | ||
after the proxying to the net server, making everything work as expected. | ||
## Encodings | ||
This smpp implementation supports 3 encodings: `ASCII` (GSM 03.38), `LATIN1`, and `UCS2`. | ||
Respective data_coding for these encodings are `0x01`, `0x03`, and `0x08`. | ||
data_coding for these encodings are `0x01`, `0x03`, and `0x08` respectively. | ||
@@ -162,4 +193,3 @@ Default encoding for `data_coding:0` is `ASCII`. You can change it as follows: | ||
API | ||
------- | ||
## API | ||
@@ -257,2 +287,3 @@ ### smpp.connect(url, [callback]) | ||
If options include `key` and `cert`, a TLS secured server will be created. | ||
Include `rejectUnauthorized: false` to disable the certificate validation. | ||
@@ -275,3 +306,3 @@ ### smpp.Server | ||
*for other server methods/events documentations see node's `net.Server` docs.* | ||
_for other server methods/events documentations see node's `net.Server` docs._ | ||
@@ -292,7 +323,7 @@ ### smpp.PDU | ||
* For `Integer` parameters (no matter what the length is) you must specify a | ||
- For `Integer` parameters (no matter what the length is) you must specify a | ||
value of type `number` in JavaScript. | ||
* For `Octet-String` and `COctet-String` parameters you can specify either a | ||
- For `Octet-String` and `COctet-String` parameters you can specify either a | ||
`Buffer` or a `String`. | ||
* For the fields that accept SMPP Time Format (`broadcast_end_time`, | ||
- For the fields that accept SMPP Time Format (`broadcast_end_time`, | ||
`schedule_delivery_time`, `validity_period`, `final_date`) you can specify a | ||
@@ -303,3 +334,3 @@ Javascript Date instance which will be automatically converted to a SMPP | ||
converted to '000000000430000R'. | ||
* For `short_message` and `message_payload` fields you can specify a buffer or a | ||
- For `short_message` and `message_payload` fields you can specify a buffer or a | ||
string or an object containing `udh` and `message` properties, while `udh` is a | ||
@@ -311,15 +342,15 @@ buffer and `message` is either a string or a buffer. strings will be | ||
`esm_class` is automatically set if `udh` exists. | ||
* `sm_length` parameter is not needed. It will be automatically set depending on | ||
- `sm_length` parameter is not needed. It will be automatically set depending on | ||
the length of the `short_message`. | ||
* `dest_address` parameter in `submit_multi` operation must be an array of | ||
- `dest_address` parameter in `submit_multi` operation must be an array of | ||
objects containing either `dest_addr_ton`, `dest_addr_npi` and, | ||
`destination_addr` properties or `dl_name` property for SME addresses or | ||
Distribution Lists respectively. | ||
* `unsuccess_sme` parameter in `submit_multi_resp` operation must be an array of | ||
- `unsuccess_sme` parameter in `submit_multi_resp` operation must be an array of | ||
objects containing `dest_addr_ton`, `dest_addr_npi`, `destination_addr` and, | ||
`error_status_code` properties. | ||
* `number_of_dests` and `no_unsuccess` parameters are not needed. They will be | ||
- `number_of_dests` and `no_unsuccess` parameters are not needed. They will be | ||
automatically set depending on the `dest_address` and `unsuccess_sme` parameters | ||
respectively. | ||
* TLV parameters which can be specified multiple times | ||
- TLV parameters which can be specified multiple times | ||
(e.g. `broadcast_area_identifier`), must be specified as an array, even if you | ||
@@ -355,9 +386,17 @@ want to specifiy just one item. | ||
Roadmap | ||
------- | ||
* More test coverage. | ||
* Add some usage examples (e.g client, server, and cluster examples) | ||
## Upgrade notes | ||
License | ||
------- | ||
### upgrade to version 0.6.0 | ||
- Support for Nodejs < v4 (2015) has been dropped due compatibility issues with | ||
findhit-proxywrap library | ||
- Proxy protocol v1 support has been added as an experimental feature. Disabled by default, | ||
will be completely ignored if not enabled. | ||
## Roadmap | ||
- Add some usage examples (e.g client, server, and cluster examples) | ||
## License | ||
node-smpp is released under the MIT license. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
73291
2084
391
3
+ Addedfindhit-proxywrap@^0.3.12
+ Addedfindhit-proxywrap@0.3.13(transitive)
+ Addedlodash@4.17.21(transitive)