Compare commits

..

No commits in common. "master" and "v1.0.1" have entirely different histories.

61 changed files with 1647 additions and 5631 deletions

View file

@ -1,16 +0,0 @@
# Node.js/npm stuff
node_modules
npm-debug.log
# Cryptalk documentation
docs
# Git stuff
.git
.gitignore
# Travis CI configuration
.travis.yml
# Heroku metadata
app.json

View file

@ -1,2 +0,0 @@
client/public/js/cryptalk.min.js
node_modules

View file

@ -1,38 +0,0 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true,
"amd": true
},
"extends": "eslint:recommended",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"indent": [
"error",
"tab"
],
"linebreak-style": 0,
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"eqeqeq": [
"error",
"always"
],
"no-undef": [
"warn"
],
"no-console": [
"warn"
]
}
}

2
.github/FUNDING.yml vendored
View file

@ -1,2 +0,0 @@
github: [hexagon]
ko_fi: hexagon_56k

View file

@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

View file

@ -1,71 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '45 11 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View file

@ -1,30 +0,0 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build:ci

61
.gitignore vendored
View file

@ -1,32 +1,29 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Commenting this out is preferred by some people, see
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
# Users Environment Variables
.lock-wscript
docs/Thumbs.db
# Vim temp files
*.swp
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Commenting this out is preferred by some people, see
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
# Users Environment Variables
.lock-wscript
docs/Thumbs.db

View file

@ -1,18 +0,0 @@
# Docker stuff
.dockerignore
Dockerfile
docker-entrypoint.sh
# Heroku stuff
app.json
# Source code
client/source/
# Dev config
rollup.config.js
.eslintignore
.eslintrc.json
# Github stuff
.github

View file

@ -1,6 +0,0 @@
FROM keymetrics/pm2:16-alpine
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN npm install --no-cache --production
EXPOSE 8080
CMD [ "pm2-runtime", "start", "pm2.json" ]

View file

@ -1,22 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014-2021 Hexagon <github.com/Hexagon>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The MIT License (MIT)
Copyright (c) 2014 Robin Nilsson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

157
README.md
View file

