Node.js - copy file from FTP A to FTP B by stream - node.js

I want to copy data from FTP A to FTP B.
I use stream instead of the local temp file.
But i cannot read the data from stream.
transformComponent.js
const Transform = require('stream').Transform;
const util = require('util');
const transformComponent = module.exports = function(options) {
if (!(this instanceof transformComponent))
return new Parser(options);
Transform.call(this, options);
};
util.inherits(transformComponent, Transform);
transformComponent.prototype._transform = function(data, encoding, callback) {
this.push(data);
callback();
};
transformComponent.prototype._flush = function(callback) {
callback();
};
ftpComponent.js
const Client = require('ftp');
var ftp = new Client();
var ftpComponent = function(host){
this.host = host;
this.port = 21;
this.user = "";
this.password = "";
};
function connectionProperties(){
let properties ={
host: this.host,
port: this.port
};
if(this.user && this.user!== "")
properties.user = this.user;
if(this.password && this.password !== "")
properties.password = this.password;
return properties;
}
function download(filepath, target){
ftp.on('ready', function () {
ftp.get(filepath, function(err, stream) {
if (err) throw err;
stream.once('close', function() { ftp.end(); });
stream.pipe(target);
});
});
ftp.connect(this.connectionProperties());
}
function upload(source, filename){
ftp.on('ready', function () {
// Upload files to the server:
ftp.put(source, filename, function(err) {
if (err) throw err;
ftp.end();
});
});
ftp.connect(this.connectionProperties());
}
ftpComponent.prototype = {
connectionProperties: connectionProperties,
pull : download,
push : upload
}
module.exports = ftpComponent;
My usage :
const ftp = require('./node_component/ftpComponent.js');
const transform = require('./node_component/transformComponent.js');
var ftpSourceObject = new ftp('host A');
var ftpTargetObject = new ftp('host B');
ftpSourceObject.user = usernameA;
ftpSourceObject.password = passwordA;
ftpTargetObject.user = usernameB;
ftpTargetObject.password = passwordB;
var temp = new transform();
ftpTargetObject.push(temp, 'file-cp.txt');
ftpSourceObject.pull('file.txt', temp);
I can write data into stream from FTP A.
But when reading data from stream and put on FTP B.
It shows the error like below.
Error: Unable to parse PASV server response
at Object.reentry [as cb] (/home/locadmin/fileshareservice/app/node_modules/ftp/lib/connection.js:857:19)
at Parser.<anonymous> (/home/locadmin/fileshareservice/app/node_modules/ftp/lib/connection.js:117:20)
at Parser.emit (events.js:182:13)
at Parser._write (/home/locadmin/fileshareservice/app/node_modules/ftp/lib/parser.js:59:10)
at doWrite (_stream_writable.js:410:12)
at writeOrBuffer (_stream_writable.js:394:5)
at Parser.Writable.write (_stream_writable.js:294:11)
at Socket.ondata (/home/locadmin/fileshareservice/app/node_modules/ftp/lib/connection.js:273:20)
at Socket.emit (events.js:182:13)
at addChunk (_stream_readable.js:283:12)
But when i run just a single line
var temp = new transform();
ftpTargetObject.push(temp, 'file-cp.txt');
temp.write('123');
temp.end();
//ftpSourceObject.pull('file.txt', temp);
It work successful.
How can I pass the stream as parameter to function??
Sorry for my poor English...
My source code reference source is https://bountify.co/node-js-script-for-copying-files-between-two-ftp-locations.

