Permalink
Browse files

revamped for command line use

  • Loading branch information...
0 parents commit 5f607fa1e90a9d9a9a25c373614e2a87821acf9f @jed jed committed Oct 11, 2011
Showing with 453 additions and 0 deletions.
  1. +20 −0 LICENSE.txt
  2. +144 −0 README.md
  3. +91 −0 bin/crx.js
  4. +1 −0 index.js
  5. +19 −0 package.json
  6. +167 −0 src/crx.js
  7. BIN test/myFirstExtension/icon.png
  8. +11 −0 test/myFirstExtension/manifest.json
20 LICENSE.txt
@@ -0,0 +1,20 @@
+Copyright (c) 2011 Jed Schmidt, http://jed.is/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
144 README.md
@@ -0,0 +1,144 @@
+crxmake
+=======
+
+crxmake is a [node.js](http://nodejs.org/) module for packing and serving Google Chrome extensions.
+
+## Requirements
+
+* [node.js](http://nodejs.org/), tested with 0.4.12
+* openssl
+* zip
+
+## Install
+
+ $ npm install crxmake
+
+## API
+
+### Constructor
+
+#### crx = new ChromeExtension(options, [callback])
+
+Returns a `ChromeExtension` instance. If an optional callback is provided, the `load` method is called.
+
+### Methods
+
+#### crx.load([callback])
+
+Loads data for the instance. If the instance has a `package` key, the `loadFromPackage` method is called. If the instance has a `sourcePath` key, `loadFromSourcePath` is called.
+
+#### crx.loadFromPackage([callback])
+
+Populates the instance based on the contents of the `package` buffer.
+
+#### crx.loadFromSourcePath([callback])
+
+Populates the instance based on the directory at `sourcePath` and the `privateKey` buffer.
+
+#### crx.generateAppId()
+
+Uses a hash of the public key to generate the app ID used to uniquely identify the extension, and caches it in the `appID` property.
+
+#### crx.generateUpdateXml()
+
+Calculates the app ID and pulls the version and update URL from the manifest, and returns an XML file that can be served to enable autoupdates, as described [here](http://code.google.com/chrome/extensions/autoupdate.html#H2-2).
+
+### Properties
+
+#### crx.package
+
+A buffer containing the source of the extension, which can be served as the `.crx` file.
+
+#### crx.publicKey
+
+The public key for the extension, which is generated from `privateKey` when the extension is built.
+
+#### crx.privateKey
+
+The private key for the extension. This is used to generate the public key and sign the package.
+
+#### crx.version
+
+The version of the extension. This is currently fixed at `2`.
+
+#### crx.signature
+
+A cryptographic signature used to verify that the private key was used to sign the package.
+
+#### crx.contents
+
+A zip file representing the extension's source tree.
+
+#### crx.manifest
+
+An object parsed from the extensions `manifest.json` file.
+
+## Example
+
+```javascript
+// from ./test/test.js
+
+var fs = require("fs")
+ , assert = require("assert")
+ , join = require("path").join
+ , http = require("http")
+
+ , ChromeExtension = require("../")
+
+ , extPath = join(__dirname, "myFirstExtension")
+ , crxPath = extPath + ".crx"
+ , key = fs.readFileSync(extPath + ".pem")
+
+// create an extension with the existing key
+new ChromeExtension({sourcePath: extPath, privateKey: key}, function(err, fromPath){
+ // make sure no error occurred and that something was returned
+ assert.ok(!err)
+ assert.ok(!!fromPath)
+
+ // make sure that the sizes and names are the same
+ assert.equal(fromPath.publicKey.length, 162)
+ assert.equal(fromPath.signature.length, 128)
+ assert.equal(fromPath.manifest.name, "My First Extension")
+
+ // use the created extension to create a new instance
+ new ChromeExtension({package: fromPath.package}, function(err, fromPackage) {
+ // make sure no error occurred and that something was returned
+ assert.ok(!err)
+ assert.ok(!!fromPackage)
+
+ // make sure that the public keys are the same
+ assert.equal(
+ fromPath.publicKey.toString(),
+ fromPackage.publicKey.toString()
+ )
+
+ // make sure that the signatures are the same
+ assert.equal(
+ fromPath.signature.toString(),
+ fromPackage.signature.toString()
+ )
+
+ // make sure that the contents are the same
+ assert.equal(
+ fromPath.contents.length,
+ fromPackage.contents.length
+ )
+
+ // write the extension to disk for further testing
+ fs.writeFile(crxPath, fromPath.package, function() {
+ console.log("Open the following extension for further testing:\n%s", crxPath)
+ })
+ })
+})
+```
+
+## TODO
+
+* Find out how to generate packages without keys and obtain a `.pem` file
+
+Copyright
+---------
+
+Copyright (c) 2011 Jed Schmidt. See LICENSE.txt for details.
+
+Send any questions or comments [here](http://twitter.com/jedschmidt).
91 bin/crx.js
@@ -0,0 +1,91 @@
+#!/usr/bin/env node
+
+var path = require("path")
+ , fs = require("fs")
+ , child = require("child_process")
+
+ , program = require("commander")
+ , ChromeExtension = require("..")
+
+ , resolve = path.resolve
+ , join = path.join
+ , spawn = child.spawn
+ , exec = child.exec
+
+ , cwd = process.cwd()
+
+program
+ .version("0.2.0")
+ .option("-f, --file [file]", "input/output <file> instead of stdin/stdout")
+ .option("-p, --private-key <file>", "relative path to private key [key.pem]")
+ // coming soon
+ // .option("-x, --xml", "output autoupdate xml instead of extension ")
+
+program
+ .command("keygen [directory]")
+ .description("generate a private key in [directory]/key.pem")
+ .action(keygen)
+
+program
+ .command("pack [directory]")
+ .description("pack [directory] into a .crx extension")
+ .action(pack)
+
+// program
+// .command("unpack [directory]")
+// .description("unpack a .crx extension into a directory")
+// .action(unpack)
+
+program.parse(process.argv)
+
+function keygen(dir, cb) {
+ dir = resolve(cwd, dir)
+
+ var key = join(dir, "key.pem")
+
+ path.exists(key, function(exists) {
+ if (exists) return cb && cb()
+
+ var pubPath = key + ".pub"
+ , command = "ssh-keygen -N '' -b 1024 -t rsa -f key.pem"
+
+ exec(command, {cwd: dir}, function(err) {
+ if (err) throw err
+
+ // TODO: find a way to prevent .pub output
+ fs.unlink(pubPath)
+ cb && cb()
+ })
+ })
+}
+
+function pack(dir) {
+ var input = resolve(cwd, dir)
+ , output =
+ program.file === true ? input + ".crx" :
+ program.file ? resolve(cwd, program.file) : false
+
+ , stream = output ? fs.createWriteStream(output) : process.stdout
+ , key = program.privateKey
+ ? resolve(cwd, program.privateKey)
+ : join(input, "key.pem")
+
+ , crx = new ChromeExtension
+
+ fs.readFile(key, function(err, data) {
+ if (err) keygen(dir, pack.bind(null, dir))
+
+ crx.privateKey = data
+
+ crx.load(input, function(err) {
+ if (err) throw err
+
+ this.pack(function(err, data){
+ if (err) throw err
+
+ stream.end(data)
+ this.destroy()
+ })
+ })
+ })
+}
1 index.js
@@ -0,0 +1 @@
+module.exports = require("./src/crx.js")
19 package.json
@@ -0,0 +1,19 @@
+{
+ "author": "Jed Schmidt <tr@nslator.jp> (http://jed.is)",
+ "name": "crx",
+ "description": "Build Google Chrome extensions with node.js",
+ "version": "0.1.1",
+ "homepage": "https://github.com/jed/crx",
+ "bin": {
+ "crx": "./bin/crx.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/jed/crx.git"
+ },
+ "engines": {
+ "node": "~0.4.12"
+ },
+ "dependencies": {},
+ "devDependencies": {}
+}
167 src/crx.js
@@ -0,0 +1,167 @@
+var fs = require("fs")
+ , join = require("path").join
+ , crypto = require("crypto")
+ , child = require("child_process")
+ , spawn = child.spawn
+ , exec = child.exec
+
+module.exports = new function() {
+ function ChromeExtension() {
+ this.path = join("/tmp", "crx-" + (Math.random() * 1e17).toString(36))
+ }
+
+ ChromeExtension.prototype = this
+
+ this.destroy = function() {
+ spawn("rm", ["-rf", this.path])
+ }
+
+ this.pack = function(cb) {
+ this.loadManifest(function() {
+ this.generatePublicKey(function() {
+ this.loadContents(function() {
+ this.generateSignature()
+ this.generatePackage()
+
+ cb.call(this, null, this.package)
+ })
+ })
+ })
+ }
+
+ this.load = function(path, cb) {
+ fs.stat(path, function(err, stat) {
+ if (stat.isDirectory()) this.loadFromDir(path, cb)
+
+ else if (stat.isFile()) this.loadFromFile(path, cb)
+ }.bind(this))
+ }
+
+ this.loadFromDir = function(path, cb) {
+ spawn("cp", ["-R", path, this.path]).on("exit", cb.bind(this))
+ }
+
+ this.readFile = function(name, cb) {
+ var path = join(this.path, name)
+
+ fs.readFile(path, "binary", function(err, data) {
+ if (err) return cb.call(this, err)
+
+ cb.call(this, null, this[name] = data)
+ }.bind(this))
+ }
+
+ this.writeFile = function(path, data, cb) {
+ path = join(this.path, path)
+
+ fs.writeFile(path, data, function(err, data) {
+ if (err) return cb.call(this, err)
+
+ cb.call(this)
+ }.bind(this))
+ }
+
+ this.loadFromFile = function(path, cb) {
+ fs.readFile(path, function(err, data) {
+ if (err) return cb.call(this, err)
+
+ path = this.path + ".zip"
+ data = data.slice(16 + crx[8] + crx[12])
+
+ fs.writeFile(path, data, function(err) {
+ if (err) return cb.call(this, err)
+
+ spawn("unzip", [path], {dir: this.path}, function() {
+ fs.unlink(path)
+ cb.call(this)
+ })
+ }.bind(this))
+ }.bind(this))
+ }
+
+ this.loadManifest = function(cb) {
+ this.readFile("manifest.json", function(err, data) {
+ if (!err) {
+ try { this.manifest = JSON.parse(data.toString()) }
+ catch (e) { err = e }
+ }
+
+ err
+ ? cb.call(this, err)
+ : cb.call(this, null, this.manifest)
+ })
+ }
+
+ this.generatePublicKey = function(cb) {
+ var rsa = spawn("openssl", ["rsa", "-pubout", "-outform", "DER"])
+
+ rsa.stdout.on("data", function(data) {
+ this.publicKey = data
+ cb && cb.call(this, null, this)
+ }.bind(this))
+
+ rsa.stdin.end(this.privateKey)
+ }
+
+ this.generateSignature = function() {
+ return this.signature = new Buffer(
+ crypto
+ .createSign("sha1")
+ .update(this.contents)
+ .sign(this.privateKey),
+
+ "binary"
+ )
+ }
+
+ this.loadContents = function(cb) {
+ var command = "zip -qr -9 -X - . -x key.pem"
+ , options = {cwd: this.path, encoding: "binary"}
+
+ exec(command, options, function(err, data) {
+ if (err) return cb.call(this, err)
+
+ this.contents = new Buffer(data, "binary")
+
+ cb.call(this)
+ }.bind(this))
+ }
+
+ this.generatePackage = function() {
+ var signature = this.signature
+ , publicKey = this.publicKey
+ , contents = this.contents
+
+ , keyLength = publicKey.length
+ , sigLength = signature.length
+ , zipLength = contents.length
+ , length = 16 + keyLength + sigLength + zipLength
+
+ , crx = new Buffer(length)
+
+ crx.write("Cr24" + Array(13).join("\x00"), "binary")
+
+ crx[4] = 2
+ crx[8] = keyLength
+ crx[12] = sigLength
+
+ publicKey.copy(crx, 16)
+ signature.copy(crx, 16 + keyLength)
+ contents.copy(crx, 16 + keyLength + sigLength)
+
+ return this.package = crx
+ }
+
+ this.generateAppId = function() {
+ return this.appId = crypto
+ .createHash("sha256")
+ .update(this.publicKey)
+ .digest("hex")
+ .slice(0, 32)
+ .replace(/./g, function(x) {
+ return (parseInt(x, 16) + 10).toString(26)
+ })
+ }
+
+ return ChromeExtension
+}
BIN test/myFirstExtension/icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 test/myFirstExtension/manifest.json
@@ -0,0 +1,11 @@
+{
+ "name": "My First Extension",
+ "version": "1.0",
+ "description": "The first extension that I made.",
+ "browser_action": {
+ "default_icon": "icon.png"
+ },
+ "permissions": [
+ "http://api.flickr.com/"
+ ]
+}

0 comments on commit 5f607fa

Please sign in to comment.