libsmbclient を使って SMB Storage にアクセス

libsmbclient を使ってみる

Samba ソースツリーには libsmbclient という SMB File System にアクセスするためのライブラリが付属しています((たいがいの Linux Distribution ではライブラリは別パッケージになっています。ライブラリ本体のパッケージ名称はたいてい libsmbclient で,開発用ライブラリのパッケージ名は Ubuntu の場合 libsmbclient-devRedHat 系の場合 libsmbclient-devel です))。

使い方に関するドキュメントがほとんど存在しないのですが,libsmbclient - 揮発性のメモ2にある通り doxygen 形式コメントや Samba に付属の example/libsmbclient 以下が参考になります。

一番わかりやすい http://www.google.com/codesearch?hl=ja&q=show:NHC8hpwkhdI:qSUhgzDmDtI:AJ4gI8Rq5ZQ&sa=N&ct=rd&cs_p=http://us1.samba.org/samba/ftp/samba-latest.tar.gz&cs_f=samba-3.0.25b/examples/libsmbclient/testread.c&start=1*1 をベースにちょっと書き換えてみました。

#include <stdio.h>
#include <string.h>             /* for strncpy() */
#include <errno.h>              /* for errno */
#include <sys/types.h>          /* for constants (O_RDONLY) */
#include <libsmbclient.h>

static void
get_auth_data_fn(const char *pServer, const char *pShare,
                 char *pWorkgroup, int maxLenWorkgroup,
                 char *pUsername, int maxLenUsername,
                 char *pPassword, int maxLenPassword)
{
    fprintf(stderr, "**** auth_fn\n");

    fprintf(stderr, "Workgroup: [%s]\n", pWorkgroup);
    fprintf(stderr, "Username: [%s]\n",  pUsername);

    strncpy(pPassword, "PASSWORD", maxLenPassword - 1);
}

int
main(int argc, char *argv[])
{
    const char *path = argv[1];     /* "smb://SERVER/PATH/FILE"; */
    int debug_level = 0;
    static char buffer[1024];

    int fd, ret, last_error;
    FILE *fp;

    fp = fopen("output", "w");

    smbc_init(get_auth_data_fn, debug_level);

    fd = smbc_open(path, O_RDONLY, 0);
    if (fd < 0) {
        perror("smbc_open");
        return 1;
    }

    do {
        ret = smbc_read(fd, buffer, sizeof(buffer));
        last_error = errno;

        fprintf(stderr, "**** smbc_read: result(%d)\n", ret);

        if (ret > 0) {
            fwrite(buffer, 1, ret, fp);
            fwrite(buffer, 1, ret, stdout);
        }
    } while (ret > 0);

    fclose(fp);

    if (ret < 0) {
        errno = last_error;
        perror("smbc_read");
        return 1;
    }

    smbc_close(fd);

    return 0;
}

ざっと見てわかるのは,*nix の低レベル I/O レイヤ / Socket レイヤと同じようなテイストの関数群が用意されており,smbc_open() して取得したハンドルを元に smbc_read()smbc_close() すればよいことがわかります。また,事前に認証情報を取得する callback 関数を smbc_init() で登録しておくようです。今回は固定パスワードを仕込んでいますが,ここでユーザに入力させることもできます。

簡潔なインタフェースですね。

ビルドして実行してみます。

$ gcc -o testread -lsmbclient testread.c

$ ./testread smb://SERVER/PATH/FILE

**** auth_fn
Workgroup: [MYGROUP]
Username: [dayflower]
**** smbc_read: result(207)

...... blah blah blah ......

**** smbc_read: result(0)

Workgroup 名や User 名は,smb.conf やログイン情報をもとにデフォルトで自動的に設定されているようです。smbc_open() 時に get_auth_data_fn() が callback され,無事ファイルの内容を取得できました。

より新しいインタフェースを使用してみる

上記の低レベル I/O 互換関数群は libsmb_compat.c で定義された legacy なエミュレーションレイヤです*2。より本体に近い関数群があります。こちらは SMB アクセス用コンテキスト構造体を持ち回してアクセスを行います。またこのコンテキスト構造体にアクセスのための関数ポインタが含まれています。

なんだか難しそう?

実は単純に,

