Laravel IO Socket and redis over SSL https connection - laravel

Currently have IO sockets with laravel broadcasting with redis working perfectly. Until I then set up an SSL cert on the domain.
I have redis-server running on port 3001.
Then there is a socket.js set to listen to 3000.
My JS on the page I listen via io('//{{ $listen }}:3000').
Any guidance would be great on how to get this working over https. Would I just use 443 as the port?
Thanks.
My socket.js
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var Redis = require('ioredis');
var redis = new Redis();
redis.subscribe('notifications', function(err, count) {
});
redis.on('message', function(channel, message) {
console.log('Message Recieved: ' + message);
message = JSON.parse(message);
io.emit(channel + ':' + message.event, message.data);
});
http.listen(3000, function(){
console.log('Listening on Port 3000');
});

First, setup your serverOptions object:
var serverOptions = {
port: 3000,
host: 127.0.0.1, //address to your site
key: '/etc/nginx/ssl/your_site/server.key', //Or whatever the path to your SSL is
cert: '/etc/nginx/ssl/your_site/server.crt',
NPNProtocols: ['http/2.0', 'spdy', 'http/1.1', 'http/1.0']
}
For the NPNProtocols, you may not care for all of them, but they're provided for reference.
Now just create the server:
var app = require('https').createServer(serverOptions),
io = require('socket.io')(app);
This should be pretty plug and play into your source at his point.\
As a side note your stuff is completely wide open and anyone can listen on your web socket, so nothing should be sent that is private through here. If you need to make your data private, then you're going to need 1 of two things;
Something like JWT-Auth Token
Something custom that interface with the Redis queue:
Here's an example of the latter:
var SECRET_KEY = '<YOUR_LARAVEL_SECRET_KEY>';
var laravel_session_parser = {
ord: function (string) {
return string.charCodeAt(0);
},
decryptSession: function (cookie, secret) {
if (cookie) {
var session_cookie = JSON.parse(new Buffer(cookie, 'base64'));
var iv = new Buffer(session_cookie.iv, 'base64');
var value = new Buffer(session_cookie.value, 'base64');
var rijCbc = new mcrypt.MCrypt('rijndael-128', 'cbc');
rijCbc.open(secret, iv);
var decrypted = rijCbc.decrypt(value).toString();
var len = decrypted.length - 1;
var pad = laravel_session_parser.ord(decrypted.charAt(len));
return phpunserialize.unserialize(decrypted.substr(0, decrypted.length - pad));
}
return null;
},
getUidFromObj: function (obj, pattern) {
var regexp = /login_web_([a-zA-Z0-9]+)/gi;
if (pattern) {
regexp = pattern;
}
var u_id = null;
for (var key in obj) {
var matches_array = key.match(regexp);
if (matches_array && matches_array.length > 0) {
u_id = obj[matches_array[0]];
return u_id;
}
}
return u_id;
},
getRedisSession: function (s_id, cb) {
var _sessionId = 'laravel:' + s_id;
client.get(_sessionId, function (err, session) {
if (err) {
cb && cb(err);
return;
}
cb && cb(null, session);
});
},
getSessionId: function (session, _callback) {
var u_id = null,
err = null;
try {
var laravelSession = phpunserialize.unserialize(phpunserialize.unserialize(session));
u_id = laravel_session_parser.getUidFromObj(laravelSession);
} catch (err) {
_callback(err, null);
}
_callback(err, u_id);
},
ready: function (socket, _callback) {
if (typeof socket.handshake.headers.cookie === 'string') {
var cookies = cookie.parse(socket.handshake.headers.cookie);
var laravel_session = cookies.laravel_session;
var session_id = laravel_session_parser.decryptSession(laravel_session, SECRET_KEY);
laravel_session_parser.getRedisSession(session_id, function (err, session) {
if (!err && session) {
laravel_session_parser.getSessionId(session, function (err, user_id) {
if (user_id) {
_callback(null, session_id, user_id, laravel_session)
} else {
_callback(new Error('Authentication error'), null);
}
});
} else {
_callback(new Error('Authentication error'), null);
}
});
}
}
};
Now you can just have IO get an instance of the individuals session when they establish a connection to socket.io
io.on('connection', function (socket) {
laravel_session_parser.ready(socket, function(err, session_id, user_id, laravel_session) {
//log out the variables above to see what they provide
});
});
Note, I prefer to use dotenv in NodeJS to share environment variables between Laravel and Node.
Then you can do process.env.APP_KEY and you don't need to worry about sharing variables.
Also of note, that script above is not complete and is not production ready, it's just meant to be used as an example.

