libsmbclient を使って SMB Storage にアクセス
libsmbclient を使ってみる
Samba ソースツリーには libsmbclient という SMB File System にアクセスするためのライブラリが付属しています((たいがいの Linux Distribution ではライブラリは別パッケージになっています。ライブラリ本体のパッケージ名称はたいてい libsmbclient
で,開発用ライブラリのパッケージ名は Ubuntu の場合 libsmbclient-dev
,RedHat 系の場合 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 order
に bcast
を含めておくことで OK でした。たしかに smb.conf
の man を読むと
When Samba is functioning in ADS security mode (
security = ads
) it is advised to use following settings forname 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 以降で実装されたみたいです