diff --git a/gulpfile.babel.js b/gulpfile.babel.js
index 19130e2..ecc1788 100644
--- a/gulpfile.babel.js
+++ b/gulpfile.babel.js
@@ -21,6 +21,17 @@ gulp.task('bundle', function() {
.pipe(gulp.dest('src/public'));
});
+gulp.task('dev', function() {
+ return browserify('src/js/main.js', {
+ debug: true
+ }).transform(babel.configure({
+ presets: ['es2015']
+ })).bundle()
+ .pipe(source('main.js'))
+ .pipe(buffer())
+ .pipe(gulp.dest('src/public'));
+});
+
gulp.task('start', function() {
nodemon({
script: 'index.js',
@@ -29,7 +40,7 @@ gulp.task('start', function() {
env: {
'NODE_ENV': 'development'
},
- tasks: ['bundle']
+ tasks: ['dev']
});
});
diff --git a/package.json b/package.json
index f9b9512..1e85754 100644
--- a/package.json
+++ b/package.json
@@ -11,9 +11,12 @@
"express": "^4.13.3",
"gulp": "^3.9.0",
"gulp-uglify": "^1.5.1",
+ "moment": "^2.11.2",
"mustache-express": "^1.2.2",
+ "sanitize-html": "^1.11.3",
"serve-favicon": "^2.3.0",
"shortid": "^2.2.4",
+ "slug": "^0.9.1",
"socket.io": "^1.4.0",
"underscore": "^1.8.3",
"uuid": "^2.0.1"
@@ -32,6 +35,11 @@
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0"
},
+ "scripts": {
+ "start": "gulp start",
+ "bundle": "gulp bundle",
+ "test": "gulp test"
+ },
"author": "Daniel Seripap",
"license": "MIT"
}
diff --git a/readme.md b/readme.md
index 253f612..c167549 100644
--- a/readme.md
+++ b/readme.md
@@ -3,20 +3,17 @@
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
-
# For es6 compatability, be sure you have the latest stable version of Node JS installed
npm install -g n
n stable
-
npm install
- # Bundle JS files
- npm run bundle
-
+ # Bundle JS files (for deployment)
+ npm bundle
# Start a local instance of darkwire
npm start
-Create a **.secret** file in **/src** folder with a your session secret. It doesn't matter what it is- just keep it private.
+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`
@@ -48,11 +45,11 @@ Darkwire does not provide any guarantee that the person you're communicating wit
## File Transfer
-Files are not transferred over the wire-only the file name and extension. Darkwire encodes documents into base64 using [btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa) and is encrypted the same way chat messages are.
+Darkwire encodes documents (up to 1MB) into base64 using [btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa) and is encrypted the same way chat messages are.
1. When a file is "uploaded", the document is encoded on the client and the server recieves the encrypted base64 string.
2. The server sends the encrypted base64 string to clients in the same chat room.
-3. Clients recieving the encrypted base64 string then decrypts the string, then decodes the base64 string using [atob](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/atob).
+3. Clients recieving the encrypted base64 string then decrypts and decodes the base64 string using [atob](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/atob).
## Sockets & Server
diff --git a/src/app.js b/src/app.js
index 343660a..7d3193d 100644
--- a/src/app.js
+++ b/src/app.js
@@ -50,7 +50,13 @@ app.get('/:roomId', (req, res) => {
let roomExists = _.findWhere(rooms, {_id: roomId}) || false;
if (roomExists) {
- return res.render('index', {username: shortid.generate()});
+ return res.render('index', {
+ APP: {
+ version: process.env.npm_package_version,
+ ip: req.headers['x-forwarded-for']
+ },
+ username: shortid.generate()
+ });
}
return res.redirect('/');
diff --git a/src/js/chat.js b/src/js/chat.js
new file mode 100644
index 0000000..2bf881a
--- /dev/null
+++ b/src/js/chat.js
@@ -0,0 +1,307 @@
+import _ from 'underscore';
+import sanitizeHtml from 'sanitize-html';
+
+export default class Chat {
+ constructor(darkwire, socket) {
+ this.usernamesInMemory = [];
+ this.FADE_TIME = 150; // ms
+ this.TYPING_TIMER_LENGTH = 400; // ms
+ this.typing = false;
+ this.lastTypingTime = null;
+ this.darkwire = darkwire;
+ this.socket = socket;
+ this.messages = $('.messages'); // Messages area
+ this.inputMessage = $('.inputMessage'); // Input message input box
+ this.chatPage = $('.chat.page');
+ this.bindEvents();
+ }
+
+ // Log a message
+ log(message, options) {
+ let html = options && options.html === true || false;
+ let $el;
+
+ let matchedUsernames = this.checkIfUsername(message.split(' '));
+
+ if (matchedUsernames.length > 0) {
+ for (let i = 0; i < matchedUsernames.length; i++) {
+ let usernameContainer = $('')
+ .text(matchedUsernames[i])
+ .css('color', this.getUsernameColor(matchedUsernames[i]));
+ message = message.replace(matchedUsernames[i], usernameContainer.prop('outerHTML'));
+ }
+ }
+
+ if (options && options.error) {
+ $el = $('
').addClass('log').html(message);
+ } else if (options && options.info) {
+ $el = $('').addClass('log').html(message);
+ } else {
+ $el = $('').addClass('log').html(message);
+ }
+
+ this.addMessageElement($el, options);
+ }
+
+ checkIfUsername(words) {
+ let matchedUsernames = [];
+ this.darkwire.users.forEach((user) => {
+ let usernameMatch = new RegExp('^' + user.username + '$', 'g');
+ for (let i = 0; i < words.length; i++) {
+ let exactMatch = words[i].match(usernameMatch) || false;
+ let usernameInMemory = this.usernamesInMemory.indexOf(words[i]) > -1;
+
+ if (exactMatch && exactMatch.length > -1 || usernameInMemory) {
+ if (!usernameInMemory) {
+ this.usernamesInMemory.push(words[i]);
+ }
+ matchedUsernames.push(words[i]);
+ }
+ }
+ });
+ return matchedUsernames;
+ }
+
+ // Gets the color of a username through our hash function
+ getUsernameColor(username) {
+ const COLORS = [
+ '#e21400', '#ffe400', '#ff8f00',
+ '#58dc00', '#dd9cff', '#4ae8c4',
+ '#3b88eb', '#f47777', '#d300e7',
+ '#99FF33', '#99CC33', '#999933',
+ '#996633', '#993333', '#990033',
+ ];
+ // Compute hash code
+ let hash = 7;
+ for (let i = 0; i < username.length; i++) {
+ hash = username.charCodeAt(i) + (hash << 5) - hash;
+ }
+ // Calculate color
+ let index = Math.abs(hash % COLORS.length);
+ return COLORS[index];
+ }
+
+ bindEvents() {
+ var _this = this;
+ // Select message input when clicking message body, unless selecting text
+ this.messages.on('click', () => {
+ if (!this.getSelectedText()) {
+ this.inputMessage.focus();
+ }
+ });
+
+ this.inputMessage.on('input propertychange paste change', function() {
+ _this.updateTyping();
+ let message = $(this).val().trim();
+ if (message.length) {
+ $('#send-message-btn').addClass('active');
+ } else {
+ $('#send-message-btn').removeClass('active');
+ }
+ });
+ }
+
+ // Updates the typing event
+ updateTyping() {
+ if (this.darkwire.connected) {
+ if (!this.typing) {
+ this.typing = true;
+ this.socket.emit('typing');
+ }
+ this.lastTypingTime = (new Date()).getTime();
+
+ setTimeout(() => {
+ let typingTimer = (new Date()).getTime();
+ let timeDiff = typingTimer - this.lastTypingTime;
+ if (timeDiff >= this.TYPING_TIMER_LENGTH && this.typing) {
+ this.socket.emit('stop typing');
+ this.typing = false;
+ }
+ }, this.TYPING_TIMER_LENGTH);
+ }
+ }
+
+ addChatTyping(data) {
+ data.typing = true;
+ data.message = 'is typing';
+ this.addChatMessage(data);
+ }
+
+ getSelectedText() {
+ let text = '';
+ if (typeof window.getSelection != 'undefined') {
+ text = window.getSelection().toString();
+ } else if (typeof document.selection != 'undefined' && document.selection.type == 'Text') {
+ text = document.selection.createRange().text;
+ }
+ return text;
+ }
+
+ getTypingMessages(data) {
+ return $('.typing.message').filter(function(i) {
+ return $(this).data('username') === data.username;
+ });
+ }
+
+ removeChatTyping(data) {
+ this.getTypingMessages(data).fadeOut(function() {
+ $(this).remove();
+ });
+ }
+
+ slashCommands(trigger) {
+ let validCommands = [];
+ let expectedParams = 0;
+ const triggerCommands = [{
+ command: 'nick',
+ description: 'Changes nickname.',
+ paramaters: ['{username}'],
+ usage: '/nick {username}',
+ action: () => {
+ let newUsername = trigger.params[0] || false;
+
+ if (newUsername > 16) {
+ return this.log('Username cannot be greater than 16 characters.', {error: true});
+ }
+
+ // Remove things that arent digits or chars
+ newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
+
+ if (!newUsername.match(/^[A-Z0-9]/i)) {
+ return this.log('Username must start with a letter or number.', {error: true});
+ }
+
+ this.darkwire.updateUsername(newUsername).then((socketData) => {
+ let modifiedSocketData = {
+ username: window.username,
+ newUsername: socketData.username,
+ publicKey: socketData.publicKey
+ };
+
+ this.socket.emit('update user', modifiedSocketData);
+ window.username = username = socketData.username;
+ });
+ }
+ }, {
+ command: 'help',
+ description: 'Shows a list of commands.',
+ paramaters: [],
+ usage: '/help',
+ action: () => {
+ validCommands = validCommands.map((command) => {
+ return '/' + command;
+ });
+
+ this.log('Valid commands: ' + validCommands.join(', '), {info: true});
+ }
+ }];
+
+ const color = () => {
+ const hexTex = new RegExp(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i);
+ };
+
+ triggerCommands.forEach((command) => {
+ validCommands.push(command.command);
+ });
+
+ let commandToTrigger = _.findWhere(triggerCommands, {command: trigger.command});
+
+ if (commandToTrigger) {
+ expectedParams = commandToTrigger.paramaters.length;
+ if (trigger.params.length > expectedParams || trigger.params.length < expectedParams) {
+ return this.log('Missing or too many paramater. Usage: ' + commandToTrigger.usage, {error: true});
+ }
+ return commandToTrigger.action.call();
+ }
+
+ this.log(trigger.command + ' is not a valid command. Type /help for a list of valid commands.', {error: true});
+ return false;
+ }
+
+ executeCommand(trigger) {
+ trigger = trigger || false;
+ if (trigger) {
+ let command = trigger.command;
+ this.slashCommands(trigger);
+ }
+ }
+
+ parseCommand(cleanedMessage) {
+ let trigger = {
+ command: null,
+ params: []
+ };
+
+ if (cleanedMessage.indexOf('/') === 0) {
+ this.inputMessage.val('');
+ let parsedCommand = cleanedMessage.replace('/', '').split(' ');
+ trigger.command = sanitizeHtml(parsedCommand[0]) || null;
+ // Get params
+ if (parsedCommand.length >= 2) {
+ for (let i = 1; i < parsedCommand.length; i++) {
+ trigger.params.push(parsedCommand[i]);
+ }
+ }
+
+ return trigger;
+ }
+
+ return false;
+ }
+
+ addChatMessage(data, options) {
+ if (!data.message.trim().length) {
+ return;
+ }
+
+ let messageType = data.messageType || 'text';
+
+ // Don't fade the message in if there is an 'X was typing'
+ let $typingMessages = this.getTypingMessages(data);
+ options = options || {};
+ if ($typingMessages.length !== 0) {
+ options.fade = false;
+ $typingMessages.remove();
+ }
+
+ let $usernameDiv = $('')
+ .text(data.username)
+ .css('color', this.getUsernameColor(data.username));
+ let $messageBodyDiv = $('');
+ if (messageType !== 'text') {
+ $messageBodyDiv.html(this.darkwire.addFileToQueue(data));
+ } else {
+ $messageBodyDiv.html(unescape(data.message));
+ }
+
+ let typingClass = data.typing ? 'typing' : '';
+
+ let $messageDiv = $('')
+ .data('username', data.username)
+ .addClass(typingClass)
+ .append($usernameDiv, $messageBodyDiv);
+
+ this.addMessageElement($messageDiv, options);
+ }
+
+ addMessageElement(el, options) {
+ let $el = $(el);
+
+ if (!options) {
+ options = {};
+ }
+
+ $el.hide().fadeIn(this.FADE_TIME);
+ this.messages.append($el);
+
+ this.messages[0].scrollTop = this.messages[0].scrollHeight; // minus 60 for key
+ }
+
+ replaceMessage(id, message) {
+ let container = $(id);
+ if (container) {
+ container.html(message);
+ }
+ }
+
+}
diff --git a/src/js/darkwire.js b/src/js/darkwire.js
index 1b13949..ed43389 100644
--- a/src/js/darkwire.js
+++ b/src/js/darkwire.js
@@ -9,7 +9,33 @@ export default class Darkwire {
this._myUserId = false;
this._connected = false;
this._users = [];
+ this._fileQueue = [];
this._keys = {};
+ this._autoEmbedImages = false;
+ }
+
+ getFile(id) {
+ let file = _.findWhere(this._fileQueue, {id: id}) || false;
+
+ if (file) {
+ // TODO: Destroy object from memory when retrieved
+ }
+
+ return file.data;
+ }
+
+ set fileQueue(fileQueue) {
+ this._fileQueue = fileQueue;
+ return this;
+ }
+
+ get autoEmbedImages() {
+ return this._autoEmbedImages;
+ }
+
+ set autoEmbedImages(state) {
+ this._autoEmbedImages = state;
+ return this._autoEmbedImages;
}
get keys() {
@@ -76,9 +102,35 @@ export default class Darkwire {
return this._users;
}
- encodeMessage(message, messageType) {
+ updateUsername(username) {
+ return new Promise((resolve, reject) => {
+ 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]
+ });
+ });
+ }
+ });
+ }
+
+ encodeMessage(message, messageType, additionalData) {
// Don't send unless other users exist
return new Promise((resolve, reject) => {
+ additionalData = additionalData || {};
// if (this._users.length <= 1) {
// console.log('rejected:' + this._users);
// reject();
@@ -95,7 +147,12 @@ export default class Darkwire {
let signature = null;
let signingKey = null;
let encryptedMessageData = null;
+ let messageToEncode = {
+ text: escape(message),
+ additionalData: additionalData
+ };
+ messageToEncode = JSON.stringify(messageToEncode);
// Generate new secret key and vector for each message
this._cryptoUtil.createSecretKey()
.then((key) => {
@@ -146,7 +203,7 @@ export default class Darkwire {
})
.then((data) => {
secretKeys = data;
- messageData = this._cryptoUtil.convertStringToArrayBufferView(message);
+ messageData = this._cryptoUtil.convertStringToArrayBufferView(messageToEncode);
return this._cryptoUtil.signKey(messageData, signingKey);
})
.then((data) => {
@@ -202,7 +259,7 @@ export default class Darkwire {
})
.then((data) => {
decryptedMessageData = data;
- decryptedMessage = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data));
+ decryptedMessage = JSON.parse(this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data)));
return this._cryptoUtil.decryptSigningKey(signingKeyArrayBuffer, this._keys.private);
})
.then((data) => {
@@ -217,10 +274,29 @@ export default class Darkwire {
resolve({
username: username,
message: decryptedMessage,
- messageType: data.messageType
+ messageType: data.messageType,
+ timestamp: data.timestamp
});
}
});
});
}
+
+ generateMessage(fileId, fileName, messageType) {
+ let message = 'is attempting to send you ' + fileName + ' (' + messageType + ')';
+ message += '
WARNING: We cannot strictly verify the integrity of this file, its recipients or its owners. By accepting this file, you are liable for any risks that may arise from reciving this file.';
+ message += '
Accept File ';
+
+ return message;
+ }
+
+ addFileToQueue(data) {
+ let fileData = {
+ id: data.additionalData.fileId,
+ data: data
+ };
+ this._fileQueue.push(fileData);
+ return this.generateMessage(data.additionalData.fileId, data.additionalData.fileName, data.messageType);
+ }
+
}
diff --git a/src/js/fileHandler.js b/src/js/fileHandler.js
index 509d955..fdedccc 100644
--- a/src/js/fileHandler.js
+++ b/src/js/fileHandler.js
@@ -1,9 +1,14 @@
+import _ from 'underscore';
+import uuid from 'uuid';
+
export default class FileHandler {
- constructor(darkwire, socket) {
- if (window.File && window.FileReader && window.FileList && window.Blob && window.btoa && window.atob) {
+ constructor(darkwire, socket, chat) {
+ this.localFileQueue = [];
+ if (window.File && window.FileReader && window.FileList && window.Blob && window.btoa && window.atob && window.Blob && window.URL) {
this._isSupported = true;
this.darkwire = darkwire;
this.socket = socket;
+ this.chat = chat;
this.listen();
} else {
this._isSupported = false;
@@ -14,16 +19,22 @@ export default class FileHandler {
return this._isSupported;
}
- encodeFile(event) {
+ set isSupported(state) {
+ this._isSupported = state;
+ return this;
+ }
+
+ confirmTransfer(event) {
+ const validFileTypes = ['png','jpg','jpeg','gif','zip','rar','gzip','pdf','txt','json','doc','docx'];
const file = event.target.files && event.target.files[0];
if (file) {
+ const fileExt = file.name.split('.').pop().toLowerCase();
- // let encodedFile = {
- // fileName: file.name,
- // fileSize: file.fileSize,
- // base64: null
- // };
+ if (validFileTypes.indexOf(fileExt) <= -1) {
+ alert('file type not supported');
+ return false;
+ }
// Support for only 1MB
if (file.size > 1000000) {
@@ -31,32 +42,98 @@ export default class FileHandler {
alert('Max filesize is 1MB.');
return false;
}
+ let fileId = uuid.v4();
- const reader = new FileReader();
-
- reader.onload = (readerEvent) => {
- const base64 = window.btoa(readerEvent.target.result);
- this.darkwire.encodeMessage(base64, 'file').then((socketData) => {
- this.socket.emit('new message', socketData);
- });
+ let confirmMessage = 'You are about to send ' + file.name + ' to all parties in this chat. Confirm | Cancel';
+ let fileData = {
+ id: fileId,
+ file: file
};
+ this.localFileQueue.push(fileData);
+ this.chat.addChatMessage({
+ username: username,
+ message: confirmMessage
+ });
+ this.filesSent++;
- reader.readAsBinaryString(file);
}
return false;
}
- decodeFile(base64) {
- return window.atob(base64);
+ encodeFile(fileId) {
+ const fileData = _.findWhere(this.localFileQueue, {id: fileId});
+ const file = fileData.file || false;
+
+ if (file) {
+ // TODO: Remove file from local queue
+ } else {
+ return false;
+ }
+
+ const reader = new FileReader();
+ const fileType = file.type || 'file';
+
+ reader.onload = (readerEvent) => {
+ const base64 = window.btoa(readerEvent.target.result);
+ const additionalData = {
+ fileId: fileId,
+ fileName: file.name
+ };
+ this.darkwire.encodeMessage(base64, fileType, additionalData).then((socketData) => {
+ this.chat.replaceMessage('#transfer-' + fileId, 'Sent ' + file.name);
+ this.socket.emit('new message', socketData);
+ });
+ this.resetInput();
+ };
+
+ reader.readAsBinaryString(file);
+ }
+
+ destroyFile(fileId) {
+ const file = _.findWhere(this.localFileQueue, {id: fileId});
+ this.localFileQueue = _.without(this.localFileQueue, file);
+ return this.chat.replaceMessage('#transfer-' + fileId, 'The file transfer for ' + file.file.name + ' has been canceled.');
+ }
+
+ createBlob(base64, fileType) {
+ base64 = unescape(base64);
+ return new Promise((resolve, reject) => {
+ const sliceSize = 1024;
+ let byteCharacters = window.atob(base64);
+ let byteArrays = [];
+
+ for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
+ let slice = byteCharacters.slice(offset, offset + sliceSize);
+
+ let byteNumbers = new Array(slice.length);
+ for (let i = 0; i < slice.length; i++) {
+ byteNumbers[i] = slice.charCodeAt(i);
+ }
+
+ let byteArray = new Uint8Array(byteNumbers);
+
+ byteArrays.push(byteArray);
+ }
+
+ resolve(new window.Blob(byteArrays, {type: fileType}));
+ });
+ }
+
+ createUrlFromBlob(blob) {
+ return window.URL.createObjectURL(blob);
}
listen() {
// browser API
- document.getElementById('fileInput').addEventListener('change', this.encodeFile.bind(this), false);
+ document.getElementById('fileInput').addEventListener('change', this.confirmTransfer.bind(this), false);
// darkwire
return this;
}
+
+ resetInput() {
+ document.getElementById('fileInput').value = '';
+ }
}
diff --git a/src/js/main.js b/src/js/main.js
index 3f48076..72403f8 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -1,6 +1,8 @@
+import _ from 'underscore';
import Darkwire from './darkwire';
import WindowHandler from './window';
import CryptoUtil from './crypto';
+import Chat from './chat';
let fs = window.RequestFileSystem || window.webkitRequestFileSystem;
@@ -8,28 +10,9 @@ $(function() {
const darkwire = new Darkwire();
const cryptoUtil = new CryptoUtil();
- let FADE_TIME = 150; // ms
- let TYPING_TIMER_LENGTH = 400; // ms
-
- let COLORS = [
- '#e21400', '#ffe400', '#ff8f00',
- '#58dc00', '#dd9cff', '#4ae8c4',
- '#3b88eb', '#f47777', '#d300e7',
- ];
-
let $window = $(window);
- let $messages = $('.messages'); // Messages area
- let $inputMessage = $('.inputMessage'); // Input message input box
- let $key = $('.key');
- let $genKey = $('.new_key');
let $participants = $('#participants');
- let $chatPage = $('.chat.page'); // The chatroom page
-
- let username;
- let typing = false;
- let lastTypingTime;
-
let roomId = window.location.pathname.length ? window.location.pathname : null;
if (!roomId) { return; }
@@ -43,7 +26,8 @@ $(function() {
});
let socket = io(roomId);
- const windowHandler = new WindowHandler(darkwire, socket);
+ const chat = new Chat(darkwire, socket);
+ const windowHandler = new WindowHandler(darkwire, socket, chat);
FastClick.attach(document.body);
@@ -56,142 +40,22 @@ $(function() {
// Sets the client's username
function initChat() {
- username = window.username;
// warn not incognitor
- if (!fs) {
- console.log('no fs');
- } else {
+ if (fs) {
fs(window.TEMPORARY,
100,
- log.bind(log, 'WARNING: Your browser is not in incognito mode!'));
- }
-
- // If the username is valid
- if (username) {
- $chatPage.show();
- $inputMessage.focus();
-
- Promise.all([
- cryptoUtil.createPrimaryKeys()
- ])
- .then(function(data) {
- darkwire.keys = {
- public: data[0].publicKey,
- private: data[0].privateKey
- };
- return Promise.all([
- cryptoUtil.exportKey(data[0].publicKey, 'spki')
- ]);
- })
- .then(function(exportedKeys) {
- // Tell the server your username and send public keys
- socket.emit('add user', {
- username: username,
- publicKey: exportedKeys[0]
+ () => {
+ chat.log('WARNING: Your browser is not in incognito mode!', {error: true});
});
- });
- }
- }
-
- // Log a message
- function log(message, options) {
- let html = options && options.html === true || false;
- let $el;
- if (html) {
- $el = $('').addClass('log').html(message);
- } else {
- $el = $('').addClass('log').text(message);
- }
- addMessageElement($el, options);
- }
-
- // Adds the visual chat message to the message list
- function addChatMessage(data, options) {
- if (!data.message.trim().length) {
- return;
}
- let messageType = data.messageType || 'text';
-
- // Don't fade the message in if there is an 'X was typing'
- let $typingMessages = getTypingMessages(data);
- options = options || {};
- if ($typingMessages.length !== 0) {
- options.fade = false;
- $typingMessages.remove();
- }
-
- let $usernameDiv = $('')
- .text(data.username)
- .css('color', getUsernameColor(data.username));
- let $messageBodyDiv = $('');
- // TODO: Ask client if accept/reject attachment
- // If reject, destroy object in memory
- // If accept, render image or content dispose
- if (messageType === 'file') {
- let image = new Image();
- image.src = `data:image/png;base64,${data.message}`;
- $messageBodyDiv.html(image);
- } else {
- $messageBodyDiv.html(data.message);
- }
-
- let typingClass = data.typing ? 'typing' : '';
-
- let $messageDiv = $('')
- .data('username', data.username)
- .addClass(typingClass)
- .append($usernameDiv, $messageBodyDiv);
-
- addMessageElement($messageDiv, options);
- }
-
- // Adds the visual chat typing message
- function addChatTyping(data) {
- data.typing = true;
- data.message = 'is typing';
- addChatMessage(data);
- }
-
- // Removes the visual chat typing message
- function removeChatTyping(data) {
- getTypingMessages(data).fadeOut(function() {
- $(this).remove();
+ darkwire.updateUsername(username).then((socketData) => {
+ chat.chatPage.show();
+ chat.inputMessage.focus();
+ socket.emit('add user', socketData);
});
}
- // Adds a message element to the messages and scrolls to the bottom
- // el - The element to add as a message
- // options.fade - If the element should fade-in (default = true)
- // options.prepend - If the element should prepend
- // all other messages (default = false)
- function addMessageElement(el, options) {
- let $el = $(el);
-
- // Setup default options
- if (!options) {
- options = {};
- }
- if (typeof options.fade === 'undefined') {
- options.fade = true;
- }
- if (typeof options.prepend === 'undefined') {
- options.prepend = false;
- }
-
- // Apply options
- if (options.fade) {
- $el.hide().fadeIn(FADE_TIME);
- }
- if (options.prepend) {
- $messages.prepend($el);
- } else {
- $messages.append($el);
- }
-
- $messages[0].scrollTop = $messages[0].scrollHeight; // minus 60 for key
- }
-
// Prevents input from having injected markup
function cleanInput(input) {
let message = $('').html(input).text();
@@ -199,45 +63,6 @@ $(function() {
return message;
}
- // Updates the typing event
- function updateTyping() {
- if (darkwire.connected) {
- if (!typing) {
- typing = true;
- socket.emit('typing');
- }
- lastTypingTime = (new Date()).getTime();
-
- setTimeout(function() {
- let typingTimer = (new Date()).getTime();
- let timeDiff = typingTimer - lastTypingTime;
- if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
- socket.emit('stop typing');
- typing = false;
- }
- }, TYPING_TIMER_LENGTH);
- }
- }
-
- // Gets the 'X is typing' messages of a user
- function getTypingMessages(data) {
- return $('.typing.message').filter(function(i) {
- return $(this).data('username') === data.username;
- });
- }
-
- // Gets the color of a username through our hash function
- function getUsernameColor(username) {
- // Compute hash code
- let hash = 7;
- for (let i = 0; i < username.length; i++) {
- hash = username.charCodeAt(i) + (hash << 5) - hash;
- }
- // Calculate color
- let index = Math.abs(hash % COLORS.length);
- return COLORS[index];
- }
-
// Keyboard events
$window.keydown(function(event) {
@@ -245,43 +70,16 @@ $(function() {
if (event.which === 13 && $('.inputMessage').is(':focus')) {
handleMessageSending();
socket.emit('stop typing');
- typing = false;
+ chat.typing = false;
}
});
- $inputMessage.on('input propertychange paste change', function() {
- updateTyping();
- let message = $(this).val().trim();
- if (message.length) {
- $('#send-message-btn').addClass('active');
- } else {
- $('#send-message-btn').removeClass('active');
- }
- });
-
// Select message input when closing modal
$('.modal').on('hidden.bs.modal', function(e) {
- $inputMessage.focus();
+ chat.inputMessage.focus();
});
- // Select message input when clicking message body, unless selecting text
- $('.messages').on('click', function() {
- if (!getSelectedText()) {
- $inputMessage.focus();
- }
- });
-
- function getSelectedText() {
- var text = '';
- if (typeof window.getSelection != 'undefined') {
- text = window.getSelection().toString();
- } else if (typeof document.selection != 'undefined' && document.selection.type == 'Text') {
- text = document.selection.createRange().text;
- }
- return text;
- }
-
// Whenever the server emits 'login', log the login message
socket.on('user joined', function(data) {
darkwire.connected = true;
@@ -293,38 +91,40 @@ $(function() {
$('#first-modal').modal('show');
}
- log(data.username + ' joined');
+ chat.log(data.username + ' joined');
renderParticipantsList();
});
});
+ socket.on('user update', (data) => {
+ updateUser(data);
+ });
+
// Whenever the server emits 'new message', update the chat body
socket.on('new message', function(data) {
- darkwire.decodeMessage(data).then((data) => {
+ darkwire.decodeMessage(data).then((decodedMessage) => {
if (!windowHandler.isActive) {
windowHandler.notifyFavicon();
darkwire.audio.play();
}
- if (data.messageType === 'file') {
- // let file = windowHandler.fileHandler.decodeFile(data.message);
- // let chatMessage = {
- // username: data.username,
- // message: file
- // }
- addChatMessage(data, {messageType: 'file'});
- } else {
- addChatMessage(data);
- }
+
+ let data = {
+ username: decodedMessage.username,
+ message: decodedMessage.message.text,
+ messageType: decodedMessage.messageType,
+ additionalData: decodedMessage.message.additionalData
+ };
+ chat.addChatMessage(data);
});
});
// Whenever the server emits 'user left', log it in the chat body
socket.on('user left', function(data) {
- log(data.username + ' left');
+ chat.log(data.username + ' left');
addParticipantsMessage(data);
- removeChatTyping(data);
+ chat.removeChatTyping(data);
darkwire.removeUser(data);
@@ -333,12 +133,12 @@ $(function() {
// Whenever the server emits 'typing', show the typing message
socket.on('typing', function(data) {
- addChatTyping(data);
+ chat.addChatTyping(data);
});
// Whenever the server emits 'stop typing', kill the typing message
socket.on('stop typing', function(data) {
- removeChatTyping(data);
+ chat.removeChatTyping(data);
});
initChat();
@@ -365,9 +165,9 @@ $(function() {
let li;
if (user.username === window.username) {
// User is me
- li = $('' + user.username + ' (you)').css('color', getUsernameColor(user.username));
+ li = $('' + user.username + ' (you)').css('color', chat.getUsernameColor(user.username));
} else {
- li = $('' + user.username + '').css('color', getUsernameColor(user.username));
+ li = $('' + user.username + '').css('color', chat.getUsernameColor(user.username));
}
$('#participants-modal ul.users')
.append(li);
@@ -377,27 +177,38 @@ $(function() {
$('#send-message-btn').click(function() {
handleMessageSending();
socket.emit('stop typing');
- typing = false;
+ chat.typing = false;
});
$('.navbar-collapse ul li a').click(function() {
$('.navbar-toggle:visible').click();
});
- let audioSwitch = $('input.bs-switch').bootstrapSwitch();
+ let audioSwitch = $('input.sound-enabled').bootstrapSwitch();
+ let autoEmbedImages = $('input.auto-embed-files').bootstrapSwitch();
audioSwitch.on('switchChange.bootstrapSwitch', function(event, state) {
darkwire.audio.soundEnabled = state;
});
+ autoEmbedImages.on('switchChange.bootstrapSwitch', (event, state) => {
+ darkwire.autoEmbedImages = state;
+ });
+
function handleMessageSending() {
- let message = $inputMessage;
+ let message = chat.inputMessage;
let cleanedMessage = cleanInput(message.val());
+ let isCommand = chat.parseCommand(cleanedMessage);
+
+ if (isCommand) {
+ return chat.executeCommand(isCommand, this);
+ }
+
// Prevent markup from being injected into the message
darkwire.encodeMessage(cleanedMessage, 'text').then((socketData) => {
message.val('');
$('#send-message-btn').removeClass('active');
- addChatMessage({
+ chat.addChatMessage({
username: username,
message: cleanedMessage
});
@@ -407,4 +218,65 @@ $(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) {
+ return windowHandler.fileHandler.encodeFile(fileId);
+ }
+
+ return chat.log('Requested file transfer is no longer valid. Please try again.', {error: true});
+ };
+
+ window.triggerFileDestroy = function(context) {
+ const fileId = context.getAttribute('data-file');
+ if (fileId) {
+ return windowHandler.fileHandler.destroyFile(fileId);
+ }
+
+ return chat.log('Requested file transfer is no longer valid. Please try again.', {error: true});
+ };
+
+ window.triggerFileDownload = function(context) {
+ const fileId = context.getAttribute('data-file');
+ const file = darkwire.getFile(fileId);
+ windowHandler.fileHandler.createBlob(file.message, file.messageType).then((blob) => {
+ let url = windowHandler.fileHandler.createUrlFromBlob(blob);
+
+ if (file) {
+ if (file.messageType.match('image.*')) {
+ let image = new Image();
+ image.src = url;
+ chat.replaceMessage('#file-transfer-request-' + fileId, image);
+ } else {
+ let downloadLink = document.createElement('a');
+ downloadLink.href = url;
+ downloadLink.target = '_blank';
+ downloadLink.innerHTML = 'Download ' + file.additionalData.fileName;
+ chat.replaceMessage('#file-transfer-request-' + fileId, downloadLink);
+ }
+ }
+
+ darkwire.encodeMessage('Downloaded ' + file.additionalData.fileName, 'text').then((socketData) => {
+ socket.emit('new message', socketData);
+ }).catch((err) => {
+ console.log(err);
+ });
+
+ });
+ };
+
});
diff --git a/src/js/window.js b/src/js/window.js
index db196c9..fedd3ba 100644
--- a/src/js/window.js
+++ b/src/js/window.js
@@ -1,9 +1,9 @@
import FileHandler from './fileHandler';
export default class WindowHandler {
- constructor(darkwire, socket) {
+ constructor(darkwire, socket, chat) {
this._isActive = false;
- this.fileHandler = new FileHandler(darkwire, socket);
+ this.fileHandler = new FileHandler(darkwire, socket, chat);
this.newMessages = 0;
this.favicon = new Favico({
@@ -31,6 +31,7 @@ export default class WindowHandler {
enableFileTransfer() {
if (this.fileHandler.isSupported) {
+ console.log('enabled');
$('#send-file').click((e) => {
e.preventDefault();
$('#fileInput').trigger('click');
diff --git a/src/public/style.css b/src/public/style.css
index 873608f..9d3b61e 100644
--- a/src/public/style.css
+++ b/src/public/style.css
@@ -296,3 +296,19 @@ html.no-touchevents .chat #input-icons {
#fileInput {
display: none;
}
+
+.file-disclaimer {
+ font-size: 10px;
+ color: red;
+ line-height: 10px;
+}
+
+.log-info {
+ color: #FFF;
+ font-weight: bold;
+}
+
+.log-error {
+ color: yellow;
+ font-weight: bold;
+}
diff --git a/src/room.js b/src/room.js
index d8ea3bc..ef9075e 100644
--- a/src/room.js
+++ b/src/room.js
@@ -26,7 +26,8 @@ class Room {
data: data.data,
vector: data.vector,
secretKeys: data.secretKeys,
- signature: data.signature
+ signature: data.signature,
+ timestamp: new Date
});
});
@@ -46,7 +47,8 @@ class Room {
thisIO.emit('user joined', {
username: socket.username,
numUsers: this.numUsers,
- users: this.users
+ users: this.users,
+ timestamp: new Date
});
});
@@ -75,7 +77,8 @@ class Room {
username: socket.username,
numUsers: this.numUsers,
users: this.users,
- id: socket.user.id
+ id: socket.user.id,
+ timestamp: new Date
});
// remove room from rooms array
@@ -84,6 +87,35 @@ class Room {
}
}
});
+
+ // Update user
+ socket.on('update user', (data) => {
+ 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
+ });
+
+ });
+
});
}
diff --git a/src/views/index.mustache b/src/views/index.mustache
index 4d9f076..1e7238a 100644
--- a/src/views/index.mustache
+++ b/src/views/index.mustache
@@ -68,13 +68,13 @@
-
WARNING: This product is in beta and its source code has not been peer-reviewed or undergone a security audit. This is a demo only and not intended for security-critical use. View source code.
+ {{>partials/disclaimer}}
-
darkwire.io is the simplest way to chat with encryption online. Chat history is never stored on a server or database, and plain text messages are never transferred over the wire.
-
-
Questions/comments? Email us at hello[at]darkwire.io
+
Questions/comments? Email us at hello[at]darkwire.io
+ Found a bug or want a new feature? Open a ticket on Github.