2013年5月17日金曜日

自前でWevDAVサーバを立てて、DropboxをWebDAV化する

さくら VPS に Dropbox をインストールして WebDAV 化する | 澍法雨に感動した。一言で言うと、LinuxサーバをDropboxのWevDAVゲートウェイにするという技である。記事タイトルにはさくらVPSとあるが、別にさくらVPSでなくても、普通のLinuxサーバでよい。
この手を使えば、OtixoとかDropDavとかの有料サービスを使わなくても、DropboxにWebDAV経由でアクセスできる。
WebDAVの何がイイかって、普通のDropboxクライアントアプリと違ってPCのローカルにファイルが残らないし、Proxyだって超えられちゃう。
Windows7ならWebDAVをネットワークドライブとしてマウントできるので、使い勝手もローカルフォルダと同じ。こいつぁすげえ。
早速、手持ちのCentOSサーバでやってみた。
LinuxのCLI版Dropboxクライアントを入れる
CLI版Dropboxはユーザー単位でインストールする仕様である。 Dropbox をダウンロード - Dropbox に従いインストールする。ちなみに今回は32bit。
[dsp74118@hoge]$ cd ~ && wget -O - "https://www.dropbox.com/download?plat=lnx.x86" | tar xzf -
管理用CLIスクリプトもダウンロードし、PATHが通ってるとこ(/usr/binとか)に置く。
[root@hoge]# wget https://www.dropbox.com/download?dl=packages/dropbox.py
[root@hoge]# chmod +x dropbox.py
[root@hoge]# mv dropbox.py /usr/bin/
サーバ起動時にDropboxデーモンを自動起動させるため、 DropboxをLinuxで利用する方法 - maruko2 Note.から /etc/init.d/dropbox と /etc/sysconfig/dropbox を頂戴する。

chkconfigで自動起動をonにする。
[root@hoge]# chkconfig --add dropbox
[root@hoge]# chkconfig --list dropbox
dropbox         0:off   1:off   2:on    3:on    4:on    5:on    6:off
起動してみる。
[root@hoge]#  service dropbox start
dropboxd を起動中                                        [  OK  ]
Dropboxを使う時のお約束として、lansync(LAN同期)をOffにする。lansyncとは同一セグメント内のDropbox間でファイルを同期するという機能だが、こいつがOnになっていると周囲にUDP17500番のブロードキャストを撒き散らしてお行儀が悪いため、自宅などの狭い環境で使う時以外は必ずOffっておくのがマナーだ。
設定は、Dropboxクライアントが動いている状態で、ユーザー毎に行う。設定内容は~/.dropboxの下のDBに書き込まれている模様(暗号化されててDBの中は見れない)。
[dsp74118@hoge]$ dropbox.py lansync n
ここまででDropbox側の設定は終わり。
Dropbox同期ディレクトリを、WebDAV用ディレクトリに移す
WebDAV化する前に少し下準備をする。
CLI版のDropboxクライアントは、ユーザーホーム(~/)の下にDropboxというディレクトリを作って、そこがDropboxサーバと同期する仕様になっている。
これではWebDAV化するには色々と不都合なので、WebDAV用ディレクトリ配下に移すこととする。
今回は、/data/webdav/ ディレクトリをWebDAVとして公開し、その配下にユーザー別のディレクトリを、さらにその配下にDropboxディレクトリを作る方針とした。
例えば、dsp74118ユーザのDropbox同期ディレクトリは、/data/webdav/dsp74118/Dropbox/ となる。
Linux版で同期ディレクトリの場所を変える方法はいくつかあるようだが、ひとまず単純にシンボリックリンクを使う方法を採用。
Dropboxクライアントを止めた状態で~/Dropboxを/data/webdav/dsp74118 に移動させ、ユーザーホームからシンボリックリンクを張ってやる。
[root@hoge]# service dropbox stop
dropboxd を停止中                                          [  OK  ]
[root@hoge]# mkdir -p /data/webdav/dsp74118
[root@hoge]# mv /home/dsp74118/Dropbox /data/webdav/dsp74118/
[root@hoge]# ln -s /data/webdav/dsp74118/Dropbox /home/dsp74118/
[root@hoge]# service dropbox start
dropboxd を起動中                                        [  OK  ]
このディレクトリには、利用ユーザー本人とapacheの両方が読み書きアクセスできる必要があるので、オーナーとパーミッションを変更する。
[root@hoge]# chown -R dsp74118:apache /data/webdav/dsp74118
[root@hoge]# chmod -R 770 /data/webdav/dsp74118
ApacheでWebDAVを動かす
ではいよいよWebDAVを動かす。 今回はポートベースのバーチャルホストでWebDAVを動かすことにした。
httpd.confに↓みたいに書いて
(snip)
LoadModule dav_module modules/mod_dav.so
LoadModule dav_fs_module modules/mod_dav_fs.so
(snip)
<VirtualHost *:30080>
    ServerName hogedav
    DocumentRoot /data/webdav
    <Directory /data/webdav>
         DAV on
    </Directory>
