2017年7月6日の記事で紹介した通り、クリアコードは組み込みLinux向けにMozilla Firefoxを移植するプロジェクトGecko EmbeddedをWebDINO Japan(旧Mozilla Japan)様と共同で立ち上げ、開発を進めております。Yoctoを使用してFirefoxをビルドしたりハードウェアクセラレーションを有効化する際のノウハウを蓄積して公開することで、同じ問題に悩む開発者の助けになることを目指しています。
この記事では、Gecko Embedded本体ではなく周辺のミドルウェアのFluent Bitを移植した話を書きます。 Fluent Bitは、センサなどの組み込み機器向けのデータコレクタで、Fluentdより軽量です。
1月時点では、fluent-bitのビルド及び動作は以下のボードで確認しています。
レシピはGitHubにて公開しています。https://github.com/clear-code/meta-fluent
Yoctoに組み込むには、meta-fluentを git clone
したのち、(bitbakeのビルドディレクトリ)/conf/local.confへ
IMAGE_INSTALL_append = " fluent-bit "
(bitbakeのビルドディレクトリ)/conf/bblayers.confへ
BBLAYEYS += " ${TOPDIR}/../meta-fluent "
をそれぞれ追加し、bitbakeを実行します。
また、fluent-bit本体にもYoctoレシピがありましたがx86_64向けのビルドしかされていないようだったため、armv7hまたはaarch64向けのクロスビルドが通らないことをIssueにしました。
このIssueはこのpull requestによって解決済みです。
fluent-bitを上記の設定ででビルドした場合、fluent-bit
コマンドが起動イメージにインストールされます。
$ fluent-bit -i dummy -o stdout
Fluent-Bit v0.12.10
Copyright (C) Treasure Data
[2017/12/27 14:04:42] [ info] [engine] started
となれば動作確認は完了です。 設定ファイルの書き方やどのようなプラグインがあるかはFluent Bitの公式サイトのドキュメントを参照してください。
fluent-bitをarmv7hとaarch64がターゲットのYoctoレシピに載せ、RZ/Gシリーズのボードに載せた話を紹介しました。 ARMなどの組み込み向けボードはPCに比べてリソースの制約がありますが、fluent-bitではFluentdよりも軽量のため、リソース不足により動作が遅くなることは少ないでしょう。
現在クリアコードでは様々なSoCにGecko(Firefox)を移植するプロジェクト「Gecko Embedded」を進めています。
対象としているOSは組み込み用のLinuxですが、最近のSoC向けに提供されているLinuxベースのBSP(Board Support Package)はYoctoレシピの形で提供されることが多くなりました。SoCベンダーから提供されるYoctoのバージョンは、ベンダー側でビルドや動作を保証するために、最新版ではなく、少し前のバージョンであることがほとんどです。
一方で、対象BSP上でアプリケーション開発を行っていると、最新版のYoctoを使用したくなる場面も発生します。
しかし、開発対象のSoCで最新のYoctoを試すのは、移植が間に合っておらず困難なことが多いです。
対象としている問題は必ずしもSoC依存とは限らないため、この様な場面ではPC上で動作確認を行うのが手軽で便利でしょう。今回はその方法を紹介します。
Yoctoの公式ドキュメントは以下にまとめられています。
手っ取り早くPC上でビルドおよび動作を確認をする方法は「Yocto Project Quick Start」にまとめられています。最新のリリース版である2.4(Rocko)向けのドキュメントは以下になります。
本記事の内容は上記のドキュメントをベースとしていますので、正確な情報はそちらを参照して下さい。ただし、上記ドキュメントはリリース版を対象としているのに対し、本記事では開発版の最新を対象としますので、使用するブランチが異なります。
なお、Yoctoのバージョンは番号ではなくコードネームで識別されることも多いです。バージョン番号とコードネームの対応は以下のページで知ることができます。
ここではUbuntu 16.04LTSを対象としてビルド手順を紹介します。
まず、ビルド環境に必要なツールをインストールします。
$ sudo apt-get install gawk wget git-core diffstat unzip texinfo gcc-multilib \
build-essential chrpath socat cpio python python3 python3-pip python3-pexpect \
xz-utils debianutils iputils-ping libsdl1.2-dev xterm
次に、作業用ディレクトリを作成してYoctoレシピをダウンロードし、ビルド設定を行います。
$ export BUILD_DIR=/path/to/build (パスは適切に置き換えて下さい)
$ mkdir $BUILD_DIR
$ cd $BUILD_DIR
$ git clone git://git.yoctoproject.org/poky
$ source poky/oe-init-build-env
(ここでカレントディレクトリが自動的に $BUILD_DIR/build になります)
必要に応じて$BUILD_DIR/build/confディレクトリ下の設定ファイルを修正しますが、デフォルトではx86用のビルドイメージが作成されるようにセットされています。今回はこのデフォルトのビルド設定を使用しますので、設定ファイルの修正は必要ありません。
ここまでのセットアップが済んだら、以下のコマンドでビルドを実行します。
$ bitbake core-image-sato
以上でブートイメージを作成することができます。
なお、一度シェルを抜けるとsource poky/oe-init-build-env
でセットされたビルド用の環境変数がリセットされてしまいますので、次にビルドする際には、事前に再度source poky/oe-init-build-env
を実行する必要があります。
以下のコマンドを実行すると、ビルドしたブートイメージをQEMUでブートすることができます。
$ runqemu qemux86
以上でOSの起動はできましたが、本記事はGecko Embeddedプロジェクトを対象としたものなので、Firefoxもビルドに加えてみましょう。Firefoxをビルドするためには、meta-openembeddedレイヤおよびmeta-browserレイヤを追加する必要があります。
$ cd $BUILD_DIR
$ git clone git://git.openembedded.org/meta-openembedded
$ git clone https://github.com/OSSystems/meta-browser.git
$ cd build
また、conf/bblayers.conf
およびconf/local.conf
にそれぞれ以下の行を追加します。
$BUILD_DIR/build/conf/bblayers.conf
:
BBLAYERS += " \
${TOPDIR}/../meta-openembedded/meta-oe \
${TOPDIR}/../meta-browser"
$BUILD_DIR/build/conf/local.conf
:
IMAGE_INSTALL_append = " firefox "
IMAGE_INSTALL_append = " source-han-sans-jp-fonts "
再度bitbake core-image-sato
を実行することで、Firefoxをブートイメージに追加することができます。
なお、Firefoxはラウンチャーから起動することができますが、デフォルトでは/etc/resolv.conf
が設定されていないため、名前解決を行うことができません(参考: Bug 5322)。ホスト名でWebサイトを開きたい場合は、ブートイメージ内の/etc/resolv.conf
に適切なDNSサーバを設定する必要があります。例えば以下のような行を追加すると良いでしょう。
nameserver 8.8.8.8
これで無事Webサイトを表示できるようになります。
上記の手順は最新の開発版を対象としているため、ときにはビルドが通らないこともありますが、フリーソフトウェアですので問題に気がついたときは開発元にフィードバックしましょう。
Yoctoへのフィードバック方法は下記のページを参照すると良いでしょう。
meta-openembeddedやmeta-browserへのフィードバックは、基本的にOpenEmbeddedプロジェクトのメーリングリストに行います。
meta-browserに関しては、GitHub上のissueやPull Requestでも受け付けてくれるようです。
PC上でYoctoの最新版を試す方法を紹介しました。特別な手順はほとんどありませんが、周りを見渡すと意外と活用されていないことに気がついたので、敢えて紹介してみました。
Gecko Embeddedプロジェクトではmeta-browserのFirefoxを最新のESR版に更新する作業も進めたいと考えていますが、開発リソースが足りず手が回っていないのが実情です。上記手順で開発環境を整えて、この作業を手伝って下さる方が現れることを期待しています。
Firefoxのアドオンを個人でインストールする場合、Firefoxのアドオンマネージャを経由するか、Add-onsサイトからインストールすることでしょう。
しかし、そのアドオンが利用できるようになるまでにどのようなプロセスを経るのかについては、あまり意識されることはありません。 一方で法人利用の場合、アドオンを自由にインストールさせたくないということも多いです。そのためアドオン利用の可否を制御したいという需要があります。その場合には上記のプロセスがどのようになっているのかを意識する必要がでてきます。
今回はFirefoxのアドオン利用の可否に影響する3つの壁について紹介します。
アドオンをインストールする際の最初の壁となるのが、 xpinstall.enabled
という設定項目です。
この値が true
のときのみアドオンをAdd-onsサイトからインストールすることができます。
アドオンを検索してインストールすることができるのも true
のときのみです。
またこの値はアドオンマネージャーの「アドオンを入手」という項目が表示されるか否かにも影響を与えます。true
ならば表示され、 false
だと表示されなくなります。
アドオンを入手が表示されている状態は上の図のとおりです。
アドオンを入手が表示されていない状態は上の図のとおりです。
ただし、アドオンのファイルを明示的に選択して「ファイルからインストール」することは xpinstall.enabled
の値に関わらず、依然として可能です。
法人向けカスタマイズではこれらも無効にすることが多いです。
アドオンをインストールする際の2つめの壁となるのが、 extensions.enabledScopes
という設定項目です。
この値によって、配置したアドオンが認識され、アドオンマネージャの一覧に表示されるか否かが決まります。
Firefoxがアドオンを自動的に認識する配置場所はいくつかあります。
Firefoxは上記のパターンそれぞれに整数値を割り当てており、 extensions.enabledScopes
にはそれらを加算した値を設定することになっています。
配置したアドオンが認識された後、壁となるのが、 extensions.autoDisableScopes
という設定項目です。
この値によって、配置したアドオンが自動的に無効化されるか否かが決まります。
この項目は、ユーザーの明示的な指示を介さずに何らかのアドオンがインストールされたことをFirefoxが検出した際に、当該アドオンを本当に有効化してよいかどうかユーザーに確認を求める、という機能の挙動を制御します。意図せずマルウェアなどの悪意あるソフトウェアが配置されてしまっても、この設定が適切であれば無効化できる可能性があります。
extensions.enabledScopes
同様に値を設定することで、自動的に無効化する対象を制御します。
注意すべきは、「ユーザーの明示的な指示を介さずにインストールされた」アドオンが対象となることです。ユーザーが明示的にインストールしたものについてはこの設定の対象外です。アドオン利用をさせたくない事例では、そもそもインストールさせないというカスタマイズも合わせて必要になります。
これまで述べた3つの壁を図示すると次のようになります。
このように上記の条件をすべて乗り越えたアドオンだけが利用できるようになります。
今回は、普段Firefoxのアドオンを利用していて意識することのない、「Firefoxのアドオン利用の可否に影響する3つの壁」について紹介しました。Firefoxにインストールされるアドオンを管理したい企業内利用では重要なポイントになります。
extensions.enabledScopes
および extensions.autoDisableScopes
の値による挙動の違いの詳細については、別の機会に記事にしたいと思います。
FirefoxやThunderbirdの導入やカスタマイズでお困りで、自力での解決が難しいという場合には、有償サポート窓口までぜひ一度ご相談下さい。
FirefoxやThunderbirdのアドオンを使用していて、ある日急に動かなくなった、という話はよくあります。 そのような場合にどうやって原因を特定し解決するのか、Thunderbird用のアドオンであるTheme Font & Size Changer for TBでの場合を例に解説してみます。 トラブルシューティングの一事例として参考にしていただければ幸いです。
まず何をおいても最初に確認するべきは、そのアドオンがどのような性質を持ちどのような機能を実現するアドオンなのかという点です。
今回問題が起こった「Theme Font & Size Changer for TB」はThunderbirdのGUIの文字サイズや配色を変更するアドオンです。 Thunderbirdではメールの内容を解釈したり表示したりする部分と全体的なUIを制御する部分とはほとんど関係がないため、例えば「受信したメールの内容」は問題と無関係である可能性が高い、といった推測ができます。 また、特定のWebサービスと連携するようなアドオンでもないため、Webサービス側からの接続遮断や、Webサービスのメンテナンスによる機能停止といった物もこの問題には関係していないと考えられます。
このように、原因究明を迅速に行うためには、可能性が低い事柄を調査の対象から一旦除外するのが有効です。
次に確認するべきなのは、可能性として除外しきれなかった範囲における、問題が発生し始めた時期に行った操作との関連性です。 例えば以下のような事柄です。
変化が可逆的な物である場合、まずそれを元に戻してみるのが基本です。 今回の事例では問題の発生前後で特にこれといった操作を行っておらず(休暇明けに出勤したら突然アドオンが動作しなくなっていた、のような状況を想像して下さい)、ユーザーが行った操作は原因ではない可能性が高いと思われました。
Thunderbirdは自動更新機能を持っており、ユーザーが気がつかないうちに新しいバージョンがダウンロードされている事があります。 アドオンが新しいバージョンのThunderbirdに対応していない場合、Thunderbirdの自動更新に伴ってアドオンが無効化されたり、アドオンが動作しなくなったりする事があります。
今回の事例では、問題の発生前後でThunderbirdの新しいリリースは行われていませんでした。 ですので、この可能性は除外しました。
Thunderbird本体と同様に、自動更新によってアドオンが新しいバージョンに入れ替わった事で問題が起こる事もあります。 一般的にはあまりありませんが、「更新前は正常に動いていた機能が更新後には動かなくなる」という種類の問題(後退バグ、あるいはリグレッション)が混入する場合があるからです。
また、問題が起こっているアドオンそのもの以外の他のアドオンが更新された事がきっかけで問題が起こる事もあります。 アドオン同士が衝突して片方あるいは両方の動作が妨げられるという事例は枚挙にいとまがありません。
ただ、今回の事例では、問題の発生前後でアドオンの更新は特に発生していませんでした。 問題が起こっているアドオンの最終更新日は2017年11月で、だいぶ以前の事ですので、自動更新が急に行われたという事は考えにくいです。
問題の発生前後で特にこれといった変化が無かったとなると、後はアドオンの初期化処理を丁寧に追って、どの時点で問題が発生しているのかを突き止めるという事になります。
Thunderbird用アドオンは、現在Firefox用のアドオンで「レガシー」と位置づけられている種類のアドオンと同様の作りになっています。 今回問題となったアドオンの、問題発生時の最新版であるバージョン62.0は、その中でもBootstrapped Extensionと呼ばれる、アドオンのインストール・アンインストール時にThunderbird自体の再起動が不要な種類の物でした。
Bootstrapped Extensionの初期化処理のステップを追うと、以下の箇所のThemeFontSizeChangerBootstrapAddon.compile()
がfalse
を返しているために初期化処理が中断されているという事が分かりました。
function startup(data, reason) {
try{
if(!ThemeFontSizeChangerBootstrapAddon.compile()) return;
ThemeFontSizeChangerBootstrapAddon.startup(data,reason);
} catch(e){
ThemeFontSizeChangerBootstrapAddon.lg(e,1);
}
}
さらにこのメソッドの内容を調査すると、以下の通り実装されている事が分かりました。
...
const build = 1507391194;
...
compile:function(){
if((build+'').length != 10) Services.prompt.alert(null, 'CompileAddon', 'An error occured');
if(new Date().getTime() > ((build+'').length == 10 ? build*1000 : build)+(3*30*24*60*60*1000)) return false;
return true;
}
new Date().getTime()
は実行時のUNIX時刻をミリ秒単位で取得する物です。1507391194
というのはいわゆるUNIX時間(協定世界時1970年1月1日0時0分0秒からの経過秒数による日時の表現)で2017年10月7日15時46分34秒を指しており、この箇所全体では「現在日時がその3ヶ月後(2018年1月5日15時46分34秒)を超えているとfalse
が返される」という意味になっています。
つまり、それまでは全く正常に動作していたこのアドオンは、世界中で今年の1月5日以降急に動作しなくなるよう設計されていたという、時限式のタイマーが組み込まれていたという事が分かりました。
以上の事から、この問題の対策として「当該箇所を無害化する改修を施す」あるいは「実行環境の時計を意図的にずらす」のいずれかを実施する必要があるという事になります。
「Theme Font & Size Changer for TB」を例にとって、アドオンが急に動作しなくなったという場合の調査の具体的な進め方をご紹介しました。
当社のFirefox/Thunderbrid法人向けサポートでは、アドオンに関するものについても、障害の原因調査から対策のご提案、改修版のアドオンの提供や開発元へのフィードバックなどの対応を有償にて行っております。 業務で使用しているアドオンで致命的な問題が発生していてお困りの場合には、ぜひ一度当社までご相談下さい。
クリアコードの藤本です。皆さん、Pythonでプログラムを書いていますか?
本記事から始まる前後編2回の連載では、Pythonの CFFIライブラリ — Cで実装された処理をPythonから呼び出すための仕組み — について解説したいと思います。
ここで "Cで実装された処理を呼び出す" とは、
例えば /usr/lib/libc.so
に格納されているCのルーチンを、
Pythonプログラムから直接コールすることを指しています。
前編にあたる本記事では、
まず最初にCFFIライブラリの大きな仕組みについて簡単な説明を加えた上で、
実際にこのライブラリを動かしてみたいと思います。
Pythonで会員制のWebサービスを開発しているとしましょう。
ユーザーの認証処理をセキュアに実装するために、 暗号ライブラリ libsodium のパスワードハッシュ関数を使いたいと考えたとします。 この暗号ライブラリは、もともと C(++) 向けに提供されているものなので、 何とか工夫してPythonから必要な関数を呼び出せるようにする必要があります。 どうすれば、これを実現できるでしょうか?
この問題に対する伝統的な戦略は、CPythonが提供しているC言語向けのAPIを利用して、 拡張モジュールを作成することです。具体的には次のようにヘッダ定義を読み込んで、 ラッパ関数を一つ一つ作成していくことになります:
#include <Python.h>
#include <sodium.h>
/*
* PythonのC/APIで crypto_pwhash_str_verify() 関数をラップする
*/
static PyObject* sodium_pwhash_str_verify(PyObject *self, PyObject *args)
{
char *hash, *password;
Py_ssize_t len;
int res;
// 引数をCの型に変換する (Python -> C)
if (!PyArg_ParseTuple(args, "yyn", &hash, &password, &len))
return NULL;
// 対象の関数をコールする
res = crypto_pwhash_str_verify(hash, password, len);
// 返り値をPythonのオブジェクトに変換する (C -> Python)
return PyLong_FromLong(res);
}
// 他の関数も定義してモジュールとしてエクスポートする
この戦略は20年以上に渡って用いられており、 2018年現在も多くのPythonモジュールがこの方式を採用しています。 したがって、この方式が現実に(それもかなり有効に)機能することにはほとんど異論がありません。
一方で、この方式で拡張モジュールを作成するのには、 それなりの背景知識が要求されるというのもまた事実です。 およそC/APIは実行系/ランタイムの内部実装と表裏一体なので、 CPythonに特有の仕組みや細かい決まりごと(たとえば参照カウントの扱いや例外をめぐる処理) をおさえておく必要があるからです。
CFFIはこのようなPythonとCの間の橋渡しを楽にするために開発されたFFI (Foreign Function Interface) ライブラリです。 LuaJITのFFI実装を参考に、2012年に開発がスタートしました。 2015年にメジャーバージョンの1.0.1がリリースされており、 既に2Dグラフィック処理の cairo や、 サウンドサーバーの JACK との連携などに幅広く利用されています。
最初の例に戻りましょう。
そもそも、Cで提供される関数は(原則として)入出力が型によってきっちり定義されるので、
Pythonとの結合部分はある程度自動で生成できるんじゃないか、というのはごく自然な発想です。
例えば、int foo(const char *)
という関数定義が与えられたとすると、
引数の char*
をPythonのバイト列に、
返り値の int
をPythonの整数オブジェクトに機械的に対応させるのは、
それほど難しいことではなさそうです。
CFFIライブラリはまさにこの"つなぎ"の部分の自動生成を行ってくれます。
実例で見てみましょう。まずは、次の内容をbuild.py
というファイルに保存します:
from cffi import FFI
ffibuilder = FFI()
# libsodiumのヘッダ情報をincludeする
ffibuilder.set_source("sodium", """
#include <sodium.h>
""", libraries=["sodium"])
# Python化したいC関数の定義を記述する
ffibuilder.cdef("""
int crypto_pwhash_str_verify(const char * hash,
const char * const passwd,
unsigned long long passwdlen);
""")
if __name__ == "__main__":
ffibuilder.compile(verbose=True)
この例では、libsodiumの関数 crypto_pwhash_str_verify()
を移植しています
(上のC/APIの例で用いたのと同じ関数です)。
set_source()
で読み込むライブラリを指定し、
cdef()
で関数定義を与えているのが見て取れると思います。
依存関係をインストールした上で、このスクリプトを実行しましょう。 すると、CFFIが定義情報からPython向けのラッパ実装を自動的に生成し、 コンパイルまで行ってくれます:
# 必要なライブラリのインストール (Ubuntu/Debian)
$ sudo apt install python3-cffi libsodium-dev
...
# C拡張コード生成 + コンパイル
$ python3 build.py
...
# 生成結果
$ ls
build.py sodium.c sodium.cpython-35m-x86_64-linux-gnu.so sodium.o
このsodium.c
がC拡張のソースコードで、
sodium.*.so
というのがコンパイル済みのモジュールです。
このモジュールはそのままPythonから呼び出すことができます。 試しに、適当な入力を与えてみましょう:
>>> from sodium import lib
>>> passwd = b'secret'
>>> pwhash = (b'$argon2i$v=19$m=131072,t=6,p=1$r/g1+z50+W9RWUMRy4xu+g$'
... b'BNWoK+o6Hlcu98scoCxlNrYGo8hacShQ2nkc4RS5wZk')
>>> lib.crypto_pwhash_str_verify(pwhash, passwd, len(passwd))
0
この例で、C言語の型とPythonのオブジェクトが透過的に接続されているのが 確認いただけると思います。
連載の前編を締めるにあたって、大急ぎで次の二つの点を指摘しておきたいと思います。
まず第一は、このコードジェネレーティングの戦略は万能の解決策ではないということです。 前節で見たとおり、簡単な関数であれば簡単に移植できます。 しかし、真面目にライブラリを移植しはじめると、 ほぼ間違いなく自動生成だけでは対応できないケースに出くわすことになります。
この代表例がポインタです。とくに、Cのコードでよく使われるテクニックとして 「呼び出し側でデータ領域を確保して、ポインタ経由で関数に中身を書き換えてもらう」 というコードパターンがありますが、これをどうPythonのコードに置き換えるかは、 決して自明ではありません。例えば、次の関数を移植する方法を考察してみてください:
// `size`バイト分のランダムデータで`buf`を埋める
void randombytes_buf(void * const buf, const size_t size);
この問題の解決は後編に回したいと思います。
もう一つは、CFFIライブラリの技術的な位置づけについてです。 実はここまでの「CFFI = C拡張の自動生成ライブラリ」という定式化はかなり話を端折ったものです。 この点については、他の競合する技術(Cython/ctypes/SWIG)との関係で説明する必要があるので、 後編で一節をまるまる割く予定です。
Firefox 56およびそれ以前のバージョンで使用可能だった従来型アドオンと、Firefox 57以降のバージョンで使用可能なWebExtensionsベースのアドオンでは、法人での利用においても様々な点で違いがあります。その1つが、管理者による設定の集中管理の方法です。
現在のFirefox ESR52およびThunderbird 52で従来型のアドオンの設定を集中管理するには、Firefox/Thunderbird自体の設定を集中管理するための仕組みであるMCDを使用します。
その一方で、WebExtensionsベースのアドオン用には標準的な設定の集中管理のための仕組みが用意されていません。 Firefox ESR60以降のバージョン(正確には、Firefox 58以降)ではManaged StorageというAPIが実装されているため、将来的にはこれが主流となっていくと思われます(実際、当社で開発しているIE View WEをはじめとしてManaged Storageに対応しているアドオンは既に存在しています)。 しかしながら、この機能を使うためにはアドオン自体がそれ前提の設計となっている必要があり、Managed Storageでは設定を管理できないというアドオンの設定の集中管理の方法は依然として必要となります。
この記事ではその1つとして、設定をCookieで管理している種類のアドオンについての対応をご紹介します。
WebExtensionsベースのアドオンは、一般的なWebページに組み込むJavaScriptのコードと同じ要領で記述します。 Cookieを使って設定を保存したい場合も、以下のようにごく一般的な書き方で実現できます。
function setCookie(key, value) {
// 10年間だけ設定を保持する場合。無期限であれば expires=... の部分自体を省略する。
const TEN_YEARS_IN_MSEC = 1000 * 60 * 60 * 24 * 356 * 10;
const expireDate = new Date(Date.now() + TEN_YEARS_IN_MSEC);
document.cookie = `${key}=${value}; expires=${expireDate.toGMTString()}; path=/`;
}
// setCookie('Enabled', 'true') のように使う
function getCookie(key) {
var parts = document.cookie.split(`${key}=`);
if (parts.length > 1)
return parts[1].split(';')[0];
else
return '';
}
// getCookie('Enabled') で 'true' のような文字列値が返る
ところで、一般的にCookieはホスト名とパスに紐付ける形で保存されます。 Webページであればホスト名は明白ですが、アドオンから保存したCookieの場合、ホスト名はどうなるのでしょうか?
アドオンは通常、他のアドオンとの識別のためのIDを持っています。これは多くの場合メールアドレスのような形式で表記され、addon-name@clear-code.com
のような文字列です。
ということは、このIDがCookie保存時のホスト名の代わりに使われるのでしょうか。
実はそうではありません。実際にはアドオンのIDではなく、自動生成された内部的なUUIDが使われます。 これには、いわゆるフィンガープリンティングを防ぐためという意味があります。
仮にアドオンの内部リソースに対して、moz-extension://addon-name.at.clear-code.com/main.js
のようにアドオンのIDに基づいたURLが割り当てられていたとすると、Webページ上のコンテンツから容易に読み込みを試行できてしまい、訪問者の使用アドオンを特定されたり、アドオンの使用状況から個人を特定されてしまう可能性があります。
このようなフィンガープリンティングを無効化する目的で、Firefoxはアドオンがインストールされるごとに新しいUUID(ユニークな識別子となる文字列)を自動生成し、それを使ってアドオンの内部リソースを moz-extension://a93cdbe2-b549-401c-ad50-56572822cee3/main.js
のようなURLで表すようになっています。
この例でホスト名に相当する部分に現れているa93cdbe2-b549-401c-ad50-56572822cee3
がそのアドオンに割り当てられたUUIDで、環境ごとに異なる物が生成され、それどころか同じ環境であっても、そのアドオンをアンインストールした後で再インストールすると新たに別のUUIDが生成されます。
アドオンで保存したCookieは、この自動生成されたUUIDをホスト名代わりにして管理されています。
MCD用設定ファイルは一般的にlockPref()
などのディレクティブを使用して設定を定義します。
しかし実際の所は、MCD用設定ファイルはJavaScriptとして解釈されており、if
による条件分岐やfor
などでのループ処理の他、XPConnect経由で各種XPCOMコンポーネントの機能を呼び出すことすらもできます。
応用すれば、MCD用設定ファイルでアドオン用のCookieを設定するという事もできます。
MCD用設定ファイルの中でCookieの値を設定する際は、上記のような一般的なWeb上のJavaScriptの作法とは事なり、Cookieを管理するためのXPCOMコンポーネントを直接呼び出す方法を取ります。以下はFirefox ESR52で動作する実装例です。
function setCookie(params) {
const ONE_DAY_IN_MSEC = 1000 * 60 * 60 * 24;
const CM = Components.classes['@mozilla.org/cookiemanager;1']
.getService(Components.interfaces.nsICookieManager2);
const expiry = params.expireDays ?
Date.now() + (params.expireDays * ONE_DAY_IN_MSEC) :
null;
try {
CM.remove(params.host,
params.key,
params.path || '/',
false,
'');
} catch(e) {}
CM.add(params.host,
params.path || '/',
params.key,
params.value,
false,
false,
false,
false,
expiry,
{});
}
setCookie({
host: 'a93cdbe2-b549-401c-ad50-56572822cee3',
key: 'Enabled',
value: 'true'
});
例の最後に示している通り、Cookieを保存するホスト名としてアドオンの内部UUIDを明示的に指定する必要があります。 そうなると問題になるのが、その内部UUIDはどこで調べられるのか?という事です。
WebExtensionsベースのアドオンをインストールすると、そのアドオンに関する情報がextensions.webextensions.uuids
という名前の文字列型の設定に保存されます。この値はJSON形式なので、JSON.parse()
で簡単に解析できます。
以下はFirefox ESR52で動作する、必要な情報が既に存在している場合とまだ存在していない場合の両方に対応した、アドオンの内部UUIDを取得する実装の例です。
const { Services } = Components.utils.import('resource://gre/modules/Services.jsm', {});
function tryInitializeCookie() {
try {
// アドオンのIDからUUIDを解決する
let uuids = Services.prefs.getCharPref('extensions.webextensions.uuids');
if (uuids) {
let json = JSON.parse(uuids);
let host = json['addon-name@clear-code.com']; // 'a93cdbe2-b549-401c-ad50-56572822cee3'
if (host) {
setCookie({
host,
key: 'Enabled',
value: 'true'
});
return true;
}
}
} catch(e) {}
return false;
}
// アドオンの情報がすでに存在していれば、即座にCookieを上書きする
if (!tryInitializeCookie()) {
// そうでない(初回起動時で、アドオンの情報がまだ存在していない)場合、
// 設定値の変化を監視して、アドオンの情報が保存された時点で
// Cookieを上書きする
Services.prefs.addObserver('extensions.webextensions.uuids', (subject, topic, data) => {
if (data == 'extensions.webextensions.uuids' &&
topic == 'nsPref:changed')
tryInitializeCookie();
}, false);
}
このようなコードをMCD用設定ファイルに記載しておくことで、Cookieで設定を管理する種類のアドオンであっても管理者側で設定を集中管理できます。
WebExtensionsベースのアドオンのうち、Cookieで設定を管理するように設計されたアドオンについて、MCD用設定ファイルで設定を集中管理する方法を解説しました。
現時点ではこの記事で解説したような方法でCookieを編集できますが、この方法は今後のFirefoxの更新で使えなくなっていく可能性があります。
特にFirefox 57でレガシーアドオンのサポートが打ち切られて以降は、Firefoxの内部的なAPIの互換性を維持する必然性がなくなったということで、その変化が一層加速しています。
また、設定の保存方法にはこれ以外にも、Web APIとして一般的なlocalStorage
や、WebExtensions APIのLocal Storageなど色々な方法があり、それらの対応にも気を配っていく必要があります。
当社では、FirefoxやThunderbirdの使用時に起こる様々なトラブルに対する技術的サポートを有償にて行っております。 管理者側でアドオンの設定を管理したいものの、方法が不明でお困りという場合には、ぜひ当社までご相談下さい。
LogstashとFluentdはどちらもログ収集を行うソフトウェアです。 この二つのソフトウェアの内部で扱われているデータのエンコード方法が異なるため、どちらかでデータ形式の変換を行わないといけません。 この記事はLogstashとFluentdの連携方法のうち、Logstash側で変換を行い、連携する方法を解説します。
LogstashもFluentdと同じくプラグイン機構を持っています。
の4種のプラグインが作成できるようになっています。
FluentdのforwardプロトコルはHALOやHANDSHAKEを省略することもできるTCPの上に乗っているプロトコルです。 そのため、TCPで待ち受けることができます。この時に、msgpackをデコードできる必要があります。
下図では、Fluentdの out_forward
プラグインからLogstashのinputプラグインの一つのtcpプラグインで受け取り、
codecプラグインのlogstash-codec-fluentプラグインにより、Forwardプロトコルで規定されたmsgpackをデコードします。
+---------------------+ +----------------------+
| | | |
| |out_forward input/tcp + fluent codec |
| Fluentd +-----------------------------> Logstash |
| | tcp/24224 | |
| | | |
+---------------------+ +----------------------+
この時、FluentdとLogstashの設定ファイルはそれぞれ次のようになります。
<source>
@type dummy
@id dummy_input
tag my.logs
</source>
<match my.logs>
@type forward
time_as_integer true
<server>
host localhost
</server>
<buffer>
flush_interval 1s
</buffer>
</match>
この時、 time_as_integer true
の設定をしていることに注意してください。
これは、logstash-code-fluentのissueにしてありますが、
Fluentdがmsgpackのエンコードで使用している EventTime
拡張が扱えないことによるものです。
input {
tcp {
codec => fluent
port => 24224
}
}
output {
stdout { codec => json }
}
実際に接続するには、それぞれ別の端末から設定ファイルを読み込んで実行するようにします。
$ logstash -f logstash-in.conf
$ bundle exec fluentd -c fluent-out.conf
logstash側のターミナルで
[2018-01-24T10:52:15,914][INFO ][logstash.pipeline ] Pipeline started {"pipeline.id"=>"main"}
[2018-01-24T10:52:16,009][INFO ][logstash.agent ] Pipelines running {:count=>1, :pipelines=>["main"]}
<snip>
{"message":"dummy","tags":["my.logs"],"@timestamp":"2018-01-24T01:54:08.000Z","host":"localhost","@version":"1","@metdata":{"ip_address":"0:0:0:0:0:0:0:1"},"port":55571}
の表示が出たら成功です。
+---------------------+ +----------------------+
| | | |
| |output/tcp in_forward |
| Logstash +-----------------------------> Fluentd |
| | tcp/24224 | |
| | | |
+---------------------+ +----------------------+
この時、FluentdとLogstashの設定ファイルはそれぞれ次のようになります。
<source>
@type forward
</source>
<match log>
@type stdout
</match>
input {
generator {
lines => [
"line 1",
"line 2",
"line 3"
]
count => 3
}
}
output {
tcp {
codec => fluent
host => localhost
port => 24224
}
}
ここで、Logstash側ではtagの指定をしていないことに注意してください。 現在執筆しているときにリリースされているlogstash-codec-fluent v3.1.5ではユーザー定義のタグを使用するとFluentd側に受け渡せないバグがあります。 このバグについても開発元に報告しました。
実際に接続するには、それぞれ別の端末から設定ファイルを読み込んで実行するようにします。
$ bundle exec fluentd -c fluent-in.conf
$ logstash -f logstash-out.conf
Fluentd側のターミナルで
2018-01-24 12:21:53.000000000 +0900 log: {"message":"line 3","@timestamp":"2018-01-24T03:21:53.008Z","host":"<hostname>.local","@version":"1","sequence":2}
2018-01-24 12:21:53.000000000 +0900 log: {"message":"line 2","@timestamp":"2018-01-24T03:21:53.006Z","host":"<hostname>.local","@version":"1","sequence":0}
2018-01-24 12:21:53.000000000 +0900 log: {"message":"line 1","@timestamp":"2018-01-24T03:21:53.007Z","host":"<hostname>.local","@version":"1","sequence":1}
2018-01-24 12:21:53.000000000 +0900 log: {"message":"line 3","@timestamp":"2018-01-24T03:21:53.007Z","host":"<hostname>.local","@version":"1","sequence":1}
2018-01-24 12:21:52.000000000 +0900 log: {"message":"line 1","@timestamp":"2018-01-24T03:21:52.980Z","host":"<hostname>.local","@version":"1","sequence":0}
2018-01-24 12:21:53.000000000 +0900 log: {"message":"line 2","@timestamp":"2018-01-24T03:21:53.007Z","host":"<hostname>.local","@version":"1","sequence":1}
2018-01-24 12:21:53.000000000 +0900 log: {"message":"line 2","@timestamp":"2018-01-24T03:21:53.008Z","host":"<hostname>.local","@version":"1","sequence":2}
2018-01-24 12:21:53.000000000 +0900 log: {"message":"line 3","@timestamp":"2018-01-24T03:21:53.007Z","host":"<hostname>.local","@version":"1","sequence":0}
2018-01-24 12:21:53.000000000 +0900 log: {"message":"line 1","@timestamp":"2018-01-24T03:21:53.007Z","host":"<hostname>.local","@version":"1","sequence":2}
の表示が出たら成功です。
FluentとLogstashを連携してデータのやり取りをする方法を解説しました。 LogstashのプラグインはFluentdとは異なる種類のものが開発が活発であり、CPUやメモリ使用量の傾向が異なります。 Logstashにしかない機能を使いたいけれども柔軟に設定が可能なFluentdも引き続き使用したい時には、 この記事のようにLogstashからFluentdへのデータを受け渡す方法を試してみてはいかがでしょうか。
なかなか文章を書く時間を作れていない須藤です。
去年の末にgihyo.jpにOSS(オープンソースソフトウェア)の開発に参加したいあなたへという記事を書いていて、今年の始めに公開されました。2年ほど前からOSS Gateという「OSSの開発に参加する人を継続的に増やす取り組み」をしているのですが、そこでの知見をまとめました。
まわりにOSSの開発に参加したい人がいたら教えてあげてください。