class SMBCCONTEXT {
    void init(void);
    void free(int shutdown_ctx);
    SMBCFILE *open(const char *path, int flags, mode_t mode);
    int close(SMBCFILE *file, void);
    int read(SMBCFILE *file, void *buf, size_t bufsize);
}

のようなインタフェースを C の上で実現しているだけのことです。

実際に先ほどの例を書き直してみます(デバッグ用にファイルに書き出す機能は削除しました)。

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <libsmbclient.h>

#define False (0)       /* defined in smb.h */

static void
get_auth_data_fn(const char *pServer, const char *pShare,
                 char *pWorkgroup, int maxLenWorkgroup,
                 char *pUsername, int maxLenUsername,
                 char *pPassword, int maxLenPassword)
{
    fprintf(stderr, "**** auth_fn\n");

    fprintf(stderr, "Workgroup: [%s]\n", pWorkgroup);
    fprintf(stderr, "Username: [%s]\n",  pUsername);

    strncpy(pPassword, "0sakana3", maxLenPassword - 1);
}

int
main(int argc, char *argv[])
{
    const char *path = argv[1];     /* "smb://SERVER/PATH/FILE"; */
    int debug_level = 0;
    static char buffer[1024];

    SMBCCTX  *smbcctx;
    SMBCFILE *sfp;
    int ret, last_error;

    /* equivalent to smbc_init() */
    smbcctx = smbc_new_context();
    if (! smbcctx) {
        perror("smbc_new_context");
        return 1;
    }

    smbcctx->debug = debug_level;
    smbcctx->callbacks.auth_fn
        = get_auth_data_fn;

    if (! smbc_init_context(smbcctx)) {
        perror("smbc_init_context");
        smbc_free_context(smbcctx, False);
        return 1;
    }

    /* equivalent to smbc_open() */
    sfp = (smbcctx->open)(smbcctx, path, O_RDONLY, 0);
    if (! sfp) {
        perror("SMBCCTX::open");
        smbc_free_context(smbcctx, False);
        return 1;
    }

    do {
        /* equivalent to smbc_read() */
        ret = (smbcctx->read)(smbcctx, sfp, buffer, sizeof(buffer));
        last_error = errno;

        fprintf(stderr, "**** SMBCCTX::read: result(%d)\n", ret);

        if (ret > 0) {
            fwrite(buffer, 1, ret, stdout);
        }
    } while (ret > 0);

    if (ret < 0) {
        perror("SMBCCTX::read");
    }

    /* equivalent to smbc_close() */
    (smbcctx->close_fn)(smbcctx, sfp);

    smbc_free_context(smbcctx, False);

    return 0;
}

なんのことはない FILE * 構造体を利用した高レベル I/O アクセスや古めでない Win32 API の雰囲気に似ていますね(関数ポインタを利用しているところがちと違いますが)。


実際には先ほど述べたように libsmb_compat.c で互換 API が実装されており,そこでは上記のコードとほぼ同じことをやっているのでそんなに冗長なコードではないようです。つまり,単純に使いたいなら互換 API を使っていても問題はなさそうです。

以下余談が続きます。

余談: gvfsd-smb on Ubuntu Hardy

なんでこんなことを調べる羽目になったかというと,最近入れた Ubuntu Hardy から nautlius で Samba share にアクセスすると,すごい待たされた挙句 timeout した,といわれるようになってしまったからです。ゲストアクセス OK なストレージでは問題ないのですが,ADS 認証のストレージではめったにアクセスできなくなってしまいました。

以前はこのへんの仕組みは gnome-vfs という仕組みの上になりたっていましたが,Ubuntu Hardy の採用している Gnome 2.22 では gvfs という新しい仕組みを利用しています。で,gvfs の評判がいまいちよろしくない。

気が進まないながら gvfsd-smb まわりのコードをのぞくことにしました。するとこいつはどうやら libsmbclient を利用しているらしい。とりあえず libsmbclient を直接触るコードを書いてみると原因の切り分けができるんじゃね?と思って,上記のようなコードがでっちあげられたわけです。

で,実際に debug level をあげて実行してみると……

$ ./testread smb://SERVER/PATH/FILE

