2009/09/07


この文章はperl.jonallen.info に掲載されていた記事の和訳です。意訳が含まれている可能性があります。間違いがあればご連絡下さい。
by Jon Allen (JJ) - posted on Wednesday, 26 August 2009

ここ2、3年にわたって、Perlでの開発はCatalystやDBIx::Class、Moose等のエキサイティングな新技術により変わってました。

しかしながら、これらや他のツールに共通して言える事が1つあります - それらはこれらがPerl本体の配布物ではなくCPANの一部という事です。共有ホスティングサーバなど信頼されている環境においては、ユーザはルート権限なしでCPANモジュールをシステムにインストールする事が難しいでしょう。 ただ幸い、単純解があります - それが local::lib です。


local::lib の紹介

local::lib は CPAN ディストリビューションをホームディレクトリににインストールできる様にあらゆる設定を行うPerlモジュールです。これはルート権限が必要ない事を意味していて、システムのPerlや他のユーザに干渉せずに確実にインストール出来る事を意味しています。

もちろん、local::lib それ自信も root 権限なしにインストール出来る為、システム管理者を苦しめる必要性は全くありません。


インストールウォークスルー

クリーンインストールされた Ubuntu 9.04 で local::lib 使用するデモンストレーションを行います。

まず最初にhttp://search.cpan.org/dist/local-libで最新版をダウンロードしデスクトップへ .tar.gz を保存します。

search.cpan.org   Downloading local::lib

次に、Terminalセッションを開いて、以下のコマンドを入力してアーカイブを解凍します:

cd Desktop
tar -zxf local-lib-1.004004.tar.gz
cd local-lib-1.004004

"bootstrap"にする事を local::lib に教えてあげる必要があります。これでホームディレクトリにperl5というフォルダが作成され local::lib をそこにインストールする様 toolchain に命令します。

perl Makefile.PL --bootstrap
Bootstrapping local::lib

インストールが自動的に構成される様に一度か二度尋ねられます。デフォルト値('yes')で良いのでただ単にEnterキーを叩きましょう。

Automatic CPAN configuration   CPAN config complete

設定が終了したら以下のコマンドを入力し local::lib をビルド、テスト、インストールして下さい:

make
make test
make install

Installing local::lib

もうちょっとです。残りは1工程です。インストールされたモジュールがどこに保存されるかを Perl に教えてあげる為に幾つかの環境変数を設定します。

echo 'eval $(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)' >>~/.bashrc

Setting environment variables

全て完了です。実行していたターミナルウィンドウを全て閉じて、新しい設定が反映される様に、新しいTerminalセッションを開きましょう。


local::lib のテスト

ちゃんと動くか簡単なモジュールをインストールしてみましょう。新しいターミナルウィンドウを開き、CPAN シェルを起動します:

perl -MCPAN -eshell

CPAN プロンプトが開き、以下を入力します

install Acme::Time::Baby
Installing Acme::Time::Baby

モジュールをダウンロードしてビルド、テスト、インストールします - マニュアルを見ることもないでしょう。exit をタイプして CPAN シェルから抜けたら簡単なワンライナーでインストールされたモジュールをテスト出来ます:

perl -MAcme::Time::Baby -E 'say babytime'

Acme::Time::Baby installed!

CPAN を恐れるな

さて動作確認です。Catalyst をインストールしましょう。

Catalyst は多くのモジュールに依存した巨大なモジュールですが CPAN.pm でセットアップするので問題はありません。しかしながら初期状態では、CPAN シェルが依存を見付ける度にインストールすべきかどうか尋ねて来て、とても退屈になります。最初ですから自動的に依存に従う様 CPAN.pm に教えてあげます。

perl -MCPAN -eshell
o conf prerequisites_policy follow
o conf build_requires_install_policy yes
o conf commit

Setting CPAN.pm preferences

注意 それでも時折、モジュールがそれら自身の構成に依存した質問をして来るので何が起きるか目を光らせておくべきでしょう。

