mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-19 11:02:58 +00:00
Merge pull request #7 from seripap/feature/fileTransfer
Abstraction of darkwire, file transfer, slash commands
This commit is contained in:
commit
b3befa1247
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
npm-debug.log
|
||||
src/public/main.js
|
||||
|
@ -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
|
||||
|
@ -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'}
|
||||
);
|
||||
|
||||
|
11
package.json
11
package.json
@ -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"
|
||||
|
27
readme.md
27
readme.md
@ -1,31 +1,30 @@
|
||||
# Darkwire.io
|
||||
|
||||
[](https://travis-ci.org/seripap/darkwire.io) []()
|
||||
|
||||
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
|
||||
|
||||
|
@ -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
345
src/js/chat.js
Normal 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 = /<br \/>/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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 = '';
|
||||
}
|
||||
}
|
||||
|
345
src/js/main.js
345
src/js/main.js
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
76
src/public/vendor/autogrow.js
vendored
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n$/, '<br/> ')
|
||||
.replace(/\n/g, '<br/>')
|
||||
.replace(/ {2,}/g, function(space){ return times(' ', 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);
|
35
src/room.js
35
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,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;
|
||||
}
|
||||
}
|
||||
|
@ -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">×</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">×</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>
|
||||
|
4
src/views/partials/ip.mustache
Normal file
4
src/views/partials/ip.mustache
Normal 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}}
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
1
test/acceptance/fileUtility.js
Normal file
1
test/acceptance/fileUtility.js
Normal file
@ -0,0 +1 @@
|
||||
($('#fileInput').show)();
|
52
test/acceptance/nightwatch-local.json
Normal file
52
test/acceptance/nightwatch-local.json
Normal 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
BIN
test/acceptance/ricky.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
Loading…
x
Reference in New Issue
Block a user