ExecBusinessHandler.java
/*
* This file is part of Waarp Project (named also Waarp or GG).
*
* Copyright (c) 2019, Waarp SAS, 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.gateway.ftp.control;
import io.netty.channel.Channel;
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.Reply451Exception;
import org.waarp.common.command.exception.Reply502Exception;
import org.waarp.common.command.exception.Reply504Exception;
import org.waarp.common.database.DbSession;
import org.waarp.common.database.data.AbstractDbData.UpdatedInfo;
import org.waarp.common.database.exception.WaarpDatabaseNoConnectionException;
import org.waarp.common.future.WaarpFuture;
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.QUIT;
import org.waarp.ftp.core.control.BusinessHandler;
import org.waarp.ftp.core.data.FtpTransfer;
import org.waarp.ftp.core.exception.FtpNoFileException;
import org.waarp.ftp.core.file.FtpFile;
import org.waarp.ftp.core.session.FtpSession;
import org.waarp.ftp.filesystembased.FilesystemBasedFtpRestart;
import org.waarp.gateway.ftp.config.AUTHUPDATE;
import org.waarp.gateway.ftp.config.FileBasedConfiguration;
import org.waarp.gateway.ftp.database.DbConstantFtp;
import org.waarp.gateway.ftp.exec.AbstractExecutor;
import org.waarp.gateway.ftp.file.FileBasedAuth;
import org.waarp.gateway.ftp.file.FileBasedDir;
import java.io.File;
import java.io.IOException;
/**
* BusinessHandler implementation that allows pre and post actions on any
* operations and specifically on
* transfer operations
*/
public class ExecBusinessHandler extends BusinessHandler {
private static final String
TRANSFER_DONE_BUT_FORCE_DISCONNECTION_SINCE_AN_ERROR_OCCURS_ON_POST_OPERATION =
"Transfer done but force disconnection since an error occurs on PostOperation";
private static final String
POST_EXECUTION_IN_ERROR_FOR_TRANSFER_SINCE_NO_FILE_FOUND =
"PostExecution in Error for Transfer since No File found";
/**
* Internal Logger
*/
private static final WaarpLogger logger =
WaarpLoggerFactory.getLogger(ExecBusinessHandler.class);
/**
* Associated DbFtpSession
*/
private DbSession dbFtpSession;
/**
* Associated DbR66Session
*/
private DbSession dbR66Session;
private boolean internalDb;
@Override
public final void afterTransferDoneBeforeAnswer(final FtpTransfer transfer)
throws CommandAbstractException {
// if Admin, do nothing
if (getFtpSession() == null || getFtpSession().getAuth() == null) {
return;
}
final FileBasedAuth auth = (FileBasedAuth) getFtpSession().getAuth();
if (auth.isAdmin()) {
return;
}
final long specialId = auth.getSpecialId();
final ReplyCode replyCode = getFtpSession().getReplyCode();
logger.debug("Transfer done but action needed: {}", !(replyCode !=
ReplyCode.REPLY_250_REQUESTED_FILE_ACTION_OKAY &&
replyCode !=
ReplyCode.REPLY_226_CLOSING_DATA_CONNECTION));
if (replyCode != ReplyCode.REPLY_250_REQUESTED_FILE_ACTION_OKAY &&
replyCode != ReplyCode.REPLY_226_CLOSING_DATA_CONNECTION) {
// Do nothing
final String message = "Transfer done with code: " +
getFtpSession().getReplyCode().getMesg();
WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
message, getFtpSession().getReplyCode(),
this);
return;
}
// if STOR like: get file (can be STOU) and execute external action
final FtpCommandCode code = transfer.getCommand();
logger.debug("Checking action vs auth after transfer: {}", code);
switch (code) {
case RETR:
// nothing to do since All done
WaarpActionLogger.logAction(dbFtpSession, specialId,
"Retrieve executed: OK", this,
getFtpSession().getReplyCode(),
UpdatedInfo.RUNNING);
break;
case APPE:
case STOR:
case STOU:
// execute the store command
final WaarpFuture futureCompletion = new WaarpFuture(true);
final String[] args = new String[6];
args[0] = auth.getUser();
args[1] = auth.getAccount();
args[2] = auth.getBaseDirectory();
final FtpFile file;
try {
file = transfer.getFtpFile();
} catch (final FtpNoFileException e1) {
// File cannot be sent
final String message =
"PostExecution in Error for Transfer since No File found: " +
transfer.getCommand() + ' ' + transfer.getStatus() + ' ' +
transfer.getPath();
final CommandAbstractException exc = new Reply421Exception(
POST_EXECUTION_IN_ERROR_FOR_TRANSFER_SINCE_NO_FILE_FOUND);
WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
message, exc.code, this);
throw exc;
}
try {
args[3] = file.getFile();
final File newfile = new File(args[2] + args[3]);
// Here the transfer is successful. If the file does not exist on disk
// We create it : the transfered file was empty.
if (!newfile.canRead()) {
try {
if (!newfile.createNewFile()) {
logger.error("Cannot create New Empty File");
}
} catch (final IOException e) {
throw new Reply421Exception(
POST_EXECUTION_IN_ERROR_FOR_TRANSFER_SINCE_NO_FILE_FOUND);
} catch (final SecurityException e) {
throw new Reply421Exception(
POST_EXECUTION_IN_ERROR_FOR_TRANSFER_SINCE_NO_FILE_FOUND);
}
}
if (!newfile.canRead()) {
// File cannot be sent
final String message =
"PostExecution in Error for Transfer since File is not readable: " +
transfer.getCommand() + ' ' + newfile.getAbsolutePath() + ':' +
newfile.canRead() + ' ' + transfer.getStatus() + ' ' +
transfer.getPath();
final CommandAbstractException exc = new Reply421Exception(
TRANSFER_DONE_BUT_FORCE_DISCONNECTION_SINCE_AN_ERROR_OCCURS_ON_POST_OPERATION);
WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
message, exc.code, this);
throw exc;
}
} catch (final CommandAbstractException e1) {
// File cannot be sent
final String message =
"PostExecution in Error for Transfer since No File found: " +
transfer.getCommand() + ' ' + transfer.getStatus() + ' ' +
transfer.getPath();
final CommandAbstractException exc = new Reply421Exception(
TRANSFER_DONE_BUT_FORCE_DISCONNECTION_SINCE_AN_ERROR_OCCURS_ON_POST_OPERATION);
WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
message, exc.code, this);
throw exc;
}
args[4] = transfer.getCommand().toString();
args[5] = Long.toString(specialId);
final AbstractExecutor executor =
AbstractExecutor.createAbstractExecutor(auth, args, true,
futureCompletion);
executor.run();
futureCompletion.awaitOrInterruptible();
if (futureCompletion.isSuccess()) {
// All done
WaarpActionLogger.logAction(dbFtpSession, specialId,
"Post-Command executed: OK", this,
getFtpSession().getReplyCode(),
UpdatedInfo.RUNNING);
} else {
// File cannot be sent
final String message =
"PostExecution in Error for Transfer: " + transfer.getCommand() +
' ' + transfer.getStatus() + ' ' + transfer.getPath() + "\n " +
(futureCompletion.getCause() != null?
futureCompletion.getCause().getMessage() :
"Internal error of PostExecution");
final CommandAbstractException exc = new Reply421Exception(
TRANSFER_DONE_BUT_FORCE_DISCONNECTION_SINCE_AN_ERROR_OCCURS_ON_POST_OPERATION);
WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
message, exc.code, this);
throw exc;
}
break;
default:
// nothing to do
}
}
@Override
public final void afterRunCommandKo(final CommandAbstractException e) {
final String message =
"ExecHandler: KO: " + getFtpSession() + ' ' + e.getMessage();
final long specialId =
((FileBasedAuth) getFtpSession().getAuth()).getSpecialId();
WaarpActionLogger.logErrorAction(dbFtpSession, specialId, null, message,
e.code, this);
((FileBasedAuth) getFtpSession().getAuth()).setSpecialId(
org.waarp.common.database.DbConstant.ILLEGALVALUE);
}
@Override
public final void afterRunCommandOk() {
if (!(getFtpSession().getCurrentCommand() instanceof QUIT) &&
dbR66Session != null) {
final long specialId =
((FileBasedAuth) getFtpSession().getAuth()).getSpecialId();
WaarpActionLogger.logAction(dbFtpSession, specialId,
"Transfer Command fully executed: OK", this,
getFtpSession().getReplyCode(),
UpdatedInfo.DONE);
((FileBasedAuth) getFtpSession().getAuth()).setSpecialId(
org.waarp.common.database.DbConstant.ILLEGALVALUE);
}
}
@Override
public final void beforeRunCommand() throws CommandAbstractException {
long specialId = org.waarp.common.database.DbConstant.ILLEGALVALUE;
// if Admin, do nothing
if (getFtpSession() == null || getFtpSession().getAuth() == null) {
return;
}
final FileBasedAuth auth = (FileBasedAuth) getFtpSession().getAuth();
if (auth.isAdmin()) {
logger.debug("Admin user so all actions are allowed");
return;
}
// Test limits
final FtpConstraintLimitHandler constraints =
((FileBasedConfiguration) getFtpSession().getConfiguration()).getConstraintLimitHandler();
if (constraints != null) {
if (!auth.isIdentified()) {
// ignore test since it can be an Admin connection
} else if (auth.isAdmin()) {
// ignore test since it is an Admin connection (always valid)
} else if (!FtpCommandCode.isSpecialCommand(
getFtpSession().getCurrentCommand().getCode())) {
// Authenticated, not Admin and not Special Command
if (constraints.checkConstraintsSleep(1)) {
if (constraints.checkConstraints()) {
// Really overload so refuse the command
logger.info(
"Server overloaded. {} Try later... \n" + getFtpSession(),
constraints.lastAlert);
if (FileBasedConfiguration.fileBasedConfiguration.getFtpMib() !=
null) {
FileBasedConfiguration.fileBasedConfiguration.getFtpMib()
.notifyOverloaded(
"Server overloaded",
getFtpSession().toString());
}
throw new Reply451Exception("Server overloaded. Try later...");
}
}
}
}
final FtpCommandCode code = getFtpSession().getCurrentCommand().getCode();
logger.debug("Checking action vs auth before command: {}", code);
switch (code) {
case APPE:
case STOR:
case STOU:
auth.setSpecialId(specialId);
if (!auth.getCommandExecutor().isValidOperation(true)) {
throw new Reply504Exception("STORe like operations are not allowed");
}
// create entry in log
specialId =
WaarpActionLogger.logCreate(dbFtpSession, "PrepareTransfer: OK",
getFtpSession().getCurrentCommand()
.getArg(), this);
auth.setSpecialId(specialId);
// nothing to do now
break;
case RETR:
auth.setSpecialId(specialId);
if (!auth.getCommandExecutor().isValidOperation(false)) {
throw new Reply504Exception(
"RETRieve like operations are not allowed");
}
// create entry in log
specialId =
WaarpActionLogger.logCreate(dbFtpSession, "PrepareTransfer: OK",
getFtpSession().getCurrentCommand()
.getArg(), this);
auth.setSpecialId(specialId);
// execute the external retrieve command before the execution of RETR
final WaarpFuture futureCompletion = new WaarpFuture(true);
final String[] args = new String[6];
args[0] = auth.getUser();
args[1] = auth.getAccount();
args[2] = auth.getBaseDirectory();
final String filename = getFtpSession().getCurrentCommand().getArg();
final FtpFile file = getFtpSession().getDir().setFile(filename, false);
args[3] = file.getFile();
args[4] = code.toString();
args[5] = Long.toString(specialId);
final AbstractExecutor executor =
AbstractExecutor.createAbstractExecutor(auth, args, false,
futureCompletion);
executor.run();
futureCompletion.awaitOrInterruptible();
if (futureCompletion.isSuccess()) {
// File should be ready
if (!file.canRead()) {
logger.error("PreExecution in Error for Transfer since " +
"File downloaded but not ready to be retrieved: {} " +
" {} \n " + (futureCompletion.getCause() != null?
futureCompletion.getCause().getMessage() :
"File downloaded but not ready to be retrieved"), args[4],
args[3]);
throw new Reply421Exception(
"File downloaded but not ready to be retrieved");
}
WaarpActionLogger.logAction(dbFtpSession, specialId,
"Pre-Command executed: OK", this,
getFtpSession().getReplyCode(),
UpdatedInfo.RUNNING);
} else {
// File cannot be retrieved
logger.error("PreExecution in Error for Transfer since " +
"File cannot be prepared to be retrieved: {} " +
" {} \n " + (futureCompletion.getCause() != null?
futureCompletion.getCause().getMessage() :
"File cannot be prepared to be retrieved"), args[4], args[3]);
throw new Reply421Exception(
"File cannot be prepared to be retrieved");
}
break;
default:
// nothing to do
}
}
@Override
protected final void cleanSession() {
// Nothing
}
@Override
public final void exceptionLocalCaught(final Throwable cause) {
if (FileBasedConfiguration.fileBasedConfiguration.getFtpMib() != null) {
final String mesg;
if (cause != null && cause.getMessage() != null) {
mesg = cause.getMessage();
} else {
if (getFtpSession() != null) {
mesg = "Exception while " + getFtpSession().getReplyCode().getMesg();
} else {
mesg = "Unknown Exception";
}
}
FileBasedConfiguration.fileBasedConfiguration.getFtpMib().notifyError(
"Exception trapped", mesg);
}
if (FileBasedConfiguration.fileBasedConfiguration.getMonitoring() != null) {
if (getFtpSession() != null) {
FileBasedConfiguration.fileBasedConfiguration.getMonitoring()
.updateCodeNoTransfer(
getFtpSession().getReplyCode());
}
}
}
@Override
public final void executeChannelClosed() {
if (AbstractExecutor.useDatabase && !internalDb && dbR66Session != null) {
dbR66Session.disconnect();
dbR66Session = null;
}
if (dbFtpSession != null) {
dbFtpSession.disconnect();
dbFtpSession = null;
}
}
@Override
public final void executeChannelConnected(final Channel channel) {
if (AbstractExecutor.useDatabase) {
if (org.waarp.common.database.DbConstant.admin != null) {
try {
dbR66Session =
new DbSession(org.waarp.common.database.DbConstant.admin, false);
} catch (final WaarpDatabaseNoConnectionException e1) {
logger.warn("Database not ready due to {}", e1.getMessage());
final QUIT command =
(QUIT) FtpCommandCode.getFromLine(getFtpSession(),
FtpCommandCode.QUIT.name());
getFtpSession().setNextCommand(command);
dbR66Session = null;
internalDb = true;
}
}
}
if (DbConstantFtp.gatewayAdmin != null) {
try {
dbFtpSession = new DbSession(DbConstantFtp.gatewayAdmin, false);
} catch (final WaarpDatabaseNoConnectionException e1) {
logger.warn("Database not ready due to {}", e1.getMessage());
final QUIT command = (QUIT) FtpCommandCode.getFromLine(getFtpSession(),
FtpCommandCode.QUIT.name());
getFtpSession().setNextCommand(command);
dbFtpSession = null;
}
}
}
@Override
public final FileBasedAuth getBusinessNewAuth() {
return new FileBasedAuth(getFtpSession());
}
@Override
public final FileBasedDir getBusinessNewDir() {
return new FileBasedDir(getFtpSession());
}
@Override
public final FilesystemBasedFtpRestart getBusinessNewRestart() {
return new FilesystemBasedFtpRestart(getFtpSession());
}
@Override
public final String getHelpMessage(final String arg) {
return
"This FTP server is only intend as a Gateway. RETRieve actions may be unallowed.\n" +
"This FTP server refers to RFC 959, 775, 2389, 2428, 3659 and " +
"supports XDIGEST, XCRC, XMD5 and XSHA1 commands.\n" +
"XCRC, XMD5 and XSHA1 take a simple filename as argument, XDIGEST " +
"taking algorithm (among CRC32, ADLER32, MD5, MD2, " +
"SHA-1, SHA-256, SHA-384, SHA-512) followed by filename " +
"as arguments, and return 250 digest-value is the digest of filename.";
}
@Override
public final String getFeatMessage() {
final StringBuilder builder =
new StringBuilder("Extensions supported:").append('\n').append(
getDefaultFeatMessage());
if (getFtpSession().getConfiguration().getFtpInternalConfiguration()
.isAcceptAuthProt()) {
builder.append('\n').append(getSslFeatMessage());
}
builder.append('\n').append(FtpCommandCode.SITE.name()).append(' ')
.append("AUTHUPDATE").append("\nEnd");
return builder.toString();
}
@Override
public final String getOptsMessage(final String[] args)
throws CommandAbstractException {
if (args.length > 0) {
if (args[0].equalsIgnoreCase(FtpCommandCode.MLST.name()) ||
args[0].equalsIgnoreCase(FtpCommandCode.MLSD.name())) {
return getMLSxOptsMessage(args);
}
throw new Reply502Exception("OPTS not implemented for " + args[0]);
}
throw new Reply502Exception("OPTS not implemented");
}
@Override
public final AbstractCommand getSpecializedSiteCommand(
final FtpSession session, final String line) {
if (getFtpSession() == null || getFtpSession().getAuth() == null) {
return null;
}
if (!session.getAuth().isAdmin()) {
return null;
}
if (line == null) {
return null;
}
final String command;
String arg;
if (line.indexOf(' ') == -1) {
command = line;
arg = null;
} else {
command = line.substring(0, line.indexOf(' '));
arg = line.substring(line.indexOf(' ') + 1);
if (arg.length() == 0) {
arg = null;
}
}
final String COMMAND = command.toUpperCase();
if (!"AUTHUPDATE".equals(COMMAND)) {
return null;
}
final AbstractCommand abstractCommand = new AUTHUPDATE();
abstractCommand.setArgs(session, COMMAND, arg, FtpCommandCode.SITE);
return abstractCommand;
}
}