TDateTime のナゾ

ほとんどの VCL オブジェクト は TDateTime 型を使って日付値と時刻値を示します。TDateTime 値の整数部は西暦 1899 年 12 月 30 日からの経過日数を示します。小数部はその日の経過時間(24 時間制)です。

んーなぜ 1899/12/30 からなんでしょうね。でもそういえば Visual Basic も日付時刻型って実質浮動小数点値だった気がする。

Date の場合、時刻は 00:00:00 であり、その日の午前0時である。Time の場合日付は1899年12月30日となっているが、これは VB 6 の日付を扱う起点となる日付である。具体的な起点はというと、値がゼロになる位置を表示させれば分かる。VB 6 の日付時刻は倍精度浮動小数点数値で扱われるので、0# の値を渡せば良い。具体的には、プログラムの10行目に書いたとおりである。実行結果を見て分かるとおり、1899/12/30 00:00:00 が起点となる。

連載:プロフェッショナルVB.NETプログラミング 第5回 日付時刻の取得とフォーマット(1/3) - @IT

んあーこっちが先なのか。たぶん*1

まだしも 1899/12/31 00:00:00 を起点とすれば 1900/1/1 が 1 になって心情的に理解できなくもないですが*2,実際には 2 ですよね。謎。

ちなみに(少なくとも Delphi の場合)

TDateTime 値が負の値である場合は,時刻部分を別個に扱わなければなりません。小数部は TDateTime 値の符号とは無関係に 1 日 24 時間の端数を表します。たとえば,1899 年 12 月 29 日の午前 6:00 は -1 + 0.25(-0.75)ではなく -1.25 です。-1 と 0 の間の TDateTime 値は存在しません。

日付・時間の差分の計算が楽になるから浮動小数点値にしてると思ったんですが 1989/12/30 をまたぐと計算がおかしくなりますね。自力で計算するんじゃなくて SecondSpan 等使うほうが吉,と。

追記

なんとなく理由の予想はついたような。TDateTime も TDate も TTime も実質同じ浮動小数点型なわけですが,「時間」だけ(つまり TTime)を扱うなら,日付部分(つまり整数部分)は 0 として利用する局面が多いかと思います。

でもやっぱり 12/30 じゃなくて 12/31 でいいじゃん,と思いますが,おそらく,24:00 (= 1.0)が欲しかったんではないか,と。もちろん時刻としてはありえない値ですが,span として考えたときに 24:00 がやっぱりいるなぁ,じゃあバッファとして2日分にしておくか,と。

憶測にすぎませんが一応。

*1:VB.NET だと基数は 0001/01/01 です。ちなみに Delphi 1.0 もそう。なぜか扱いが逆転してます。

*2:でも 0 のほうが好み

IE コンポーネントにおけるイベントの発生順序

一般化しづらいのですがあえて書くと,Navigate(URL) 後の基本的な流れは,こうです。

  1. BeforeNavigate2 イベント発生
  2. NavigateComplete2 イベント発生
  3. DocumentComplete イベント発生
  4. ProgressChange (Progress = 0, ProgressMax = 0) イベント発生(※)

2〜4の間に

  • CommandStateChange イベント
  • DownloadBegin イベント
  • DownloadComplete イベント
  • ProgressChange イベント
  • TitleChange イベント
  • StatusTextChange イベント

が順不同で発生します。

ちょっと複雑な例

アサヒコムのある記事の場合の例ですが,

BeforeNavigate2: http://www.asahi.com/life/update/(略)
NavigateComplete2: http://www.asahi.com/life/update/(略)
BeforeNavigate2: http://pagead2.googlesyndication.com/(略)
NavigateComplete2: http://pagead2.googlesyndication.com/(略)
DocumentComplete: http://pagead2.googlesyndication.com/(略)
DocumentComplete: http://www.asahi.com/life/update/(略)

このように JavaScript で googlead を読み込んでいるのでそれらからもイベントが発生してしまいます。

それじゃ本当の DocumentComplete は?と思いますが,TWebBrowser の LocationURL にドキュメント全体の URL が入っている(そしてブレない)ので,それと比較すれば判定できるかと。


続いて msn の例ですが,