@ -1,120 +1,39 @@
![cryptalk](https://i.imgur.com/1sH36n5.png)
![cryptalk](/docs/screenshot.png)
![Node.js CI](https://github.com/Hexagon/cryptalk/workflows/Node.js%20CI/badge.svg?branch=master)
[![npm version](https://badge.fury.io/js/cryptalk.svg)](https://badge.fury.io/js/cryptalk)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/753ef40cec1747c2b5025f834635375b)](https://www.codacy.com/gh/Hexagon/cryptalk/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=Hexagon/cryptalk&amp;utm_campaign=Badge_Grade)
Cyptalk is a HTML5/Node.js based encrypted instant chat
# Cryptalk
Features
========
Cyptalk is a HTML5/Node.js based, client side (E2EE) encrypted instant chat
* Client side AES-256-CBC encryption/decryption (the server is just a messenger)
* 256 bit key derived from your passphrase using PBKDF2
* Optional nicknames
* Random (UUID v4) channel name generation för less guessability
* Quick-links (not recommended) using http://server/#Room:Passphrase
* Super simple setup
* Notification sounds (mutable)
* Native popup notifications
* Configurable page title
## Features
* Client side AES-256-CBC encryption/decryption (the server is just a messenger)
* 256 bit key derived from your passphrase using PBKDF2
* Messages torched after a configurable delay, default is 600s.
* Simple setup using npm, Docker or Heroku
* Notification sounds (mutable)
* Native popup notifications
* Configurable page title
* Nicknames, optional.
* Quick-links using http://server/#Room:Passphrase, optional and insecure
## Installing
### Docker setup
To run latest cryptalk with docker, exposed on host port 80, simply run the following command to pull it from docker hub
```bash
sudo docker run -d --restart=always -p 80:8080 hexagon/cryptalk
```
### Heroku setup
Click the button below
[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/hexagon/cryptalk)
### Docker setup without using docker hub
Clone this repo, enter the new directory.
Build image
```bash
docker build . --tag="hexagon/cryptalk"
```
Run container, enable start on boot, expose to port 80 at host
```bash
sudo docker run -d --restart=always -p 80:8080 hexagon/cryptalk
```
Browse to ```http://<ip-of-server>/```
Done!
### npm setup
Regular setup
========
Install node.js, exact procedure is dependant on platform and distribution.
Install the app from npm
Create a now folder, browse to it, and install the app from npm
```bash
npm install cryptalk -g
npm install cryptalk
````
Then issue the following to start the app
```bash
cryptalk
```
Browse to ```http://localhost:8080```
Done!
## Usage
Developer setup
========
```
Available commands:
Client:
/key StrongPassphrase Sets encryption key
/nick NickName Sets an optional nick
/mute Audio on
/unmute Audio off
/clear Clear on-screen buffer
/help This
/title Set your local page title
/torch AfterSeconds Console messages are torched
after this amount of seconds
(default 600).
Room:
/join RoomId Join a room
/leave Leave the room
/count Count participants
Host:
/connect Connect to host
/disconnect Disconnect from host
You can select any of the five last commands/messages with up/down key.
Due to security reasons, /key command is not saved, and command
history is automatically cleared after one minute of inactivity.
It is highly recommended to use incognito mode while chatting,
to prevent browsers from keeping history or cache.
```
## Development
Install node.js (development require >=12.0), exact procedure is dependant on platform and distribution.
Install node.js, exact procedure is dependant on platform and distribution.
Clone this repo
```bash
@ -129,9 +48,41 @@ npm install
Start server
```bash
npm run start
node server.js
```
Browse to ```http://localhost:8080```
To work on the JavaScript, edit the code in ```client/source/```. To test the changes, first run ```npm run build``` to lint, build and minify the code. Then restart the server.
Usage
========
```
Available commands:
Client:
/key StrongPassphrase Sets encryption key
/nick NickName Sets an optional nick
/mute Audio on
/unmute Audio off
/clear Clear on-screen buffer
/help This
/title Set your local page title
Room:
/generate Generate random room
/join RoomId Join a room
/leave Leave the room
/count Count participants
Host:
/hosts List available hosts
/connect HostIndex Connect to selected host
/disconnect Disconnect from host
You can select any of the five last commands/messages with up/down key.
Due to security reasons, /key command is not saved, and command
history is automatically cleared after one minute of inactivity.
```

View file

@ -1,12 +0,0 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Email hexagon@56k.guru. Do NOT report an issue, we will have a look at it asap.

View file

@ -1,16 +0,0 @@
{
"name": "Cryptalk",
"description": "Client side (E2EE) encrypted instant chat",
"keywords": [
"cryptalk",
"crypto-js",
"AES",
"secure",
"html5",
"encryption",
"privacy",
"chat",
"e2ee"
],
"repository": "https://github.com/Hexagon/cryptalk"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,36 +0,0 @@
import utils from './$.utils.js';
import proto from './$.proto.js';
// Create a custom edition of Array, extended with $.proto
function ElementArray () {}
ElementArray.prototype = new Array;
for(var k in proto) ElementArray.prototype[k] = proto[k];
// Create to actual dollar function
function Dollar (selector) {
let matches = new ElementArray();
if (selector !== undefined) {
if (selector === document) {
matches.push(document);
} else if (selector === window) {
matches.push(window);
} else {
let match = document.querySelectorAll(selector);
if (match) {
for( var i=0; i < match.length; i++) {
matches.push(match[i]);
}
}
}
}
return matches;
}
// Add utils to Dollar
for(var l in utils) Dollar[l] = utils[l];
export default Dollar;

View file

@ -1,43 +0,0 @@
var
exports = {};
// Extremely naive implementations of .html() and .append()
exports.html = function (string) {
this.forEach(function (element) {
element.innerHTML = string;
});
return this;
};
exports.append = function (string) {
this.forEach(function (element) {
element.innerHTML += string;
});
return this;
};
exports.first = function () {
return this[0];
};
// Naive implementations of .on()
exports.on = function (eventName, callback) {
this.forEach(function (element) {
if (element.addEventListener) {
element.addEventListener(eventName, callback, false);
} else if (element.attachEvent) {
element.attachEvent('on' + eventName, callback);
}
});
return this;
};
exports.focus = function () {
// It doesn't make sense to focus all matched elements. So we settle for the first one
if(this[0]) {
this[0].focus();
}
return this;
};
export default exports;

View file

@ -1,107 +0,0 @@
/* global io */
import { AES, SHA1, enc } from 'crypto-js';
var
exports = {},
reDigits = /^\d+$/;
// Namespace websocket
exports.io = io;
// Namespace SHA1
exports.SHA1 = function (string) {
return SHA1(string).toString();
};
// Namespace encode
exports.AES = {
decrypt: function (string, fgh) {
return AES.decrypt(string, fgh).toString(enc.Utf8);
},
encrypt: function (string, fgh) {
return AES.encrypt(string, fgh).toString();
}
};
exports.ssplit = function (string, seperator) {
var components = string.split(seperator);
return [components.shift(), components.join(seperator)];
};
exports.activeElement = function () {
try { return document.activeElement; } catch (e) { return; }
};
/**
* Removes all characters but 0 - 9 from given string.
*
* @method digits
* @param {String} str The string to sanitize
* @return {String} The sanitized string
* @example
* $.digits('foo8bar'); // `8`
* $.digits('->#5*duckM4N!!!111'); // `54111`
*/
exports.isDigits = function(value) {
return reDigits.test(value);
};
/**
* A very simple templating function.
* @param {} str [description]
* @param {[type]} map [description]
* @return {[type]} [description]
*/
exports.template = function (str, map) {
return str && str.replace(/{(\w+)}/gi, function(outer, inner) {
return Object.prototype.hasOwnProperty.call(map, inner) ? map[inner] : outer /* '' */;
});
};
exports.getJSON = function (path, onSuccess, onError) {
var request = new XMLHttpRequest();
request.open('GET', path, true);
request.onreadystatechange = function() {
if (this.readyState === 4) {
if (this.status >= 200 && this.status < 400) {
try {
onSuccess && onSuccess(JSON.parse(this.responseText));
} catch (e) {
onError && onError();
}
} else {
onError && onError();
}
}
};
request.send();
request = null;
};
// Part of this is originating from mustasche.js
// Code: https://github.com/janl/mustache.js/blob/master/mustache.js#L43
// License: https://github.com/janl/mustache.js/blob/master/LICENSE
exports.escapeHtml = (function () {
var pattern = /[&<>"'/]/g,
entities = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;',
'/': '&#x2F;'
};
return function (string) {
return String(string).replace(pattern, function (s) {
return entities[s];
});
};
}());
export default exports;

View file

@ -1,89 +0,0 @@
/* Usage:
mediator.on('audio:play', playTones );
mediator.on('audio:on', on );
mediator.on('audio:off', off );
mediator.on('audio:mute', mute );
mediator.on('audio:unmute', unmute );
*/
// Sounds module, used for emitting those annoying bl-up sounds when receiving a message
import queue from './queue.js';
import mediator from 'qbus';
import templates from './templates.js';
var
// Private variables
ac = false,
enabled = true,
muted = false,
// Recursive function for playing tones
// accepts an array of [tone,start_ms,duration_ms] - entries
playTones = function (tones, i) {
// Parameter defaults
i = (i === undefined) ? 0 : i;
// Stop if we've reached the end of iteration, and require ac, also stop if we're muted
if (!ac || !enabled || !(i < Object.keys(tones).length) || muted) {
return;
}
// Add tones to execution queue
var current_tones = tones[i],
freqs = current_tones[0],
start = current_tones[1],
duration = current_tones[2];
var o = ac.createOscillator();
var g = ac.createGain();
o.frequency.value = freqs;
o.connect(g);
g.gain.value = 0.25;
g.connect(ac.destination);
queue.add_function_delayed(start,function() { o.noteOn ? o.noteOn(0) : o.start(0); });
queue.add_function_delayed(start+duration,function() { o.noteOff ? o.noteOff(0) : o.stop(0); });
// Iterate, or start playing
i++;
if( i < Object.keys(tones).length ) {
playTones(tones,i);
} else {
queue.run();
}
},
on = function() {
enabled = true;
},
off = function() {
enabled = false;
},
mute = function() {
muted = true;
mediator.emit('console:info',templates.messages.muted);
},
unmute = function() {
muted = false;
mediator.emit('console:info',templates.messages.unmuted);
};
// Find audio context
if (window.AudioContext || window.webkitAudioContext) {
ac = new (window.AudioContext || window.webkitAudioContext);
}
// Connect events
mediator.on('audio:play', function(tones) {playTones(tones); } );
mediator.on('audio:on', on );
mediator.on('audio:off', off );
mediator.on('audio:mute', mute );
mediator.on('audio:unmute', unmute );

View file

@ -1,87 +0,0 @@
/*
Accepts:
mediator.on('command:help', ...);
mediator.on('command:nick', ...);
mediator.on('command:key', ...);
mediator.on('command:key', ...);
mediator.on('command:torch', ...);
mediator.on('command:title', ...);
Emits:
mediator.emit('nick:changed',...);
mediator.emit('key:changed',...);
mediator.emit('console:clear',...);
mediator.emit('console:info',...);
mediator.emit('console:error',...);
*/
import $ from './$.js';
export default function (mediator, settings, templates) {
var
// Private properties
nick,
key,
setKey = function(payload) {
/*if (!host) {
return post('error', templates.messages.key_no_host);
}*/
// Make sure the key meets the length requirements
if (payload.length > settings.key.maxLen) {
return mediator.emit('console:error',templates.messages.key_to_long);
} else if (payload.length < settings.key.minLen) {
return mediator.emit('console:error',templates.messages.key_to_short);
}
// Set key
key = payload;
// Keep other modules informed
mediator.emit('key:changed',key);
// Inform that the key has been set
return mediator.emit('console:info', templates.messages.key_ok );
},
help = function () { mediator.emit('console:motd', templates.help); },
clear = function () { mediator.emit('console:clear'); },
setTorch = function (payload) { mediator.emit('console:torch',payload); },
setNick = function (payload) {
// Make sure the nick meets the length requirements
if (payload.length > settings.nick.maxLen) {
return mediator.emit('console:error', $.template(templates.messages.nick_to_long, { nick_maxLen: settings.nick.maxLen } ));
} else if (payload.length < settings.nick.minLen) {
return mediator.emit('console:error', $.template(templates.messages.nick_to_short, {nick_minLen: settings.nick.minLen } ));
}
// Set nick
nick = payload;
// Keep other modules informed
mediator.emit('nick:changed', nick);
// Inform that the nick has been set
mediator.emit('console:info', $.template(templates.messages.nick_set, { nick: $.escapeHtml(nick)}));
},
title = function(payload) {
mediator.emit('window:title',payload);
mediator.emit('console:info', $.template(templates.messages.title_set, { title: $.escapeHtml(payload)}));
};
mediator.on('command:help', help);
mediator.on('command:clear', clear);
mediator.on('command:nick', setNick);
mediator.on('command:key', setKey);
mediator.on('command:torch', setTorch);
mediator.on('command:title', title);
}

View file

@ -1,216 +0,0 @@
/*
Accepts:
mediator.on('console:clear', clear);
mediator.on('console:torch', ttl)
mediator.on('console:motd', motd);
mediator.on('console:info', info);
mediator.on('console:error', error);
mediator.on('console:server', server);
mediator.on('console:message', message);
mediator.on('console:lockinput', lockInput);
mediator.on('console:unlockInput', unlockInput);
mediator.on('console:param', param);
Emits:
mediator.emit('notification:send',...);
mediator.emit('audio:play',...);
ToDo
*/
import $ from './$.js';
export default function(mediator,settings,templates, sounds) {
var // Collection of DOM components
components = {
chat: $('#chat'),
input: $('#input'),
inputWrapper: $('#input_wrapper')
},
// Collection of parameters
parameters = {},
// Adds a new message to the DOM
commands = {
post: function (type, text, nick) {
var tpl = templates.post[type],
uniqueId = 'msg_' + new Date().getTime() + '_' + Math.round(Math.random()*1000000),
post,
data = Object.assign({}, settings, {
nick: nick,
timestamp: new Date().toLocaleTimeString(),
id: uniqueId
});
data.text = $.template(text, data);
post = $.template(tpl, data);
// Request a notification
commands.showNotification(type, nick, text);
// Expire message
setTimeout(function() {
var parent = components.chat.first(),
child = $('#'+uniqueId).first();
parent.removeChild(child);
}, settings.ttl);
// Append the post to the chat DOM element
components.chat.append(post);
},
torch: function (ttl) {
ttl = parseInt(ttl, 10);
if( ttl > 0 && ttl < 3600) {
mediator.emit('console:info', $.template(templates.messages.torch_is_now, { ttl: ttl }) );
settings.ttl = ttl*1000;
} else {
mediator.emit('console:error', $.template(templates.messages.torch_not_set) );
}
},
param: function (p) {
parameters = Object.assign({}, parameters, p);
},
showNotification: function (type, nick, text) {
var title = (type !== 'message' ? 'Cryptalk' : nick),
icon = (type === 'message' ? 'gfx/icon_128x128.png' : (type === 'error' ? 'gfx/icon_128x128_error.png' : 'gfx/icon_128x128_info.png'));
// Emit notification
mediator.emit('notification:send', {
title: title.substring(0, 20),
body: text.substring(0, 80),
icon: icon
});
// Emit sound
if (type === 'message') {
mediator.emit('audio:play', sounds.message);
}
},
motd: function (message) {
commands.post('motd', message);
},
info: function (message) {
commands.post('info', message);
},
error: function (message) {
commands.post('error', message);
},
server: function (message) {
commands.post('server', message);
},
message: function (data) {
commands.post('message', data.message, data.nick);
},
clearInput: function () {
components.input[0].value = '';
},
clear: function () {
components.chat[0].innerHTML = '';
},
lockInput: function () {
components.input[0].setAttribute('disabled', 'disabled');
components.inputWrapper[0].className = 'loading';
},
unlockInput: function () {
components.input[0].removeAttribute('disabled');
components.inputWrapper[0].className = '';
components.input.focus();
}
},
// Handler for the document`s keyDown-event.
onKeyDown = function (e) {
var buffer,
parts,
payload,
command;
// The Document object is bound to this element.
// If the active element is not the input, focus on it and exit the function.
// Ignore this when ctrl and/or alt is pressed!
if (!e.ctrlKey && !e.altKey && components.input[0] !== $.activeElement()) {
components.input.focus();
return;
}
// Return immediatly if the buffer is empty or if the hit key was not <enter>
if (e.keyCode !== 13 || !(buffer = components.input[0].value)) {
return;
}
// Handle command
if ((buffer[0] || buffer.slice(0, 1)) === '/') {
parts = $.ssplit(buffer.slice(1), ' ');
command = parts[0];
payload = parts[1];
// Shout this command to all modules
mediator.emit(
'command:' + command,
payload,
function(retvals, recipients) {
if(!recipients) {
commands.post('error', $.template(templates.messages.unrecognized_command, { commandName: command }));
return;
} else {
commands.clearInput();
}
}
);
} else /* Handle ordinary message */ {
if(!parameters.room || !parameters.key ) {
// Make sure that the user has joined a room and the key is set
return (!parameters.room) ? commands.post('error', templates.messages.msg_no_room) : commands.post('error', templates.messages.msg_no_key);
}
// Before sending the message.
// Encrypt message using room UUID as salt and key as pepper.
mediator.emit(
'socket:emit',
{
data: 'message:send',
payload: {
room: $.SHA1(parameters.room),
msg: $.AES.encrypt(buffer, $.SHA1(parameters.room) + parameters.key).toString(),
nick: parameters.nick ? $.AES.encrypt(parameters.nick, $.SHA1(parameters.room) + parameters.key).toString() : false
}
}
);
// And clear the the buffer
commands.clearInput();
}
};
// Bind the necessary DOM events
$(document).on('keydown', onKeyDown);
// Put focus on the message input
components.input.focus();
// Connect events
for (var commandName in commands) {
if (commandName !== 'post') {
mediator.on('console:' + commandName, commands[commandName]);
}
}
mediator.on('console:post', function (data) {
commands.post(data.type, data.data, data.nick);
});
}

View file

@ -1,82 +0,0 @@
import mediator from './vendor/castrato.js';
import win from './window.js';
import notifications from './notifications.js';
import templates from './templates.js';
import settings from './settings.js';
import host from './host.js';
import client from './client.js';
import cons from './console.js';
import room from './room.js';
import sounds from './sounds.js';
// Inititalize modules
let wind = win(mediator);
cons(mediator, settings, templates,sounds);
notifications(mediator, settings, wind);
client(mediator, settings, templates);
host(mediator, settings, templates);
room(mediator, settings, templates);
// Mediate between modules
mediator
.on('window:focused', function() {
mediator.emit('audio:off');
mediator.emit('notification:off');
})
.on('window:blurred',function() {
mediator.emit('audio:on');
mediator.emit('notification:on');
})
.on('command:mute', function () {
mediator.emit('audio:mute');
})
.on('command:unmute', function () {
mediator.emit('audio:unmute');
})
// Help console and host keep track of current states
.on('room:changed', function(room) {
mediator
.emit('console:param', {
room: room
})
.emit('host:param', {
room: room
});
})
.on('nick:changed', function(nick) {
mediator.emit('console:param', {
nick: nick
});
})
.on('key:changed', function(key) {
mediator
.emit('console:param', {
key: key
})
.emit('host:param', {
key: key
});
});
// Connect to the default host
mediator.emit('command:connect', undefined, function() {
// Join room and set key if a hash in the format #Room:Key has been provided
var hash = window.location.hash;
if ( hash ) {
var parts = hash.slice(1).split(':');
if ( parts[0] ) {
mediator.emit('command:join', parts[0]);
}
if ( parts[1] ) {
mediator.emit('command:key', parts[1]);
}
}
});

View file

@ -1,15 +0,0 @@
export default function (mediator, debug) {
if (debug) {
mediator.on('*', function (data, done, name) {
if (name !== 'console:post' && name !== 'notification:send') {
mediator.emit('console:post', {
type: 'server',
data: name + (data ? '(' + JSON.stringify(data) + ')' : ''),
debug: 1
});
}
done();
});
}
}

View file

@ -1,180 +0,0 @@
/*
Accepts:
mediator.on('command:host', host);
mediator.on('command:connect', connect);
mediator.on('command:disconnect', disconnect);
mediator.on('command:reconnect', disconnect);
Emits:
mediator.on('socket:emit', emit);
eslint no-console: ["error", { allow: ["warn", "error"] }]
*/
import $ from './$.js';
export default function (mediator, settings, templates) {
var
// Private properties
socket,
host = {
host: '',
connected: false
},
// Collection of parameters
parameters = {},
emit = function(payload) {
// Route message from mediator to socket
if(socket) socket.emit(payload.data,payload.payload);
},
hostInfo = function () {
mediator.emit('info', JSON.stringify(host || {}));
},
connect = function (toHost, done) {
mediator.emit('console:lockInput');
if (host && host.connected) {
mediator.emit('console:error', $.template(templates.messages.already_connected, {
host: host.host
}));
mediator.emit('console:unlockInput');
return;
}
// Push 'Connecting...' message
mediator.emit('console:info', $.template(templates.messages.connecting, {host: host.host}));
// Show motd (placed here to enable server specific motds in future)
mediator.emit('console:motd', settings.motd);
// The one and only socket
socket = $.io(host.host, {
forceNew: true,
'force new connection': true
});
// Bind socket events
socket
.on('room:joined', function () {
mediator.emit('console:info', $.template(templates.messages.joined_room, { roomName: $.escapeHtml(parameters.room) } ));
// Automatically count persons on join
socket.emit('room:count');
})
.on('room:left', function () {
mediator.emit('console:info', $.template(templates.messages.left_room, { roomName: $.escapeHtml(parameters.room) } ));
mediator.emit('room:changed',false);
})
.on('message:send', function (data) {
var decrypted = $.AES.decrypt(data.msg, $.SHA1(parameters.room) + parameters.key),
sanitized = $.escapeHtml(decrypted),
nick = !data.nick ? templates.default_nick : $.escapeHtml($.AES.decrypt(data.nick, $.SHA1(parameters.room) + parameters.key));
if (!decrypted) {
mediator.emit('console:error', templates.messages.unable_to_decrypt);
} else {
mediator.emit('console:message', { message: sanitized, nick: nick } );
}
})
.on('message:server', function (data) {
if( data.msg ) {
var sanitized = $.escapeHtml(data.msg);
if( templates.server[sanitized] ) {
if( data.payload !== undefined ) {
var sanitized_payload = $.escapeHtml(data.payload);
mediator.emit('console:server', $.template(templates.server[sanitized], { payload: sanitized_payload }));
} else {
mediator.emit('console:server', templates.server[sanitized]);
}
} else {
mediator.emit('console:error', templates.server.bogus);
}
} else {
mediator.emit('console:error', templates.server.bogus);
}
})
.on('connect', function () {
// Tell the user that the chat is ready to interact with
mediator.emit('console:info', $.template(templates.messages.connected, {
host: host.host
}));
// Set window title
mediator.emit('window:title', settings.title);
// Unlock input
mediator.emit('console:unlockInput');
done();
host.connected = true;
})
.on('disconnect', function () {
host.connected = false;
// Tell the user that the chat is ready to interact with
mediator.emit('console:info', $.template(templates.messages.disconnected, {
host: host.host
}));
// Revert title
mediator.emit('room:changed',undefined);
mediator.emit('window:title',templates.client.title);
})
.on('connect_error', function () {
host.connected = false;
mediator.emit('console:error', templates.messages.socket_error);
// Unlock input
mediator.emit('console:unlockInput');
});
return;
},
reconnect = function (foo, done) {
if (host) {
if (host.connected) {
disconnect();
connect(host, done);
} else {
connect(host, done);
}
} else {
done();
return mediator.emit('console:error', templates.messages.reconnect_no_host);
}
},
disconnect = function () {
socket.disconnect();
},
param = function (p) {
parameters = Object.assign({}, parameters, p );
};
mediator.on('command:host', hostInfo);
mediator.on('command:connect', connect);
mediator.on('command:disconnect', disconnect);
mediator.on('command:reconnect', reconnect);
mediator.on('socket:emit', emit);
mediator.on('host:param', param);
}

View file

@ -1,42 +0,0 @@
var exports = {},
queue = [],
now = function () {
return performance.now() || Date.now();
};
exports.add_function_delayed = function(delay, callback, data) {
queue.push({
func: callback,
pushed: now(),
delay: delay,
data: data
});
};
exports.get = function () {
return queue;
};
exports.run = function () {
var i = 0,
current,
lrt_inner;
while ((current = queue[i++])) {
if (now() - current.pushed > current.delay) {
current.func();
queue.splice(i - 1, 1);
}
}
if (queue.length) {
// Waste a ms to prevent callstack overflow
lrt_inner = now();
while (now() - lrt_inner < 1) { void 0; }
exports.run();
}
};
export default exports;

View file

@ -1,69 +0,0 @@
/*
Accepts:
mediator.on('command:join', ...);
mediator.on('command:leave', ...);
mediator.on('command:key', ...);
Emits:
mediator.emit('room:changed',...);
mediator.emit('console:error',...);
mediator.emit('socket:emit',...);
*/
import $ from './$.js';
export default function (mediator, settings, templates) {
var // Private properties
room = false,
join = function(payload) {
if (room !== false) {
mediator.emit('console:error',
$.template(templates.messages.already_in_room, {
room: room
})
);
} else if (payload.length >= settings.room.maxLen) {
mediator.emit('console:error', $.template(templates.messages.room_name_too_long));
} else if (payload.length < settings.room.minLen) {
mediator.emit('console:error', $.template(templates.messages.room_name_too_short));
} else {
room = payload;
mediator
.emit('room:changed', room)
.emit('socket:emit', {
data: 'room:join',
payload: $.SHA1(room)
});
}
},
leave = function() {
if (room !== false) {
mediator.emit('socket:emit', {
data: 'room:leave',
payload: $.SHA1(room)
});
room = false;
} else {
mediator.emit('console:error', templates.messages.leave_from_nowhere);
}
},
count = function() {
if (room) {
mediator.emit('socket:emit', {data: 'room:count'});
} else {
mediator.emit('console:error', templates.messages.not_in_room);
}
};
// Connect events
mediator.on('command:join', join);
mediator.on('command:leave', leave);
mediator.on('command:count', count);
}

View file

@ -1,34 +0,0 @@
export default {
title: 'Claytonia Chat',
ttl: 600000,
motd: '<pre>\n\n' +
' Welcome to Claytonia Chat \n' +
' Tip of the day: /help \n' +
' Public Room: /join Claytonia \n' +
' Public Key: /key Claytonia \n' +
' Everyone in the room must have the same key to decrypt messages. \n' +
'----------------------------------------------------------------------' +
'</pre>',
nick: {
maxLen: 20,
minLen: 2,
},
key: {
maxLen: 1024,
minLen: 8,
},
room: {
minLen: 1,
maxLen: 64
},
notifications: {
maxOnePerMs: 3000
}
};

View file

@ -1,300 +0,0 @@
/**
* Licensed under the MIT License
*
* Copyright (c) 2014 Pehr Boman (github.com/unkelpehr)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/** @license Licenced under MIT - castrato - ©2014 Pehr Boman <github.com/unkelpehr> */
let
/**
* Contains the next unique node id.
*
* @property index
* @type {Integer}
* @private
*/
index = 0,
/**
* Contains all subscriptions
*
* @property subs
* @type {Object}
* @private
*/
subs = {},
/**
* Contains all emits that has been done with the `persistent` parameter set to `true`.
*
* @property emits
* @type {Object}
* @private
*/
emits = {},
/**
* An empty function that does not accept any arguments.
*
* @property noop
* @type {function}
* @private
*/
noop = function () {};
/**
* Creates a new entry in the `subs` object.
*
* @method on
* @private
* @param {Integer} fromId The unique subscriber ID.
* @param {String} event The event to subscribe to.
* @param {Function} handler A function to execute when the event is triggered.
*/
function on (fromId, event, handler, once) {
let i, item, subscription = [fromId, handler, handler.length > 1];
// Create if needed a namespace for this event and push the subscription.
(subs[event] || (subs[event] = [])).push(subscription);
// If it exists a persistent event that matches that which is currently being bound;
// loop through and each of them and emit to this handler.
if (emits[event]) {
i = 0;
subscription = [subscription];
while ((item = emits[event][i++])) {
emit(
0, // `persistent`
0, // `event`
item[0], // `data`
item[1], // `handler`
subscription // `explicitSubs`
);
if (once) {
break;
}
}
}
}
/**
* Removes all event handlers originating from `fromId` and optionally filter by handler.
*
* @method off
* @private
* @param {Integer} fromId The unique subscriber ID.
* @param {String} event The event to unsubscribe from.
* @param {Function} [handler=null] The original handler that was attached to this event. If not passed, all subscriptions will be removed.
*/
function off (fromId, event, handler) {
let sub,
i = 0,
toSubs = subs[event];
if (toSubs) {
while ((sub = toSubs[i++])) {
if (sub[0] === fromId && (!handler || handler === sub[1])) {
toSubs.splice(--i, 1);
}
}
}
}
/**
* Loops through all subscriptions, calling all handlers attached to given `event`.
*
* @method emit
* @private
* @param {Integer} fromId The unique subscriber ID.
* @param {String} event The event to emit
* @param {Object} [data=undefined] Parameters to pass along to the event handler.
* @param {Function} [handler=undefined] A function to execute when all event handlers has returned.
*/
function emit (persistent, event, data, callback, explicitSubs) {
let sub,
toSubs = explicitSubs || subs[event] || [],
total = toSubs.length,
left,
loop,
answers = [],
done;
// Add any wildcard subscriptions to the target subscriptions.
if (subs['*']) {
toSubs = toSubs.concat(subs['*']);
}
// Wildcard subscriptions shouldn't be counted as subscribers when passed to a possible emit callback.
loop = left = toSubs.length;
// Don't continue setup for calling all the subscribers if there isn't any.
if (loop) {
// If the emit function does not include a callback;
// we still have to set `done` to `noop` so that event callbacks
// does not try to execute something that is not a function.
done = !callback ? noop : function (data) {
if (data) {
answers.push(data);
}
if (!--left) {
callback(answers, total);
callback = 0;
}
};
// Execute all handlers that are bound to this event.
// Passing `done` if the handler expects it - otherwise decrementing the `left` variable.
while ((sub = toSubs[--loop])) {
sub[1](data, sub[2] ? done : left--, event);
}
}
// `func` get destructed when called.
// It has to be called at least once - even if no one was subscribing.
// Execute it if it still exists.
if (!left && callback) {
callback(answers, total);
}
// Save this emit if the `persistent` parameter is set to `true`.
if (persistent) {
(emits[event] || (emits[event] = [])).push([data, callback]);
}
}
/**
* Castrato entrypoint
*
* @constructor
* @returns {Castrato}
*/
function Castrato () {
this.nodeId = index++;
return this;
}
/**
* Execute all handlers attached to the given event.
*
* @method emit
* @param {String} event The event to emit
* @param {Object} [data=undefined] Parameters to pass along to the event handler.
* @param {Function} [func=undefined] A function to execute when all event handlers has returned.
* @return {Castrato} `this`
* @example
* $.emit('something');
* $.emit('something', { foo: 'bar' });
* $.emit('something', { foo: 'bar' }, function (data, subscribers) {
* console.log('Emit done, a total of ' + subscribers + ' subscribers returned: ', data);
* });
*/
Castrato.prototype.emit = function (persistent, event, data, handler) {
// emit('something', { data: true }, function () {});
if (persistent !== true && persistent !== false) {
handler = data;
data = event;
event = persistent;
persistent = false;
}
emit(persistent, event, data, handler);
return this;
};
/**
* Attach an event handler function for an event.
*
* @method on
* @param {String} event The event to subscribe to.
* @param {Function} handler A function to execute when the event is triggered.
* @return {Castrato} `this`
* @example
* $.on('something', function (data) {
* console.log('Got something!', data);
* });
*/
Castrato.prototype.on = function (event, handler) {
on(this.nodeId, event, handler);
return this;
};
/**
* Attach an event handler function for an event which will only be fired once.
*
* @method once
* @param {String} event The event to subscribe to.
* @param {Function} handler A function to execute when the event is triggered.
* @return {Castrato} `this`
* @example
* $.once('something', function (data) {
* console.log('Got something!', data);
* });
*/
Castrato.prototype.once = function (event, handler) {
on(this.nodeId, event, function wrapper (data, done) {
off(this.nodeId, event, wrapper);
handler(data, (handler.length > 1) ? done : done());
}, true);
return this;
};
/**
* Removes an event handler function for an event.
*
* @method off
* @param {String} event The event to unsubscribe from.
* @param {Function} [handler=null] The original handler that was attached to this event. If not passed, all subscriptions will be removed.
* @return {Castrato} `this`
* @example
* $.off('something');
* $.off('something else', handler);
*/
Castrato.prototype.off = function (event, handler) {
off(this.nodeId, event, handler);
return this;
};
// Only used in testing.
// Should get removed in production (and will be removed in the minified version)
Castrato.prototype.destroy = function () {
this.nodeId = 0;
index = 0;
subs = {};
emits = {};
return this;
};
// Always return instance?
/**
* @type {Castrato}
*/
let castrato = new Castrato();
// Export both named and default
export default castrato;
export { castrato };

View file

@ -1,17 +0,0 @@
[Unit]
Description=Cryptalk Node.js App
Documentation=https://github.com/Hexagon/cryptalk
After=network.target
[Service]
ExecStart=/usr/bin/npm run start
WorkingDirectory=/home/cryptochat/cryptalk
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PATH=/usr/bin
User=cryptochat
Group=cryptochat
[Install]
WantedBy=multi-user.target

View file

Before

(image error) Size: 20 KiB

After

(image error) Size: 20 KiB

3478
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,58 +1,42 @@
{
"name": "cryptalk",
"version": "1.2.9",
"description": "Encrypted HTML5/Node.JS instant chat",
"main": "server/server.js",
"preferGlobal": true,
"private": false,
"scripts": {
"test": "echo \"No tests written yet\" && exit 0",
"build": "npm update && npm outdated && npm run test:lint && npx rollup -c rollup.config.js && npm run build:minify && npm run build:cleanup",
"build:ci": "npm run test:lint && npx rollup -c rollup.config.js && npm run build:minify && npm run build:cleanup",
"build:minify": "uglifyjs client/public/js/cryptalk.js --source-map -o client/public/js/cryptalk.min.js",
"build:cleanup": "(rm client/public/js/cryptalk.js || del client\\public\\js\\cryptalk.js)",
"test:lint": "eslint ./client/source/**/*.js ./server/*.js",
"start": "node ./server/server.js"
},
"keywords": [
"cryptalk",
"chat",
"crypto-js",
"AES",
"secure",
"html5",
"encryption",
"privacy",
"e2ee"
],
"author": "Hexagon <Hexagon@GitHub>",
"contributors": [
{
"name": "Pehr Boman",
"email": "unkelpehr@gmail.com"
}
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/Hexagon/cryptalk.git"
},
"bin": "./server/server.js",
"dependencies": {
"serve": "^13.0.2",
"socket.io": "^4.3.1"
},
"os": [
"darwin",
"linux",
"win32"
],
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.6",
"crypto-js": "^4.1.1",
"eslint": "^8.1.0",
"rollup": "^2.59.0",
"uglify-js": "^3.14.3"
}
}
{
"name" : "cryptalk",
"version" : "1.0.1",
"description" : "Encrypted HTML5/Node.JS instant chat",
"main" : "server.js",
"subdomain": "cryptalk",
"analyze": true,
"preferGlobal": false,
"private": false,
"keywords": [
"cryptalk",
"fandango",
"crypto-js",
"AES",
"secure",
"html5"
],
"author": "Hexagon <Hexagon@GitHub>",
"contributors": [{
"name": "Pehr Boman",
"email": "unkelpehr@gmail.com"
}],
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/Hexagon/cryptalk.git"
},
"bin" : {
"npm" : "./server.js"
},
"dependencies": {
"node-uuid": ">= 1.4.1",
"express.io": ">= 1.1.13"
},
"os": [
"darwin",
"linux",
"win32"
],
"devDependencies": {}
}

View file

@ -1,11 +0,0 @@
{
"name": "cryptalk",
"script": "server/server.js",
"instances": "1",
"env": {
"NODE_ENV": "development"
},
"env_production" : {
"NODE_ENV": "production"
}
}

View file

@ -1,33 +1,35 @@
/*------------------------------------*\
GENERIC
\*------------------------------------*/
html {
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
box-sizing: border-box; /* Opera/IE 8+ */
}
*,
*:before,
*:after {
*, *:before, *:after {
box-sizing: inherit;
font-family: monospace, 'Courier New';
font-size: 12px;
}
body,
html {
body, html {
min-height: 100%;
min-width: 600px;
background-color: #181a1d;
background-color: #000000;
overflow: hidden;
padding: 0px;
margin: 0px;
color: #00dd00;
margin:0px;
color: #00DD00;
}
.good { color: #99ff99; }
.good { color: #99FF99; }
.bad { color: #ff7777; }
.info { color: #99ffff; }
.info { color:#99FFFF; }
.neutral { color: #eeeeee; }
/*------------------------------------*\
CHAT
\*------------------------------------*/
@ -37,19 +39,15 @@ html {
bottom: 40px;
position: absolute;
list-style-type: none;
padding: 0;
margin: 0;
padding:0;
margin:0;
}
/* Messages */
#chat li {
white-space: pre;
padding: 2px 15px;
color: #808008;
}
#chat li .timestamp {
color: #a0a00a;
color: #343434;
}
/* Message types */
@ -57,27 +55,24 @@ html {
#chat i {
font-style: normal;
}
#chat i.motd {
color: #99ff99;
display:inline-block;
line-height: 12px !important;
}
#chat i.motd { color: #99FF99; display:inline-block; line-height: 12px !important; }
#chat i.info { color: #999999; }
#chat i.server { color: #99ffff; }
#chat i.server { color: #99FFFF; }
#chat i.error { color: #ff7777; }
#chat i.message { color: #eeeeee; }
#chat i.nick { color: #99ff99; }
#chat i.nick { color: #99FF99; }
#chat i.fatal { color: #ff7777; }
/*------------------------------------*\
INPUT & LOADER
\*------------------------------------*/
#input_wrapper {
right: 0;
bottom: 0;
left: 0;
right:0;
bottom:0;
left:0;
position: absolute;
height: 30px;
height:30px;
}
#input,
@ -92,35 +87,24 @@ html {
padding: 5px 5px 5px 15px;
color: #ffffff;
background-color:#272a2e;
color: #FFFFFF;
background-color:#141414;
height:30px;
border-top: 2px solid #153315;
}
#input {
z-index: 1;
}
#loader {
z-index: 0;
line-height: 20px;
font-size: 14px;
font-weight: 100;
font-family: tahoma;
}
.loading #loader {
z-index: 2;
}
#input { z-index: 1; }
#loader { z-index: 0; line-height: 20px; font-size: 14px; font-weight: 100; font-family: tahoma;}
/*------------------------------------*\
SPINNER
\*------------------------------------*/
.loading #loader { z-index: 2; }
.loading #loader span {
margin-left:-2px;
-webkit-animation: rotation 1s infinite linear;
}
@-webkit-keyframes rotation {
from { -webkit-transform: rotate(0deg); }
to { -webkit-transform: rotate(359deg); }
from {-webkit-transform: rotate(0deg);}
to {-webkit-transform: rotate(359deg);}
}

View file

Before

(image error) Size: 8.7 KiB

After

(image error) Size: 8.7 KiB

View file

Before

(image error) Size: 8.2 KiB

After

(image error) Size: 8.2 KiB

View file

Before

(image error) Size: 7.8 KiB

After

(image error) Size: 7.8 KiB

View file

Before

(image error) Size: 628 B

After

(image error) Size: 628 B

View file

Before

(image error) Size: 1.4 KiB

After

(image error) Size: 1.4 KiB

View file

Before

(image error) Size: 3.3 KiB

After

(image error) Size: 3.3 KiB

View file

@ -1,13 +1,15 @@
<!doctype html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Cryptalk</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="css/default.css">
<link rel="icon" type="image/png" href="gfx/icon_32x32.png">
<script src="/socket.io/socket.io.js"></script>
<head profile="http://www.w3.org/2005/10/profile">
<link rel="icon"
type="image/png"
href="gfx/icon_32x32.png">
</head>
<body>
@ -23,7 +25,9 @@
<input type="text" id="input" />
</div>
<script src="js/cryptalk.min.js"></script>
<!-- Only include the script needed for loading the app -->
<script src="js/vendor/fandango.v20140924.min.js"></script>
<script src="js/bootstrap.js"></script>
</body>
</html>

40
public/js/bootstrap.js vendored Normal file
View file

@ -0,0 +1,40 @@
// This Javascript file is the only file besides fandango.js that will be fetched through DOM.
// Setup fandango
fandango.defaults({
baseUrl: 'js/cryptalk_modules/',
paths: {
websocket: 'https://cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.16/socket.io.min.js',
// Newer version:
// We'll have to fix the Access Control issue first though (https://github.com/Automattic/socket.io-client/issues/641).
// websocket: 'https://cdn.socket.io/socket.io-1.1.0.js',
aes: 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js',
SHA1: 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/sha1.js',
domReady: 'https://cdnjs.cloudflare.com/ajax/libs/require-domReady/2.0.1/domReady.min.js'
},
// CryptoJs AES does not support AMD modules. We'll have to create a shim.
shim: {
aes: {
exports: function () { // String (the global variable name) or a function; returning the desired variable.
return CryptoJS.AES;
}
},
SHA1: {
exports: function () {
return CryptoJS.SHA1;
}
}
}
});
// Require main cryptalk module.
require(['cryptalk'], function () {}, function (e) {
document.getElementById('chat').innerHTML = '<li><i class="fatal">Fatal: An error was thrown during initialization causing the application to stop.<br>Examine the logs for more details.</i></li>';
if (console.log) {
console.log(e);
}
throw e;
});

View file

@ -0,0 +1,179 @@
define(['fandango', 'websocket', 'aes', 'SHA1'], function (fandango, websocket, aes, SHA1) {
var exports = {
selector: 0,
utilities: {},
prototype: {}
},
// Shortcuts
utils = exports.utilities,
proto = exports.prototype,
each = fandango.each,
/**
* Regex for matching NaN.
*
* @property reNaN
* @type {Regex}
* @private
*/
reDigits = /^\d+$/;
// The DOM selector engine
exports.selector = function (selector) {
var match,
matches = [];
if (selector === document) {
matches.push(document);
} else {
selector = selector.slice(1);
if ((match = document.getElementById(selector))) {
matches.push(match);
}
}
// Must ALWAYS return a native array: []
return matches;
};
// Namespace SHA1
utils.SHA1 = function (string) {
return SHA1(string).toString();
};
// Namespace encode
utils.AES = {
decrypt: function (string, fgh) {
return aes.decrypt(string, fgh).toString(CryptoJS.enc.Utf8);
},
encrypt: function (string, fgh) {
return aes.encrypt(string, fgh).toString();
}
};
// Namespace websocket
utils.Websocket = {
connect: websocket.connect,
on: websocket.on
};
utils.ssplit = function (string, seperator) {
var components = string.split(seperator);
return [components.shift(), components.join(seperator)];
};
utils.activeElement = function () {
try { return document.activeElement; } catch (e) {}
};
/**
* Removes all characters but 0 - 9 from given string.
*
* @method digits
* @param {String} str The string to sanitize
* @return {String} The sanitized string
* @example
* $.digits('foo8bar'); // `8`
* $.digits('->#5*duckM4N!!!111'); // `54111`
*/
utils.isDigits = function(value) {
return reDigits.test(value);
};
/**
* A very simple templating function.
* @param {} str [description]
* @param {[type]} map [description]
* @return {[type]} [description]
*/
utils.template = function (str, map) {
return str && str.replace(/{(\w+)}/gi, function(outer, inner) {
return map.hasOwnProperty(inner) ? map[inner] : outer /* '' */;
});
};
utils.getJSON = function (path, onSuccess, onError) {
var data, request = new XMLHttpRequest();
request.open('GET', path, true);
request.onreadystatechange = function() {
if (this.readyState === 4) {
if (this.status >= 200 && this.status < 400) {
try {
onSuccess && onSuccess(JSON.parse(this.responseText));
} catch (e) {
onError && onError();
}
} else {
onError && onError();
}
}
};
request.send();
request = null;
};
// Part of this is originating from mustasche.js
// Code: https://github.com/janl/mustache.js/blob/master/mustache.js#L43
// License: https://github.com/janl/mustache.js/blob/master/LICENSE
utils.escapeHtml = (function () {
var pattern = /[&<>"'\/]/g,
entities = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;'
};
return function (string) {
return String(string).replace(pattern, function (s) {
return entities[s];
});
};
}());
// Extremely naive implementations of .html() and .append()
proto.html = function (string) {
each(this, function (element) {
element.innerHTML = string;
});
return this;
};
proto.append = function (string) {
for (var i = 0, len = this.length; i < len; i++) {
this[0].innerHTML += string;
}
return this;
};
// Naive implementations of .on()
proto.on = function (eventName, callback) {
for (var i = 0, len = this.length; i < len; i++) {
if (this[0].addEventListener) {
this[0].addEventListener(eventName, callback, false);
} else if (this[0].attachEvent) {
this[0].attachEvent('on' + eventName, callback);
}
}
return this;
};
proto.focus = function () {
this[0].focus();
return this;
};
return exports;
})

View file

@ -0,0 +1,69 @@
/* Usage:
mediator.emit('audio:play',message);
mediator.emit('audio:on');
mediator.emit('audio:off');
*/
// Sounds module, used for emitting those annoying bl-up sounds when receiving a message
define(['queue','mediator'], function (queue,mediator) {
var ac = false,
enabled = true,
// Recursive function for playing tones, takes an array of [tone,start_ms,duration_ms] - entries
// i is only used for recursion
playTones = function (tones, i) {
// Parameter defaults
i = (i === undefined) ? 0 : i;
// Stop if we've reached the end of iteration, and require ac
if (!ac || !enabled || !(i < Object.keys(tones).length)) {
return;
}
// Add tones to execution queue
var current_tones = tones[i],
freqs = current_tones[0],
start = current_tones[1],
duration = current_tones[2];
var o = ac.createOscillator();
var g = ac.createGain();
o.frequency.value = freqs;
o.connect(g);
g.gain.value = 0.25;
g.connect(ac.destination);
queue.add_function_delayed(start,function() { o.noteOn(0); });
queue.add_function_delayed(start+duration,function() { o.noteOff(0); });
// Iterate, or start playing
i++;
if( i < Object.keys(tones).length ) {
playTones(tones,i);
} else {
queue.run();
}
},
on = function() {
enabled = true;
},
off = function() {
enabled = false;
};
if (window.AudioContext || window.webkitAudioContext) {
ac = new (window.AudioContext || window.webkitAudioContext);
}
mediator.on('audio:play', function (message) { playTones(message); });
mediator.on('audio:on', function (message) { on(); });
mediator.on('audio:off', function (message) { off(); });
});

View file

@ -0,0 +1,552 @@
// Main cryptalk module
define({
compiles: ['$'],
requires: ['mediator', 'hosts', 'templates', 'audio', 'fandango','notifications', 'sounds', 'win']
}, function ($, requires, data) {
var socket,
key,
host,
room,
hash,
nick,
mute = false,
settings = {},
history = [],
history_pos = -1,
history_keep = 4,
history_timer,
// Collection of DOM components
components = {
chat: $('#chat'),
input: $('#input'),
inputWrapper: $('#input_wrapper')
},
// Shortcut
hosts = requires.hosts,
fandango = requires.fandango,
mediator = requires.mediator,
templates = requires.templates,
sounds = requires.sounds,
win = requires.win,
lockInput = function () {
components.input[0].setAttribute('disabled', 'disabled');
components.inputWrapper[0].className = 'loading';
},
unlockInput = function () {
components.input[0].removeAttribute('disabled');
components.inputWrapper[0].className = '';
components.input.focus();
},
showNotification = function (type, nick, text) {
var title = (type!='message') ? 'Cryptalk' : nick,
icon = (type == 'message') ? 'gfx/icon_128x128.png' : (type == 'error') ? 'gfx/icon_128x128_error.png' : 'gfx/icon_128x128_info.png';
// Emit notification
mediator.emit('notification:send',
{
title: title.substring(0, 20),
body: text.substring(0, 80),
icon: icon
});
// Emit sound
if ( type == 'message' ) mediator.emit('audio:play',sounds.message);
},
// Adds a new message to the DOM
post = function (type, text, clearChat, clearBuffer, nick) {
var tpl = templates.post[type],
post,
data = fandango.merge({}, settings, {
nick: nick,
room: room
});
data.text = $.template(text, data);
post = $.template(tpl, data);
// Always clear the input after a post
if (clearBuffer) {
clearInput();
}
showNotification(type, nick, text);
// Append the post to the chat DOM element
components.chat[clearChat ? 'html' : 'append'](post);
},
// Chat related commands
commands = {
help: function (payload, done) {
post('motd', templates.help);
done();
},
host: function () {
post('info', JSON.stringify(host || {}));
},
hosts: function (force, done) {
var i = 0,
left = hosts.hosts.length,
host,
strhosts = '\n',
callback = function (host, index, isUp) {
return function (hostSettings) {
host.settings = (isUp ? hostSettings : 0);
strhosts += $.template(templates.messages[(isUp ? 'host_available' : 'host_unavailable')], {
name: host.name,
path: host.path,
index: index
});
if (--left === 0) {
post('info', strhosts);
done();
}
};
};
//
force = (force && force.toLowerCase() === 'force');
// Loop through all the hosts
while ((host = hosts.hosts[i])) {
if (!force && host.settings !== undefined) {
if (host.settings) {
callback(host, i, 1)();
} else {
callback(host, i, 0)();
}
} else {
require([host.path], callback(host, i, 1), callback(host, i, 0));
}
i++;
}
},
connect: function (toHost, done) {
var request;
if (host && host.connected) {
done();
post('error', $.template(templates.messages.already_connected, {
host: host.name || 'localhost'
}));
return done();
}
if ($.isDigits(toHost)) {
if ((host = hosts.hosts[+toHost])) {
if (host.settings) {
settings = host.settings;
} else {
request = host.path;
}
} else {
post('error', 'Undefined host index: ' + toHost);
return done();
}
} else if (fandango.is(toHost, 'untyped')) {
settings = toHost.settings;
} else { // Assume string
request = toHost;
}
if (request) {
return require([request], function (settings) {
host.settings = settings;
commands.connect(toHost, done);
}, function () {
post('error', 'Could not fetch host settings: ' + request);
return done();
});
}
// Push 'Connecting...' message
post('info', $.template(templates.messages.connecting, {
host: host.name || 'localhost'
}));
// The one and only socket
socket = $.Websocket.connect(host.host, {
forceNew: true,
'force new connection': true
});
// Bind socket events
socket
.on('room:generated', function (data) {
var sanitized = $.escapeHtml(data);
post('server', $.template(templates.server.room_generated, { payload: sanitized }));
socket.emit('room:join', sanitized);
})
.on('room:joined', function (data) {
room = $.escapeHtml(data);
post('info', $.template(templates.messages.joined_room, { roomName: room }));
// Automatically count persons on join
socket.emit('room:count');
})
.on('room:left', function () {
post('info', $.template(templates.messages.left_room, { roomName: room }));
// Clear history on leaving room
clearHistory();
room = false;
})
.on('message:send', function (data) {
var decrypted = $.AES.decrypt(data.msg, room + key),
sanitized = $.escapeHtml(decrypted),
nick = !data.nick ? templates.default_nick : $.escapeHtml($.AES.decrypt(data.nick, room + key));
if (!decrypted) {
post('error', templates.messages.unable_to_decrypt);
} else {
post('message', sanitized, false, false, nick);
}
})
.on('message:server', function (data) {
if( data.msg ) {
var sanitized = $.escapeHtml(data.msg);
if( templates.server[sanitized] ) {
if( data.payload !== undefined ) {
var sanitized_payload = $.escapeHtml(data.payload);
post('server', $.template(templates.server[sanitized], { payload: sanitized_payload }));
} else {
post('server', templates.server[sanitized]);
}
} else {
post('error', templates.server.bogus);
}
} else {
post('error', templates.server.bogus);
}
})
.on('connect', function () {
// Tell the user that the chat is ready to interact with
post('info', $.template(templates.messages.connected, {
host: host.name || 'localhost'
}));
host.connected = 1;
done();
})
.on('disconnect', function () {
room = 0;
key = 0;
host.connected = 0;
// Tell the user that the chat is ready to interact with
post('info', $.template(templates.messages.disconnected, {
host: host.name || 'localhost'
}));
})
.on('error', function () {
room = 0;
key = 0;
host.connected = 0;
post('error', templates.messages.socket_error);
done();
});
},
reconnect: function (foo, done) {
if (host) {
if (host.connected) {
commands.disconnect();
commands.connect(host, done);
} else {
commands.connect(host, done);
}
} else {
done();
return post('error', templates.messages.reconnect_no_host);
}
},
disconnect: function () {
socket.disconnect();
},
clear: function () {
components.chat.html('');
// Clear command history on clearing buffer
clearHistory();
},
leave: function () {
if (room) {
socket.emit('room:leave', room);
} else {
post('error', templates.messages.leave_from_nowhere);
}
},
count: function () {
if (room) {
socket.emit('room:count');
} else {
post('error', templates.messages.not_in_room);
}
},
key: function (payload) {
if (!host) {
return post('error', templates.messages.key_no_host);
}
// Make sure the key meets the length requirements
if (payload.length > settings.key_maxLen) {
return post('error', templates.messages.key_to_long);
} else if (payload.length < settings.key_minLen) {
return post('error', templates.messages.key_to_short);
}
// Set key
key = payload;
// Inform that the key has been set
post('info', (room ? templates.messages.key_ok_ready : templates.messages.key_ok_but_no_room));
},
nick: function (payload) {
// Make sure the key meets the length requirements
if (payload.length > settings.nick_maxLen) {
return post('error', templates.messages.nick_to_long);
} else if (payload.length < settings.nick_minLen) {
return post('error', templates.messages.nick_to_short);
}
// Set nick
nick = payload;
// Inform that the key has been set
post('info', $.template(templates.messages.nick_set, { nick: $.escapeHtml(nick)}));
},
mute: function () {
mute = true;
return post('info', templates.messages.muted);
},
unmute: function () {
mute = false;
return post('info', templates.messages.unmuted);
},
title: function (payload) {
win.setTitle(payload);
return post('info', $.template(templates.messages.title_set, { title: $.escapeHtml(payload)}));
},
join: function (payload) {
if (!host) {
return post('error', templates.messages.join_no_host);
}
return (
room
? post('error', templates.messages.already_in_room)
: socket.emit('room:join', $.SHA1(payload))
);
},
generate: function (payload) {
return (
room
? post('error', templates.messages.already_in_room)
: socket.emit('room:generate')
);
}
},
// Push input buffer to history
pushHistory = function (b) {
history.push(b);
// Shift oldest buffer if we have more than we should keep
if (history.length > history_keep) {
history.shift();
}
},
// Clear input buffer history
clearHistory = function() {
history = [];
history_pos = -1;
},
// Clear input buffer
clearInput = function() {
fandango.subordinate(function () {
components.input[0].value = '';
});
},
// Handler for the document`s keyDown-event.
onKeyDown = function (e) {
var buffer,
parts,
payload,
command,
save;
// The Document object is bound to this element.
// If the active element is not the input, focus on it and exit the function.
// Ignore this when ctrl and/or alt is pressed!
if (!e.ctrlKey && !e.altKey && components.input[0] !== $.activeElement()) {
return components.input.focus();
}
// Reset command history clear timer
clearTimeout(history_timer);
history_timer = setTimeout(clearHistory, 60000);
// Check for escape key, this does nothing but clear the input buffer and reset history position
if (e.keyCode == 27) {
history_pos = -1;
clearInput();
return;
}
// Check for up or down-keys, they handle the history position
if (e.keyCode == 38 || e.keyCode == 40) {
if (e.keyCode == 38 ) { history_pos = (history_pos > history.length - 2) ? -1 : history_pos = history_pos + 1; }
else { history_pos = (history_pos <= 0) ? -1 : history_pos = history_pos - 1; }
var input = components.input[0];
input.value = (history_pos == -1) ? '' : history[history.length-1-history_pos];
// Wierd hack to move caret to end of input-box
setTimeout(function() {if(input.setSelectionRange) input.setSelectionRange(input.value.length, input.value.length);}, 0);
return;
}
// Return immediatly if the buffer is empty or if the hit key was not <enter>
if (e.keyCode !== 13 || !(buffer = components.input[0].value)) {
return;
}
// Reset current history position to 0 (last command)
history_pos = -1;
// Handle command
if ((buffer[0] || buffer.slice(0, 1)) === '/') {
parts = $.ssplit(buffer.slice(1), ' ');
command = parts[0];
payload = parts[1];
// Check that there is an handler for this command
if (!commands[command]) {
pushHistory(buffer);
return post('error', $.template(templates.messages.unrecognized_command, { commandName: command }));
}
// Some commands are asynchrounous;
// If the command expects more than one argument, the second argument is a callback that is called when the command is done.
if (commands[command].length > 1) {
// Lock the input from further interaction
lockInput();
// Execute command handler with callback function.
commands[command](payload, unlockInput);
} else {
// Execute normally.
commands[command](payload);
}
// Clear input field
clearInput();
// Save to history
if(command !== 'key') {
pushHistory(buffer);
}
} else /* Handle ordinary message */ {
if (!room || !key) {
// Push buffer to history and clear input field
pushHistory(buffer); clearInput();
// Make sure that the user has joined a room and the key is set
return (!room) ? post('error', templates.messages.msg_no_room) : post('error', templates.messages.msg_no_key);
}
// Before sending the message.
// Encrypt message using room UUID as salt and key as pepper.
socket.emit('message:send', {
room: room,
msg: $.AES.encrypt(buffer, room + key).toString(),
nick: nick ? $.AES.encrypt(nick, room + key).toString() : false
});
// And clear the the buffer
clearInput();
// Save to history
pushHistory(buffer);
}
};
// Bind the necessary DOM events
$(document).on('keydown', onKeyDown);
// Put focus on the message input
components.input.focus();
// Post the help/welcome message
post('motd', templates.motd, true);
// Route mediator messages
mediator.on('window:focused',function() {
mediator.emit('audio:off');
mediator.emit('notification:off');
});
mediator.on('window:blurred',function() {
if( !mute ) mediator.emit('audio:on');
mediator.emit('notification:on');
});
unlockInput();
// It's possible to provide room and key using the hashtag.
// The room and key is then seperated by semicolon (room:key).
// If there is no semicolon present, the complete hash will be treated as the room name and the key has to be set manually.
commands.connect(hosts.autoconnect, function() {
if (host && (hash = window.location.hash)) {
parts = hash.slice(1).split(':');
parts[0] && commands.join(parts[0]);
parts[1] && commands.key(parts[1]);
}
});
});

View file

@ -0,0 +1,20 @@
define({
// Used to autoconnect to specific host.
// Points to a specific index in the 'hosts' array.
// Use -1 to not autoconnect.
autoconnect: 0,
// A collection of hosts to choose from
hosts: [
{
name: 'default',
host: '',
path: '/js/cryptalk_modules/settings.js'
}/*,
{
name: 'Example',
host: 'http://www.example.com',
path: 'http://www.example.com/js/cryptalk_modules/settings.js'
}*/
]
});

View file

@ -0,0 +1,188 @@
(function (self, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof exports === 'object') { // Node
module.exports = factory();
} else {
// Attaches to the current context
self.castrato = factory;
}
}(this, (function () {
var
/**
* Contains the next unique node id.
*
* @property index
* @type {Integer}
* @private
*/
index = 0,
/**
* Contains all subscriptions
*
* @property subs
* @type {Object}
* @private
*/
subs = {},
/**
* An empty function.
* This method does not accept any arguments.
*
* @property noop
* @type {function}
* @private
*/
noop = function () {};
/**
* Creates a new entry in the `subs` object.
*
* @method on
* @private
* @param {Integer} fromId The unique subscriber ID.
* @param {String} event The event to subscribe to.
* @param {Function} func A function to execute when the event is triggered.
*/
function on (fromId, event, func) {
(subs[event] || (subs[event] = [])).push([fromId, func, func.length]);
}
/**
* Removes all event handlers originating from `fromId` and optionally filter by handler.
*
* @method off
* @private
* @param {Integer} fromId The unique subscriber ID.
* @param {String} event The event to unsubscribe from.
* @param {Function} [func=null] The original handler that was attached to this event. If not passed, all subscriptions will be removed.
*/
function off (fromId, event, func) {
var sub,
i = 0,
toSubs = subs[event];
if (toSubs) {
while ((sub = toSubs[i++])) {
if (sub[0] === fromId && (!func || func === sub[1])) {
toSubs.splice(--i, 1);
}
}
}
}
/**
* Loops through all subscriptions, calling all handlers attached to given `event`.
*
* @method emit
* @private
* @param {Integer} fromId The unique subscriber ID.
* @param {String} event The event to emit
* @param {Object} [data=undefined] Parameters to pass along to the event handler.
* @param {Function} [func=undefined] A function to execute when all event handlers has returned.
*/
function emit (persistent, event, data, func) {
var sub,
toSubs = subs[event] || [],
total = toSubs.length,
left = total,
loop = total,
answers = [],
done;
if (loop) {
done = !func ? noop : function (data) {
if (data) {
answers.push(data);
}
if (!--left) {
func(answers, total);
func = 0;
}
};
while ((sub = toSubs[--loop])) {
sub[1](data, (sub[2] > 1) ? done : left--);
}
}
// `func` get destructed when called.
// It has to be called at least once - even if no one was subscribing.
// Execute it if it still exists.
if (func) {
func(answers, total);
}
}
return function () {
var nodeId = index++;
return {
/**
* Execute all handlers attached to the given event.
*
* @method emit
* @param {String} event The event to emit
* @param {Object} [data=undefined] Parameters to pass along to the event handler.
* @param {Function} [func=undefined] A function to execute when all event handlers has returned.
* @return {Object} `this`
* @example
* $.emit('something');
* $.emit('something', { foo: 'bar' });
* $.emit('something', { foo: 'bar' }, function (data, subscribers) {
* console.log('Emit done, a total of ' + subscribers + ' subscribers returned: ', data);
* });
*/
emit: function (persistent, event, data, func) {
// emit('something', { data: true }, function () {});
if (persistent !== true || persistent !== false) {
func = data;
data = event;
event = persistent;
persistent = false;
}
emit(persistent, event, data, func);
return this;
},
/**
* Attach an event handler function for one event.
*
* @method on
* @param {String} event The event to subscribe to.
* @param {Function} func A function to execute when the event is triggered.
* @return {Object} `this`
* @example
* $.on('something', function (data) {
* console.log('Got something!', data);
* });
*/
on: function (event, func) {
on(nodeId, event, func);
return this;
},
/**
* Removes an event handler function for one event.
*
* @method off
* @param {String} event The event to unsubscribe from.
* @param {Function} [func=null] The original handler that was attached to this event. If not passed, all subscriptions will be removed.
* @return {Object} `this`
* @example
* $.off('something');
* $.off('something else', handler);
*/
off: function (event) {
off(nodeId, event);
return this;
}
};
};
}())));

View file

@ -1,125 +1,115 @@
/*
Usage
// Send an notification
mediator.emit('notification:send',{
title: 'Woop',
body: 'Woop woop',
icon: 'gfx/icon.png'
});
// Turn notifications on
mediator.emit('notification:on');
// Turn notifications off
mediator.emit('notification:off');
*/
export default function(mediator, settings, win) {
var enabled = true,
native_supported = false,
new_title,
original_title,
blink_timer,
interval,
last,
now = function () {
return performance.now() || Date.now();
},
on = function () {
enabled = true;
},
off = function () {
enabled = false;
},
resetState = function() {
clearTimeout(blink_timer);
if (original_title !== undefined) win.setTitle(original_title);
original_title = undefined;
new_title = undefined;
},
doBlink = function() {
if(enabled) {
if( win.getTitle() === original_title )
win.setTitle( new_title );
else
win.setTitle( original_title);
blink_timer = setTimeout(doBlink,interval);
} else {
resetState();
}
},
enableNative = function() {
if( native_supported && Notification.permission !== 'denied' ) {
Notification.requestPermission();
}
},
blinkTitleUntilFocus = function(t,i) {
interval = (i === undefined) ? 1000 : i;
if ( enabled && original_title === undefined ) {
new_title = t;
original_title = win.getTitle();
doBlink();
}
},
notify = function(title,body,icon,fallback) {
// Only notify while in background, and if sufficient time has passed
if( enabled && (now() - last) > settings.notifications.maxOnePerMs ) {
// Set default value for fallback parameter
if ( fallback === undefined) fallback = false;
if ( native_supported && Notification.permission === 'granted') {
// Create notification
var n = new Notification(title, {body: body, icon:icon});
// Handle on show event
n.onshow = function () {
// Automatically close the notification after 5000ms
setTimeout(function(){n.close();},3000);
};
last = now();
} else if ( fallback ) {
blinkTitleUntilFocus('Attention', 1000);
}
}
};
native_supported = (window.Notification !== undefined);
mediator.on('notification:send',function(data) { notify(data.title,data.body,data.icon,true); });
mediator.on('notification:on',function() { on(); });
mediator.on('notification:off',function() { off(); });
// Always enable native notifications
enableNative();
// Start with notifications disabled
off();
// If this is undefined, notifications will fail to show
last = now();
// Make sure we are at square one
resetState();
}
/*
Usage
// Send an notification
mediator.emit('notification:send',{
title: 'Woop',
body: 'Woop woop',
icon: 'gfx/icon.png'
});
// Turn notifications on
mediator.emit('notification:on');
// Turn notifications off
mediator.emit('notification:off');
*/
define(['mediator','win'], function (mediator, win) {
var enabled = true,
native_supported = false,
new_title,
original_title,
blink_timer,
interval,
now = function () {
return performance.now() || Date.now();
},
on = function () {
enabled = true;
},
off = function () {
enabled = false;
},
resetState = function() {
clearTimeout(blink_timer);
if (original_title !== undefined) setTitle(original_title);
original_title = undefined;
new_title = undefined;
window_active = true;
},
doBlink = function() {
if(enabled) {
if( win.getTitle() == original_title )
win.setTitle( new_title );
else
win.setTitle( original_title);
blink_timer = setTimeout(doBlink,interval);
} else {
resetState();
}
},
enableNative = function() {
if( native_supported && Notification.permission !== 'denied' ) {
Notification.requestPermission(function (status) {
Notification.permission = status;
});
}
},
blinkTitleUntilFocus = function(t,i) {
interval = (i == undefined) ? 1000 : i;
if ( enabled ) {
new_title = t;
original_title = getTitle();
doBlink();
}
},
notify = function(title,body,icon,fallback) {
// Only notify while in background
if( enabled) {
// Set default value for fallback parameter
if ( fallback === undefined) fallback = false;
if ( native_supported && Notification.permission === "granted") {
// Create notification
var n = new Notification(title, {body: body, icon:icon});
// Handle on show event
n.onshow = function () {
// Automatically close the notification after 5000ms
setTimeout(function(){n.close();},3000);
}
} else if ( fallback ) {
exports.blinkTitleUntilFocus("Attention",1000);
}
}
};
native_supported = (window.Notification !== undefined);
mediator.on('notification:send',function(data) { notify(data.title,data.body,data.icon,true); });
mediator.on('notification:on',function() { on(); });
mediator.on('notification:off',function() { off(); });
enableNative();
off();
// Make sure we are at square one
resetState();
});

View file

@ -0,0 +1,44 @@
define(function (){
var exports = {},
queue = [],
now = function () {
return performance.now() || Date.now();
};
exports.add_function_delayed = function(delay, callback, data) {
queue.push({
func: callback,
pushed: now(),
delay: delay,
data: data
});
}
exports.get = function () {
return queue;
}
exports.run = function () {
var i = 0,
current,
lrt_inner;
while (current = queue[i++]) {
if (now() - current.pushed > current.delay) {
current.func();
queue.splice(i - 1, 1);
}
}
if (queue.length) {
// Waste a ms to prevent callstack overflow
lrt_inner = now();
while (now() - lrt_inner < 1) { void 0; };
exports.run();
}
}
return exports;
});

View file

@ -0,0 +1,7 @@
define({
nick_maxLen: 20,
nick_minLen: 3,
key_maxLen: Infinity,
key_minLen: 8
});

View file

@ -1,20 +1,20 @@
export default {
message: [
[261.63*2,0,50],
[261.63*3,0,50],
[261.63*4,50,50],
[261.63*5,50,50]
],
person_joined: [
[261.63*3,0,200],
[261.63*1,0,200],
[261.63*3,200,500],
[261.63*2,200,500]
],
person_left: [
[261.63*3,0,200],
[261.63*2,0,200],
[261.63*3,200,500],
[261.63*1,200,500]
]
};
define({
message: [
[261.63*2,0,50],
[261.63*3,0,50],
[261.63*4,50,50],
[261.63*5,50,50]
],
person_joined: [
[261.63*3,0,200],
[261.63*1,0,200],
[261.63*3,200,500],
[261.63*2,200,500]
],
person_left: [
[261.63*3,0,200],
[261.63*2,0,200],
[261.63*3,200,500],
[261.63*1,200,500]
]
});

View file

@ -1,6 +1,22 @@
// The templating function only supports variables.
// Define a variable as so: {variable_name}
export default {
define({
motd: '<pre>\n\n' +
'▄████▄ ██▀███ ▓██ ██▓ ██▓███ ▄▄▄█████▓ ▄▄▄ ██▓ ██ ▄█▀ \n' +
'▒██▀ ▀█ ▓██ ▒ ██▒▒██ ██▒▓██░ ██▒▓ ██▒ ▓▒▒████▄ ▓██▒ ██▄█▒ \n' +
'▒▓█ ▄ ▓██ ░▄█ ▒ ▒██ ██░▓██░ ██▓▒▒ ▓██░ ▒░▒██ ▀█▄ ▒██░ ▓███▄░ \n' +
'▒▓▓▄ ▄██▒▒██▀▀█▄ ░ ▐██▓░▒██▄█▓▒ ▒░ ▓██▓ ░ ░██▄▄▄▄██ ▒██░ ▓██ █▄ \n' +
'▒ ▓███▀ ░░██▓ ▒██▒ ░ ██▒▓░▒██▒ ░ ░ ▒██▒ ░ ▓█ ▓██▒░██████▒▒██▒ █▄ \n' +
'░ ░▒ ▒ ░░ ▒▓ ░▒▓░ ██▒▒▒ ▒▓▒░ ░ ░ ▒ ░░ ▒▒ ▓▒█░░ ▒░▓ ░▒ ▒▒ ▓▒ \n' +
' ░ ▒ ░▒ ░ ▒░▓██ ░▒░ ░▒ ░ ░ ▒ ▒▒ ░░ ░ ▒ ░░ ░▒ ▒░ \n' +
'░ ░░ ░ ▒ ▒ ░░ ░░ ░ ░ ▒ ░ ░ ░ ░░ ░ \n' +
'░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ \n' +
'░ ░ ░ \n' +
' https://github.com/hexagon/cryptalk \n' +
' \n' +
' Tip of the day: /help \n' +
'----------------------------------------------------------------------' +
'</pre>',
help: '<pre> \n' +
'Cryptalk, encrypted instant chat. \n' +
@ -15,17 +31,16 @@ export default {
' /clear Clear on-screen buffer \n' +
' /help This \n' +
' /title Set your local page title \n' +
' /torch AfterSeconds Console messages are torched \n' +
' after this amount of seconds \n' +
' (default 600). \n' +
' \n' +
'Room: \n' +
' /generate Generate random room \n' +
' /join RoomId Join a room \n' +
' /leave Leave the room \n' +
' /count Count participants \n' +
' \n' +
'Host: \n' +
' /connect Connect to host \n' +
' /hosts List available hosts \n' +
' /connect HostIndex Connect to selected host \n' +
' /disconnect Disconnect from host \n' +
' \n' +
'You can select any of the five last commands/messages with up/down key.\n' +
@ -44,11 +59,11 @@ export default {
// All post templates will have access to the properties in the 'settings' module,
// along with the current nick, room, mute-status and of course the message ('text').
post: {
motd: '<li id="{id}"><i class="motd">{text}</i></li>',
info: '<li id="{id}"><i class="timestamp">[{timestamp}] </i>INF&gt; <i class="info">{text}</i></li>',
server: '<li id="{id}"><i class="timestamp">[{timestamp}] </i>SRV&gt; <i class="server">{text}</i></li>',
error: '<li id="{id}"><i class="timestamp">[{timestamp}] </i>ERR&gt; <i class="error">{text}</i></li>',
message: '<li id="{id}"><i class="timestamp">[{timestamp}] </i>MSG&gt; <i class="nick">{nick}&gt;</i> <i class="message">{text}</i></li>'
motd: '<li><i class="motd">{text}</i></li>',
info: '<li>INF&gt; <i class="info">{text}</i></li>',
server: '<li>SRV&gt; <i class="server">{text}</i></li>',
error: '<li>ERR&gt; <i class="error">{text}</i></li>',
message: '<li><i class="nick">{nick}&gt;</i> <i class="message">{text}</i></li>'
},
// All message templates will have access to the properties in the 'settings' module,
@ -56,7 +71,8 @@ export default {
messages: {
key_to_short: 'Hmm, that\'s a weak key, try again...',
key_to_long: 'Man that\'s a long key. Make it a tad short, \'kay?',
key_ok: 'Key set, you can now start communicating.',
key_ok_ready: 'Key set, you can now start communicating.',
key_ok_but_no_room: 'Key set, you can now join a room and start communicating.',
key_no_host: 'You have to connect to a host before setting the key.',
join_no_host: 'You have to connect to a host before joining a room.',
@ -70,9 +86,6 @@ export default {
msg_no_key: 'You have to set an encryption key before sending a message. See /help.',
leave_from_nowhere: 'How are you supposed to leave, while being nowhere?',
torch_is_now: 'Messages are now torched after {ttl} seconds.',
torch_not_set: 'Invalid torch delay entered, nothing changed. See /help.',
title_set: 'The title of this window is now \'{title}\'.',
muted: 'Notifications and sounds are now muted.',
@ -80,12 +93,9 @@ export default {
unrecognized_command: 'Unrecognized command: "{commandName}"',
room_name_too_long: 'Isn\'t that a bit long?',
room_name_too_short: 'Nah, too short.',
joined_room: 'Joined room {roomName}.',
left_room: 'Left room {roomName}.',
already_in_room: 'You are already in a room ({room}), try /leave first.',
joined_room: 'Joined room {room}',
left_room: 'Left room {room}',
already_in_room: 'You are already in a room ({room}), stoopid.',
unable_to_decrypt: 'Unabled to decrypt received message, keys does not match.',
@ -103,13 +113,9 @@ export default {
server: {
person_joined: 'A person joined this room.',
person_left: 'A person left this room.',
person_count: 'There are {payload} people in this room, including you.',
person_single: 'You are the only person in this room.',
room_generated: 'Room {payload} generated.',
person_count: 'There is {payload} person(s) in this room, including you.',
command_failed: 'Server command failed, you\'re probably trying to du something bogus.',
bogus: 'Received a bogus message from server.'
},
client: {
title: 'Cryptalk - Offline'
}
};
});

View file

@ -1,42 +1,39 @@
/*
Accepts:
'window:title'
Emits:
'window:focused'
'window:blurred'
Exports:
title = window.getTitle();
window.setTitle(title);
*/
export default function(mediator) {
var exports = {},
focusCallback = function() {
mediator.emit('window:focused');
},
blurCallback = function() {
mediator.emit('window:blurred');
};
exports.setTitle = function(t) { document.title = t; },
exports.getTitle = function() { return document.title; };
// Keep track of document focus/blur
if (window.addEventListener){
// Normal browsers
window.addEventListener('focus', focusCallback, true);
window.addEventListener('blur', blurCallback, true);
} else {
// IE
window.observe('focusin', focusCallback);
window.observe('focusout', blurCallback);
}
mediator.on('window:title',exports.setTitle);
return exports;
}
/*
Emits:
'window:focused'
'window:blurred'
Exports:
title = window.getTitle();
window.setTitle(title);
*/
define(['mediator'],function (mediator){
var exports = {},
focusCallback = function() {
mediator.emit('window:focused');
},
blurCallback = function() {
mediator.emit('window:blurred');
};
exports.setTitle = function(t) { document.title = t; },
exports.getTitle = function() { return document.title; };
// Keep track of document focus/blur
if (window.addEventListener){
// Normal browsers
window.addEventListener("focus", focusCallback, true);
window.addEventListener("blur", blurCallback, true);
} else {
// IE
window.observe("focusin", focusCallback);
window.observe("focusout", blurCallback);
}
return exports;
});

10
public/js/vendor/aes.v3.1.2.min.js vendored Normal file
View file

@ -0,0 +1,10 @@
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
(function(){for(var q=CryptoJS,x=q.lib.BlockCipher,r=q.algo,j=[],y=[],z=[],A=[],B=[],C=[],s=[],u=[],v=[],w=[],g=[],k=0;256>k;k++)g[k]=128>k?k<<1:k<<1^283;for(var n=0,l=0,k=0;256>k;k++){var f=l^l<<1^l<<2^l<<3^l<<4,f=f>>>8^f&255^99;j[n]=f;y[f]=n;var t=g[n],D=g[t],E=g[D],b=257*g[f]^16843008*f;z[n]=b<<24|b>>>8;A[n]=b<<16|b>>>16;B[n]=b<<8|b>>>24;C[n]=b;b=16843009*E^65537*D^257*t^16843008*n;s[f]=b<<24|b>>>8;u[f]=b<<16|b>>>16;v[f]=b<<8|b>>>24;w[f]=b;n?(n=t^g[g[g[E^t]]],l^=g[g[l]]):n=l=1}var F=[0,1,2,4,8,
16,32,64,128,27,54],r=r.AES=x.extend({_doReset:function(){for(var c=this._key,e=c.words,a=c.sigBytes/4,c=4*((this._nRounds=a+6)+1),b=this._keySchedule=[],h=0;h<c;h++)if(h<a)b[h]=e[h];else{var d=b[h-1];h%a?6<a&&4==h%a&&(d=j[d>>>24]<<24|j[d>>>16&255]<<16|j[d>>>8&255]<<8|j[d&255]):(d=d<<8|d>>>24,d=j[d>>>24]<<24|j[d>>>16&255]<<16|j[d>>>8&255]<<8|j[d&255],d^=F[h/a|0]<<24);b[h]=b[h-a]^d}e=this._invKeySchedule=[];for(a=0;a<c;a++)h=c-a,d=a%4?b[h]:b[h-4],e[a]=4>a||4>=h?d:s[j[d>>>24]]^u[j[d>>>16&255]]^v[j[d>>>
8&255]]^w[j[d&255]]},encryptBlock:function(c,e){this._doCryptBlock(c,e,this._keySchedule,z,A,B,C,j)},decryptBlock:function(c,e){var a=c[e+1];c[e+1]=c[e+3];c[e+3]=a;this._doCryptBlock(c,e,this._invKeySchedule,s,u,v,w,y);a=c[e+1];c[e+1]=c[e+3];c[e+3]=a},_doCryptBlock:function(c,e,a,b,h,d,j,m){for(var n=this._nRounds,f=c[e]^a[0],g=c[e+1]^a[1],k=c[e+2]^a[2],p=c[e+3]^a[3],l=4,t=1;t<n;t++)var q=b[f>>>24]^h[g>>>16&255]^d[k>>>8&255]^j[p&255]^a[l++],r=b[g>>>24]^h[k>>>16&255]^d[p>>>8&255]^j[f&255]^a[l++],s=
b[k>>>24]^h[p>>>16&255]^d[f>>>8&255]^j[g&255]^a[l++],p=b[p>>>24]^h[f>>>16&255]^d[g>>>8&255]^j[k&255]^a[l++],f=q,g=r,k=s;q=(m[f>>>24]<<24|m[g>>>16&255]<<16|m[k>>>8&255]<<8|m[p&255])^a[l++];r=(m[g>>>24]<<24|m[k>>>16&255]<<16|m[p>>>8&255]<<8|m[f&255])^a[l++];s=(m[k>>>24]<<24|m[p>>>16&255]<<16|m[f>>>8&255]<<8|m[g&255])^a[l++];p=(m[p>>>24]<<24|m[f>>>16&255]<<16|m[g>>>8&255]<<8|m[k&255])^a[l++];c[e]=q;c[e+1]=r;c[e+2]=s;c[e+3]=p},keySize:8});q.AES=x._createHelper(r)})();

26
public/js/vendor/domReady.v2.0.1.min.js vendored Normal file
View file

@ -0,0 +1,26 @@
/*
MIT License
-----------
Copyright (c) 2010-2011, The Dojo Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
define(function(){"use strict";function e(e){var t;for(t=0;e.length>t;t+=1)e[t](h)}function t(){var t=c;l&&t.length&&(c=[],e(t))}function i(){l||(l=!0,o&&clearInterval(o),t())}function n(e){return l?e(h):c.push(e),n}var r,s,o,a="undefined"!=typeof window&&window.document,l=!a,h=a?document:null,c=[];if(a){if(document.addEventListener)document.addEventListener("DOMContentLoaded",i,!1),window.addEventListener("load",i,!1);else if(window.attachEvent){window.attachEvent("onload",i),s=document.createElement("div");try{r=null===window.frameElement}catch(u){}s.doScroll&&r&&window.external&&(o=setInterval(function(){try{s.doScroll(),i()}catch(e){}},30))}"complete"===document.readyState&&i()}return n.version="2.0.1",n.load=function(e,t,i,r){r.isBuild?i(null):n(i)},n});

View file

@ -0,0 +1,21 @@
(function(q){function C(e){this.name="TimeoutError";this.message=e||""}function D(e){this.name="RejectedError";this.message=e||""}function G(e,d,g,h){var c,a,b=[],f=0,m,l,p,s=function(a){return function(c,l){b[a]=n[c].exports;e.length===++f&&(!l&&h&&clearTimeout(m),d&&d(b))}};h=h||x.timeout;for(p=0;l=e[p++];)if((c=n[l])&&1!==c.state)if(2===c.state)if(a=new D('Could resolve all dependencies; dependency "'+l+'" has been rejected.'),g)g(a);else throw a;else 3===c.state&&s(p-1)(l,1);else v[l]||(v[l]=
[]),v[l].unshift(s(p-1));h&&e.length<b.length&&(m=setTimeout(function(){for(var c,d,f=0,l=0;c=e[f++];)for(l=0;d=b[l++];)c===d&&(b.splice(--l,1),e.splice(--f,1));a=new C("Load timeout of "+h+'ms exceeded for module(s) "'+e.join('", "')+'".');if(g)g(a);else throw a;},h))}function y(){var e=!1,d,g,h,c,a,b,f,m;if(arguments[0]!==E){for(d=0;(g=arguments[d++])&&(_type=(typeof g)[0]||(typeof g).slice(0,1));)"s"===_type?a?h=1:a=g:"f"===_type?f?m?h=1:m=g:f=g:"o"===_type&&(k.is(g,"array")?(b={requires:g},e=
!0):b?f=g:b=g),h&&(c=new TypeError("define called with unrecognized signature; `"+Array.prototype.join.call(arguments,", ")+"`."));b=b||{};a=a||b.UID;f=f||b.factory;m=m||b.onRejected}else(d=z.pop())?(a=arguments[1],b=d[0],f=d[1],m=d[2],e=d[3]):c=Error("Inconsistent naming queue");c||f||(b?(f=b,b={}):c=Error('Missing factory for module "'+a+'"'));!c&&n[a]&&(c=Error('Duplicate entry for module "'+a+'"'));if(c)if(m=m||arguments[2])m(c);else throw c;else a?(d=n[a]=b,d.UID=a,d.amdStyle=e,d.factory=f,d.state=
1,d.requires=k.is(d.requires,"array")&&!!d.requires.length&&d.requires,d.inherits=k.is(d.inherits,"array")&&!!d.inherits.length&&d.inherits,d.compiles=k.is(d.compiles,"array")&&!!d.compiles.length&&d.compiles,d.instances||(d.instances={instance1:{}})):z.unshift([b,f,m,e])}var n={},x={deepCopy:!0,baseUrl:"",namespace:"default",timeout:1E3,paths:{},shim:{}},k={},E={a:1},v={},z=[],A=Array.prototype.push;C.prototype=Error.prototype;D.prototype=Error.prototype;k.is=function(){var e=Object.prototype.hasOwnProperty,
d=Object.prototype.toString,g={array:Array.isArray||function(a){return"[object Array]"==d.call(a)},arraylike:function(a){if(!a||!a.length&&0!==a.length||g.window(a))return!1;var b=a.length;return 1===a.nodeType||g.array(a)||!g["function"](a)&&(0===b||"number"===typeof b&&0<b&&!k.is(a,"primitive")&&b-1 in a)},"boolean":function(a){return!0===a||!1===a},element:function(a){return!(!a||1!==a.nodeType)},finite:function(a){return isFinite(a)&&!isNaN(parseFloat(a))},integer:Number.isInteger||function(a){return"number"===
typeof a&&g.finite(a)&&-9007199254740992<a&&9007199254740992>a&&Math.floor(a)===a},iterable:function(a){try{1 in obj}catch(b){return!1}return!0},nan:function(a){return g.number(a)&&a!=+a},number:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},object:function(a){return a===Object(a)},primitive:function(a){return!0===a||!1===a||null==a||!!{string:1,number:1}[typeof a]},string:function(a){return"string"==typeof a||a instanceof String},undefined:function(a){return void 0===a},untyped:function(a){if(!a||
a.nodeType||"[object Object]"!==d.call(a)||g.window(a))return!1;try{if(a.constructor&&!e.call(a,"constructor")&&!e.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(b){return!1}for(var c in a);return void 0===c||e.call(a,c)},window:function(a){return null!=a&&a==a.window},empty:function(a){if(a){if(k.is(a,"array"))return 0===a.length;for(var b in a)if(e.call(a,b))return!1}return!0}},h=0,c=["Arguments","Date","Function","RegExp"];for(;4>h;h++)g[c[h].toLowerCase()]=function(a){a="[object "+
a+"]";return function(b){return d.call(b)==a}}(c[h]);g.args=g.arguments;g.bool=g["boolean"];g.plain=g.untyped;return function(a,b){if(g["function"](b))return a===b(a);if(!g.string(b))return a===b;if((b=b.toLowerCase())&&g[b])return g[b](a);throw'Unknown type "'+b+'"';}}();k.each=function(){var e=Array.prototype.some;return function(d,g,h){var c,a;if(void 0===d)return obj;h=h||k;if(e&&d.some===e)return d.some(g,h),d;if(k.is(d,"array")||k.is(d,"arraylike")){c=0;for(a=d.length;c<a&&(void 0===d[c]||!g.call(h,
d[c],c,d));c++);return d}for(c in d)if(d.hasOwnProperty(c)&&g.call(h,d[c],c,d))break;return d}}();k.merge=function(e){var d,g,h,c,a,b=arguments.length,f=!1;e=arguments[0];var m=1,l,p,s=0;if(!0===e||!1===e)f=e,e=arguments[m],m++;k.is(arguments[b-1],"function")&&(a=arguments[b-1],b--);if(void 0===e)e={};else if(k.is(e,"primitive"))throw new TypeError("Cannot merge primitive data types: merge("+Array.prototype.join.call(arguments,", ")+")");p=k.is(e,"array");if(m<b)for(;m<=b;)if(c=arguments[m++]){p=
p&&k.is(c,"array");for(h in c)g=e[h],d=c[h],void 0===d||d===e?p&&s--:(f&&d&&((l=k.is(d,"array"))||k.is(d,"untyped"))&&(d=k.merge(f,g||(l?k.is(g,"array")?g:[]:k.is(g,"untyped")?g:{}),d,a)),a&&a(h,e,c)||(s&&(h=+h+s),e[h]=d));s=0}return e};k.subordinate=function(){function e(b){if(!b||b.data===c&&b.source&&(d===b.source||d.self&&d.self===b.source)){var e=a.shift();b=e[0];var e=e[1],l=e.length;l?1===l?b(e[0]):2===l?b(e[0],e[1]):3===l?b(e[0],e[1],e[2]):4===l?b(e[0],e[1],e[2],e[3]):b.apply(0,e):b()}}var d=
window,g=Array.prototype.slice,h=d.postMessage,c="__fandango@"+Math.random().toString(36),a=[],b=function(){return h?function(){h(c,"*")}:function(){setTimeout(e)}}();h&&(d.addEventListener?d.addEventListener("message",e,!1):d.attachEvent&&d.attachEvent("onmessage",e));return function(c){b(a.push([arguments[arguments.length-1],g.call(arguments).slice(0,-1)])-1)}}();var H=function(){var e={},d=[].slice,g=function(){return[]};return function(h,c){function a(a,c){return new b(a,c)}function b(a,b){var c=
0,d=f(a,b),e=d.length;this.context=b||document;this.selector=a;for(this.length=e;c<e;c++)this[c]=d[c];return this}var f,k,l,p;if(e[h])return e[h];for(p=c.length;k=c[--p];){k.selector&&(f=k.selector);if(source=k.prototype)if("function"===typeof source)source.call(b.prototype);else if(b.prototype.prototype)for(l in source)source.hasOwnProperty(l)&&(b.prototype[l]=source[l]);else b.prototype=source;if(source=k.utilities)if("function"===typeof source)source.utilities.call(a);else for(l in source)source.hasOwnProperty(l)&&
(a[l]=source[l])}b.prototype.length=0;b.prototype.selector="";b.prototype.splice=d;b.prototype.fandango=1;f||(f=g);return e[h]=a}}(),F=function(){function e(d,e,h,c){var a,b=n[d],f=k.is(b.factory,"function"),m=f?b.factory.length:0,l=b.amdStyle,p,s;c&&(a=H(b.compiles+"",c));for(s in b.instances)c=b.instances[s],c.context=void 0===c.context?b.context:c.context,c.deepCopy=void 0===c.deepCopy?b.deepCopy:c.deepCopy,c.namespace=void 0===c.namespace?b.namespace:c.namespace,p=[],l?p=e||[]:(a&&p.push(a),e&&
p.push(e),m>p.length&&p.push(c.data?k.merge(c.deepCopy,{},c.data,b.data):b.data),m>p.length&&p.push(k.merge(c.deepCopy,{},c,b))),c.exports=f?b.factory.apply(c.context,p)||{}:b.factory,void 0===b.exports&&(b.exports=h?c.exports?k.merge(!0,{},h,c.exports):h:c.exports);b.state=3;if(e=v[d])for(;e.length;)e.pop()(d)}return function g(h,c,a){var b,f;f=n[h];var m,l,p,s;b=[];var r;if(f){a=k.is(f.factory,"function")?f.factory.length:0;m=f.amdStyle;if(!c&&(f.requires||f.compiles||f.inherits)){f.requires&&A.apply(b,
f.requires);f.inherits&&A.apply(b,f.inherits);f.compiles&&A.apply(b,f.compiles);for(c=0;r=b[c++];)if(n[r])if(3===n[r].state)b.splice(--c,1);else if(2===n[r].state)throw Error('Could not instantiate "'+UID+'"; dependency "'+dependency+'" has been rejected.');if(0<b.length){k.subordinate(b,function(){g(h,!0)},B);return}}void 0===(b=f.deepCopy)&&(b=x.deepCopy);f=n[h]=k.merge(b,{},x,f);if(a&&f.requires)for(p=f.requires&&(m?[]:{}),c=0;r=f.requires[c++];)m?p.push(n[r].exports):k.is(n[r].exports,"function")?
p[r]=n[r].exports:k.merge(b,p[r]={},n[r].exports);if(f.inherits)for(s=f.inherits&&{},c=0;r=f.inherits[c++];)k.merge(b,s,n[r].exports);if(a&&f.compiles)for(l=f.compiles&&[],c=0;r=f.compiles[c++];)l.push(n[r].exports);e(h,p,s,l)}else if(f=Error('Tried to invoke non-existent module "'+h+'"'),a)a(f);else throw f;}}();y.amd={};var B=function(){var e={},d=document.getElementsByTagName("head")[0],g="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),h=function(a){var b=document.createElement("script");
b.setAttribute("id",a);b.type="text/javascript";b.charset="utf-8";b.async=!0;return b},c=!(!(c=h())||!c.attachEvent||c.attachEvent.toString&&0>c.attachEvent.toString().indexOf("[native code")||g),a=function(){var a={"http://":1,"https://":1,"file://":1};return function(b){var c=b.toLowerCase(),e=c[0]||c.slice(0,1);"/"===e||"\\"===e||"h"===e&&(a[c.slice(0,7)]||a[c.slice(0,8)])||"f"===e&&a[c.slice(0,7)]||(b=x.baseUrl+b);".js"!==b.slice(-3)&&(b+=".js");return b}}(),b=function(a,b,c,d){return function(f){e[b]=
0;a.error=f||!0;d.parentElement.removeChild(d);if(a.onError)a.onError(f,c,b)}},f=function(a,b,c,d,f){return function(){if((!f||"loaded"===d.readyState||"complete"===d.readyState)&&2===e[b]){e[b]=3;if(a.onPartial)a.onPartial(b,c,d);f?d.detachEvent("onreadystatechange",a.loadListener):(d.removeEventListener("load",a.loadListener),d.removeEventListener("error",a.errorListener));if(a.loaded.push(b)===a.paths.length&&a.onDone)a.onDone(a.load)}}},m=function(a){for(var g=0,k,m,n;!a.error&&(k=a.paths[g++]);)m=
a.names[g-1],e[k]=2,n=h(k),c?n.attachEvent("onreadystatechange",f(a,k,m,n,!0)):(a.loadListener=f(a,k,m,n),a.errorListener=b(a,k,m,n),n.addEventListener("load",a.loadListener,!1),n.addEventListener("error",a.errorListener,!1)),n.src=k,d.appendChild(n)};return function(b,c,d,f){var g=[],h=[],t={paths:[],names:[],loaded:[],error:null,loadListener:null,errorListener:null},v,u,w,z,A;t.onError=function(a,b,c){a instanceof Error||(a=Error('Could require "'+b+'"; an error occurred while trying to load "'+
c+'".'));if(d)d(a);else throw a;};for(A=0;u=b[A++];)if(n[u])if(3===n[u].state)h.push(n[u].exports);else{if(2===n[u].state)throw Error("require(): Rejected dependency. Cannot continue.");F(u,null,t.onError);g.push(u)}else v=a(x.paths[u]||u),e[v]||(t.paths.push(v),t.names.push(u),e[v]=2),g.push(u);g.length?(t.paths&&(t.onPartial=function(a,b,c){n[b]||((w=x.shim[b])?(z=k.is(w.exports,"function")?w.exports:function(){return q[w.exports]},y(b,z,t.onError)):y(E,b,t.onError));F(b,null,t.onError);f&&f(a,
c)},m(t)),G(g,function(a){c.apply(null,a)},t.onError)):c&&c.apply(null,h)}}();y("fandango",k);var I=q.fandango,J=q.define,K=q.require,w={fn:k,define:y,require:B,noConflict:function(e){q.define===y&&(q.define=J);q.require===B&&(q.require=K);e&&q.fandango===w&&(q.fandango=I);return w},defaults:function(e){return e?k.merge(x,e):x}};q.define||(q.define=w.define);q.require||(q.require=w.require);q.fandango=w;q.modules=n;q.awaitingDependency=v;q.awaitingName=z})(this);

2
public/js/vendor/socket.io.v0.9.16.min.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,14 +0,0 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
module.exports = {
input: 'client/source/cryptalk.js',
output: {
file: 'client/public/js/cryptalk.js',
format: 'iife'
},
plugins: [
nodeResolve(),
commonjs()
]
};

91
server.js Normal file
View file

@ -0,0 +1,91 @@
var express = require('express.io'),
uuid = require('node-uuid'),
app = express();app.http().io();
app.use(express.static(__dirname + '/public'));
app.io.route('room', {
generate: function(req) {
var room = uuid.v4();
req.socket.emit('room:generated',room);
},
join: function(req) {
if( req.data ) {
req.socket.emit('room:joined',req.data);
req.socket.join(req.data);
req.socket.broadcast.to(req.data).emit('message:server', {msg:'person_joined'} );
req.socket.current_room = req.data;
} else {
req.socket.emit('message:server', {msg:'command_failed'} );
}
},
leave: function(req) {
if( req.data ) {
req.socket.emit('room:left');
req.socket.leave(req.data);
req.socket.broadcast.to(req.data).emit('message:server', {msg:'person_left'} );
req.socket.current_room = undefined;
} else {
req.socket.emit('message:server', {msg:'command_failed'} );
}
},
count: function(req) {
if( req.socket.current_room !== undefined ) {
// This will fail on socket.io >= 1.0
var client_count = app.io.sockets.clients(req.socket.current_room).length;
req.socket.emit('message:server', {msg:'person_count', payload: client_count } );
} else {
req.socket.emit('message:server', {msg:'command_failed'} );
}
}
});
app.io.route('message', {
send: function(req) {
// Check that the user is in a room
if(req.data && req.data.room) {
// Check that the message size is within bounds
var total_msg_size = (req.data.msg) ? req.data.msg.length : 0 + (req.data.nick) ? req.data.nick.length : 0;
if( total_msg_size <= 4096) {
// Check that at least 100ms has passed since last message
if( req.socket.last_message === undefined || new Date().getTime() - req.socket.last_message > 100 ) {
req.socket.broadcast.to(req.data.room).emit('message:send', { msg: req.data.msg, nick: req.data.nick} );
req.socket.emit('message:send', { msg: req.data.msg, nick: req.data.nick} );
req.socket.last_message = new Date().getTime();
} else {
// Do not complain if message rate is too fast, that would only generate more traffic
}
} else {
// Message size is out of bounds, complain
req.socket.emit('message:server', {msg:'command_failed'} );
}
}
}
});
app.io.sockets.on('connection', function(socket) {
socket.on('disconnect', function() {
// Notify other users of the room
if( socket.current_room !== undefined ) {
socket.broadcast.to(socket.current_room).emit('message:server', {msg:'person_left'} );
}
});
});
app.listen(8080, function(){
console.log('listening on *:8080');
});

View file

@ -1,107 +0,0 @@
#!/usr/bin/env node
const
handler = require('serve-handler'),
port = process.env.PORT || 8080,
path = require('path');
let
server,
io;
// Create http server, handle files.assets
server = require('http').createServer(function (req, res) {
return handler(req, res, {
public: path.resolve(__dirname, '../client/public')
});
});
// Append socket.io to http server
io = require('socket.io')(server),
// Listen to port env:PORT or 8080
server.listen(port, function(){
console.log('listening on *:' + port); // eslint-disable-line no-console
});
io.on('connection', function(socket) {
socket.on('room:join', function(req) {
if( req ) {
socket.emit('room:joined',req);
socket.join(req);
socket.broadcast.to(req).emit('message:server', {msg:'person_joined'} );
socket.current_room = req;
} else {
socket.emit('message:server', {msg:'command_failed'} );
}
});
socket.on('room:leave', function(req) {
if( req ) {
socket.emit('room:left');
socket.leave(req);
socket.broadcast.to(req).emit('message:server', {msg:'person_left'} );
socket.current_room = undefined;
} else {
socket.emit('message:server', {msg:'command_failed'} );
}
});
socket.on('room:count', function () {
if( socket.current_room !== undefined ) {
let clientsInRoom = 0;
if( io.sockets.adapter.rooms.has(socket.current_room) ) {
clientsInRoom = io.sockets.adapter.rooms.get(socket.current_room).size;
}
if( clientsInRoom > 1) {
socket.emit('message:server', {msg:'person_count', payload: clientsInRoom } );
} else {
socket.emit('message:server', {msg:'person_single'} );
}
} else {
socket.emit('message:server', {msg:'command_failed'} );
}
});
socket.on('message:send', function(req) {
// Check that the user is in a room
if(req && req.room) {
// Check that the message size is within bounds
var total_msg_size = (req.msg) ? req.msg.length : 0 + (req.nick) ? req.nick.length : 0;
if( total_msg_size <= 4096) {
// Check that at least 100ms has passed since last message
if( socket.last_message === undefined || new Date().getTime() - socket.last_message > 100 ) {
socket.broadcast.to(req.room).emit('message:send', { msg: req.msg, nick: req.nick} );
socket.emit('message:send', { msg: req.msg, nick: req.nick} );
socket.last_message = new Date().getTime();
} else {
// Do not complain if message rate is too fast, that would only generate more traffic
}
} else {
// Message size is out of bounds, complain
socket.emit('message:server', {msg:'command_failed'} );
}
}
});
socket.on('disconnect', function() {
// Notify other users of the room
if( socket.current_room !== undefined ) {
socket.broadcast.to(socket.current_room).emit('message:server', {msg:'person_left'} );
}
});
});