I fix it.
transformComponent.js
const Transform = require('stream').Transform;
const util = require('util');
const transformComponent = module.exports = function(options) {
if (!(this instanceof transformComponent))
return new Parser(options);
Transform.call(this, options);
};
util.inherits(transformComponent, Transform);
transformComponent.prototype._transform = function(data, encoding, callback) {
this.push(data);
callback();
};
transformComponent.prototype._flush = function(callback) {
callback();
};
ftpComponent.js
const Client = require('ftp');
var ftpComponent = function(host){
this.host = host;
this.port = 21;
this.user = "";
this.password = "";
};
function connectionProperties(){
let properties ={
host: this.host,
port: this.port
};
if(this.user && this.user!== "")
properties.user = this.user;
if(this.password && this.password !== "")
properties.password = this.password;
return properties;
}
function download(filepath, target){
var ftp = new Client();
ftp.on('ready', function () {
ftp.get(filepath, function(err, stream) {
if (err) throw err;
stream.once('close', function() { ftp.end(); });
stream.pipe(target);
});
});
ftp.connect(this.connectionProperties());
}
function upload(source, filename){
var ftp = new Client();
ftp.on('ready', function () {
// Upload files to the server:
ftp.put(source, filename, function(err) {
if (err) throw err;
ftp.end();
});
});
ftp.connect(this.connectionProperties());
}
ftpComponent.prototype = {
connectionProperties: connectionProperties,
pull : download,
push : upload
}
module.exports = ftpComponent;
My usage :
const ftp = require('./node_component/ftpComponent.js');
const transform = require('./node_component/transformComponent.js');
var ftpSourceObject = new ftp('host A');
var ftpTargetObject = new ftp('host B');
ftpSourceObject.user = usernameA;
ftpSourceObject.password = passwordA;
ftpTargetObject.user = usernameB;
ftpTargetObject.password = passwordB;
var temp = new transform();
ftpTargetObject.push(temp, 'file-cp.txt');
ftpSourceObject.pull('file.txt', temp);

Related

How to call another prototype async function from another in the same class

I have one async prototype function and that is called from another async prototype function. But, I got the error below. How to resolve?
I use node.js v14.1.0.
UnhandledPromiseRejectionWarning: TypeError: this.saySomething2 is not a function
const Person = function() {
console.log("CALLED PERSON");
};
Person.prototype.saySomething = async function() {
this.saySomething2();
};
Person.prototype.saySomething2 = async function() {
//do something
console.log("hello");
};
(async function() {
var ape = new Person();
await ape.saySomething();
}());
update: adding actual code.
// constructor function for the KingInfo class
function KingInfo(
date,
employeeKey,
workDayTypeName) {
this._date = date;
this._employeeKey = employeeKey;
this._workDayTypeName = workDayTypeName;
}
KingInfo.prototype.sync = async function(){
var isSucceed = false;
let syncType = await this.getSyncType();
if(syncType === "post"){
}
else if(syncType === "patch"){
}
else{
}
return isSucceed;
}
//#return: "post", "patch", "none"
KingInfo.prototype.getSyncType = async function(){
let syncType = "none";
let sql = [];
sql.push("SELECT count(*) as count");
...
let records = await db_module.query(sql.join(""));
let count = records[0].count;
if(count <= 0){
syncType = "post";
}
else{
let isSame = await this.compareToDataInDB();
if(!isSame){
syncType = "patch";
}
}
return syncType;
}
KingInfo.prototype.compareToDataInDB = async function(){
let date = this._date;
let empKey = this._employeeKey;
let sql = [];
sql.push("SELECT workDayTypeName");
...
let records = await db_module.query(sql.join(""));
let record = records[0];
let res = false;
if(workDayTypeName === record.workDayTypeName){
res = true;
}
return res;
}
(async function(){
let date = "2020-08-01";
let employeeKey = "";
let workDayTypeName = "aaaaa";
let kingInfo = new KingInfo(
date,
employeeKey,
workDayTypeName);
kingInfo.sync();
}());
module.exports = {
KingInfo: KingInfo
}
I got this error:
UnhandledPromiseRejectionWarning: TypeError: Cannot read property '_date' of undefined
UnhandledPromiseRejectionWarning: TypeError: this.compareToDataInDB is not a function
update:
adding code for db module.
this module might effect badlly?
db.js
"use strict";
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
database: 'something'
});
function query(sql){
return new Promise(function(resolve, reject) {
connection.connect((err) => {
if (err) throw err;
connection.query(sql, function (err, result, fields) {
if (err) throw err;
resolve(result);
});
});
});
}
module.exports = {
query: query
}

Not able to mock S3 download request using proxyquire