BeforeNavigate2: http://jp.msn.com/
NavigateComplete2: http://jp.msn.com/
BeforeNavigate2: about:blank
BeforeNavigate2: about:blank
BeforeNavigate2: about:blank
BeforeNavigate2: about:blank
BeforeNavigate2: about:blank
BeforeNavigate2: about:blank
BeforeNavigate2: about:blank
BeforeNavigate2: about:blank
DocumentComplete: http://jp.msn.com/
DocumentComplete: http://jp.msn.com/
DocumentComplete: http://jp.msn.com/
DocumentComplete: http://jp.msn.com/
DocumentComplete: http://jp.msn.com/
DocumentComplete: http://jp.msn.com/
DocumentComplete: http://jp.msn.com/
DocumentComplete: http://jp.msn.com/

これも JavaScript で読み込んでいるのですが非常に判断しにくい例です下記追記参照。

最初 iframe 使うとそうなるのかな,と思ったのですが今の msn のページは iframe 使ってないようですし……Ajax で遅延ロードするとこうなるのかなぁ。

BeforeNavigate2 と DocumentComplete の数が対応してないのがちょっと困り者です。


お口なおし?でわかりやすい例として, google.com にアクセスして google.co.jp にリダイレクトされる例です。

BeforeNavigate2: http://www.google.com/
NavigateComplete2: http://www.google.co.jp/
DocumentComplete: http://www.google.co.jp/

NavigateComplete2 の時点でリダイレクト先の URI になっています。サイトによっては BeforeNavigate2 の時点でもリダイレクト先になっていたような……*1

ともかく,この場合 TWebBrowser.LocationURL は http://www.google.co.jp/ になっているのでやはりそちらと比較すればオーケー。

ProgressChange イベントについて

スタート時は ProgressMax が 10000 で,ドキュメントをある程度読み込むまではそのまま Progress がちょっとずつ増えていきます。が,ドキュメント読み込み後は臨機応変に ProgressMax も増えます。なので Progress rate がリニアに増えていくことを期待してはいけません。また,まれに Progress > ProgressMax になるときもありますし。

Progress == -1 の時に読み込み終了,とドキュメントに書いてあります。経験上,上の Step 4 のように(↑※参照),ドキュメントを完全に読み込み終えた時点で Progress = 0, ProgressMax = 0 のイベントが発生するようです(あくまで目安ということで*2)。

NavigationError 発生時のイベント順序

  1. BeforeNavigate2 イベント発生
  2. NavigateError イベント発生
  3. NavigateError イベント発生
  4. NavigateComplete2 イベント発生
  5. DocumentComplete イベント発生

NavigateError がダブっているのは打ち間違いではありません。エラー画面(ドキュメント)を表示する必要があるので,それらの都合で NavigateError イベントが複数回発生することもあります。

しかもエラー画面の表示時に NavigateComplete2 イベント/DocumentComplete イベントが発生するので最終段階だけではエラーとして判断しがたいことがあります。まぁ内的にフラグをもてばいいですし,もっと簡単にはイベントハンドラの Cancel 変数に True を渡すとそこで Navigate 処理が終了するのでその後のイベントは発生しません*3

NavigateError イベントは Status コードを付随しますが,VT_I4(vtInteger)です。サーバサイドのエラーであれば,例えば 404 などのお馴染みのものです。クライアントサイドのエラー(DNS lookup 失敗等)には Status コードは $80000000 みたく 32bit 数値になります。これらについては SDK のドキュメント参照。

このドキュメントのステータスを後から取得する方法がありそうなものですが……みつかりませんでした。DOM 系のプロパティで何かありましたっけ?

結論

ドキュメントを完全に読み込み終えた,と完璧に判断するのは難しいです。下記追記参照。Ajax の場合完全な終わりはありませんし。

プログラマーが読込完了の判断基準を決める必要があります。

追記 2007/09/26

引数pDispがTWebBrowserのApplicationプロパティと一致する場合、そのページは最上位のフレーム(Topのフレーム)であると判定できます。


... snip ...


フレームが使われたページでは、各フレームごとにこのイベントが発生しますが、完了した順に発生するため最上位のフレームは一番最後にこのイベントが発生します。

http://griffy.pekori.to/soft/technologies.html

あーこれでいけました。

*1:301 でローカルにキャッシュされてる場合かなぁ

*2:シンプルに読み込み完了時点を求めるという点では意外に一番いいかも

*3:gmail 等だとエラー時 Cancel := True してしまうときちんと読み込めませんが

TWebBrowser での POST

