Better sanitation of chat messages, support for multiline messages, updated username change method

This commit is contained in:
Dan Seripap 2016-02-24 11:33:54 -05:00
parent 383999b766
commit 57cef2f44f
17 changed files with 400 additions and 109 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store
node_modules node_modules
npm-debug.log npm-debug.log
src/public/main.js src/public/main.js

View File

@ -20,6 +20,6 @@ before_script:
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
- sleep 5 # give xvfb some time to start - sleep 5 # give xvfb some time to start
- gulp bundle - gulp bundle
- node index.js & - npm start &
- sleep 5 - sleep 5
script: node_modules/mocha/bin/mocha test/unit --compilers js:babel-core/register && node_modules/nightwatch/bin/nightwatch --test test/acceptance/index.js --config test/acceptance/nightwatch.json -e chrome script: npm run test-travis

View File

@ -62,7 +62,7 @@ gulp.task('test', function() {
let acceptanceTest = spawn( let acceptanceTest = spawn(
'node_modules/nightwatch/bin/nightwatch', 'node_modules/nightwatch/bin/nightwatch',
['--test', 'test/acceptance/index.js', '--config', 'test/acceptance/nightwatch.json'], ['--test', 'test/acceptance/index.js', '--config', 'test/acceptance/nightwatch-local.json'],
{stdio: 'inherit'} {stdio: 'inherit'}
); );

View File

@ -12,6 +12,7 @@
"forever": "^0.15.1", "forever": "^0.15.1",
"gulp": "^3.9.0", "gulp": "^3.9.0",
"gulp-uglify": "^1.5.1", "gulp-uglify": "^1.5.1",
"he": "^0.5.0",
"moment": "^2.11.2", "moment": "^2.11.2",
"mustache-express": "^1.2.2", "mustache-express": "^1.2.2",
"sanitize-html": "^1.11.3", "sanitize-html": "^1.11.3",
@ -39,13 +40,10 @@
"vinyl-source-stream": "^1.1.0" "vinyl-source-stream": "^1.1.0"
}, },
"scripts": { "scripts": {
"dev": "gulp start", "start": "npm run bundle && gulp start",
"test": "gulp test"
},
"scripts": {
"start": "gulp start",
"bundle": "gulp bundle", "bundle": "gulp bundle",
"test": "gulp test" "test": "npm run bundle && gulp test",
"test-travis": "node_modules/mocha/bin/mocha test/unit --compilers js:babel-core/register && node_modules/nightwatch/bin/nightwatch --test test/acceptance/index.js --config test/acceptance/nightwatch.json -e chrome"
}, },
"author": "Daniel Seripap", "author": "Daniel Seripap",
"license": "MIT" "license": "MIT"

View File

