Merge pull request #7 from seripap/feature/fileTransfer

Abstraction of darkwire, file transfer, slash commands
This commit is contained in:
Daniel Seripap 2016-02-25 10:12:55 -05:00
commit b3befa1247
20 changed files with 1062 additions and 334 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

@ -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']
});
});
@ -51,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,9 +12,13 @@
"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",
"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"
@ -36,8 +40,11 @@
"vinyl-source-stream": "^1.1.0"
},
"scripts": {
"dev": "gulp start",
"test": "gulp test"
"start": "npm run bundle && node index.js",
"dev": "npm run bundle && gulp start",
"bundle": "gulp bundle",
"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,31 +1,30 @@
# 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
# For es6 compatability, be sure you have the latest stable version of Node JS installed
# Get latest version of NodeJS for ES2015 support
npm install -g n
n stable
npm install
# Bundle JS files
npm run bundle
# Starting dev environment
npm run dev
# Running tests locally
brew install chromedriver
npm test
# 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.
# Changing ports, default is 3000
port=3000 npm start
Darkwire is now running on `http://localhost:3000`
### Deployment
Build source
gulp bundle
### How it works
Darkwire uses a combination of asymmetric encryption (RSA-OAEP), symmetric session keys (AES-CBC) and signing keys (HMAC) for security.
@ -48,11 +47,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

View File

@ -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('/');

345
src/js/chat.js Normal file
View File

@ -0,0 +1,345 @@
import _ from 'underscore';
import sanitizeHtml from 'sanitize-html';
import he from 'he';
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 = $('<span/>')
.text(matchedUsernames[i])
.css('color', this.getUsernameColor(matchedUsernames[i]));
message = message.replace(matchedUsernames[i], usernameContainer.prop('outerHTML'));
}
}
if (options && options.error) {
$el = $('<li class="log-error">').addClass('log').html(message);
} else if (options && options.info) {
$el = $('<li class="log-info">').addClass('log').html(message);
} else {
$el = $('<li>').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}'],
multiple: false,
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(window.username, newUsername).then((socketData) => {
let modifiedSocketData = {
username: window.username,
newUsername: socketData.username
};
this.socket.emit('update user', modifiedSocketData);
window.username = username = socketData.username;
});
}
}, {
command: 'help',
description: 'Shows a list of commands.',
paramaters: [],
multiple: false,
usage: '/help',
action: () => {
validCommands = validCommands.map((command) => {
return '/' + command;
});
this.log('Valid commands: ' + validCommands.sort().join(', '), {info: true});
}
}, {
command: 'me',
description: 'Invoke virtual action',
paramaters: ['{action}'],
multiple: true,
usage: '/me {action}',
action: () => {
expectedParams = 100;
let actionMessage = trigger.params.join(' ');
this.darkwire.encodeMessage(actionMessage, 'action').then((socketData) => {
this.addChatMessage({
username: username,
message: actionMessage,
messageType: 'action'
});
this.socket.emit('new message', socketData);
}).catch((err) => {
console.log(err);
});
}
}];
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 (expectedParams && trigger.params.length > expectedParams || expectedParams && trigger.params.length < expectedParams) {
if (!commandToTrigger.multple && trigger.params.length < 1) {
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 = $('<span class="username"/>')
.text(data.username)
.css('color', this.getUsernameColor(data.username));
let $messageBodyDiv = $('<span class="messageBody">');
if (messageType === 'text' || messageType === 'action') {
if (messageType === 'action') {
$usernameDiv.css('color','').prepend('*');
}
let unescapedMessage = unescape(data.message);
let lineBreaks = /&#x3C;br \/&#x3E;/g;
unescapedMessage = unescapedMessage.replace(lineBreaks, '<br />');
$messageBodyDiv.html(unescapedMessage);
} else {
$messageBodyDiv.html(this.darkwire.addFileToQueue(data));
}
let typingClass = data.typing ? 'typing' : '';
let actionClass = data.messageType === 'action' ? 'action' : '';
let $messageDiv = $('<li class="message"/>')
.data('username', data.username)
.addClass(typingClass)
.addClass(actionClass)
.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);
}
}
}