きちんと確かめてなかったんでハマりました。

The post data specified by PostData is passed as a SAFEARRAY Data Type structure.
The VARIANT should be of type VT_ARRAY and point to a SAFEARRAY Data Type.
The SAFEARRAY Data Type should be of element type VT_UI1, dimension one,
and have an element count equal to the number of bytes of post data.

PostData のところは VT_STRING ではなくて Byte の VT_ARRAY じゃないといけないです。あと,Delphi の場合,Navigate の第1引数以外は var OleVariant なので(C++ でいう & 参照体),定数を直接与えられないです。

ということでサンプルコード。

procedure TForm1.Button1Click(Sender: TObject);
var
  str: AnsiString;
  p: PChar;
  vFlag, vTarget, vPostdata, vHeaders: OleVariant;
begin
  str := 'user=hogehoge&password=fugafuga';

  vPostdata := VarArrayCreate([0, Length(str) - 1], varByte);
  p := VarArrayLock(vPostdata);
  try
    Move(PChar(str)^, p^, Length(str));
  finally
    VarArrayUnlock(vPostdata);
  end;

  vFlag     := 2 or 4 or 8; 
  vTarget   := Null;
  vHeaders  := 'Content-Type: application/x-www-form-urlencoded'#13#10;

  WebBrowser1.Navigate('http://example.com/', vFlag, vTarget, vPostdata, vHeaders);
end;

Shaw Communicationsを参考にしました。

ミソは,

  • PostData は VarArrayCreate() で Array Variant を作る
  • よくサンプルで for ループでまわして文字列を配列化してるのがあるけど,パフォーマンスがよくない。
  • だから VarArrayLock でロックして memcpy する(そうしていいよって Delphi のヘルプにも書いてある)
  • Move() は memcpy と引数の順番が違うし,ポインタではなく実体のほうを渡さなくちゃいけないので ^ を使ってる
  • Delphi の文字列は C でいう文字列と違う。PChar(str) というのはキャストしているのではなく,文字列のバイト列へのポインタを取得しているととらえればいいかな。
  • 2 or 4 or 8 というのは navNoHistory*1 | navNoReadFromCache | navNoWriteToCache。これらの定数が shdocvw で定義されてなかったんで。
  • Null というのは nil とは違う。VT_NULL を返す関数。
  • 経験上 Content-Length をヘッダに含めるとうまくいかない。

*1:これを定義するとブラウザの Forward, Back が効かなくなります。ということはヒストリ分ちょっと省メモリになるかな?

TWebBrowser コンポーネントを使う

今更〜〜〜ですが,必要にせまられて作ってたら忘れそうなのでメモメモ。

DelphiIE コンポーネントを使うなら下記のサイトがまとまってます。

ま,TWebBrowser とか IWebBrowser2 とかでググると今でも結構たくさんのページがヒットしますんで。

(上記ページでも言及されていますが)そのまま TWebBrowser を使うといろいろ不具合があります。対処法もいろいろあるのですが,面倒なら下記の TUIWebBrowser コンポーネントを使うと楽らしいです(今回の案件では使いませんでした)。

GET じゃなく POST で Navigate したい

Navigate(URL, Flags, TargetFrameName, PostData, Headers) という引数のプロシージャがあるので PostData に適切に設定すれば POST になる……のですが,ヘッダの Content-Type に application/x-www-form-urlencoded を指定するのを忘れてはいけません。たぶん Content-Length も指定したほうがいいかも。

試してみて色々気をつけるべきことを書きました⇒TWebBrowser での POST - daily dayflower

読み込まれたドキュメントの DOM をいじりたい

例えば自動ログインとかのオートクルーズ機能をつけるときに DOM をプログラムから操作したくなりますよね。

DocumentComplete イベントが発生した後であれば,OleObject の Document プロパティに JavaScript でいうところの document オブジェクトが存在するので,JavaScript で DOM をいじるかのようにアレコレできます。実際には IHTMLDocument(1〜5)インタフェースを介するわけですが,Delphi なので透過的に扱えます。オートメーション万歳*1

e := WebBrowser1.OLEObject.Document.documentElement.getElementById('ほげほげ');

とか。

オンメモリで作成した HTML を読み込ませたい

