Engineering Note

プログラミングなどの技術的なメモ

FTPクライアントプログラムの作成②(Javaによるネットワークプログラミング)

ftp

本記事は、Javaによるネットワークプログラミングについての学習メモとなります。

その中でNIOフレームワークであるNettyを使った実装方法について学んでいきます。

今回は、前回作成したFTPクライアントプログラムにデータ転送用コネクションの機能としてlsコマンドを追加してみます。

 

 

FTPについて

FTPはFile Transfer Protocol(ファイル・トランスファープロトコル )の略で、その名の通りファイル転送を行うためのプロトコルになります。

その歴史は古く、最初に登場したのは1971年4月でRFC114としてまとめられています。

 

 

FTPではTCPポート20番と21番を使いデータ転送を行っていきます。

TCPポート20番はデータ転送用コネクションとして使用し、TCPポート21番は制御用コネクションとして使用します。

前回では制御用コネクション機能のみを実装し、pwdコマンドやcdコマンドなどを実装してみました。

 

 

今回はデータ転送用コネクションを実装し、lsコマンドを入力した際のディレクトリ一覧を取得してみます。

 

アクティブモードとパッシブモード

データ転送用コネクションを作成する際にアクティブモードパッシブモードというものがあります。

アクティブモードでは、FTPクライアント側でサーバソケットを作成し、FTPサーバ側にPORTコマンドを用いて、以下のように接続先のIPアドレス及びポート番号を通知します。

 

PORT 192,168,1,1,4,30

 

上記の先頭から4つのカンマ区切りの数字はIPアドレス:192.168.1.1を表し、FTPクライアント側のIPアドレスとなっています。

最後の2つの数字はポート番号を計算するために使い、ここでは「4 × 256 + 30  = 1054」となり、ポート番号1054で接続待ちをします。

パッシブモードでは、FTPクライアント側からPASV(EPSV)コマンドを用いてFTPサーバ側で新たにサーバソケットを作成し、接続先のポート番号を通知します。

CentOS7でのftpコマンドをインストールした際に、lsコマンドを実行すると自動的にEPSV(拡張パッシブモード)コマンドをFTPサーバ側に送付し、以下のようなレスポンスが返ってきます。

 

229 Entering Extended Passive Mode (|||8236|).

 

上記のパイプで囲まれた数字(8236)がFTPサーバ側で生成したデータ転送用コネクションのポート番号となり、FTPクライアントはこちらに接続しデータを受信することができます。

なお、アクティブモードではFTPクライアント側でサーバソケットを作成しなければならないことからポートを開放する必要があるのとNATの問題が生じてきます。

これはセキュリティ上でも問題があるため、パッシブモードで接続しに行きます。

また、パッシブモードではFTPサーバ側で新たにサーバソケットを作成しますが、こちらのポートがランダムに生成されるため、その分ファイアウォールの穴が大きくなってしまいます。

 

Nettyについて

Javaには元々java.ioというパッケージが存在しますが、ブロッキングIOやストリーム指向であることから、Java 1.4からjava.nioというパッケージが導入されました。

このNIO(New IO)では、ノンブロッキングIOとバッファ指向で設計され、さらにセレクタを利用し単一スレッドで複数の入力チャネルをモニタ出来るようになり、様々な改良が加えられました。

今回はこのNIOのフレームワークであるNettyを利用していきます。

以下が公式ドキュメントになります。

 

 

FTPクライアントプログラムの作成

それではFTPクライアントプログラムにlsコマンドを追加していきます。

 

# SimpleTCPChannelHandler.java
package ftpclient;


import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class SimpleTCPChannelHandler extends SimpleChannelInboundHandler<String> { 
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        String[] remoteIp = ctx.channel().remoteAddress().toString().split("/");
        System.out.println("[*]connect to " + remoteIp[1]);
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("[*]server connection closed.");
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        int status_code = Integer.parseInt(msg.substring(0, 3));
        switch (status_code) {
            case 220:
                FtpUtil.doLoginUser(ctx.channel());
                break;
            case 221:
                ctx.disconnect();
                break;
            case 331:
                FtpUtil.doLoginPass(ctx.channel());
                break;
            case 530:
                System.out.println("login failed.");
                FtpUtil.command(ctx.channel());
                break;
            case 229:
                int port = Integer.parseInt(FtpUtil.getPortNum(msg));
                String data = FtpUtil.recvData("localhost", port);
                System.out.print(data);
                break;
            case 226:
                break;
            case 250:
            case 230:
            case 150:
                FtpUtil.command(ctx.channel());
                break;
            default:
                System.out.print(msg);
                FtpUtil.command(ctx.channel());
        }
    }
}

 

 

# SimpleTCPClientBootstrap.java
package ftpclient;