</VirtualHost>
(snip)
Apacheを再起動する。
[root@hoge]# service httpd restart
※本当はアクセス制御をしたり、httpsにするべきだと思うけど、とりあえずテストなのでその辺は手抜き(本稿の趣旨でもないので)。
早速試してみる
では、WindowsマシンにてWebDAVなDropboxを体験してみる。 まずはネットワークドライブとしてマウントする。
Figure.1 WebDAVをネットワークドライブとしてマウント
マウントしたドライブを開く。ちゃんと"Dropbox"ディレクトリが見える。
Figure.2 WebDAV内のDropboxフォルダ
Dropboxフォルダの中はというと…。ちゃんとDropboxと同期されていることが分かる。
Figure.3 中身はバッチリDropboxと同期されている
では、書き込みのテストをする。"WebDavTest"フォルダを作成し、"てすてす.txt"ファイルを保存してみる。
Figure.4 テストファイルを作成
作成したファイルがちゃんとサーバに同期されたか、DropboxのWeb UIで確認してみる。
Figure.5 テストファイルをWeb UIで確認
OK、OK。 この後、ファイル削除もやってみたが、何の問題もなく成功した。
まとめ
それほど手間もかからず、DropboxがWebDAVストレージになった。うーん便利。
あ、実際に使う時は、https化とアクセス制御は絶対にやってください。でないとDropbox内のファイルが世間にフルオープンになっちゃうので。

まあ、ライバルのSkyDriveは元からWebDAVが使えたりするんだけど…。

2013年5月12日日曜日

AC版グラディウスⅡ備忘録

最近グラⅡのリハビリを始めたので、過去の記憶が曖昧な部分を文章に起こしておくこととする。
3速で自機を画面一番下まで移動させる方法(Y座標調整)
3速でレバーを真上または真下に入れると、自機は1フレームごとに所定のドット数(多分3ドットだけど確証がないので、本文ではNドットとする)移動する。つまり、真上や真下にはNドットの倍数しか移動できない(1ドット単位の調整不可)。
自機と画面下端との距離がNドット以下の場合、レバーを真下に入れても自機はそれより下に動かない(画面の一番下まで行くことができず隙間ができたままとなる)。
Figure.1 この状態でレバーを真下に入れてもそれ以上下に行けない

3速で自機を画面の一番下に移動させるには、斜め移動を織り交ぜる必要がある。グラⅡは真上・真下への移動と斜め移動とでY座標の移動速度が異なり、斜め移動の際に1フレームで移動するY座標はNドットではないので、ドット単位の調整が可能となる。
具体的にはレバーを斜め上(左上or右上)にチョンと入れた後、真下に移動すればよい。ビックバイパーの腹とパワーアップゲージが密着したら成功。※隙間がなくなるまで繰り返すこと。
Figure.2 Y座標調整成功
一度自機が画面下まで行けるようになったら、以降斜め移動をせず上下左右の4方向にだけ移動している限り、Y座標の移動距離はNドットの倍数なので、「一番下まで行ける状態」をキープすることができる。※レバーを斜めに入れると、またY座標がズレてしまい、一番下まで行けない状態に戻ってしまう。
オプションハンター
■1周目
2面の前衛の最後の蛇行編隊が出てくる寸前に4つ目のオプションを取る。
クリスタルコアを自爆寸前(レーザー連射4回の後、単発レーザーを2回撃った時)に倒すと、4面前衛でオプハンが出る。
※クリスタルコアを自爆させると、ステージ切り替えでオプハンを消せるが、ボスの点数がもったいない。

■2周目以降
3面の音楽が1ループした後、2ループ目の第1小節の終わりで4つ目のオプションを取る。
クリスタルコアを、1回目のレーザー連射の後の単発レーザーを6回撃った後に倒すと、ステージ切り替えでオプハンを消せる。

■以後、1周目・2周目以降共通
4面ボスの第1形態を速攻で倒し、第2形態でオプハンが出てくるまで待つ。1個食わす。
5面の音楽が1ループした後、2ループ目の第2小節の終わりで4つ目のオプションを取る。
5面ジャンプモアイ終了直後にオプハンが出るので、スルー。
7面ビッグコア戦でオプハンが出るまで粘り、スルー。ビッグコアをすぐ倒す。以後、ゴーレム、テトランを速攻で倒す。ガウ第1形態は自爆させ、第2形態は速攻で倒す。ファイアードラゴンに死亡寸前までダメージを与えておき、オプハンが出るまで粘り、出てきたらファイアードラゴンをすぐ倒す。オプハンはスルー。
8面中ボス寸前で出てくるので、スルー。中ボス自爆させる。
8面クラブ寸前で出てくるので、1個食わす。そのままクリアする。

以後、各面の忘れがちなネタ。
1面
1匹目のドラゴンの出現位置は、前衛最後の上の編隊をレーン変更後に全滅させた場合、その赤カプセルよりちょっと上。背景の赤い星も目安になる。
2面
中盤で青カプセルを出すと稼げるが、調整方法要検討。(めぞん一刻氏の攻略本に書いてあったはずだが内容失念)
5面
■オプション4個の時のジャンプモアイのフォーメーション
画面中央やや下あたりから左→上→右下と動いて下図のようなフォーメーションを貼る。自機の位置も下図参照。
Figure.3 ジャンプモアイ戦のフォーメーション
基本的にリップルのみ撃つが、スタート時に右下にいたジャンプモアイが左にジャンプして自機の真下に来た時はミサイルも撃つ。
ジャンプモアイの自爆タイミングは10回目のジャンプの着地寸前。

■復活(残機潰し)の時の自機の位置

Figure.4 残機潰しのX座標合わせ(SPEEDUPを目安にする)
Y座標は上のモアイの頭スレスレがいいみたい。
あと、連付でボタン押しっぱなしだと、イオンリングの切れ目でショットが2発とも右に抜けてしまい死ぬことがあるので、イオンリングが途切れる時はショットを撃ちやめること。
Figure.5 残機潰しのY座標
■ボス
下の口にオプションを食わす場合、SPEEDUPとMISSILEのちょうど間にする。
Y座標調整をすると下の口を早く発狂させられるというネタがあったはずだが、失念。(これもめぞん一刻氏の攻略本ネタ)
7面
■ガウ第2形態を画面一番下でハメる
Y座標調整をして自機を画面一番下に移動させると、ガウが画面一番下でハマり、第2形態のレーザーに対して安地になる。
ガウ第1形態の最中にY座標調整をするのは厳しいので、テトラン戦の前にやっておく。テトランとガウ第1形態は斜め移動せずに戦う。
8面
■8面の壁剥がれ地帯に青カプセルが出るよう調整する
ぐらにどっとこむの中の人の動画で知ったパターン。6面で青カプセルが出た後、赤カプセルを3つ出した状態でクリアし、7面赤ザブを全部倒せばOK。赤ザブ戦で青カプセルを取ってしまうと余計な赤カプセルが出てしまって調整が狂うと思う。