@ -1,5 +1,7 @@
# Darkwire.io # Darkwire.io
[![Build Status](https://travis-ci.org/seripap/darkwire.io.svg?branch=master)](https://travis-ci.org/seripap/darkwire.io) [![GitHub release](https://img.shields.io/github/release/seripap/darkwire.io.svg)]()
Simple encrypted web chat. Powered by [socket.io](http://socket.io) and the [web cryptography API](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto). Simple encrypted web chat. Powered by [socket.io](http://socket.io) and the [web cryptography API](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto).
### Installation ### Installation
@ -10,11 +12,12 @@ Simple encrypted web chat. Powered by [socket.io](http://socket.io) and the [web
# Bundle JS files (for deployment) # Bundle JS files (for deployment)
npm bundle npm bundle
# Start a local instance of darkwire # Running tests locally
brew install chromedriver # Installs chromedriver to /usr/local/bin
npm test
# Start a local instance of darkwire / for dev
npm start npm start
Create a **.secret** file in the **/src** folder with a your session secret. It doesn't matter what it is- just keep it private.
Darkwire is now running on `http://localhost:3000` Darkwire is now running on `http://localhost:3000`
### Deployment ### Deployment

View File

@ -1,5 +1,6 @@
import _ from 'underscore'; import _ from 'underscore';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import he from 'he';
export default class Chat { export default class Chat {
constructor(darkwire, socket) { constructor(darkwire, socket) {
@ -172,11 +173,10 @@ export default class Chat {
return this.log('Username must start with a letter or number.', {error: true}); return this.log('Username must start with a letter or number.', {error: true});
} }
this.darkwire.updateUsername(newUsername).then((socketData) => { this.darkwire.updateUsername(window.username, newUsername).then((socketData) => {
let modifiedSocketData = { let modifiedSocketData = {
username: window.username, username: window.username,
newUsername: socketData.username, newUsername: socketData.username
publicKey: socketData.publicKey
}; };
this.socket.emit('update user', modifiedSocketData); this.socket.emit('update user', modifiedSocketData);
@ -302,7 +302,10 @@ export default class Chat {
if (messageType === 'action') { if (messageType === 'action') {
$usernameDiv.css('color','').prepend('*'); $usernameDiv.css('color','').prepend('*');
} }
$messageBodyDiv.html(unescape(data.message)); let unescapedMessage = unescape(data.message);
let lineBreaks = /<br \/>/g;
unescapedMessage = unescapedMessage.replace(lineBreaks, '<br />');
$messageBodyDiv.html(unescapedMessage);
} else { } else {
$messageBodyDiv.html(this.darkwire.addFileToQueue(data)); $messageBodyDiv.html(this.darkwire.addFileToQueue(data));
} }

View File

@ -51,12 +51,35 @@ export default class Darkwire {
return this._connected; return this._connected;
} }
get audio() {
return this._audio;
}
get users() { get users() {
return this._users; return this._users;
} }
get audio() { getUserById(id) {
return this._audio; return _.findWhere(this._users, {id: id});
}
getUserByName(username) {
return _.findWhere(this._users, {username: username});
}
updateUser(data) {
return new Promise((resolve, reject) => {
let user = this.getUserById(data.id);
if (!user) {
return reject();
}
let oldUsername = user.username;
user.username = data.username;
resolve(oldUsername);
});
} }
addUser(data) { addUser(data) {
@ -97,9 +120,16 @@ export default class Darkwire {
return this._users; return this._users;
} }
updateUsername(username) { updateUsername(username, newUsername) {
let user = null;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (newUsername) {
user = this.getUserByName(username);
}
if (username) { if (username) {
if (!user) {
Promise.all([ Promise.all([
this._cryptoUtil.createPrimaryKeys() this._cryptoUtil.createPrimaryKeys()
]) ])
@ -118,6 +148,12 @@ export default class Darkwire {
publicKey: exportedKeys[0] publicKey: exportedKeys[0]
}); });
}); });
} else {
resolve({
username: newUsername,
publicKey: user.publicKey
});
}
} }
}); });
} }

View File

