aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPacien TRAN-GIRARD2014-05-15 16:00:45 +0200
committerPacien TRAN-GIRARD2014-05-15 16:00:45 +0200
commitd8bae84fff7977844fa2b57b15b86a831b73aed2 (patch)
treed243a1729f0239421c6454f0b76243b6e1729ce7
parent4f21da45d77211da1aab786bceda8b19f9ccbcbc (diff)
downloadwebcastor-d8bae84fff7977844fa2b57b15b86a831b73aed2.tar.gz
First version
-rw-r--r--.project18
-rw-r--r--Procfile1
-rw-r--r--README.md8
-rw-r--r--package.json33
-rw-r--r--views/channel.html28
-rw-r--r--views/client.html83
-rw-r--r--views/home.html31
-rw-r--r--views/layout.html11
-rw-r--r--webcastor.js216
9 files changed, 429 insertions, 0 deletions
diff --git a/.project b/.project
new file mode 100644
index 0000000..45abfac
--- /dev/null
+++ b/.project
@@ -0,0 +1,18 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<projectDescription>
3 <name>Webcastor</name>
4 <comment></comment>
5 <projects>
6 </projects>
7 <buildSpec>
8 <buildCommand>
9 <name>com.eclipsesource.jshint.ui.builder</name>
10 <arguments>
11 </arguments>
12 </buildCommand>
13 </buildSpec>
14 <natures>
15 <nature>org.nodeclipse.ui.NodeNature</nature>
16 <nature>org.eclipse.wst.jsdt.core.jsNature</nature>
17 </natures>
18</projectDescription>
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 @@
1Webcastor 1Webcastor
2========= 2=========
3
4Webcastor is a simple websocket broadcaster based on socket.io and node.js.
5
6When a broadcaster sends a message to a channel, it is broadcast to all other clients (including other broadcasters) connected to the channel.
7
8Channels are created with a generated ID and an optional broadcaster password.
9
10This 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 @@
1{
2 "name": "Webcastor",
3 "version": "0.1.0",
4 "description": "Webcastor",
5 "main": "webcastor.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified! Configure in package.json\" && exit 1"
8 },
9 "repository": "",
10 "keywords": [
11 "node.js",
12 "eclipse",
13 "nodeclipse"
14 ],
15 "author": "",
16 "license": "MIT",
17 "readmeFilename": "README.md",
18 "dependencies": {
19 "body-parser": "^1.1.1",
20 "express": "^4.1.2",
21 "hogan-express": "^0.5.2",
22 "http": "0.0.0",
23 "logfmt": "^1.1.2",
24 "password-hash": "^1.2.2",
25 "redis": "^0.10.1",
26 "socket.io": "^0.9.16",
27 "socket.io-wildcard": "^0.1.1",
28 "url": "^0.10.1"
29 },
30 "engines": {
31 "node": "0.10.x"
32 }
33}
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 @@
1<h2>Create a channel</h2>
2
3<h3>Channel created with given password</h3>
4
5<form>
6 <label for="channel">Channel ID</label>
7 <input type="text" value="{{channel}}" readonly id="channel">
8 <br>
9 <label for="url">URL</label>
10 <input type="text" value="{{url}}" readonly id="url">
11</form>
12
13<style type="text/css">
14 label {
15 display: inline-block;
16 width: 100px;
17 }
18</style>
19
20<script type="text/javascript">
21 channel.addEventListener("click", function(event) {
22 this.select();
23 });
24
25 url.addEventListener("click", function(event) {
26 this.select();
27 });
28</script>
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 @@
1<h2>Test client</h2>
2
3<form>
4 <label for="channelField">Channel</label>
5 <input type="text" id="channelField">
6 <label for="passwordField">Password (optional)</label>
7 <input type="password" id="passwordField">
8 <input type="submit" value="Connect" id="connectButton">
9</form>
10<br>
11<form>
12 <label for="messageField">Message</label>
13 <input type="text" disabled id="messageField">
14 <input type="submit" value="Send" disabled id="sendButton">
15</form>
16
17<hr>
18<span>Log</span>
19<pre id="console"></pre>
20
21 <script src="/socket.io/socket.io.js"></script>
22<script type="text/javascript">
23 var console = document.getElementById("console");
24
25 function println(str) {
26 console.innerHTML = str + "\n" + console.innerHTML;
27 }
28
29 function changeControlState(connected) {
30 socketConnected = connected;
31 channelField.disabled = connected;
32 passwordField.disabled = connected;
33 if (connected) {
34 connectButton.value = "Disconnect";
35 } else {
36 connectButton.value = "Connect";
37 }
38
39 messageField.disabled = !connected;
40 sendButton.disabled = !connected;
41 }
42
43 var socket;
44 var socketConnected = false;
45
46 function connectSocket(channel, password) {
47 socket = io.connect("/", {
48 "query" : "channel=" + channel + "&password=" + password,
49 "force new connection" : true
50 });
51
52 socket.on("connect", function() {
53 changeControlState(true);
54 println("connected");
55 });
56
57 socket.on("disconnect", function() {
58 changeControlState(false);
59 println("disconnected");
60 });
61
62 socket.on("message", function(message) {
63 println(message);
64 });
65 }
66
67
68 connectButton.addEventListener("click", function(event) {
69 event.preventDefault();
70 if (socketConnected) {
71 socket.disconnect();
72 } else {
73 connectSocket(channelField.value, passwordField.value);
74 }
75 });
76
77 sendButton.addEventListener("click", function(event) {
78 event.preventDefault();
79 socket.emit("message", messageField.value);
80 messageField.value = "";
81 });
82
83</script> \ 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 @@
1<h2>Create a channel</h2>
2
3<form method="post" action="/">
4 <label for="passwordField">Password (optional)</label>
5 <input type="password" id="passwordField" name="password">
6 <input type="submit" id="randomButton" value="Generate random password">
7 <input type="submit" value="Create a new channel">
8</form>
9
10<br>
11<hr>
12
13<a href="/client">Test client</a> - <a href="https://github.com/Pacien/Webcastor">Sources</a>
14
15<script type="text/javascript">
16 randomButton.addEventListener("click", function(event) {
17 event.preventDefault();
18
19 var randomPassword = Math.random().toString(36).substr(2, 10);
20
21 passwordField.type = "text";
22 passwordField.value = randomPassword;
23 });
24
25 passwordField.addEventListener("input", function(event) {
26 if (passwordField.value.length <= 1) {
27 passwordField.type = "password";
28 }
29 });
30
31</script>
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 @@
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="UTF-8">
5 <title>Webcastor</title>
6 </head>
7 <body>
8 <h1>Webcastor</h1>
9 {{{ yield }}}
10 </body>
11</html> \ 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 @@
1/*
2 * This file is part of Webcastor <https://github.com/Pacien/Webcastor>.
3
4 * Webcastor is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8
9 * Webcastor is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13
14 * You should have received a copy of the GNU General Public License
15 * along with Webcastor. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18var imports = {
19 http : require('http'),
20 url : require('url'),
21
22 express : require('express'),
23 logfmt : require('logfmt'),
24 bodyParser : require('body-parser'),
25 hogan : require('hogan-express'),
26
27 socketio : require('socket.io'),
28 socketioWildcard : require('socket.io-wildcard'),
29
30 redis : require('redis'),
31
32 passwordHash : require('password-hash'),
33};
34
35var params = {
36 database : {
37 persistent : process.env.REDISCLOUD_URL !== undefined
38 && process.env.REDISCLOUD_URL !== null,
39 server : process.env.REDISCLOUD_URL,
40 },
41
42 server : {
43 port : (process.env.PORT !== undefined) ? process.env.PORT : '8080',
44 },
45
46 messageSizeLimit : 8000,
47};
48
49var VolatileKeyValueStore = {
50 create : function() {
51 var self = Object.create(this);
52 this.store = {};
53 return self;
54 },
55
56 set : function(key, value) {
57 this.store[key] = value;
58 },
59
60 get : function(key, callback) {
61 var value = this.store[key];
62 callback(null, value);
63 },
64};
65
66var Channel = {
67 create : function(password) {
68 var uniqueName = this.generateUniqueName();
69 this.open(uniqueName, password);
70 return uniqueName;
71 },
72
73 open : function(name, password) {
74 var hashedPassword = imports.passwordHash.generate(password);
75 Server.db.set(name, hashedPassword);
76 },
77
78 generateUniqueName : function() {
79 var uniqueName;
80 while (uniqueName === this.previousName) {
81 uniqueName = (+new Date()).toString(36).toUpperCase();
82 }
83 this.previousName = uniqueName;
84 return uniqueName;
85 },
86
87 getPassword : function(name, callback) {
88 Server.db.get(name, function(err, hashedPassword) {
89 callback(hashedPassword);
90 });
91 },
92};
93
94var Server = {
95 init : function() {
96 if (params.database.persistent) {
97 this.db = this.connectToDb();
98 } else {
99 this.db = VolatileKeyValueStore.create();
100 }
101
102 this.app = this.createApp();
103 this.server = imports.http.createServer(this.app);
104 this.io = imports.socketioWildcard(imports.socketio)
105 .listen(this.server);
106
107 this.addHandlers(this.app, this.io);
108
109 this.server.listen(params.server.port);
110 },
111
112 createApp : function() {
113 var app = imports.express();
114
115 app.use(imports.logfmt.requestLogger());
116 app.use(imports.bodyParser());
117 app.set('view engine', 'html');
118 app.set('layout', 'layout');
119 app.enable('view cache');
120 app.engine('html', imports.hogan);
121
122 return app;
123 },
124
125 connectToDb : function() {
126 console.log(params.database.server);
127 var redisURL = imports.url.parse(params.database.server);
128 console.log(redisURL);
129 var db = imports.redis.createClient(redisURL.port, redisURL.hostname, {
130 no_ready_check : true
131 });
132 db.auth(redisURL.auth.split(':')[1]);
133 db.on('error', function(err) {
134 console.log('Redis error encountered: ', err);
135 });
136
137 return db;
138 },
139
140 addHandlers : function(app, io) {
141 app.get('/client', function(req, res) {
142 res.render('client');
143 });
144
145 app.get('/', function(req, res) {
146 res.render('home');
147 });
148
149 app.post('/', function(req, res) {
150 var password = req.body.password;
151 if (password === null) {
152 password = '';
153 }
154
155 var channelName = Channel.create(password);
156
157 console.log('Created new channel ' + channelName);
158
159 res.render('channel', {
160 channel : channelName,
161 url : req.protocol + '://' + req.host + '/' + channelName,
162 });
163 });
164
165 io.sockets.on('connection', function(socket) {
166 var query = imports.url.parse(socket.handshake.url, true).query;
167 var channel = query.channel;
168 var password = query.password;
169
170 console.log('Incomming connection on ' + channel);
171 Server.handleSocket(socket, channel, password);
172 });
173 },
174
175 handleSocket : function(socket, channel, password) {
176 Channel.getPassword(channel, function(hashedPassword) {
177
178 if (hashedPassword === null) {
179 console.log('Client joined and unknown channel');
180 return;
181 }
182
183 console.log(hashedPassword);
184
185 socket.join(channel);
186
187 if (!imports.passwordHash.verify(password, hashedPassword)) {
188 console.log('Client joined ' + channel);
189 return;
190 }
191
192 console.log('Broadcaster joined ' + channel);
193
194 socket.on('*', function(event) {
195 Server.broadcast(socket, channel, event);
196 });
197 });
198 },
199
200 broadcast : function(socket, channel, event) {
201 var JSONmessage = JSON.stringify(event);
202
203 var messageSize = encodeURI(JSONmessage).split(/%..|./).length - 1;
204 if (messageSize > params.messageSizeLimit) {
205 console.log('Not broadcasting ' + JSONmessage + ' (' + messageSize
206 + ' bytes)');
207 return;
208 }
209
210 console.log('Broadcasting ' + JSONmessage + ' (' + messageSize
211 + ' bytes)');
212 socket.broadcast.to(channel).emit(event.name, event.args);
213 },
214};
215
216Server.init();