elvn
Version:
Command line time management for nerds
317 lines (280 loc) • 9.41 kB
JavaScript
var fs = require('fs');
var qs = require('querystring');
var crypto = require('crypto');
var url = require('url');
var tire = require('tire');
var util = require('util');
var DEFAULT_SERVER_PATH = "https://kucoe.net/elvn/";
exports.Sync = function (email, noKey, serverPath, basePath, items, cli) {
this.email = email;
this.noKey = noKey;
this.serverPath = serverPath ? serverPath : DEFAULT_SERVER_PATH;
this.basePath = basePath;
this.items = items;
this.cli = cli;
this.key = null;
this.toSave = false;
this.authorized = false;
};
var AuthFailedError = function () {
Error.call(this, 'Not authorized');
};
util.inherits(AuthFailedError, Error);
exports.Sync.prototype = {
restore: function () {
var path = this.basePath + "items.bak";
tire(function (next) {
fs.exists(path, next);
}, this, this.errFn())(function (exists, next) {
if (exists) {
fs.readFile(path, 'utf-8', next);
}
})(function (err, data) {
var o = JSON.parse(data);
this.items.commit(o);
this.showRestoredStatus();
});
},
backup: function () {
var path = this.basePath + "items.bak";
fs.writeFile(path, JSON.stringify(this.items.getItems()), 'utf-8');
},
pull: function (cb) {
this.backup();
tire(this.auth, this, this.errFn(cb))
(function (token, next) {
this.req('items/' + token, next);
})
(function (remote) {
if (remote.error) {
cb && cb(this.reqError(remote.error));
} else if (!remote.value) {
this.push(cb);
} else {
this.items.commit(JSON.parse(remote.value));
this.showSynchronizedStatus();
cb && cb();
}
});
},
push: function (cb) {
tire(this.auth, this, this.errFn(cb))
(function (token, next) {
this.req("items/" + token, {items: JSON.stringify(this.items.getItems())}, next);
})(function (remote) {
if (remote.error) {
cb && cb(this.reqError(remote.error));
} else {
this.showSynchronizedStatus();
cb && cb();
}
});
},
auth: function (cb) {
var k = null;
tire(function (next) {
this.req("users/0/" + this.email, next);
}, this, cb)(function (remote, next) {
if (remote.value === 0) {
this.register(next);
} else {
this.getKey(next);
}
})(function (key, next) {
k = key;
this.req("auth", {email: this.email, key: key}, next);
})(function (remote) {
if (remote.error) {
cb(this.reqError(remote.error));
} else {
this.authorized = true;
if (this.toSave) {
var path = this.basePath + "sync.key";
this.saveKey(path, k);
}
this.key = k;
cb(remote.value);
}
});
},
register: function (cb) {
var k = null;
tire(function (next) {
this.askForPassword(false, next);
}, this, cb)(function (password, next) {
k = this.hash(password);
this.toSave = !this.noKey;
this.req('users', {email: this.email, key: k}, next);
})(function () {
cb(k);
});
},
askForPassword: function (exists, cb) {
if (exists) {
this.cli.password("Please enter your password:", cb);
} else {
this.cli.password("Please choose password to register with:", cb);
}
},
getKey: function (cb) {
if (!this.key) {
var path = this.basePath + "sync.key";
tire(function (next) {
fs.exists(path, next);
}, this, cb)(function (exists, next) {
if (exists) {
this.existedKey(path, next);
} else {
this.newKey(path, next);
}
})(function (key) {
cb.call(this, key);
});
} else {
cb(this.key);
}
},
newKey: function (path, cb) {
var key = null;
tire(function (next) {
this.askForPassword(true, next);
}, this, cb)(function (password) {
key = this.hash(password);
this.toSave = !this.noKey;
cb.call(this, key);
});
},
saveKey: function (path, key) {
tire(function (next) {
fs.writeFile(path, key, 'utf-8', next);
}, this)(function (err, next) {
this.creationTime(path, next);
})(function (err, time) {
fs.writeFile(path, this.encrypt(key, time), 'utf-8');
});
},
existedKey: function (path, cb) {
var key = null;
tire(function (next) {
fs.readFile(path, 'utf-8', next);
}, this, cb)(function (err, data, next) {
key = data;
this.creationTime(path, next);
})(function (err, time) {
cb.call(this, this.decrypt(key, time));
});
},
hash: function (password) {
return crypto.createHash('sha512').update(password).digest("hex");
},
encrypt: function (key, time) {
var encrypt = crypto.createCipher('aes-256-cbc', '' + time);
var a = encrypt.update(key, 'hex', 'base64');
var b = encrypt.final('base64');
return a + b;
},
decrypt: function (crypted, time) {
var decrypt = crypto.createDecipher('aes-256-cbc', '' + time);
var a = decrypt.update(crypted, 'base64', 'hex');
var b = decrypt.final('hex');
return a + b;
},
creationTime: function (path, cb) {
fs.stat(path, function (err, stat) {
cb(err, stat.ctime.getTime());
});
},
req: function (resource, params, cb, method) {
var self = this;
if (typeof params === 'function') {
cb = params;
params = null;
}
var path = this.serverPath + resource;
var opts = url.parse(path);
opts.method = method || 'POST';
opts.headers = {"User-Agent": "Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt)"};
if (opts.method == 'POST') {
opts.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
params = qs.stringify(params);
opts.headers['Content-Length'] = params ? params.length : 0;
var connect = require(path.substr(0, 8) == 'https://' ? 'https' : 'http');
var req = connect.request(opts);
if (params) {
req.write(params);
}
req.on('response', function (res) {
res.setEncoding('utf8');
res.body = '';
res.on('data', function (chunk) {
res.body += chunk;
});
res.on('end', function () {
try {
cb.call(self, JSON.parse(res.body));
} catch (e) {
var s = 'Not well response' + res.body;
console.error(s);
cb.call(self, new Error(s));
}
});
});
req.on('error', function (e) {
var s = e.message;
switch (e.code) {
case 'ECONNREFUSED':
s = 'Connection refused from address: ' + path;
break;
case 'ECONNRESET':
s = 'Connection reset for address: ' + path;
break;
case 'ENOTFOUND':
s = 'Could not reach server by address: ' + path;
break;
default:
s = 'Connection problem for adsress: ' + path + ' - ' + s;
}
console.error(s);
cb.call(self, new Error(s));
});
req.end();
},
reqError: function (error) {
if (error === 'authentication failed') {
return new AuthFailedError();
} else {
return new Error(error);
}
},
errFn: function (cb) {
return function (err) {
if (typeof err === 'string') {
err = new Error(err);
}
if (err instanceof AuthFailedError) {
this.showAuthFailedStatus();
fs.unlink(this.basePath + "sync.key")
} else if (err) {
this.showSynchronizedFailedStatus(err.message);
}
cb && cb(err);
};
},
showAuthFailedStatus: function () {
this.fire("Authorization failed for " + this.email);
},
showSynchronizedStatus: function () {
this.fire("Synchronized successfully as " + this.email);
},
showRestoredStatus: function () {
this.fire("Restored successfully");
},
showSynchronizedFailedStatus: function (message) {
this.fire("Synchronization failed, cause:" + message);
},
fire: function (message) {
console.log(message);
this.cli.stream.emit('line', '\n');
}
};