some rearrangements

master
Marc Wäckerlin 8 years ago
parent 94b952fbcc
commit cc4f5d6619
  1. 990
      nodejs/public/javascripts/safechat.js
  2. 0
      nodejs/public/sounds/beep.mp3
  3. 27
      nodejs/views/index.ejs

@ -37,19 +37,24 @@
// 1 2 3 4 5 6 7 8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
function SafeChatClient(success, notice, error) {
function SafeChat() {
/// Cache Client's Key from local Strorage
var k = null;
function browserSupported() {
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange
return window.indexedDB && window.crypto.getRandomValues && Storage
/// Create UID from a name by appending an E-Mail
function uid(name) {
return name+' <'+name+'@'+hostname+'>'
}
/// Get User Key
/// @class Crypto cryptographic functions
/** @param view is of class SafeChat.View */
function Crypto(view) {
/// cache client's key from local strorage
var k = null
/// detect hosstname, default to safechat.ch
var hostname = window.location.hostname!='localhost'?window.location.hostname:'safechat.ch'
/// get user key
/** @internal key ist cached in k
@return key */
function key() {
@ -58,127 +63,203 @@ function SafeChatClient(success, notice, error) {
return k = openpgp.key.readArmored(localStorage.key)
}
/// Get Own User Name
/** Get user name as user id of first public key */
function uid() {
/// get own user name
/** get user name as user id of first public key */
function user() {
if (k || key()) return k.pub.keys[0].getUserIds()[0]
return null
}
/// Create New User
/// create New User
function createuser(user, email, pwd) {
notice("generating keys")
view.notice("generating keys")
openpgp.generateKey({
numBits: 4096,
userIds: [{name: user, email: email}],
passphrase: pwd
}).then(function(keyPair) {
success("keys generated")
view.success("keys generated")
localStorage.key = keyPair.privateKeyArmored
k = keyPair.key
}).catch(function(e) {
console.log(e)
error("generating key pairs failed")
view.fatal("generating key pairs failed")
})
}
/// open private key with password
/** @return @c true if password matches */
function password(pwd) {
return (k || keys()) && k.keys[0].decrypt(pwd)
}
/// Encrypt Message
function encrypt(targetkeys, message, done, failed) {
function encrypt(message, targetkeys, done, failed) {
if (!k) return false
openpgp.encrypt({
publicKeys: targets.keys.concat(k.keys),
publicKeys: targetkeys.keys.concat(k.keys),
privateKeys: k,
data: message,
armor: false})
.then(done)
.else(failed)
armor: false
}).then(done).else(failed)
return true
}
/// Decrypt Message
function decrypt(message) {
function decrypt(message, sourcekeys, done, failed) {
if (!k) return false
openpgp.decrypt({
privateKeys: k.keys,
publicKeys: sourcekeys.keys,
message: message
}).then(done).else(failed)
return true
}
}
/// database that stores in indexed db
function DataBase() {
function user(name, key) {
}
}
/// manage local copy of users
function Users() {
var users = new Map()
function add(usr) {
if (!users[usr.name])
users[usr.name].valid = true
else
users[usr.name].valid = users[usr.name].valid && users[usr.name].key == usr.key
users[usr.name].key = usr.key
users[usr.name].online = usr.online
}
}
/// manage local copy of messages
function Messages() {
var messages = {};
function add() {
}
var password = null; ///< password, only stored temporary, until reload
var username = null; ///< username, only used during registration
var filecontent = new Array(); ///< temporary storage for attachments
var socket = io.connect();
var hostname = window.location.hostname!='localhost'?window.location.hostname:'safechat.ch';
}
/// @class Communication client socket communication
/** @param view is of class SafeChat.View */
function Communication(controller) {
var socket = io.connect()
function broadcast(signal, data) {
console.log("<=snd "+signal)
socket.broadcast.emit(signal, data)
}
function emit(signal, data) {
console.log("<-snd "+signal)
socket.emit(signal, data)
}
socket
.on("login", controller.loggedin)
.on("fail", controller.fail)
.on("user", controller.user)
.on("users", controller.users)
.on("message", controller.message)
.on("messages", controller.messages)
.io
.on("connect", controller.connected)
.on("reconnect", controller.connected)
.on("disconnect", controller.disconnected)
.on("error", controller.disconnected)
}
/// @class View provides the glue to the GUI in the index.ejs file
/** View provides the following callbacks:
- status updates:
- @c notice(msg) to display information
- @c warning(msg)
- @c error(msg)
- @c fatal(msg) */
function View() {
var nexttimer = null
/// Padding for numbers in dates
function pad(n) {
return n<10 ? '0'+n : n
}
function uid(name) {
return name+' <'+name+'@'+hostname+'>';
/// escape text to show in html @see htmldec
function htmlenc(html) {
return $('<div/>').text(html).html()
}
/// Convert number of bytes to readable text
function size(num) {
if (num>0.6*1024) {
if (num>0.6*1024*1024) {
if (num>0.6*1024*1024*1024) {
if (num>0.6*1024*1024*1024*1024) {
return Math.round(num/1024/1024/1024/1024)+"TB";
} else {
return Math.round(num/1024/1024/1024)+"GB";
}
} else {
return Math.round(num/1024/1024)+"MB";
/// decode html encoded text @see htmlenc
function htmldec(data) {
return $('<div/>').html(data).text()
}
} else {
return Math.round(num/1024)+"kB";
/// alert user accoustically or by vibration
/** alert user, e.g. that a new message has arrived. */
function beep() {
if (navigator.vibrate) navigator.vibrate(1000)
(new Audio("sounds/beep.mp3")).play()
}
} else {
return num+"B";
/// show fatal error
/** something completely failed, abort
@param msg */
function fatal(msg) {
if (nexttimer) clearTimeout(nexttimer)
if (msg) {
error(msg)
$('#fatal-msg').html(msg)
}
show('fatal')
}
var reboottimer = null;
/// Show error messsage
/** Fades in an error message and logs to console.
/// show error messsage
/** shows an error message and logs to console.
@param data (optional) The error can be a string or any structure.
Strings are shown to the user, structures are logged only.
@param stay (optional) If not given as @c true, reloads page after 5s. */
function error(data, stay) {
$("#status").hide();
@param next (optional) next function to call */
function error(data, next) {
if (nexttimer) clearTimeout(nexttimer)
$("#status").hide()
$("#status").addClass("error")
$("#status").removeClass("notice")
$("#status").removeClass("success")
if (data) {
if (typeof data == 'string') {
$("#status").html(data);
console.log("error: "+data);
$("#status").html(data)
console.log("error: "+data)
} else {
$("#status").html('unknown error: '+JSON.stringify(data));
console.log("error: "+JSON.stringify(data));
$("#status").html('error')
console.log("error: "+JSON.stringify(data))
}
} else {
$("#status").html('error');
console.log("error");
}
$("#status").show();
if (!stay) {
console.log("reboot in 5s");
console.log((new Error('stacktrace')).stack);
if (!reboottimer) reboottimer = setTimeout(function() {
reboottimer = null;
start();
}, 5000);
$("#status").html('error')
console.log("error")
}
$("#status").show()
if (next) nexttimer = setTimeout(function() {
nexttimer = null
next()
}, 5000)
}
/// Show notice messsage
/** Fades in an notice message and logs to console.
/// show notice messsage
/** shows an notice message and logs to console.
@param text (optional) The data is a string. */
function notice(text) {
$("#status").hide()
@ -186,137 +267,228 @@ function notice(text) {
$("#status").removeClass("error")
$("#status").removeClass("success")
if (text) {
$("#status").html(text);
console.log("notice: "+text);
$("#status").html(text)
console.log("notice: "+text)
} else {
$("#status").html('');
console.log("notice");
$("#status").html('')
console.log("notice")
}
$("#status").show();
$("#status").show()
}
/// Show notice messsage
/** Fades in an success message and logs to console.
/// show success messsage
/** shows an success message and logs to console.
@param text (optional) The data is a string. */
function success(text) {
$("#status").hide();
$("#status").hide()
$("#status").addClass("success")
$("#status").removeClass("error")
$("#status").removeClass("notice")
if (text) {
$("#status").html(text);
console.log("success: "+text);
$("#status").html(text)
console.log("success: "+text)
} else {
$("#status").html('');
console.log("success");
$("#status").html('')
console.log("success")
}
$("#status").show();
$("#status").show()
}
/// Show status message in the main screen area
/** @param id HTML id to be shown.
@param msg The success message text */
function status(id, msg) {
console.log("state: "+id);
if (msg) success(msg); else $("#status").hide();
$("#main").children(":not(#"+id+")").hide();
$("#main #"+id).show();
$("#main #"+id+" form input:first-child").focus();
}
function emit(signal, data) {
console.log("<-snd "+signal);
socket.emit(signal, data);
/// show a specific screen given the element id
/** @param id html id to be shown.
@param msg (optional) the success message text */
function show(id, msg) {
console.log("state: "+id)
if (msg) success(msg) else $("#status").hide()
$("#main").children(":not(#"+id+")").hide()
$("#main #"+id).show()
$("#main #"+id+" form input:first-child").focus()
}
/// show server connected status
function connected() {
console.log("server connected");
$("#connectionstatus #bad").hide();
$("#connectionstatus #good").show();
success("server connected");
console.log("server connected")
$("#connectionstatus #bad").hide()
$("#connectionstatus #good").show()
success("server connected")
}
/// show server disconnected status
function disconnected() {
console.log("server disconnected");
$("#connectionstatus #good").hide();
$("#connectionstatus #bad").show();
error("server disconnected", true);
console.log("server disconnected")
$("#connectionstatus #good").hide()
$("#connectionstatus #bad").show()
error("server disconnected", true)
}
function connectionstatus() {
if (socket.connected) connected(); else disconnected();
/// toggle menu display
function togglemenu() {
$("#menu").toggle()
}
function htmlenc(html) {
return $('<div/>').text(html).html();
function checkFeature(id, query) {
if (query) $('#'+id+':before')
.css('color', 'green')
.css('content', '&#x2714;')
else $('#'+id+':before')
.css('color', 'red')
.css('content', '&#x2718;')
if (query) $('#'+id)
.css('color', 'green')
.css('text-decoration', 'line-through')
else $('#'+id)
.css('color', 'red')
.css('text-decoration', 'none')
}
function htmldec(data) {
return $('<div/>').html(data).text();
function checkFeatures() {
$('ul.features').css('list-style-type', 'none')
checkFeature("localstorage", Storage)
checkFeature("indexeddb", window.indexedDB)
checkFeature("randomvalues", window.crypto.getRandomValues)
checkFeature("vibrate", navigator.vibrate)
checkFeature("filereader", window.FileReader)
}
/// Alert user
/** Alert user, e.g. that a new message has arrived. */
function beep(user) {
if (user) success("message from "+htmlenc(user)+" received");
navigator.vibrate =
navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate;
if (navigator.vibrate) {
// vibration API supported
navigator.vibrate(1000);
}
(new Audio("sounds/A-Tone-His_Self-1266414414.mp3")).play();
}
function DataTransfer() {
/// Toggle Menu Display
function togglemenu() {
$("#menu").toggle();
}
var reboottimer = null
var data = new DataTransfer()
/// Download Profile Backup
/// download profile backup
function backup() {
var download = document.createElement('a');
download.href = 'data:attachment/text,'+encodeURI(JSON.stringify(localStorage));
download.target = '_blank';
var now = new Date();
var download = document.createElement('a')
download.href = 'data:attachment/text,'+encodeURI(JSON.stringify(localStorage))
download.target = '_blank'
var now = new Date()
download.download =
pad(now.getFullYear())+pad(now.getMonth()+1)+pad(now.getDate())+
"-"+userid()+"@"+hostname+".bak";
"-"+userid()+"@"+hostname+".bak"
var clickEvent = new MouseEvent("click", {
"view": window,
"bubbles": true,
"cancelable": false
});
download.dispatchEvent(clickEvent);
togglemenu();
})
download.dispatchEvent(clickEvent)
togglemenu()
}
/// Upload Profile Backup
function restore(evt) {
if (!window.FileReader)
return error("your browser does not support file upload", true);
for (var i=0, f; f=evt.target.files[i]; ++i) {
var file = f;
var reader = new FileReader();
var file = f
var reader = new FileReader()
reader.onload = function(evt) {
if (evt.target.error) return error("error reading file", true);
if (evt.target.readyState==0) return notice("waiting for data …");
if (evt.target.readyState==1) return notice("loading data …");
var parsed=JSON.parse(evt.target.result);
togglemenu();
localStorage.pubkey = parsed.pubkey;
localStorage.privkey = parsed.privkey;
success("backup is restored");
console.log("reboot after restore in 2s");
if (!reboottimer) reboottimer = setTimeout(function() {
reboottimer = null;
start();
}, 2000);
if (evt.target.error) return error("error reading file")
if (evt.target.readyState==0) return notice("waiting for data …")
if (evt.target.readyState==1) return notice("loading data …")
var parsed=JSON.parse(evt.target.result)
togglemenu()
localStorage.pubkey = parsed.pubkey
localStorage.privkey = parsed.privkey
success("backup is restored")
console.log("reboot after restore in 2s")
if (!reboottimer && reboot) reboottimer = setTimeout(function() {
reboottimer = null
}, 2000)
}
reader.readAsText(file);
reader.readAsText(file)
}
}
if (!window.FileReader) {
$('restore-menu-item').hide()
error("your browser does not support file upload")
}
}
}
/// @class Controller defines the programm flow
function Controller(view) {
var db = new Database()
var communication = new Communication(this)
var users = new Users()
function fail(msg) {
console.log('rcv-> fail('+msg+')')
error(msg)
}
function loggedin() {
console.log("rcv-> login")
success("login successful")
chat()
}
function user(usr) {
console.log("rcv-> user")
if (usr.exits) users.add(usr)
}
function users() {
console.log("rcv-> users")
}
function message(msg) {
console.log("rcv-> message")
}
function messages(msgs) {
console.log("rcv-> messages")
}
this.connected = view.connected
this.reconnect = view.connected
this.disconnect = view.disconnected
this.error = view.disconnected
function login() {
}
function user(usr) {
if (usr.exists) db.adduser
}
function initBrowser() {
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange
navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate
return window.indexedDB && window.crypto.getRandomValues && Storage
}
function run() {
}
function start() {
view.reboot = run
var compatible = initBrowser()
view.checkFeatures()
if (!compatible)
view.fatal("your browser is not supported")
else
run()
}
}
//==============================================================================
//------------------------------------------------------------------------------
return new Controller(new View())
}
var filecontent = new Array() ///< temporary storage for attachments
var reboottimer = null
function connectionstatus() {
if (socket.connected) connected() else disconnected()
}
/// Configure local groups
/** … */
function groups() {
@ -335,19 +507,19 @@ function groups() {
@param pwd2 The repeated password. */
function checkpwd(pwd, pwd2) {
$("#register").submit(function(event) {
return false;
});
if (pwd==pwd2) password=pwd;
else password=null;
if (!password||password.length<1) password=null;
$("#createuser").prop("disabled", !(username && password));
return false
})
if (pwd==pwd2) password=pwd
else password=null
if (!password||password.length<1) password=null
$("#createuser").prop("disabled", !(username && password))
if (password) {
if (username) success("user is ready to be created");
else notice("password matches, please chose a valid user name");
if (username) success("user is ready to be created")
else notice("password matches, please chose a valid user name")
} else {
if (username) notice("passwords don't match");
else if ($('#user').val()) notice("user name is already in use");
else notice("please chose a user name");
if (username) notice("passwords don't match")
else if ($('#user').val()) notice("user name is already in use")
else notice("please chose a user name")
}
}
@ -357,28 +529,28 @@ function checkpwd(pwd, pwd2) {
button if the receiver of the message exists on the server. */
function checkpartner(user) {
$("#chat").submit(function(event) {
return false;
});
emit("user", uid(user));
return false
})
emit("user", uid(user))
}
/// Create Local Public-/Private-Key Pair
/** Called if user has not yet his keys, just generates a new key pair. */
function createkeypair(user, pwd) {
notice("generating keys");
notice("generating keys")
openpgp.generateKey({
numBits: 4096,
userIds: [{name: user, email: user+'@'+hostname}],
passphrase: pwd
}).then(function(keyPair) {
success("keys generated");
localStorage.pubkey = keyPair.publicKeyArmored;
localStorage.privkey = keyPair.privateKeyArmored;
login();
success("keys generated")
localStorage.pubkey = keyPair.publicKeyArmored
localStorage.privkey = keyPair.privateKeyArmored
login()
}).catch(function(e) {
console.log(e)
error("generating key pairs failed")
});
})
}
/// Get Own Public Key
@ -386,13 +558,13 @@ function createkeypair(user, pwd) {
function publicKey() {
if (typeof localStorage.pubkey == 'undefined') {
if (typeof localStorage.pubKey == 'undefined') {
return null;
return null
} else {
localStorage.pubkey = localStorage.pubKey;
localStorage.removeItem(pubKey);
localStorage.pubkey = localStorage.pubKey
localStorage.removeItem(pubKey)
}
}
return openpgp.key.readArmored(localStorage.pubkey);
return openpgp.key.readArmored(localStorage.pubkey)
}
/// Get Own Private Key
@ -400,13 +572,13 @@ function publicKey() {
function privateKey() {
if (typeof localStorage.privkey == 'undefined') {
if (typeof localStorage.privKey == 'undefined') {
return null;
return null
} else {
localStorage.privkey = localStorage.privKey;
localStorage.removeItem(privKey);
localStorage.privkey = localStorage.privKey
localStorage.removeItem(privKey)
}
}
return openpgp.key.readArmored(localStorage.privkey);
return openpgp.key.readArmored(localStorage.privkey)
}
/// Get Own User Name
@ -415,147 +587,147 @@ function userid() {
if (!publicKey() ||
publicKey().keys.length < 1 ||
publicKey().keys[0].getUserIds().length < 1) return null
return publicKey().keys[0].getUserIds()[0];
return publicKey().keys[0].getUserIds()[0]
}
/// Clear Message Text And Attachments
/** Does not remove the receiver's name */
function clearmessage() {
$("#message").prop(":disabled", true);
filecontent = new Array();
$('#preview').empty();
$("#msg").val("");
$("#message").prop(":disabled", false);
$("#message").prop(":disabled", true)
filecontent = new Array()
$('#preview').empty()
$("#msg").val("")
$("#message").prop(":disabled", false)
}
function guessfilename(mimetype, user, date) {
if (!user) user = userid();
if (!date) date = new Date();
var ext = mimetype.replace(/.*\/(x-)?/i, "");
if (!user) user = userid()
if (!date) date = new Date()
var ext = mimetype.replace(/.*\/(x-)?/i, "")
return pad(date.getFullYear())+pad(date.getMonth()+1)+pad(date.getDate())
+"-"+ext+"-"+user+"@"+hostname+'.'+ext;
+"-"+ext+"-"+user+"@"+hostname+'.'+ext
}
/// Display Image Attachments
function attachments(files, id, from, date) {
if (files) files.forEach(function(file) {
console.log(file);
if (!file.name) file.name = guessfilename(file.type, from, date);
var a = document.createElement('a');
a.href = file.content;
a.download = file.name;
a.target = '_blank';
console.log(file)
if (!file.name) file.name = guessfilename(file.type, from, date)
var a = document.createElement('a')
a.href = file.content
a.download = file.name
a.target = '_blank'
if (file.type.match('^image/')) {
var img = document.createElement('img');
img.title = file.name;
img.src = file.content;
a.appendChild(img);
var img = document.createElement('img')
img.title = file.name
img.src = file.content
a.appendChild(img)
} else if (file.type.match('^video/')) {
var video = document.createElement('video');
video.controls = true;
video.title = file.name;
video.src = file.content;
a.appendChild(video);
var video = document.createElement('video')
video.controls = true
video.title = file.name
video.src = file.content
a.appendChild(video)
} else {
var img = document.createElement('img');
img.title = file.name;
img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg";
a.appendChild(img);
var img = document.createElement('img')
img.title = file.name
img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"
a.appendChild(img)
}
$(id).append(a);
});
$(id).append(a)
})
}
var recorder;
var recorder
function done() {
if (recorder) {
recorder.stop();
recorder.stop()
recorder.recording(function(data) {
previewfile(data, "video/webm");
abort();
});
previewfile(data, "video/webm")
abort()
})
}
}
function abort() {
if (recorder) {
$("#videorecorder").hide();
recorder.release();
delete recorder; recorder = null;
$("#videorecorder").hide()
recorder.release()
delete recorder recorder = null
}
}
/// Record Video from builtin camera
function recordvideo() {
try {
abort();
$("#videorecorder").show();
abort()
$("#videorecorder").show()
recorder = new MediaStreamRecorder({
video: {
width: {ideal: 180},
height: {ideal: 160}
},
audio: true
});
})
recorder.on("ready", function() {
$("#videorecorder video").attr("src", recorder.preview());
$("#videorecorder video").css("width", 180);
$("#videorecorder video").css("height", 160);
$("#videorecorder video").attr("width", 180);
$("#videorecorder video").attr("height", 160);
recorder.start();
});
$("#videorecorder video").attr("src", recorder.preview())
$("#videorecorder video").css("width", 180)
$("#videorecorder video").css("height", 160)
$("#videorecorder video").attr("width", 180)
$("#videorecorder video").attr("height", 160)
recorder.start()
})
} catch (e) {
console.log(e);
error("cannot access camera", true);
console.log(e)
error("cannot access camera", true)
}
}
function previewfile(content, type, name) {
if (!name) name = guessfilename(type);
if (!name) name = guessfilename(type)
if (type.match('^image/')) {
var img = document.createElement("img");
var img = document.createElement("img")
img.onload = function() { // resize image to maximum 400px
var MAX = 400;
var width = img.width;
var height = img.height;
var MAX = 400
var width = img.width
var height = img.height
if (width > MAX) {
height *= MAX / width;
width = MAX;
height *= MAX / width
width = MAX
}
if (height > MAX) {
width *= MAX / height;
height = MAX;
}
var canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
width *= MAX / height
height = MAX
}
var canvas = document.createElement("canvas")
canvas.width = width
canvas.height = height
var ctx = canvas.getContext("2d")
ctx.drawImage(img, 0, 0, width, height)
img.onload = function() {
filecontent.push({name: name, type: type, content: img.src});
$("#preview").append(img);
success('image is ready to be sent');
filecontent.push({name: name, type: type, content: img.src})
$("#preview").append(img)
success('image is ready to be sent')
}
img.src = canvas.toDataURL(file.type);
img.title = name+"\n"+size(img.src.length);
img.src = canvas.toDataURL(file.type)
img.title = name+"\n"+size(img.src.length)
}
img.src=content;
img.src=content
} else if (type.match('^video/')) {
filecontent.push({name: name, type: type, content: content});
var video = document.createElement("video");
video.setAttribute("controls", "controls");
video.setAttribute("loop", "loop");
video.setAttribute("src", content);
video.setAttribute("title", name+"\n"+size(content.length));
$("#preview").append(video);
filecontent.push({name: name, type: type, content: content})
var video = document.createElement("video")
video.setAttribute("controls", "controls")
video.setAttribute("loop", "loop")
video.setAttribute("src", content)
video.setAttribute("title", name+"\n"+size(content.length))
$("#preview").append(video)
} else {
filecontent.push({name: name, type: type, content: content});
var img = document.createElement("img");
img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg";
img.title = name+"\n"+size(content.length);
$("#preview").append(img);
filecontent.push({name: name, type: type, content: content})
var img = document.createElement("img")
img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"
img.title = name+"\n"+size(content.length)
$("#preview").append(img)
}
}
@ -568,17 +740,17 @@ function previewfile(content, type, name) {
Stores data in global variable @ref filecontent. */
function fileupload(evt) {
if (!window.FileReader)
return error("your browser does not support file upload", true);
return error("your browser does not support file upload", true)
for (var i=0, f; f=evt.target.files[i]; ++i) {
var file = f;
var reader = new FileReader();
var file = f
var reader = new FileReader()
reader.onload = function(evt) {
if (evt.target.error) return error("error reading file", true);
if (evt.target.readyState==0) return notice("waiting for data …");
if (evt.target.readyState==1) return notice("loading data …");
previewfile(evt.target.result, file.type, file.name);
if (evt.target.error) return error("error reading file", true)
if (evt.target.readyState==0) return notice("waiting for data …")
if (evt.target.readyState==1) return notice("loading data …")
previewfile(evt.target.result, file.type, file.name)
}
reader.readAsDataURL(file);
reader.readAsDataURL(file)
}
}
@ -588,112 +760,106 @@ function fileupload(evt) {
@param name The receiver's name. */
function setreceiver(name) {
$("#recv").val(name);
checkpartner(name);
$("#msg").focus();
$("#recv").val(name)
checkpartner(name)
$("#msg").focus()
}
var userMap = null;
var userMap = null
function users(userlist) {
console.log("rcv-> users");
userMap = new Array();
$("#allusers").empty();
console.log("rcv-> users")
userMap = new Array()
$("#allusers").empty()
userlist.forEach(function(usr) {
userMap[usr.name] = usr.pubkey;
userMap[usr.name] = usr.pubkey
$("#allusers").append('<option value="'+htmlenc(usr.name)+'">')
$("#allusers").hide();
console.log(" user: "+usr.name);
});
localStorage.userMap = JSON.stringify(userMap);
}
function fail(msg) {
console.log("rcv-> fail");
error(msg);
}
function loggedin() {
console.log("rcv-> login");
success("login successful");
chat();
$("#allusers").hide()
console.log(" user: "+usr.name)
})
localStorage.userMap = JSON.stringify(userMap)
}
function user(usr) {
if (usr.exists) console.log("rcv-> user("+usr.name+")");
else console.log("rcv-> user("+usr.name+"): name is available");
if (usr.exists) console.log("rcv-> user("+usr.name+")")
else console.log("rcv-> user("+usr.name+"): name is available")
if ($("#newuser").is(":visible") && usr.name==uid($('#user').val())) {
// same username as in the create user form
$("#createuser").prop("disabled", usr.exists); // todo: check password
$("#createuser").prop("disabled", usr.exists) // todo: check password
if (!usr.exists) {
username = usr.name;
success("user name "+usr.name+" is available");
username = usr.name
success("user name "+usr.name+" is available")
} else {
username = null;
error("user name "+usr.name+" is in use", true);
username = null
error("user name "+usr.name+" is in use", true)
}
}
if ($("#chat").is(":visible") && usr.name==uid($("#recv").val())) { // same username as in receiver
$('#send').prop("disabled", !usr.exists);
$("label[for=send] img").css("opacity", usr.exists?"1.0":"0.4");
$("label[for=send] img").css("filter", usr.exists?"alpha(opacity=100)":"alpha(opacity=40)");
if (usr.exists) success("recipient exists");
else error("unknown recipient", true);
$('#send').prop("disabled", !usr.exists)
$("label[for=send] img").css("opacity", usr.exists?"1.0":"0.4")
$("label[for=send] img").css("filter", usr.exists?"alpha(opacity=100)":"alpha(opacity=40)")
if (usr.exists) success("recipient exists")
else error("unknown recipient", true)
}
if (userMap == null) {
if (localStorage.userMap) {
userMap = JSON.parse(localStorage.userMap);
userMap = JSON.parse(localStorage.userMap)
} else {
userMap = new Array();
userMap = new Array()
}
}
if (usr.exists && usr.pubkey && userMap[usr.name] != usr.pubkey) {
userMap[usr.name] = usr.pubkey;
userMap[usr.name] = usr.pubkey
$("#allusers").append('option value="'+htmlenc(usr.name)+'"')
localStorage.userMap = JSON.stringify(userMap);
localStorage.userMap = JSON.stringify(userMap)
}
}
function queryuser(usr) {
console.log("query user: "+uid(usr));
socket.emit("user", uid(usr));
console.log("query user: "+uid(usr))
socket.emit("user", uid(usr))
}
/// Get a user's public key.
/** The first time, gets it from the server, later from the cache. */
function getPublicKey(user) {
var deferredObject = $.Deferred();
if (userMap && userMap[user]) deferredObject.resolve(userMap[user]);
else deferredObject.reject("unknown user");
return deferredObject.promise();
var deferredObject = $.Deferred()
if (userMap && userMap[user]) deferredObject.resolve(userMap[user])
else deferredObject.reject("unknown user")
return deferredObject.promise()
}
/// Received a list of messages from server
function messages(msgs) {
console.log("rcv-> messages("+msgs.length+")");
console.log("rcv-> messages("+msgs.length+")")
if (!password || !privateKey())
return setTimeout(function() {emit("messages");}, 1000); // try again later
status("allmessages");
notice("load messages, please wait …");
msgs.forEach(function(msg) {message(msg, true);});
status("chat");
return setTimeout(function() {emit("messages")}, 1000) // try again later
status("allmessages")
notice("load messages, please wait …")
msgs.forEach(function(msg) {message(msg, true)})
status("chat")
}
/// Received a message from server
function message(m, internal) {
if (!internal) console.log("rcv-> message("+m.user+")");
if (!password || !privateKey()) return;
var key=openpgp.key.readArmored(m.pubkey);
if (key.err) return error("key of sender unreadable", true);
var message = openpgp.message.readArmored(m.msg);
var privkey = privateKey().keys[0];
if (!internal) console.log("rcv-> message("+m.user+")")
if (!password || !privateKey()) return
var key=openpgp.key.readArmored(m.pubkey)
if (key.err) return error("key of sender unreadable", true)
var message = openpgp.message.readArmored(m.msg)
var privkey = privateKey().keys[0]
if (privkey.decrypt(password)) // prepare own key
openpgp.decrypt({
privateKeys: privkey,
publicKeys: key.keys,
message: message
}).then(function(msg) { // decryption succeded
openpgp.decrypt({
privateKeys: privkey,
publicKeys: key.keys,
message: message
}).then(function(msg) { // decryption succeded
// prepend message to list of messages
var message = JSON.parse(msg.text);
var message = JSON.parse(msg.text)
$("#msgs") // todo: check msg.signatures[0].valid
.prepend('<div id="id'+(m.id)+'" class="msg '+
(m.user==userid()?"me":"other")+
@ -710,54 +876,54 @@ function message(m, internal) {
'</span></div>'+
'<div class="text">'+
htmlenc(message.text)+
'</div></div><div class="clear"/>');
'</div></div><div class="clear"/>')
// show attachments
attachments(message.files, '#id'+m.id+' .text', m.user, new Date(m.time));
attachments(message.files, '#id'+m.id+' .text', m.user, new Date(m.time))
// calculate and show emoticons
$('#id'+m.id).emoticonize();
if (!internal) beep(m.user);
$('#id'+m.id).emoticonize()
if (!internal) beep(m.user)
}).catch(function(e) {
// not for me
success();
});
success()
})
}
/// Send Message To Server
/** User wants to send a message. Encrypt message with own private and
the receiver's public key, then send it to the server. */
function sendmessage(recv, txt) {
notice("1/3 preparing message …");
$("#message").prop(":disabled", true);
notice("1/3 preparing message …")
$("#message").prop(":disabled", true)
getPublicKey(recv) // get receiver's public key
.done(function(pk) {
var key=openpgp.key.readArmored(pk);
var key=openpgp.key.readArmored(pk)
if (!pk||key.err) {
$("#message").prop(":disabled", false);
error("receiver's key not found", true);
return;
}
var privkey = privateKey().keys[0];
privkey.decrypt(password); // get own private key ready
var message = JSON.stringify({receiver: recv, text: txt, files: filecontent});
notice("2/3 encrypting message …");
$("#message").prop(":disabled", false)
error("receiver's key not found", true)
return
}
var privkey = privateKey().keys[0]
privkey.decrypt(password) // get own private key ready
var message = JSON.stringify({receiver: recv, text: txt, files: filecontent})
notice("2/3 encrypting message …")
openpgp.encrypt({publicKeys: key.keys.concat(publicKey().keys),
privateKeys: privkey,
data: message,
armor: false})
.then(function(msg) { // message is encrypted
notice("3/3 sending message …");
emit("message", {user: userid(), content: msg});
clearmessage();
notice("3/3 sending message …")
emit("message", {user: userid(), content: msg})
clearmessage()
})
.catch(function(e) {
$("#message").prop(":disabled", false);
error("encryption of message failed", true);
});
$("#message").prop(":disabled", false)
error("encryption of message failed", true)
})
})
.fail(function(e) {
$("#message").prop(":disabled", false);
error("user not found", true);
});
$("#message").prop(":disabled", false)
error("user not found", true)
})
}
/// Check And Set Password
@ -768,12 +934,12 @@ function sendmessage(recv, txt) {
@param pwd The password to check. */
function setpw(pwd) {
if (privateKey().keys[0].decrypt(pwd)) {
success("password matches");
$("#removeKey").hide();
password = pwd;
chat();
success("password matches")
$("#removeKey").hide()
password = pwd
chat()
} else {
notice("password does not match");
notice("password does not match")
}
}
@ -783,35 +949,35 @@ function setpw(pwd) {
setpw() continues automatically. No submit is required by the
user. */
function getpwd() {
if (password) return;
$("#removeKey").show();
status("getpwd");
if (password) return
$("#removeKey").show()
status("getpwd")
}
function deleteUser() {
var uid = userid();
localStorage.removeItem(pubkey);
localStorage.removeItem(privkey);
error("user "+uid+" permanentely lost");
var uid = userid()
localStorage.removeItem(pubkey)
localStorage.removeItem(privkey)
error("user "+uid+" permanentely lost")
}
function removeKey() {
togglemenu();
$("#removeKey").hide();
status('forgotpassword');
togglemenu()
$("#removeKey").hide()
status('forgotpassword')
}
/// Main Chat Window
/** Gets chat widgets from server and displays them. Starts timer for
get() which polls for new messages. */
var firsttime = true;
var firsttime = true
function chat() {
if (!password) return getpwd();
status("chat");
if (!password) return getpwd()
status("chat")
if (firsttime && $('#msgs').is(':empty')) {
firsttime = false;
notice("getting previous messages, please wait …");
emit("messages");
firsttime = false
notice("getting previous messages, please wait …")
emit("messages")
}
}
@ -827,80 +993,70 @@ function chat() {
different, then this is a complete failure, something went
terribly wrong. */
function login() {
$("#username").html(userid()+"@"+hostname);
$("#username").html(userid()+"@"+hostname)
emit("login", {name: userid(),
pubkey: localStorage.pubkey});
success("login sent to server");
pubkey: localStorage.pubkey})
success("login sent to server")
}
/// Get And Display Form To Create New User
/** Shows user creation form. On submit, a private key is generated in
createkeypair(), then login() creates the user. */
function newuser() {
status("newuser");
status("newuser")
}
/// Check if local storage is available
function checkLocalStorage() {
var test = 'test';
var test = 'test'
try {
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
localStorage.setItem(test, test)
localStorage.removeItem(test)
return true
} catch(e) {
status("nolocalstorage");
error("local storage not available");
status("nolocalstorage")
error("local storage not available")
}
return false;
return false
}
/// Initial Function: Startup
/** Decide whether to login or to create a new user */
function start() {
$("#menu").hide();
//status("startup");
$("#menu").hide()
//status("startup")
if (checkLocalStorage())
try {
if (!userid()) {
newuser();
newuser()
} else {
login();
login()
}
} catch (m) {
console.log(m.stack);
error(m);
console.log(m.stack)
error(m)
}
}
function init() {
/// On Load, Call @ref start
$(window.onbeforeunload = function() {
return "Are you sure you want to navigate away?";
});
return "Are you sure you want to navigate away?"
})
/// Allow Running in Background on Android
document.addEventListener('deviceready', function () {
if (cordova && cordova.plugins.backgroundMode) {
cordova.plugins.backgroundMode.enable();
}
}, false);
socket.io.on("connect", connected);
socket.io.on("reconnect", connected);
socket.io.on("disconnect", disconnected);
socket.io.on("error", disconnected);
socket.on("login", loggedin);
socket.on("fail", fail);
socket.on("user", user);
socket.on("users", users);
socket.on("message", message);
socket.on("messages", messages);
connectionstatus();
cordova.plugins.backgroundMode.enable()
}
}, false)
connectionstatus()
if (openpgp.initWorker("openpgp.worker.min.js"))
console.log("asynchronous openpgp enabled");
console.log("asynchronous openpgp enabled")
else
console.log("asynchronous openpgp failed");
emit('users');
start();
console.log("asynchronous openpgp failed")
emit('users')
start()
}
/// Start Main Loop
$(init);
$(init)