2013年5月10日金曜日

ownCloud5.0.5+クライアント1.2.5で複数アカウント・複数PC間のファイル共有を試す

約1年越しでownCloudを触ってみてるのでレポートする。

ownCloud5.0で漸く実用レベルに?

昨年はownCloud4.0とWindows版クライアントアプリ1.0.3で検証していたが、そこから情報更新が滞ったのには訳がある。実のところ、複数のPC間でファイル共有を試してみたところ、ファイル更新時にうまく同一ファイルと認識されずコンフリクト扱いとなってしまう現象が頻繁に発生した。これでは使いものにならないので、検証をやめていたのだ。

今年の3月にownCloudが5.0になり、クライアントアプリも先月1.2.5になったので、再度試してみた。

複数アカウントでのファイル共有を検証

昨年のエントリ自前でDropBoxもどきを作れるownCloudを入れてみたの冒頭で述べたように、私がownCloudに期待しているのは、おじさんでも使えるリポジトリを構築することである。セキュリティポリシー的にDropBox等のパブリックなサービスを使えない企業は多いと思うので、オンプレミスで構築できることが重要だ。
ownCloudをリポジトリ的に使うためには、複数のアカウントで同一ファイルを共有し、お互いが更新できる必要がある。また、履歴管理も必須だ。
このようなシナリオを想定して検証をスタート。

検証環境を下記のように定める。
サーバ
ownCloud
5.0.5

クライアント
PCOSクライアントアプリアカウント
PC1Windows71.2.5dsp74118
PC2Windows71.2.5member1

ownCloudのWeb UIの管理画面でアカウントを作成。
Figure.1 アカウントの作成
アカウント"dsp74118"の方で、Web UIにてフォルダを作成。名前は"share"とした。member1との共有設定を行う。
Figure.2 フォルダの共有設定
PC1にて、共有フォルダ"share"をローカルフォルダと同期させる。ここでは"D:\share"とした。
Figure.3 PC1の同期設定
検証用に適当な文書を作ってみた。Excelおじさんらしく、Excelの台帳的なものにする。
Figure.4 検証に使うドキュメントの初版
検証用文書を、PC1のローカル同期フォルダに保存。
Figure.5 ローカル同期フォルダに文書を保存
しばらく待つと、PC1のクライアントアプリによって、文書がowncloudサーバに同期(アップロード)される。下図はWeb UIにてファイルの存在を確認したところ。
Figure.6 ファイルが同期された様子
では、共有フォルダがmember1でどのように見えるか確認する。
member1のWeb UIを見ると、"Shared"というフォルダが自動作成されている。
Figure.7 Sharedフォルダが自動作成された様子
"Shared"の中を見ると、dsp74118アカウントが共有設定を行った"share"フォルダが確認できる。
Figure.8 Sharedフォルダ内に共有フォルダが見える
PC2にてローカルフォルダとの同期設定を行う。リモートパスは、↑で確認したように"/Shared/share"となる。ここではローカルの"D:\ownCloudShare"と同期するよう設定した。
Figure.9 PC2の同期設定
しばらく待つと、ローカル同期フォルダに、dsp74118アカウントが作成したファイルが同期(ダウンロード)されてきた。
Figure.10 共有フォルダ内のファイルが無事ダウンロードされた。
では、PC2側でこのドキュメントを開き、更新してみる。下図の赤字部分を修正し、上書き保存する。
Figure.11 PC2にてドキュメントを更新
しばらく待つと…。あれれー、PC2のクライアントアプリがエラーを吐いたぞ、どうなってる。しかし、ファイル名の右には「アップロード済み」の表示が。????訳が分からない。同期(アップロード)できたのかい? できなかったのかい? どっちなんだい!
Figure.12 クライアントアプリで何かエラーが出た
しばらく待ってからPC1のクライアントアプリを確認すると、「同期されたファイル」に「ダウンロード済み」と出てきた。どうやらPC2側の同期(アップロード)処理は無事成功し、PC1への同期(ダウンロード)も行われたようだ。
Figure.13 PC1にてダウンロード完了
PC1上のファイル。Figure.5と比較すると、更新日時が変わったことが確認できる。
Figure.14 PC1上のファイルの更新日付が変わった
PC1でファイルを開いてみると、ちゃんと更新版になってた。
Figure.15 PC1でファイルを開いてみたところ
履歴機能を検証
次に、履歴機能を検証してみる。過去バージョンを取り出す操作は、Web UIでしか行えない。この辺りもDropBoxと同じだ。
履歴機能には2通りの使い方がある。
  1. Web UIにて任意のバージョンのファイルを指定してダウンロードする
  2. サーバ上のファイルを任意のバージョンのファイルに戻す(revert)
