Microsoft Word で LL 言語から差し込み文書作成

タイトルは釣り。
簡単な帳票 (Excel 系じゃなくて Word 系の文書) をプログラムから作成する必要がでてきたので調べた。テンプレートとなる文書にちょっとした変数の差し込みを行う必要があるケース。
ただし

  • 旧来の Microsoft Office Document (OLE2) はサポートしない
  • COM や UNO は使わない
  • (ODF や OOXML の) 仕様書は読まない

LibreOffice (OpenOffice.org) や最近の Microsoft Office だと,XML ファイルを ZIP で固めているだけなので単純な差し込みだけならわりと簡単にできそう。
注意: ODF にしても OOXML にしても XML namespace を使っているので,厳密なことをいうと以下の例のようにタグ名を決め打ちで置換していてはうまくいかないことがありうる。とはいえそうそう変更しないだろうからこのままでうまくいくだろうし,気になるなら対象タグの xmlns の部分だけバリデーション (というかサポートするターゲットかどうか判別) すればいいんじゃないかと思う。

OpenDocument Format (ODF) 編

LibreOffice 3.3.2 Writer on Ubuntu 11.04 で作成したドキュメントで検証をおこなった。
内容は「こんにちは ○○ さん」というだけの単純なもの。○○の部分にプログラムから差しこみたい。

テンプレート文書の作成

差しこみフィールドの作成のやり方。

  1. 「挿入」メニューから「フィールド」「その他」メニューを選択する
  2. 「機能」タブを選択
  3. 「フィールドタイプ」で「プレースホルダ」を選択,「書式」は「テキスト」を選択
  4. プレースホルダ」に何らかのプレースホルダ名 (テンプレート変数的なもの) をいれる
  5. 「挿入」ボタンを押下
展開
$ unzip ../hello.odt

Archive:  ../hello.odt
 extracting: mimetype                
  inflating: content.xml             
  inflating: manifest.rdf            
  inflating: styles.xml              
 extracting: meta.xml                
 extracting: Thumbnails/thumbnail.png  
  inflating: Configurations2/accelerator/current.xml  
   creating: Configurations2/progressbar/
   creating: Configurations2/floater/
   creating: Configurations2/popupmenu/
   creating: Configurations2/toolpanel/
   creating: Configurations2/menubar/
   creating: Configurations2/toolbar/
   creating: Configurations2/images/Bitmaps/
   creating: Configurations2/statusbar/
  inflating: settings.xml            
  inflating: META-INF/manifest.xml   

無駄に空フォルダが多いが,実態は追える範囲。

解析と置換

文書の内容はおもに content.xml に記述されている。
今回の主眼となる部分の抜粋 (改行やインデントは編集してある; 元は全部1行*1 )。

<text:p text:style-name="Standard">
  こんにちは
  <text:placeholder text:placeholder-type="text">&lt;hogehoge&gt;</text:placeholder>
  さん!
</text:p>

単純明快。
Perlワンライナーでざっくり置換してみた。XML を展開・再構築するほうが行儀いいんだろうけど,バリデーションを行うのでもなければ PCRE で必要十分な気がする。

$ perl -i -pe 's{<text:placeholder .*?>
                 \s* &lt;hogehoge&gt; \s*
                 </text:placeholder>      }{dayflower}gxms' content.xml

これで一応完成。だけど,文書の統計情報 (何文字含まれているかとか何ページあるかとか) も文書に含まれている。基本的に文書ファイルのプロパティで表示するためだけのものなので間違っていても構わないんだけど,なんか気持ち悪いので削除しておく。
統計情報の実態は meta.xml というファイルの <meta:document-statistic/> タグに存在する。以下は例。

<meta:document-statistic
   meta:table-count="0"
   meta:image-count="0"
   meta:object-count="0"
   meta:page-count="1"
   meta:paragraph-count="1"
   meta:word-count="3"
   meta:character-count="20"/>

なので meta.xml ファイルから該当する XML タグを削除しておけばよい。

再構築

普通に再圧縮するだけ。

$ zip -r9 ../new.odt *

updating: Configurations2/ (stored 0%)
updating: META-INF/ (stored 0%)
......

作成された文書を再度 LibreOffice で開いてみたところ,きちんと埋め込みされていた。

Office Open XML (OOXML) 編

Microsoft Office 2010 Word で作成したドキュメントで検証をおこなった。

テンプレート文書の作成

差しこみフィールドの作成のやり方。

  1. 「挿入」メニュータブにおいて「クイックパーツ」を選択し「フィールド」メニューを選択
  2. 「フィールドの選択」で「差し込み印刷」フィルタを選び*2,「フィールドの名前」から「MergeField」を選択
  3. 「フィールドプロパティ」の「フィールド名」に適宜名前 (テンプレート変数的なもの) をつける
展開
$ unzip ../hello.docx

Archive:  ../hello.docx
  inflating: [Content_Types].xml     
  inflating: _rels/.rels             
  inflating: word/_rels/document.xml.rels  
  inflating: word/document.xml       
  inflating: word/theme/theme1.xml   
  inflating: word/settings.xml       
  inflating: word/webSettings.xml    
  inflating: word/stylesWithEffects.xml  
  inflating: docProps/core.xml       
  inflating: word/styles.xml         
  inflating: word/fontTable.xml      
  inflating: docProps/app.xml        

無駄な空フォルダは少ない。結構多くのファイルに分散しているのは ODF と同等。

解析と置換

文書の内容はおもに word/document.xml に記載されている。
今回のターゲットとなる部位の抜粋。