TWebBrowser.Document プロパティを IStream と見做して Load するのが正攻法といわれてますが(⇒http://www.monazilla.org/document/directwrite.html),正直めんどうくさいですよね。

先ほどの DOM の件を応用して考えると,

WebBrowser1.OLEObject.Document.documentElement.innerHTML := 'ほげほげ〜';

みたくすればいけるかなと思うのですが,このプロパティ,読込はできるんですが(⇒http://hpcgi1.nifty.com/MADIA/DelphiBBS/wwwlng.cgi?print+200511/05110006.txt)書込しようとすると

innerHTML プロパティを設定できませんでした。この操作に 対して無効なターゲット要素です。

と怒られてしまいます。

んで,解決策としては,まず about:blank に Navigate してやり,DocumentComplete イベントを待ちます。この時点で documentElemement.innerHTML は

<HEAD></HEAD>
<BODY></BODY>

というブランクに非常に近いものになっているんですが,ここで WebBrowser1.OLEObject.Document.write(ほげほげ) してやると,上記の内容は消されて新しい html を表示することができます。

ただし,これ,実質 document.write(〜) を行っているようなものなので,連続して行うと,どんどん追記されてしまいます。なので新たに読み込みさせたい場合,その都度 about:blank に Navigate() してやる必要があります。結局この方法も面倒になってしまいました。

別に about:blank である必要はありませんでした。DocumentComplete イベント後,初回の Document.write() 時のみ,既存のドキュメントは上書きされます。

なお,body 要素の中だけ書き換えればいいのなら,Document.body.innerHTML であれば書込可能ですので,それが一番楽かも。

TWebBrowser を非表示にして色々やりたい

TWebBrowser を Visible := false にして使おうと思っても,NavigateComplete2 イベントは発生するものの,DocumentComplete イベントは発生しません。事実上使えないわけです。印刷とか。

で,対処法は,TWebBrowser コンポーネントを親コンポーネントから見えない領域に追いやってしまえばよいです。

  • たとえば TWebBrowser コンポーネントを Width 800, Height 600 で作成
  • Left を -800 とかにする(念のためもうちっと大きめなほうがいいかも)
  • コンポーネント(フォーム等)の AutoScroll プロパティを false にする(フォームにスクロールバーが出てしまうため)

単純な仕組みですが思いつくのに半日かかりました。このへんをダイナミックにいじると,表示したり隠したりもできます。

*1:Visual C++ でも Compiler COM Support を利用すればそんなに大変ではありませんが

Apacheで統合Windows認証を使う

前書き

統合 Windows 認証とは,ドメインの認証情報を使って HTTP サーバに認証してもらう方式です。Windows クライアントがドメインにログインしていれば,認証ダイアログが出現することなく自動的に認証されます。統合 Windows 認証には以下の2通りがあります。

今回はわけあって NTLM 認証を扱います。

Apache on Unix*1 で NTLM 認証をサポートするものには,有名なもので以下の物があります。

前者 2 つはほぼ同じもの(2 つめが改良版で Apache 2.2 にも対応している)ですが,後者の mod_auth_ntlm_winbind は次の利点があります。

  • Samba に付属の ntlm_auth ヘルパコマンド*2をバックエンドに利用 / おまかせしている
  • そのため所属グループ単位の認証等ができる
  • NTLMv2 に対応している(らしい);Vista のデフォルト設定でも OK
  • SPNEGO 認証に対応している(はず);オープン規格万歳 & よりシンプルなネゴシエーション
  • Basic 認証にも対応している*3

デメリットとしては,

  • Samba suite がインストールされている必要がある
  • winbindd がきちんと設定されている必要がある
  • smbd / nmbd も立ち上がっているほうがいいと思う
  • バックエンドとして ntlm_auth を子プロセスとして起動するのでちょっと重そう

一応,ntlm_auth コマンドは同一 connection だと使い回すのでそこまで重さについては気遣わなくてもいいはず。あと,どうやら smbd / nmbd だけではなく winbindd も立ち上がってなくてもよいみたいです。ただし smb.conf の winbind のための設定はしておく必要があります。その代わり NTLM だとどうかわかりませんが少なくとも SPNEGO だと,net join してある必要があります。

やっぱり winbindd が立ち上がってないとダメなケースに遭遇しました。あと,ntlm_auth というヘルパスクリプトhttpd のサーバ権限で起きるのですが,/var/cache/samba/winbindd_privileged/pipe にアクセスできるようにしておく必要があります。

今回は mod_auth_ntlm_winbind を扱います。単純に Unofficial mod_ntlm に気づかなかっただけです。

NTLM 認証の流れ

The NTLM Authentication Protocol and Security Support Provider】が参考になります。

  1. Client がコンテンツを要求する
  2. Server が 401 Unauthorized を返す
    • サポートしている認証様式として WWW-Authenticate: NTLM を返す
    • ここで一度 Connection を切る
  3. Client が Authorization: NTLM hogehoge というヘッダつきでコンテンツを要求する
    • hogehoge は nagotiate パケット
    • ここから keep-alive 接続である必要がある
  4. Server が 401 Unauthorized を返す
    • WWW-Authenticate: NTLM fugafuga ヘッダを返す
    • fugafuga は challenge パケット
  5. Client が Authrorization: NTLM gomogomo というヘッダつきでコンテンツを要求する
    • gomogomo は authenticate パケット
  6. Server は authenticate パケットを認証し,正しければ 200 OK を返す
  7. ここから同じコネクションで WWW-Authenticate / Authorizatation ヘッダなしでコンテンツをやりとりする

mod_auth_ntlm_winbind のビルド

どうやら Fedora のレポジトリに入るみたいです。ですがとりあえず今のところは手ビルド,ってことで。
配布元から必要なファイルをダウンロードします。configure.in, Makefile.in, mod_auth_ntlm_winbind.c が必須です。また .in しかないことからおわかりのとおり,autoconf も必要です。ほか RedHat 系なら apxs が必要なので httpd-devel もインストールしておいてください。

そのままビルドして一応動くのですが,いくらか気にくわないところがあったので,パッチをおいておきます。

--- mod_auth_ntlm_winbind.c.orig        2007-01-29 13:00:09.000000000 +0900
+++ mod_auth_ntlm_winbind.c 2007-07-11 12:22:13.000000000 +0900
@@ -80,4 +80,10 @@
 #endif
 
+#if defined(APACHE2) && AP_SERVER_MINORVERSION_NUMBER >= 2
+#define APACHE22 1
+#include "ap_provider.h"
+#include "mod_auth.h"
+#endif
+
 /* The name of the NTLM authentication scheme.  This appears in the
    'WWW-Authenticate' header in the initial HTTP request. */
@@ -490,5 +496,5 @@
 /* Call winbind to authenticate a (user, password)
    pair */
-static int winbind_authenticate_plaintext( request_rec *r, ntlm_config_rec * crec, char *user, char *pass)
+static int winbind_authenticate_plaintext( request_rec *r, ntlm_config_rec * crec, const char *user, const char *pass)
 {
     ntlm_connection_context_t *ctxt = get_connection_context( r->connection );
@@ -917,5 +923,5 @@
         } else
             sent_pw = "";
-        if ((s = strchr(sent_user, '\\')) != NULL
+        if (1 || (s = strchr(sent_user, '\\')) != NULL
             || (s = strchr(sent_user, '/')) != NULL) {
 
@@ -950,4 +956,10 @@
                                           : "Authorization");
     const char *auth_line2;
+#ifdef APACHE22
+    const char *current_auth = ap_auth_type(r);
+    if (!current_auth || (strcasecmp(current_auth, NTLM_AUTH_NAME) && strcasecmp(current_auth, NEGOTIATE_AUTH_NAME))) {
+        return DECLINED;
+    }
+#endif
 
     /* Trust the authentication on an existing connection */
@@ -1040,4 +1052,21 @@
 }
 
+#ifdef APACHE22
+static authn_status check_ntlm_winbind_plaintext(request_rec *r,
+                                                 const char *user,
+                                                 const char *password)
+{
+    ntlm_config_rec *conf = ap_get_module_config(r->per_dir_config,
+                                                 &auth_ntlm_winbind_module);
+    return
+        winbind_authenticate_plaintext(r, conf, user, password) == OK
+            ? AUTH_GRANTED : AUTH_DENIED;
+}
+
+static const authn_provider authn_ntlm_winbind_provider = {
+    &check_ntlm_winbind_plaintext,
+    NULL
+};
+#endif
 
 static void register_hooks(apr_pool_t *pool)
@@ -1045,4 +1074,7 @@
     ap_hook_pre_connection(ntlm_pre_conn,NULL,NULL,APR_HOOK_MIDDLE);
     ap_hook_check_user_id(check_user_id,NULL,NULL,APR_HOOK_MIDDLE);
+#ifdef APACHE22
+    ap_register_provider(pool,AUTHN_PROVIDER_GROUP,"ntlm_winbind","0",&authn_ntlm_winbind_provider);
+#endif
 };
 

変更したところは,

  • mod_auth_basic の authn バックエンドとして使えるようにした(あまりメリットはないです)
  • デフォルトの plaintext 認証ではなぜかドメイン名つきだと認証できなかったので(smb.conf の設定のせいかもしれません)ドメイン名なしのユーザ名でも winbind 認証に投げるようにした
  • AuthType 設定がなされているかチェックするようにした

の以上です。

winbindd の設定

winbindd の設定については他のリソースを参照するか手前味噌ながら【winbind で Linux の認証を ActiveDirectory にまかせる - daily dayflower】を参照してください。

Apache の設定(NTLM 認証編)

先ほど述べた NTLM 認証の流れをみていただければわかりますが,keep-alive 接続がサポートされていることが前提になっています。ですから KeepAlive は必ず On にしておいてください。RedHat 系の httpd.conf だとデフォルトで Off と書いてあるのではまりました。

モジュール自身の設定については README に書いてありますが,間違い*4もあるので書いておきます。

デフォルト設定は以下のようになっています。

NTLMAuth      off    # NTLM認証を有効にする
NegotiateAuth off    # SPNEGO認証を有効にする
NTLMBasicAuth off    # (モジュール内蔵)Basic認証を有効にする

NTLMBasicAuthoritateive on  # (実は使われていない)
NTLMBasicRealm "REALM"      # モジュール内蔵Basic認証の場合のレルム

NTLMAuthHelper "ntlm_auth --helper-protocol=squid-2.5-ntlmssp"
NegotiateAuthHelper "ntlm_auth --helper-protocol=gss-spnego"
PlaintextAuthHelper "ntlm_auth --helper-protocol=squid-2.5-basic"

ですから NTLM 認証のみを行う場合の典型的なコンフィグ(.htaccess とか <Location> 内とか)は以下のようになります。

Require valid-user

AuthType NTLM

NTLMAuth on
NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp"

これだけです。実は一番最後の行も先に挙げたデフォルトの通りなので必要ありません。

では認証してみます。ドメインにログオンした WindowsIE からアクセスすると,ダイアログなしで認証されます。ログを抜粋すると,

mod_auth_ntlm_winbind.c: doing ntlm auth dance
mod_auth_ntlm_winbind.c: Launched ntlm_helper, pid 14277
mod_auth_ntlm_winbind.c: creating auth user
mod_auth_ntlm_winbind.c: parsing reply from helper to YR TlRM...

libsmb/ntlmssp.c:debug_ntlmssp_flags(63)
  Got NTLMSSP neg_flags=0xa208b207

mod_auth_ntlm_winbind.c: got response: TT TlRM...
mod_auth_ntlm_winbind.c: sending back TlRM....

mod_auth_ntlm_winbind.c: doing ntlm auth dance
mod_auth_ntlm_winbind.c: Using existing auth helper 14277
mod_auth_ntlm_winbind.c: parsing reply from helper to KK TlRM...

libsmb/ntlmssp.c:ntlmssp_server_auth(672)
  Got user=[dayflower] domain=[HOGE] workstation=[WINDOWS] len1=24 len2=24
libsmb/ntlmssp_sign.c:ntlmssp_sign_init(338)
  NTLMSSP Sign/Seal - Initialising with flags:
libsmb/ntlmssp.c:debug_ntlmssp_flags(63)
  Got NTLMSSP neg_flags=0xa2088205

mod_auth_ntlm_winbind.c: got response: AF dayflower
mod_auth_ntlm_winbind.c: authenticated dayflower
mod_auth_ntlm_winbind.c: retaining user dayflower
mod_auth_ntlm_winbind.c: keepalives: 2

のようになっていました(YR だの TT だの KK だの AF だのが気になる方は,【Squid NTLM authentication project】の【The Squid-NTLM helper protocol】参照)。また,$ENV['REMOTE_USER'] に dayflower, $ENV['AUTH_TYPE'] が NTLM となっていました。

実は Firefox 2 (for Linux) も NTLM 認証に対応しています。この場合認証ダイアログが出るので,ドメインのユーザ名とパスワードを入力すると認証されます。

応用編(fallback として Basic 認証も使う)

このままだと例えば Opera (for Linux) ではアクセスできなくなってしまうので,Basic 認証も付け加えてみましょう。この場合,mod_auth_ntlm_winbind 組み込みの Basic 認証メカニズムを使う必要があります*5

Require valid-user

AuthType NTLM

NTLMAuth on
NTLMBasicAuth on
NTLMBasicRealm "Input Windows ID / Password"

NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp"
PlaintextAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-basic"

これで,

  • WindowsIE からだと(「統合 Windows 認証」が有効な場合)認証ダイアログなしで認証
  • WindowsFirefox からだと条件付きで*6認証ダイアログなしで認証
  • LinuxFirefox からだと認証ダイアログが出現するが NTLM 認証で
  • Linux / WindowsOpera からだと Basic 認証で

認証できるようになりました。

応用編(任意のグループだけアクセス許可する)

man ntlm_auth の内容から,

NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp
                --require-membership-of='Domain Admins'"

みたいにすればいけるのではないかなぁと思うのですが,ためしていません。

おまけ(ドメインアカウントで Basic 認証だけ使う)

AuthBasicProvider として登録するようにソースを変更したので,Basic 認証のみを使う場合,

Require valid-user

AuthType Basic
AuthName "Input ID / Password"
AuthBasicProvider file ntlm_winbind

AuthUserFile "/foo/bar/.htpasswd"
PlaintextAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-basic"

のようにすることもできます。この例だと,

  • .htpasswd にユーザがいれば OK
  • いなければ NTLM にユーザがいれば OK

のようになります。

積み残し

SPNEGO 認証もいけるはず,なんですがどうしてもうまくいきません。Kerberos のチケットをうまく validate できてないみたいなので(私的環境の AD / )DNS まわりの問題かなぁと思ったりしていますが,検証めんどくさい……

keep-alived な connection では一度認証したら二度としないという点で NTLM 認証はあやういな,と思いました。サーバ側の実装を間違えると。

*1:win32 の場合,素直に IIS を利用するか mod_auth_sspi を利用してください

*2:もともとは Squid で利用するために開発されたものです

*3:通信路をドメインパスワードが解読可能な形で流れるのであんまりおすすめできない

*4:AuthType は複数指定しても無効で一番最後の指定が有効になる;のに複数指定した例がある

*5:事実上「認証ハンドラ」モジュールは単一であることしか考慮されていない気がします……Apacheのコードが

*6:network.automatic-ntlm-auth.trusted-uris に適切な URI(一部で可;たとえば http:// とかでも OK)を指定する

CentOS 5 で Samba を PDC にする

まえがき

ありがちなネタですが Samba 3 + OpenLDAPLinux Box を PDC にしてみました。目新しいことは書いてません。

特に以下の2サイトを参考に設定しました。

前者はミラクル・リナックスの小田切さんのお書きになった記事で,このとおりに Step by step で進んでいくと,Samba による PDC を立ち上げることができます。今回はほぼ前者の記事の手順を踏襲しましたが,時代が進んで変わった部分,CentOS の場合にどうしたか,という差分についてのみ触れます。

環境は,CentOS 5 の VMware Server 環境上に NAT ネットワークを構築し,ゲスト OS として CentOS 5(PDC),Windows 2000, Windows XP をインストールしてテストしました。

OpenLDAP まわり

Samba のスキーマとしては /usr/share/doc/samba-3.0.xxx/LDAP/samba.schema を /etc/openldap/schema にコピーして使いました。直接指定してもいいんでしょうけど。

記事中には特に言及されていませんが,OpenLDAP サーバがバックエンドとして用いる BDB の設定ファイルをおいておかないと slapd の起動時に文句をいわれてしまいます。/etc/openldap/DB_CONFIG.example を「/var/lib/ldap/DB_CONFIG」にコピーします(参考→【http://www.ie.u-ryukyu.ac.jp:16080/howto/index.php?Software%2FOpenLDAP#de286edd】)。

smbldap-tools まわり

smbldap-tools を配布していた idealx のサイトはもうありません。対応するページといえば,【IDX-smbldap-tools download | SourceForge.net】かな。ちなみに sourceforge には smbldap-tools の追加スクリプトプロジェクト【SMBLDAP-TOOLS Addons】があります。

私の場合は,samba パッケージに付属の smbldap-tools を使いました。といってもデフォルトではサーチパスにインストールされていないので,/usr/share/doc/samba-3.0.xxx/LDAP/smbldap-tools-0.9.2/ 以下に移動し,Makefile を適宜書き換えて(デフォルトがすごい内容になってるんですもの)make install しました。

prefix=/usr
sysconfdir=/etc

に書き換えると RedHat ぽいインストール先になります。以上の手順に沿うと,記事中で smbldap-HOGEHOGE.pl となっているところの末尾の「.pl」は指定する必要がありません。

あと必要な Perl モジュールをインストールします。Net::LDAP, Crypt::SmbHash, Unicode::MapUTF8 あたりです(/usr/bin/smbldap_tools.pm の先頭をみるとわかります)。Unicode::MapUTF8 は utf8 か Encode モジュールを使えば必要なくできそうですが,やっていません。

[ThinkIT] 第3回:NT4.0→Samba3.0への移行(2) (4/4)】の「smbldap-tools の設定」で書かれている設定ファイルの場所,指定の仕方が変更になっています。設定ファイルは /etc/smbldap-tools/smbldap.conf と /etc/smbldap-tools/smbldap_bind.conf に分散しており,設定子の先頭に「$」をつける必要はありません。

samba の設定

Windows95/98/Meはドメイン・ログオンするのにSamba PDC上で何も設定が必要ないが、SambaとWindowsNT/2000/XP/2003をドメイン・メンバに加える場合は、PDCマシンの上でドメイン・メンバ・マシンのマシンアカウントを作成する必要がある。

[ThinkIT] 第5回:Sambaドメインに参加 (1/3)

とありますが,smb.conf に「add machine script」が適切に設定されていれば,手作業でマシンアカウントを登録する必要はありません。Windows マシンからのドメインへの参加時に自動的にマシンアカウントが生成されます。

はまりどころ

OpenLDAP の管理者 DN(識別子)が記事中ちょっとブレたりしてるんですが*1,ActiveDirectory に習い,

uid=Administrator,ou=Users,dc=hoge,dc=example,dc=com

で統一しました。

ユーザマネージャ,サーバマネージャは Windows 2000 SP4 から展開して取得する方法もあるんですが,面倒だったのでWindows NT 4.0 用のものを取得しました。英語版になっちゃいますけど。

最終的にドメインユーザを登録して Domain Controller として利用する場合,前回,前々回の日記で書いたように pam_mkhomedir を /etc/pam.d/system-auth に設定するほうがよいでしょう(設定しなくても NETLOGON 共有と profiles 共有が生きていれば,一応のログインはできます)。

鼻唄まじりに記事どおりにやっていたらうまく smb が立ち上がらなかったりしましたが,その場合なにかの手順や設定がちゃんとできていないのでしょう。私の場合,nsswitch.conf に ldap を追加するのを忘れていたのがハマった原因でした。

RedHat 系の LDAP サーバ起動順の不具合

nsswitch.conf で ldap を指定している場合,messagebus (dbus-daemon) も ldap 経由で情報を得ようとします。ここで問題になるのが,/etc/rc.d で ldap より先に messagebus が指定されているため,システム起動時に messagebus の段階でほぼ halt 状態になってしまうことです(参考→【ブート時に udev が LDAP (or NIS) を見に行って帰ってこない - ドレッシングのような】)。対策として 2 通りあります。

私はシステムへの影響が少なそうな後者を選びました。/etc/ldap.conf*2 に,

bind_policy soft 

という設定を付け加えました。これで無事すんなりシステム起動するようになりました。

おわりに

*1:具体的には【[ThinkIT] 第3回:NT4.0→Samba3.0への移行(2) (3/4)】の「ldap admin dn」のところが cn=Manager, ... になってるんですが,後述されている smb.conf の例からいって記述ミスと思います

*2:/etc/openldap/ldap.conf とは別物です。まぎらわしい。

JIS2004 フォントを入れてみたけれど

Windows XPJIS2004 対応 MS ゴシック & MS 明朝フォントをいれてみたりしたんですが,2のビットマップフォントデザインがおかしいのはともかくとして,一部の Web ページで日本語フォントが SimSun に なったりしてなんだかおかしいのでアンインストールしました。

FontLink をとくにいじったつもりはないんですが私だけ?