keeweb/app/scripts/auto-type/auto-type-runner.js

430 lines
14 KiB
JavaScript
Raw Normal View History

2017-01-31 07:50:28 +01:00
const AutoTypeObfuscator = require('./auto-type-obfuscator');
const AutoTypeEmitterFactory = require('./auto-type-emitter-factory');
const Format = require('../util/format');
const Logger = require('../util/logger');
2016-04-19 21:19:08 +02:00
2017-01-31 07:50:28 +01:00
const emitterLogger = new Logger('auto-type-emitter');
2016-04-19 21:19:08 +02:00
emitterLogger.setLevel(localStorage.autoTypeDebug ? Logger.Level.All : Logger.Level.Warn);
2016-04-09 00:21:58 +02:00
2017-01-31 07:50:28 +01:00
const AutoTypeRunner = function(ops) {
2016-04-08 17:40:00 +02:00
this.ops = ops;
2016-04-09 08:29:20 +02:00
this.pendingResolvesCount = 0;
2016-04-09 00:21:58 +02:00
this.entry = null;
this.now = new Date();
2016-04-08 17:40:00 +02:00
};
AutoTypeRunner.PendingResolve = { pending: true };
AutoTypeRunner.Keys = {
tab: 'tab', enter: 'enter', space: 'space',
up: 'up', down: 'down', left: 'left', right: 'right', home: 'home', end: 'end', pgup: 'pgup', pgdn: 'pgdn',
insert: 'ins', ins: 'ins', delete: 'del', del: 'del', backspace: 'bs', bs: 'bs', bksp: 'bs', esc: 'esc',
win: 'win', lwin: 'win', rwin: 'rwin', f1: 'f1', f2: 'f2', f3: 'f3', f4: 'f4', f5: 'f5', f6: 'f6',
f7: 'f7', f8: 'f8', f9: 'f9', f10: 'f10', f11: 'f11', f12: 'f12', f13: 'f13', f14: 'f14', f15: 'f15', f16: 'f16',
add: 'add', subtract: 'subtract', multiply: 'multiply', divide: 'divide',
numpad0: 'n0', numpad1: 'n1', numpad2: 'n2', numpad3: 'n3', numpad4: 'n4',
numpad5: 'n5', numpad6: 'n6', numpad7: 'n7', numpad8: 'n8', numpad9: 'n9'
};
AutoTypeRunner.Substitutions = {
2016-04-09 00:21:58 +02:00
title: function(runner, op) { return runner.getEntryFieldKeys('Title', op); },
username: function(runner, op) { return runner.getEntryFieldKeys('UserName', op); },
url: function(runner, op) { return runner.getEntryFieldKeys('URL', op); },
password: function(runner, op) { return runner.getEntryFieldKeys('Password', op); },
notes: function(runner, op) { return runner.getEntryFieldKeys('Notes', op); },
group: function(runner) { return runner.getEntryGroupName(); },
2016-04-09 08:29:20 +02:00
totp: function(runner, op) { return runner.getOtp(op); },
2016-04-09 00:21:58 +02:00
s: function(runner, op) { return runner.getEntryFieldKeys(op.arg, op); },
'dt_simple': function(runner) { return runner.dt('simple'); },
'dt_year': function(runner) { return runner.dt('Y'); },
'dt_month': function(runner) { return runner.dt('M'); },
'dt_day': function(runner) { return runner.dt('D'); },
'dt_hour': function(runner) { return runner.dt('h'); },
'dt_minute': function(runner) { return runner.dt('m'); },
'dt_second': function(runner) { return runner.dt('s'); },
'dt_utc_simple': function(runner) { return runner.udt('simple'); },
'dt_utc_year': function(runner) { return runner.udt('Y'); },
'dt_utc_month': function(runner) { return runner.udt('M'); },
'dt_utc_day': function(runner) { return runner.udt('D'); },
'dt_utc_hour': function(runner) { return runner.udt('h'); },
'dt_utc_minute': function(runner) { return runner.udt('m'); },
'dt_utc_second': function(runner) { return runner.udt('s'); }
2016-04-08 17:40:00 +02:00
};
AutoTypeRunner.prototype.resolve = function(entry, callback) {
this.entry = entry;
try {
this.resolveOps(this.ops);
2016-04-09 08:29:20 +02:00
if (!this.pendingResolvesCount) {
2016-04-08 17:40:00 +02:00
callback();
2016-04-08 23:12:56 +02:00
} else {
this.resolveCallback = callback;
2016-04-08 17:40:00 +02:00
}
} catch (e) {
return callback(e);
}
};
AutoTypeRunner.prototype.resolveOps = function(ops) {
2017-01-31 07:50:28 +01:00
for (let i = 0, len = ops.length; i < len; i++) {
const op = ops[i];
2016-04-09 10:41:52 +02:00
if (op.type === 'group') {
this.resolveOps(op.value);
} else {
this.resolveOp(op);
}
2016-04-08 17:40:00 +02:00
}
};
AutoTypeRunner.prototype.resolveOp = function(op) {
if (op.value.length === 1 && !op.sep) {
// {x}
op.type = 'text';
return;
}
if (op.value.length === 1 && op.sep === ' ') {
// {x 3}
op.type = 'text';
2017-01-31 07:50:28 +01:00
const ch = op.value;
let text = ch;
const len = +op.arg;
2016-04-08 17:40:00 +02:00
while (text.length < len) {
text += ch;
}
op.value = text;
return;
}
2017-01-31 07:50:28 +01:00
const lowerValue = op.value.toLowerCase();
const key = AutoTypeRunner.Keys[lowerValue];
2016-04-08 17:40:00 +02:00
if (key) {
if (op.sep === ' ' && +op.arg > 0) {
// {TAB 3}
op.type = 'group';
op.value = [];
2017-01-31 07:50:28 +01:00
const count = +op.arg;
for (let i = 0; i < count; i++) {
2016-04-08 23:12:56 +02:00
op.value.push({type: 'key', value: key});
2016-04-08 17:40:00 +02:00
}
} else {
// {TAB}
op.type = 'key';
op.value = key;
}
return;
}
2017-01-31 07:50:28 +01:00
const substitution = AutoTypeRunner.Substitutions[lowerValue];
2016-04-08 17:40:00 +02:00
if (substitution) {
2016-04-08 23:12:56 +02:00
// {title}
2016-04-08 17:40:00 +02:00
op.type = 'text';
op.value = substitution(this, op);
if (op.value === AutoTypeRunner.PendingResolve) {
2016-04-09 08:29:20 +02:00
this.pendingResolvesCount++;
2016-04-08 17:40:00 +02:00
}
return;
}
2016-04-08 23:12:56 +02:00
if (!this.tryParseCommand(op)) {
throw 'Bad op: ' + op.value;
}
};
AutoTypeRunner.prototype.tryParseCommand = function(op) {
switch (op.value.toLowerCase()) {
2016-04-08 17:40:00 +02:00
case 'clearfield':
2016-04-08 23:12:56 +02:00
// {CLEARFIELD}
2016-04-08 17:40:00 +02:00
op.type = 'group';
2016-04-09 20:10:11 +02:00
op.value = [
{ type: 'key', value: 'end' },
{ type: 'key', value: 'home', mod: { '+': true } },
{ type: 'key', value: 'bs' }
];
2016-04-08 23:12:56 +02:00
return true;
2016-04-08 17:40:00 +02:00
case 'vkey':
2016-04-08 23:12:56 +02:00
// {VKEY 10} {VKEY 0x1F}
2016-04-08 17:40:00 +02:00
op.type = 'key';
2016-04-08 23:12:56 +02:00
op.value = parseInt(op.arg);
if (isNaN(op.value) || op.value <= 0) {
throw 'Bad vkey: ' + op.arg;
}
return true;
2016-04-08 17:40:00 +02:00
case 'delay':
2016-04-08 23:12:56 +02:00
// {DELAY 5} {DELAY=5}
2016-04-08 17:40:00 +02:00
op.type = 'cmd';
op.value = op.sep === '=' ? 'setDelay' : 'wait';
2016-04-08 23:12:56 +02:00
if (!op.arg) {
2016-04-26 21:40:18 +02:00
throw 'Delay requires milliseconds count';
2016-04-08 23:12:56 +02:00
}
if (isNaN(+op.arg)) {
throw 'Bad delay: ' + op.arg;
}
2016-04-23 17:34:49 +02:00
if (op.arg < 0) {
2016-04-08 23:12:56 +02:00
throw 'Delay requires positive interval';
}
2016-04-08 17:40:00 +02:00
op.arg = +op.arg;
2016-04-08 23:12:56 +02:00
return true;
default:
return false;
2016-04-08 17:40:00 +02:00
}
};
2016-04-09 00:21:58 +02:00
AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
2016-04-26 21:40:18 +02:00
if (!field || !this.entry) {
2016-04-09 00:21:58 +02:00
return '';
}
2017-01-31 07:50:28 +01:00
const value = this.entry.getFieldValue(field);
2016-04-09 00:21:58 +02:00
if (!value) {
return '';
}
if (value.isProtected) {
op.type = 'group';
2017-01-31 07:50:28 +01:00
const ops = [];
2016-07-17 13:30:38 +02:00
value.forEachChar(ch => {
2016-04-23 18:31:39 +02:00
if (ch === 10 || ch === 13) {
ops.push({type: 'key', value: 'enter'});
} else {
ops.push({type: 'text', value: String.fromCharCode(ch)});
}
2016-04-09 00:21:58 +02:00
});
2016-04-24 14:08:46 +02:00
return ops;
2016-04-23 18:31:39 +02:00
} else {
2017-01-31 07:50:28 +01:00
const parts = value.split(/[\r\n]/g);
2016-04-23 18:31:39 +02:00
if (parts.length === 1) {
return value;
}
op.type = 'group';
2017-01-31 07:50:28 +01:00
const partsOps = [];
2016-07-17 13:30:38 +02:00
parts.forEach(part => {
2016-04-23 18:31:39 +02:00
if (partsOps.length) {
partsOps.push({type: 'key', value: 'enter'});
}
if (part) {
partsOps.push({type: 'text', value: part});
}
});
return partsOps;
2016-04-09 00:21:58 +02:00
}
};
AutoTypeRunner.prototype.getEntryGroupName = function() {
2016-04-26 21:40:18 +02:00
return this.entry && this.entry.group.get('title');
2016-04-09 00:21:58 +02:00
};
AutoTypeRunner.prototype.dt = function(part) {
switch (part) {
case 'simple':
return this.dt('Y') + this.dt('M') + this.dt('D') + this.dt('h') + this.dt('m') + this.dt('s');
case 'Y':
return this.now.getFullYear().toString();
case 'M':
return Format.pad(this.now.getMonth() + 1, 2);
case 'D':
return Format.pad(this.now.getDate(), 2);
case 'h':
return Format.pad(this.now.getHours(), 2);
case 'm':
return Format.pad(this.now.getMinutes(), 2);
case 's':
return Format.pad(this.now.getSeconds(), 2);
default:
throw 'Bad part: ' + part;
}
};
AutoTypeRunner.prototype.udt = function(part) {
switch (part) {
case 'simple':
return this.udt('Y') + this.udt('M') + this.udt('D') + this.udt('h') + this.udt('m') + this.udt('s');
case 'Y':
return this.now.getUTCFullYear().toString();
case 'M':
return Format.pad(this.now.getUTCMonth() + 1, 2);
case 'D':
return Format.pad(this.now.getUTCDate(), 2);
case 'h':
return Format.pad(this.now.getUTCHours(), 2);
case 'm':
return Format.pad(this.now.getUTCMinutes(), 2);
case 's':
return Format.pad(this.now.getUTCSeconds(), 2);
default:
throw 'Bad part: ' + part;
}
};
2016-04-09 08:29:20 +02:00
AutoTypeRunner.prototype.getOtp = function(op) {
2016-04-26 21:40:18 +02:00
if (!this.entry) {
return '';
}
2016-04-09 08:29:20 +02:00
this.entry.initOtpGenerator();
if (!this.entry.otpGenerator) {
return '';
}
2016-07-17 13:30:38 +02:00
this.entry.otpGenerator.next(otp => {
this.pendingResolved(op, otp, otp ? undefined : 'OTP error');
2016-04-09 08:29:20 +02:00
});
return AutoTypeRunner.PendingResolve;
};
AutoTypeRunner.prototype.pendingResolved = function(op, value, error) {
2017-01-31 07:50:28 +01:00
const wasPending = op.value === AutoTypeRunner.PendingResolve;
2016-04-09 08:29:20 +02:00
if (value) {
op.value = value;
}
if (!wasPending) {
return;
}
this.pendingResolvesCount--;
if ((this.pendingResolvesCount === 0 || error) && this.resolveCallback) {
this.resolveCallback(error);
this.resolveCallback = null;
}
};
2016-04-09 10:41:52 +02:00
AutoTypeRunner.prototype.obfuscate = function() {
this.obfuscateOps(this.ops);
};
AutoTypeRunner.prototype.obfuscateOps = function(ops) {
2017-01-31 07:50:28 +01:00
for (let i = 0, len = ops.length; i < len; i++) {
const op = ops[i];
2016-04-09 10:41:52 +02:00
if (op.mod) {
continue;
}
if (op.type === 'text') {
this.obfuscateOp(op);
} else if (op.type === 'group') {
2017-01-31 07:50:28 +01:00
const onlyText = op.value.every(grOp => grOp.type === 'text' && !grOp.mod);
2016-04-09 10:41:52 +02:00
if (onlyText) {
this.obfuscateOp(op);
} else {
this.obfuscateOps(op.value);
}
}
}
};
AutoTypeRunner.prototype.obfuscateOp = function(op) {
2017-01-31 07:50:28 +01:00
let letters = [];
2016-04-09 10:41:52 +02:00
if (op.type === 'text') {
if (!op.value || op.value.length <= 1) {
return;
}
letters = op.value.split('');
} else {
2016-07-17 13:30:38 +02:00
op.value.forEach(grOp => letters.push.apply(letters, grOp.value.split('')));
2016-04-09 10:41:52 +02:00
}
if (letters.length <= 1) {
return;
}
2017-01-31 07:50:28 +01:00
const obfuscator = new AutoTypeObfuscator(letters);
2016-04-09 14:55:27 +02:00
op.value = obfuscator.obfuscate();
op.type = 'group';
2016-04-09 10:41:52 +02:00
};
2016-04-09 16:27:42 +02:00
AutoTypeRunner.prototype.run = function(callback) {
2016-04-19 21:19:08 +02:00
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this));
2016-04-09 16:27:42 +02:00
this.emitterState = {
callback: callback,
stack: [],
ops: this.ops,
opIx: 0,
mod: {},
2016-04-10 08:32:18 +02:00
activeMod: {},
finished: null
2016-04-09 16:27:42 +02:00
};
2016-04-09 20:10:11 +02:00
this.emitNext();
2016-04-09 16:27:42 +02:00
};
2016-04-09 20:10:11 +02:00
AutoTypeRunner.prototype.emitNext = function(err) {
if (err) {
2016-04-10 08:32:18 +02:00
this.emitterState.finished = true;
2016-04-09 20:10:11 +02:00
this.emitterState.callback(err);
return;
}
2016-04-10 08:32:18 +02:00
if (this.emitterState.finished) {
this.emitterState.callback();
return;
}
2016-04-09 16:27:42 +02:00
this.resetEmitterMod(this.emitterState.mod);
if (this.emitterState.opIx >= this.emitterState.ops.length) {
2017-01-31 07:50:28 +01:00
const state = this.emitterState.stack.pop();
2016-04-09 16:27:42 +02:00
if (state) {
_.extend(this.emitterState, { ops: state.ops, opIx: state.opIx, mod: state.mod });
this.emitNext();
} else {
this.resetEmitterMod({});
2016-04-10 08:32:18 +02:00
this.emitterState.finished = true;
2016-04-19 21:19:08 +02:00
emitterLogger.debug('waitComplete');
2016-04-10 08:32:18 +02:00
this.emitter.waitComplete();
2016-04-09 16:27:42 +02:00
}
return;
}
2017-01-31 07:50:28 +01:00
const op = this.emitterState.ops[this.emitterState.opIx];
2016-04-09 16:27:42 +02:00
if (op.type === 'group') {
if (op.mod) {
this.setEmitterMod(op.mod);
}
this.emitterState.stack.push({
ops: this.emitterState.ops,
opIx: this.emitterState.opIx + 1,
mod: _.clone(this.emitterState.mod)
});
_.extend(this.emitterState, {
ops: op.value,
opIx: 0,
mod: _.clone(this.emitterState.activeMod)
});
this.emitNext();
return;
}
this.emitterState.opIx++;
if (op.mod) {
this.setEmitterMod(op.mod);
}
switch (op.type) {
case 'text':
2016-04-19 21:19:08 +02:00
emitterLogger.debug('text', op.value);
if (op.value) {
this.emitter.text(op.value);
} else {
this.emitNext();
2016-04-19 21:19:08 +02:00
}
2016-04-09 16:27:42 +02:00
break;
case 'key':
2016-04-19 21:19:08 +02:00
emitterLogger.debug('key', op.value);
2016-04-09 16:27:42 +02:00
this.emitter.key(op.value);
break;
case 'cmd':
2017-01-31 07:50:28 +01:00
const method = this.emitter[op.value];
2016-04-09 16:27:42 +02:00
if (!method) {
throw 'Bad cmd: ' + op.value;
}
2016-04-19 21:19:08 +02:00
emitterLogger.debug(op.value, op.arg);
2016-04-09 16:27:42 +02:00
method.call(this.emitter, op.arg);
break;
default:
throw 'Bad op: ' + op.type;
}
};
AutoTypeRunner.prototype.setEmitterMod = function(addedMod) {
Object.keys(addedMod).forEach(function(mod) {
if (addedMod[mod] && !this.emitterState.activeMod[mod]) {
2016-04-19 21:19:08 +02:00
emitterLogger.debug('mod', mod, true);
2016-04-09 16:27:42 +02:00
this.emitter.setMod(mod, true);
this.emitterState.activeMod[mod] = true;
}
}, this);
};
AutoTypeRunner.prototype.resetEmitterMod = function(targetState) {
Object.keys(this.emitterState.activeMod).forEach(function(mod) {
if (this.emitterState.activeMod[mod] && !targetState[mod]) {
2016-04-19 21:19:08 +02:00
emitterLogger.debug('mod', mod, false);
2016-04-09 16:27:42 +02:00
this.emitter.setMod(mod, false);
delete this.emitterState.activeMod[mod];
}
}, this);
2016-04-08 17:40:00 +02:00
};
module.exports = AutoTypeRunner;