Java tutorial
/** * This file is part of Waarp Project. * * Copyright 2009, Frederic Bregier, and individual contributors by the @author tags. See the * COPYRIGHT.txt in the distribution for a full listing of individual contributors. * * All Waarp Project is free software: you can redistribute it and/or modify it under the terms of * the GNU General Public License as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Waarp . If not, see * <http://www.gnu.org/licenses/>. */ package org.waarp.ftp.core.control; import java.io.IOException; import java.net.ConnectException; import java.nio.channels.ClosedChannelException; import java.util.concurrent.RejectedExecutionException; import io.netty.channel.Channel; import io.netty.channel.ChannelException; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.ssl.SslHandler; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import org.waarp.common.command.ReplyCode; import org.waarp.common.command.exception.CommandAbstractException; import org.waarp.common.command.exception.Reply421Exception; import org.waarp.common.command.exception.Reply503Exception; import org.waarp.common.crypto.ssl.WaarpSslUtility; import org.waarp.common.logging.WaarpLogger; import org.waarp.common.logging.WaarpLoggerFactory; import org.waarp.ftp.core.command.AbstractCommand; import org.waarp.ftp.core.command.FtpCommandCode; import org.waarp.ftp.core.command.access.USER; import org.waarp.ftp.core.command.internal.ConnectionCommand; import org.waarp.ftp.core.command.internal.IncorrectCommand; import org.waarp.ftp.core.control.ftps.FtpsInitializer; import org.waarp.ftp.core.data.FtpTransferControl; import org.waarp.ftp.core.exception.FtpNoConnectionException; import org.waarp.ftp.core.session.FtpSession; import org.waarp.ftp.core.utils.FtpChannelUtils; /** * Main Network Handler (Control part) implementing RFC 959, 775, 2389, 2428, 3659 and supports XCRC * and XMD5 commands. * * @author Frederic Bregier * */ public class NetworkHandler extends SimpleChannelInboundHandler<String> { /** * Internal Logger */ private static final WaarpLogger logger = WaarpLoggerFactory.getLogger(NetworkHandler.class); /** * Business Handler */ private final BusinessHandler businessHandler; /** * Internal store for the SessionInterface */ private final FtpSession session; /** * The associated Channel */ private Channel controlChannel = null; /** * ChannelHandlerContext that could be used whenever needed */ private volatile ChannelHandlerContext ctx; /** * Constructor from session * * @param session */ public NetworkHandler(FtpSession session) { super(); this.session = session; businessHandler = session.getBusinessHandler(); businessHandler.setNetworkHandler(this); } /** * @return the businessHandler */ public BusinessHandler getBusinessHandler() { return businessHandler; } /** * @return the session */ public FtpSession getFtpSession() { return session; } /** * * @return the Control Channel */ public Channel getControlChannel() { return controlChannel; } /** * Run firstly executeChannelClosed. * */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { if (session == null || session.getDataConn() == null || session.getDataConn().getFtpTransferControl() == null) { super.channelInactive(ctx); return; } // Wait for any command running before closing (bad client sometimes // don't wait for answer) int limit = 100; while (session.getDataConn().getFtpTransferControl().isFtpTransferExecuting()) { Thread.sleep(10); limit--; if (limit <= 0) { logger.warn("Waiting for transfer finished but 1s is not enough"); break; // wait at most 1s } } businessHandler.executeChannelClosed(); // release file and other permanent objects businessHandler.clear(); session.clear(); super.channelInactive(ctx); } /** * Initialize the Handler. * */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { this.ctx = ctx; Channel channel = ctx.channel(); controlChannel = channel; session.setControlConnected(); FtpChannelUtils.addCommandChannel(channel, session.getConfiguration()); if (isStillAlive(ctx)) { // Make the first execution ready AbstractCommand command = new ConnectionCommand(getFtpSession()); session.setNextCommand(command); // This command can change the next Command businessHandler.executeChannelConnected(channel); // Answer ready to continue from first command = Connection messageRunAnswer(ctx); getFtpSession().setReady(true); } } /** * If the service is going to shutdown, it sends back a 421 message to the connection * * @return True if the service is alive, else False if the system is going down */ private boolean isStillAlive(ChannelHandlerContext ctx) { if (session.getConfiguration().isShutdown) { session.setExitErrorCode("Service is going down: disconnect"); writeFinalAnswer(ctx); return false; } return true; } /** * Default exception task: close the current connection after calling exceptionLocalCaught and * writing if possible the current replyCode. * */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { this.ctx = ctx; Throwable e1 = cause; Channel channel = ctx.channel(); if (session == null) { // should not be logger.warn("NO SESSION", e1); return; } if (e1 instanceof ConnectException) { ConnectException e2 = (ConnectException) e1; logger.warn("Connection impossible since {} with Channel {}", e2.getMessage(), channel); } else if (e1 instanceof ChannelException) { ChannelException e2 = (ChannelException) e1; logger.warn("Connection (example: timeout) impossible since {} with Channel {}", e2.getMessage(), channel); } else if (e1 instanceof ClosedChannelException) { logger.debug("Connection closed before end"); session.setExitErrorCode("Internal error: disconnect"); if (channel.isActive()) { writeFinalAnswer(ctx); } return; } else if (e1 instanceof CommandAbstractException) { // FTP Exception: not close if not necessary CommandAbstractException e2 = (CommandAbstractException) e1; logger.warn("Command Error Reply {}", e2.getMessage()); session.setReplyCode(e2); businessHandler.afterRunCommandKo(e2); if (channel.isActive()) { writeFinalAnswer(ctx); } return; } else if (e1 instanceof NullPointerException) { NullPointerException e2 = (NullPointerException) e1; logger.warn("Null pointer Exception: " + ctx.channel().toString(), e2); try { if (session != null) { session.setExitErrorCode("Internal error: disconnect"); if (businessHandler != null && session.getDataConn() != null) { businessHandler.exceptionLocalCaught(e1); if (channel.isActive()) { writeFinalAnswer(ctx); } } } } catch (NullPointerException e3) { } return; } else if (e1 instanceof IOException) { IOException e2 = (IOException) e1; logger.warn("Connection aborted since {} with Channel {}", e2.getMessage(), channel); logger.warn(e1); } else if (e1 instanceof RejectedExecutionException) { logger.debug("Rejected execution (shutdown) from {}", channel); return; } else { logger.warn("Unexpected exception from Outband Ref Channel: " + channel.toString() + " Exception: " + e1.getMessage(), e1); } session.setExitErrorCode("Internal error: disconnect"); businessHandler.exceptionLocalCaught(e1); if (channel.isActive()) { writeFinalAnswer(ctx); } } /** * Simply call messageRun with the received message * */ @Override public void channelRead0(ChannelHandlerContext ctx, String e) { this.ctx = ctx; if (isStillAlive(ctx)) { // First wait for the initialization to be fully done if (!session.isReady()) { session.setReplyCode(ReplyCode.REPLY_421_SERVICE_NOT_AVAILABLE_CLOSING_CONTROL_CONNECTION, null); businessHandler.afterRunCommandKo(new Reply421Exception(session.getReplyCode().getMesg())); writeIntermediateAnswer(ctx); return; } String message = e; AbstractCommand command = FtpCommandCode.getFromLine(getFtpSession(), message); logger.debug("RECVMSG: {} CMD: {} " + command.getCode(), message, command.getCommand()); // First check if the command is an ABORT, QUIT or STAT if (!FtpCommandCode.isSpecialCommand(command.getCode())) { // Now check if a transfer is on its way: illegal to have at // same time two commands (except ABORT). Wait is at most 100x // RETRYINMS=1s FtpTransferControl control = session.getDataConn().getFtpTransferControl(); boolean notFinished = control.waitFtpTransferExecuting(); if (notFinished) { session.setReplyCode(ReplyCode.REPLY_503_BAD_SEQUENCE_OF_COMMANDS, "Previous transfer command is not finished yet"); businessHandler.afterRunCommandKo(new Reply503Exception(session.getReplyCode().getMesg())); writeIntermediateAnswer(ctx); return; } } // Default message session.setReplyCode(ReplyCode.REPLY_200_COMMAND_OKAY, null); // Special check for SSL AUTH/PBSZ/PROT/USER/PASS/ACCT if (FtpCommandCode.isSslOrAuthCommand(command.getCode())) { session.setNextCommand(command); messageRunAnswer(ctx); return; } if (session.getCurrentCommand().isNextCommandValid(command)) { logger.debug("Previous: " + session.getCurrentCommand().getCode() + " Next: " + command.getCode()); session.setNextCommand(command); messageRunAnswer(ctx); } else { if (!session.getAuth().isIdentified()) { session.setReplyCode(ReplyCode.REPLY_530_NOT_LOGGED_IN, null); session.setNextCommand(new USER()); writeFinalAnswer(ctx); return; } command = new IncorrectCommand(); command.setArgs(getFtpSession(), message, null, FtpCommandCode.IncorrectSequence); session.setNextCommand(command); messageRunAnswer(ctx); } } } /** * Write the current answer and eventually close channel if necessary (421 or 221) * * @return True if the channel is closed due to the code */ private boolean writeFinalAnswer(ChannelHandlerContext ctx) { if (session.getReplyCode() == ReplyCode.REPLY_421_SERVICE_NOT_AVAILABLE_CLOSING_CONTROL_CONNECTION || session.getReplyCode() == ReplyCode.REPLY_221_CLOSING_CONTROL_CONNECTION) { session.getDataConn().getFtpTransferControl().clear(); writeIntermediateAnswer(ctx).addListener(WaarpSslUtility.SSLCLOSE); return true; } writeIntermediateAnswer(ctx); session.setCurrentCommandFinished(); return false; } /** * Write an intermediate Answer from Business before last answer also set by the Business * * @return the ChannelFuture associated with the write */ public ChannelFuture writeIntermediateAnswer(ChannelHandlerContext ctx) { logger.debug("Answer: " + session.getAnswer()); return ctx.writeAndFlush(session.getAnswer()); } /** * Write an intermediate Answer from Business before last answer also set by the Business * * @return the ChannelFuture associated with the write */ public ChannelFuture writeIntermediateAnswer() { return writeIntermediateAnswer(ctx); } /** * To be extended to inform of an error to SNMP support * * @param error1 * @param error2 */ protected void callForSnmp(String error1, String error2) { // ignore } /** * Execute one command and write the following answer */ private void messageRunAnswer(final ChannelHandlerContext ctx) { boolean error = false; logger.debug("Code: " + session.getCurrentCommand().getCode()); try { businessHandler.beforeRunCommand(); AbstractCommand command = session.getCurrentCommand(); logger.debug("Run {}", command.getCommand()); command.exec(); businessHandler.afterRunCommandOk(); } catch (CommandAbstractException e) { logger.debug("Command in error", e); error = true; session.setReplyCode(e); businessHandler.afterRunCommandKo(e); } logger.debug("Code: " + session.getCurrentCommand().getCode() + " [" + session.getReplyCode() + "]"); if (error) { if (session.getCurrentCommand().getCode() != FtpCommandCode.INTERNALSHUTDOWN) { writeFinalAnswer(ctx); } // In error so Check that Data is closed if (session.getDataConn().isActive()) { logger.debug("Closing DataChannel while command is in error"); try { session.getDataConn().getCurrentDataChannel().close(); } catch (FtpNoConnectionException e) { // ignore } } return; } if (session.getCurrentCommand().getCode() == FtpCommandCode.AUTH || session.getCurrentCommand().getCode() == FtpCommandCode.CCC) { controlChannel.config().setAutoRead(false); ChannelFuture future = writeIntermediateAnswer(ctx); session.setCurrentCommandFinished(); if (session.getCurrentCommand().getCode() == FtpCommandCode.AUTH) { logger.debug("SSL to be added to pipeline"); ChannelHandler sslHandler = ctx.pipeline().first(); if (sslHandler instanceof SslHandler) { logger.debug("Already got a SslHandler"); } else { logger.debug("Add Explicitely SSL support to Command"); // add the SSL support sslHandler = FtpsInitializer.waarpSslContextFactory.initInitializer(true, FtpsInitializer.waarpSslContextFactory.needClientAuthentication()); session.prepareSsl(); WaarpSslUtility.addSslHandler(future, ctx.pipeline(), sslHandler, new GenericFutureListener<Future<? super Channel>>() { public void operationComplete(Future<? super Channel> future) throws Exception { logger.debug("Handshake: " + future.isSuccess() + ":" + ((Channel) future.get()).toString(), future.cause()); if (!future.isSuccess()) { String error2 = future.cause() != null ? future.cause().getMessage() : "During Handshake"; logger.error("Cannot finalize Ssl Command channel " + error2); callForSnmp("SSL Connection Error", error2); session.setSsl(false); ctx.close(); } else { logger.debug("End of initialization of SSL and command channel: " + ctx.channel()); session.setSsl(true); } } }); } } else if (session.getCurrentCommand().getCode() == FtpCommandCode.CCC) { logger.debug("SSL to be removed from pipeline"); // remove the SSL support session.prepareSsl(); WaarpSslUtility.removingSslHandler(future, controlChannel, false); } } else if (session.getCurrentCommand().getCode() != FtpCommandCode.INTERNALSHUTDOWN) { writeFinalAnswer(ctx); } } }