lighttpdでWebDAV + LDAPを利用する。

結論を言うとユーザの権限をフラットにしておく場合には使えます。が、ユーザの権限をグループ毎に分ける場合には、LDAPのグループ、というかグループでの認証には未対応なのでApacheを使うと良いでしょう。

lighttpdの設定。

WebDAV + LDAPを使う方法だけとりあえず記録しておきました。

/etc/lighttpd/lighttpd.conf

diff --git a/lighttpd/lighttpd.conf b/lighttpd/lighttpd.conf
index 32eb9d5..d605901 100644
--- a/lighttpd/lighttpd.conf
+++ b/lighttpd/lighttpd.conf
@@ -31,3 +31,16 @@ compress.filetype           = ( "application/x-javascript", "
text/css", "text/ht

 include_shell "/usr/share/lighttpd/create-mime.assign.pl"
 include_shell "/usr/share/lighttpd/include-conf-enabled.pl"
+
+$HTTP["host"] == "hoge.example.org" {
+       server.document-root = "/var/www/"
+       alias.url = ( "/dav" => "/var/www/dav" )
+       $HTTP["url"] =~ "^/dav($|/)" {
+               webdav.activate = "enable"
+               webdav.is-readonly = "disable"
+               webdav.sqlite-db-name = "/var/run/lighttpd/lighttpd.webdav_lock.
db"
+               auth.require = ( "" => ( "method" => "basic",
+                                        "realm" => "hoge",
+                                        "require" => "valid-user" ) )
+       }
+}
  • webdav.activateをenableでWebDAVを有効に

  • webdav.is-readonlyをdisableで書き込み可に

  • webdav.sqlite-db-nameにWebDAV用のロックファイルを指定

  • auth.requireでLDAPのBASIC認証の認証方式を指定

/etc/lighttpd/conf-available/05-auth.conf

diff --git a/lighttpd/conf-available/05-auth.conf b/lighttpd/conf-available/05-auth.conf
index 5d896e3..4323034 100644
--- a/lighttpd/conf-available/05-auth.conf
+++ b/lighttpd/conf-available/05-auth.conf
@@ -2,13 +2,13 @@

 server.modules                += ( "mod_auth" )

-# auth.backend                 = "plain"
+auth.backend                 = "ldap"
 # auth.backend.plain.userfile  = "lighttpd.user"
 # auth.backend.plain.groupfile = "lighttpd.group"

-# auth.backend.ldap.hostname   = "localhost"
-# auth.backend.ldap.base-dn    = "dc=my-domain,dc=com"
-# auth.backend.ldap.filter     = "(uid=$)"
+auth.backend.ldap.hostname   = "ldapserver"
+auth.backend.ldap.base-dn    = "o=hoge,dc=example,dc=org"
+auth.backend.ldap.filter     = "(uid=$)"

 # auth.require                 = ( "/server-status" =>
 #                                (
  • auth.backendで認証方法をLDAPに指定

  • auth.backend.ldap.hostnameにLDAPサーバのホスト名またはIPアドレスを指定

  • auth.backend.ldap.base-dnにLDAPのbase DNを指定

  • auth.backend.ldap.filterにLDAPのユーザ認証用に(uid=$)をフィルター指定

モジュールの有効化

DebianのlighttpdはApacheと同様、利用できる設定(conf-available/)と有効になっている設定(conf-enable/)があるので、WebDAVとLDAPの認証を有効にするためには、/usr/sbin/lighty-enable-modコマンドを使用します。

$ sudo lighty-enable-mod auth
Available modules: auth accesslog cgi evasive evhost expire fastcgi flv-streaming no-www \
proxy rrdtool simple-vhost ssi ssl status userdir usertrack webdav fastcgi-php debian-doc
Already enabled modules:
Enabling auth: ok
Run /etc/init.d/lighttpd force-reload to enable changes
$ sudo lighty-enable-mod webdav
Available modules: auth accesslog cgi evasive evhost expire fastcgi flv-streaming no-www \
proxy rrdtool simple-vhost ssi ssl status userdir usertrack webdav fastcgi-php debian-doc
Already enabled modules: auth
Enabling webdav: ok
Run /etc/init.d/lighttpd force-reload to enable changes

lighttpdでグループでの認証が未対応な理由。

lighttpd.confのauth.requireのパラメータのrequireにgroupとしても、下記のログがでます。

2011-07-14 16:11:11: (http_auth.c.402) group ... (not implemented)
2011-07-14 16:11:11: (http_auth.c.416) nothing matched
2011-07-14 16:11:11: (http_auth.c.888) rules didn't match

え、”note implemented”って…。

Sidのlighttpd 1.4.29のソースコード(src/http_auth.c)を見ると、

/* the part before the = is user|group|host */

k = r;
k_len = eq - r;
v = eq + 1;
v_len = r_len - k_len - 1;

if (k_len == 4) {
        if (0 == strncmp(k, "user", k_len)) {
                if (username &&
                    username_len == v_len &&
                    0 == strncmp(username, v, v_len)) {
                        return 0;
                }
        } else if (0 == strncmp(k, "host", k_len)) {
                log_error_write(srv, __FILE__, __LINE__, "s", "host ... (not implemented)");
        } else {
                log_error_write(srv, __FILE__, __LINE__, "s", "unknown key");
                return -1;
        }
} else if (k_len == 5) {
        if (0 == strncmp(k, "group", k_len)) {
                log_error_write(srv, __FILE__, __LINE__, "s", "group ... (not implemented)");
        } else {
                log_error_write(srv, __FILE__, __LINE__, "ss", "unknown key", k);
                return -1;
        }

と、ユーザ認証しか対応していないのでした。ググってみると、過去にも同じような話がありました。

http://redmine.lighttpd.net/issues/1817

LDAP-Group support for HTTP-Authentication Support for using ldap DN in auth-require, example:

auth.require = (
    "/" => (
        "method" => "basic",
        "realm" => "test lighty auth",
        "require" => "group=cn=coolguys,ou=groups,dc=foo,dc=org|user=admin|group=cn=group2,ou=groups,dc=foo,dc=org"
    )
)


Patch also available at  `http://danielbond.org/patches/lighttpd-http_auth.c-ldap_group.diff <http://redmine.lighttpd.net/attachments/download/678/lighttpd-http_auth.c-ldap_group.diff>`_

これに対応するパッチ も添付されているのですが、最初に投稿されてから2年以上も経っているのに未だマージされていません。

というわけで、lightyにこだわる理由も特にないので、結局今回はApacheで行うことにしました。

こまちゃん監視システム10日経過して。

7/9から11日経過してアップロードされたデータが約165MB、ファイル数にして約15300枚 1 。1時間あたりでのアップロードの最大数が500枚弱とかでも、負荷も特になく淡々と処理してくれています。今のところ5分周期でアップロードしていますが、100枚単位でアップロード直後されても、すぐにviewで確認できるので、CouchDB意外とやるなぁという感じです 2

ちなみに5000ドキュメント程度のときに、CouchDBのホスティングサービス 3 にレプリケーションを起こってみましたがが、だいたい10~15分程度だったと思います。viewサーバの分散目的でレプリケーションを使うなら、continuousオプションを使ってレプリケーションを行えば、アップロードされると同時に自動的に差分がレプリケーションされます。URLのディスパッチをうまくやればCouchDBでCDN的な使い方もできるので、そっちの使い方も何か考えたいですねー。

1

サムネイル作っているので正確にはその2倍ですが。

2

今まではAndroid用のCouchDBを専ら使っていたので。

3

Iris Couch

こまちゃん監視システムをPicasaからCouchDBに切り替えた。

3.11の震災をきっかけに、 こまちゃん監視サーバを構築しました が、運用しはじめて問題が3つほど出てきました。

  • 写真のアップロード先をPicasaにしていましたが、1アルバムへのアップロード数の上限が1000ファイルまでと少ない

    • こまちゃんが写らないときは、30分毎のスナップショットだけなので問題ありませんが、写る場合は数秒で数十枚は撮影されます。なので写る場合の時間当たりの平均枚数は100-200枚になるため、すぐに上限に達してしまいます

    • そのためアップロードとは別に定期的に削除が必要でした

  • 一度にアップロードまたは削除するファイル数が多いとエラーになる

    • これはアップロード用に使っているgoogleclだけでなく、webブラウザ経由でも同じ問題です

    • また、googleclでは写真の検索が期待値通りにならない、という問題もあります 1

  • 致命的なのは、ヨメがGoogleアカウントのID、パスワードを覚えないので、 「こまちゃんの写真を見られない」と頻繁に言われる

    • 原因はキャッシュが切れてログアウトされているのに、ログインしないままアクセスしたり、ログインしてもアクセス権限のないID, パスワードを入力していたりといったところです。 2

    • これらはシステム的な問題ではありませんが、ヨメがログインとかを気にせずに使えるようにする必要があります。

      • 普通の人には複数のアカウントを管理して、パスワードも別にしておいて、というのは結構敷居が高いのですね。

上記の問題をクリアして、簡単にDBとアプリをコピーできるようにする必要があるので、データのアップロード先をPicasaからCouchDBに変更しました。

アップロード先をCouchDBに変更する上での仕様

ざっくりやること決めて、最低限の機能を実装したので、まとめてみました。

  • こまちゃん監視カメラで取得した写真画像を、撮影された時刻と一緒にアップロードする

  • 時間単位でサムネイルをプレビューできるようにする

  • 定期的なスナップショットは不要とする

  • 撮影された時間単位で写真の枚数を表示し、過去に遡る形で10件(断続的に10時間分)をリストする

  • 指定したサムネイルから実際の画像ファイルを表示する

という感じです。詳細は次のとおりです。 ソースコードはPicasaの時と同様に、 github に晒しています。

アップロード処理

  • オリジナル画像からサムネイルを生成する

'''Generating thumbnail.'''
 def generateThumbnail(self):
     import Image, os
     image = Image.open(self.filename,mode='r')
     image.thumbnail([60,60])
     self.thumbnail_name = os.path.splitext(self.filename)[0] + "_s.jpg"
     image.save(self.thumbnail_name)
  • オリジナル画像とサムネイルは撮影時刻などのメタ情報を紐付けるため、CouchDBのStandaloneの添付ファイルを使わず、ドキュメントのインラインの添付ファイルにする

     '''generate dict as document.'''
     def generateDict(self):
         self.doc = {
 (snip)
             "photo":self.filename,
             "thumbnail":self.thumbnail_name,
             "_attachments":{
                 self.filename:
                     {
                     "content_type":"image/jpeg",
                     "data":self.image_base64
                     },
                 self.thumbnail_name:
                     {
                     "content_type":"image/jpeg",
                     "data":self.thumbnail_base64
                     }
                 }
             }



* インラインの添付ファイルにするため、画像ファイルはbase64にエンコードする
'''Encoding photo image file to base64 ascii strings.'''
def encodeBase64(self, image):
    import base64
    return base64.encodestring(open(image,"rb").read())
  • 時刻情報などのメタ情報は、画像のファイル名から取得する

    • 当初、ctimeから取得しようとしましたが、撮影された時刻と、ファイルが生成されてファイルシステムに書き込まれた時刻に30-60秒ほどのタイムラグがあるので、撮影された時刻が正確なファイル名から取得することにしました

'''Getting date info from filename.'''
def getDate(self):
    import re
    t = re.match('^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})',
                 re.match('(\d+)-(\d+)-(\d+)-(\d).jpg', self.filename)
                 .group(2))
    self.year = t.group(1)
    self.mon = t.group(2)
    self.mday = t.group(3)
    self.hour = t.group(4)
    self.min = t.group(5)
    self.sec = t.group(6)
  • メタ情報とbase64にエンコードした画像をJSONにする

    • 1画像=1ドキュメントです

  • まとめて複数画像をアップロードするため、CouchDBの_bulk_docs APIを使うために、単一のJSONオブジェクトにする

'''Serializing JSON for bulk_docs.'''
def serializedJson(self):
    import json
    self.bulk_docs = json.JSONEncoder().encode({
            "all_or_nothing":"true",
            "docs":self.docs
            })
  • JSON化済みの画像は重複アップロードを防ぐために、削除する

# remove jpg files.
             os.remove(self.filename)
             os.remove(self.thumbnail_name)

上記のJSONに変換するための処理のコードは こちら です。

次に、実際のファイルをアップロードするための処理は今回はcurlコマンドで_bulk_docs APIにPOSTするだけにしました。

  • 複数の画像をまとめたJSONをアップロードする(_bulk_docs)

curl -X POST -H 'Content-Type:application/json' -d @${JSON} \
         http://${USER}:${PASS}@${HOSTNAME}/${DB}/_bulk_docs

コードは これ

CouchDBの処理

CouchDB側はデータストア兼画像ビューワーなので、前述の通り、撮影された時間帯のリストアップ、時間帯毎にサムネイルの一覧表示、選んだサムネイルを拡大表示、前後に撮影した写真の直接表示ができればOKです。

  • ドキュメントの年(year)、月(mon)、日(mday)、時(hour)の配列をキーに、_view/dateで一時間当たりの画像の総数を取得する

with-couchdb/petviewer/views/date/map.js

 function(doc) {
     if (doc.photo) {
         var arrayDate = [doc.year, doc.mon, doc.mday, doc.hour];
         emit(arrayDate, 1);
     }
 }





* reduce処理で一時間当たりのMap処理の結果の総数を算出する

with-couchdb/petviewer/views/date/reduce.js

function(keys,values) {
    return sum(values);
}

追記

CouchDBのReduce処理には組み込み関数が用意されているので、上記のようにsumを行う場合は、

_sum

とするだけでも大丈夫です 3 。他には基本的な統計情報を出すための_stat関数も用意されています。

  • MapReduce処理の結果から時間帯毎に画像の件数を取得し、mustacheとjQuery Mobileを使ってリンクリストにする 4

with-couchdb/petviewer/lists/hours.js

(snip)
    var datalist = [];
    var row;
    while (row = getRow()) {
        datalist.push({
            year: row.key[0],
            mon: row.key[1],
            mday: row.key[2],
            hour: row.key[3],
            num: row.value
        });
    }
(snip)

with-couchdb/petviewer/templates/hours.html

(snip)
      <div data-role="content">
        <ul data-role="listview">
          {{#datalist}}
          <li><a href="../hour/thumbnail?key=%22{{year}}{{mon}}{{mday}}{{hour}}%22" data-ajax="false">{{mon}}/{{mday}} {{hour}}時</a><span class="ui-li-co
unt">{{num}}</li>
          {{/datalist}}
        </ul>
      </div>
(snip)
  • “YYYYMMDDhh”を検索キーとして_view/thumbnailを取得する

with-couchdb/petviewer/views/thumbnail/map.js

function(doc) {
    if(doc.photo) {
            emit(doc.year + doc.mon + doc.mday + doc.hour, doc);
    };
}
  • _view/thumbnailの結果を_list/hourに渡して、mustacheでサムネイルの一覧画面を生成する

with-couchdb/petviewer/lists/hour.js

(snip)
    var datalist = [];
    var row;
    while (row = getRow()) {
        datalist.push({
            _id: row.value._id,
            thumbnail: row.value.thumbnail,
            photo: row.value.photo,
            year: row.value.year,
            mon: row.value.mon,
            mday: row.value.mday,
            hour: row.value.hour,
            min: row.value.min,
            sec: row.value.sec
        });
    }
(snip)
}

with-couchdb/petviewer/templates/hour.html

(snip)
      <div data-role="content">
          {{#datalist}}
          <span>
            <a href="../../_show/photo/{{_id}}" data-ajax="false">
              <img id="thumbnail" src="../../../../{{_id}}/{{thumbnail}}"
                   alt="{{year}}/{{mon}}/{{mday}} {{hour}}:{{min}}:{{sec}}"/></a>

          {{/datalist}}
      </div>
(snip)
  • photo showで、画像の表示を行う

with-couchdb/petviewer/shows/photo.js

function (doc, req) {
(snip)
    data = {
        _id: doc._id,
        photo: doc.photo,
        year: doc.year,
        mon: doc.mon,
        mday: doc.mday,
        hour: doc.hour,
        min: doc.min,
        sec: doc.sec
    };
(snip)
  • 画像の表示画面では時系列で前後の画像表示画面に直接遷移できるようにする

    • ドキュメントには時系列で前後のドキュメントの情報は持っていません。なので、JavaScriptで、時刻(YYYYMMDDhh)をキーに_view/thumbnailから一時間あたりの全ドキュメントのdoc._idをJSONで取得し、それから前後の写真をそれぞれ指すdoc._idを配列として保持し、表示している写真のdoc._idをキーに前後を検索し、リンクを作ります

with-couchdb/petviewer/_attachments/js/pointer.js

function getSearchKey() {
    return $('input#searchkey').val();
}

function basename(path) {
    return path.replace(/\\/g,'/').replace( /.*\//, '' );
}

function getDocId () {
    return basename(window.location.pathname);
}

function getJsonUri() {
    return "../../_view/thumbnail?key=%22" + getSearchKey() + "%22";
}

function getThumbnailListUri() {
    return "../../_list/hour/thumbnail?key=%22" + getSearchKey() + "%22";
}

$.getJSON(getJsonUri(),
          function(data) {

              var id = [];
              var nextid = [];
              var previd = [''];
              var idlist = new Array();

              // parse JSON.
              $.each(data, function(key, val) {

                  if (data.rows) {
                      $.each(val, function(key2, val2) {

                          $.each(val2, function(key3, val3) {

                              if (key3 == "id") {
                                  id.push(val3);
                              }
                          });
                      });
                  }
              });

              // next id list.
              nextid = id.slice(0);
              nextid.shift();
              nextid.push('');

              // previous id list.
              previd = previd.concat(id.slice(0));
              previd.pop();

              // Array idlist is [["id", "previd", "nextid"], [],...]
              for (var i in id) {
                  idlist.push([
                      id[i],
                      previd[i],
                      nextid[i]
                  ]);
              }

              // search previd, nextid by id.
              for (var i = 0; i < idlist
1

ドキュメントどおりの正規表現の結果にならないという…。

2

Googleアカウントだけでなく、Google Appsのアカウントもあるのです。

3

というか、このブログを書いた後に、 Writing and Querying MapReduce Views in CouchDB を読んで知りました。

4

mustacheとjQuery Mobileについては、 以前のエントリ を参照のこと。

トイレチップ×10頂きました。

hiyuh さんに、こまちゃん宛にトイレチップ(我が家での通称はこまチップ)を10袋頂きました!

http://twitter.com/#!/hiyuh/status/89496406418661377:

@mkouhei ×10おくったった :DDDless than a minute ago

今日届いた時の様子はこちら↓

../../../_images/20110711004553.jpg

こまチップが笑っとる

../../../_images/20110711004551.jpg

こまちゃんと一緒に撮ってみた。ワシの左手が邪魔ですな。

../../../_images/20110711004552.jpg

重ねるとものすごい高さですw

ありがとうございます!!

sshでProxyCommandを使うとlastlogは更新されない。

あるサーバの背後にあるサーバにSSHでログインしたい場合、~/.ssh/configに、

Host targetserver-*
        ProxyCommand ssh StSt nc -w 22222 %h %p

てな幹事で設定しておいて、ログインしますよね。この場合はStStサーバ 1 を踏み台にしますが、StStではlastlogは更新されませんよ、というお話。そんだけ。

1

Stepping Stoneの略。踏み台って、英語では踏み石なんですね。