View File

@ -9,9 +9,20 @@ export default class Darkwire {
this._myUserId = false;
this._connected = false;
this._users = [];
this._fileQueue = [];
this._keys = {};
}
getFile(id) {
let file = _.findWhere(this._fileQueue, {id: id}) || false;
if (file) {
// TODO: Destroy object from memory when retrieved
}
return file.data;
}
get keys() {
return this._keys;
}
@ -30,12 +41,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) {
@ -76,14 +110,48 @@ export default class Darkwire {
return this._users;
}
encodeMessage(message, messageType) {
updateUsername(username, newUsername) {
let user = null;
return new Promise((resolve, reject) => {
if (newUsername) {
user = this.getUserByName(username);
}
if (username) {
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
});
}
}
});
}
encodeMessage(message, messageType, additionalData) {
// Don't send unless other users exist
return new Promise((resolve, reject) => {
// if (this._users.length <= 1) {
// console.log('rejected:' + this._users);
// reject();
// return;
// };
additionalData = additionalData || {};
// if there is a non-empty message and a socket connection
if (message && this._connected) {
@ -95,7 +163,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 +219,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 +275,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 +290,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 = '<div id="file-transfer-request-' + fileId + '">is attempting to send you ' + fileName + ' (' + messageType + ')';
message += '<br><small class="file-disclaimer"><strong>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.</strong></small>';
message += '<br><a onclick="triggerFileDownload(this);" data-file="' + fileId + '">Accept File</a></div>';
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);
}
}

View File

@ -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 = '<span id="transfer-' + fileId + '">You are about to send ' + file.name + ' to all parties in this chat. <a onclick="triggerFileTransfer(this);" data-file="' + fileId + '">Confirm</a> | <a onclick="triggerFileDestroy(this)" data-file="' + fileId + '">Cancel</a></span>';
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 = '';
}
}

View File

@ -1,6 +1,11 @@
import _ from 'underscore';
import Darkwire from './darkwire';
import WindowHandler from './window';
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;
@ -8,28 +13,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 +29,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,232 +43,48 @@ $(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 = $('<li>').addClass('log').html(message);
} else {
$el = $('<li>').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 = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
let $messageBodyDiv = $('<span class="messageBody">');
// 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 = $('<li class="message"/>')
.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();
chat.log(moment().format('MMMM Do YYYY, h:mm:ss a'), {info: true});
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 = $('<div/>').html(input).text();
message = Autolinker.link(message);
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];
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');
typing = false;
chat.typing = false;
event.preventDefault();
}
});
$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 +96,43 @@ $(function() {
$('#first-modal').modal('show');
}
log(data.username + ' joined');
chat.log(data.username + ' joined');
renderParticipantsList();
});
});
socket.on('user update', (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
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 +141,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 +173,9 @@ $(function() {
let li;
if (user.username === window.username) {
// User is me
li = $('<li>' + user.username + ' <span class="you">(you)</span></li>').css('color', getUsernameColor(user.username));
li = $('<li>' + user.username + ' <span class="you">(you)</span></li>').css('color', chat.getUsernameColor(user.username));
} else {
li = $('<li>' + user.username + '</li>').css('color', getUsernameColor(user.username));
li = $('<li>' + user.username + '</li>').css('color', chat.getUsernameColor(user.username));
}
$('#participants-modal ul.users')
.append(li);
@ -377,27 +185,33 @@ $(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();
audioSwitch.on('switchChange.bootstrapSwitch', function(event, state) {
darkwire.audio.soundEnabled = state;
});
function handleMessageSending() {
let message = $inputMessage;
let message = chat.inputMessage;
let cleanedMessage = cleanInput(message.val());
let slashCommand = chat.parseCommand(cleanedMessage);
if (slashCommand) {
return chat.executeCommand(slashCommand, 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 +221,51 @@ $(function() {
});
}
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);
});
});
};
});

View File

