Skip to content
This repository has been archived by the owner on Apr 23, 2019. It is now read-only.

Commit

Permalink
Merge pull request #9 from feathersjs/socket-reconnect
Browse files Browse the repository at this point in the history
Socket reconnect
  • Loading branch information
ekryski committed Nov 23, 2016
2 parents 3fb852a + c22bd3a commit 5c2fe02
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 108 deletions.
159 changes: 84 additions & 75 deletions src/passport.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const debug = Debug('feathers-authentication-client');

export default class Passport {
constructor (app, options) {
if (app.passport) {
throw new Error('You have already registered authentication on this client app instance. You only need to do it once.');
}

this.options = options;
this.app = app;
this.storage = app.get('storage') || this.getStorage(options.storage);
Expand All @@ -14,95 +18,100 @@ export default class Passport {

app.set('storage', this.storage);
this.getJWT().then(this.setJWT);

this.setupSocketListeners();
}

connected () {
setupSocketListeners() {
const app = this.app;
const socket = app.io || app.primus;
const emit = app.io ? 'emit' : 'send';
const reconnected = app.io ? 'reconnect' : 'reconnected';

return new Promise((resolve, reject) => {
if (app.rest) {
return resolve();
}

const socket = app.io || app.primus;
if (!socket) {
return;
}

if (!socket) {
return reject(new Error(`It looks like your client connection has not been configured.`));
socket.on(reconnected, () => {
debug('Socket reconnected');

// If socket was already authenticated then re-authenticate
// it with the server automatically.
if (socket.authenticated) {
const data = {
strategy: this.options.jwtStrategy,
accessToken: app.get('accessToken'),
};
this.authenticateSocket(data, socket, emit)
.then(this.setJWT)
.catch(error => {
debug('Error re-authenticating after socket reconnect', error);
socket.authenticated = false;
app.emit('reauthentication-error', error);
});
}
});

// If the socket is not connected yet we have to wait for the `connect` event
if ((app.io && !socket.connected) || (app.primus && socket.readyState !== 3)) {
const connected = app.primus ? 'open' : 'connect';
debug('Waiting for socket connection');

socket.on(connected, () => {
debug('Socket connected');
if (socket.io) {
socket.io.engine.on('upgrade', () => {
debug('Socket upgrading');

// If socket was already authenticated then re-authenticate
// it with the server automatically.
if (socket.authenticated) {
const data = {
strategy: this.options.jwtStrategy,
accessToken: app.get('accessToken'),
};

this.authenticateSocket(data, socket, emit)
.then(this.setJWT)
.catch(error => {
debug('Error re-authenticating after socket upgrade', error);
socket.authenticated = false;
app.emit('reauthentication-error', error);
});
}
});
}
}

const emit = app.io ? 'emit' : 'send';
const disconnect = app.io ? 'disconnect' : 'end';
const reconnecting = app.io ? 'reconnecting' : 'reconnect';
const reconnected = app.io ? 'reconnect' : 'reconnected';
connected () {
const app = this.app;

// If one of those events happens before `connect` the promise will be rejected
// If it happens after, it will do nothing (since Promises can only resolve once)
socket.on(disconnect, () => {
debug('Socket disconnected');
socket.authenticated = false;
socket.removeAllListeners();
});
if (app.rest) {
return Promise.resolve();
}

socket.on(reconnecting, () => {
debug('Socket reconnecting');
});
const socket = app.io || app.primus;

socket.on(reconnected, () => {
debug('Socket reconnected');

// If socket was already authenticated then re-authenticate
// it with the server automatically.
if (socket.authenticated) {
const data = {
strategy: this.options.jwtStrategy,
accessToken: app.get('accessToken'),
};
this.authenticateSocket(data, socket, emit)
.then(this.setJWT)
.catch(error => {
debug('Error re-authenticating after socket upgrade', error);
socket.authenticated = false;
app.emit('reauthentication-error', error);
});
}
});
if (!socket) {
return Promise.reject(new Error(`It looks like your client connection has not been configured.`));
}

if (socket.io) {
socket.io.engine.on('upgrade', () => {
debug('Socket upgrading', arguments);

// If socket was already authenticated then re-authenticate
// it with the server automatically.
if (socket.authenticated) {
const data = {
strategy: this.options.jwtStrategy,
accessToken: app.get('accessToken'),
};
this.authenticateSocket(data, socket, emit)
.then(this.setJWT)
.catch(error => {
debug('Error re-authenticating after socket upgrade', error);
socket.authenticated = false;
app.emit('reauthentication-error', error);
});
}
});
}
if ((app.io && socket.connected) || (app.primus && socket.readyState === 3)) {
debug('Socket already connected');
return Promise.resolve(socket);
}

return new Promise((resolve, reject) => {
const connected = app.primus ? 'open' : 'connect';
const disconnect = app.io ? 'disconnect' : 'end';
debug('Waiting for socket connection');

resolve(socket);
});
} else {
debug('Socket already connected');
const handleDisconnect = () => {
debug('Socket disconnected before it could connect');
socket.authenticated = false;
};

// If disconnect happens before `connect` the promise will be rejected.
socket.once(disconnect, handleDisconnect);
socket.once(connected, () => {
debug('Socket connected');
debug(`Removing ${disconnect} listener`);
socket.removeListener(disconnect, handleDisconnect);
resolve(socket);
}
});
});
}

Expand Down
6 changes: 6 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ describe('Feathers Authentication Client', () => {
.configure(auth());
});

it('throws an error if registered twice', () => {
expect(() => {
client.configure(auth());
}).to.throw(Error);
});

describe('default options', () => {
it('sets the authorization header', () => {
expect(client.passport.options.header).to.equal('authorization');
Expand Down
35 changes: 16 additions & 19 deletions test/integration/primus.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const app = createApplication({ secret: 'supersecret' }, 'primus');
let options;

describe('Primus client authentication', function () {
this.timeout(20000);
let socket;
let server;
let client;
Expand All @@ -30,7 +31,6 @@ describe('Primus client authentication', function () {
server = app.listen(port);
server.once('listening', () => {
socket = new Socket(baseURL);

client = feathers()
.configure(hooks())
.configure(primus(socket, { timeout: 1000 }))
Expand All @@ -50,9 +50,7 @@ describe('Primus client authentication', function () {

after(done => {
socket.socket.close();
server.close(() => {
done();
});
server.close(done);
});

it('can use client.passport.getJWT() to get the accessToken', () => {
Expand Down Expand Up @@ -144,23 +142,22 @@ describe('Primus client authentication', function () {

it('authenticates automatically after reconnection', done => {
client.authenticate(options).then(response => {
setTimeout(() => {
app.primus.end({ reconnect: true });
server.close();

const newApp = createApplication({ secret: 'supersecret' }, 'primus');

const newServer = newApp.listen(port, () => {});
newServer.once('listening', () => {
newApp.primus.once('connection', serverSocket => {
serverSocket.once('authenticate', data => {
expect(data.accessToken).to.equal(response.accessToken);
newServer.close();
done();
app.primus.end({ reconnect: true });
server.close(() => {
setTimeout(() => {
const newApp = createApplication({ secret: 'supersecret' }, 'primus');

server = newApp.listen(port);
server.once('listening', () => {
newApp.primus.on('connection', s => {
s.once('authenticate', data => {
expect(data.accessToken).to.equal(response.accessToken);
done();
});
});
});
});
}, 500);
}, 1000);
});
});
});

Expand Down
30 changes: 16 additions & 14 deletions test/integration/socketio.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,15 @@ const app = createApplication({ secret: 'supersecret' }, 'socketio');
let options;

describe('Socket.io client authentication', function () {
this.timeout(5000);
this.timeout(20000);
let socket;
let server;
let client;
let serverSocket;

before(done => {
server = app.listen(port);
server.once('listening', () => {
app.io.on('connect', s => {
serverSocket = s;
});

socket = io(baseURL);

client = feathers()
.configure(hooks())
.configure(socketio(socket))
Expand Down Expand Up @@ -141,14 +135,22 @@ describe('Socket.io client authentication', function () {

it('authenticates automatically after reconnection', done => {
client.authenticate(options).then(response => {
serverSocket.once('authenticate', data => {
expect(data.accessToken).to.equal(response.accessToken);
done();
app.io.close();
server.close(() => {
setTimeout(() => {
const newApp = createApplication({ secret: 'supersecret' }, 'socketio');
server = newApp.listen(port);

server.once('listening', () => {
newApp.io.on('connect', s => {
s.once('authenticate', data => {
expect(data.accessToken).to.equal(response.accessToken);
done();
});
});
});
}, 1000);
});

setTimeout(() => {
app.io.sockets.emit('reconnect');
}, 500);
});
});

Expand Down

0 comments on commit 5c2fe02

Please sign in to comment.