#! /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 pkg = require('./package.json'); 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 v${pkg.version}`); } function showHelp() { console.log('Usage:'); console.log(' - node keeweb-plugin sign '); console.log(' sign plugin and exit'); console.log(' - node keeweb-plugin watch '); 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 serveManifestAppCache = res => { res.writeHead(200); res.end('CACHE MANIFEST\nNETWORK:\n*\n'); }; 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 === '/manifest.appcache') { return serveManifestAppCache(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; }