keeweb/plugins/keeweb-plugin/keeweb-plugin.js

248 lines
8.1 KiB
JavaScript

#! /usr/bin/env node
/**
* KeeWeb plugin creator
* (C) Antelle 2019, MIT license https://github.com/keeweb/keeweb
*/
/* eslint-disable no-console */
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const https = require('https');
const args = process.argv.splice(2);
const op = args.shift();
const bumpVersion = args.some((arg) => arg === '--bump-version');
const privateKeyPath = args
.filter((arg) => arg.startsWith('--private-key='))
.map((arg) => arg.replace('--private-key=', ''))[0];
const signerModule = args
.filter((arg) => arg.startsWith('--signer-module='))
.map((arg) => arg.replace('--signer-module=', ''))[0];
const serverPort = args
.filter((arg) => arg.startsWith('--port='))
.map((arg) => arg.replace('--port=', ''))[0];
showBanner();
switch (op) {
case 'sign':
signPlugin();
break;
case 'watch':
watchSignPlugin();
break;
default:
showHelp();
}
function showBanner() {
console.log(`KeeWeb plugin utils`);
}
function showHelp() {
console.log('Usage:');
console.log(' - node keeweb-plugin sign <plugin_name>');
console.log(' sign plugin and exit');
console.log(' - node keeweb-plugin watch <plugin_name>');
console.log(' watch plugin directory and sign on changes');
console.log('');
console.log('Optional arguments:');
console.log(' --bump-version');
console.log(' bump version in package.json');
console.log(' --private-key=/path/to/your/key.pem');
console.log(' path to your private key');
}
function signPlugin(packageName) {
if (!packageName) {
packageName = getPackageArg();
}
const manifest = JSON.parse(fs.readFileSync(path.join(packageName, 'manifest.json')));
let signPromise = Promise.resolve(false);
for (const res of Object.keys(manifest.resources)) {
console.log(`Signing ${res}...`);
let fileName;
switch (res) {
case 'js':
fileName = 'plugin.js';
break;
case 'css':
fileName = 'plugin.css';
break;
case 'loc':
fileName = manifest.locale.name + '.json';
break;
}
signPromise = signPromise.then((changed) => {
return signResource(packageName, fileName).then((signature) => {
if (manifest.resources[res] !== signature) {
manifest.resources[res] = signature;
changed = true;
}
return changed;
});
});
}
signPromise
.then((changed) => {
if (changed) {
if (bumpVersion) {
manifest.version = manifest.version.replace(/\d+$/, (v) => +v + 1);
}
fs.writeFileSync(
path.join(packageName, 'manifest.json'),
JSON.stringify(manifest, null, 2)
);
console.log('Done, package manifest updated');
} else {
console.log('No changes');
}
})
.catch((e) => {
console.error('Error', e);
});
}
function signResource(packageName, fileName) {
fileName = path.join(packageName, fileName);
const data = fs.readFileSync(fileName);
if (signerModule) {
return require(signerModule)(data);
} else {
const privateKey = fs.readFileSync(
privateKeyPath || path.join(packageName, 'private_key.pem'),
'binary'
);
return Promise.resolve().then(() => {
const sign = crypto.createSign('RSA-SHA256');
sign.write(data);
sign.end();
return sign.sign(privateKey).toString('base64');
});
}
}
function watchSignPlugin() {
const packageName = getPackageArg();
let changed = {};
let updateTimer;
fs.watch(packageName, { persistent: true }, (eventType, fileName) => {
if (fileName.lastIndexOf('manifest.json', 0) === 0) {
return;
}
changed[fileName] = true;
if (updateTimer) {
clearTimeout(updateTimer);
}
updateTimer = setTimeout(() => {
console.log('Changed:', Object.keys(changed).join(', '));
signPlugin(packageName);
updateTimer = null;
changed = {};
}, 1000);
});
servePlugin(packageName);
console.log('Waiting for changes...');
}
function servePlugin(packageName) {
const options = {
key: fs.readFileSync(path.join(__dirname, 'self-signed-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'self-signed-cert.pem'))
};
const port = serverPort || 8089;
let keeWebHtmlCached;
const serveKeeWebHtml = (res) => {
if (keeWebHtmlCached) {
res.writeHead(200);
res.end(keeWebHtmlCached);
} else {
https.get('https://app.keeweb.info', (kwRes) => {
if (kwRes.statusCode !== 200) {
console.error(
'Error loading https://app.keeweb.info: HTTP status ' + kwRes.statusCode
);
res.writeHead(500);
return res.end(
'Error loading https://app.keeweb.info: HTTP status ' + kwRes.statusCode
);
}
const data = [];
kwRes.on('data', (chunk) => data.push(chunk));
kwRes.on('end', () => {
keeWebHtmlCached = Buffer.concat(data)
.toString('utf8')
.replace('(no-config)', 'config.json');
serveKeeWebHtml(res);
});
kwRes.on('error', (e) => {
console.error('Error loading https://app.keeweb.info', e);
res.writeHead(500);
res.end('Error loading https://app.keeweb.info');
});
});
}
};
const serveConfig = (res) => {
res.writeHead(200);
res.end(`{"settings":{},"plugins":[{"url":"/"}]}`);
};
https
.createServer(options, (req, res) => {
console.log('GET', req.connection.remoteAddress, req.url);
const filePath = path.resolve(
packageName,
'.' + req.url.replace(/\.\./g, '').replace(/\?.*/, '')
);
const packagePath = path.resolve(packageName);
if (!filePath.startsWith(packagePath)) {
res.writeHead(404);
res.end('Not found');
return;
}
if (req.url === '/') {
return serveKeeWebHtml(res);
} else if (req.url === '/config.json') {
return serveConfig(res);
}
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('Not found');
} else {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', true);
res.setHeader('Access-Control-Allow-Methods', 'GET');
res.setHeader('Content-type', 'text/plain');
res.writeHead(200);
res.end(data);
}
});
})
.listen(port);
console.log(`Open this URL in your browser or add it to KeeWeb: https://127.0.0.1:${port}`);
console.log("If you see a browser warning about an unsafe website, click Proceed, it's safe.");
}
function getPackageArg() {
const packageName = args.shift();
if (!packageName) {
showHelp();
return process.exit(1);
}
if (!fs.existsSync(packageName)) {
console.error('Package folder does not exist');
return process.exit(1);
}
if (!fs.existsSync(path.join(packageName, 'manifest.json'))) {
console.error('Package manifest.json does not exist');
return process.exit(1);
}
return packageName;
}