【その1 Web UIにて任意のバージョンのファイルを指定してダウンロードする場合】
下図のように、ファイル名の「バージョン」リンクをクリックし、取り出したいバージョンのタイムスタンプを選択する。ここでは初版のファイルを指定してみる。
※なお、過去バージョンがない(初回アップロード後に一度も更新されていない)ファイルの場合は"No older versions available"と表示される。
Figure.16 ダウンロードしたいバージョンを指定
その後、"ダウンロード"をクリックすると、ダウンロードダイアログが出てくるので、ローカルの適当なところに保存する。
Figure.17 ファイルのダウンロード
ダウンロードしたファイルを開くと、ちゃんと初版が取り出せたことが分かる。
Figure.18 古いファイルがダウンロードできたことを確認
【その2 サーバ上のファイルを任意のバージョンのファイルに戻す(revert)】
 次に、サーバ上のファイルを過去バージョンに戻す検証をする。"All versions"をクリック。
Figure.19 All versionsをクリック
 戻したいバージョンの"Revert"ボタンをクリック。ここでは初版を選ぶ。
Figure.20 任意のバージョンにRevert
その後、"ダウンロード"をクリックすると、初版のファイルがダウンロードできることが確認できた。

では、クライアントアプリはどう動くか?
Figure.21 PC1上のファイルの更新日付が変わらない
…あれ。ファイルの更新日付は、新しいバージョンのままだ。初版に戻ってくれない。
これじゃイマイチだなー。クライアントのローカルファイルも古いバージョンに戻ってほしいわ。

所感
同期できてるのにエラーと表示されたり、まだまだ不安定な感じが否めない。
サーバ上のファイルを旧バージョンにRevertした時にクライアントのファイルが旧バージョンに戻らないのもイマイチだが、技術的に難しいのだろうか。DropBoxとかだとどうだっけ。(試してない…)
あと、今回の検証では大量ファイルのテストをしていないが、ファイル数が多いと同期にめっちゃ時間がかかる。これも実用を考えると結構厳しい。
惜しいところまで来てるけど、まだ実戦投入には不安が残るように感じた。自分一人で使うならいいんだけど。更なる品質向上に期待したい。

2013年5月6日月曜日

Vimperatorプラグインnextlink.jsを動くようにした(WIP)

