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

@ -30,7 +30,7 @@
<ul id="menu" style="display: none"> <ul id="menu" style="display: none">
<li onclick="backup()">Download Backup</li> <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="groups" onclick="groups()">Edit Groups</li>
<li id="removeKey" style="display: none" onclick="removeKey()">Password Forgotten</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> <li id="android-download" href="safechat.apk"><a href="safechat.apk">Download Android-App</a></li>
@ -182,6 +182,25 @@
</div> </div>
</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 --> <!-- Error: Missing JavaScript -->
<noscript> <noscript>
<h2>JavasScript Required!</h2> <h2>JavasScript Required!</h2>
@ -193,12 +212,6 @@
<p><a href="<%= package.documentation %>" target="_blank">more information</a></p> <p><a href="<%= package.documentation %>" target="_blank">more information</a></p>
</noscript> </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 --> <!-- Notice: Setup Messages -->
<div id="allmessages" style="display: none"> <div id="allmessages" style="display: none">
<p>Setting up all previous messages, please wait …</p> <p>Setting up all previous messages, please wait …</p>

Loading…
Cancel
Save