const awsSdk = require('aws-sdk');
module.exports = {
downloadFile(bucketName, fileName, callback) {
var s3 = new awsSdk.S3();
var params = { Bucket: bucketName, Key: fileName };
var file = require('fs').createWriteStream('abcd.jpg');
var stream = s3.getObject(params).on('error', function (err) {
console.log(' download.on');
callback('error', null);
})
.createReadStream()
stream.pipe(file);
}
}
Above, is the code i need to mock in my Test class using proxyquire. The issue is, im not able to mock methods like getObject,on,createReadStream. Also after mocking i need to emit the error event from test class that will trigger the above on method which will call the callback in my test class where exception is tested.
Below is my test class code.
const proxyquire = require('proxyquire').noCallThru().noPreserveCache();
const sinon = require('sinon');
const emitter = require('events').EventEmitter;
function Download() {
emitter.call(this);
this.error = function () {
this.emit('error');
}
}
describe('S3 download test', () => {
awsSdkMock = {}
awsSdkMock.config = {};
awsSdkMock.config.region = { };
var s3Instance = {};
awsSdkMock.S3 = function (){
return s3Instance;
};
var object = {};
s3Instance.getObject = function (params){
return object;
};
var request = {};
const errorCallBack = sinon.spy();
var err = 'error';
object.on = function (err, errorCallBack) {
console.log('object.on');
errorCallBack();
return request;
};
var stream = {};
request.createReadStream = function(){
console.log('createReadStream');
return stream
};
stream.pipe = function(file){
console.log('download.error');
// download.error();
};
Download.prototype.__proto__ = emitter.prototype;
var download = new Download();
const s3 = proxyquire('./../../../modules/s3', {
'aws-sdk': awsSdkMock
})
it('Error in download test', (done) => {
const errorCallBack = sinon.spy();
s3.downloadFile('123', 'abcd', errorCallBack);
sinon.assert.calledWith(errorCallBack, 'error', null);
done();
});
})
Any help is appreciated.

TTS with a websocket:Creating a WAV from binary data fails

I try to create a WAV from binary data using the websocket connection on a node.js server. I use the BluemixTTS to create the speech. Here is my current code:
'use strict';
const WebSocket = require('ws');
var express = require('express');
var watson = require('watson-developer-cloud');
var vcapServices = require('vcap_services');
var extend = (extend = require('util')._extend);
var fs = require('fs');
var ttsConfig = extend(
{
version: 'v1',
url: 'https://stream.watsonplatform.net/text-to-speech/api',
username: 'myusernamehere',
password: "mypasswordhere"
},
vcapServices.getCredentials('text_to_speech')
);
var ttsAuthService = watson.authorization(ttsConfig);
var websocket;
ttsAuthService.getToken({ url: ttsConfig.url }, function(err, token) {
if (err) {
console.log('Error retrieving token: ', err);
return;
}
var voice = 'en-US_AllisonVoice';
var wsURI = 'wss://stream.watsonplatform.net/text-to-speech/api/v1/synthesize?voice=' +
voice + '&watson-token=' + token;
websocket = new WebSocket(wsURI);
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
});
function onOpen(evt) {
var message = {
text: 'Hello world',
accept: 'audio/wav',
timings: ['words']
};
websocket.send(JSON.stringify(message));
}
var messages;
var audioStream = null;
function onMessage(evt) {
if (typeof evt.data === 'string') {
messages += evt.data;
} else {
if(audioStream == null){
audioStream = evt.data;
}else{
audioStream += evt.data;
}
}
}
function onClose(evt) {
console.log(messages);
var wstream = fs.createWriteStream('test.wav');
wstream.write((audioStream));
wstream.end();
}
function onError(evt) {
}
I get the token, trigger the TTS specifying audio/wav, build my buffer in the onMessage method and then write it into a .wav file. Everything seems fine.
However the file is somehow broken, and cannot be opened with any music-player. Do I miss some special encoding ?
Any help is appreciated
Regards,
Rambazamba
As the data contains a buffer one has to write the buffer in the file directly each time you get a message and then close the file stream in the onClose event. Like this:
var messages;
var wstream = fs.createWriteStream('test.wav');
function onMessage(evt) {
if (typeof evt.data === 'string') {
messages += evt.data;
} else {
wstream.write(evt.data)
}
}
function onClose(evt) {
console.log(messages);
wstream.end();
}
function onError(evt) {
}

WebSocket connection to 'ws://localhost:3434/' failed: Connection closed before receiving a handshake response