先日の記事の続き。とりあえず、一部サイトで動くようになったので、公開する。
あまりちゃんとテストしてないが、Googleの検索結果、@IT、Bloggerあたりで正常動作確認済み。動かないサイトはITProなど。まだ追求はしていない。2013.05.07 ITProで動かない理由は判明(文末に追記)
直し方はかなり「無理やり」であり、ホントに「とりあえず動く」だけなので、公式(https://github.com/vimpr/vimperator-plugins)へのpull requestはしないつもり。versionは"0.3.9 altered"としておいた。
HTMLをDOMとしてパースする処理はhttp://jsdo.it/kjunichi/qDCfあたりを参考にさせていただいた。

根本的に直すのであれば_libly.jsの修正が必要だと思うが、流石に影響範囲が広すぎるし、そもそもそういう直し方でいいのかという議論もあると思うので、俺みたいな素人ではなくvimprのコアなメンバーの皆様のご判断が必要と考えている。
/*** BEGIN LICENSE BLOCK {{{
  Copyright (c) 2008 suVene

  Released under the GPL license
  http://www.gnu.org/copyleft/gpl.html
}}}  END LICENSE BLOCK ***/
// PLUGIN_INFO//{{{
var PLUGIN_INFO = xml`

  nextlink
  mapping "[[", "]]" by AutoPagerize XPath.
  AutoPagerize 用の XPath より "[[", "]]" をマッピングします。
  suVene
  hogelog
  dsp74118の補完庫
  0.3.9 altered
  GPL
  2.2pre
  https://github.com/vimpr/vimperator-plugins/raw/master/nextlink.js
  <![CDATA[
== Needs Library ==
- _libly.js(ver.0.1.38)
  @see http://coderepos.org/share/browser/lang/javascript/vimperator-plugins/trunk/_libly.js

== Option ==
>||
  let g:nextlink_followlink = "true"
||<
と設定することにより、"[[", "]]" の動作は、カレントのタブに新しくページを読み込むようになります。

>||
  let g:nextlink_prevmap = "[n"
  let g:nextlink_nextmap = "]n"
||<
のように設定することにより、"[[", "]]" 以外のキーに割り当てることができます。

SITEINFOが無い場合の処理を
>||
  let g:nextlink_nositeinfo_act = "f"
||<
のように設定できます。現在は
f:
  Vimperatorの"[[", "]]"の動作
e:
  マッチするSITEINFOが無いことを知らせる(デフォルト設定)
n:
  何もしない
が設定可能です

/info//nextlink-local-siteinfo に
>||
[
  {
    "url":          "^http://[^.]+\\.google\\.(?:[^.]+\\.)?[^./]+/search\\b",
    "nextLink":     "id('navbar')//td[last()]/a",
    "pageElement":  "id('res')/div",
    "exampleUrl":   "http://www.google.com/search?q=nsIObserver",
  },
]
||<
のような JSON を置くことでローカルで SITEINFO を設定できます

== TODO ==
  ]]>
`;
//}}}
liberator.plugins.nextlink = (function() {

// initialize //{{{
if (!liberator.plugins.libly) {
  liberator.log("nextlink: needs _libly.js");
  return;
}

var libly = liberator.plugins.libly;
var $U = libly.$U;
var logger = $U.getLogger("nextlink");
var $H = Cc["@mozilla.org/browser/global-history;2"].getService(Ci.nsIGlobalHistory2);
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
const UUID = "{3b72c049-a347-4777-96f6-b128fc76ed6a}"; // siteinfo cache key

const DEFAULT_PREVMAP = "[[";
const DEFAULT_NEXTMAP = "]]";
var prevMap = liberator.globalVariables.nextlink_prevmap || DEFAULT_PREVMAP;
var nextMap = liberator.globalVariables.nextlink_nextmap || DEFAULT_NEXTMAP;

var isFollowLink = typeof liberator.globalVariables.nextlink_followlink == "undefined" ?
                   false : $U.eval(liberator.globalVariables.nextlink_followlink);

const MICROFORMAT = {
    url:          "^https?://.",
    nextLink:     '//a[translate(normalize-space(@rel), "NEXT", "next")="next"] | //link[translate(normalize-space(@rel), "NEXT", "next")="next"]',
    insertBefore: '//*[contains(concat(" ", @class, " "), " autopagerize_insert_before ")]',
    pageElement:  '//*[contains(concat(" ", @class, " "), " autopagerize_page_element ")]',
}

const nositeinfoActions = {
  // vimperator [[, ]] action
  f: function(doc, count) {
       if (count < 0) {
         return buffer.followDocumentRelationship("previous");
       }
       return buffer.followDocumentRelationship("next");
     },
  e: function(doc, count) {
       var url = doc.location.href;
       return liberator.echo("No SITEINFO match " + url);
     },
  n: function() true,
};
var actpattern = liberator.globalVariables.nextlink_nositeinfo_act || "e";
var nositeinfoAct = nositeinfoActions[actpattern];


var localSiteinfo = storage.newMap("nextlink-local-siteinfo", {store: false});
if (localSiteinfo)
  localSiteinfo = [ info for ([ i, info ] in localSiteinfo) ];

var pageNaviCss = xml`
    `;
//}}}

var NextLink = function() {//{{{
  this.initialize.apply(this, arguments);
};
NextLink.prototype = {
  initialize: function(pager) {

    this.initialized = false;
    this.siteinfo = [];
    this.pager = pager;
    this.browserModes = config.browserModes || [ modes.NORMAL, modes.VISUAL ];
    this.is2_0later = config.autocommands.some(function ([ k, v ]) k == "DOMLoad"); // toriaezu

    var wedata = new libly.Wedata("AutoPagerize");
    wedata.getItems(24 * 60 * 60 * 1000, null,
      $U.bind(this, function(isSuccess, data) {
        if (!isSuccess) return;
        this.siteinfo = data.map(function(item) item.data);

        if (localSiteinfo)
          this.siteinfo = this.siteinfo.concat(localSiteinfo);
        this.siteinfo = this.siteinfo.sort(function(a, b) b.url.length - a.url.length); // sort url.length desc

        this.initialized = true;
      })
    );

    this.customizeMap(this);
  },
  initDoc: function(context, doc) {
    var value = doc[UUID] = {};
    value.siteinfo = this.getSiteinfo(doc);
    this.pager.initDoc(context, doc);
  },
  getSiteinfo: function(doc) {
    function valid(prop)
      $U.getNodesFromXPath(MICROFORMAT[prop], doc).length > 0;
    if (valid("nextLink") && valid("pageElement")) return MICROFORMAT;
    var url = doc.location.href;
    for (let i = 0, len = this.siteinfo.length; i < len; i++) {
      if (url.match(this.siteinfo[i].url) && this.siteinfo[i].url != "^https?://.") {
        return this.siteinfo[i];
      }
    }

    return null;
  },
  nextLink: function(count) {
    if (!this.initialized) {
      liberator.echo("before initialized.");
      return false;
    }
    var doc = window.content.document;
    if (!doc[UUID])
      this.initDoc(this, doc);

    this.pager.nextLink(doc, count);
  },
  customizeMap: function(context) {
    mappings.addUserMap(context.browserModes, [ prevMap ], "customize by nextlink.js",
      function(count) context.nextLink(count > 0 ? -1 * count : -1),
      { count: true });

    mappings.addUserMap(context.browserModes, [ nextMap ], "customize by nextlink.js",
      function(count) context.nextLink(count > 0 ? count : 1),
      { count: true });
  },
};//}}}

var Autopager = function() {};//{{{
Autopager.prototype = {
  initDoc: function(context, doc) {
    doc[UUID].loadURLs = [];

    if (context.is2_0later) {
      let css = $U.xmlToDom(pageNaviCss, doc);
      let node = doc.importNode(css, true);
      doc.body.insertBefore(node, doc.body.firstChild);
      //doc.body.appendChild(css);
    }
  },
  nextLink: function(doc, count) {
    var value = doc[UUID];

    // TODO: support MICROFORMAT
    // rel="next", rel="prev"

    if (!value.siteinfo && nositeinfoAct) {
      return nositeinfoAct(doc, count);
    }

    var curPage = this.getCurrentPage(doc);
    logger.log(curPage);
    var page = (count < 0 ? Math.round : Math.floor)(curPage + count);
    if (page <= 1) {
      value.isLoading = false;
      doc.defaultView.scrollTo(0, 0);
      return true;
    }
    if (this.focusPagenavi(doc, page)) {
      value.isLoading = false;
      return true;
    }

    if (value.isLoading) {
      logger.echo("loading now...");
      return false;
    }

    value.isLoading = true;

    if (value.terminate) {
      value.isLoading = false;
      logger.echo("terminated.");
      return false;
    }

    var req = this.createNextRequest(doc);
    if (!req) {
      value.isLoading = false;
      let win = doc.defaultView;
      win.scrollTo(0, win.scrollMaxY);
      logger.echo("end of pages.");
      return true;
    }

    req.addEventListener("success", $U.bind(this, this.onSuccess));
    req.addEventListener("failure", $U.bind(this, this.onFailure));
    req.addEventListener("exception", $U.bind(this, this.onFailure));
    req.get();
  },
  onSuccess: function(res) {
    var doc = res.req.options.doc;
    var url = doc.location.href;
    var value = doc[UUID];
    var pages = this.getHTMLDocument(value.siteinfo.pageElement, res)
    var resDoc = res.doc;
    var reqUrl = res.req.url;
    var [ next ] = $U.getNodesFromXPath(value.siteinfo.nextLink, resDoc);

    value.loadURLs.push(reqUrl);
    value.next = next;
    value.isLoading = false;

    // set reqUrl link-state visited
    $H.addURI(makeURI(reqUrl), false, true, makeURI(url));

    if (!pages || pages.length == 0) return;

    var addPageNum = this.getPageNum(doc) + 1;
    this.addPage(doc, resDoc, pages, reqUrl, addPageNum);
    this.focusPagenavi(doc, addPageNum);
  },
  addPage: function(doc, resDoc, pages, reqUrl, addPageNum) {
    var url = doc.location.href;
    var value = doc[UUID];
    if (!value.insertPoint)
      value.insertPoint = this.getInsertPoint(doc, value.siteinfo);
    var insertPoint = value.insertPoint;

    this.insertRule(doc, addPageNum, reqUrl, pages[0], insertPoint);

    pages.forEach(function(elem) {
      var pe = resDoc.importNode(elem, true);
      insertPoint.parentNode.insertBefore(pe, insertPoint);
    });
    return true;
  },
  onFailure: function(res) {
    logger.log("onFailure");
    var doc = res.req.options.doc;
    var url = doc.location.href;
    var value = doc[UUID];
    value.isLoading = false;
    value.terminate = true;
    logger.echoerr("nextlink: loading failed. " + "[" + res.status + "]" + res.statusText + " > " + res.req.url);
  },
  focusPagenavi: function(doc, page) {
    var xpath = '//*[@id="vimperator-nextlink-' + page + '"]';
    var [ elem ] = $U.getNodesFromXPath(xpath, doc);
    var win = doc.defaultView;
    if (elem) {
      let p = $U.getElementPosition(elem);
      win.scrollTo(0, p.top);
      return true;
    }
    return false;
  },
  createNextRequest: function(doc) {
    var value = doc[UUID];
    var url = doc.location.href;
    var next = value.next;
    if (!next)
      [ next ] = $U.getNodesFromXPath(value.siteinfo.nextLink, doc);
    if (!next)
      return false;

    var reqUrl = $U.pathToURL(next, url, doc);
    if (value.loadURLs.some(function(url) url == reqUrl)) return false;

    var req = new libly.Request(
                reqUrl, null,
                { asynchronous: true, encoding: doc.characterSet,
                  doc: doc }
              );
    return req;
  },
  getPageNum: function(doc) {
    var xpath = '//*[@class="vimperator-nextlink-page"]';
    var page = 1 + $U.getNodesFromXPath(xpath, doc).length;
    return page;
  },
  getCurrentPage: function(doc) {
    var xpath = '//*[@class="vimperator-nextlink-page"]';
    var markers = $U.getNodesFromXPath(xpath, doc);
    var win = doc.defaultView;
    var curPos = win.scrollY;

    // top of page
    if (curPos <= 0) return 1.0;

    // bottom of page
    if (curPos >= win.scrollMaxY) {
      if (markers.length > 0) {
        let lastMarker = $U.getElementPosition(markers[markers.length-1]).top;
        if (curPos <= lastMarker) return markers.length + 1;
      }
      return markers.length + 1.5;
    }

    // return n.5 if between n and n+1
    var page = 1.0;
    for (let i = 0, len = markers.length; i < len; i++) {
      let pos = $U.getElementPosition(markers[i]).top;
      if (curPos == pos) return page + 1;
      if (curPos < pos) return page + 0.5;
      ++page;
    }

    return page + 0.5;
  },
  getInsertPoint: function(doc, siteinfo) {
    var insertPoint, lastPageElement;

    if (siteinfo.insertBefore)
      [ insertPoint ] = $U.getNodesFromXPath(siteinfo.insertBefore, doc);

    if (!insertPoint) {
      let elems = $U.getNodesFromXPath(siteinfo.pageElement, doc);
      if (elems.length > 0) lastPageElement = elems.pop();
    }

    if (lastPageElement)
      insertPoint = lastPageElement.nextSibling ||
        lastPageElement.parentNode.appendChild(doc.createTextNode(" "));
    return insertPoint;
  },
  insertRule: function(doc, addPageNum, reqUrl, page, insertPoint) {
    var p = doc.createElementNS(HTML_NAMESPACE, "p");
    p.id = "vimperator-nextlink-" + addPageNum;
    p.innerHTML = 'page: ' + addPageNum + "";
    p.className = "vimperator-nextlink-page";

    var tagName;
    if (page && page.tagName)
      tagName = page.tagName.toLowerCase();

    if (tagName == "tr") {
      let insertParent = insertPoint.parentNode;
      let colNodes = getElementsByXPath("child::tr[1]/child::*[self::td or self::th]", insertParent);

      let colums = 0;
      for (let i = 0, l = colNodes.length, f = function(col) {
        colums += parseInt(col, 10) || 1;
      }; i < l; f(colNodes[i++].getAttribute("colspan")));
      let td = insertParent.insertBefore(doc.createElement("tr"), insertPoint)
                           .appendChild(doc.createElement("td"));
      td.setAttribute("colspan", colums);
      td.appendChild(p);
    } else if (tagName == "li") {
      insertPoint.parentNode.insertBefore(doc.createElementNS(HTML_NAMESPACE, "li"), insertPoint)
                 .appendChild(p);
    } else {
      insertPoint.parentNode.insertBefore(p, insertPoint);
    }
  },
  // alternative function of 'createHTMLDocument' in _libly.js
  createHTMLDocument: function(str, xmlns) {
    var createXSLTProcessor = function() {
      var xsl = (new DOMParser()).parseFromString(
        ['',
         '',
         '',
         ''].join("\n"), "text/xml");

      var xsltp = new XSLTProcessor();
      xsltp.importStylesheet(xsl);
      var doc = xsltp.transformToDocument(
        document.implementation.createDocument("", "", null));
      return doc;
    };
    var doc = createXSLTProcessor();
    var range={};
    if (!document.caretRangeFromPoint) { 
      range= doc.createRange();
    } else {
      range = doc.caretRangeFromPoint(0, 0);
    }
    let text = str.replace(/^[\s\S]*?]*)?>[\s]*|<\/body[ \t\r\n]*>[\S\s]*$/ig, '');
    let fragment = range.createContextualFragment(text);
    let doctype = document.implementation.createDocumentType('html', '-//W3C//DTD HTML 4.01//EN', 'http://www.w3.org/TR/html4/strict.dtd');
    let htmlFragment = document.implementation.createDocument(null, 'html', doctype);
    htmlFragment.documentElement.appendChild(htmlFragment.importNode(fragment,true));
    return htmlFragment;
  },
  // alternative function of 'getHTMLDocument' in _libly.js
  getHTMLDocument: function(xpath, res, xmlns, ignoreTags, callback, thisObj) {
    if (!res.doc) {
      res.htmlFragmentstr = libly.$U.getHTMLFragment(res.responseText);
      res.htmlStripScriptFragmentstr = libly.$U.stripTags(res.htmlFragmentstr, ignoreTags);
      res.doc = this.createHTMLDocument(res.htmlStripScriptFragmentstr, xmlns);
    }
    if (!xpath) xpath = '//*';
    return libly.$U.getNodesFromXPath(xpath, res.doc, callback, thisObj);
  }
};
//}}}

var FollowLink = function() {};//{{{
FollowLink.prototype = {
  initDoc: function(context, doc) {
  },
  nextLink: function(doc, count) {
    var url = doc.location.href;
    var value = doc[UUID];

    function followXPath(xpath) {
      var [ elem ] = $U.getNodesFromXPath(xpath, doc);
      if (elem) {
        let tagName = elem.tagName.toLowerCase();
        if (tagName == "link") {
          liberator.open(elem.href);
        } else {
          buffer.followLink(elem, liberator.CURRENT_TAB);
        }
        return true;
      }
      return false;
    }

    if (count < 0) {
      let xpath = [ "link", "a" ].map(function(e)
        "//" + e + '[translate(normalize-space(@rel), "PREV", "prev")="prev"]')
                                 .join(" | ");

      if (followXPath(xpath)) return;
      buffer.followDocumentRelationship("previous");
    } else {
      let xpath = [ "link", "a" ].map(function(e)
        "//" + e + '[translate(normalize-space(@rel), "NEXT", "next")="next"]')
                                 .join(" | ");
      if (followXPath(xpath)) return;

      if (value.siteinfo && followXPath(value.siteinfo.nextLink)) return;
      buffer.followDocumentRelationship("next");
    }
  }
};
//}}}

var instance = new NextLink((isFollowLink ? new FollowLink() : new Autopager()));
return instance;

})();
// vim: set fdm=marker sw=2 ts=2 sts=0 et:

