JavaでMock FTPサーバー作ってみた

こんにちは。都築です。
 
先日、JavaでFTPサーバーとファイルを送受信する機能を作成しました。その際にMockFtpServerを利用し、仮想的にFTPサーバーを作りました。
 
ローカル環境でのテストなどで必要になる場面もあるかと思いますので、ぜひご参考にしていただければと思います。

環境

今回ご紹介する方法は下記環境で動作しています。

OS
Windows 8.1
Java
Java8
MockFtpServer
2.5
Win SCP
5.16.5

準備

まずはMockFtpServerを利用するために下記サイトからjarをダウンロードします。
http://sourceforge.net/projects/mockftpserver/files/mockftpserver
 
ダウンロードして展開すると、MockFtpServer-2.5.jarがありますので、Javaで使用するためにプロジェクトのビルドパスを通します。

メインクラスの作成

ビルドパスを通したら、まずはメインとなるStubFtpServerクラスを実装します。MockFtpServerでは基本的にStubFtpServerクラスのインスタンスにFTPの各コマンドに対応したハンドラーを設定することにより、FTPサーバーの機能を仮想的に実現しています。
 
例えば、下記の様にFTPコマンドのUSER、PASS、LIST、LIST、RETRコマンドに対応したハンドラーをStubFtpServerに設定して開始します。

import org.mockftpserver.core.command.CommandNames;
import org.mockftpserver.stub.StubFtpServer;

public class FtpMockMain {

	public static void main(String[] args) {
		StubFtpServer stubFtpServer = new StubFtpServer();
		stubFtpServer.setServerControlPort(21);

		stubFtpServer.setCommandHandler(CommandNames.USER, new OriginalUserCommandHandler());
		stubFtpServer.setCommandHandler(CommandNames.PASS, new OriginalPassCommandHandler());
		stubFtpServer.setCommandHandler(CommandNames.PWD, new OriginalPwdCommandHandler());
		stubFtpServer.setCommandHandler(CommandNames.LIST, new OriginalListCommandHandler());
		stubFtpServer.setCommandHandler(CommandNames.RETR, new OriginalFileRetrCommandHandler());

		stubFtpServer.start();
	}
}

 
このメイン処理を実行することで、各FTPコマンドが実行された際に対応するクラスに実装された挙動が実行されます。

各ハンドラーの作成

次にStubFtpServerに設定した各コマンドに対応したハンドラークラスを実装していきます。FTPコマンドとMockFtpServerの継承クラスのマッピングは下記サイトに掲載されていますので、各コマンドに対応したクラスを継承したクラスを実装していきます。
http://mockftpserver.sourceforge.net/stubftpserver-commandhandlers.html
 
今回は上記に記載した通り、ログインからファイル取得までに必要なクラスを実装していますが、ファイル送信など他のコマンドに対応した挙動が必要な場合は適宜クラスを作成していきます。

USERハンドラーの作成

まずFTPサーバーにログインするために、USERコマンドに対応したクラスを作成します。
 
USERコマンドはUserCommandHandlerを継承したクラスを作成します。ここではユーザー名をセッションに保持するだけの処理としています。

import org.mockftpserver.core.command.Command;
import org.mockftpserver.core.command.InvocationRecord;
import org.mockftpserver.core.command.ReplyCodes;
import org.mockftpserver.core.session.Session;
import org.mockftpserver.core.session.SessionKeys;
import org.mockftpserver.stub.command.UserCommandHandler;

public class OriginalUserCommandHandler extends UserCommandHandler {

    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {
        
        // ユーザー名をセッションに保持
        String username = command.getRequiredParameter(0);
        session.setAttribute(SessionKeys.USERNAME, username);
        
        if (replyCode == 0) {
            sendReply(session, ReplyCodes.USER_NEED_PASSWORD_OK, replyMessageKey, replyText, null);
        } else {
            sendReply(session);
        }
    }
}

PASSハンドラーの作成

次にログイン時にパスワードをチェックするPASSコマンドに対応したクラスを作成します。PASSコマンドはPassCommandHandlerを継承したクラスを作成します。
 
ここでは先ほどのOriginalUserCommandHandlerで保持したユーザー名と、入力されたパスワードがそれぞれ「admin」、「pass」と一致するかチェックするようにしています。

import org.mockftpserver.core.command.Command;
import org.mockftpserver.core.command.InvocationRecord;
import org.mockftpserver.core.command.ReplyCodes;
import org.mockftpserver.core.session.Session;
import org.mockftpserver.core.session.SessionKeys;
import org.mockftpserver.stub.command.PassCommandHandler;

