Drizzleのプラグインを書いてみた (認証系)

DrizzleのSASLプラグインを書いてみた。

Drizzleとは

前坂さんのブログ記事@ITの記事などを参照してもらえれば。

一言でいうと「Lean and Mean MySQL」。

Drizzleをビルドするには

1次情報の垂れ流し失礼。

まず、Protocol Bufferslibeventという2大バズワードコンポーネントと、PCREを利用しているのでこいつらはインストール必須。

あとは非必須コンポーネントとして、

  • OpenSSL
  • zlib
  • libintl (gettext)
  • readline

がある。

ビルドのためのツール類としては以下が必要。

  • libtool
  • automake
  • autoconf
  • bison
  • g++ (gcc)

また、ソースはBazaarを使って引っ張ってくるなどするのがよいと思われるので。これもインストール。

1. ソースのクローン

Wikiの説明を見ると、init-repoしてなんたらと書いてあるけど、これはパッチを作ったりとかゴリゴリ開発する人以外は無用な措置なのでスキップ。*1

% bzr branch lp:drizzle
2. autotoolsによるconfigure生成
% cd drizzle
% config/autorun.sh
3. configureとビルド

ビルド中にprotocコマンドを呼び出すので、PATH環境変数の設定に注意。
(prefix=/usr/localを仮定)

% ./configure --disable-nls --prefix=${prefix}
% make
4. インストール
% make install
5. 実行

mysqldを立ち上げる時はmysqld_safeスクリプトを利用していたので、きっとdrizzled_safeスクリプトを使うに違いないと思いきやWikiを見ると「壊れている」ので使わない方がいいとのこと。

% ${prefix}/sbin/drizzled --no-defaults --basedir=${prefix} --datadir=${prefix}/var &

もしこれでshared libraryが見つからない的なエラーが出たらLD_LIBRARY_PATHとかDYLD_LIBRARY_PATHとかを設定する必要があるかもしれない。

プラグインの書き方

  1. とりあえずdrizzle/plugin/hello_worldなどをdrizzle/plugin以下に名前を変えてコピー
  2. ディレクトリ配下にあるplug.inファイルの記述を書き換える。
  3. Makefile.amの内容を書き換える。
  4. ソースを準備する。
  5. ソースツリーの最上位にあるautom4te.cacheを削除してautotools cacheをクリアする。
  6. config/autorun.shを実行
  7. configure && make && make install
1. とりあえずdrizzle/plugin/hello_worldなどをdrizzle/plugin以下に名前を変えてコピー

今回は認証系のプラグインを作りたかったのでdrizzle/auth_pamを元にした。

% cd drizzle/plugin
% cp -R auth_pam auth_sasl
% cd auth_sasl
% mv auth_pam.cc auth_sasl.cc
2. ディレクトリ配下にあるplug.inファイルの記述を書き換える。

plug.inなどというふざけた名前のファイルの中身はautoconfスクリプトである。こいつをm4/plugins.m4がスキャンしてビルドシステムに反映する仕組みになっている。

DRIZZLE_PLUGIN(auth_pam,[PAM Authenication Plugin],
        [PAM based authenication.])