<w:p w:rsidR="00332438" w:rsidRDefault="00792A30">
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t>こんにちは</w:t>
  </w:r>
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t xml:space="preserve"> </w:t>
  </w:r>
  <w:fldSimple w:instr=" MERGEFIELD  hogehoge  \* MERGEFORMAT ">
    <w:r>
      <w:rPr>
        <w:noProof/>
      </w:rPr>
      <w:t>&#171;hogehoge&#187;</w:t>
    </w:r>
  </w:fldSimple>
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t xml:space="preserve"> </w:t>
  </w:r>
  <w:r>
    <w:rPr>
      <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t>さん!</w:t>
  </w:r>
  <w:bookmarkStart w:id="0" w:name="_GoBack"/>
  <w:bookmarkEnd w:id="0"/>
</w:p>

埋め込みを行う立場から見ると,ちょっとめんどい (あくまで ODF に比べての話だけど)。MergeField じゃないもっと適切なフィールドタイプがあるのかもしれない。

フィールド埋め込みについては Perlワンライナーで書くなら以下のような感じ。

$ perl -pe 's{<w:fldSimple .*?>
                (.*? <w:t>)
                  .*?
                (</w:t> .*?)
              </w:fldSimple>    }{$1dayflower$2}gxms' document.xml

さきほどの ODF の例とは置換内容がちょっと違う (全埋め込みフィールド対象になっちゃってる) けど,ちゃんとやるなら置換後のほうにコード仕込んでとかやるかな。
OOXML でもやはり文書の統計情報が存在する。実態は docProps/app.xml のいくつかのタグ。以下抜粋。

<Pages>1</Pages>
<Words>9</Words>
<Characters>52</Characters>
<Lines>1</Lines>
<Paragraphs>1</Paragraphs>
再構築
$ zip -r9 ../new2.docx *
  adding: [Content_Types].xml (deflated 75%)
  adding: _rels/ (stored 0%)
......

作成された文書を再度 Microsoft Word 2010 で開いてみたところ,きちんと埋め込みされていた。

同じ値を複数ヶ所に同時に埋め込みたい場合

もちろん全プレースホルダを上記のようにプログラムから書き換えてもいいんだけど,もっとスマートな解決策もある。OpenDocument などにはユーザ定義変数のようなものがあって,それを文書中に埋め込むことができる。この変数の値の部分だけ書き換えれば,文書中のそのユーザ変数フィールドは全部置き換わることになる。
以下は OpenDocument での例。面倒なので Microsoft Word ではどうやるか調べていないが,同等の機能はたぶんあると思う。

  1. 「挿入」メニューから「フィールド」「その他」メニューを選択する
  2. 「変数」タブを選択
  3. 「フィールドタイプ」で「ユーザー欄」を選択
  4. 「名前」フィールドに「変数名」を,「値」フィールドにデフォルトの値を入力
  5. (書式は「テキスト」を選択しておくのが無難)

これでユーザ定義変数が定義される (「選択」ペインにユーザ変数一覧が表示される)。
あとは文書中の挿入したい場所にカーソルを移動して「フィールド」の「挿入」ダイアログをさきほどと同じように出し,「選択」ペインで作成したユーザ定義変数を選択し,「挿入」ボタンを押下すれば埋め込まれる。
このときの content.xml の内容は以下の通り。

<text:user-field-decls>
  <text:user-field-decl office:value-type="string" office:string-value="hogehoge" text:name="holder"/>
</text:user-field-decls>
<text:p text:style-name="Standard">
  こんにちは
  <text:user-field-get text:name="holder">hogehoge</text:user-field-get>
  さん
  <text:user-field-get text:name="holder">hogehoge</text:user-field-get>
  さん
  <text:user-field-get text:name="holder">hogehoge</text:user-field-get> さん!
</text:p>

さきほどと差しこみフィールドの実現のされ方が結構違っている。
変数部分 (<text:user-field-decl>) のほうだけ書き換えればいいので,適当なワンライナーだけど (ほんとうなら text:name のほうでチェックするべき)

$ perl -i -pe 's{string-value="hogehoge"}{string-value="dayflower"}gxms' content.xml

こんな感じで。
実態のドキュメント部分 (<text:user-field-get>) のほうは置き換えてないけど,LibreOffice で再度開いた際には変数の埋めこまれた部分は自動的に変更されていた。ファイル自体はちょっと気持ち悪い状態だけど。

メモ

その他既存のアプローチについて (COM 以外)。

  • Perl の場合
    • OpenOffice-OODoc を使えば OpenDocument を読み・書き・操作できるみたい?そこそこメンテナンスされているようだ。
  • Ruby の場合
    • OOo4R という OpenDocumento を読み書きするプロジェクトがあるようだが 2004 年あたりで停滞しているようだ。
    • roo というプロジェクトがあるが表計算系 (OpenDocument ods, Excel xls, xlsx, Google Spreadsheet) の読み込みしかできないようだ。
    • UFOOAR というプロジェクトがあり Ruby 用の UNO ブリッジを目指しているようだ。2008 年あたりで停滞か?
  • Java の場合
    • そもそも UNO を使えば Java から OpenOffice.org, LibreOffice 等を操れるはず。
    • Apache POI で OLE2 (旧来の MS Office document コンテナ) や OOXML のファイルを読み・書き・操作できるようだ。
      • とはいえコアはあくまでコンテナの読み書きっぽい。各アプリケーション (Word など) の文書を操作する高次 API も用意されているようだが (例: Apache POI HWPF) クオリティについては不明。

*1:このため vim で編集しようとしたら半ばフリーズ気味になってしまった。

*2:もちろんフィルタリングせず全フィールドタイプから選択してもいい。

winbind による Active Directory 認証 on Ubuntu 11.04

Ubuntu での winbind による Active Directory 認証は以前 winbind で Linux 認証 on Ubuntu - daily dayflower で書いたんだけど,だいぶ古くなったんで今の環境むけに。一応,対象 Natty だけど Maverick とかたぶん Lucid あたりまで適用できると思う。

面倒といえば面倒なのでそれを回避したいなら LikeWise 使うとか (第174回 Likewise OpenでActive Directoryを利用する:Ubuntu Weekly Recipe|gihyo.jp … 技術評論社 参照)。ただ LikeWise もカスタマイズされた Samba 等を内包してたりするんで,中身が見えないと嫌な人には向いてない。

