From 668fdd91663ddb0d69d047e36c9721f38bb43144 Mon Sep 17 00:00:00 2001 From: NuSkooler Date: Thu, 23 Oct 2014 22:18:38 -0600 Subject: [PATCH] * Better view inheritance. Experimental ButtonView. User stuff --- core/button_view.js | 20 ++- core/edit_text_view.js | 22 +-- core/mci_view_factory.js | 10 ++ core/user.js | 280 +++++++++++++++------------------------ core/view.js | 22 ++- core/view_controller.js | 2 +- mods/art/MCI_TEST3.ANS | Bin 4413 -> 4491 bytes mods/matrix.js | 17 +++ 8 files changed, 171 insertions(+), 202 deletions(-) diff --git a/core/button_view.js b/core/button_view.js index c66fd33b..9596af8e 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -2,9 +2,12 @@ 'use strict'; var TextView = require('./text_view.js').TextView; +var miscUtil = require('./misc_util.js'); var util = require('util'); var assert = require('assert'); +exports.ButtonView = ButtonView; + function ButtonView(client, options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); @@ -15,15 +18,10 @@ function ButtonView(client, options) { util.inherits(ButtonView, TextView); ButtonView.prototype.onKeyPress = function(key, isSpecial) { - // we accept input so this must be implemented -- nothing to do here, however - // :TODO: Move this to View along with default asserts; update EditTextView to call View -} + ButtonView.super_.prototype.onKeyPress.call(this, key, isSpecial); -ButtonView.prototype.onSpecialKeyPress = function(keyName) { - assert(this.hasFocus); - assert(this.acceptsInput); - assert(this.specialKeyMap); - - // :TODO: see notes about making base handle 'enter' key(s) - // ...just make enter = enter | space for a button by default -} \ No newline at end of file + // allow spacebar to 'click' buttons + if(' ' === key) { + this.emit('action', 'accept'); + } +}; \ No newline at end of file diff --git a/core/edit_text_view.js b/core/edit_text_view.js index a8329c4d..c24c0290 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -22,10 +22,7 @@ function EditTextView(client, options) { util.inherits(EditTextView, TextView); -EditTextView.prototype.onKeyPress = function(key, isSpecial) { - assert(this.hasFocus); - assert(this.acceptsInput); - +EditTextView.prototype.onKeyPress = function(key, isSpecial) { if(isSpecial) { return; } @@ -43,26 +40,19 @@ EditTextView.prototype.onKeyPress = function(key, isSpecial) { this.client.term.write(key); } } + + EditTextView.super_.prototype.onKeyPress.call(this, key, isSpecial); }; EditTextView.prototype.onSpecialKeyPress = function(keyName) { - assert(this.hasFocus); - assert(this.acceptsInput); - assert(this.specialKeyMap); + // :TODO: handle 'enter' & others for multiLine if(this.isSpecialKeyMapped('backspace', keyName)) { if(this.text.length > 0) { this.text = this.text.substr(0, this.text.length - 1); this.clientBackspace(); } - } else if(this.isSpecialKeyMapped('enter', keyName)) { - if(this.multiLine) { - } else { - // :TODO: by default handle this in View/base. Can always "absorb" the call here for special handling - this.emit('action', 'accepted'); - } - } else if(this.isSpecialKeyMapped('next', keyName)) { - // :TODO: by default handle next in View/base - this.emit('action', 'next'); } + + EditTextView.super_.prototype.onSpecialKeyPress.call(this, keyName); }; \ No newline at end of file diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index 9fbbf488..d4c9bf9d 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -3,6 +3,7 @@ var TextView = require('./text_view.js').TextView; var EditTextView = require('./edit_text_view.js').EditTextView; +var ButtonView = require('./button_view.js').ButtonView; var assert = require('assert'); exports.MCIViewFactory = MCIViewFactory; @@ -37,6 +38,15 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { view = new EditTextView(this.client, options); break; + + case 'BV' : + if(mci.args.length > 0) { + options.text = mci.args[0]; + options.dimens = { width : options.text.length }; + } + + view = new ButtonView(this.client, options); + break; } return view; diff --git a/core/user.js b/core/user.js index 8959cc98..324c7878 100644 --- a/core/user.js +++ b/core/user.js @@ -1,40 +1,103 @@ /* jslint node: true */ 'use strict'; +var userDb = require('./database.js').dbs.user; var crypto = require('crypto'); var assert = require('assert'); -//var database = require('./database.js'); -var userDb = require('./database.js').dbs.user; +exports.User = User; +exports.getUserId = getUserId; +exports.createNew = createNew; +exports.generatePasswordDerivedKey = generatePasswordDerivedKey; +exports.persistAll = persistAll; -exports.User = User; +function User() { + var self = this; -var PBKDF2 = { + this.id = 0; + this.userName = ''; + + this.isValid = function() { + if(self.id <= 0 || self.userName.length < 2) { + return false; + } + + return this.hasValidPassword(); + }; + + this.hasValidPassword = function() { + if(!this.properties || !this.properties.pw_pbkdf2_salt || !this.properties.pw_pbkdf2_dk) { + return false; + } + + return this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2 && + this.prop_name.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2; + }; + + this.isRoot = function() { + return 1 === this.id; + }; + + this.isSysOp = this.isRoot; // alias +} + +User.PBKDF2 = { iterations : 1000, keyLen : 128, saltLen : 32, }; -var UserErrorCodes = Object.freeze({ - NONE : 'No error', - INVALID_USER : 'Invalid user', - INVALID_PASSWORD : 'Invalid password', -}); - -function User() { - var self = this; - - this.id = 0; - this.userName = ''; - this.groups = []; - this.permissions = []; - this.properties = {}; - - +function getUserId(userName, cb) { + userDb.get( + 'SELECT id ' + + 'FROM user ' + + 'WHERE user_name LIKE ?;', + [ userName ], + function onResults(err, row) { + cb(err, row.id); + } + ); } -User.generatePasswordDerivedKey = function(password, cb) { - crypto.randomBytes(PBKDF2.saltLen, function onRandomSalt(err, salt) { +function createNew(user, cb) { + assert(user.userName && user.userName.length > 1, 'Invalid userName'); + + userDb.run( + 'INSERT INTO user (user_name) ' + + 'VALUES (?);', + [ user.userName ], + function onUserInsert(err) { + if(err) { + cb(err); + } else { + user.id = this.lastID; + + // + // Allow converting user.password -> Salt/DK + // + if(user.password && user.password.length > 0) { + generatePasswordDerivedKey(user.password, function onDkGenerated(err, dk) { + user.properties = user.properties || { + pw_pbkdf2_salt : dk.salt, + pw_pbkdf2_dk : dk.dk, + }; + + persistAll(user, function onUserPersisted() { + cb(null, user.id); + }); + }); + } else { + persistAll(user, function onUserPersisted() { + cb(null, user.id); + }); + } + } + } + ); +} + +function generatePasswordDerivedKey(password, cb) { + crypto.randomBytes(User.PBKDF2.saltLen, function onRandomSalt(err, salt) { if(err) { cb(err); return; @@ -44,171 +107,44 @@ User.generatePasswordDerivedKey = function(password, cb) { password = new Buffer(password).toString('hex'); - crypto.pbkdf2(password, salt, PBKDF2.iterations, PBKDF2.keyLen, function onDerivedKey(err, dk) { + crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, function onDerivedKey(err, dk) { if(err) { cb(err); - return; + } else { + cb(null, { dk : dk.toString('hex'), salt : salt } ); } - - cb(null, { dk : dk.toString('hex'), salt : salt }); }); }); -}; +} -// -// :TODO: createNewUser(userName, password, groups) +function persistProperties(user, cb) { + assert(user.id > 0); -User.addNew = function(user, cb) { - userDb.run('INSERT INTO user (user_name) VALUES(?);', [ user.userName ], function onUserInsert(err) { - if(err) { - cb(err); - return; - } + var stmt = userDb.prepare( + 'REPLACE INTO user_property (user_id, prop_name, prop_value) ' + + 'VALUES (?, ?, ?);'); - user.id = this.lastID; - user.persist(cb); + Object.keys(user.properties).forEach(function onProp(name) { + stmt.run(user.id, name, user.properties[name]); }); -}; -User.prototype.persist = function(cb) { - var self = this; + stmt.finalize(function onFinalized() { + if(cb) { + cb(); + } + }); +} - if(0 === this.id || 0 === this.userName.length) { - cb(new Error(UserErrorCodes.INVALID_USER)); - return; - } +function persistAll(user, cb) { + assert(user.id > 0); userDb.serialize(function onSerialized() { userDb.run('BEGIN;'); - // :TODO: Create persistProperties(id, {props}) - var stmt = userDb.prepare('REPLACE INTO user_property (user_id, prop_name, prop_value) VALUES(?, ?, ?);'); - Object.keys(self.properties).forEach(function onPropName(propName) { - stmt.run(self.id, propName, self.properties[propName]); - }); + persistProperties(user); - stmt.finalize(function onFinalized() { - userDb.run('COMMIT;'); - cb(null, self.id); - }); + userDb.run('COMMIT;'); }); -}; -// :TODO: make standalone function(password, dk, salt) -User.prototype.validatePassword = function(password, cb) { - assert(this.properties.pw_pbkdf2_salt); - assert(this.properties.pw_pbkdf2_dk); - - var self = this; - - password = new Buffer(password).toString('hex'); - - crypto.pbkdf2(password, this.properties.pw_pbkdf2_salt, PBKDF2.iterations, PBKDF2.keyLen, function onDerivedKey(err, dk) { - if(err) { - cb(err); - return; - } - - // Constant time compare - var propDk = new Buffer(self.properties.pw_pbkdf2_dk, 'hex'); - - console.log(propDk); - console.log(dk); - - if(propDk.length !== dk.length) { - cb(new Error('Unexpected buffer length')); - return; - } - - var c = 0; - for(var i = 0; i < dk.length; i++) { - c |= propDk[i] ^ dk[i]; - } - cb(null, c === 0); - }); -}; - -// :TODO: make this something like getUserProperties(id, [propNames], cb) -function getUserDerivedKeyAndSalt(id, cb) { - var properties = {}; - userDb.each( - 'SELECT prop_name, prop_value ' + - 'FROM user_property ' + - 'WHERE user_id = ? AND prop_name="pw_pbkdf2_salt" OR prop_name="pw_pbkdf2_dk";', - [ id ], - function onPwPropRow(err, propRow) { - if(err) { - cb(err); - } else { - properties[propRow.prop_name] = propRow.prop_value; - } - }, - function onComplete() { - cb(null, properties); - } - ); -} - -User.loadWithCredentials = function(userName, password, cb) { - userDb.get('SELECT id, user_name FROM user WHERE user_name LIKE ? LIMIT 1;"', [ userName ], function onUserIds(err, userRow) { - if(err) { - cb(err); - return; - } - - if(!userRow) { - cb(new Error(UserErrorCodes.INVALID_USER)); - return; - } - - // Load dk & salt properties for password validation - getUserDerivedKeyAndSalt(userRow.id, function onDkAndSalt(err, props) { - var user = new User(); - user.properties = props; - - user.validatePassword(password, function onValidatePw(err, isCorrect) { - if(err) { - cb(err); - return; - } - - if(!isCorrect) { - cb(new Error(UserErrorCodes.INVALID_PASSWORD)); - return; - } - - // userName and password OK -- load the rest. - - user.id = userRow.id; - user.userName = userRow.user_name; - - cb(null, user); - }); - }); - }); -}; - -User.prototype.setPassword = function(password, cb) { - // :TODO: validate min len, etc. here? - - crypto.randomBytes(PBKDF2.saltLen, function onRandomSalt(err, salt) { - if(err) { - cb(err); - return; - } - - password = Buffer.isBuffer(password) ? password : new Buffer(password, 'hex'); - - crypto.pbkdf2(password, salt, PBKDF2.iterations, PBKDF2.keyLen, function onPbkdf2Generated(err, dk) { - if(err) { - cb(err); - return; - } - - cb(null, dk); - - this.properties['pw.pbkdf2.salt'] = salt; - this.properties['pw.pbkdf2.dk'] = dk; - }); - }); -}; \ No newline at end of file + cb(); +} \ No newline at end of file diff --git a/core/view.js b/core/view.js index c4f13b15..edd0bf88 100644 --- a/core/view.js +++ b/core/view.js @@ -6,10 +6,11 @@ var util = require('util'); var assert = require('assert'); var ansi = require('./ansi_term.js'); -exports.View = View; +exports.View = View; +exports.VIEW_SPECIAL_KEY_MAP_DEFAULT = VIEW_SPECIAL_KEY_MAP_DEFAULT; var VIEW_SPECIAL_KEY_MAP_DEFAULT = { - enter : [ 'enter' ], + accept : [ 'enter' ], exit : [ 'esc' ], backspace : [ 'backspace' ], del : [ 'del' ], @@ -105,4 +106,21 @@ View.prototype.setFocus = function(focused) { assert(this.acceptsFocus, 'View does not accept focus'); this.hasFocus = focused; +}; + +View.prototype.onKeyPress = function(key, isSpecial) { + assert(this.hasFocus, 'View does not have focus'); + assert(this.acceptsInput, 'View does not accept input'); +}; + +View.prototype.onSpecialKeyPress = function(keyName) { + assert(this.hasFocus, 'View does not have focus'); + assert(this.acceptsInput, 'View does not accept input'); + assert(this.specialKeyMap, 'No special key map defined'); + + if(this.isSpecialKeyMapped('accept', keyName)) { + this.emit('action', 'accept'); + } else if(this.isSpecialKeyMapped('next', keyName)) { + this.emit('action', 'next'); + } }; \ No newline at end of file diff --git a/core/view_controller.js b/core/view_controller.js index bbd30f06..4318f4aa 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -40,7 +40,7 @@ function ViewController(client) { self.nextFocus(); break; - case 'accepted' : + case 'accept' : // :TODO: check if id is submit, etc. self.nextFocus(); break; diff --git a/mods/art/MCI_TEST3.ANS b/mods/art/MCI_TEST3.ANS index b7a94830ded381b30091d54ebad6ccb90fcbe9da..daf6b6243087aba5f110ca914f9ad7835b32c89a 100644 GIT binary patch delta 165 zcmdn1)UCW>K9{Hh5J*QG<^r*WvvjmUE=XYV60U91Fd3kJ{PNWw2_(f