DRIZZLE_PLUGIN_DYNAMIC(auth_pam,   [libauth_pam.la])
DRIZZLE_PLUGIN_ACTIONS(auth_pam,  [
  AC_CHECK_HEADERS(security/pam_appl.h)
  AM_CONDITIONAL(BUILD_AUTH_PAM,[test "$ac_cv_header_security_pam_appl_h" = "yes"])
  AS_IF([test "$ac_cv_header_security_pam_appl_h" = "no"],
   [AC_MSG_WARN([Couldn't find PAM headers, pam_auth will not be built])])
])

こんな内容になっていると思う。このauth_pamというのがプラグインIDなので適宜自分のプラグイン用に書き換えていく。

重要なm4マクロについて。


  • DRIZZLE_PLUGIN

    DRIZZLE_PLUGIN(プラグインID, プラグインの名前, プラグインの説明)

    プラグイン毎に必須のマクロ。プラグインの名前や説明を指定する。


  • DRIZZLE_PLUGIN_DYNAMIC

    DRIZZLE_PLUGIN_DYNAMIC(プラグインID, libプラグインID.la)

    プラグインが動的ロードされるタイプの場合は、このマクロを記述する。


  • DRIZZLE_PLUGIN_STATIC

    DRIZZLE_PLUGIN_STATIC(プラグインID, libプラグインID.a)

    プラグインが静的ロードされるタイプの場合は、このマクロを記述する。


  • DRIZZLE_PLUGIN_MANDATORY

    DRIZZLE_PLUGIN_MANDATORY(プラグインID)

    強制的にプラグインをビルドさせる場合にこのマクロを記述する。DRIZZLE_PLUGIN_STATICと一緒に使用されるケースが多いと思われる。


  • DRIZZLE_PLUGIN_DISABLED

    DRIZZLE_PLUGIN_DISABLED(プラグインID)

    上のマクロとは逆に、デフォルトでビルドされないプラグインにはこのマクロを指定する。


  • DRIZZLE_PLUGIN_ACTIONS

    DRIZZLE_PLUGIN_ACTIONS(プラグインID, アクション)

    プラグインのビルドにあたって、追加のautoconfチェックが必要な時に指定する。



とりあえず今回はSASLのプラグインを書きたいので以下のように書き換えた。

DRIZZLE_PLUGIN(auth_sasl,[SASL Authenication Plugin],
        [SASL based authenication.])
DRIZZLE_PLUGIN_DYNAMIC(auth_sasl,   [libauth_sasl.la])
DRIZZLE_PLUGIN_ACTIONS(auth_sasl,  [
  drizzle_sasl_usable=
  AC_CHECK_HEADERS(sasl/sasl.h, [drizzle_sasl_usable=yes])
  AC_CHECK_HEADERS([netinet/in.h netinet6/in6.h])
  AM_CONDITIONAL(BUILD_AUTH_SASL,[test "$drizzle_sasl_usable" = "yes"])
  AS_IF([test "$drizzle_sasl_usable" != "yes"],
   [AC_MSG_WARN([Couldn't find SASL headers, auth_sasl will not be built])])
])

netinet/in.hとnetinet6/in6.hはあとで出てくるソースコード中でインクルードされるヘッダ群。netinet6/in6.hはnetinet/in.hからインクルードされるのだけど、一応。

3. Makefile.amの内容を書き換える。
MYSQLDATAdir =          $(localstatedir)
MYSQLSHAREdir =         $(pkgdatadir)
MYSQLBASEdir=           $(prefix)
MYSQLLIBdir=            $(pkglibdir)
pkgplugindir =      $(pkglibdir)/plugin

if BUILD_AUTH_PAM
EXTRA_LTLIBRARIES = libauth_pam.la
pkgplugin_LTLIBRARIES = @plugin_auth_pam_shared_target@
libauth_pam_la_LDFLAGS =    -module -rpath $(pkgplugindir) -lpam
libauth_pam_la_CPPFLAGS=    $(GLOBAL_CPPFLAGS) -DDRIZZLE_DYNAMIC_PLUGIN
libauth_pam_la_SOURCES =    auth_pam.cc
endif

まあこんな内容になっているので、

MYSQLDATAdir =          $(localstatedir)
MYSQLSHAREdir =         $(pkgdatadir)
MYSQLBASEdir=           $(prefix)
MYSQLLIBdir=            $(pkglibdir)
pkgplugindir =      $(pkglibdir)/plugin

if BUILD_AUTH_SASL
EXTRA_LTLIBRARIES = libauth_sasl.la
pkgplugin_LTLIBRARIES = @plugin_auth_sasl_shared_target@
libauth_sasl_la_LDFLAGS =   -module -rpath $(pkgplugindir)
libauth_sasl_la_LIBADD  =   -lsasl2
libauth_sasl_la_CPPFLAGS=   $(GLOBAL_CPPFLAGS) -DDRIZZLE_DYNAMIC_PLUGIN
libauth_sasl_la_SOURCES =   auth_sasl.cc
endif

こんな風にした。元のauth_pam用のMakefile.amでは、libpamの指定がLDFLAGSにあるけど、これは誤り。リンクしたいライブラリはlibプラグインID_la_LIBADDに指定すべき。

4. ソースを準備する。

とりあえず解説はなしで。

auth_sasl.cc:

/*
 -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
 *  vim:expandtab:shiftwidth=2:tabstop=2:smarttab:
  Sections of this where taken/modified from mod_auth_path for Apache 
*/

#define DRIZZLE_SERVER 1
#include <drizzled/server_includes.h>
#include <drizzled/plugin_authentication.h>
#include <sasl/sasl.h>
#include <stdlib.h>
#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef HAVE_NETINET6_IN6_H
#include <netinet6/in6.h>
#endif
#include <arpa/inet.h>

static const char* drizzle_auth_sasl_get_level_label(int level)
{
  switch (level)
  {
  case SASL_LOG_NONE:
    return "NONE";
  case SASL_LOG_ERR:
    return "ERROR";
  case SASL_LOG_FAIL:
    return "FAIL";
  case SASL_LOG_WARN:
    return "WARN";
  case SASL_LOG_NOTE:
    return "NOTE";
  case SASL_LOG_DEBUG:
    return "NOTE";
  case SASL_LOG_TRACE:
    return "TRACE";
  case SASL_LOG_PASS:
    return "*TRACE*";
  }
  return "UNKNOWN";
}

static int drizzle_auth_sasl_log_cb(void *ctx, int level, const char *msg)
{
  push_warning_printf((Session*)ctx,
    level >= SASL_LOG_NOTE ?
      DRIZZLE_ERROR::WARN_LEVEL_NOTE:
      DRIZZLE_ERROR::WARN_LEVEL_WARN,
    0, "[auth_sasl:%s] %s",
    drizzle_auth_sasl_get_level_label(level),
    msg);
  return SASL_OK;
}

static char* drizzle_auth_sasl_sockaddr_to_str(struct sockaddr_storage* addr)
{
  char *retval= NULL;
  int port;
  switch (addr->ss_family) {
  case AF_INET:
    retval= (char*)malloc(24);
    if (!retval)
      return NULL;
    port= ntohs(((struct sockaddr_in*)addr)->sin_port);
    if (!inet_ntop(AF_INET, &((struct sockaddr_in*)addr)->sin_addr,
        retval, 24))
    {
      free(retval);
      return NULL;
    }
    break;
  case AF_INET6:
    retval= (char*)malloc(54);
    if (!retval)
      return NULL;
    port= ntohs(((struct sockaddr_in6*)addr)->sin6_port);
    if (!inet_ntop(AF_INET6, &((struct sockaddr_in6*)addr)->sin6_addr,
        retval, 54))
    {
      free(retval);
      return NULL;
    }
    break;
  default:
    return NULL;
  }

  snprintf(retval + strlen(retval), 7, ";%d", port);
  return retval;
}

static bool drizzle_auth_sasl_authenticate(Session *session, const char *password)
{
  sasl_callback_t callbacks[]=
  {
    { SASL_CB_LOG, (int(*)())&drizzle_auth_sasl_log_cb, session },
    { SASL_CB_LIST_END, 0, 0 }
  };
  bool retval= false;
  sasl_conn_t *conn= NULL;
  char *ip_str_local= NULL, *ip_str_remote= NULL;
 
  {
    struct sockaddr_storage ss;
    socklen_t ss_sz = sizeof(ss);
    if (getsockname(vio_fd(session->net.vio), (struct sockaddr*)&ss, &ss_sz))
    {
      push_warning_printf(session,
        DRIZZLE_ERROR::WARN_LEVEL_WARN, 0,
        "Failed to retrieve the local address");
      goto out;
    }
    ip_str_local= drizzle_auth_sasl_sockaddr_to_str(&ss);
    if (!ip_str_local)
      goto out;
  }
 
  {
    struct sockaddr_storage ss;
    socklen_t ss_sz = sizeof(ss);
    if (getpeername(vio_fd(session->net.vio), (struct sockaddr*)&ss, &ss_sz))
    {
      push_warning_printf(session,
        DRIZZLE_ERROR::WARN_LEVEL_WARN, 0,
        "Failed to retrieve the remote address");
      goto out;
    }
    ip_str_remote= drizzle_auth_sasl_sockaddr_to_str(&ss);
    if (!ip_str_remote)
      goto out;
  }

  if (SASL_OK != sasl_server_new(
      "drizzle", NULL, NULL, ip_str_local, ip_str_remote,
      callbacks, 0, &conn)) {
    push_warning_printf(session,
      DRIZZLE_ERROR::WARN_LEVEL_WARN, 0,
      "[auth_sasl] failed to create a SASL instance.");
    goto out;
  }

  int err = sasl_checkpass(conn,
      session->main_security_ctx.user, strlen(session->main_security_ctx.user),
      password, strlen(password));
  switch (err)
  {
  case SASL_OK:
    retval = true;
    break;
  case SASL_NOVERIFY:
  case SASL_NOUSER:
    push_warning_printf(session,
      DRIZZLE_ERROR::WARN_LEVEL_NOTE, 0,
      "[auth_sasl] authentication failure: %s",
      session->main_security_ctx.user);
    break;
  default:
    push_warning_printf(session,
      DRIZZLE_ERROR::WARN_LEVEL_NOTE, 0,
      "[auth_sasl] unexpected error occurred during authentication (%d).", err);
    break;
  }

out:
  if (conn)
  {
    sasl_dispose(&conn);
    conn= NULL;
  }
  free(ip_str_local);
  free(ip_str_remote);
  return retval;
}

static int drizzle_auth_sasl_initialize(void *p)
{
  static sasl_callback_t callbacks[]=
  {
    { SASL_CB_LIST_END, 0, 0 }
  };
  authentication_st *auth= (authentication_st *)p;

  if (sasl_server_init(callbacks, "drizzle") != SASL_OK)
    return 1;
  auth->authenticate= drizzle_auth_sasl_authenticate;

  return 0;
}

static int drizzle_auth_sasl_finalize(void *p)
{
  (void)p;
  sasl_done();
  return 0;
}

mysql_declare_plugin(auth_sasl)
{
  DRIZZLE_AUTH_PLUGIN,
  "sasl",
  "0.1",
  "Moriyoshi Koizumi",
  "SASL based authenication.",
  PLUGIN_LICENSE_GPL,
  drizzle_auth_sasl_initialize, /* Plugin Init */
  drizzle_auth_sasl_finalize, /* Plugin Deinit */
  NULL,   /* status variables */
  NULL,   /* system variables */
  NULL    /* config options */
}
mysql_declare_plugin_end;
4. ソースツリーの最上位にあるautom4te.cacheを削除してautotools cacheをクリアする。/ 5. config/autorun.shを実行 / 6. configure && make && make install

ここは説明不要だと思うので割愛。

SASLプラグインのテスト

1. /usr/lib/sasl2/drizzle.confを作成

中身はこんな感じ:

sasl_pwcheck_method: auxprop
sasl_mech_list: plain
sasl_auxprop_plugin: sasldb
sasldb_path: /Users/moriyoshi/opt/drizzle/etc/sasldb2

sasl_mech_listに指定されている認証メカニズムは利用しないのでダミーだけど一応。

2. sasldb2の生成

saslpasswd2を使う。

% sasl2passwd -a drizzle -f /Users/moriyoshi/opt/drizzle/etc/sasldb2 -c hoge
Password: hoge
Again (for verification): hoge
3. drizzledの起動

--plugin_loadをつけて起動する。

% ${prefix}/sbin/drizzled --plugin_load=libauth_sasl.so --basedir=${prefix} --datadir=${prefix}/var  

*1:要はinit-repoして作ったレポジトリディレクトリ以下にbranchしたツリーは、同じファイルとかメタデータが共有されるのでディスク領域も削減できてスピードも向上して便利だよ、って話。