インストールを開始する準備が整いました。

install Catalyst::Devel

CPAN.pm が全ての Catalyst 依存モジュールをビルドしてくれます。少々時間が経って、以下のメッセージが出ていればインストール成功です:

FLORA/Catalyst-Devel-1.19.tar.gz
/usr/bin/make install -- OK

Catalyst installed!

ちゃんと動作しているかを検証するので CPAN シェルを終了して新しい Catalyst アプリケーションを作成しましょう。

~/perl5/bin/catalyst.pl MyApp
cd MyApp
perl Makefile.PL

Starting a new Catalyst application   Starting a new Catalyst application

注意 catalyst.pl スクリプトは ~/perl5/bin ディレクトリにあります - ここが local::lib の標準的なスクリプトインストール先です。

Catalystアプリケーションはスタンドアロンの開発用サーバを含んでいるので、簡単にアプリケーションを実行できます:

script/myapp_server.pl -r -d

Running the Catalyst development server   Catalyst server running

これで http://localhost:3000をブラウズすれば、光り輝くCatalystのテストページが表示されます!

The Catalyst test page!

Notes

  • いくつかの Perl モジュールは速度を向上やC言語のライブラリ関数と強調する為にC言語で書かれています。CPANの全てを使用するにはCコンパイラをマシンにインストールする必要があります。Linuxや他のUNIXシステムで大概Cコンパイラが既にインストールされていますが(which ccをタイプしてみましょう)、そうでない場合でもベンダは GCC パッケージを提供しているはずです。Macでは Xcode developer tools (OS XをDVDからインストール)をインストールすると必要な物は全て揃うでしょう。

  • 私がテストで使用しているシステム Ubuntu 9.04 は Perl 5 version 10.0 を含んでいます。以下のコマンドで Perl がどのバージョンかをチェックできます。

    perl -v

    Checking your Perl version

    もし Perl 5 のリリースよりも古い物、すなわち v5.8.9 以前のバージョンをお使いであれば、システム管理者かOSベンダーにアップグレードを求めるべきしょう。 Perl5 version 10.0 では smart matching、named regex captures、a switch statement や state variables といった多くの新機能が導入されています - http://perldoc.perl.org/perl5100delta.html を参照してと全ての新機能を確認して下さい。

  • インストール済みのモジュールを全て削除してやり直すのであれば、ローカルの ~/.cpan と ~/perl5 ディレクトリを削除するだけです:

    rm -rf ~/.cpan
    rm -rf ~/perl5

    そして ~/.bashrc を編集し以下の行を削除します:

    eval $(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)

    以上に示した手順で local::lib がインストール出来ます。

Posted at by



2009/09/03


こんな拡張が欲しい人なんて半ば病気ですよ。

夜になるとエアコン無しに過ごせる涼しい季節になって来ました。皆さん如何お過ごしでしょうか。

最近Google Chromeを使っているのですが、AutoPagerizeのGoogle Chrome拡張を入れてみて感動し、被はてなブックマーク数を画面下に表示する拡張が欲しくなったので、つい勢いで作ってしまいました。
はいはい。病気病気

スクリーンショットはこんな感じ
chrome-hbcount

os0xさんの記事をふんだんに参考にしながらなんとか作り上げました(os0x++)。
以下その作成手順。

