Skip to content

Commit

Permalink
refactor: separate API from CLI (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
teppeis authored and koba04 committed Jul 22, 2020
1 parent 25598e4 commit 990f74e
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 133 deletions.
4 changes: 3 additions & 1 deletion packages/plugin-packer/package.json
Expand Up @@ -33,10 +33,12 @@
"adm-zip": "^0.4.7",
"eslint": "^3.18.0",
"eslint-config-teppeis": "^5.3.0",
"glob": "^7.1.1",
"intelli-espower-loader": "^1.0.1",
"mocha": "^3.2.0",
"power-assert": "^1.4.2",
"rimraf": "^2.6.1"
"rimraf": "^2.6.1",
"sinon": "^2.1.0"
},
"homepage": "https://github.com/teppeis/kintone-plugin-packer",
"repository": {
Expand Down
105 changes: 105 additions & 0 deletions packages/plugin-packer/src/cli.js
@@ -0,0 +1,105 @@
'use strict';

const path = require('path');
const fs = require('fs');
const archiver = require('archiver');
const denodeify = require('denodeify');
const writeFile = denodeify(fs.writeFile);
const recursive = denodeify(require('recursive-readdir'));
const streamBuffers = require('stream-buffers');
const debug = require('debug')('cli');

const packer = require('./');

/**
* @param {string} pluginDir path to plugin directory.
* @param {Object=} options {ppk: string}.
* @return {!Promise<string>} The resolved value is a path to the output plugin zip file.
*/
function cli(pluginDir, options) {
options = options || {};
const packerLocal = options.packerMock_ ? options.packerMock_ : packer;

// 1. check if pluginDir is a directory
if (!fs.statSync(pluginDir).isDirectory()) {
throw new Error(`${pluginDir} should be a directory.`);
}

// 2. check pluginDir/manifest.json
if (!fs.statSync(path.join(pluginDir, 'manifest.json')).isFile()) {
throw new Error('Manifest file $PLUGIN_DIR/manifest.json not found.');
}

const outputDir = path.dirname(path.resolve(pluginDir));
debug(`outDir : ${outputDir}`);

return recursive(pluginDir).then(files => {
files.forEach(file => {
const basename = path.basename(file);
// 3. check dot files
if (/^\./.test(basename)) {
throw new Error(`PLUGIN_DIR must not contain dot files or directories : ${file}`);
}
// 4. check *.ppk
if (/\.ppk$/.test(basename)) {
throw new Error(`PLUGIN_DIR must not contain * .ppk : ${file}`);
}
});
}).then(() => {
// 5. generate new ppk if not specified
const ppkFile = options.ppk;
let privateKey;
if (ppkFile) {
debug(`loading an existing key: ${ppkFile}`);
privateKey = fs.readFileSync(ppkFile, 'utf8');
}

// 6. package plugin.zip
return createContentsZip(pluginDir)
.then(contentsZip => packerLocal(contentsZip, privateKey))
.then(output => {
if (!ppkFile) {
fs.writeFileSync(path.join(outputDir, `${output.id}.ppk`), output.privateKey, 'utf8');
}
return outputPlugin(outputDir, output.plugin);
});
});
}

module.exports = cli;

/**
* Create contents.zip
*
* @param {string} pluginDir
* @return {!Promise<!Buffer>}
*/
function createContentsZip(pluginDir) {
return new Promise((res, rej) => {
const output = new streamBuffers.WritableStreamBuffer();
const archive = archiver('zip');
output.on('finish', () => {
debug(`contents.zip: ${archive.pointer()} bytes`);
res(output.getContents());
});
archive.pipe(output);
archive.on('error', e => {
rej(e);
});
archive.directory(pluginDir, '.');
archive.finalize();
});
}

/**
* Create and save plugin.zip
*
* @param {string} outputDir
* @param {!Buffer} plugin
* @return {!Promise<string>} The value is output path of plugin.zip.
*/
function outputPlugin(outputDir, plugin) {
const outputPath = path.join(outputDir, 'plugin.zip');
return writeFile(outputPath, plugin)
.then(arg => outputPath);
}
3 changes: 0 additions & 3 deletions packages/plugin-packer/src/hex2a.js
@@ -1,6 +1,5 @@
'use strict';

const assert = require('assert');
const N_TO_A = 'a'.charCodeAt(0) - '0'.charCodeAt(0);
const A_TO_K = 'k'.charCodeAt(0) - 'a'.charCodeAt(0);

Expand All @@ -11,8 +10,6 @@ const A_TO_K = 'k'.charCodeAt(0) - 'a'.charCodeAt(0);
* @return {string}
*/
function hex2a(hex) {
assert.equal(typeof hex, 'string');

return Array.from(hex).map(s => {
if ('0' <= s && s <= '9') {
return String.fromCharCode(s.charCodeAt(0) + N_TO_A);
Expand Down
115 changes: 21 additions & 94 deletions packages/plugin-packer/src/index.js
@@ -1,128 +1,53 @@
'use strict';

const path = require('path');
const fs = require('fs');
const archiver = require('archiver');
const RSA = require('node-rsa');
const denodeify = require('denodeify');
const recursive = denodeify(require('recursive-readdir'));
const streamBuffers = require('stream-buffers');
const debug = require('debug')('packer');

const sign = require('./sign');
const uuid = require('./calc-uuid');
const uuid = require('./uuid');

/**
* @param {string} pluginDir path to plugin directory.
* @param {Object=} options {ppk: string}.
* @return {Promise<string>} The resolved value is a path to the output plugin zip file.
* @param {!Buffer} contentsZip The zipped plugin contents directory.
* @param {string=} privateKey The private key (PKCS#1 PEM).
* @return {!Promise<{plugin: !Buffer, privateKey: string, id: string}>}
*/
function packer(pluginDir, options) {
options = options || {};

// 1. check if pluginDir is a directory
if (!fs.statSync(pluginDir).isDirectory()) {
throw new Error(`${pluginDir} should be a directory.`);
}

// 2. check pluginDir/manifest.json
if (!fs.statSync(path.join(pluginDir, 'manifest.json')).isFile()) {
throw new Error('Manifest file $PLUGIN_DIR/manifest.json not found.');
}

const outputDir = path.dirname(path.resolve(pluginDir));
debug(`outDir: ${outputDir}`);

return recursive(pluginDir).then(files => {
files.forEach(file => {
const basename = path.basename(file);
// 3. check dot files
if (/^\./.test(basename)) {
throw new Error(`PLUGIN_DIR must not contain dot files or directories: ${file}`);
}
// 4. check *.ppk
if (/\.ppk$/.test(basename)) {
throw new Error(`PLUGIN_DIR must not contain *.ppk: ${file}`);
}
});
}).then(() => {
// 5. generate new ppk if not specified
const ppkFile = options.ppk;
function packer(contentsZip, privateKey) {
return Promise.resolve().then(() => {
let key;
let privateKey;
if (ppkFile) {
debug(`loading an existing key: ${ppkFile}`);
privateKey = fs.readFileSync(ppkFile, 'utf8');
if (privateKey) {
key = new RSA(privateKey);
} else {
debug('generating a new key');
key = new RSA({b: 1024});
privateKey = key.exportKey('pkcs1-private');
}

// 6. zip plugin contents
return createContentsZip(pluginDir).then(contentsZip => {
// 7. sign to contents
const signature = sign(contentsZip, privateKey);

// 8. export public key
const publicKey = key.exportKey('pkcs8-public-der');

if (!ppkFile) {
// 9. calc UUID
const id = uuid(publicKey);
debug(`id: ${id}`);
// 10. output private key if nothing
fs.writeFileSync(path.join(outputDir, `${id}.ppk`), privateKey, 'utf8');
}
// 11. zip plugin
const outputPath = path.join(outputDir, 'plugin.zip');
return outputPlugin(outputPath, contentsZip, publicKey, signature);
});
});
}

module.exports = packer;

/**
* Create contents.zip
*
* @param {string} pluginDir
* @return {!Promise<!Buffer>}
*/
function createContentsZip(pluginDir) {
return new Promise((res, rej) => {
const output = new streamBuffers.WritableStreamBuffer();
const archive = archiver('zip');
output.on('finish', () => {
debug(`contents.zip: ${archive.pointer()} bytes`);
res(output.getContents());
});
archive.pipe(output);
archive.on('error', e => {
rej(e);
});
archive.directory(pluginDir, '.');
archive.finalize();
const signature = sign(contentsZip, privateKey);
const publicKey = key.exportKey('pkcs8-public-der');
const id = uuid(publicKey);
debug(`id : ${id}`);
return zip(contentsZip, publicKey, signature)
.then(plugin => ({plugin: plugin, privateKey: privateKey, id: id}));
});
}

/**
* Create and save plugin.zip
* Create plugin.zip
*
* @param {string} outputPath
* @param {!Buffer|!stream.Readable|string} contentsZip
* @param {!Buffer|!stream.Readable|string} publicKey
* @param {!Buffer|!stream.Readable|string} signature
* @return {!Promise<undefined>}
* @return {!Promise<!Buffer>}
*/
function outputPlugin(outputPath, contentsZip, publicKey, signature) {
function zip(contentsZip, publicKey, signature) {
return new Promise((res, rej) => {
const output = fs.createWriteStream(outputPath);
const output = new streamBuffers.WritableStreamBuffer();
const archive = archiver('zip');
output.on('close', () => {
output.on('finish', () => {
debug(`plugin.zip: ${archive.pointer()} bytes`);
res(outputPath);
res(output.getContents());
});
archive.pipe(output);
archive.on('error', e => {
Expand All @@ -134,3 +59,5 @@ function outputPlugin(outputPath, contentsZip, publicKey, signature) {
archive.finalize();
});
}

module.exports = packer;
4 changes: 2 additions & 2 deletions packages/plugin-packer/src/sign.js
Expand Up @@ -3,9 +3,9 @@
const RSA = require('node-rsa');

/**
* @param {Buffer} contents
* @param {!Buffer} contents
* @param {string} privateKey
* @return {Promise<string>} signature
* @return {!Buffer} signature
*/
function sign(contents, privateKey) {
const key = new RSA(privateKey, 'pkcs1-private-pem', {signingScheme: 'pkcs1-sha1'});
Expand Down
@@ -1,8 +1,13 @@
'use strict';

const crypto = require('crypto');

const hex2a = require('./hex2a');

/**
* @param {!Buffer} publicKey
* @return {string}
*/
function uuid(publicKey) {
const hash = crypto.createHash('sha256');
hash.update(publicKey);
Expand Down

0 comments on commit 990f74e

Please sign in to comment.