2013.05.07 追記
ITProで動かなかったのは、wedataのこのエントリのせいっぽい。
ITPro Active内の記事はこれでいいみたいだけど、他のページはこの設定だとマッチしない。後で直しておく。

2013年4月16日火曜日

Vimperatorプラグイン nextlink.js が動かないので調べた

2013.05.06 続編あり(とりあえず動くようにした)。 
 nextlink.jsとは何か
nextlink.jsは、AutoPagerize風のページ継ぎ足しを手動で実行することができるVimperatorプラグイン(だった)。
Googleの検索結果などの複数ページに跨るコンテンツを表示した状態で、キーボードより ]] と入力すると、1ページ目の下に2ページ目が継ぎ足される。さらに]]と入力すると、下に3ページ目が継ぎ足される(はずであった)。

括弧内に過去形で書いたのは、久しぶりに使ってみたらちゃんと動かなかったからである。2ページ目は取れるが、3ページ目以降が取れない。
ちょっと調べてみたところ、なんとなく原因が判明。

いま彼に何が起きているのか
nextlink.jsの仕様はこうだ。
]]を入力すると、現在のページ内からXPathを使って次ページへのリンク(アンカー)を取得し、AjaxでそのURLのコンテンツ(次ページ)を取ってくる、という動きをする。この辺の処理には、_libly.js というライブラリに含まれる関数を使っている。
ところが、試しに_libly.jsにGoogle検索結果の2ページ目を取得させてみたところ、次ページへのリンク部分が
<a class="pn" id="pnnext>
となっていた。うん、href属性がない。そんなばかな。これでは3ページ目のURLが分からない。

