FtpServerの動作をカスタマイズするFtplet
前回紹介したApache FtpServer(以下、FtpServer)の特徴的な機能のひとつに、FTPサーバの動作を独自に拡張する「Ftplet」と呼ばれる仕組みがある。FtpletはFTPクライアントからの要求によって発生するイベントを処理するための軽量オブジェクトで、FtpServer自体がFtpletのためのコンテナとして動作するようになっている。したがって開発者は独自のFtpletを作成しデプロイすることによって、FtpServerに任意の機能を追加することができる。
独自のFtpletはorg.apache.ftpserver.ftplet.Ftpletインタフェースを実装することによって作成できる。Ftpletには初期化のためのinit()メソッドとサービス終了のためのdestroy()メソッドのほかに、各FTPイベントを処理するためのコールバックメソッドを実装する。Ftpletインタフェースには次の4種類のコールバックメソッドが定義されている。
- onConnect - クライアントからの接続が確立した際に呼び出される
- onDisconnect - クライアントからの接続が切断された際に呼び出される
- beforeCommand - クライアントから要求されたコマンドが実行される前に呼び出される
- afterCommand - クライアントから要求されたコマンドが実行された後に呼び出される
またこのFtpletインタフェースを実装したクラスとしてDefaultFtpletクラスがあり、上記に加えてさまざまなイベントに対応したコールバックメソッドの空の実装が用意されている。たとえばログイン時に呼び出されるonLogin()や、ファイルダウンロードの前後に呼び出されるonDeleteStart()/onDeleteEnd()などといったメソッドがある。
これらのコールバックメソッドはFtpSessionオブジェクトやFtpRequestオブジェクトなどを受け取り、そこからセッションやクライアントの情報を取り出すことができる。また処理の実行結果としてFtpletResultオブジェクトを返す。FtpletResultオブジェクトは次の4つのいずれかの値であり、FtpletコンテナはFtpletから返された値に応じて次の動作を決める。
- DEFAULT - Ftpletコンテナは次に処理すべきメソッドがあればそれを呼び出す。なければクライアントからの要求を実行する
- NO_FTPLET - Ftpletコンテナは以降の全てのFtpletの処理を行わずにクライアントからの要求を実行する
- SKIP - Ftpletコンテナはクライアントからの要求をスキップする
- DISCONNECT - Ftpletコンテナはクライアントからの要求をスキップし、クライアントとの接続を切断する
次に示すMyFtpletクラスは、DefaultFtpletを継承して作成したFtpletである。この例では、onConnect()メソッドとonLogin()メソッドをオーバーライドすることによって、接続時およびログイン時に接続元の情報を表示するようにした。これらの情報はFtpSessionオブジェクトから取得することができる。また、beforeCommand()メソッドをオーバーライドして要求されたコマンドを表示している。コマンドの情報はFtpRequestオブジェクトから取得できる。
リスト1
package jp.co.mycom.toolde;
import org.apache.ftpserver.ftplet.DefaultFtplet;
import org.apache.ftpserver.ftplet.FtpRequest;
import org.apache.ftpserver.ftplet.FtpSession;
import org.apache.ftpserver.ftplet.FtpletResult;
public class MyFtplet extends DefaultFtplet {
@Override
public FtpletResult onConnect(FtpSession session) {
System.out.println("Connected from " + session.getClientAddress());
return FtpletResult.DEFAULT;
}
@Override
public FtpletResult onLogin(FtpSession session, FtpRequest request) {
System.out.println("Logged in: ");
System.out.println(" User : " + session.getUser().getName());
System.out.println(" Login time : " + session.getLoginTime());
return FtpletResult.DEFAULT;
}
@Override
public FtpletResult beforeCommand(FtpSession session, FtpRequest request) {
System.out.println("Requested command: ");
System.out.println(" Command : " + request.getCommand());
System.out.println(" Argument : " + request.getArgument());
System.out.println(" Command String : " + request.getRequestLine());
return FtpletResult.DEFAULT;
}
}
作成したFtpletをFtpServerにセットするには、ServerFactoryクラスのsetFtplets()メソッドを使用する。次のコードは、MyFtpletをセットして起動する例である。「Ftpletをセット」の部分以外は、前回示したコード例と同様のものだ。まずMapオブジェクトにMyFtpletオブジェクトを任意のキーと共に格納し、そのMapをSeverFactoryにセットすればよい。
リスト2
package jp.co.mycom.toolde;
import java.io.File;
import java.util.Map;
import java.util.HashMap;
import org.apache.ftpserver.FtpServer;
import org.apache.ftpserver.FtpServerFactory;
import org.apache.ftpserver.ftplet.Ftplet;
import org.apache.ftpserver.ftplet.FtpException;
import org.apache.ftpserver.ftplet.UserManager;
import org.apache.ftpserver.listener.ListenerFactory;
import org.apache.ftpserver.usermanager.PropertiesUserManagerFactory;
import org.apache.ftpserver.usermanager.SaltedPasswordEncryptor;
import org.apache.ftpserver.usermanager.impl.BaseUser;
public class FtpletSample {
public FtpletSample() {
try {
FtpServerFactory serverFactory = new FtpServerFactory();
ListenerFactory listenerFactory = new ListenerFactory();
// ポートの設定
listenerFactory.setPort(2221);
// リスナの設定
serverFactory.addListener("default", listenerFactory.createListener());
// ユーザ認証設定
PropertiesUserManagerFactory userManagerFactory = new PropertiesUserManagerFactory();
userManagerFactory.setPasswordEncryptor(new SaltedPasswordEncryptor());
UserManager userManager = userManagerFactory.createUserManager();
BaseUser user = new BaseUser();
user.setName("ユーザ名");
user.setPassword("パスワード");
user.setHomeDirectory("ホームディレクトリへのパス");
userManager.save(user);
serverFactory.setUserManager(userManager);
// Ftpletをセット
Map ftpletMap = new HashMap();
ftpletMap.put("MyFtplet", new MyFtplet());
serverFactory.setFtplets(ftpletMap);
// FTPサーバをスタート
FtpServer server = serverFactory.createServer();
server.start();
} catch (FtpException ex) {
ex.printStackTrace();
}
}
public static void main(String[] args) {
FtpletSample doit = new FtpletSample();
}
}
このFtpletSample.javaをコンパイルして実行し、任意のFTPクライアントからアクセス/ログインすると、サーバ側のコンソールには次のように表示され、Ftpletが動作していることが確認できるはずだ。
プロンプト1
Connected from /127.0.0.1:2245
Requested command:
Command : USER
Argument : [ユーザ名]
Command String : USER [ユーザ名]
Requested command:
Command : PASS
Argument : [パスワード]
Command String : PASS [パスワード]
Logged in:
User : [ユーザ名]
Login time : Wed May 13 15:48:12 JST 2009
Requested command:
Command : PWD
Argument : null
Command String : XPWD
スタンドアロンで起動するFtpServerにFtpletを組み込むには
上記の例は、FtpServerをJavaプログラム内から実行する場合にFtpletを組み込むケースのものである。ではスタンドアロンで起動するFtpServerに任意のFtpletを組み込むにはどうしたらいいだろうか。それには、使用するFtpletに関する設定をXMLの設定ファイルに追加すればよい。
設定ファイルのサンプルは[FTP_HOME]\res\confフォルダに納められている。特にftpd-full.xmlにはFtpletの設定例も掲載されているので参考にして欲しい。MyFtpletを使用したい場合には、次のような設定を追加すればよい。
リスト3
<ftplets>
<ftplet name="MyFtplet">
<beans:bean class="jp.co.mycom.toolde.MyFtplet">
</beans:bean>
</ftplet>
</ftplets>
そして、次のいずれかの方法でFtpServerがMyFtpletのクラスファイルにアクセスできるようにしておく。
- 環境変数CLASSPATHにMyFtplet.classまたはそれを含むJARファイルを追加する
- [FTP_HOME]\common\classesフォルダにMyFtplet.classを(パッケージの階層構造ごと)配置する
- [FTP_HOME]\common\libフォルダにMyFtplet.classを含むJARファイルを配置する
この状態で設定ファイルを指定してbin\ftpd.batを実行すれば、MyFtpletが組み込まれたFtpServerが起動する。例えばカレントディレクトリが[FTPHOME]、設定ファイルを[FTPHOME]\res\conf\ftpd-mine.xmlとしたとすると、次のコマンドを実行すればよい。
プロンプト2
> bin\ftpd.bat res/conf/ftpd-mine.xml
FtpServerはそれ単体でも十分な機能を持ったFTPサーバだが、Ftpletを活用することでそれぞれのニーズに応じたカスタマイズが可能となる。Ftpletの基本的な仕組みは非常にシンプルなので使用方法も難しくはない。現状ではまたFtplet関連のドキュメントがあまり充実していないが、それを差し置いても興味深いツールと言えるだろう。