まず、manifest.jsonを用意しました。
manifest.json
{
  "name": "hatena bookmark counter",
  "description": "hatena bookmark counter for google chrome. hatena bookmark is social bookmark web service. see 'http://b.hatena.ne.jp/'. this extension show image of numbers which how many users bookmarked the URL.",
  "version": "0.0.1",
  "icons": { "normal": "hbcount.gif" },
  "permissions": [ "tabs" ],
  "toolstrips": [ "hbcount.html" ],
  "content_scripts": [
    {
      "js": [ "hbcount.js" ],
      "matches": [ "http://*/*" ]
    }
  ]
}
今回の拡張は、ページがロードされた瞬間に発動するjavascriptと、Google Chrome起動中に画面下で常駐するためのtoolstripsと呼ばれるHTMLコンテンツを使いました。
まずユーザスクリプト側。
hbcount.js
if (window == top{
  var port = chrome.extension.connect();
  port.onMessage.addListener(function(data) {
    location.href = data.url;
  });
  port.postMessage({url: location.href});
}
ユーザスクリプト側からはtoolstripのコントロールに対してアクセス出来ませんのでpostMessageでメッセージを送信し被はてなブックマーク数アイコンを更新します。なおtoolstripにあるアイコンをクリックすると、はてなブックマークエントリページを開く様になっているのですが、toolstrip側から現在開いているタブのコンテンツにはアクセスする事も出来たのですが、少し危険を感じたのでpostMessageを使いtoolstrip側からユーザスクリプト側にURLを返信してlocation.hrefでの画面遷移をさせています。
真ん中のイベント待ちはその為の物です。なお、topかどうかを確認しているのは現在のURL以外でもこの拡張が走ってしまうのを防止している小細工です。例えばiframeなんかでアフィが表示されているとそれに対してもこの拡張が実行されてしまいます。

次に本体であるtoolstripのソースは以下の通り。
hbcount.html
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<script type="text/javascript">
window.onload = function() {
  var hbcount = document.getElementById("hbcount");
  chrome.extension.onConnect.addListener(function(port) {
    function update(url) {
      hbcount.src = 'hbcount.gif';
      hbcount.onclick = null;
      if (url.match(/^chrome:\/\//)) return;
      url = url.replace(/#/g, '%23');
      hbcount.src = 'http://b.hatena.ne.jp/entry/image/' + url;
      hbcount.onclick = function() { port.postMessage({'url': 'http://b.hatena.ne.jp/entry/' + url}); }
    }
    port.onMessage.addListener(function(data) { update(data.url); });
    chrome.tabs.onSelectionChanged.addListener(function(id, props) {
      chrome.tabs.get(id, function(tab) { update(tab.url); });
    });
  });
};
</script>
<img src="hbcount.gif" id="hbcount" style="cursor: pointer;" title="はてなブックマーク"/>
ユーザスクリプトからメッセージを受け取りupdate関数にて更新します。アイコンクリック時のメッセージ返信を行う処理もここにあります。
その下にある処理ではタブが切り替わったタイミングで現在の被ブックマーク数を更新する処理になります。この場合、いくらmanifest.jsonでhttp://のみと宣言していてもタブに対するイベント登録を行うのでchrome://といったURLも対象になってしまいます。update関数でURLを避けているのはその為です。

さて、ここから話がそれ始めます。
Google Chromeの拡張はchromeブラウザ自身でパッキングする事が出来ます。ただchromeへのパスをいちいち書いてられないのでMakefileを書きました。
Makefile
ifndef CHROME
ifneq ($(windir),)

# Windows
DEST = "$(shell pwd)\chrome-hbcount"
CHROME = "$(USERPROFILE)/Local Settings/Application Data/Google/Chrome/Application/chrome.exe"

else

# Other Platform: Linux? Mac?
DEST = $(shell pwd)/chrome-hbcount

CHROME = $(shell which crxmake)
ifeq ($(CHROME),)
CHROME = $(shell which google-chrome)
endif
ifeq ($(CHROME),)
CHROME = $(shell which chromium-browser)
endif
ifeq ($(CHROME),)
CHROME = chrome
endif
endif
endif

SRCS = hbcount.gif hbcount.html hbcount.js manifest.json

all : chrome-hbcount.crx

first : $(SRCS)
    @-rm -r $(DEST)
    @mkdir $(DEST)
    @cp $(SRCS) $(DEST)/.
    $(CHROME) --pack-extension=$(DEST)

chrome-hbcount.crx : $(SRCS)
    @-rm -r $(DEST)
    @mkdir $(DEST)
    @cp $(SRCS) $(DEST)/.
    $(CHROME) --pack-extension=$(DEST) --pack-extension-key=chrome-hbcount.pem

clean:
    @-rm *.crx
    @-rm -r $(DEST)
初回だけmake first、以降はmakeでビルドです。
WindowsでもLinuxでもMacでも使える様に、かつgoogle-chromeが入っていない場合にはchromium-browserを使う様になっています。ちなみにcrxmakeというのはConstellationさんが書いたchrome拡張のパッケージングツールです(Constellation++)。chromeを起動する事なくパッケージング出来ます。なおchrome-hbcountという部分を書き換えれば、他の拡張でも使えるかもしれません。

さらに拡張を書いていると拡張のバージョンがちょくちょくあがります。
Google Chrome(chromium-browser)の拡張にはupdate_urlという設定項目があり、決まった形のXMLファイルを用意しておけば自動アップデート出来る様になる予定があるそうなのですが、拡張を書き換える度にいちいちupdate.xmlを書き換えてアップロードするも面倒くさそうだったので、手順を自動化出来る様にしてみました。簡単には
  • manifest.jsonからバージョンを抜き出す
  • 抜き出したバージョンでupdate.xmlを更新する
  • githubのダウンロードページにあるファイルを一旦削除する
  • 拡張モジュール(crx)とupdate.xmlをアップロードする
という一連の流れをPerlスクリプトにしてみました。
dist-upload.pl
#!/usr/bin/perl

use strict;
use warnings;

use JSON;
use Perl6::Slurp;
use WWW::Mechanize;
use Net::GitHub::Upload;
use Config::Pit;

my $config = pit_get('github-upload', require => {
    'login' => 'your login id on github.com',
    'password' => 'your password on github.com',
});

my $manifest = from_json(slurp 'manifest.json');
my $id = $manifest->{id};
my $version = $manifest->{version};

open my $fh, '>update.xml';
print $fh <<EOF;
<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
    <app appid="$id">
        <updatecheck codebase="http://cloud.github.com/downloads/mattn/chrome-hbcount/chrome-hbcount.crx" version="$version" />
    </app>
</gupdate>
EOF
close $fh;

my $mech = WWW::Mechanize->new;
$mech->get('https://github.com/login');
$mech->submit_form(
    form_number => 2,
    fields      => {
        login => $config->{login},
        password => $config->{password},
});

$mech->get('http://github.com/mattn/chrome-hbcount/downloads');
for my $form (@{$mech->forms}) {
    if ($form->action =~ /^http:\/\/github.com\/mattn\/chrome-hbcount\/downloads\//) {
        print "deleting ".$form->action."\n";
        $mech->request($form->click);
    }
}

chomp(my $user  = `git config github.user`);
chomp(my $token = `git config github.token`);
my $gh = Net::GitHub::Upload->new(
    login => $user,
    token => $token,
);
print "uploading chrome-hbcount.crx\n";
$gh->upload( repos => 'mattn/chrome-hbcount', file  => 'chrome-hbcount.crx' );
print "uploading update.xml\n";
$gh->upload( repos => 'mattn/chrome-hbcount', file  => 'update.xml' );
こちらもmattnとかchrome-hbcountという部分を書き換えれば、他の拡張で使えるかもしれません。ちなみにNet::GitHub::Uploadはtypesterさんのモジュールです(typester++)。
これで、あとは
  • 拡張を書き換える
  • manifest.jsonのバージョンを上げる
  • make
  • dist-upload.pl
という単純な手順で皆様に最新の拡張を自動ダウンロードして頂ける...予定です。まだupdate.xmlを使った自動アップデート機能は動いてないらしいです。


さて、はてなブックマークカウンタ拡張に話を戻して...
コードおよび拡張ファイルはgithubに置いてあります。Downloadのページからでもダウンロード出来ますが、以下のリンクでインストール出来る様になっています。
hatena bookmark counter for google chrome
リポジトリは以下のリンク先にあります。
mattn's chrome-hbcount at master - GitHu

hatena bookmark counter for google chrome

http://github.com/mattn/chrome-hbcount/tree/master
ライセンスはBSDにしましたので、どうぞお好きに使って下さい。流用してdelicious拡張なんてのもいいですね。
Posted at by



2009/09/02


以前、Growl For Windows紹介記事gntp-sendという単純なCのプログラム(パスワードハッシュのみサポート)を書きました。その後、GNTPプロトコルの仕様に合わせパスワードハッシュ以外にもAES/DES/3DESな暗号化通信もサポートしたPerlモジュール、Growl::GNTPを書きました。

C/C++言語から暗号モジュールを扱う上でcryptライブラリはライセンス制約がありますが、public domainなCrypto++を使えばな制約もなくなります。
久々boostを触ってみようとリハビリがてらboost::asioとcrypto++を使って、暗号化通信をサポートしたGNTP Growlプログラムを書いてみた。

crypto++には、ハッシュアルゴリズムや暗号アルゴリズムがごった煮で含まれており、フィルタとして使ったりCBCモードで使ったりと、かなり便利になっています。
例えば乱数を得たいのであれば
CryptoPP::SecByteBlock salt(8);
CryptoPP::AutoSeededRandomPool rng;
rng.GenerateBlock(salt.begin(), salt.size());
こんな感じに、またMD5 Digestのhex文字列を取得したいのであれば
CryptoPP::SecByteBlock passtext(CryptoPP::Weak1::MD5::DIGESTSIZE);
CryptoPP::Weak1::MD5 hash;
hash.Update((byte*)password_.c_str(), password_.size());
hash.Update(salt.begin(), salt.size());
hash.Final(passtext);
CryptoPP::SecByteBlock digest(CryptoPP::Weak1::MD5::DIGESTSIZE);
hash.CalculateDigest(digest.begin(), passtext.begin(), passtext.size());
といった感じに。さらにCBCモードで文字列を暗号化したいならば
CryptoPP::CBC_Mode<CryptoPP::DES>::Encryption
  encryptor(passtext.begin(), iv.size(), iv.begin());

std::string cipher_text;
CryptoPP::StringSource(text, true,
  new CryptoPP::StreamTransformationFilter(encryptor,
  new CryptoPP::StringSink(cipher_text)
  ) // StreamTransformationFilter
); // StringSource
こんな風に書くことも出来る。これだけそろえば出来たも同前。
boost::asioの便利なiostreamを使って
asio::ip::tcp::iostream sock(hostname, port);
sock << "こんにちわこんにちわ!";
綺麗な書き方も出来る。ちなみにboost::asioを使えばWindowsやUNIX上での特有なコードが現れる事がないので、同じソースでWindowsやLinuxでもコンパイル出来てしまうので非常にありがたいですね。

以下、全体のソースです。ヘッダファイルになってます。
#ifndef gntp_h
#define gntp_h

#define CRYPTOPP_ENABLE_NAMESPACE_WEAK 1
#include <sstream>
#include <iostream>
#include <string>
#include <cryptopp/osrng.h>
#include <cryptopp/files.h>
#include <cryptopp/hex.h>
#include <cryptopp/md5.h>
#include <cryptopp/des.h>
#include <cryptopp/aes.h>
#include <cryptopp/filters.h>
#include <cryptopp/modes.h>

#include <asio.hpp>

class gntp {
private:
  static inline std::string to_hex(CryptoPP::SecByteBlock& in) {
    std::string out;
    CryptoPP::HexEncoder hex( NULL, true, 2, "" );
    hex.Attach(new CryptoPP::StringSink(out));
    hex.PutMessageEnd(in.begin(), in.size());
    return out;
  }

  void send(const char* method, std::stringstream& stm) {
    asio::ip::tcp::iostream sock(hostname_, port_);
    if (!sock) return;

    // initialize salt and iv
    CryptoPP::SecByteBlock salt(8), iv(8);
    rng.GenerateBlock(salt.begin(), salt.size());
    rng.GenerateBlock(iv.begin(), iv.size());

    if (!password_.empty()) {
      // get digest of password+salt hex encoded
      CryptoPP::SecByteBlock passtext(CryptoPP::Weak1::MD5::DIGESTSIZE);
      CryptoPP::Weak1::MD5 hash;
      hash.Update((byte*)password_.c_str(), password_.size());
      hash.Update(salt.begin(), salt.size());
      hash.Final(passtext);
      CryptoPP::SecByteBlock digest(CryptoPP::Weak1::MD5::DIGESTSIZE);
      hash.CalculateDigest(digest.begin(), passtext.begin(), passtext.size());

      // initialize crypt
      CryptoPP::CBC_Mode<CryptoPP::DES>::Encryption
        encryptor(passtext.begin(), iv.size(), iv.begin());

      std::string cipher_text;
      CryptoPP::StringSource(stm.str(), true,
        new CryptoPP::StreamTransformationFilter(encryptor,
        new CryptoPP::StringSink(cipher_text)
        ) // StreamTransformationFilter
      ); // StringSource

      sock << "GNTP/1.0 "
        << method
        << " DES:" << to_hex(iv)
        << " MD5:" << to_hex(digest) << "." << to_hex(salt)
        << "\r\n"
        << cipher_text << "\r\n\r\n";
    } else {
      sock << "GNTP/1.0 "
        << method
        << " NONE\r\n"
        << stm.str() << "\r\n";
    }

    while (1) {
      std::string line;
      if (!std::getline(sock, line)) {
        break;
      }
      //std::cout << "[" << line << "]" << std::endl;
      if (line.find("GNTP/1.0 -ERROR") == 0)
        throw "failed to register notification";
      if (line == "\r") break;
    }
  }

  std::string application_;
  std::string hostname_;
  std::string port_;
  std::string password_;
  CryptoPP::AutoSeededRandomPool rng;
public:
  gntp(std::string application = "gntp-send", std::string password = "",
      std::string hostname = "localhost", std::string port = "23053") :
    application_(application),
    password_(password),
    hostname_(hostname),
    port_(port) { }

  void regist(const char* name) {
    std::stringstream stm;
    stm << "Application-Name: " << application_ << "\r\n";
    stm << "Notifications-Count: 1\r\n";
    stm << "\r\n";
    stm << "Notification-Name: " << name << "\r\n";
    stm << "Notification-Display-Name: " << name << "\r\n";
    stm << "Notification-Enabled: True\r\n";
    stm << "\r\n";
    send("REGISTER", stm);
  }

  void notify(const char* name, const char* title, const char* text, const char* icon = NULL) {
    std::stringstream stm;
    stm << "Application-Name: " << application_ << "\r\n";
    stm << "Notification-Name: " << name << "\r\n";
    if (icon) stm << "Notification-Icon: " << icon << "\r\n";
    stm << "Notification-Title: " << title << "\r\n";
    stm << "Notification-Text: " << text << "\r\n";
    stm << "\r\n";
    send("NOTIFY", stm);
  }
};

#endif
使い方は非常に簡単。
#include "gntp.h"

int main(void) {
  gntp client("my growl application", "my-password-is-secret");
  client.regist("my-event");
  client.notify("my-event", "タイトル", "本文", "http://mattn.kaoriya.net/images/logo.png");
  return 0;
}
これだけで暗号化通信をサポートしたGNTP Growl通信が出来てしまいます。ちなみにDESな部分をAESに変えればそのままAES暗号通信する事が出来ます。CBC素晴らしい。

本当はboost 1.40.0に含まれる新しい機能を触るつもりだったのですが、まったく触れてもいませんね...苦笑

追記 githubにソースあげておいた。
mattn's gntppp at master - GitHub

GNTP++: gntp client library writen in C++

http://github.com/mattn/gntppp/tree/master
Posted at by