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

Socket reconnect #9

Merged
merged 3 commits into from
Nov 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we only set up the reconnect listeners once per app instance instead of every time connect was called.

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