node.jsでPassbookサーバをつくってみる(4)

もはや、筆者にとってライフワークとなりつつあるこの連載。なぜ、連載形式にしてしまったのかを後悔しつつ第4回目です。

今回はこれまで作ったコードをとりまとめ、実際に使えるようにリファクタリングと必要な処理を追加してPassbookサービスを作るためのベースとなるようにします。

まずはmanifest.jsonを作る処理と署名をする処理をメインコードから分離します。署名する処理はさっくり分離、manifest.jsonを作る処理はちょっと考えて以下のようなコードにしました。

manifest.js

var crypto = require('crypto');

HASH_ALGO = 'sha1';
HASH_FORMAT = 'hex';

//
// contentsはObject(Map)を想定
//
exports.createManifest = function(contents) {
  var manifestHash = {};

  for (var name in contents) {
    // SHA1 ハッシュの計算
    var sha1sum = crypto.createHash(HASH_ALGO);
    sha1sum.update(contents[name]);
    manifestHash[name] = sha1sum.digest(HASH_FORMAT);
  }

  return manifestHash;
}

signature.js

var path = require('path');
var child = require('child_process');

KEY_DIR = './keys/';

//
// manifest.jsonの署名を行う
//
exports.createSignature = function(manifest, options, callback) {
  // javascriptにはデフォルト引数がないのよ...
  if (typeof callback !== 'function') {
    callback = options;
    options = null;
  }

  var signer = null;
  var certfile = null;
  var passin = null;

  if (options) {
    signer = options['signer'];
    certfile = options['certfile'];
    passin = options['passin'];
  } else {
    signer = path.resolve(KEY_DIR, 'pass.pem');
    certfile = path.resolve(KEY_DIR, 'wwdr.pem');
    passin = 'pass:pass';
  }

  var args = [
    "smime",
    "-sign", "-binary",
    "-signer", signer,
    "-certfile", certfile,
    "-passin", passin
  ];

  var sign = child.execFile("openssl", args, { stdio: "pipe" }, function(error, stdout, stderr) {
    if (error) {
      callback(new Error(stderr));
    } else {
      var signature = stdout.split(/\n\n/)[3];
      callback(null, new Buffer(signature, "base64"));
    }
  });

  sign.stdin.write(manifest);
  sign.stdin.end();
}

これで処理を分離しました。
これら2つのファイルはlibディレクトリを作って、 放りこんでおきます。

次にpass.jsonやicon.pngなどのテンプレートを読み込む処理を分離します。ざっくり書いてみると以下のような感じになります。

template.js

var fs = require('fs');
var util = require('util');
var path = require('path');

TEMPLATE_DIR = './templates/';

var templateCache = null;

exports.readTemplate = function(templateDir, callback) {
  if (templateCache) {
    callback(null, templateCache);
    return;
  }

  fs.readdir(templateDir, function(err, files) {
    if (err) {
      callback(err, null);
    } else {
      var contents = {};
      files.forEach(function(file) {
        // file読み込みはsyncで
        contents[file] = fs.readFileSync(TEMPLATE_DIR + file);
        util.log(util.inspect(contents[file]));
      });
      templateCache = contents;
      callback(null, contents);
    }
  });
};

テンプレートファイル読み込みのところも非同期にしたいところですが、コーディングの手間の割には得られるメリットが少ないので、とりあえず同期に。
また、テンプレートはサーバ実行中は変更しないので、読み込んだデータはキャッシュしておくことにします。(ちょっとマズいコードですが…)

次にpass.jsonの中身を書き換える処理を追加します。
各Passを一意に識別するためのキーとして serialNumber があります。これを生成毎にユニークになるようにします。
pass.jsonはその名の通り、JSON形式なのでObject化してからserialNumberをupdateすることを意識して以下のようなコードにしました。

serial.js

var util = require('util');
var crypto = require('crypto');
var serial = exports;

function generateSerial() {
  var d = new Date();
  var randomData = new Buffer(crypto.randomBytes(32), 'binary');
  var gen = util.format('%d%s', d.getTime(), randomData.toString('base64'));
  return gen;
}

serial.generate = generateSerial;

serial.update = function(pass) {
  pass['serialNumber'] = generateSerial();
}

ついでにログ用のモジュールも作ってみました。

log.js

var util = require('util');
var log = exports;

log.inspect = function(obj) {
  util.log(util.inspect(obj));
};

log.log = function(mes) {
  util.log(mes);
};

あとはこれらを組み合わせて server.js を更新するだけです。
今回作成した各ファイルをlibディレクトリの中に入れて、以下のような配置にします。

.
├── keys
│   ├── pass.pem
│   └── wwdr.pem
├── lib
│   ├── log.js
│   ├── manifest.js
│   ├── serial.js
│   ├── signature.js
│   └── template.js
├── node_modules
│   └── node-zip
│       ├── README.md
│       ├── lib
│       │   └── nodeZip.js
│       ├── package.json
│       └── vendor
│           └── jszip
│               ├── jszip-deflate.js
│               ├── jszip-inflate.js
│               ├── jszip-load.js
│               └── jszip.js
├── server.js
└── templates
    ├── icon.png
    ├── logo.png
    └── pass.json

この配置にもとづいて server.js を更新して、HTTPでPassを生成するようにすると…

server.js

var util = require('util');
var sig = require('./lib/signature');
var man = require('./lib/manifest');
var template = require('./lib/template');
var log = require('./lib/log');
var serial = require('./lib/serial');
var http = require('http');

require('node-zip');

TEMPLATE_DIR = './templates/';

http.createServer(function(req, res) {
  log.log('request start');

  template.readTemplate(TEMPLATE_DIR, function(err, contents) {
    if (err) throw err;

    var pass = JSON.parse(contents['pass.json'].toString('utf8'));
    serial.update(pass);
    contents['pass.json'] = new Buffer(JSON.stringify(pass), 'utf8');

    var zip = new JSZip();
    for (var name in contents) {
      zip.file(name, contents[name].toString('binary'), {binary: true});
    }

    var manifest = JSON.stringify(man.createManifest(contents));
    zip.file('manifest.json', manifest, {binary: true});

    sig.createSignature(manifest, function(error, signature) {
      if (error) {
        throw err;
      } else {
        zip.file('signature', signature.toString('binary'), {binary:true});

        res.writeHead(200, {'Content-Type': 'application/vnd.apple.pkpass'});
        res.end(zip.generate({base64:false}), 'binary');
        log.log('request end');
      }
    });
  });
}).listen(8888, '127.0.0.1');

となります。
ここまでこれば、あとはnodeコマンドでサーバを起動するだけ。起動後、iOSシミュレータで http://localhost:8888/ にアクセスするとPassが取得できるようになっています。

あとはPassbookアプリとのI/Fを組込む必要がありますが、それはまた次回にでも….

なお、今回作ったソースは https://bitbucket.org/tetsuo/passbook-server で公開してます。
実際に試してみたいかたはAppleのデベロッパーサイトでPassbook用の証明書を取得して、keysディレクトリに証明書つっこんで、templatesディレクトリ内のpass.jsonファイルを適宜書き換えれば試せますので、ぜひ。

tetsuo

tetsuo の紹介

こう見えてテックファームの開発チームのえらい人。
カテゴリー: iOS タグ: , , , パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です