diff --git a/nodejs/public/javascripts/safechat.js b/nodejs/public/javascripts/safechat.js index f1bde7a..db17ee3 100644 --- a/nodejs/public/javascripts/safechat.js +++ b/nodejs/public/javascripts/safechat.js @@ -41,9 +41,15 @@ function SafeChat() { /// Create UID from a name by appending an E-Mail function uid(name) { - return name+' <'+name+'@'+hostname+'>' + return name+' <'+mail(name)+'>' } + function mail(name) { + var hostname = window.location.hostname!='localhost'?window.location.hostname:'safechat.ch' + return name+'@'+hostname + } + + //============================================================================== /// @class Crypto cryptographic functions /** @param view is of class SafeChat.View */ function Crypto(controller) { @@ -51,42 +57,23 @@ function SafeChat() { /// 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' - + /// detect hostname, default to safechat.ch /// get user key /** @internal key ist cached in k @return key */ - function key() { - if (k) return k - if (typeof localStorage.key == 'undefined') return null - return k = openpgp.key.readArmored(localStorage.key) + this.key = function() { + if (k) return k // cached key + if (typeof localStorage.privkey === 'undefined') return null + return k = openpgp.key.readArmored(localStorage.privkey) } /// get own user name /** get user name as user id of first public key */ - function user() { + this.user = function() { if (k || key()) return k.pub.keys[0].getUserIds()[0] return null } - /// create New User - function createuser(user, email, pwd) { - controller.notice("generating keys") - openpgp.generateKey({ - numBits: 4096, - userIds: [{name: user, email: email}], - passphrase: pwd - }).then(function(keyPair) { - controller.success("keys generated") - localStorage.key = keyPair.privateKeyArmored - k = keyPair.key - }).catch(function(e) { - console.log(e) - controller.fatal("generating key pairs failed") - }) - } - /// open private key with password /** @return @c true if password matches */ function password(pwd) { @@ -116,8 +103,15 @@ function SafeChat() { return true } + //------------------------------------------------------------------------------ + if (openpgp.initWorker("openpgp.worker.min.js")) + console.log("asynchronous openpgp enabled") + else + console.log("asynchronous openpgp failed") + } + //============================================================================== /// database that stores in indexed db function DataBase() { @@ -126,6 +120,7 @@ function SafeChat() { } + //============================================================================== /// manage local copy of users function Users() { @@ -142,6 +137,7 @@ function SafeChat() { } + //------------------------------------------------------------------------------ /// manage local copy of messages function Messages() { @@ -152,6 +148,7 @@ function SafeChat() { } + //============================================================================== /// @class Communication client socket communication /** @param view is of class SafeChat.View */ function Communication(controller) { @@ -163,9 +160,13 @@ function SafeChat() { socket.broadcast.emit(signal, data) } - function emit(signal, data) { + function emit(signal, data, next) { console.log("<-snd "+signal) - socket.emit(signal, data) + socket.emit(signal, data, next) + } + + this.lookup = function(usr, next) { + emit('user', usr, next) } socket @@ -183,6 +184,7 @@ function SafeChat() { } + //============================================================================== /// @class View provides the glue to the GUI in the index.ejs file /** View provides the following callbacks: - status updates: @@ -299,7 +301,7 @@ function SafeChat() { @param msg (optional) the success message text */ function show(id, msg) { console.log("state: "+id) - if (msg) success(msg) else $("#status").hide() + if (msg) success(msg); else $("#status").hide(); $("#main").children(":not(#"+id+")").hide() $("#main #"+id).show() $("#main #"+id+" form input:first-child").focus() @@ -327,21 +329,15 @@ function SafeChat() { } function checkFeature(id, query) { - if (query) $('#'+id+':before') - .css('color', 'green') - .css('content', '✔') - else $('#'+id+':before') - .css('color', 'red') - .css('content', '✘') if (query) $('#'+id) .css('color', 'green') - .css('text-decoration', 'line-through') + .prepend('') else $('#'+id) .css('color', 'red') - .css('text-decoration', 'none') + .prepend('') } - function checkFeatures() { + this.checkFeatures = function() { $('ul.features').css('list-style-type', 'none') checkFeature("localstorage", Storage) checkFeature("indexeddb", window.indexedDB) @@ -350,6 +346,54 @@ function SafeChat() { checkFeature("filereader", window.FileReader) } + /// @name create new user + /// @{ + + this.newuser = function() { + show('newuser') + } + + var user = null + var pwd = false + + function invalid(usr) { + return !user || !user.exists && user.name.length<3 + } + + this.available = function(usr) { + user = usr + console.log("props:", invalid(user) || !pwd) + $("#createuser").prop(":disabled", invalid(user) || !pwd) + if (user.length==0) + notice("please chose a user name") + else if (user.length<3) + notice("please chose a longer user name") + else if (user.exists) + notice("user name is already in use") + else if (!pwd) + notice("please chose a password") + else + success("user is ready to be created") + } + + this.passwords = function(pwd1, pwd2) { + pwd = pwd1==pwd2 && pwd1.length>5 + console.log("props:", invalid(user) || !pwd) + $("#createuser").prop(":disabled", invalid(user) || !pwd) + if (pwd1.length==0) + notice('please chose a password') + else if (pwd1.length<6) + notice('please chose a longer password') + else if (pwd1 != pwd2) + notice("passwords don't match") + else if (invalid(user)) + notice("please chose a user name") + else + success("user is ready to be created") + } + + /// @} + function DataTransfer() { var reboottimer = null @@ -405,10 +449,11 @@ function SafeChat() { } + //============================================================================== /// @class Controller defines the programm flow function Controller(view) { - var db = new Database() + var db = new DataBase() var crypto = new Crypto(this) var communication = new Communication(this) var users = new Users() @@ -461,6 +506,33 @@ function SafeChat() { // @} + /// @name signals from view + /// @{ + + /// @name new user registration + /// @{ + + this.lookup = function(usr) { + if (usr.length > 2) communication.lookup(uid(usr), function(res) { + view.available(res) + }) + } + + this.checkpasswords = view.passwords + + this.createuser = function(name, pwd) { + crypto.createuser(name, name+'@'+hostname, pwd).then(function() { + if (!crypto.password(pwd)) + fatal("private key decryption failed") + else + chat() + }) + } + + /// @} + + /// @} + function initBrowser() { window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction @@ -469,242 +541,248 @@ function SafeChat() { return window.indexedDB && window.crypto.getRandomValues && Storage } - function register() { + var newuser = view.newuser + + function chat() { + } + + function password() { } function login() { - if (!crypto.key()) register() - else password() + if (!crypto.key()) newuser(); else password(); } - function run() { + this.run = function() { login() } - function start() { - view.reboot = run + this.start = function() { + view.reboot = this.run var compatible = initBrowser() view.checkFeatures() if (!compatible) view.fatal("your browser is not supported") else - run() + this.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() { - } - /// Check if password is set and matches the repeated password - /** Checks if both passwords are identical and valid and gives - feedback to the user. +//============================================================================== +//------------------------------------------------------------------------------ + +var filecontent = new Array() ///< temporary storage for attachments +var reboottimer = null - Called when user edits the password fields. +function connectionstatus() { + if (socket.connected) connected(); else disconnected(); +} - Sets @ref username and checks @ref password - if both are well - defined, enables the submit button. +/// Configure local groups +/** … */ +function groups() { +} - @param pwd The password. - @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)) - if (password) { - if (username) success("user is ready to be created") +/// Check if password is set and matches the repeated password +/** Checks if both passwords are identical and valid and gives + feedback to the user. + + Called when user edits the password fields. + + Sets @ref username and checks @ref password - if both are well + defined, enables the submit button. + + @param pwd The password. + @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)) + if (password) { + 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 (username) notice("passwords don't match") 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") + } +} - /// Checks if the receiver of a message exists on server. - /** Calls checknewuser.php on server and enables the message submit - button if the receiver of the message exists on the server. */ - function checkpartner(user) { - $("#chat").submit(function(event) { - return false - }) - emit("user", uid(user)) - } +/// Checks if the receiver of a message exists on server. +/** Calls checknewuser.php on server and enables the message submit + button if the receiver of the message exists on the server. */ +function checkpartner(user) { + $("#chat").submit(function(event) { + 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") - 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() - }).catch(function(e) { - console.log(e) - error("generating key pairs failed") - }) - } +/// 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") + 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() + }).catch(function(e) { + console.log(e) + error("generating key pairs failed") + }) +} - /// Get Own Public Key - /** @return public key object */ - function publicKey() { - if (typeof localStorage.pubkey == 'undefined') { - if (typeof localStorage.pubKey == 'undefined') { - return null - } else { - localStorage.pubkey = localStorage.pubKey - localStorage.removeItem(pubKey) - } +/// Get Own Public Key +/** @return public key object */ +function publicKey() { + if (typeof localStorage.pubkey == 'undefined') { + if (typeof localStorage.pubKey == 'undefined') { + return null + } else { + localStorage.pubkey = localStorage.pubKey + localStorage.removeItem(pubKey) } - return openpgp.key.readArmored(localStorage.pubkey) } + return openpgp.key.readArmored(localStorage.pubkey) +} - /// Get Own Private Key - /** @return private key object */ - function privateKey() { - if (typeof localStorage.privkey == 'undefined') { - if (typeof localStorage.privKey == 'undefined') { - return null - } else { - localStorage.privkey = localStorage.privKey - localStorage.removeItem(privKey) - } +/// Get Own Private Key +/** @return private key object */ +function privateKey() { + if (typeof localStorage.privkey == 'undefined') { + if (typeof localStorage.privKey == 'undefined') { + return null + } else { + localStorage.privkey = localStorage.privKey + localStorage.removeItem(privKey) } - return openpgp.key.readArmored(localStorage.privkey) } + return openpgp.key.readArmored(localStorage.privkey) +} - /// Get Own User Name - /** Get user name as user id of first public key */ - function userid() { - if (!publicKey() || - publicKey().keys.length < 1 || - publicKey().keys[0].getUserIds().length < 1) return null - return publicKey().keys[0].getUserIds()[0] - } +/// Get Own User Name +/** Get user name as user id of first public key */ +function userid() { + if (!publicKey() || + publicKey().keys.length < 1 || + publicKey().keys[0].getUserIds().length < 1) return null + 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) - } +/// 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) +} - function guessfilename(mimetype, user, date) { - if (!user) user = userid() +function guessfilename(mimetype, user, date) { + 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 - } + var ext = mimetype.replace(/.*\/(x-)?/i, "") + return pad(date.getFullYear())+pad(date.getMonth()+1)+pad(date.getDate()) + +"-"+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) +/// 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' - if (file.type.match('^image/')) { - 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) - } else { - 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) - }) - } + 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) + } 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) + } else { + 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) + }) +} - var recorder +var recorder - function done() { - if (recorder) { - recorder.stop() - recorder.recording(function(data) { - previewfile(data, "video/webm") - abort() - }) - } +function done() { + if (recorder) { + recorder.stop() + recorder.recording(function(data) { + previewfile(data, "video/webm") + abort() + }) } +} - function abort() { - if (recorder) { - $("#videorecorder").hide() - recorder.release() - delete recorder recorder = null - } +function abort() { + if (recorder) { + $("#videorecorder").hide() + recorder.release() + delete recorder + recorder = null } +} - /// Record Video from builtin camera - function recordvideo() { - try { - 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() - }) - } catch (e) { - console.log(e) - error("cannot access camera", true) - } +/// Record Video from builtin camera +function recordvideo() { + try { + 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() + }) + } catch (e) { + console.log(e) + error("cannot access camera", true) } +} - function previewfile(content, type, name) { - if (!name) name = guessfilename(type) +function previewfile(content, type, name) { + if (!name) name = guessfilename(type) if (type.match('^image/')) { var img = document.createElement("img") img.onload = function() { // resize image to maximum 400px @@ -748,334 +826,341 @@ function SafeChat() { img.title = name+"\n"+size(content.length) $("#preview").append(img) } - } +} - /// Upload Attachment - /** Prepares attachment to be sent in a message. If the attachment is - an image, it resizes the image to 400px on the lager side. - - By now, only images are supported. - - Stores data in global variable @ref filecontent. */ - function fileupload(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() - reader.onload = function(evt) { - if (evt.target.error) return error("error reading file", true) +/// Upload Attachment +/** Prepares attachment to be sent in a message. If the attachment is + an image, it resizes the image to 400px on the lager side. + + By now, only images are supported. + + Stores data in global variable @ref filecontent. */ +function fileupload(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() + 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) - } - reader.readAsDataURL(file) + if (evt.target.readyState==1) return notice("loading data …") + previewfile(evt.target.result, file.type, file.name) } + reader.readAsDataURL(file) } +} - /// Sets Receiver's Name - /** Called when clicked on a receiver's name. Sets focus to the - message text field. +/// Sets Receiver's Name +/** Called when clicked on a receiver's name. Sets focus to the + message text field. - @param name The receiver's name. */ - function setreceiver(name) { - $("#recv").val(name) - checkpartner(name) - $("#msg").focus() - } + @param name The receiver's name. */ +function setreceiver(name) { + $("#recv").val(name) + checkpartner(name) + $("#msg").focus() +} - var userMap = null - function users(userlist) { - console.log("rcv-> users") - userMap = new Array() - $("#allusers").empty() - userlist.forEach(function(usr) { - userMap[usr.name] = usr.pubkey - $("#allusers").append('