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

Commit 65e3fb9

Browse files
author
Justin Slattery
committed
Basic v0.2 functionality complete. Lots of dox strings added.
1 parent 24fae43 commit 65e3fb9

File tree

3 files changed

+552
-59
lines changed

3 files changed

+552
-59
lines changed

core.js

Lines changed: 114 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,69 @@
1-
var express = require('express')
2-
, app = express.createServer()
3-
, connect = require('connect')
4-
, jade = require('jade')
5-
, socket = require('socket.io').listen(app)
6-
, _ = require('underscore')._
1+
/*!
2+
* nodechat.js
3+
* Copyright(c) 2011 Justin Slattery <justin.slattery@fzysqr.com>
4+
* MIT Licensed
5+
*/
6+
7+
/**
8+
* Include core dependencies.
9+
*/
10+
var _ = require('underscore')._
711
, Backbone = require('backbone')
8-
, models = require('./models/models')
12+
13+
/**
14+
* Include our own modules
15+
*/
16+
var models = require('./models/models')
917
, auth = require('./lib/auth');
1018

19+
/**
20+
* Require redis and setup the client
21+
*/
1122
var redis = require('redis')
1223
, rc = redis.createClient()
13-
, redisStore = require('connect-redis');
1424

1525
rc.on('error', function(err) {
1626
console.log('Error ' + err);
1727
});
1828

29+
/**
30+
* Setup connect, express, socket, and the connect-redis session store
31+
*/
32+
var express = require('express')
33+
, app = express.createServer()
34+
, connect = require('connect')
35+
, jade = require('jade')
36+
, socket = require('socket.io').listen(app)
37+
, redisStore = require('connect-redis');
1938

20-
//configure express to use jade
2139
app.set('view engine', 'jade');
2240
app.set('view options', {layout: false});
23-
24-
//configure express to use redis as session store
2541
app.use(express.bodyParser());
2642
app.use(express.cookieParser());
2743
app.use(express.session({ store: new redisStore(), secret: 'Secretly I am an elephant' }));
2844

29-
30-
//setup routes
31-
app.get('/logout', function(req, res){
32-
// destroy the user's session to log them out
33-
// will be re-created next request
34-
req.session.destroy(function(){
35-
res.redirect('home');
36-
});
37-
});
38-
45+
/**
46+
* Route: GET /login
47+
*
48+
* Template: login.jade
49+
*/
3950
app.get('/login', function(req, res){
4051
res.render('login');
4152
});
4253