import java.net.ConnectException;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class SimpleTCPClientBootstrap {   
    public void start(String host, int port) throws InterruptedException, ConnectException {
        System.out.println("[*]connecting to " + host + ":" + port);
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap clientBootstrap = new Bootstrap();
            clientBootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new SimpleTCPChannelInitializer());
            ChannelFuture cf = clientBootstrap.connect(host, port).sync();
            Channel channel = cf.sync().channel();
            while (channel.isActive()){

            }
            cf.channel().closeFuture().sync();                        
        } finally {
            eventLoopGroup.shutdownGracefully();   
        }        
    }
}

 

 

# SimpleTCPChannelInitializer.java
package ftpclient;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class SimpleTCPChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new StringEncoder());
        socketChannel.pipeline().addLast(new StringDecoder());
        socketChannel.pipeline().addLast(new SimpleTCPChannelHandler());
    }
}

 

 

# FtpUtil.java
package ftpclient;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;


public class FtpUtil {
    private static String user;
    private static String pass;
    private static BufferedReader reader = new BufferedReader(
        new InputStreamReader(System.in));
    public static void doLoginUser(Channel channel) {
        try {
            System.out.print("user: ");
            user = reader.readLine();
            channel.writeAndFlush("USER " + user + "\r\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void doLoginPass(Channel channel) {
        try {
            System.out.print("pass: ");
            pass = reader.readLine();
            channel.writeAndFlush("PASS " + pass + "\r\n");
            channel.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void showMenu() {
        System.out.println("2.command 9.quit");
        System.out.print("ftp> ");
    }
    public static int selectMenu() {
        int select = 9;
        try {
            select = Integer.parseInt(reader.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return select;
    }
    public static void command(Channel channel) {
        String command;
        String[] commandList;
        try {
            System.out.print("ftp> ");
            command = reader.readLine();
            commandList = command.split(" ");
            switch (commandList[0]) {
                case "pwd":
                    channel.writeAndFlush("PWD\r\n");
                    break;
                case "cd":
                    if (commandList.length == 1) {
                        try {
                            System.out.print("remote directory: ");
                            String path = reader.readLine();
                            channel.writeAndFlush("CWD " + path + "\r\n");                        
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    } else {
                        channel.writeAndFlush("CWD " + commandList[1] + "\r\n");
                    }
                    break;
                case "quit":
                    channel.writeAndFlush("QUIT\r\n");
                    break;
                case "delete":
                    channel.writeAndFlush("DELE " + commandList[1] + "\r\n");
                    break;
                case "rmdir":
                    channel.writeAndFlush("RMD " + commandList[1] + "\r\n");
                    break;
                case "user":
                    channel.writeAndFlush("USER " + commandList[1] + "\r\n");
                    break;
                case "ls":
                    channel.writeAndFlush("EPSV 2\r\n");
                    channel.writeAndFlush("LIST\r\n");
                    break;
                case "quote":
                    channel.writeAndFlush("EPSV 2\r\n");
                    break;
                default:
                    System.out.println("invalid command.");
                    FtpUtil.command(channel);
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static String recvData(String host, int port) throws IOException, UnknownHostException {
        Socket dataSocket = new Socket(host, port);
        BufferedReader br = new BufferedReader(new InputStreamReader(dataSocket.getInputStream()));
        StringBuilder sb = new StringBuilder();
        while (br.ready()) {
            sb.append(br.readLine());
            sb.append("\r\n");
        }
        dataSocket.close();
        br.close();
        return sb.toString();
    }
    public static String getPortNum(String msg) {
        Pattern pat = Pattern.compile("\\|(\\d+)\\|");
        Matcher mat = pat.matcher(msg);
        if (mat.find()) {
            return mat.group(1);
        } else {
            return null;
        }
    }
}

 

 

# Main.java
package ftpclient;

import java.net.ConnectException;

public class Main {
    public static void main(String[] args) {
        try {
            SimpleTCPClientBootstrap client = new SimpleTCPClientBootstrap();
            client.start("localhost", 21);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ConnectException e) {
            System.out.println("[*]failed to connect server.");
        }
    }
}

 

動作確認

それでは、上記プログラムを実行し、ローカル環境で起動しているvsftpdに対して接続してみます。

 

 [*]connecting to localhost:21
 [*]connect to 127.0.0.1:21
 user: test
 pass: test
 ftp> ls
 drwxr-xr-x    2 1001     1001           18 Jul 25 12:56 test
 -rw-rw-r--    1 1001     1001            0 Jul 29 12:27 test2
 -rw-rw-r--    1 1001     1001            0 Jul 29 12:27 test3
 ftp> quit
 [*]server connection closed.

 

最後に

今回はデータ転送用コネクションの機能としてlsコマンドについて学びました。

次回はファイル転送機能を作成していきます。

 

参考書籍

基礎からわかるTCP/IP Javaネットワークプログラミング