調べてみたところ、現在の_libly.jsには問題があり、Ajaxの関数 libly.Request#get で取得したHTML文書から<a>のhref属性を取ることができない状態のようである。

この辺の話が下記に出ている。
_libly.jsのcreateHTMLDocumentに使っているnsIScriptableUnescapeHTMLのバグ - Vimple Star Sprites - vimperatorグループ
XPCOMのバグ? ということで、Vimple Star Spritesの中の人である寺田さんがバグ報告している。
bug 6538 ? nsIScriptableUnescapeHTML#parseFragmentでアンカー要素のhref属性値が空になる
が、ほんの少し議論された後、放置されているようである(日付は、なんと2009年3月)。文面を読む限り、仕様だってことなんかな…。
困ったもんだが、直すなら
  • _libly.js のHTMLパース処理を書き直す
  • nextlink.js 側で個別に対応する
のどちらかになる。
前者は影響範囲が大きすぎて俺みたいな雑魚には辛いので、後者の方向で少し対策を考えてみたい。暇があれば。

2013年4月13日土曜日

単身赴任先のマンションに「フレッツ 光ネクスト マンション・スーパーハイスピードタイプ 隼」を引いた

2013年4月から関西で単身赴任することになってしまったので関西に引っ越してきたわけだが、引っ越しといえばインターネット回線契約がつきもの。
引っ越し先の近所にある某家電量販店に相談に行くと、フレッツ光かzaq(J:COM)のどちらかならすぐできる、と言われたので、折角なので「フレッツ 光ネクスト マンション・スーパーハイスピードタイプ 隼」をチョイス。マンションだと、折角光回線を引いてもラストワンマイル(建物内)がVDSLとかにされて超しょべえことが多いが、この「隼」くんはマンションでも各部屋まで光ケーブルを引っ張ってくれるグレートな奴だ。しかも1Gbpsだ。
ただしプロバイダはYahoo BB指定。キャンペーンで色々特典がついたので、まあよしとする。

