View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  
21  package org.waarp.gateway.ftp.control;
22  
23  import io.netty.channel.Channel;
24  import org.waarp.common.command.ReplyCode;
25  import org.waarp.common.command.exception.CommandAbstractException;
26  import org.waarp.common.command.exception.Reply421Exception;
27  import org.waarp.common.command.exception.Reply451Exception;
28  import org.waarp.common.command.exception.Reply502Exception;
29  import org.waarp.common.command.exception.Reply504Exception;
30  import org.waarp.common.database.DbSession;
31  import org.waarp.common.database.data.AbstractDbData.UpdatedInfo;
32  import org.waarp.common.database.exception.WaarpDatabaseNoConnectionException;
33  import org.waarp.common.future.WaarpFuture;
34  import org.waarp.common.logging.WaarpLogger;
35  import org.waarp.common.logging.WaarpLoggerFactory;
36  import org.waarp.ftp.core.command.AbstractCommand;
37  import org.waarp.ftp.core.command.FtpCommandCode;
38  import org.waarp.ftp.core.command.access.QUIT;
39  import org.waarp.ftp.core.control.BusinessHandler;
40  import org.waarp.ftp.core.data.FtpTransfer;
41  import org.waarp.ftp.core.exception.FtpNoFileException;
42  import org.waarp.ftp.core.file.FtpFile;
43  import org.waarp.ftp.core.session.FtpSession;
44  import org.waarp.ftp.filesystembased.FilesystemBasedFtpRestart;
45  import org.waarp.gateway.ftp.config.AUTHUPDATE;
46  import org.waarp.gateway.ftp.config.FileBasedConfiguration;
47  import org.waarp.gateway.ftp.database.DbConstantFtp;
48  import org.waarp.gateway.ftp.exec.AbstractExecutor;
49  import org.waarp.gateway.ftp.file.FileBasedAuth;
50  import org.waarp.gateway.ftp.file.FileBasedDir;
51  
52  import java.io.File;
53  import java.io.IOException;
54  
55  /**
56   * BusinessHandler implementation that allows pre and post actions on any
57   * operations and specifically on
58   * transfer operations
59   */
60  public class ExecBusinessHandler extends BusinessHandler {
61    private static final String
62        TRANSFER_DONE_BUT_FORCE_DISCONNECTION_SINCE_AN_ERROR_OCCURS_ON_POST_OPERATION =
63        "Transfer done but force disconnection since an error occurs on PostOperation";
64  
65    private static final String
66        POST_EXECUTION_IN_ERROR_FOR_TRANSFER_SINCE_NO_FILE_FOUND =
67        "PostExecution in Error for Transfer since No File found";
68  
69    /**
70     * Internal Logger
71     */
72    private static final WaarpLogger logger =
73        WaarpLoggerFactory.getLogger(ExecBusinessHandler.class);
74  
75    /**
76     * Associated DbFtpSession
77     */
78    private DbSession dbFtpSession;
79    /**
80     * Associated DbR66Session
81     */
82    private DbSession dbR66Session;
83    private boolean internalDb;
84  
85    @Override
86    public final void afterTransferDoneBeforeAnswer(final FtpTransfer transfer)
87        throws CommandAbstractException {
88      // if Admin, do nothing
89      if (getFtpSession() == null || getFtpSession().getAuth() == null) {
90        return;
91      }
92      final FileBasedAuth auth = (FileBasedAuth) getFtpSession().getAuth();
93      if (auth.isAdmin()) {
94        return;
95      }
96      final long specialId = auth.getSpecialId();
97      final ReplyCode replyCode = getFtpSession().getReplyCode();
98      logger.debug("Transfer done but action needed: {}", !(replyCode !=
99                                                            ReplyCode.REPLY_250_REQUESTED_FILE_ACTION_OKAY &&
100                                                           replyCode !=
101                                                           ReplyCode.REPLY_226_CLOSING_DATA_CONNECTION));
102     if (replyCode != ReplyCode.REPLY_250_REQUESTED_FILE_ACTION_OKAY &&
103         replyCode != ReplyCode.REPLY_226_CLOSING_DATA_CONNECTION) {
104       // Do nothing
105       final String message = "Transfer done with code: " +
106                              getFtpSession().getReplyCode().getMesg();
107       WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
108                                        message, getFtpSession().getReplyCode(),
109                                        this);
110       return;
111     }
112     // if STOR like: get file (can be STOU) and execute external action
113     final FtpCommandCode code = transfer.getCommand();
114     logger.debug("Checking action vs auth after transfer: {}", code);
115     switch (code) {
116       case RETR:
117         // nothing to do since All done
118         WaarpActionLogger.logAction(dbFtpSession, specialId,
119                                     "Retrieve executed: OK", this,
120                                     getFtpSession().getReplyCode(),
121                                     UpdatedInfo.RUNNING);
122         break;
123       case APPE:
124       case STOR:
125       case STOU:
126         // execute the store command
127         final WaarpFuture futureCompletion = new WaarpFuture(true);
128         final String[] args = new String[6];
129         args[0] = auth.getUser();
130         args[1] = auth.getAccount();
131         args[2] = auth.getBaseDirectory();
132         final FtpFile file;
133         try {
134           file = transfer.getFtpFile();
135         } catch (final FtpNoFileException e1) {
136           // File cannot be sent
137           final String message =
138               "PostExecution in Error for Transfer since No File found: " +
139               transfer.getCommand() + ' ' + transfer.getStatus() + ' ' +
140               transfer.getPath();
141           final CommandAbstractException exc = new Reply421Exception(
142               POST_EXECUTION_IN_ERROR_FOR_TRANSFER_SINCE_NO_FILE_FOUND);
143           WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
144                                            message, exc.code, this);
145           throw exc;
146         }
147         try {
148           args[3] = file.getFile();
149           final File newfile = new File(args[2] + args[3]);
150 
151           // Here the transfer is successful. If the file does not exist on disk
152           // We create it : the transfered file was empty.
153           if (!newfile.canRead()) {
154             try {
155               if (!newfile.createNewFile()) {
156                 logger.error("Cannot create New Empty File");
157               }
158             } catch (final IOException e) {
159               throw new Reply421Exception(
160                   POST_EXECUTION_IN_ERROR_FOR_TRANSFER_SINCE_NO_FILE_FOUND);
161             } catch (final SecurityException e) {
162               throw new Reply421Exception(
163                   POST_EXECUTION_IN_ERROR_FOR_TRANSFER_SINCE_NO_FILE_FOUND);
164             }
165           }
166 
167           if (!newfile.canRead()) {
168             // File cannot be sent
169             final String message =
170                 "PostExecution in Error for Transfer since File is not readable: " +
171                 transfer.getCommand() + ' ' + newfile.getAbsolutePath() + ':' +
172                 newfile.canRead() + ' ' + transfer.getStatus() + ' ' +
173                 transfer.getPath();
174             final CommandAbstractException exc = new Reply421Exception(
175                 TRANSFER_DONE_BUT_FORCE_DISCONNECTION_SINCE_AN_ERROR_OCCURS_ON_POST_OPERATION);
176             WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
177                                              message, exc.code, this);
178             throw exc;
179           }
180         } catch (final CommandAbstractException e1) {
181           // File cannot be sent
182           final String message =
183               "PostExecution in Error for Transfer since No File found: " +
184               transfer.getCommand() + ' ' + transfer.getStatus() + ' ' +
185               transfer.getPath();
186           final CommandAbstractException exc = new Reply421Exception(
187               TRANSFER_DONE_BUT_FORCE_DISCONNECTION_SINCE_AN_ERROR_OCCURS_ON_POST_OPERATION);
188           WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
189                                            message, exc.code, this);
190           throw exc;
191         }
192         args[4] = transfer.getCommand().toString();
193         args[5] = Long.toString(specialId);
194         final AbstractExecutor executor =
195             AbstractExecutor.createAbstractExecutor(auth, args, true,
196                                                     futureCompletion);
197         executor.run();
198         futureCompletion.awaitOrInterruptible();
199         if (futureCompletion.isSuccess()) {
200           // All done
201           WaarpActionLogger.logAction(dbFtpSession, specialId,
202                                       "Post-Command executed: OK", this,
203                                       getFtpSession().getReplyCode(),
204                                       UpdatedInfo.RUNNING);
205         } else {
206           // File cannot be sent
207           final String message =
208               "PostExecution in Error for Transfer: " + transfer.getCommand() +
209               ' ' + transfer.getStatus() + ' ' + transfer.getPath() + "\n   " +
210               (futureCompletion.getCause() != null?
211                   futureCompletion.getCause().getMessage() :
212                   "Internal error of PostExecution");
213           final CommandAbstractException exc = new Reply421Exception(
214               TRANSFER_DONE_BUT_FORCE_DISCONNECTION_SINCE_AN_ERROR_OCCURS_ON_POST_OPERATION);
215           WaarpActionLogger.logErrorAction(dbFtpSession, specialId, transfer,
216                                            message, exc.code, this);
217           throw exc;
218         }
219         break;
220       default:
221         // nothing to do
222     }
223   }
224 
225   @Override
226   public final void afterRunCommandKo(final CommandAbstractException e) {
227     final String message =
228         "ExecHandler: KO: " + getFtpSession() + ' ' + e.getMessage();
229     final long specialId =
230         ((FileBasedAuth) getFtpSession().getAuth()).getSpecialId();
231     WaarpActionLogger.logErrorAction(dbFtpSession, specialId, null, message,
232                                      e.code, this);
233     ((FileBasedAuth) getFtpSession().getAuth()).setSpecialId(
234         org.waarp.common.database.DbConstant.ILLEGALVALUE);
235   }
236 
237   @Override
238   public final void afterRunCommandOk() {
239     if (!(getFtpSession().getCurrentCommand() instanceof QUIT) &&
240         dbR66Session != null) {
241       final long specialId =
242           ((FileBasedAuth) getFtpSession().getAuth()).getSpecialId();
243       WaarpActionLogger.logAction(dbFtpSession, specialId,
244                                   "Transfer Command fully executed: OK", this,
245                                   getFtpSession().getReplyCode(),
246                                   UpdatedInfo.DONE);
247       ((FileBasedAuth) getFtpSession().getAuth()).setSpecialId(
248           org.waarp.common.database.DbConstant.ILLEGALVALUE);
249     }
250   }
251 
252   @Override
253   public final void beforeRunCommand() throws CommandAbstractException {
254     long specialId = org.waarp.common.database.DbConstant.ILLEGALVALUE;
255     // if Admin, do nothing
256     if (getFtpSession() == null || getFtpSession().getAuth() == null) {
257       return;
258     }
259     final FileBasedAuth auth = (FileBasedAuth) getFtpSession().getAuth();
260     if (auth.isAdmin()) {
261       logger.debug("Admin user so all actions are allowed");
262       return;
263     }
264     // Test limits
265     final FtpConstraintLimitHandler constraints =
266         ((FileBasedConfiguration) getFtpSession().getConfiguration()).getConstraintLimitHandler();
267     if (constraints != null) {
268       if (!auth.isIdentified()) {
269         // ignore test since it can be an Admin connection
270       } else if (auth.isAdmin()) {
271         // ignore test since it is an Admin connection (always valid)
272       } else if (!FtpCommandCode.isSpecialCommand(
273           getFtpSession().getCurrentCommand().getCode())) {
274         // Authenticated, not Admin and not Special Command
275         if (constraints.checkConstraintsSleep(1)) {
276           if (constraints.checkConstraints()) {
277             // Really overload so refuse the command
278             logger.info(
279                 "Server overloaded. {} Try later... \n" + getFtpSession(),
280                 constraints.lastAlert);
281             if (FileBasedConfiguration.fileBasedConfiguration.getFtpMib() !=
282                 null) {
283               FileBasedConfiguration.fileBasedConfiguration.getFtpMib()
284                                                            .notifyOverloaded(
285                                                                "Server overloaded",
286                                                                getFtpSession().toString());
287             }
288             throw new Reply451Exception("Server overloaded. Try later...");
289           }
290         }
291       }
292     }
293     final FtpCommandCode code = getFtpSession().getCurrentCommand().getCode();
294     logger.debug("Checking action vs auth before command: {}", code);
295     switch (code) {
296       case APPE:
297       case STOR:
298       case STOU:
299         auth.setSpecialId(specialId);
300         if (!auth.getCommandExecutor().isValidOperation(true)) {
301           throw new Reply504Exception("STORe like operations are not allowed");
302         }
303         // create entry in log
304         specialId =
305             WaarpActionLogger.logCreate(dbFtpSession, "PrepareTransfer: OK",
306                                         getFtpSession().getCurrentCommand()
307                                                        .getArg(), this);
308         auth.setSpecialId(specialId);
309         // nothing to do now
310         break;
311       case RETR:
312         auth.setSpecialId(specialId);
313         if (!auth.getCommandExecutor().isValidOperation(false)) {
314           throw new Reply504Exception(
315               "RETRieve like operations are not allowed");
316         }
317         // create entry in log
318         specialId =
319             WaarpActionLogger.logCreate(dbFtpSession, "PrepareTransfer: OK",
320                                         getFtpSession().getCurrentCommand()
321                                                        .getArg(), this);
322         auth.setSpecialId(specialId);
323         // execute the external retrieve command before the execution of RETR
324         final WaarpFuture futureCompletion = new WaarpFuture(true);
325         final String[] args = new String[6];
326         args[0] = auth.getUser();
327         args[1] = auth.getAccount();
328         args[2] = auth.getBaseDirectory();
329         final String filename = getFtpSession().getCurrentCommand().getArg();
330         final FtpFile file = getFtpSession().getDir().setFile(filename, false);
331         args[3] = file.getFile();
332         args[4] = code.toString();
333         args[5] = Long.toString(specialId);
334         final AbstractExecutor executor =
335             AbstractExecutor.createAbstractExecutor(auth, args, false,
336                                                     futureCompletion);
337         executor.run();
338         futureCompletion.awaitOrInterruptible();
339         if (futureCompletion.isSuccess()) {
340           // File should be ready
341           if (!file.canRead()) {
342             logger.error("PreExecution in Error for Transfer since " +
343                          "File downloaded but not ready to be retrieved: {} " +
344                          " {} \n   " + (futureCompletion.getCause() != null?
345                              futureCompletion.getCause().getMessage() :
346                              "File downloaded but not ready to be retrieved"), args[4],
347                          args[3]);
348             throw new Reply421Exception(
349                 "File downloaded but not ready to be retrieved");
350           }
351           WaarpActionLogger.logAction(dbFtpSession, specialId,
352                                       "Pre-Command executed: OK", this,
353                                       getFtpSession().getReplyCode(),
354                                       UpdatedInfo.RUNNING);
355         } else {
356           // File cannot be retrieved
357           logger.error("PreExecution in Error for Transfer since " +
358                        "File cannot be prepared to be retrieved: {} " +
359                        " {} \n   " + (futureCompletion.getCause() != null?
360               futureCompletion.getCause().getMessage() :
361               "File cannot be prepared to be retrieved"), args[4], args[3]);
362           throw new Reply421Exception(
363               "File cannot be prepared to be retrieved");
364         }
365         break;
366       default:
367         // nothing to do
368     }
369   }
370 
371   @Override
372   protected final void cleanSession() {
373     // Nothing
374   }
375 
376   @Override
377   public final void exceptionLocalCaught(final Throwable cause) {
378     if (FileBasedConfiguration.fileBasedConfiguration.getFtpMib() != null) {
379       final String mesg;
380       if (cause != null && cause.getMessage() != null) {
381         mesg = cause.getMessage();
382       } else {
383         if (getFtpSession() != null) {
384           mesg = "Exception while " + getFtpSession().getReplyCode().getMesg();
385         } else {
386           mesg = "Unknown Exception";
387         }
388       }
389       FileBasedConfiguration.fileBasedConfiguration.getFtpMib().notifyError(
390           "Exception trapped", mesg);
391     }
392     if (FileBasedConfiguration.fileBasedConfiguration.getMonitoring() != null) {
393       if (getFtpSession() != null) {
394         FileBasedConfiguration.fileBasedConfiguration.getMonitoring()
395                                                      .updateCodeNoTransfer(
396                                                          getFtpSession().getReplyCode());
397       }
398     }
399   }
400 
401   @Override
402   public final void executeChannelClosed() {
403     if (AbstractExecutor.useDatabase && !internalDb && dbR66Session != null) {
404       dbR66Session.disconnect();
405       dbR66Session = null;
406     }
407     if (dbFtpSession != null) {
408       dbFtpSession.disconnect();
409       dbFtpSession = null;
410     }
411   }
412 
413   @Override
414   public final void executeChannelConnected(final Channel channel) {
415     if (AbstractExecutor.useDatabase) {
416       if (org.waarp.common.database.DbConstant.admin != null) {
417         try {
418           dbR66Session =
419               new DbSession(org.waarp.common.database.DbConstant.admin, false);
420         } catch (final WaarpDatabaseNoConnectionException e1) {
421           logger.warn("Database not ready due to {}", e1.getMessage());
422           final QUIT command =
423               (QUIT) FtpCommandCode.getFromLine(getFtpSession(),
424                                                 FtpCommandCode.QUIT.name());
425           getFtpSession().setNextCommand(command);
426           dbR66Session = null;
427           internalDb = true;
428         }
429       }
430     }
431     if (DbConstantFtp.gatewayAdmin != null) {
432       try {
433         dbFtpSession = new DbSession(DbConstantFtp.gatewayAdmin, false);
434       } catch (final WaarpDatabaseNoConnectionException e1) {
435         logger.warn("Database not ready due to {}", e1.getMessage());
436         final QUIT command = (QUIT) FtpCommandCode.getFromLine(getFtpSession(),
437                                                                FtpCommandCode.QUIT.name());
438         getFtpSession().setNextCommand(command);
439         dbFtpSession = null;
440       }
441     }
442   }
443 
444   @Override
445   public final FileBasedAuth getBusinessNewAuth() {
446     return new FileBasedAuth(getFtpSession());
447   }
448 
449   @Override
450   public final FileBasedDir getBusinessNewDir() {
451     return new FileBasedDir(getFtpSession());
452   }
453 
454   @Override
455   public final FilesystemBasedFtpRestart getBusinessNewRestart() {
456     return new FilesystemBasedFtpRestart(getFtpSession());
457   }
458 
459   @Override
460   public final String getHelpMessage(final String arg) {
461     return
462         "This FTP server is only intend as a Gateway. RETRieve actions may be unallowed.\n" +
463         "This FTP server refers to RFC 959, 775, 2389, 2428, 3659 and " +
464         "supports XDIGEST, XCRC, XMD5 and XSHA1 commands.\n" +
465         "XCRC, XMD5 and XSHA1 take a simple filename as argument, XDIGEST " +
466         "taking algorithm (among CRC32, ADLER32, MD5, MD2, " +
467         "SHA-1, SHA-256, SHA-384, SHA-512) followed by filename " +
468         "as arguments, and return 250 digest-value is the digest of filename.";
469   }
470 
471   @Override
472   public final String getFeatMessage() {
473     final StringBuilder builder =
474         new StringBuilder("Extensions supported:").append('\n').append(
475             getDefaultFeatMessage());
476     if (getFtpSession().getConfiguration().getFtpInternalConfiguration()
477                        .isAcceptAuthProt()) {
478       builder.append('\n').append(getSslFeatMessage());
479     }
480     builder.append('\n').append(FtpCommandCode.SITE.name()).append(' ')
481            .append("AUTHUPDATE").append("\nEnd");
482     return builder.toString();
483   }
484 
485   @Override
486   public final String getOptsMessage(final String[] args)
487       throws CommandAbstractException {
488     if (args.length > 0) {
489       if (args[0].equalsIgnoreCase(FtpCommandCode.MLST.name()) ||
490           args[0].equalsIgnoreCase(FtpCommandCode.MLSD.name())) {
491         return getMLSxOptsMessage(args);
492       }
493       throw new Reply502Exception("OPTS not implemented for " + args[0]);
494     }
495     throw new Reply502Exception("OPTS not implemented");
496   }
497 
498   @Override
499   public final AbstractCommand getSpecializedSiteCommand(
500       final FtpSession session, final String line) {
501     if (getFtpSession() == null || getFtpSession().getAuth() == null) {
502       return null;
503     }
504     if (!session.getAuth().isAdmin()) {
505       return null;
506     }
507     if (line == null) {
508       return null;
509     }
510     final String command;
511     String arg;
512     if (line.indexOf(' ') == -1) {
513       command = line;
514       arg = null;
515     } else {
516       command = line.substring(0, line.indexOf(' '));
517       arg = line.substring(line.indexOf(' ') + 1);
518       if (arg.length() == 0) {
519         arg = null;
520       }
521     }
522     final String COMMAND = command.toUpperCase();
523     if (!"AUTHUPDATE".equals(COMMAND)) {
524       return null;
525     }
526     final AbstractCommand abstractCommand = new AUTHUPDATE();
527     abstractCommand.setArgs(session, COMMAND, arg, FtpCommandCode.SITE);
528     return abstractCommand;
529   }
530 }