@ -5,6 +5,7 @@ import CryptoUtil from './crypto';
import Chat from './chat'; import Chat from './chat';
import moment from 'moment'; import moment from 'moment';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import he from 'he';
let fs = window.RequestFileSystem || window.webkitRequestFileSystem; let fs = window.RequestFileSystem || window.webkitRequestFileSystem;
@ -61,25 +62,21 @@ $(function() {
// Prevents input from having injected markup // Prevents input from having injected markup
function cleanInput(input) { function cleanInput(input) {
let message = sanitizeHtml(_.escape(input), { input = input.replace(/\r?\n/g, '<br />');
allowedTags: ['b', 'i', 'em', 'strong', 'a'], let sanitized = he.encode(input);
allowedAttributes: { sanitized = Autolinker.link(sanitized);
'a': ['href'] return sanitized;
}
});
// let message = $('<div/>').html(input).text();
message = Autolinker.link(message);
return _.escape(message);
} }
// Keyboard events // Keyboard events
$window.keydown(function(event) { $window.keydown(function(event) {
// When the client hits ENTER on their keyboard and chat message input is focused // When the client hits ENTER on their keyboard and chat message input is focused
if (event.which === 13 && $('.inputMessage').is(':focus')) { if (event.which === 13 && !event.shiftKey && $('.inputMessage').is(':focus')) {
handleMessageSending(); handleMessageSending();
socket.emit('stop typing'); socket.emit('stop typing');
chat.typing = false; chat.typing = false;
event.preventDefault();
} }
}); });
@ -107,7 +104,10 @@ $(function() {
}); });
socket.on('user update', (data) => { socket.on('user update', (data) => {
updateUser(data); darkwire.updateUser(data).then((oldUsername) => {
chat.log(oldUsername + ' changed name to ' + data.username);
renderParticipantsList();
});
}); });
// Whenever the server emits 'new message', update the chat body // Whenever the server emits 'new message', update the chat body
@ -227,20 +227,6 @@ $(function() {
}); });
} }
function updateUser(data) {
let logMessage = data.username + ' changed name to ';
darkwire.removeUser(data);
data.username = data.newUsername;
logMessage += data.username;
let importKeysPromises = darkwire.addUser(data);
Promise.all(importKeysPromises).then(() => {
chat.log(logMessage);
renderParticipantsList();
});
}
window.triggerFileTransfer = function(context) { window.triggerFileTransfer = function(context) {
const fileId = context.getAttribute('data-file'); const fileId = context.getAttribute('data-file');
if (fileId) { if (fileId) {

View File

@ -31,7 +31,6 @@ export default class WindowHandler {
enableFileTransfer() { enableFileTransfer() {
if (this.fileHandler.isSupported) { if (this.fileHandler.isSupported) {
console.log('enabled');
$('#send-file').click((e) => { $('#send-file').click((e) => {
e.preventDefault(); e.preventDefault();
$('#fileInput').trigger('click'); $('#fileInput').trigger('click');

View File

@ -9,7 +9,7 @@ html {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
html, body, input { html, body, input, textarea {
font-family: font-family:
"SourceCodePro-Regular", "SourceCodePro-Regular",
"HelveticaNeue-Light", "HelveticaNeue-Light",
@ -160,6 +160,10 @@ input {
font-size: 16px; font-size: 16px;
} }
textarea {
font-size: 20px;
}
.log { .log {
color: gray; color: gray;
font-size: 70%; font-size: 70%;
@ -196,19 +200,24 @@ input {
/* Input */ /* Input */
.inputContainer {
position: fixed;
right: 0;
left: 0;
bottom: 0;
width: 100%;
padding-bottom: 10px;
}
.inputMessage{ .inputMessage{
background: black !important; background: black !important;
color: white !important; color: white !important;
border: none; border: none;
border-top: 1px solid #282828; border-top: 1px solid #282828;
bottom: 0;
height: 60px; height: 60px;
left: 0;
outline: none; outline: none;
padding-left: 10px;
position: fixed;
right: 0;
width: 100%; width: 100%;
padding: 10px 75px 10px 10px;
/*Fix for inner shadow on iOS*/ /*Fix for inner shadow on iOS*/
-webkit-appearance: none; -webkit-appearance: none;
border-radius: 0px; border-radius: 0px;
@ -218,7 +227,7 @@ input {
position: fixed; position: fixed;
bottom: 0px; bottom: 0px;
right: 0px; right: 0px;
padding: 15px; padding: 22px;
cursor: pointer; cursor: pointer;
} }

76
src/public/vendor/autogrow.js vendored Normal file
View File

@ -0,0 +1,76 @@
(function($)
{
/**
* Auto-growing textareas; technique ripped from Facebook
*
*
* http://github.com/jaz303/jquery-grab-bag/tree/master/javascripts/jquery.autogrow-textarea.js
*/
$.fn.autogrow = function(options)
{
return this.filter('textarea').each(function()
{
var self = this;
var $self = $(self);
var minHeight = $self.height();
var noFlickerPad = $self.hasClass('autogrow-short') ? 0 : parseInt($self.css('lineHeight')) || 0;
var settings = $.extend({
preGrowCallback: null,
postGrowCallback: null
}, options );
var shadow = $('<div></div>').css({
position: 'absolute',
top: -10000,
left: -10000,
width: $self.width(),
fontSize: $self.css('fontSize'),
fontFamily: $self.css('fontFamily'),
fontWeight: $self.css('fontWeight'),
lineHeight: $self.css('lineHeight'),
resize: 'none',
'word-wrap': 'break-word'
}).appendTo(document.body);
var update = function(event)
{
var times = function(string, number)
{
for (var i=0, r=''; i<number; i++) r += string;
return r;
};
var val = self.value.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n$/, '<br/>&nbsp;')
.replace(/\n/g, '<br/>')
.replace(/ {2,}/g, function(space){ return times('&nbsp;', space.length - 1) + ' ' });
// Did enter get pressed? Resize in this keydown event so that the flicker doesn't occur.
if (event && event.data && event.data.event === 'keydown' && event.keyCode === 13) {
val += '<br />';
}
shadow.css('width', $self.width());
shadow.html(val + (noFlickerPad === 0 ? '...' : '')); // Append '...' to resize pre-emptively.
var newHeight=Math.max(shadow.height() + noFlickerPad, minHeight);
if(settings.preGrowCallback!=null){
newHeight=settings.preGrowCallback($self,shadow,newHeight,minHeight);
}
$self.height(newHeight);
if(settings.postGrowCallback!=null){
settings.postGrowCallback($self);
}
}
$self.change(update).keyup(update).keydown({event:'keydown'},update);
$(window).resize(update);
update();
});
};
})(jQuery);

View File

@ -93,26 +93,21 @@ class Room {
if (data.newUsername.length > 16) { if (data.newUsername.length > 16) {
return false; return false;
} }
this.users = _.without(this.users, socket.user); let user = _.find(this.users, (users) => {
let modifiedUser = { return users === socket.user;
id: socket.user.id, });
username: data.newUsername,
publicKey: data.publicKey
};
this.users.push(modifiedUser); if (user) {
user.username = data.newUsername;
socket.username = data.newUsername; socket.username = user.username;
socket.user = modifiedUser; socket.user = user;
thisIO.emit('user update', { thisIO.emit('user update', {
username: socket.username,
id: socket.user.id, id: socket.user.id,
username: data.username,
newUsername: data.newUsername,
publicKey: data.publicKey,
users: this.users,
timestamp: new Date() timestamp: new Date()
}); });
}
}); });

View File

@ -53,12 +53,14 @@
</li> </li>
</ul> </ul>
</div> </div>
<input class="inputMessage" placeholder="Type here..."/> <div class="inputContainer">
<textarea class="inputMessage" placeholder="Type here..."/></textarea>
<div id="input-icons"> <div id="input-icons">
<span class="glyphicon glyphicon-file" id="send-file"></span> <span class="glyphicon glyphicon-file" id="send-file"></span>
<input type="file" id="fileInput"> <input type="file" name="fileUploader" id="fileInput">
<span class="glyphicon glyphicon-send" id="send-message-btn" aria-hidden="true"></span> <span class="glyphicon glyphicon-send" id="send-message-btn" aria-hidden="true"></span>
</div> </div>
</div>
</li> </li>
</ul> </ul>
@ -173,6 +175,7 @@
<script src="/vendor/bootstrap-switch.min.js"></script> <script src="/vendor/bootstrap-switch.min.js"></script>
<script src="/vendor/web-crypto-shim.js"></script> <script src="/vendor/web-crypto-shim.js"></script>
<script src="/vendor/fastclick-1.0.6.min.js"></script> <script src="/vendor/fastclick-1.0.6.min.js"></script>
<script src="/vendor/autogrow.js"></script>
<script src="/main.js"></script> <script src="/main.js"></script>
</body> </body>
</html> </html>

View File

@ -1,7 +1,11 @@
/*jshint -W030 */
import App from '../../package.json';
describe('Darkwire', () => { describe('Darkwire', () => {
describe('starting a room', () => { describe('Creating a room', () => {
var testingRoom = null;
let browser; let browser;
before((client, done) => { before((client, done) => {
@ -25,31 +29,31 @@ describe('Darkwire', () => {
done(); done();
}); });
it('should show welcome modal', () => { it('Should show welcome modal', () => {
browser browser
.waitForElementVisible('#first-modal', 5000) .waitForElementVisible('#first-modal', 5000)
.assert.containsText('#first-modal .modal-title', 'Welcome to darkwire.io'); .expect.element('#first-modal').to.be.visible;
}); });
it('should have correct header', () => { it('Should be started with NPM', () => {
browser.expect.element('#first-modal .modal-title').text.to.equal('Welcome to darkwire.io'); browser.expect.element('#first-modal .modal-title').text.to.equal('Welcome to darkwire.io v' + App.version);
}); });
describe('opening a second window', () => { describe('Joining chat room', () => {
before((client, done) => { before((client, done) => {
browser.url((result) => { browser.url((result) => {
let urlSplit = result.value.split('/'); let urlSplit = result.value.split('/');
let roomId = urlSplit[urlSplit.length - 1]; testingRoom = urlSplit[urlSplit.length - 1];
let url = 'http://localhost:3000/' + roomId; let url = 'http://localhost:3000/' + testingRoom;
browser.execute(() => { browser.execute(() => {
window.open('http://localhost:3000/', '_blank'); window.open('http://localhost:3000/', '_blank');
}, [], () => { }, [], () => {
browser.window_handles((result) => { browser.windowHandles((result) => {
browser.switchWindow(result.value[1], () => { browser.switchWindow(result.value[1], () => {
browser.execute((id) => { browser.execute((id) => {
window.open('http://localhost:3000/' + id, '_self'); window.open('http://localhost:3000/' + id, '_self');
}, [roomId], () => { }, [testingRoom], () => {
done(); done();
}); });
}); });
@ -58,25 +62,26 @@ describe('Darkwire', () => {
}); });
}); });
it('should not show welcome modal', () => { it('Should not show welcome modal', () => {
browser.assert.hidden('#first-modal'); browser.assert.hidden('#first-modal');
}); });
describe('sending messages', () => { describe('Sending chat message', () => {
before((client, done) => { before((client, done) => {
browser.waitForElementPresent('ul.users li:nth-child(2)', 5000, () => { browser.waitForElementPresent('ul.users li:nth-child(2)', 5000, () => {
browser.setValue('input.inputMessage', ['Hello world', browser.Keys.RETURN], () => { browser.setValue('textarea.inputMessage', ['Hello world!', browser.Keys.RETURN], () => {
done(); done();
}); });
}); });
}); });
it('should work', () => { it('Should send a message', () => {
browser.window_handles((result) => { browser.windowHandles((result) => {
browser.switchWindow(result.value[0], () => { browser.switchWindow(result.value[0], () => {
browser.waitForElementPresent('span.messageBody', 5000, () => { browser.waitForElementPresent('span.messageBody', 5000, () => {
browser.assert.containsText('span.messageBody', 'Hello world'); browser.pause(2000);
browser.assert.containsText('span.messageBody', 'Hello world!');
}); });
}); });
}); });
@ -86,5 +91,129 @@ describe('Darkwire', () => {
}); });
describe('Slash Commands', () => {
before((client, done) => {
let url = 'http://localhost:3000/' + testingRoom;
browser.url(url, () => {
browser.windowHandles((result) => {
browser.switchWindow(result.value[0], () => {
browser.execute((id) => {
window.open('http://localhost:3000/' + id, '_self');
}, [testingRoom], () => {
done();
});
});
});
});
});
describe('/me', () => {
before((client, done) => {
browser.windowHandles((result) => {
browser.switchWindow(result.value[0], () => {
browser.waitForElementPresent('ul.users li:nth-child(2)', 5000, () => {
browser.setValue('textarea.inputMessage', ['/me is no stranger to love', browser.Keys.RETURN], () => {
done();
});
});
});
});
});
it('Should express an interactive action', () => {
browser.windowHandles((result) => {
browser.switchWindow(result.value[0], () => {
browser.waitForElementPresent('span.messageBody', 5000, () => {
browser.pause(5000);
browser.assert.containsText('.action span.messageBody', 'is no stranger to love');
});
});
});
});
});
describe('/nick', () => {
before((client, done) => {
browser.url('http://localhost:3000/' + testingRoom, () => {
browser.waitForElementPresent('ul.users li:nth-child(2)', 5000, () => {
browser.setValue('textarea.inputMessage', ['/nick rickAnsley', browser.Keys.RETURN], () => {
done();
});
});
});
});
it('Should change username', () => {
browser.windowHandles((result) => {
browser.switchWindow(result.value[3], () => {
browser.pause(5000);
browser.assert.containsText('.log:last-child', 'rickAnsley');
});
});
});
});
});
describe('Before file transfer: Image: Confirm sending', () => {
before((client, done) => {
let url = 'http://localhost:3000/' + testingRoom;
browser.url(url, () => {
browser.windowHandles((result) => {
browser.switchWindow(result.value[0], () => {
browser.execute((id) => {
window.open('http://localhost:3000/' + id, '_self');
}, [testingRoom], () => {
browser.waitForElementPresent('#send-file', 5000, () => {
browser.execute(() => {
$('input[name="fileUploader"]').show();
}, [], () => {
browser.waitForElementPresent('input[name="fileUploader"]', 5000, () => {
let testFile = __dirname + '/ricky.jpg';
browser.setValue('input[name="fileUploader"]', testFile, (result) => {
done();
});
});
});
});
});
});
});
});
});
it('Should prompt user confirmation', () => {
browser.windowHandles((result) => {
browser.switchWindow(result.value[0], () => {
browser.waitForElementPresent('span.messageBody', 5000, () => {
browser.pause(5000);
browser.assert.containsText('span.messageBody', 'You are about to send ricky.jpg to all parties in this chat. Confirm | Cancel');
});
});
});
});
it('Should show sent confirmation message', () => {
browser.windowHandles((result) => {
browser.switchWindow(result.value[0], () => {
browser.waitForElementPresent('span.messageBody a:first-child', 5000, () => {
browser.click('span.messageBody a:first-child', () => {
browser.waitForElementNotPresent('span.messageBody a:first-child', 5000, () => {
browser.assert.containsText('span.messageBody', 'Sent ricky.jpg');
});
});
});
});
});
});
});
}); });
}); });

View File

@ -0,0 +1 @@
($('#fileInput').show)();

View File

@ -0,0 +1,52 @@
{
"src_folders" : ["test"],
"output_folder" : "reports",
"custom_commands_path" : "",
"custom_assertions_path" : "",
"page_objects_path" : "",
"globals_path" : "",
"test_runner" : "mocha",
"selenium" : {
"start_process" : true,
"server_path" : "test/acceptance/bin/selenium-server-standalone-2.52.0.jar",
"log_path" : false,
"host" : "127.0.0.1",
"port" : 4444,
"cli_args" : {
"webdriver.chrome.driver" : "/usr/local/bin/chromedriver",
"webdriver.ie.driver" : ""
}
},
"test_settings" : {
"default" : {
"launch_url" : "http://localhost",
"selenium_port" : 4444,
"selenium_host" : "localhost",
"silent": true,
"screenshots" : {
"enabled" : false,
"path" : ""
},
"desiredCapabilities": {
"browserName": "chrome",
"javascriptEnabled": true,
"acceptSslCerts": true,
"chromeOptions" : {
"binary": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
}
}
},
"chrome" : {
"desiredCapabilities": {
"browserName": "chrome",
"javascriptEnabled": true,
"acceptSslCerts": true,
"chromeOptions" : {
"args" : ["-e", "--no-sandbox"]
}
}
}
}
}

BIN
test/acceptance/ricky.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB