From d8bae84fff7977844fa2b57b15b86a831b73aed2 Mon Sep 17 00:00:00 2001 From: Pacien TRAN-GIRARD Date: Thu, 15 May 2014 16:00:45 +0200 Subject: First version --- .project | 18 +++++ Procfile | 1 + README.md | 8 ++ package.json | 33 ++++++++ views/channel.html | 28 +++++++ views/client.html | 83 ++++++++++++++++++++ views/home.html | 31 ++++++++ views/layout.html | 11 +++ webcastor.js | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 429 insertions(+) create mode 100644 .project create mode 100644 Procfile create mode 100644 package.json create mode 100644 views/channel.html create mode 100644 views/client.html create mode 100644 views/home.html create mode 100644 views/layout.html create mode 100644 webcastor.js diff --git a/.project b/.project new file mode 100644 index 0000000..45abfac --- /dev/null +++ b/.project @@ -0,0 +1,18 @@ + + + Webcastor + + + + + + com.eclipsesource.jshint.ui.builder + + + + + + org.nodeclipse.ui.NodeNature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..d1c50f6 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node webcastor.js \ No newline at end of file diff --git a/README.md b/README.md index 0e2474a..4f55f2a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ Webcastor ========= + +Webcastor is a simple websocket broadcaster based on socket.io and node.js. + +When a broadcaster sends a message to a channel, it is broadcast to all other clients (including other broadcasters) connected to the channel. + +Channels are created with a generated ID and an optional broadcaster password. + +This application is currently running on [https://webcastor.herokuapp.com/](https://webcastor.herokuapp.com/). diff --git a/package.json b/package.json new file mode 100644 index 0000000..da6dad6 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "Webcastor", + "version": "0.1.0", + "description": "Webcastor", + "main": "webcastor.js", + "scripts": { + "test": "echo \"Error: no test specified! Configure in package.json\" && exit 1" + }, + "repository": "", + "keywords": [ + "node.js", + "eclipse", + "nodeclipse" + ], + "author": "", + "license": "MIT", + "readmeFilename": "README.md", + "dependencies": { + "body-parser": "^1.1.1", + "express": "^4.1.2", + "hogan-express": "^0.5.2", + "http": "0.0.0", + "logfmt": "^1.1.2", + "password-hash": "^1.2.2", + "redis": "^0.10.1", + "socket.io": "^0.9.16", + "socket.io-wildcard": "^0.1.1", + "url": "^0.10.1" + }, + "engines": { + "node": "0.10.x" + } +} diff --git a/views/channel.html b/views/channel.html new file mode 100644 index 0000000..a1c13b5 --- /dev/null +++ b/views/channel.html @@ -0,0 +1,28 @@ +

Create a channel

+ +

Channel created with given password

+ +
+ + +
+ + +
+ + + + diff --git a/views/client.html b/views/client.html new file mode 100644 index 0000000..c322042 --- /dev/null +++ b/views/client.html @@ -0,0 +1,83 @@ +

Test client

+ +
+ + + + + +
+
+
+ + + +
+ +
+Log +

+
+ 
+
\ No newline at end of file
diff --git a/views/home.html b/views/home.html
new file mode 100644
index 0000000..f6fef09
--- /dev/null
+++ b/views/home.html
@@ -0,0 +1,31 @@
+

Create a channel

+ +
+ + + + +
+ +
+
+ +Test client - Sources + + diff --git a/views/layout.html b/views/layout.html new file mode 100644 index 0000000..c929bc9 --- /dev/null +++ b/views/layout.html @@ -0,0 +1,11 @@ + + + + + Webcastor + + +

Webcastor

+ {{{ yield }}} + + \ No newline at end of file diff --git a/webcastor.js b/webcastor.js new file mode 100644 index 0000000..67b96d3 --- /dev/null +++ b/webcastor.js @@ -0,0 +1,216 @@ +/* + * This file is part of Webcastor . + + * Webcastor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * Webcastor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with Webcastor. If not, see . + */ + +var imports = { + http : require('http'), + url : require('url'), + + express : require('express'), + logfmt : require('logfmt'), + bodyParser : require('body-parser'), + hogan : require('hogan-express'), + + socketio : require('socket.io'), + socketioWildcard : require('socket.io-wildcard'), + + redis : require('redis'), + + passwordHash : require('password-hash'), +}; + +var params = { + database : { + persistent : process.env.REDISCLOUD_URL !== undefined + && process.env.REDISCLOUD_URL !== null, + server : process.env.REDISCLOUD_URL, + }, + + server : { + port : (process.env.PORT !== undefined) ? process.env.PORT : '8080', + }, + + messageSizeLimit : 8000, +}; + +var VolatileKeyValueStore = { + create : function() { + var self = Object.create(this); + this.store = {}; + return self; + }, + + set : function(key, value) { + this.store[key] = value; + }, + + get : function(key, callback) { + var value = this.store[key]; + callback(null, value); + }, +}; + +var Channel = { + create : function(password) { + var uniqueName = this.generateUniqueName(); + this.open(uniqueName, password); + return uniqueName; + }, + + open : function(name, password) { + var hashedPassword = imports.passwordHash.generate(password); + Server.db.set(name, hashedPassword); + }, + + generateUniqueName : function() { + var uniqueName; + while (uniqueName === this.previousName) { + uniqueName = (+new Date()).toString(36).toUpperCase(); + } + this.previousName = uniqueName; + return uniqueName; + }, + + getPassword : function(name, callback) { + Server.db.get(name, function(err, hashedPassword) { + callback(hashedPassword); + }); + }, +}; + +var Server = { + init : function() { + if (params.database.persistent) { + this.db = this.connectToDb(); + } else { + this.db = VolatileKeyValueStore.create(); + } + + this.app = this.createApp(); + this.server = imports.http.createServer(this.app); + this.io = imports.socketioWildcard(imports.socketio) + .listen(this.server); + + this.addHandlers(this.app, this.io); + + this.server.listen(params.server.port); + }, + + createApp : function() { + var app = imports.express(); + + app.use(imports.logfmt.requestLogger()); + app.use(imports.bodyParser()); + app.set('view engine', 'html'); + app.set('layout', 'layout'); + app.enable('view cache'); + app.engine('html', imports.hogan); + + return app; + }, + + connectToDb : function() { + console.log(params.database.server); + var redisURL = imports.url.parse(params.database.server); + console.log(redisURL); + var db = imports.redis.createClient(redisURL.port, redisURL.hostname, { + no_ready_check : true + }); + db.auth(redisURL.auth.split(':')[1]); + db.on('error', function(err) { + console.log('Redis error encountered: ', err); + }); + + return db; + }, + + addHandlers : function(app, io) { + app.get('/client', function(req, res) { + res.render('client'); + }); + + app.get('/', function(req, res) { + res.render('home'); + }); + + app.post('/', function(req, res) { + var password = req.body.password; + if (password === null) { + password = ''; + } + + var channelName = Channel.create(password); + + console.log('Created new channel ' + channelName); + + res.render('channel', { + channel : channelName, + url : req.protocol + '://' + req.host + '/' + channelName, + }); + }); + + io.sockets.on('connection', function(socket) { + var query = imports.url.parse(socket.handshake.url, true).query; + var channel = query.channel; + var password = query.password; + + console.log('Incomming connection on ' + channel); + Server.handleSocket(socket, channel, password); + }); + }, + + handleSocket : function(socket, channel, password) { + Channel.getPassword(channel, function(hashedPassword) { + + if (hashedPassword === null) { + console.log('Client joined and unknown channel'); + return; + } + + console.log(hashedPassword); + + socket.join(channel); + + if (!imports.passwordHash.verify(password, hashedPassword)) { + console.log('Client joined ' + channel); + return; + } + + console.log('Broadcaster joined ' + channel); + + socket.on('*', function(event) { + Server.broadcast(socket, channel, event); + }); + }); + }, + + broadcast : function(socket, channel, event) { + var JSONmessage = JSON.stringify(event); + + var messageSize = encodeURI(JSONmessage).split(/%..|./).length - 1; + if (messageSize > params.messageSizeLimit) { + console.log('Not broadcasting ' + JSONmessage + ' (' + messageSize + + ' bytes)'); + return; + } + + console.log('Broadcasting ' + JSONmessage + ' (' + messageSize + + ' bytes)'); + socket.broadcast.to(channel).emit(event.name, event.args); + }, +}; + +Server.init(); -- cgit v1.2.3