lp_load: refreshing parameters
Initialising global parameters
params.c:OpenConfFile()
 - Unable to open configuration file "/home/dayflower/.smb/smb.conf":
        No such file or directory
pm_process() returned No
lp_servicenumber: couldn't find homes
Attempting to register new charset UCS-2LE
Registered charset UCS-2LE

...... snip snip snip ......

Could not load config file: /home/dayflower/.smb/smb.conf
lp_load: refreshing parameters
params.c:pm_process() - Processing configuration file "/etc/samba/smb.conf"
Processing section "[global]"
doing parameter dos charset = CP932
doing parameter workgroup = MYGROUP
doing parameter realm = MYGROUP.EXAMPLE.COM

...... snip snip snip ......

pm_process() returned Yes
lp_servicenumber: couldn't find homes
lp_load: refreshing parameters
params.c:OpenConfFile()
 - Unable to open configuration file "/home/dayflower/.smb/smb.conf.append":
	No such file or directory
pm_process() returned No
lp_servicenumber: couldn't find homes

...... snip snip snip ......

added interface ip=192.168.0.140 bcast=192.168.0.255 nmask=255.255.255.0
Using netbios name CLIENT.
Using workgroup MYGROUP.
**** auth_fn
Workgroup: [MYGROUP]
Username: [dayflower]
smbc_server: server_n=[SERVER] server=[SERVER]
 -> server_n=[SERVER] server=[SERVER]
Opening cache file at /var/run/samba/gencache.tdb
tdb(unnamed): tdb_open_ex:
 could not open file /var/run/samba/gencache.tdb: Permission denied
gencache_init: Opening cache file /var/run/samba/gencache.tdb read-only.
sitename_fetch:
 Returning sitename for MYGROUP.EXAMPLE.COM: "Default-First-Site-Name"
no entry for SERVER#20 found.
resolve_lmhosts: Attempting lmhosts lookup for name SERVER<0x20>
startlmhosts: Can't open lmhosts file /etc/samba/lmhosts.
 Error was No such file or directory
resolve_wins: Attempting wins lookup for name SERVER<0x20>
resolve_wins: WINS server resolution selected and no WINS servers listed.
resolve_hosts: Attempting host lookup for name SERVER<0x20>
**** smbc_read: result(207)
**** smbc_read: result(-1)
smbc_read: Connection timed out

名前解決に NMB broadcast via nss-wins を使っているのはご愛嬌。

このように2つ目の smbc_read() ブロックで timeout が発生しているわけでした。残念ながら2つの smbc_read() の間にログが挟まっていないということは,libsmbclient より下のレイヤでなにかおかしなことが起きているようです。

つまり gvfsd-smb がおかしいのではなく,libsmbclient,あるいは smb.conf がおかしいようです。疑ってごめんなさい> gvfs

余談の追補

ログとソースをにらめっこしてたら,どうも初回の smbc_read_context() すら完了してなさげなこと発見。実際には static smbc_server() の中の name query でタイムアウトしてるげ。んでもっとたぐって発見した解決方法は,

[global]
name resolve order = wins bcast

のように name resolve orderbcast を含めておくことで OK でした。たしかに smb.conf の man を読むと

When Samba is functioning in ADS security mode (security = ads) it is advised to use following settings for name resolve order:

        name resolve order = wins bcast

って書いてあるし。実際には私的環境では WINS はいないので bcast だけにしてあります。

default で name resolve order = lmhosts host wins bcast って書いてあるんだけど,これ間違ってるのかな。あるいは security = ads のときだけ変わるとか。ちょっと追いきれていないです。

ちなみにドキュメントに書かれていないけど,name resolve order には kdc ないし ads というのもある模様。でも私の環境では ads はうまくいきませんでした。

2008/05/13 追記: やっぱ bcast だけにすると nss_winbind な環境で logon server がみつかんないようになってしまったので設定を外しました。なんにせよ遅かったり timeout したりみつかんなかったりするのは名前解決まわりっぽい。暫定的に lmhosts を設定すると解決するかも。

*1:本来 http://gitweb.samba.org/ のコードを引用してあれこれするべきですが,Google Code のインタフェースが軽くて使いやすかったのでちょっと古いコードですが Google Code 上のコードを利用しました。

*2:Samba-2.2 以降で実装されたみたいです