PageRenderTime 38ms CodeModel.GetById 8ms app.highlight 25ms RepoModel.GetById 1ms app.codeStats 0ms

/http/socket.js

https://github.com/uniteddiversity/scrollback
JavaScript | 478 lines | 382 code | 53 blank | 43 comment | 100 complexity | e33afdc33989e96923ac490a1b5f8c03 MD5 | raw file
  1/*
  2	Scrollback: Beautiful text chat for your community. 
  3	Copyright (c) 2014 Askabt Pte. Ltd.
  4	
  5This program is free software: you can redistribute it and/or modify it 
  6under the terms of the GNU Affero General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or any 
  8later version.
  9
 10This program is distributed in the hope that it will be useful, but 
 11WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 12or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
 13License for more details.
 14
 15You should have received a copy of the GNU Affero General Public License
 16along with this program. If not, see http://www.gnu.org/licenses/agpl.txt
 17or write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 18Boston, MA 02111-1307 USA.
 19*/
 20
 21/*
 22	Websockets gateway
 23*/
 24
 25/* global require, module, exports, console, setTimeout */
 26
 27var sockjs = require("sockjs"),core,
 28	cookie = require("cookie"),
 29	log = require("../lib/logger.js"),
 30	config = require("../config.js"),
 31	EventEmitter = require("events").EventEmitter,
 32	session = require("./session.js"),
 33	guid = require("../lib/guid.js"),
 34	crypto = require("crypto"),
 35	names = require("../lib/names.js");
 36
 37
 38var rConns = {}, users = {};
 39var pid = guid(8);
 40var sock = sockjs.createServer();
 41
 42sock.on('connection', function (socket) {
 43	var conn = { socket: socket };
 44	
 45	socket.on('data', function(d) {
 46		try { d = JSON.parse(d); log ("Socket received ", d); }
 47		catch(e) { log("ERROR: Non-JSON data", d); return; }
 48		
 49		
 50		switch(d.type) {
 51			case 'init': init(d.data, conn); break;
 52			case 'message': message(d.data, conn); break;
 53			case 'messages': messages(d.data, conn); break;
 54			case 'room': room(d.data, conn); break;
 55			case 'rooms': rooms(d.data, conn); break;
 56			case 'getUsers': getUsers(d.data, conn); break;
 57			case 'getRooms': getRooms(d.data, conn); break;
 58			case 'edit': edit(d.data, conn); break;
 59		}
 60	});
 61	
 62	conn.send = function(type, data) {
 63		data = JSON.stringify({type: type, data: data});
 64		log("Sending", data.length, 'bytes: ', data.substr(0,50));
 65		socket.write(data);
 66	};
 67	socket.on('close', function() { close(conn); });
 68});
 69
 70exports.init = function(server, coreObject) {
 71    core = coreObject;
 72    sock.installHandlers(server, {prefix: '/socket'});
 73};
 74
 75function init(data, conn) {
 76	var user, sid = data.sid, nick = data.nick, i;
 77	
 78	session.get({ sid:sid, suggestedNick:data.nick }, function(err, sess) {
 79		var rooms = sess.user.rooms;
 80		var i;
 81		conn.sid = sid;
 82		conn.rooms = [];
 83
 84		if (sess.user.id.indexOf("guest-")===0 && data.nick)	sess.user.id="guest-"+data.nick;
 85		if(sess.pid !== pid) {
 86			sess.pid = pid;
 87			for(i in rooms) {
 88				if(rooms.hasOwnProperty(i)) rooms[i] = 0;
 89			}
 90		}
 91		var query=[];
 92		if (sess.user.id.indexOf('guest-')!==0) {
 93			query.user=sess.user.id;
 94		}
 95		core.emit("members", query,function(err,d) {
 96			var m={};
 97			if (d) {
 98				for (i=0;i<d.length;i++) {
 99					m[d[i].room]=true;
100				}
101			}
102			sess.user.membership = Object.keys(m);//Room added to user object
103			
104			conn.send('init', {
105				sid: sess.cookie.value,
106				user: sess.user,
107				clientTime: data.clientTime,
108				serverTime: new Date().getTime()
109			});
110
111			//Temp for now.
112			core.emit("init", {
113				from: sess.user.id,
114				session: "web:"+conn.socket.remoteAddress+":"+conn.sid,
115				time: new Date().getTime()
116			});
117			session.set(conn.sid, sess);
118		});
119	});
120}
121
122function close(conn) {
123	if(!conn.sid) return;
124
125	session.get({sid: conn.sid}, function(err, sess) {
126		if(err) {
127			log("Couldn't find session to close.");
128			return;
129		}
130		var user = sess.user;
131		
132		conn.rooms.forEach(function(room) {
133			log("Closed connection, removing", user.id, room);
134			userAway(user, room, conn);
135		});
136		session.set(conn.sid, sess);
137	});
138}
139
140function userAway(user, room, conn) {
141	if(rConns[room]) rConns[room].splice(rConns[room].indexOf(conn), 1);
142	conn.rooms.splice(conn.rooms.indexOf(room), 1);
143	setTimeout(function() {
144		session.get({sid: conn.sid}, function(err, sess) {
145			var user = sess.user;
146			if (typeof user.rooms[room] !== "undefined" && user.rooms[room]<=1) {
147				delete user.rooms[room];
148				core.emit("message", { type: 'away', from: user.id, to: room,
149					time: new Date().getTime(), session:"web:"+conn.socket.remoteAddress+":"+ conn.sid,origin : {gateway : "web", location : "", ip :  conn.socket.remoteAddress}}, function(err, m) {
150						log(err, m);
151					});
152				if(!Object.keys(user.rooms).length) {
153					delete users[user.id];
154				}
155			}
156			else {
157				user.rooms[room]--;
158			}
159			session.set(conn.sid, sess);
160		});
161	}, 30*1000);
162	return false; // never send an away message immediately. Wait.
163}
164
165function userBack(user, room, conn) {
166
167	if(rConns[room]) rConns[room].push(conn);
168	else rConns[room] = [conn];
169	conn.rooms.push(room);
170	
171	users[user.id] = user;
172
173	if(typeof user.rooms[room] !== "undefined" && user.rooms[room]>0) {
174		user.rooms[room]++;
175		return false; // we've already sent a back message for this user for this room.
176	}
177
178	user.rooms[room] = 1;
179	return true;
180}
181
182function messages (query, conn) {
183	core.emit("messages", query, function(err, m) {
184		if(err) {
185			log("MESSAGES error", query, err);
186			conn.send('error', err);
187			return;
188		}
189		conn.send('messages', { query: query, messages: m} );
190	});
191}
192
193
194function edit(action, conn) {
195	session.get({sid: conn.sid}, function(err, sess) {
196		var user = sess.user;
197		action.from = user.id;
198		action.session = "web:"+conn.socket.remoteAddress+":"+conn.sid;
199		core.emit("edit",action, function(err, data){
200		});
201	});
202	
203}
204function message (m, conn) {
205	var sendTo;
206	if(!conn.sid) return;
207	session.get({sid: conn.sid}, function(err, sess) {
208		var user = sess.user, tryingNick, roomName;
209		roomName = m.to;
210		
211		m.from = user.id;
212		m.time = new Date().getTime();
213		m.session = "web:"+conn.socket.remoteAddress+":"+ conn.sid;
214
215		if (m.origin) m.origin.ip = conn.socket.remoteAddress;
216		else{
217			m.origin = {gateway: "web", ip: conn.socket.remoteAddress, location:"unknown"};
218		}
219
220
221		if(m.to && typeof m.to == "string") {
222			m.to = [m.to]
223		}
224		if(!m.to || !m.to.length) {
225			if(Object.keys(user.rooms).length) {
226				m.to = Object.keys(user.rooms);
227			}else {
228				m.to = [];
229			}
230		}
231		
232		if (m.type == 'back') {
233			m.to.forEach(function(room) {
234				sendTo = []
235				// it returns false if the back message for this user is already sent
236				if(userBack(user, room, conn)) {
237					sendTo.push(room);
238				}
239			});
240			session.set(conn.sid, sess);
241			m.to = sendTo;
242		} else if (m.type == 'away') {
243			if(!userAway(user, m.to, conn)) {
244				session.set(conn.sid, sess);
245				return; 
246			}
247			// it returns false if the away message for this user is not to be sent yet
248		} else if(m.type == 'nick') {
249			//validating nick name on server side 
250			if(m.ref && m.ref !== "guest-" && !validateNick(m.ref.substring(6))) {
251				return conn.send('error', {id:m.id , message: "INVALID_NAME"});
252			}
253			if(m.ref && users[m.ref] )
254				return conn.send('error', {id: m.id, message: "DUP_NICK"});
255			if(m.user){
256				if(!m.user.id) return conn.send("error", {id: m.id, message: "INVALID_NAME"} );
257				m.user.originalId = user.id;
258				if (!m.user.originalId.match(/^guest/)) {
259					return;
260				}
261				if(!m.user.accounts){m.user.accounts=[];}
262				m.user.accounts[0] = user.accounts[0];
263			}
264		}
265		
266		function sendMessage() {
267			core.emit("message", m, function (err, m) {
268				var i, user = sess.user;
269				if(err && err.message == "GUEST_CANNOT_HAVE_MEMBERSHIP"){
270					return conn.send('error', {id: m.id, message: err.message});
271				}
272				//for audience mismatch error.
273				if(err && err.message && err.message.indexOf("AUTH_FAIL")>0) {
274					return conn.send('error', {id: m.id, message: err.message});
275				}
276				if (!user || !user.id) {
277					return;
278				}
279				if(m.type == 'join'){
280					//check for user login as well
281					sess.user.membership.push(roomName);
282					session.set(conn.sid, sess);
283				}
284				if(m.type == 'part'){
285					//check for user login as well
286					sess.user.membership.splice(sess.user.membership.indexOf(roomName),1);
287					session.set(conn.sid, sess);
288				}
289				if(m && m.type && m.type == 'nick') {
290
291					//in case of logout.
292					if(/^guest-/.test(m.ref) && !/^guest-/.test(m.from)){
293						sess.user.id = m.ref;
294						sess.user.picture = "//s.gravatar.com/avatar/" + crypto.createHash('md5').update(sess.user.id).digest('hex') + "/?d=identicon&s=48";
295						sess.user.accounts = [];
296						sess.user.membership = [];
297						session.set(conn.sid, sess);
298						conn.send('init', {
299							sid: sess.cookie.value,
300							user: sess.user
301						});
302						if(/^guest-/.test(m.ref)){
303							core.emit("init",{
304								type:"init", 
305								from:m.ref, 
306								time: new Date().getTime()
307							});	
308						}
309					}
310
311					if(m.user) {
312						/*	why shallow copy? why not sess.user = m.user?
313							copying the property like accounts to the session, but the user will not send other properties.
314						*/
315						for(i in m.user) if(m.user.hasOwnProperty(i)) {
316							user[i] = m.user[i];
317						}
318					} else if(!err){
319						user.id = m.ref;
320					}
321					session.set(conn.sid, sess);
322					var query=[];
323					if (sess.user.id.indexOf('guest-')!==0) {
324						query.user=sess.user.id;
325					}
326					core.emit("members", query,function(err,d){
327						var m={};
328						if (d) {
329							for (i=0;i<d.length;i++) {
330								m[d[i].room]=true;
331							}
332						}
333						sess.user.membership = Object.keys(m);
334						if(!err){
335							conn.send('init', {
336								sid: sess.cookie.value,
337								user: sess.user
338							});
339						}
340						session.set(conn.sid, sess);
341					});
342					if(m.ref) {
343						users[m.ref] = users[user.from] || {};
344						if(users[m.from]) delete users[m.from];
345						if(m.ref.indexOf("guest-") !== 0) {
346							users["guest-"+m.from]=users[user.from];
347						}
348					}
349				}
350
351				/* 
352					Why this is not at the top?
353					this thing should be at the bottom because we need the error AUTH_UNREGISTERED to be handled properly before sending the response.
354				 */
355				if (err) {
356					return conn.send('error', {id: m.id, message: err.message});
357				}
358			});
359		}
360		
361		if(m.type=="nick" && m.ref!="guest-" &&( m.ref || m.user)) {
362			tryingNick = m.ref || m.user.id;
363			core.emit("rooms",{id:tryingNick.replace(/^guest-/,"")},function(err,data){
364				if(err) return conn.send('error', {id: m.id, message: err.message});
365				if((data.length>0) || data.id) return conn.send('error', {id: m.id, message: "DUP_NICK"});
366				sendMessage();
367			});
368		} else {
369			sendMessage();
370		}
371	});
372}
373
374
375function room (r, conn) {
376	var user;
377	session.get({sid: conn.sid}, function(err, sess) {
378		if(!conn.sid) return;
379		if(typeof r === 'object') {
380			user = sess.user;
381			r.owner = user.id;
382		}
383		core.emit("room", r, function(err, data) {
384			if(err) {
385				log("ROOM ERROR", r, err);
386				r= {
387  					queryId : r.queryId
388  				};
389				r.message = err.message;
390 				conn.send('error', r);
391			}else{
392				data.query= {
393					queryId : r.queryId
394				};
395				conn.send('room', data);
396			}
397		});
398		session.set(conn.sid, sess);
399	});
400}
401
402
403
404
405function getRooms(query, conn) {
406	core.emit("getRooms", query, function(err, data) {
407		if(err) {
408			query.message = err.message;
409			conn.send('error',query);
410			return;
411		}else {
412			conn.send('getRooms', { query: query, data: data} );
413			//conn.send('rooms', data);	
414		}
415	});
416}
417
418
419function getUsers(query, conn) {
420	core.emit("getUsers", query, function(err, data) {
421		if(err) {
422			query.message = err.message;
423			conn.send('error',query);
424			return;
425		}else {
426			conn.send('getUsers', { query: query, data: data} );
427			//conn.send('rooms', data);	
428		}
429	});
430}
431function rooms(query, conn) {
432	core.emit("rooms", query, function(err, data) {
433		if(err) {
434			log("ROOMS ERROR", query, err);
435			query.message = err.message;
436			conn.send('error',query);
437			return;
438		}else {
439			conn.send('rooms', { query: query, data: data} );
440			//conn.send('rooms', data);	
441		}
442	});
443}
444
445function validateNick(nick){
446	if (nick.indexOf("guest-")===0) return false;
447	return (nick.match(/^[a-z][a-z0-9\_\-\(\)]{2,32}$/i)?nick!='img'&&nick!='css'&&nick!='sdk':false);
448}
449
450// ----- Outgoing send ----
451
452exports.send = function (message, rooms) {
453	message.text = message.text || "";
454	log("Socket sending", message, "to", rooms);
455	
456	rooms.map(function(room) {
457		var location, to = message.to;
458		if(message.origin) {
459			location= message.origin;
460			delete message.origin;
461		}
462		//if(message.type == "text") core.occupants(message.to, function(err, data){console.log(err, data);});
463		if(rConns[room]) rConns[room].map(function(conn) {
464			message.to = room;
465			conn.send('message', message);
466		});
467		if(location) message.origin = location;
468		message.to = to;
469	});
470};
471
472
473exports.emit = function(type, action, room) {
474	if(rConns[room]) rConns[room].map(function(conn) {
475		action.to = room;
476		conn.send(type, action);
477	});		
478};