When using the cmd to run the node server.js and launch the localhost:3434 in the web browser I am getting the error:
in Chrome:
Connection closed before receiving a handshake response.
in firefox:
Firefox can't establish a connection to the server at ws://localhost:3434/
Could you pls help
server.js
var http = require('http'),
fs = require('fs');
fs.readFile('../index.html', function (err, html) {
if (err) {
throw err;
}
http.createServer(function (request, response) {
response.writeHeader(200, { "Content-Type": "text/html" });
response.write(html);
response.end();
}).listen(3434);
});
index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!--<script type="text/javascript" src="../../js/adapter.js"></script>-->
<!--<script type="text/javascript" src="../../js/webrtc.js"></script>-->
<title>Simple WebRTC video chat</title>
</head>
<body>
<header>
<video id="localVideo" autoplay="autoplay" muted="muted" style="width:40%;"></video>
<video id="remoteVideo" autoplay="autoplay" style="width:40%;"></video>
<input type="button" id="start" onclick="start(true)" value="Start Video"; />
</header>
<script>
var RTCPeerConnection = null;
var getUserMedia = null;
var attachMediaStream = null;
var reattachMediaStream = null;
var webrtcDetectedBrowser = null;
var webrtcDetectedVersion = null;
function trace(text) {
// This function is used for logging.
if (text[text.length - 1] == '\n') {
text = text.substring(0, text.length - 1);
}
console.log((performance.now() / 1000).toFixed(3) + ": " + text);
}
if (navigator.mozGetUserMedia) {
console.log("This appears to be Firefox");
webrtcDetectedBrowser = "firefox";
webrtcDetectedVersion =
parseInt(navigator.userAgent.match(/Firefox\/([0- 9]+)\./)[1]);
// The RTCPeerConnection object.
RTCPeerConnection = mozRTCPeerConnection;
// The RTCSessionDescription object.
RTCSessionDescription = mozRTCSessionDescription;
// The RTCIceCandidate object.
RTCIceCandidate = mozRTCIceCandidate;
// Get UserMedia (only difference is the prefix).
getUserMedia = navigator.mozGetUserMedia.bind(navigator);
// Creates iceServer from the url for FF.
createIceServer = function (url, username, password) {
var iceServer = null;
var url_parts = url.split(':');
if (url_parts[0].indexOf('stun') === 0) {
// Create iceServer with stun url.
iceServer = { 'url': url };
} else if (url_parts[0].indexOf('turn') === 0 &&
(url.indexOf('transport=udp') !== -1 ||
url.indexOf('?transport') === -1)) {
// Create iceServer with turn url.
// Ignore the transport parameter from TURN url.
var turn_url_parts = url.split("?");
iceServer = {
'url': turn_url_parts[0],
'credential': password,
'username': username
};
}
return iceServer;
};
// Attach a media stream to an element.
attachMediaStream = function (element, stream) {
console.log("Attaching media stream");
element.mozSrcObject = stream;
element.play();
};
reattachMediaStream = function (to, from) {
console.log("Reattaching media stream");
to.mozSrcObject = from.mozSrcObject;
to.play();
};
MediaStream.prototype.getVideoTracks = function () {
return [];
};
MediaStream.prototype.getAudioTracks = function () {
return [];
};
} else if (navigator.webkitGetUserMedia) {
console.log("This appears to be Chrome");
webrtcDetectedBrowser = "chrome";
webrtcDetectedVersion =
parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]);
// Creates iceServer from the url for Chrome.
createIceServer = function (url, username, password) {
var iceServer = null;
var url_parts = url.split(':');
if (url_parts[0].indexOf('stun') === 0) {
// Create iceServer with stun url.
iceServer = { 'url': url };
} else if (url_parts[0].indexOf('turn') === 0) {
if (webrtcDetectedVersion < 28) {
var url_turn_parts = url.split("turn:");
iceServer = {
'url': 'turn:' + username + '#' + url_turn_parts[1],
'credential': password
};
} else {
iceServer = {
'url': url,
'credential': password,
'username': username
};
}
}
return iceServer;
};
// The RTCPeerConnection object.
RTCPeerConnection = webkitRTCPeerConnection;
// Get UserMedia (only difference is the prefix).
// Code from Adam Barth.
getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
// Attach a media stream to an element.
attachMediaStream = function (element, stream) {
if (typeof element.srcObject !== 'undefined') {
element.srcObject = stream;
} else if (typeof element.mozSrcObject !== 'undefined') {
element.mozSrcObject = stream;
} else if (typeof element.src !== 'undefined') {
element.src = URL.createObjectURL(stream);
} else {
console.log('Error attaching stream to element.');
}
};
reattachMediaStream = function (to, from) {
to.src = from.src;
};
// The representation of tracks in a stream is changed in M26.
// Unify them for earlier Chrome versions in the coexisting period.
if (!webkitMediaStream.prototype.getVideoTracks) {
webkitMediaStream.prototype.getVideoTracks = function () {
return this.videoTracks;
};
webkitMediaStream.prototype.getAudioTracks = function () {
return this.audioTracks;
};
}
if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
webkitRTCPeerConnection.prototype.getLocalStreams = function () {
return this.localStreams;
};
webkitRTCPeerConnection.prototype.getRemoteStreams = function () {
return this.remoteStreams;
};
}
} else {
console.log("Browser does not appear to be WebRTC-capable");
}
var localVideo;
var remoteVideo;
var peerConnection;
var localStream;
var optional = { optional: [{ DtlsSrtpKeyAgreement: true }] }
var peerConnectionConfig = { 'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stun.l.google.com:19302' }] };
navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
localVideo = document.getElementById("localVideo");
remoteVideo = document.getElementById("remoteVideo");
serverConnection = new WebSocket('ws://localhost:3434');
serverConnection.onmessage = gotMessageFromServer;
var constraints = {
video: true,
audio: true,
};
if (navigator.getUserMedia) {
navigator.getUserMedia(constraints, getUserMediaSuccess, getUserMediaError);
} else {
alert('Your browser does not support getUserMedia API');
}
function getUserMediaSuccess(stream) {
localStream = stream;
localVideo.src = window.URL.createObjectURL(stream);
}
function start(isCaller) {
peerConnection = new RTCPeerConnection(peerConnectionConfig, optional);
peerConnection.onicecandidate = gotIceCandidate;
peerConnection.onaddstream = gotRemoteStream;
peerConnection.addStream(localStream);
if (isCaller) {
peerConnection.createOffer(gotDescription, createOfferError);
}
}
function gotMessageFromServer(message) {
if (!peerConnection) start(false);
var signal = JSON.parse(message.data);
if (signal.sdp) {
peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp), function () {
peerConnection.createAnswer(gotDescription, createAnswerError);
});
} else if (signal.ice) {
peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice));
}
}
function gotIceCandidate(event) {
if (event.candidate != null) {
serverConnection.send(JSON.stringify({ 'ice': event.candidate }));
}
}
function gotDescription(description) {
console.log('got description');
peerConnection.setLocalDescription(description, function () {
serverConnection.send(JSON.stringify({ 'sdp': description }));
}, function () { console.log('set description error') });
}
function gotRemoteStream(event) {
console.log("got remote stream");
remoteVideo.src = window.URL.createObjectURL(event.stream);
}
// Error functions....
function getUserMediaError(error) {
console.log(error);
}
function createOfferError(error) {
console.log(error);
}
function createAnswerError(error) {
console.log(error);
}
</script>
</body>
</html>
In order to handle websocket connections your http-server should handle 'upgrade'-event (see here - https://nodejs.org/api/http.html#http_event_upgrade_1)
For example, here is the working example (it uses ws-module):
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({noServer: true});
var http = require('http'),
fs = require('fs');
fs.readFile('../index.html', function (err, html) {
if (err) {
throw err;
}
var server = http.createServer(function (request, response) {
response.writeHeader(200, { "Content-Type": "text/html" });
response.write(html);
response.end();
})
server.listen(3434);
server.on('upgrade', wss.handleUpgrade);
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
ws.send('something');
});
});
Here are docs for ws - https://github.com/websockets/ws