public class OriginalPassCommandHandler extends PassCommandHandler {
    
    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {
        
        // 入力されたパスワードを保持
        String password = command.getRequiredParameter(0);
        // セッションからユーザー名を取得
        String username = (String)session.getAttribute(SessionKeys.USERNAME);
        
        // ユーザー名とパスワードが一致するかチェック
        if ("admin".equals(username) && "pass".equals(password)) {
            replyCode = ReplyCodes.PASS_OK;
            sendReply(session);
            return;
        }
        
        // ログイン失敗
        replyCode = ReplyCodes.PASS_LOG_IN_FAILED;
        sendReply(session);
    }
}

PWDハンドラーの作成

現在の作業ディレクトリを表示するPWDコマンドに対応したクラスを作成します。PWDコマンドはPwdCommandHandlerを継承したクラスを作成します。
 
ここでは現在ディレクトリが”/”として表示されるよう設定しています。

import org.mockftpserver.core.command.Command;
import org.mockftpserver.core.command.InvocationRecord;
import org.mockftpserver.core.session.Session;
import org.mockftpserver.stub.command.PwdCommandHandler;

public class OriginalPwdCommandHandler extends PwdCommandHandler {
    
    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {
        setDirectory("/");
        super.handleCommand(command, session, invocationRecord);
    }
}

LISTハンドラーの作成

ファイルの一覧を表示するLISTコマンドに対応したクラスを作成します。LISTコマンドはListCommandHandlerを継承したクラスを作成します。
 
ディレクトリおよびファイルとして表示するためにはそれぞれ下記のように文字列をDirectoryに設定します。

ディレクトリ 日時 + 半角スペース + <DIR> + 半角スペース + ディレクトリ名
ファイル 日時 + 半角スペース + ファイルサイズ + 半角スペース + ファイル名

ここではsampleというディレクトリとsample.txtというファイルが現在のディレクトリに存在するものとして表示されるようにしています。

import org.mockftpserver.core.command.Command;
import org.mockftpserver.core.command.InvocationRecord;
import org.mockftpserver.core.session.Session;
import org.mockftpserver.stub.command.ListCommandHandler;
public class OriginalListCommandHandler extends ListCommandHandler {
    
    protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {
        super.beforeProcessData(command, session, invocationRecord);
        
        StringBuilder sb = new StringBuilder();
        sb.append("2020-02-01 09:00PM");
        sb.append(" ");
        sb.append("<DIR>");
        sb.append(" ");
        sb.append("sample");
        sb.append("\r\n");
        
        sb.append("2020-02-01 10:00PM");
        sb.append(" ");
        sb.append("29000");
        sb.append(" ");
        sb.append("sample.txt");
        sb.append("\r\n");
        
        setDirectoryListing(sb.toString());
    }
}

実行

ここまででFTPサーバーにログインし、”/”ディレクトリの下にsampleというディレクトリとsample.txtというファイルを表示する仮想FTPサーバーが実装できました。
FTPMockMainクラスを実行し、WinSCPというFTP/SFTP/SCPクライアントソフトでlocalhostに接続してみます。
 
接続先としてlocalhost:21、ユーザー:admin、パスワード:passでログインします。
 

 
想定通りログインでき、”/”ディレクトリにsampleディレクトリとsample.txtファイルが存在するものとして表示されました。
 

RETRハンドラーの作成

最後にファイルを取得するRETRコマンドに対応したクラスを作成します。RETRコマンドはFileRetrCommandHandlerを継承したクラスを作成します。
 
今回はサンプルとして下記のように「C:/work/sample.txt」のファイルをセッションに設定しています。

import java.io.FileInputStream;
import java.io.InputStream;
import org.mockftpserver.core.command.Command;
import org.mockftpserver.core.command.InvocationRecord;
import org.mockftpserver.core.session.Session;
import org.mockftpserver.stub.command.FileRetrCommandHandler;

public class OriginalFileRetrCommandHandler extends FileRetrCommandHandler {
	protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {
		setFile("temp");
		super.beforeProcessData(command, session, invocationRecord);
	}

	protected void processData(Command command, Session session, InvocationRecord invocationRecord) {
		InputStream inputStream = null;
		try {
			inputStream = new FileInputStream("C:/work/sample.txt");
			byte[] buffer = new byte[512];
			int bytes;
			while ((bytes = inputStream.read(buffer)) != -1) {
				session.sendData(buffer, bytes);
			}
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}

 
WinSCPでsample.txをローカルにダウンロードすると、C:/work/sample.txtのファイルがダウンロードできました。これで仮想的にFTPサーバーが実装できました。
 

まとめ

MockFtpServerは求める挙動に応じて適宜処理を追加していく必要がありますが、特殊なツールなども不要で比較的容易にFTPサーバーが作成できました。
 
最近では色々なサービスがあり、様々な方法で仮想環境を作成することが出来るかと思います。状況や環境に応じてどのようなサービスを利用するか、どのようにテストを行なうかを検討し、より良いシステム開発をおこなっていければと思います。

記事をシェア
MOST VIEWED ARTICLES