@ -30,7 +30,7 @@
<ul id="menu" style="display: none">
<li onclick="backup()">Download Backup</li>
<li class="toolbutton"><label for="restore">Restore Backup</label><input autocomplete="off" type="file" accept="*.bak" id="restore" /></li>
<li id="restore-menu-item" class="toolbutton"><label for="restore">Restore Backup</label><input autocomplete="off" type="file" accept="*.bak" id="restore" /></li>
<li id="groups" onclick="groups()">Edit Groups</li>
<li id="removeKey" style="display: none" onclick="removeKey()">Password Forgotten</li>
<li id="android-download" href="safechat.apk"><a href="safechat.apk">Download Android-App</a></li>
@ -182,6 +182,25 @@
</div>
</div>
<!-- Fatal: Abort -->
<div id="fatal">
<h2 id="fatal-msg">Failure</h2>
<p>The SafeChat has been aborted due to a fatal error.</p>
<p>There is a problem in your browser. Please try to reload. If the problem persists, please update your web browser or try SafeChat in another browser.</p>
<p>The following java script features are required:</p>
<ul class="features">
<li id="localstorage">Local Storage</li>
<li id="indexeddb">Indexed DB</li>
<li id="randomvalues">Cryptography: Random Values</li>
</ul>
<p>The following java script features are optional:</p>
<ul class="features">
<li id="vibrate">Vibration (vibrates when new message arrives)</li>
<li id="filereader">File Reader (required to restore backup)</li>
</ul>
</div>
<!-- Error: Missing JavaScript -->
<noscript>
<h2>JavasScript Required!</h2>
@ -193,12 +212,6 @@
<p><a href="<%= package.documentation %>" target="_blank">more information</a></p>
</noscript>
<!-- Error: Missing LocalStorage -->
<div id="nolocalstorage" style="display: none">
<p>No access to local storage. Please allow access to local
storage, i.e. do not block cookies.<p>
</div>
<!-- Notice: Setup Messages -->
<div id="allmessages" style="display: none">
<p>Setting up all previous messages, please wait …</p>

Loading…
Cancel
Save