Implementing STARTTLS in a protocol in NodeJS

I'm trying to add a STARTTLS upgrade to an existing protocol (which currently works in plaintext).
As a start, I'm using a simple line-based echoing server (it's a horrible kludge with no error handling or processing of packets into lines - but it usually just works as the console sends a line-at-a-time to stdin).
I think my server is right, but both ends exit with identical errors when I type starttls:
events.js:72
throw er; // Unhandled 'error' event
^
Error: 139652888721216:error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol:../deps/openssl/openssl/ssl/s23_clnt.c:766:
at SlabBuffer.use (tls.js:232:18)
at CleartextStream.read [as _read] (tls.js:450:29)
at CleartextStream.Readable.read (_stream_readable.js:320:10)
at EncryptedStream.write [as _write] (tls.js:366:25)
at doWrite (_stream_writable.js:221:10)
at writeOrBuffer (_stream_writable.js:211:5)
at EncryptedStream.Writable.write (_stream_writable.js:180:11)
at Socket.ondata (stream.js:51:26)
at Socket.EventEmitter.emit (events.js:95:17)
at Socket.<anonymous> (_stream_readable.js:746:14)
Have I completely misunderstood how to do an upgrade on the client side?
Currently, I'm using the same method to add TLS-ness to plain streams at each end. This feels wrong, as both client and server will be trying to play the same role in the negotiation.
tlsserver.js:
r tls = require('tls');
var net = require('net');
var fs = require('fs');
var options = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
// This is necessary only if using the client certificate authentication.
requestCert: true,
// This is necessary only if the client uses the self-signed certificate.
ca: [ fs.readFileSync('client-cert.pem') ],
rejectUnauthorized: false
};
var server = net.createServer(function(socket) {
socket.setEncoding('utf8');
socket.on('data', function(data) {
console.log('plain data: ', data);
// FIXME: this is not robust, it should be processing the stream into lines
if (data.substr(0, 8) === 'starttls') {
console.log('server starting TLS');
//socket.write('server starting TLS');
socket.removeAllListeners('data');
options.socket = socket;
sec_socket = tls.connect(options, (function() {
sec_socket.on('data', function() {
console.log('secure data: ', data);
});
return callback(null, true);
}).bind(this));
} else {
console.log('plain data', data);
}
});
});
server.listen(9999, function() {
console.log('server bound');
});
client.js:
var tls = require('tls');
var fs = require('fs');
var net = require('net');
var options = {
// These are necessary only if using the client certificate authentication
key: fs.readFileSync('client-key.pem'),
cert: fs.readFileSync('client-cert.pem'),
// This is necessary only if the server uses the self-signed certificate
ca: [ fs.readFileSync('server-cert.pem') ],
rejectUnauthorized: false
};
var socket = new net.Socket();
var sec_socket = undefined;
socket.setEncoding('utf8');
socket.on('data', function(data) {
console.log('plain data:', data);
});
socket.connect(9999, function() {
process.stdin.setEncoding('utf8');
process.stdin.on('data', function(data) {
if (!sec_socket) {
console.log('sending plain:', data);
socket.write(data);
} else {
console.log('sending secure:', data);
sec_socket.write(data);
}
if (data.substr(0, 8) === 'starttls') {
console.log('client starting tls');
socket.removeAllListeners('data');
options.socket = socket;
sec_socket = tls.connect(options, (function() {
sec_socket.on('data', function() {
console.log('secure data: ', data);
});
return callback(null, true);
}).bind(this));
}
});
});
Got it working, thanks to Matt Seargeant's answer. My code now looks like:
server.js:
var ts = require('./tls_socket');
var fs = require('fs');
var options = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
// This is necessary only if using the client certificate authentication.
requestCert: false,
// This is necessary only if the client uses the self-signed certificate.
ca: [ fs.readFileSync('client-cert.pem') ],
rejectUnauthorized: false
};
var server = ts.createServer(function(socket) {
console.log('connected');
socket.on('data', function(data) {
console.log('data', data);
if (data.length === 9) {
console.log('upgrading to TLS');
socket.upgrade(options, function() {
console.log('upgraded to TLS');
});
}
});
});
server.listen(9999);
client.js:
var ts = require('./tls_socket');
var fs = require('fs');
var crypto = require('crypto');
var options = {
// These are necessary only if using the client certificate authentication
key: fs.readFileSync('client-key.pem'),
cert: fs.readFileSync('client-cert.pem'),
// This is necessary only if the server uses the self-signed certificate
ca: [ fs.readFileSync('server-cert.pem') ],
rejectUnauthorized: false
};
var socket = ts.connect(9999, 'localhost', function() {
console.log('secured');
});
process.stdin.on('data', function(data) {
console.log('sending:', data);
socket.write(data);
if (data.length === 9) {
socket.upgrade(options);
}
});
tls_socket.js:
"use strict";
/*----------------------------------------------------------------------------------------------*/
/* Obtained and modified from http://js.5sh.net/starttls.js on 8/18/2011. */
/*----------------------------------------------------------------------------------------------*/
var tls = require('tls');
var crypto = require('crypto');
var util = require('util');
var net = require('net');
var stream = require('stream');
var SSL_OP_ALL = require('constants').SSL_OP_ALL;
// provides a common socket for attaching
// and detaching from either main socket, or crypto socket
function pluggableStream(socket) {
stream.Stream.call(this);
this.readable = this.writable = true;
this._timeout = 0;
this._keepalive = false;
this._writeState = true;
this._pending = [];
this._pendingCallbacks = [];
if (socket)
this.attach(socket);
}
util.inherits(pluggableStream, stream.Stream);
pluggableStream.prototype.pause = function () {
if (this.targetsocket.pause) {
this.targetsocket.pause();
this.readable = false;
}
}
pluggableStream.prototype.resume = function () {
if (this.targetsocket.resume) {
this.readable = true;
this.targetsocket.resume();
}
}
pluggableStream.prototype.attach = function (socket) {
var self = this;
self.targetsocket = socket;
self.targetsocket.on('data', function (data) {
self.emit('data', data);
});
self.targetsocket.on('connect', function (a, b) {
self.emit('connect', a, b);
});
self.targetsocket.on('secureConnection', function (a, b) {
self.emit('secureConnection', a, b);
self.emit('secure', a, b);
});
self.targetsocket.on('secure', function (a, b) {
self.emit('secureConnection', a, b);
self.emit('secure', a, b);
});
self.targetsocket.on('end', function () {
self.writable = self.targetsocket.writable;
self.emit('end');
});
self.targetsocket.on('close', function (had_error) {
self.writable = self.targetsocket.writable;
self.emit('close', had_error);
});
self.targetsocket.on('drain', function () {
self.emit('drain');
});
self.targetsocket.on('error', function (exception) {
self.writable = self.targetsocket.writable;
self.emit('error', exception);
});
self.targetsocket.on('timeout', function () {
self.emit('timeout');
});
if (self.targetsocket.remotePort) {
self.remotePort = self.targetsocket.remotePort;
}
if (self.targetsocket.remoteAddress) {
self.remoteAddress = self.targetsocket.remoteAddress;
}
};
pluggableStream.prototype.clean = function (data) {
if (this.targetsocket && this.targetsocket.removeAllListeners) {
this.targetsocket.removeAllListeners('data');
this.targetsocket.removeAllListeners('secureConnection');
this.targetsocket.removeAllListeners('secure');
this.targetsocket.removeAllListeners('end');
this.targetsocket.removeAllListeners('close');
this.targetsocket.removeAllListeners('error');
this.targetsocket.removeAllListeners('drain');
}
this.targetsocket = {};
this.targetsocket.write = function () {};
};
pluggableStream.prototype.write = function (data, encoding, callback) {
if (this.targetsocket.write) {
return this.targetsocket.write(data, encoding, callback);
}
return false;
};
pluggableStream.prototype.end = function (data, encoding) {
if (this.targetsocket.end) {
return this.targetsocket.end(data, encoding);
}
}
pluggableStream.prototype.destroySoon = function () {
if (this.targetsocket.destroySoon) {
return this.targetsocket.destroySoon();
}
}
pluggableStream.prototype.destroy = function () {
if (this.targetsocket.destroy) {
return this.targetsocket.destroy();
}
}
pluggableStream.prototype.setKeepAlive = function (bool) {
this._keepalive = bool;
return this.targetsocket.setKeepAlive(bool);
};
pluggableStream.prototype.setNoDelay = function (/* true||false */) {
};
pluggableStream.prototype.setTimeout = function (timeout) {
this._timeout = timeout;
return this.targetsocket.setTimeout(timeout);
};
function pipe(pair, socket) {
pair.encrypted.pipe(socket);
socket.pipe(pair.encrypted);
pair.fd = socket.fd;
var cleartext = pair.cleartext;
cleartext.socket = socket;
cleartext.encrypted = pair.encrypted;
cleartext.authorized = false;
function onerror(e) {
if (cleartext._controlReleased) {
cleartext.emit('error', e);
}
}
function onclose() {
socket.removeListener('error', onerror);
socket.removeListener('close', onclose);
}
socket.on('error', onerror);
socket.on('close', onclose);
return cleartext;
}
function createServer(cb) {
var serv = net.createServer(function (cryptoSocket) {
var socket = new pluggableStream(cryptoSocket);
socket.upgrade = function (options, cb) {
console.log("Upgrading to TLS");
socket.clean();
cryptoSocket.removeAllListeners('data');
// Set SSL_OP_ALL for maximum compatibility with broken clients
// See http://www.openssl.org/docs/ssl/SSL_CTX_set_options.html
if (!options) options = {};
// TODO: bug in Node means we can't do this until it's fixed
// options.secureOptions = SSL_OP_ALL;
var sslcontext = crypto.createCredentials(options);
var pair = tls.createSecurePair(sslcontext, true, true, false);
var cleartext = pipe(pair, cryptoSocket);
pair.on('error', function(exception) {
socket.emit('error', exception);
});
pair.on('secure', function() {
var verifyError = (pair.ssl || pair._ssl).verifyError();
console.log("TLS secured.");
if (verifyError) {
cleartext.authorized = false;
cleartext.authorizationError = verifyError;
} else {
cleartext.authorized = true;
}
var cert = pair.cleartext.getPeerCertificate();
if (pair.cleartext.getCipher) {
var cipher = pair.cleartext.getCipher();
}
socket.emit('secure');
if (cb) cb(cleartext.authorized, verifyError, cert, cipher);
});
cleartext._controlReleased = true;
socket.cleartext = cleartext;
if (socket._timeout) {
cleartext.setTimeout(socket._timeout);
}
cleartext.setKeepAlive(socket._keepalive);
socket.attach(socket.cleartext);
};
cb(socket);
});
return serv;
}
if (require('semver').gt(process.version, '0.7.0')) {
var _net_connect = function (options) {
return net.connect(options);
}
}
else {
var _net_connect = function (options) {
return net.connect(options.port, options.host);
}
}
function connect(port, host, cb) {
var options = {};
if (typeof port === 'object') {
options = port;
cb = host;
}
else {
options.port = port;
options.host = host;
}
var cryptoSocket = _net_connect(options);
var socket = new pluggableStream(cryptoSocket);
socket.upgrade = function (options) {
socket.clean();
cryptoSocket.removeAllListeners('data');
// Set SSL_OP_ALL for maximum compatibility with broken servers
// See http://www.openssl.org/docs/ssl/SSL_CTX_set_options.html
if (!options) options = {};
// TODO: bug in Node means we can't do this until it's fixed
// options.secureOptions = SSL_OP_ALL;
var sslcontext = crypto.createCredentials(options);
var pair = tls.createSecurePair(sslcontext, false);
socket.pair = pair;
var cleartext = pipe(pair, cryptoSocket);
pair.on('error', function(exception) {
socket.emit('error', exception);
});
pair.on('secure', function() {
var verifyError = (pair.ssl || pair._ssl).verifyError();
console.log("client TLS secured.");
if (verifyError) {
cleartext.authorized = false;
cleartext.authorizationError = verifyError;
} else {
cleartext.authorized = true;
}
if (cb) cb();
socket.emit('secure');
});
cleartext._controlReleased = true;
socket.cleartext = cleartext;
if (socket._timeout) {
cleartext.setTimeout(socket._timeout);
}
cleartext.setKeepAlive(socket._keepalive);
socket.attach(socket.cleartext);
console.log("client TLS upgrade in progress, awaiting secured.");
};
return (socket);
}
exports.connect = connect;
exports.createConnection = connect;
exports.Server = createServer;
exports.createServer = createServer;
tls.connect() doesn't support the server doing the upgrade unfortunately.
You have to use code similar to how Haraka does it - basically creating your own shim using a SecurePair.
See here for the code we use: https://github.com/baudehlo/Haraka/blob/master/tls_socket.js#L171
STARTTLS Client-Server is trivial to implement in node, but it be a little buggy so, we need to go around.
For Server of STARTTLS we need do it:
// sock come from: net.createServer(function(sock) { ... });
sock.removeAllListeners('data');
sock.removeAllListeners('error');
sock.write('220 Go ahead' + CRLF);
sock = new tls.TLSSocket(sock, { secureContext : tls.createSecureContext({ key: cfg.stls.key, cert: cfg.stls.cert }), rejectUnauthorized: false, isServer: true });
sock.setEncoding('utf8');
// 'secureConnect' event is buggy :/ we need to use 'secure' here.
sock.on('secure', function() {
// STARTTLS is done here. sock is a secure socket in server side.
sock.on('error', parseError);
sock.on('data', parseData);
});
For Client of STARTTLS we need do it:
// sock come from: net.connect(cfg.port, curr.exchange);
// here we already read the '220 Go ahead' from the server.
sock.removeAllListeners('data');
sock.removeAllListeners('error');
sock = tls.connect({ socket: sock, secureContext : tls.createSecureContext({ key: cfg.stls.key, cert: cfg.stls.cert }), rejectUnauthorized: false });
sock.on('secureConnect', function() {
// STARTTLS is done here. sock is a secure socket in client side.
sock.on('error', parseError);
sock.on('data', parseData);
// Resend the helo message.
sock.write(helo);
});

Resources