申し込みから待つこと約2週間、昨日ようやく工事が終わった。

HGWは↓な感じ。


WAN 1Gbps。

早速BNRスピードテストで測ってみる。まず下り。

あれれー? なんか遅ぇーぞ。


上りも測ってみるが、これは。。。
土曜の夜に測るのがいけないのかもしれないので、明日の昼間再チャレンジしてみることとする。

2013年4月12日金曜日

IDP拡張フィールドを含むCRLをOpenSSLがうまく処理できなかった話(未解決)

CRLに関する備忘録。
CRLとCDPについておさらい
CRL(証明書失効リスト)とは、文字通り「失効したデジタル証明書のリスト」であり、失効済み証明書のシリアル番号が列挙されているものである。
CRLは一般的にWebサーバーやLDAPサーバー等に普通のファイルとして置かれており、デジタル証明書の検証をするシステム(認証サーバー等)はそのCRLをダウンロードして利用する。
下記は、とあるサーバーからダウンロードしたCRLの内容をOpenSSLで確認した例である。crlのファイル名はxxx.crlとする。(下記はDER形式の場合。PEMの場合は"-inform PEM"とする)
$ openssl crl -in xxx.crl -inform DER -text
Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: sha1WithRSAEncryption
        Issuer: /C=JP/O=HogeHoge, Ltd./CN=HogeHoge Public CA
        Last Update: Apr  3 10:00:00 2013 GMT
        Next Update: Apr 17 10:00:00 2013 GMT
        CRL extensions:
            X509v3 CRL Number:
                12345
            X509v3 Authority Key Identifier:
                keyid:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx

Revoked Certificates:
    Serial Number: 1234
        Revocation Date: Dec 12 00:00:01 2012 GMT
        CRL entry extensions:
            Invalidity Date:
                Dec 12 00:00:00 2012 GMT
    Serial Number: 1357
        Revocation Date: Dec 12 00:00:01 2012 GMT
        CRL entry extensions:
            Invalidity Date:
                Dec 12 00:00:00 2012 GMT
(snip)
CRLには分割発行という考え方があって、特定の目的やルールに応じてCRLを小さい単位で分割することが可能である。この辺りの説明は、すごく古い記事だがPKI基礎講座(5):証明書の有効性 - @ITが詳しい。
で、デジタル証明書にはCDP(CRL Distribution Point)というフィールドがあり、CRLのURLが記述される。つまり、「自分が失効されているかを知りたければ、このURLに置かれているCRLをチェックせよ」ということ。
以下、デジタル証明書のCDPフィールドを確認した例。証明書のファイル名はyyy.cerとする。
$ openssl x509 -inform der -in yyy.cer -text
Certificate:
    Data:
(snip)
        X509v3 extensions:
(snip)
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://hogehoge.com/hogehogeca/xxx-1.crl
(snip)
IDP拡張フィールドとは
CRLを分割した場合、CRL内にIDP(Issuing Distribution Point)という拡張フィールドが含まれるのが普通であり、自身のURLが記述されている。分割したCRLを一意に識別するために利用される。
下記はIDP拡張フィールドを含むCRLの内容を確認した例。CRLのファイル名はxxx-1.crl。
$ openssl crl -in xxx-1.crl -inform DER -text
Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: sha1WithRSAEncryption
(snip)
        CRL extensions:
(snip)
            X509v3 Issuing Distrubution Point:
                Full Name:
                  URI:http://hogehoge.com/hogehogeca/xxx-1.crl
                Only User Certificates
(snip)
デジタル証明書の検証を行う場合、
・デジタル証明書に含まれるCDPのURL

・CRLに含まれるIDPのURL
が一致することを確認した上で、そのデジタル証明書のシリアル番号がCRLに含まれるかを確認するのが正しい。
OpenSSLで問題発生
OpenSSL 1.0において、IDP拡張フィールドが含まれるCRLを使ってデジタル証明書の検証を行ったところ、エラーになってしまった。
下記がエラーの例(ca.pemはルートCA証明書)。
なお、opensslはPEM形式がデフォルトみたいなので、事前にCRLとデジタル証明書をDER形式からPEM形式に変換してある。
$ openssl verify -CAfile ca.pem -verbose -crl_check_all -CRLfile xxx-1.pem yyy.pem
yyy.pem: C = JP, O = FUGAFUGA , OU = DEV1 , CN = yyy
error 44 at 1 depth lookup:Different CRL scope
CRLのスコープが違うと怒られている。そんなはずないんだけど。

本件、未だ解決できてない。
X.509やOpenSSLに詳しい人の助言求む。
ちなみに、IDP拡張フィールドを含まないCRLを指定して同コマンドを実行したところ、想定通りにデジタル証明書の検証が行われた。