諸元

Linux マシン
Active Directory サーバ

2011年にもなるのに Windows 2000 Server なんてセキュリティの面とかいろいろ間違っている気がするけど,大人の事情。Windows Server 2003 以降なら SFU schema はいらない。というか 2000 Server でもシェル等が決め打ちでよかったりすれば SFU schema はいらない。

凡例

以前のを踏襲。

ドメイン
HOGE
Active Directory のレルム
hoge.example.com
Active Directoryドメインコントローラ
dc.hoge.example.com
クライアントホスト名
Penguin

Kerberos の設定

デフォルトでは入っていないんで krb5-config をインストール。

$ sudo apt-get install krb5-config

以前は krb5-user もインストールしていたんだけど,今は kinit しなくても net ads join できるんでインストールする必要はないと思う。実際なくてもできた。

/etc/krb5.conf の抜粋:

[libdefaults]
	default_realm = HOGE.EXAMPLE.COM

[realms]
	HOGE.EXAMPLE.COM = {
		kdc = dc.hoge.example.com
		admin_server = dc.hoge.example.com
		default_domain = hoge.example.com
	}

[domain_realm]
	.hoge.example.com = HOGE.EXAMPLE.COM
	hoge.example.com = HOGE.EXAMPLE.COM

Samba, winbind の設定

samba, winbind をインストール。

$ sudo apt-get install samba winbind

/etc/samba/smb.conf の抜粋:

[global]
	workgroup = HOGE
	realm = HOGE.EXAMPLE.COM
	security = ADS
	obey pam restrictions = Yes

	algorithmic rid base = 10000
	template homedir = /home/%U
	template shell = /bin/bash

#	winbind separator = !
	winbind cache time = 60
	winbind use default domain = Yes
	winbind nss info = sfu:HOGE
	winbind refresh tickets = Yes
	winbind normalize names = Yes
	winbind enum users = Yes
	winbind enum groups = Yes

	idmap uid = 10000-19999
	idmap gid = 10000-19999
	idmap backend = rid

winbind separator を指定するとなぜか pam モジュールでの認証がうまくいかない。winbind separator を指定した場合というか winbind normalize names と併用した場合の気がする。なので泣く泣く (?) 指定をはずしている。ま,実際には使うことほとんどないし,DOMAIN\user な指定はメジャー (かつ Windows 側との整合性もとれてる) んで泣く泣くというほどではない。ただ,いざ使うときはシェル等から使うことが多いからエスケープが必要だったりしてダサい。

Active Directory への参加とテスト

さきほど書いたとおり kinit は必要ないんでいきなり net ads join する。

$ sudo net ads join -U Administrator
Administrator's password: ********
Using short domain name -- HOGE
Joined 'PENGUIN' to realm 'hoge.example.com'
No DNS domain configured for penguin. Unabled to perform DNS Update.
DNS update failed!

以前書いたとおり,手元の環境では DNSドメインコントローラに任せていないんで,上記のように怒られている。これでも問題なく使える。

きちんと join できてるかテスト((testjoin は join の試行を行うのではなく,join がされているかどうか調べるコマンド。名前が悪い。))。

$ sudo net ads testjoin
Join is OK

できてる。

いよいよ winbind を立ち上げる。

$ sudo service winbind start
 * Starting the Winbind daemon winbind
   ...done.

なぜか winbind だけは upstart 形式になってない。ので SysV init 系の service コマンドで起動 (もちろん /etc/init.d/winbind で実行してもいいけど)。

きちんと winbind が働いているか調べる。

$ wbinfo -t
checking the trust secret for domain HOGE via RPC calls failed
Could not check secret

$ sudo wbinfo -t
checking the trust secret for domain HOGE via RPC calls succeeded

一瞬焦ったが,wbinfo -t に限っては root 権限でないと実行できないようだ。

Active Directory のグループ一覧を取得してみる (こちらはユーザ権限で可; ユーザ一覧は -u オプション)。

$ wbinfo -g
netshow administrators
dhcp users
dhcp administrators
...... (以下略)

nss の設定

winbind が起動しただけでは,Active Directory のユーザ情報は winbind 内で完結しており Linux から使えない。これを Linux のユーザ情報管理システムと結合するのが nss である。

/etc/nsswitch.conf の抜粋:

passwd:         compat winbind
group:          compat winbind
shadow:         compat winbind

shadow: んとこはいらない気もするけど一応。RHEL ではコマンドで設定できたけど,Ubuntu だと全部手書きしなくちゃなんない。

ついでに,Windows マシンの名前解決ができるように hostswins をいれておく。

hosts:          files mdns4_minimal [NOTFOUND=return] dns wins mdns4

うまくユーザ情報を利用できるかテスト。

$ id dayflower
uid=11190(dayflower) gid=10513(domain_users) 所属グループ=10513(domain_users),10512(domain_admins)

できてる。もちろん getent passwd とかでたしかめてもよい。

pam の設定

以上で Active Directory 上のユーザのアカウント (情報) が Linux のアカウントと結合された。だが,ログイン時等の認証は pam 経由でおこなっているのでそちらの設定をする必要がある。

設定に必要なファイルはインストールされているので pam-auth-update を実行するだけでよい。

$ sudo pam-auth-update

のだが,ホームディレクトリが存在していないユーザはログイン等できない。これでは不便なので,認証時にホームディレクトリが存在しない場合,自動的にホームディレクトリを作成するようにする。

ホームディレクトリの作成自体は pam_mkhomedir.so を使えばできる。これをさきほどの pam-auth-update コマンドで導入されるよう,下記のようなファイルを作成する。

/usr/share/pam-configs/mkhomedir :

Name: Automatic User Dir Generation
Default: no
Priority: 192
Session-Type: Additional
Session:
	optional	pam_mkhomedir.so skel=/etc/skel/ umask=0022