43-
54+
/**
55+
* Route: POST /login
56+
*
57+
* Calls the authentication module to verify login details. Failures are redirected back to the login page.
58+
*
59+
* If the authentication module gives us a user object back, we ask connect to regenerate the session and send the client back to index. Note: we specify a _long_ cookie age so users won't have to log in frequently. We also set the httpOnly flag to false (I know, not so secure) to make the cookie available over [Flash Sockets](http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/Socket.html).
60+
*/
4461
app.post('/login', function(req, res){
4562
auth.authenticateUser(req.body.username, req.body.password, function(err, user){
4663
if (user) {
47-
// Regenerate session when signing in
48-
// to prevent fixation
64+
// Regenerate session when signing in to prevent fixation
4965
req.session.regenerate(function(){
50-
// Store the user's primary key
51-
// in the session store to be retrieved,
52-
// or in this case the entire user object
66+
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
5367
console.log('regenerated session id ' + req.session.id);
5468
req.session.cookie.maxAge = 100 * 24 * 60 * 60 * 1000; //Force longer cookie age
5569
req.session.cookie.httpOnly = false;
@@ -65,13 +79,23 @@ app.post('/login', function(req, res){
6579
});
6680
});
6781

82+
/**
83+
* Route: GET /signup:
84+
*
85+
* Template: signup.jade
86+
*/
6887
app.get('/signup', function(req, res){
6988
res.render('signup');
7089
});
7190

91+
/**
92+
* Route: POST /signup
93+
*
94+
* Calls createNewUserAccount() in the auth module, redirects to /login if a user object is returned. Redirects to /signup if not.
95+
*/
7296
app.post('/signup', function(req, res) {
7397
auth.createNewUserAccount(req.body.username, req.body.password1, req.body.password2, req.body.email, req.body.ponies, function(err, user){
74-
if (err) {
98+
if ((err) || (!user)) {
7599
req.session.error = 'New user failed, please check your username and password.';
76100
res.redirect('back');
77101
}
@@ -82,17 +106,45 @@ app.post('/signup', function(req, res) {
82106

83107
});
84108

109+
/**
110+
* Tell connect to destory the session.
111+
*/
112+
113+
app.get('/logout', function(req, res){
114+
req.session.destroy(function(){
115+
res.redirect('home');
116+
});
117+
});
118+
119+
120+
/**
121+
* Serve up any static file requested by the client
122+
*
123+
* TODO: should restrict this to only server *public* routes.
124+
*/
85125
app.get('/*.(js|css)', function(req, res){
86126
res.sendfile('./'+req.url);
87127
});
88128

129+
/**
130+
* Server up the main index view.
131+
*
132+
* Calls the restrictAccess() middleware.
133+
*
134+
*
135+
*
136+
*/
89137
app.get('/', restrictAccess, function(req, res){
90138
res.render('index', {
91139
locals: { name: req.session.user.name, hashPass: JSON.stringify(req.session.user.hashPass) }
92140
});
93141
});
94142

95-
//This method decides what a valid login looks like. In this case, just verify that we have a session object for the user
143+
/**
144+
* Middleware that decides what a valid login looks like. In this case, just verify that we have a session object for the user.
145+
*
146+
* This is an express [route middleware](http://expressjs.com/guide.html#route-middleware). Control is passed to the middleware function before the route function is called. We use restrictAccess() to verify that we have a valid user key in the session, implying that authentication has succeeded, before we send the client to the index.jade template. If we do not have a valid user in the session, then we redirect to the '/login' route. This effectively locks down our '/' route from unauthenticated access. You could add the restrictAccess() all to any route you want to protect.
147+
*/
96148
function restrictAccess(req, res, next) {
97149
if (req.session.user) {
98150
next();
@@ -107,17 +159,28 @@ function restrictAccess(req, res, next) {
107159
var activeClients = 0;
108160
var nodeChatModel = new models.NodeChatModel();
109161

110-
162+
/**
163+
* When we have a client that shouldn't be connected, __kick 'em off!__'
164+
*
165+
* @param {object} client
166+
* @param {function} fn
167+
*/
111168
function disconnectAndRedirectClient(client, fn) {
112169
console.log('Disconnecting unauthenticated user');
113170
client.send({ event: 'disconnect' });
114171
client.connection.end();
115-
fn();
116-
return;
172+
return fn();
117173
}
118174

175+
/*
176+
*
177+
* Handle the new connection event for socket.
178+
*
179+
* connectSession() is a helper method that will verify a client's validity by checking for a cookie in the request header, then, if we find it, _pulling their session out of redis_.
180+
*
181+
* We then use the helper method in the 'connection' handler for our socket listener. Instead accepting any user connection, we are going to check that the client has a valid session (meaning they logged in). If they don't, give them the boot! If they do, then we store a copy of the session data (yay we have access!) in the client object and then setup the rest of the socket events. Finally, send them a welcome message just to prove that we remembered their profile.
182+
*/
119183
socket.on('connection', function(client){
120-
// helper function that goes inside your socket connection
121184
client.connectSession = function(fn) {
122185
if (!client.request || !client.request.headers || !client.request.headers.cookie) {
123186
disconnectAndRedirectClient(client,function() {
@@ -149,6 +212,8 @@ socket.on('connection', function(client){
149212
return;
150213
}
151214

215+
client.user = data.user;
216+
152217
activeClients += 1;
153218
client.on('disconnect', function(){clientDisconnect(client)});
154219
client.on('message', function(msg){chatMessage(client, socket, msg)});
@@ -169,6 +234,13 @@ socket.on('connection', function(client){
169234
});
170235
});
171236

237+
/*
238+
* Event handler for new chat messages. Stores the chat in redis and broadcasts it to all connected clients.
239+
*
240+
* @param {object} client
241+
* @param {object} socket
242+
* @param {json string} msg
243+
*/
172244
function chatMessage(client, socket, msg){
173245
var chat = new models.ChatEntry();
174246
chat.mport(msg);
@@ -177,7 +249,7 @@ function chatMessage(client, socket, msg){
177249
chat.set({id: newId});
178250
nodeChatModel.chats.add(chat);
179251

180-
var expandedMsg = chat.get('id') + ' ' + chat.get('name') + ': ' + chat.get('text');
252+
var expandedMsg = chat.get('id') + ' ' + client.user.name + ': ' + chat.get('text');
181253
console.log('(' + client.sessionId + ') ' + expandedMsg);
182254

183255
rc.rpush('chatentries', chat.xport(), redis.print);
@@ -190,9 +262,17 @@ function chatMessage(client, socket, msg){
190262
});
191263
}
192264

265+
/*
266+
* Event handler for client disconnectes. Simply broadcasts the new active client count.
267+
*
268+
* @param {object} client
269+
*/
193270
function clientDisconnect(client) {
194271
activeClients -= 1;
195272
client.broadcast({clients:activeClients})
196273
}
197274

275+
/**
276+
* Fire up the webserver
277+
*/
198278
app.listen(8000)

lib/auth.js

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
/* Auth.js
1+
/*! Auth.js
22
*
33
* Handles new user accounts and authentication
4-
*
54
*/
65

6+
/*
7+
* This will be a [CommonJS module](http://www.commonjs.org/) so we need to start off with some setup.
8+
*
9+
* Here we are checking to see if this code is included as a module. If it is, we go ahead and include our dependencies (in this case, our models lib, redis, and hash + friends). If we are not a module, we may as well explode because the rest of the code won't run without redis and hash.
10+
*/
711
(function () {
812
if (typeof exports !== 'undefined') {
9-
auth = exports;
1013
redis = require('redis');
1114
rc = redis.createClient();
1215
models = require('../models/models');
@@ -17,10 +20,18 @@
1720
require('hash');
1821
}
1922
else {
20-
auth = this.auth = {};
23+
throw new Error('auth.js must be loaded as a module.');
2124
}
2225

23-
auth.authenticateUser = function(name, pass, fn) {
26+
/**
27+
* Checks to see if the user exists in redis. If it does, it calls verifyUserAccount(). Otherwise callback with an error.
28+
*
29+
* @param: {string} name
30+
* @param: {string} pass
31+
* @param: {function} fn
32+
* @api: public
33+
*/
34+
exports.authenticateUser = function(name, pass, fn) {
2435
console.log('[authenticate] Starting auth for ' + name + ' with password ' + pass);
2536

2637
var rKey = 'user:' + name;
@@ -32,42 +43,74 @@
3243
}
3344
else {
3445
console.log('[authenticateUser] user: ' + name + ' found in store. Verifying password.');
35-
auth.verifyUserAccount(data, pass, fn)
46+
verifyUserAccount(data, pass, fn)
3647
}
3748
});
3849
};
3950

40-
auth.verifyUserAccount = function(foundUserName, pass, fn) {
51+
/**
52+
* Steps through the process of retreiving the salt, calculating the hash of the passed in password, then comparing it to the stored hash in redis.
53+
*
54+
* If successful, create a new user model and pass it to the callback.
55+
*
56+
* Otherwise, any failure along the way means we callback with an error.
57+
*
58+
* Assumes the passed in user exists in redis.
59+
*
60+
* @param: {string} foundUserName
61+
* @param: {string} pass
62+
* @param: {function} fn
63+
* @api: private
64+
*/
65+
var verifyUserAccount = function(foundUserName, pass, fn) {
4166
var rKey = 'user:' + foundUserName;
4267

4368
rc.get(rKey + '.salt', function(err, data){
4469
if(err) return fn(new Error('[verifyUserAccount] GET failed for key: ' + rKey + '.salt'));
4570

46-
var calculatedHash = Hash.sha512(data + '_' + pass);
47-
rc.get(rKey + '.hashPass', function(err, data) {
48-
if(err) return fn(new Error('[verifyUserAccount] GET failed for key: ' + rKey + '.hashPass'));
71+
if(data) {
72+
var calculatedHash = Hash.sha512(data + '_' + pass);
73+
rc.get(rKey + '.hashPass', function(err, data) {
74+
if(err) return fn(new Error('[verifyUserAccount] GET failed for key: ' + rKey + '.hashPass'));
4975

50-
if (calculatedHash === data) {
51-
console.log('[verifyUserAccount] Auth succeeded for ' + foundUserName + ' with password ' + pass);
76+
if (calculatedHash === data) {
77+
console.log('[verifyUserAccount] Auth succeeded for ' + foundUserName + ' with password ' + pass);
5278

53-
rc.get(rKey + '.profile', function(err, data) {
54-
if(err) return fn(new Error('[verifyUserAccount] GET failed for key: ' + rKey + '.profile' + ' for user profile'));
79+
rc.get(rKey + '.profile', function(err, data) {
80+
if(err) return fn(new Error('[verifyUserAccount] GET failed for key: ' + rKey + '.profile' + ' for user profile'));
5581

56-
var foundUser = new models.User();
57-
foundUser.mport(data);
58-
foundUser.set({'hashPass': calculatedHash});
82+
var foundUser = new models.User();
83+
foundUser.mport(data);
84+
foundUser.set({'hashPass': calculatedHash});
5985

60-
return fn(null, foundUser);
61-
});
62-
}
63-
else {
64-
return fn(new Error('invalid password'));
65-
}
66-
});
86+
return fn(null, foundUser);
87+
});
88+
}
89+
else {
90+
return fn(new Error('[verifyUserAccount] invalid password'));
91+
}
92+
});
93+
}
94+
else {
95+
return fn(new Error('[verifyUserAccount] salt not found'));
96+
}
6797
});
6898
}
6999

70-
auth.createNewUserAccount = function(name, pass1, pass2, email, ponies, fn) {
100+
/**
101+
* Verifies that the two passwords match, then use the current timestamp to salt a hash of the password. Store it all in a user model which we will save as a poor man's profile if everything succeeds.
102+
*
103+
* Any failure along the way means we callback with an error.
104+
*
105+
* @param: {string} name
106+
* @param: {string} pass1
107+
* @param: {string} pass2
108+
* @param: {string} email
109+
* @param: {string} ponies
110+
* @param: {function} fn
111+
* @api: public
112+
*/
113+
exports.createNewUserAccount = function(name, pass1, pass2, email, ponies, fn) {
71114
if (pass1 !== pass2) return fn(new Error('[createNewUserAccount] Passwords do not match'));
72115

73116
var newUser = new models.User({name: name, email: email, ponies: ponies});

0 commit comments

Comments
 (0)