Related

Configuring Custom Challenge to do token swap in AWS Cognito

How do you properly configure custom challenge by sending an access_token from
cookie without a session?
const {
CognitoUserPool,
AuthenticationDetails,
CognitoUser
} = require('amazon-cognito-identity-js');
async function asyncAuthenticateUser(cognitoUser, cognitoAuthenticationDetails) {
return new Promise(function (resolve, reject) {
cognitoUser.initiateAuth(cognitoAuthenticationDetails, {
onSuccess: resolve,
onFailure: reject,
customChallenge: resolve
})
})
}
async function asyncCustomChallengeAnswer(cognitoUser, challengeResponse) {
return new Promise(function (resolve, reject) {
cognitoUser.sendCustomChallengeAnswer(challengeResponse, {
onSuccess: resolve,
onFailure: reject,
customChallenge: reject // We do not expect a second challenge
}
})
}
// omitted part of codes for brevity...
// We have tokens as cookie already that means a successful login previously succeeded
// but this login has probably been done from a different client with a different client_id
// We call the custom auth flow along with the token we have to get a new one for the current client_id
// For this to work we need to extract the username from the cookie
let tokenDecoded = jwt_decode(cookies.access_token);
let tokenUsername = tokenDecoded['username'];
var authenticationData = {
Username: tokenUsername,
AuthParameters: {
Username: tokenUsername,
}
};
var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
var poolData = {
UserPoolId: process.env.AUTH_AMPLIFYIDENTITYBROKERAUTH_USERPOOLID,
ClientId: client_id
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var userData = {
Username: tokenUsername,
Pool: userPool
};
var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
cognitoUser.setAuthenticationFlowType("CUSTOM_AUTH");
try {
// Initiate the custom flow
await asyncAuthenticateUser(cognitoUser, authenticationDetails);
// Answer the custom challenge by providing the token
var result = await asyncCustomChallengeAnswer(cognitoUser, cookies.access_token);
var encrypted_id_token = await encryptToken(result.getIdToken().getJwtToken());
var encrypted_access_token = await encryptToken(result.getAccessToken().getJwtToken());
var encrypted_refresh_token = await encryptToken(result.getRefreshToken().getToken());
params.Item.id_token = encrypted_id_token;
params.Item.access_token = encrypted_access_token;
params.Item.refresh_token = encrypted_refresh_token;
}
catch (error) {
console.log("Token swap fail, this may be a tentative of token stealing");
return { // Redirect to login page with forced pre-logout
statusCode: 302,
headers: {
Location: '/?client_id=' + client_id + '&redirect_uri=' + redirect_uri + '&authorization_code=' + authorizationCode + '&forceAuth=true' + insertStateIfAny(event),
}
};
}
I have this part of codes where it invoke the iniateAuth then send a custom challenge answer.
// Initiate the custom flow
await asyncAuthenticateUser(cognitoUser, authenticationDetails);
// Answer the custom challenge by providing the token
var result = await asyncCustomChallengeAnswer(cognitoUser, cookies.access_token);
This complains due to the request session being empty.
The below are the auth challenge for create.
function handler(event, context, callback) {
// This function does nothing, the challenge do not need to be prepared
// Verify challenge will just verify the token provided
event.response.publicChallengeParameters = {};
event.response.publicChallengeParameters.question = 'JustGimmeTheToken';
event.response.privateChallengeParameters = {};
event.response.privateChallengeParameters.answer = 'unknown';
event.response.challengeMetadata = 'TOKEN_CHALLENGE';
event.response.challengeResult = true;
callback(null, event);
}
This is for the define challenge.
function handler(event, context, callback) {
// This function define the sequence to obtain a valid token from a valid token of another client
if (event.request.session.length == 0) {
// This is the first invocation, we ask for a custom challenge (providing a valid token)
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
} else if (
event.request.session.length == 1 &&
event.request.session[0].challengeName == 'CUSTOM_CHALLENGE' &&
event.request.session[0].challengeResult == true
) {
// The custom challenge has been answered we retrun the token
event.response.issueTokens = true;
event.response.failAuthentication = false;
}
context.done(null, event);
}
It goes both in this auth challenge but never goes to the verify challenge where
the is the below
function handler(event, context, callback) {
const params = {
AccessToken: event.request.challengeAnswer
};
const userInfo = await cognito.getUser(params).promise();
if (userInfo.Username === event.username) {
event.response.answerCorrect = true;
} else {
// Someone tried to get a token of someone else
event.response.answerCorrect = false;
}
callback(null, event);
}

ioredis bluebird a promise was created in a handler but was not returned from it

Can someone please explain to me why i'm getting this warning Warning: a promise was created in a handler but was not returned from it when I execute the following code:
cache['deviceSlave'].getBySystemId(systemId).then(function(slavesMapping) {
// do other stuff
}).catch(function(err) {
// throw error
});
Here is the rest of the code:
var Promise = require('bluebird');
var _ = require('lodash');
var Redis = require('ioredis');
var config = require('/libs/config');
var redis = new Redis({
port: config.get('redis:port'),
host: config.get('redis:host'),
password: config.get('redis:key'),
db: 0
});
var self = this;
module.exports.getBySystemId = function(systemId) {
return new Promise(function(resolve, reject) {
var systemIds = [systemId];
self.getBySystemIds(systemIds).then(function(result) {
return resolve(_.values(result)[0]);
}).catch(function(err) {
return reject(err);
});
});
};
module.exports.getBySystemIds = function(systemIds) {
return new Promise(function(resolve, reject) {
var pipeline = redis.pipeline();
_.each(systemIds, function(systemId) {
var cacheKey = 'device_slaves:' + systemId.replace(/:/g, '');
// get through pipeline for fast retrieval
pipeline.get(cacheKey);
});
pipeline.exec(function(err, results) {
if (err) return reject(err);
else {
var mapping = {};
_.each(systemIds, function(systemId, index) {
var key = systemId;
var slaves = JSON.parse(results[index][1]);
mapping[key] = slaves;
});
return resolve(mapping);
}
});
});
};
I'm using the following libraries: ioredis & bluebird.
The code executes fine and everything just works good! I just dont like the fact I get an warning which I can not solve!
Bluebird is warning you against explicit construction here. Here is how you should write the above code:
module.exports.getBySystemId = function(systemId) {
return self.getBySystemIds([systemId]).then(result => _.values(result)[0]);
};
There is no need to wrap the promise - as promises chain :)

handle browser refresh socket.io

I have a requirement using node js that handles disconnecting a user from a chat application.
I am not sure how to handle telling the difference between a browser closing and a user refreshing the browser.
client.on('disconnect', function () {
console.log( 'Disconnected' );
// run mysql code to remove user from logged in table
});
I have googled for a couple hours and cannot find a solution.
This seems like something pretty simple and I think it is the keywords that I am using.
Can someone point me in the right direction on how to handle this?
Thanks in advance.
One way would be to generate a random UID and save it to local storage. Right after the client connects, send this UID to the server and check to see if that UID exists as a connected user. On the server side, set a timeout in the disconnect that gives the user 15 seconds or so before their unique UID is deleted from the "users online" data.
Client:
// When the client starts, create the uid.
localstorage.setItem('uUID', Math.random().toString(24) + new Date());
// Emit the UID right after connection
socket.emit('userLogin', localstorage.getItem('uUID');
Server:
var currentUIDS = [];
var userIsConnected = true;
io.sockets.on('connection', function (socket) {
var currentUID = null;
socket.on('userLogin', function (data) {
if (data !== null) {
if (currentUIDS.includes(data)) {
userIsConnected = true;
currentUID = data;
}
}
});
socket.on('disconnect', function () {
userIsConnected = false;
setTimeout(function () {
if (!userIsConnected) currentUIDS.pop(currentUID);
}, 15000);
});
});
I have a better solution for that to handle multiple users:
var users = [],
users_connected = [];
io.on('connection', function(socket) {
var uid = null;
// register the new user
socket.on('register', function (user_uid) {
if ( users_connected.indexOf(user_uid) < 0 ) {
users_connected.push(user_uid);
}
if ( users.indexOf(user_uid) < 0 ) {
console.log('New user connected: ' + user_uid);
users.push(user_uid);
// notify other clients that a new user has joined
socket.broadcast.emit('user:join', {
name: user_uid,
users: users_connected.length
});
}
uid = user_uid;
});
// clean up when a user leaves, and broadcast it to other users
socket.on('disconnect', function () {
users_connected.splice( users_connected.indexOf(uid), 1);
setTimeout(function () {
if ( users_connected.indexOf(uid) < 0 ) {
socket.broadcast.emit('user:left', {
name: uid
});
var index = users.indexOf(uid);
users.splice(index, 1);
}
}, 3000);
});
});

Node.js proxy server with https support

I'm trying to write proxy server that will proxy (almost) all http/s requests. Almost all because I need catch requests for some specific https url's and as response send the file from hdd instead of real response from the web.
Whole solution should works as proxy in the browser and have to work on windows 7. I started with my own proxy based on express.js. It works great ... but unfortunately not via https. Then I was trying to use several existing node.js proxy servers from github (https://github.com/horaci/node-mitm-proxy, https://github.com/Hypermediaisobar/hyperProxy and few other) but any of them worked in windows environment on https (or I don't know how to congiure them).
Finally I found somewhere in internet code (don't have the link to source) which works via https (see code below). The problems with this code is, that I can't find right way to check the incoming request url and depending on the request url handle them in different ways.
I will be grateful if someone could help me with that.
var http = require('http');
var net = require('net');
var debugging = 0;
var regex_hostport = /^([^:]+)(:([0-9]+))?$/;
function getHostPortFromString(hostString, defaultPort) {
var host = hostString;
var port = defaultPort;
var result = regex_hostport.exec(hostString);
if (result != null) {
host = result[1];
if (result[2] != null) {
port = result[3];
}
}
return( [ host, port ] );
}
// handle a HTTP proxy request
function httpUserRequest(userRequest, userResponse) {
var httpVersion = userRequest['httpVersion'];
var hostport = getHostPortFromString(userRequest.headers['host'], 80);
// have to extract the path from the requested URL
var path = userRequest.url;
result = /^[a-zA-Z]+:\/\/[^\/]+(\/.*)?$/.exec(userRequest.url);
if (result) {
if (result[1].length > 0) {
path = result[1];
} else {
path = "/";
}
}
var options = {
'host': hostport[0],
'port': hostport[1],
'method': userRequest.method,
'path': path,
'agent': userRequest.agent,
'auth': userRequest.auth,
'headers': userRequest.headers
};
var proxyRequest = http.request(
options,
function (proxyResponse) {
userResponse.writeHead(proxyResponse.statusCode, proxyResponse.headers);
proxyResponse.on('data', function (chunk) {
userResponse.write(chunk);
}
);
proxyResponse.on('end',
function () {
userResponse.end();
}
);
}
);
proxyRequest.on('error', function (error) {
userResponse.writeHead(500);
userResponse.write(
"<h1>500 Error</h1>\r\n<p>Error was <pre>" + error + "</pre></p>\r\n</body></html>\r\n";
);
userResponse.end();
}
);
userRequest.addListener('data', function (chunk) {
proxyRequest.write(chunk);
}
);
userRequest.addListener('end', function () {
proxyRequest.end();
}
);
}
function main() {
var port = 5555; // default port if none on command line
// check for any command line arguments
for (var argn = 2; argn < process.argv.length; argn++) {
if (process.argv[argn] === '-p') {
port = parseInt(process.argv[argn + 1]);
argn++;
continue;
}
if (process.argv[argn] === '-d') {
debugging = 1;
continue;
}
}
if (debugging) {
console.log('server listening on port ' + port);
}
// start HTTP server with custom request handler callback function
var server = http.createServer(httpUserRequest).listen(port);
server.addListener('checkContinue', function (request, response){
console.log(request);
response.writeContinue();
});
// add handler for HTTPS (which issues a CONNECT to the proxy)
server.addListener(
'connect',
function (request, socketRequest, bodyhead) {
var url = request['url'];
var httpVersion = request['httpVersion'];
var hostport = getHostPortFromString(url, 443);
// set up TCP connection
var proxySocket = new net.Socket();
proxySocket.connect(
parseInt(hostport[1]), hostport[0],
function () {
console.log("ProxySocket: " + hostport[1] + " | " + hostport[0]);
proxySocket.write(bodyhead);
// tell the caller the connection was successfully established
socketRequest.write("HTTP/" + httpVersion + " 200 Connection established\r\n\r\n");
}
);
proxySocket.on('data', function (chunk) {
socketRequest.write(chunk);
}
);
proxySocket.on('end', function () {
socketRequest.end();
}
);
socketRequest.on('data', function (chunk) {
proxySocket.write(chunk);
}
);
socketRequest.on('end', function () {
proxySocket.end();
}
);
proxySocket.on('error', function (err) {
socketRequest.write("HTTP/" + httpVersion + " 500 Connection error\r\n\r\n");
socketRequest.end();
}
);
socketRequest.on('error', function (err) {
proxySocket.end();
}
);
}
); // HTTPS connect listener
}
main();
are you asking for
http://expressjs.com/4x/api.html#req.secure
req.secure -> https
http://expressjs.com/4x/api.html#req.protocol
req.protocol -> http
http://expressjs.com/4x/api.html#req.host
req.host
req.url
this should all be on your userRequest
I probably did not understand your question correctly.
Just add this line:
var https = require('https');
And when you are making regular http requests, use http.request, and for the ssl requests, https.request.

Why my model does not get synced?

I've written my first program with racer. It displays a simple text box manually bound to 'col.doc.prop' path. When I change the value, it does not apply to the store at server.
What causes my subscribed model not to get sync with server?
Server code:
var fs = require('fs');
var io = require('socket.io');
var racer = require('racer');
var mongo = require('racer-db-mongo');
racer.use(mongo);
racer.js({
entry: __dirname + '/client.js'
}, function(err, js) {
return fs.writeFileSync(__dirname + '/script.js', js);
});
var express = require('express');
var server = express.createServer();
server.use(express.static(__dirname));
server.get('/', function(req, res)
{
var model = store.createModel();
model.subscribe('col.doc', function(err, doc)
{
var prop = doc.get('prop');
if (!prop)
{
doc.set('prop', 123);
store.flush();
}
model.ref('_doc', doc);
model.bundle(function(bundle)
{
var client = require('fs').readFileSync('./client.html', 'utf-8');
client = client.replace('_init_', bundle.toString());
res.send(client);
});
});
});
var store = racer.createStore(
{
listen: server,
db:
{
type: 'Mongo',
uri: 'mongodb://localhost/racerdb'
}
});
store.set('col.doc.prop', 123);
store.flush();
server.listen(3001);
Client code:
useRacer = function()
{
var socket = io.connect('http://localhost:3001');
var racer = require('racer');
process.nextTick(function() {
racer.init(this.init, socket);
return delete this.init;
});
racer.on('ready', function(model)
{
addListener = document.addEventListener ? function(el, type, listener) {
return el.addEventListener(type, listener, false);
} : function(el, type, listener) {
return el.attachEvent('on' + type, function(e) {
return listener(e || event);
});
};
var element = document.getElementById('prop');
var listener = function()
{
var val = element.value;
model.set('col.doc.prop', val);
};
addListener(element, 'keyup', listener);
var upgrade = function(id, value)
{
if (model.connected)
{
var prop = model.get('col.doc.prop');
element.value = prop;
}
else
model.socket.socket.connect();
};
model.on('connectionStatus', upgrade);
model.on('set', 'con.*', upgrade);
});
};
The problem solved by changing some lines of the client code:
model.set('col.doc.prop', val) ==> model.set('_doc.prop', val)
model.get('col.doc.prop') ==> model.get('_doc.prop')
model.on('set', 'con.', upgrade) ==> model.on('set', '', upgrade)

Resources