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
npm-debug.log
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"
- sleep 5 # give xvfb some time to start
- gulp bundle
- node index.js &
- npm start &
- 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(
'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'}
);

View File

@ -12,6 +12,7 @@
"forever": "^0.15.1",
"gulp": "^3.9.0",
"gulp-uglify": "^1.5.1",
"he": "^0.5.0",
"moment": "^2.11.2",
"mustache-express": "^1.2.2",
"sanitize-html": "^1.11.3",
@ -39,13 +40,10 @@
"vinyl-source-stream": "^1.1.0"
},
"scripts": {
"dev": "gulp start",
"test": "gulp test"
},
"scripts": {
"start": "gulp start",
"start": "npm run bundle && gulp start",
"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",
"license": "MIT"

View File

@ -1,5 +1,7 @@
# 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).
### Installation
@ -10,11 +12,12 @@ Simple encrypted web chat. Powered by [socket.io](http://socket.io) and the [web
# Bundle JS files (for deployment)
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
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`
### Deployment

View File

@ -1,5 +1,6 @@
import _ from 'underscore';
import sanitizeHtml from 'sanitize-html';
import he from 'he';
export default class Chat {
constructor(darkwire, socket) {
@ -172,11 +173,10 @@ export default class Chat {
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 = {
username: window.username,
newUsername: socketData.username,
publicKey: socketData.publicKey
newUsername: socketData.username
};
this.socket.emit('update user', modifiedSocketData);
@ -302,7 +302,10 @@ export default class Chat {
if (messageType === 'action') {
$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 {
$messageBodyDiv.html(this.darkwire.addFileToQueue(data));
}

View File

@ -51,12 +51,35 @@ export default class Darkwire {
return this._connected;
}
get audio() {
return this._audio;
}
get users() {
return this._users;
}
get audio() {
return this._audio;
getUserById(id) {
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) {
@ -97,27 +120,40 @@ export default class Darkwire {
return this._users;
}
updateUsername(username) {
updateUsername(username, newUsername) {
let user = null;
return new Promise((resolve, reject) => {
if (newUsername) {
user = this.getUserByName(username);
}
if (username) {
Promise.all([
this._cryptoUtil.createPrimaryKeys()
])
.then((data) => {
this._keys = {
public: data[0].publicKey,
private: data[0].privateKey
};
return Promise.all([
this._cryptoUtil.exportKey(data[0].publicKey, 'spki')
]);
})
.then((exportedKeys) => {
resolve({
username: username,
publicKey: exportedKeys[0]
if (!user) {
Promise.all([
this._cryptoUtil.createPrimaryKeys()
])
.then((data) => {
this._keys = {
public: data[0].publicKey,
private: data[0].privateKey
};
return Promise.all([
this._cryptoUtil.exportKey(data[0].publicKey, 'spki')
]);
})
.then((exportedKeys) => {
resolve({
username: username,
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 moment from 'moment';
import sanitizeHtml from 'sanitize-html';
import he from 'he';
let fs = window.RequestFileSystem || window.webkitRequestFileSystem;
@ -61,25 +62,21 @@ $(function() {
// Prevents input from having injected markup
function cleanInput(input) {
let message = sanitizeHtml(_.escape(input), {
allowedTags: ['b', 'i', 'em', 'strong', 'a'],
allowedAttributes: {
'a': ['href']
}
});
// let message = $('<div/>').html(input).text();
message = Autolinker.link(message);
return _.escape(message);
input = input.replace(/\r?\n/g, '<br />');
let sanitized = he.encode(input);
sanitized = Autolinker.link(sanitized);
return sanitized;
}
// Keyboard events
$window.keydown(function(event) {
// 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();
socket.emit('stop typing');
chat.typing = false;
event.preventDefault();
}
});
@ -107,7 +104,10 @@ $(function() {
});
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
@ -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) {
const fileId = context.getAttribute('data-file');
if (fileId) {

View File

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

View File

@ -9,7 +9,7 @@ html {
-webkit-font-smoothing: antialiased;
}
html, body, input {
html, body, input, textarea {
font-family:
"SourceCodePro-Regular",
"HelveticaNeue-Light",
@ -160,6 +160,10 @@ input {
font-size: 16px;
}
textarea {
font-size: 20px;
}
.log {
color: gray;
font-size: 70%;
@ -196,19 +200,24 @@ input {
/* Input */
.inputContainer {
position: fixed;
right: 0;
left: 0;
bottom: 0;
width: 100%;
padding-bottom: 10px;
}
.inputMessage{
background: black !important;
color: white !important;
border: none;
border-top: 1px solid #282828;
bottom: 0;
height: 60px;
left: 0;
outline: none;
padding-left: 10px;
position: fixed;
right: 0;
width: 100%;
padding: 10px 75px 10px 10px;
/*Fix for inner shadow on iOS*/
-webkit-appearance: none;
border-radius: 0px;
@ -218,7 +227,7 @@ input {
position: fixed;
bottom: 0px;
right: 0px;
padding: 15px;
padding: 22px;
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,27 +93,22 @@ class Room {
if (data.newUsername.length > 16) {
return false;
}
this.users = _.without(this.users, socket.user);
let modifiedUser = {
id: socket.user.id,
username: data.newUsername,
publicKey: data.publicKey
};
this.users.push(modifiedUser);
socket.username = data.newUsername;
socket.user = modifiedUser;
thisIO.emit('user update', {
id: socket.user.id,
username: data.username,
newUsername: data.newUsername,
publicKey: data.publicKey,
users: this.users,
timestamp: new Date()
let user = _.find(this.users, (users) => {
return users === socket.user;
});
if (user) {
user.username = data.newUsername;
socket.username = user.username;
socket.user = user;
thisIO.emit('user update', {
username: socket.username,
id: socket.user.id,
timestamp: new Date()
});
}
});
});

View File

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

View File

@ -1,7 +1,11 @@
/*jshint -W030 */
import App from '../../package.json';
describe('Darkwire', () => {
describe('starting a room', () => {
describe('Creating a room', () => {
var testingRoom = null;
let browser;
before((client, done) => {
@ -25,31 +29,31 @@ describe('Darkwire', () => {
done();
});
it('should show welcome modal', () => {
it('Should show welcome modal', () => {
browser
.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', () => {
browser.expect.element('#first-modal .modal-title').text.to.equal('Welcome to darkwire.io');
it('Should be started with NPM', () => {
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) => {
browser.url((result) => {
let urlSplit = result.value.split('/');
let roomId = urlSplit[urlSplit.length - 1];
let url = 'http://localhost:3000/' + roomId;
testingRoom = urlSplit[urlSplit.length - 1];
let url = 'http://localhost:3000/' + testingRoom;
browser.execute(() => {
window.open('http://localhost:3000/', '_blank');
}, [], () => {
browser.window_handles((result) => {
browser.windowHandles((result) => {
browser.switchWindow(result.value[1], () => {
browser.execute((id) => {
window.open('http://localhost:3000/' + id, '_self');
}, [roomId], () => {
}, [testingRoom], () => {
done();
});
});
@ -58,25 +62,26 @@ describe('Darkwire', () => {
});
});
it('should not show welcome modal', () => {
it('Should not show welcome modal', () => {
browser.assert.hidden('#first-modal');
});
describe('sending messages', () => {
describe('Sending chat message', () => {
before((client, done) => {
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();
});
});
});
it('should work', () => {
browser.window_handles((result) => {
it('Should send a message', () => {
browser.windowHandles((result) => {
browser.switchWindow(result.value[0], () => {
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