実は (いつごろからか忘れたけど) pam_winbind.somkhomedir というオプションが用意され,それを使えばわざわざ pam_mkhomedir.so を利用する必要はなくなった。んでもそっちだと umask= 等のオプションはないし,システム提供の /usr/share/pam-configs/winbind を書き換える必要があるので敬遠してる。


pam の認証とは関係ないが,Domain Admins に所属するユーザは sudo できたほうがいいので,そのように設定する。

$ sudo visudo

/etc/sudoers の抜粋:

# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL
%domain_admins ALL=(ALL) ALL

winbind normalize names = Yes という設定にしているので domain_admins という指定になっている。普通に書くなら domain\ admins

あと,このままだと GNOME 等で音がでなかったり USB 機器が使えなかったりする可能性がある*1ので,/etc/group を編集して,ログインするユーザを各グループに適宜所属させている。人数が多くなると面倒 (だし一元管理の趣旨に反する) ので,その場合は /etc/security/group.conf を設定すればうまくできるのかな?ただ現状 (Natty 11.04) だと pam_grouplogin にしか記述されてないんでうまくいくのか不明。

起動スクリプト (upstart)

以上で Active Directory に登録されているユーザであればこの Linux マシンにログインできる。

そのためには winbind さえあがっていればよい。Windows マシンからこのホスト名 (例では Penguin) を名前解決するためには nmbd もあがっていたほうがよい。SMB 共有を行わないのであれば smbd はあがっている必要はない。

ということで起動時に smbd をあがらないようにしてみる (Ubuntu だと Samba をインストールすると smbd, nmbd が自動的に起動するようになっているので)。smbd, nmbd は upstart 経由でサービスコトロールされているので,下記のファイルをいじる (のだと思う。 upstart にくわしくないのでほんとうにこれでいいかわからない)。

/etc/init/smbd.conf の抜粋:

#start on (local-filesystems and net-device-up)

start on というところをコメントアウトした。

あとは今現在あがっている smbd をおとす。

$ sudo initctl stop smbd

もちろん sudo stop smbd でもよい。

*1:惰性で /etc/group いじってしまってるんで,もしいじらなかった場合に使えないのかどうか実は知らない。

Java で RSA 暗号を使う

まず OpenSSL コマンドで RSA 秘密鍵を生成する。

$ openssl genrsa -out private_key.pem 1024

Generating RSA private key, 1024 bit long modulus
........................++++++
...++++++
e is 65537 (0x10001)

末尾にビット数を指定している。OpenSSL のコマンド側だとたとえば 256 とかでも通るんだけど,これを Java のほうにもってくと

java.security.InvalidKeyException: RSA keys must be at least 512 bits long

のように怒られたりする。ただし Sun JRE 付属のプロバイダ (JCE) だとそうなだけで Bouncy Castle をプロバイダに使うと怒られなかった。

このあと openssl rsa コマンドで DER 形式にすればいいだけかなと思ったらのちのち怒られたので PKCS #8 形式かつ DER 形式に変換する。

openssl pkcs8 -in private_key.pem -topk8 -nocrypt -outform DER -out private_key.pk8

公開鍵はふつうに DER 形式で出力すればいいだけ。

openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der
writing RSA key


以上のようにして OpenSSL で生成した鍵を使って Java で暗号化・復号化してみる。

なお,本来公開鍵暗号方式では,平文を公開鍵 (受信者側が公開した鍵) で暗号化し,暗号文を秘密鍵 (受信者側が秘匿している鍵) で復号化する。だが,今回の案件では秘密鍵 (送信者側が秘匿している鍵) で暗号化し,公開鍵 (送信者側が公開した鍵) で復号をおこなった。つまり RSA による暗号化というより半ば署名的に使ったことになる。通信内容の秘匿には使えないが,

  • 第3者に暗号文を生成させたくない
  • カジュアルに解読されなければいい

などの理由によりこのような仕様とした。

プログラムは以下のとおり。

package com.example.rsajava;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.Cipher;

import org.apache.commons.codec.binary.Base64;

public class Main {
	private static final String CIPHER_ALGORITHM = "RSA";
	private static final String CIPHER_MODE = CIPHER_ALGORITHM + "/ECB/PKCS1PADDING";

	private static final String PRIVATE_KEY_FILE = "private_key.pk8";
	private static final String PUBLIC_KEY_FILE  = "public_key.der";

	public static void main(String[] args) {
		Main app = new Main();

		InputStreamReader input = new InputStreamReader(System.in);
		BufferedReader reader = new BufferedReader(input, 1);
		for (;;) {
			System.out.print("INPUT:     ");
			System.out.flush();

			String line;
			try {
				line = reader.readLine();
				if (line == null) break;
			} catch (IOException e) {
				e.printStackTrace();
				break;
			}

			byte[] encrypted = app.encryptWithPrivateKey(line.getBytes());
			System.out.print("ENCRYPTED: ");
			System.out.println(Base64.encodeBase64String(encrypted));

			byte[] decrypted = app.decryptWithPublicKey(encrypted);
			System.out.print("DECRYPTED: ");
			System.out.println(new String(decrypted));

			System.out.println();
		}
	}

	private Cipher cipher;
	private Key secret_key;
	private Key public_key;