@ -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({
@ -35,6 +35,9 @@ export default class WindowHandler {
e.preventDefault();
$('#fileInput').trigger('click');
});
} else {
$('#send-file').remove();
$('#fileInput').remove();
}
}

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,25 @@ input {
/* Input */
.inputContainer {
position: fixed;
right: 0;
left: 0;
bottom: 0;
width: 100%;
padding-bottom: 10px;
height: 60px;
}
.inputMessage{
background: black !important;
color: white !important;
border: none;
border-top: 1px solid #282828;
bottom: 0;
height: 60px;
left: 0;
// height: 60px;
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;
@ -296,3 +306,24 @@ 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;
}
.action {
font-style: italic;
color: #00FF7F;
}

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

@ -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,10 +87,34 @@ class Room {
}
}
});
// Update user
socket.on('update user', (data) => {
if (data.newUsername.length > 16) {
return false;
}
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()
});
}
});
});
}
get roomId() {
roomId() {
return this.id;
}
}

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>
@ -68,13 +70,15 @@
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">About</h4>
<p><strong>Darkwire Version:</strong> <span id="appVersion">v{{APP.version}}</span> (<a href="https://github.com/seripap/darkwire.io/releases/tag/v{{APP.version}}" target="_blank">View Release Notes</a>)<br></p>
</div>
<div class="modal-body">
<p class="bold">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. <a href="https://github.com/seripap/darkwire.io" target="_blank">View source code</a>.</p>
{{>partials/ip}}
<p>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.</p>
<p class="bold">WARNING: Darkwire does not mask IP addresses nor can verify the integrity of parties recieving messages. Proceed with caution and always confirm recipients before starting a chat session.</p>
<p>Questions/comments? Email us at hello[at]darkwire.io</p>
<p>Questions/comments? Email us at hello[at]darkwire.io<br>
Found a bug or want a new feature? <a href="https://github.com/seripap/darkwire.io/issues" target="_blank">Open a ticket on Github</a>.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
@ -97,7 +101,8 @@
</p>
<br>
<h6>Sound</h6>
<input type="checkbox" name="my-checkbox" class="form-control bs-switch" checked>
<input type="checkbox" name="sound-enabled" class="form-control bs-switch sound-enabled" checked>
<br>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default encryption-active" data-dismiss="modal">Close</button>
@ -111,11 +116,13 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Welcome to darkwire.io</h4>
<h4 class="modal-title">Welcome to darkwire.io v{{APP.version}}</h4>
</div>
<div class="modal-body">
<h6>We've placed you in a new chat room</h6>
<p class="bold">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. <a href="https://github.com/seripap/darkwire.io" target="_blank">View source code</a>.</p>
<p class="bold">This software uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto" target="_blank">Crypto Web API</a> to encrypt data which is transferred using <a href="https://en.wikipedia.org/wiki/WebSocket" target="_blank">secure WebSockets</a>. <a href="javascript:void(0)" id="about-nav" data-dismiss="modal">See Disclaimer</a>.</p>
<p><a href="https://github.com/seripap/darkwire.io" target="_blank">We believe in your privacy and full transparency. View the source code and documentation on Github.</a></p>
<br>
<h6>Invite People to This Room</h6>
<p>
@ -166,7 +173,6 @@
<script src="/favicon.js"></script>
<script src="/vendor/bootstrap-3.3.6.min.js"></script>
<script src="/vendor/autolinker.min.js"></script>
<script src="/vendor/underscore.min.js"></script>
<script src="/vendor/modernizr-custom.min.js"></script>
<script src="/vendor/bootstrap-switch.min.js"></script>
<script src="/vendor/web-crypto-shim.js"></script>

View File

@ -0,0 +1,4 @@
{{#APP.ip}}
<p class="bold">Recognized IP: <a href="http://ipinfo.io/{{APP.ip}}/json" target="_blank">{{APP.ip}}</a>*</p>
<p>*This IP is your public IP recognized by all web servers. If this IP and/or its location data seems familiar to you, we recommend using <a href="https://www.torproject.org/" target="_blank">TOR</a> or a proxy before starting a new chat session.</p>
{{/APP.ip}}

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