	public Main() {
		try {
			cipher = Cipher.getInstance(CIPHER_MODE);


			KeyFactory keyfactory = KeyFactory.getInstance(CIPHER_ALGORITHM);
			KeySpec keyspec;

			keyspec = new PKCS8EncodedKeySpec(loadBinaryFile(PRIVATE_KEY_FILE));
			secret_key = keyfactory.generatePrivate(keyspec);

			keyspec = new X509EncodedKeySpec(loadBinaryFile(PUBLIC_KEY_FILE));
			public_key = keyfactory.generatePublic(keyspec);
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}

	public byte[] encryptWithPrivateKey(byte[] source) {
		try {
			cipher.init(Cipher.ENCRYPT_MODE, secret_key);
			return cipher.doFinal(source);
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}

	public byte[] decryptWithPublicKey(byte[] source) {
		try {
			cipher.init(Cipher.DECRYPT_MODE, public_key);
			return cipher.doFinal(source);
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}

	private static byte[] loadBinaryFile(String filename) {
		try {
			FileInputStream in = new FileInputStream(filename);
			byte[] data = new byte[in.available()];
			in.read(data);
			in.close();
			return data;
		} catch (IOException e) {
			e.printStackTrace();
		}

		return new byte[0];
	}
}

かなり RSA 暗号のコード - BiBoLoG からパクっている。Base64 形式の出力のため Apache Commons Codec を利用している。あと例外処理は適当すぎる。


以下実行例。

INPUT:     Hello, World!
ENCRYPTED: gswm4jxzVI7QDM+EFqYq+uzniO81FsGfZVYsGtsecD22TPw+AqB33QKE7WzP0+fKiIPcCjyz/eJL
Z6bm+mplpY5I7QZkIOD+rtW443YaXULU/DXTf0kb/pPMOSbyHMLF5hWZJbzJSq7iGOFuyWqxZ9DB
JvlpobhKF9DLZGJ3JJ0=

DECRYPTED: Hello, World!


INPUT:     Hello, World!
ENCRYPTED: gswm4jxzVI7QDM+EFqYq+uzniO81FsGfZVYsGtsecD22TPw+AqB33QKE7WzP0+fKiIPcCjyz/eJL
Z6bm+mplpY5I7QZkIOD+rtW443YaXULU/DXTf0kb/pPMOSbyHMLF5hWZJbzJSq7iGOFuyWqxZ9DB
JvlpobhKF9DLZGJ3JJ0=

DECRYPTED: Hello, World!


INPUT:     hello, World!
ENCRYPTED: NR7vRLEkRHLpUwFzmgS2lf8FwkpUiX0HGnv6rCgx9wOc2SFupXVog3+7AJmGZlq7RA24LrRKCjsS
awIwtiyPaPDVtT+MYgmb5DWcV9fivewygvw5Ct6qRmvjUMl26+uglYkQ0yF6g250rp8NOJa1k5l/
0NBx4e7ls5Q2C7drHjw=

DECRYPTED: hello, World!

(少なくとも Java での) RSA 暗号化には初期ベクタがないので,同じ文から同じ暗号文が生成されている (1 つめの例と 2 つめの例)。しかし 3 つめの例から少なくとも先頭の文字が違っていれば暗号文の全体 (というかブロック長) が変化するので,自分で初期化ベクタを用意してやればカジュアルに傍受している側をまどわせることができると思われる。

ほか注意点

  • ブロック長 (= 鍵の長さ) で自動的に分割して暗号化してくれるわけではないので自分で分割する必要がある
    • といっても一般に通常鍵の長さをこえるほどの情報はやりとりしないであろう。共通鍵暗号の鍵を最初にやりとりするだけであれば,RSA 鍵長内におさまるわけだし
  • (すくなくとも PKCS#1) パディングをおこなうと,パディングのための領域が必要になる。1024 bit の鍵の場合に暗号化できるブロック内平文は 1024 bit に満たない

KVM で PXE ブートしようとしたら難儀した

KVM の場合,virt-install コマンドに --pxe オプションをつければ PXE ブートさせることができる。Xen と違って完全仮想化だから。ばんざい。

# virt-install --name=hogehoge --accelerate --os-variant=rhel5.4 \
 --ram=384 --vcpus=1            \
 --file=hogehoge.img            \
 --network=bridge:br0 --pxe     \
 --vnc --noautoconsole

あるいは qemu-kvm コマンドを直接叩いて network option rom を指定するという方法もあるようだ (参考, qemu-kvm の直接起動は Stray Penguin - Linux Memo (KVM-2) 参照)。

これで普通に DHCP サーバと TFTP サーバを構成していればうまく PXE ブートできるはずなのだが,手元の環境では以前 Proxy DHCP サーバで PXE ブート - daily dayflower で書いたように Proxy DHCP を使っている。

この Proxy DHCP 環境下で KVMPXE ブートさせようとしたらなかなかうまくいかなかった。


結論からいうと,KVM (Qemu) で利用しているネットワークコントローラ用の ROM (Etherboot) にバグがあった。

ちなみに Etherboot 由来のネットワークブート ROM は Oracle VirtualBox でも使っている。ので同様に Proxy DHCP 環境ではうまく PXE できない (かった)。

原因 1: Proxy DHCPOFFER の NBP file name に対応していない

Proxy DHCP サーバで PXE ブート - daily dayflower でも書いたように,PXE_DISCOVERY_CONTROL Option の bit 3 を立てると DHCPOFFER の段階でブートファイル名を返すことができる。

ところが,Etherboot 由来の PXE BIOS はこのオプションを無視する。そして UDP 4011 宛に DHCPREQUEST を投げてくる。

しかたないので pxe-pdhcp を改造して 68 番と 4011 番両ポートで待ち受けするように変更した。

原因 2: Etherboot の ProxyDHCP 時の DHCPREQUEST パケットがおかしい

これで一件落着かと思ったら,DHCPREQUEST のベンダオプション項がうまくパースできない。仕様の読み漏れかと思って Etherboot のサイトからソースを落として読んだら,バグがあった。ベンダオプション項の最後に End of Options (FFh) をつけ忘れている。

Etherboot 4.4.2 でのパッチは以下のとおり。

diff -r e53c2fc7ea6a src/core/nic.c
--- a/src/core/nic.c    Wed Feb 16 12:19:14 2011 +0900
+++ b/src/core/nic.c    Wed Feb 16 14:07:59 2011 +0900
@@ -1166,6 +1166,7 @@
                                /* Construct the ProxyDHCPREQUEST packet */
                                memcpy(ip.bp.bp_vend, rfc1533_cookie, sizeof rfc1533_cookie);
                                memcpy(ip.bp.bp_vend + sizeof rfc1533_cookie, proxydhcprequest, sizeof proxydhcprequest);
+                               ip.bp.bp_vend[sizeof rfc1533_cookie + sizeof proxydhcprequest] = RFC1533_END;
                                for (reqretry = 0; reqretry < MAX_BOOTP_RETRIES; ) {
                                        printf ( "\nSending ProxyDHCP request to %@...", arptable[ARP_PROXYDHCP].ipaddr.s_addr);
                                        udp_transmit(arptable[ARP_PROXYDHCP].ipaddr.s_addr, BOOTP_CLIENT, PROXYDHCP_SERVER,

報告しようと思ったらバグトラッカも落ちてるしレポジトリもみつからない。どうもサイトが故障していたようだ。一部サービス (ソース提供や rom-o-matic での NIC ROM 自動生成) はすでに復旧している。

一応上記のパッチを適用してビルドした ROM ではうまく PXE boot できた((NIC ROM の置き換えは,/usr/share/kvm 以下の pxe-virtio.bin などを置き換える。もともとは /usr/share/qemu-pxe-roms/ 以下の ROM ファイルへのシンボリックリンク。))。だが,前述したように,VirtualBox でも問題のある Etherboot 由来の NIC ROM を使っており,こちらをどのように差し替えるかがわからない。なので,pxe-pdhcp 側で workaround をおこなった。

ほんとは workaround のソースを示せればいいんだけど,諸般の事情*1でまだだせない。あくまで Etherboot のバグ対策だけだが,

  • DHCP Options の Class Identifier が 32 バイト続いたあとで,(本来 FFh があるべきだが) 「:UNDI:〜」とゴミが続いてしまっている
  • よって pdhcp.ccheck_dhcp_packet() において DHCP Option が 3Ah, 55h の並びになった場合,そこを終端とみなすようにする

という対策をおこなったところ,CentOS 5.5 の KVM guest および VirtualBox 4.0.2 において PXE ブートでできるようになった。

余談

Etherboot プロジェクトは実質 gPXE プロジェクトが後継となっているようだ。こちらのソースは一から書きおこしている部分が多く,上記のようなバグはない (なさそうだった)。

だが gPXE の NIC ROM を生成して KVM で使ってみたところ,なぜか主 DHCP サーバ宛に TFTP 要求をだしていた。ソース上は ProxyDHCP 用のコードもあるようだがまだきちんとインプリメントされていないのかもしれない。

なお,Etherboot では上記のパッチを適用して自分で ROM をビルドしたが,gPXE の NIC ROM は http://rom-o-matic.net/gpxe/gpxe-1.0.1/contrib/rom-o-matic/NIC ROM を生成した。いろいろ条件を指定して NIC ROM を生成できるのでおもしろいサイトである。


ちなみに,Etherboot (と gPXE) では,条件を指定することで TFTP だけでなく,HTTP 経由でイメージをとってこれる NIC ROM も作成できるようだ。また,gPXE のほうは iSCSI による SAN Boot にも対応しているらしい (id:adsaria さんによる実践例)。

*1:諸般の事情と書くとおおげさだけど,単に面倒だから。id:viver さんが pxe-pdhcp のソースを github にうつしてくれればいいなぁ。

Debian 6.0 Live イメージを PXE 経由でブート

さいきんの DebianLive 起動用イメージも用意しているらしい。

んで,Live 起動イメージはネットワークブート用のイメージも用意されている。

たとえば,http://ftp.jaist.ac.jp/pub/Linux/Debian-CD/6.0.0-live/i386/net/ をみるといくつかの種類 (GNOME 環境とか Rescue 環境とか) のネットブート用 Live イメージ (*.tar.gz) がおいてあることがわかる。

そこで,CentOS で作った PXE ブート環境に,選択肢のひとつとして Debian 6.0 Live を追加してみた。index.rb [nofuture.tv] が参考になる。

なお PXE 環境自体の構築については今回触れない。


ライブイメージディストリビューションのなかから debian-live-6.0.0-i386-standard.tar.gz を選択してみた。standard といいつつほとんど何も入ってないプレーンな状態だけど。

取得して展開すると,だいたい下記のようなディレクトリツリーになってる。

+ debian-live/
+ tftpboot/
  + tftpboot/debian-live/
  + tftpboot/debian-install/

おおまかにいうと,tftpboot/ フォルダ以下が TFTP による初期ブートに必要なファイル群 (カーネルとか initramfs とか)。debian-live/ は initramfs が起動したあとに NFS 経由で読み込むルートイメージ。NFS 経由といっても生ルートツリーを NFS マウントするわけじゃなくてルートイメージが SquashFS 形式でおいてあるんでそれを NFS 経由で読み込んでマウントする感じ。

なお tftpboot/debian-install/ のほうは,Debian をネットワークインストールする際に使う TFTP ブートイメージで,今回は使用しなかった。

NFS 用ルートイメージの用意

前述のとおり debian-live/ 以下が NFS 経由のルートイメージになるので適当な場所において NFS で公開する。

# cp -R debian-live /pub/linux/debian/squeeze

別にディレクトリはこの名前にする必要があるわけじゃない。自分の環境では上記のようにしているだけ。


上記例だと exports はこんな感じ。

# cat /etc/exports

/pub/linux/debian *(ro,all_squash,no_subtree_check,crossmnt)
/pub/linux/debian/squeeze *(ro,all_squash,no_subtree_check,crossmnt)

んで NFS デーモンを再起動。

# service nfs restart

PXE用ファイルの用意

こんどは TFTP ブート用ファイルを /tftpboot/ 以下の適当なディレクトリにコピー。

# cp -R tftpboot/debian-live /tftpboot/debian-squeeze

んで,Debian のドキュメントとかだと付属する pxelinux.cfg をそのまま使うような形で紹介されてるんだけど,そうするといままで使用していた他の PXE ブート環境がなくなってしまってもったいないので要所だけコピーする。

Debian Live の場合,たとえば debian-live/i386/boot-screens/live.cfgPXE ブート用の設定が書いてある。

label live
        menu label Live
        kernel debian-live/i386/vmlinuz-2.6.32-5-486
        append initrd=debian-live/i386/initrd.img-2.6.32-5-486 boot=live
               config netboot=nfs nfsroot=192.168.1.1:/srv/debian-live quiet

改行いれちゃってるけど,append 行のとこは1行で書く必要があると思う (以下も同様)。これをもとに,自分の pxelinux.cfg に書き足す。


今回の例示だと以下のような内容。/tftpboot/ の下とか NFSディレクトリに好き勝手な名前をつけたので,対応する部分を変更してある。

LABEL squeezelive
  MENU LABEL Debian 6.0 Live
  KERNEL debian-squeeze/i386/vmlinuz-2.6.32-5-486
  APPEND initrd=debian-squeeze/i386/initrd.img-2.6.32-5-486 boot=live config
         netboot=nfs nfsroot=<NFS Server Address>:/pub/linux/debian/squeeze quiet


以上で無事 Debian 6.0 Squeeze の Live 環境が立ち上がった。あまりにプレーンなのでほんとにうまく動いてるか自信がないので今 GNOME 版の Live イメージをダウンロード中*1


なお,Ubuntu でも同じようにすれば Live 環境の PXE 起動はできる。つっても違うところはいろいろあって

  • TFTP ブート用のファイルは netboot.tar.gz というのをとってくる
  • NFS 用ルートイメージは,Ubuntu インストール DVD をマウントしたものを NFS エクスポートすればそのまま使える
  • pxelinux.cfg まわりは preseed とか casper まわりとかを APPEND 行でいろいろ指定する必要がある

いろいろたいへんそうだけど,たいていの人は Ubuntu のライブイメージを手元に持ってるだろうから,ハードルは Debian より高くない。

Ubuntu の場合の pxelinux.cfg 例を下記においとく。

LABEL mavericklive
  MENU LABEL Ubuntu 10.10 Live
  KERNEL ubuntu-maverick/i386/casper/vmlinuz
  APPEND initrd=ubuntu-maverick/i386/casper/initrd.lz
         file=ubuntu-maverick/i386/preseed/ubuntu.seed
         boot=casper netboot=nfs
         nfsroot=<NFS Server Address>:/pub/linux/ubuntu/maverick quiet splash --

TFTP 用ファイルは /tftpboot/ubuntu-maverick/ においており,NFS 用ルートイメージが /pub/linux/ubuntu/maverick/ においてあるという前提だけど。当然ながら KERNEL 行や APPEND 行のファイルパスは適宜書き換えること。

*1:GNOME 版できちんと GNOME があがったので手順として間違ってないことを確認した。しかしいまや Debian も Live 環境からインストールできるのね。怖くてためしてないけど。

羽田空港から台北いってきた - 風景編

台北市内
日本ともそう遠くは変わらない風景*1

路地裏, 台北市内
路地裏。表の顔と裏の顔の分離。奥のトラックはギリギリじゃないか。

芸術の街?

街の各地にオブジェとか結構あった。たぶん花博は関係ない。

巨大オブジェ, 台北市内
市街地中心でもないのに,こんな巨大オブジェがそびえたってたりする。

ベンチ, 台北市内
事情があっていいアングルでとれなかったけど,このベンチかわいい。

選挙

行った時がたまたま選挙シーズンだったのか,選挙ののぼりとか各所にあった。

選挙

選挙, 迪化街

日本と違うなと思ったところ。

  1. 名前の前に番号がついてる。この番号でも投票できるのかな?
  2. (政治家として) 若手の女性はルックス重視っぽい
  3. 巨大な広告もうてるみたい
    • 下の写真ではそんなに大きくないけど,結構巨大なビル広告とかもあった
    • あと CHANGE とか広告に書いてる候補者もいたww

日本的もの

グラフィティ, 路地裏, 台北市内
日本的ってわけじゃないけど,あちらにもこういう落書き?グラフティ?ありました。どこの国もかわらんなー。

悪人
日本で公開中の映画が,上映予告とかでてると不思議。

麻布茶房
麻布茶房。こんなポップな雰囲気だっけ?ともかく,台湾滞在中に2店舗くらい見かけた気がする。

和民
あと,和民ね。広告の雰囲気とかもおんなじ気がする。料金は日本と同じくらいなんだけど,これはつまり台湾の物価からすると高い部類ですね。

エラー

街中の端末のエラー画面に惹かれてしまうのはなぜでしょうか。

MRT自販機の故障
これは MRT の券売機のエラー。エラー内容はよくわかんないや。

エラー, チェックインマシン, エバー航空
こちらはエバー航空の自動チェックインマシン。エラーってわけじゃないけど,ふつーの Windows の画面になっちゃってる。

その他

カレンダー
あるお店に入ったときにあった電子カレンダー。珍しいのでお願いして撮らせてもらった。左側は旧暦かな?


人工進化女神
整形の看板。「人工進化女神」というフレーズにひかれて撮った。


迪化街
迪化街 (という問屋街) の光景なんだけど,おばちゃんの自転車のうしろかごにヨウムが止まってる!よく逃げないな!

*1:ここは別に繁華街ではないです。繁華街はもっともっとちゃんと栄えてます。

羽田空港から台北いってきた - 食事編

食事は全般的に日本人の舌に合ってると思う。ただ八角 (スターアニス) が苦手だとつらいかも。といっても小籠包は八角の香りはしないので大丈夫。

明月湯包

たぶん支店のほうに行ったのかな。そっちのほうが広くて賑わってる印象だったんで。

タクシー移動に飽きたというのもあり,MRT で移動。MRT 六張犁駅の近くに放射路があるんだけど,その信号をいくつか渡っていかないといけないのね。んで夜ということもあり見事にいくべき道を間違えて難儀しました。MRT からいくなら,すくなくとも駅でて放射路を渡り切るとこまでの詳細な地図はもってたほうがいいかも。


キムチ小籠包, 明月湯包
キムチ入小籠包。

こういう亜流はあんまりおいしくないかなと想像してたけど,普通の小籠包に飽きた舌には新鮮で,ノーマルよりおいしく感じた。


清燉牛肉麺, 明月湯包
別に牛肉麺の名店ってことはないんだけど,清燉牛肉麺たのんだら珍しくスープと麺がべつべつにでてきた。やさしい味で美味だった。


店入って右手のワゴンに惣菜の入った小皿がならんでた。台北ナビのクーポンでもらえる野菜一品というのは,この惣菜です。クーポン適用には500元以上注文する必要あり。軽食つまむ感覚だとなかなか500元までいかないです。

阜杭豆漿

台北の食事処としては珍しく,フードコートに入ってるお店。華山市場の2F。市場といっても小規模デパートみたいな感じだけど。かなり混んでいるので,フードコートの空き席はなくはないけど大人数の場合覚悟が必要かも。


阜杭豆漿

こんな感じで地元の人がすんごい並んでる。観光客もいるけどあんまりいない。んで次々に注文がさばかれていくので,日本人としては料理の名前を書いて見せるのが一番トラブルが少ないんじゃないかと思う。


阜杭豆漿

伝統のあるお店なんだけど,小奇麗なガラス張りの厨房。


鹹豆漿と油條, 阜杭豆漿

鹹豆漿と油條を注文。油條は選択ミスだった気がする。鹹豆漿にもともと少し入ってるなんて知らなかったんだもん。油條の代わりに厚餅夾蛋を注文したかった。

鹹豆漿は味の濃い茶碗蒸しのような手作り豆腐のような (豆乳が凝固してるんだから当たり前か) 優しい味。

度小月

日本語メニューありで安心。

店内はすごく綺麗で日本のおしゃれ居酒屋的内装 (メニューもそんな感じ)。だけど,料理の値段はとても安い。

度小月

入口はいってすぐのところでおっさんが座って担仔麺をひたすら作ってる (これは2階から撮った)。


担仔麺, 度小月
担仔麺の汁なし,コリアンダー (パクチー) なし。

あと汁ありコリアンダーありの担仔麺も頼んだんだけど,コリアンダーの味がすごく支配的でまるでベトナムのブンのようだった。汁ありなしにかかわらず,コリアンダーなしで注文したほうが料理そのものの味をきちんと味わえていいかも。

丸林魯肉飯

ホテルからとても近かったんでいっただけ。ということであんまり期待してなかったけど,今回の旅行で一番のアタリだったかも。

お昼時にいったら1階は地元の人たちで芋洗い状態。

1階はビュッフェ形式で小皿に盛ってくれる。なので見た目から判断して身振り手振りで何皿か盛ってもらう。んで最後の会計のところで魯肉飯と絶叫してたら2階にいけといわれた。なので小皿料理をもって2階へ。2階は1階と違ってすいていて,ある程度の日本語が通じるおばちゃんがいた。観光客だと2階に通されるのかな。


魯肉飯, 丸林魯肉飯

ここの魯肉飯は大と小の2種類があるんだけど,この写真は「大」。「大」でも小ぶりの茶碗サイズなので「小」を頼む必要はないかと思われる。

味は,個人的にはカレーヌードルにご飯をつっこんだときのような味のように感じた。別にカレー粉は入ってないと思うけど,ジャンクな味って意味で。店員には「そぼろごはん」といわれたけど,そぼろほども肉々しくない。


ハマグリのスープ, 丸林魯肉飯

蛤のスープ。想像通りの味だけど,とても胃に優しい味でほっとする。しょうがが入っているのがまたおいしい。

百果園

カウンター後ろの黒板にはメニューが綺麗な日本語でも書いてある。なので日本人びいきの店かとおもいきや,あたる店員によってはかなり無愛想。

ハズレの店員にあたったらしく,マンゴーかき氷を指さすも NO の返事 (たしかに季節的に NG だった)。じゃあってんでフルーツ盛り合わせかき氷指さすも NO。じゃあジェラートならいいかなと,店員と相談しながらマンゴーとドラゴンフルーツを選択。

フルーツかき氷, 百果園

どーんとおっきいのが来た (ピントがあってなくてすいません)。結果的にフルーツ盛り合わせかき氷+ジェラート盛り合わせを注文したことになってたらしい。無愛想なのはネガティブじゃなくて言葉が全然通じないからだったのね。反省。

これ写真じゃ大きさはわかんないかもだけど,小ぶりのボウルくらいの器に,これでもかっ,てくらいフルーツが詰め込まれてた。親子3人だと食べきるのにも一苦労。

脆皮鮮奶甜甜圈

ドーナツ屋さん。

もともと晴光市場 (雙城夜市の開かれるあたり) に出来た店なんだけど,ホームページによると台北車站店もできたみたい。台北車站と中山駅のあいだくらい。

ホテルが近かったので晴光店で買った。ので台北車站店が実在するかはわかんない。

ドーナツ, 雙城街夜市

ドーナツ自体の写真を撮るの忘れたー。ドーナツはかなり大きいです。ミスドとかの2倍位の大きさはあるんじゃないかな。味は,皮サクサクで,まぁ普通においしいです。

台北市街東エリア (市政府のほうとか) に宿泊してる人がわざわざ晴光市場まで出張って買う価値があるかどうかまではわかんないですが,台北車站店が存在してるなら,なにかのついでにそこで買うというのはありかも。