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.icap;
22  
23  import org.apache.commons.cli.CommandLine;
24  import org.apache.commons.cli.CommandLineParser;
25  import org.apache.commons.cli.DefaultParser;
26  import org.apache.commons.cli.HelpFormatter;
27  import org.apache.commons.cli.Option;
28  import org.apache.commons.cli.OptionGroup;
29  import org.apache.commons.cli.Options;
30  import org.apache.commons.cli.ParseException;
31  import org.waarp.common.command.exception.Reply550Exception;
32  import org.waarp.common.file.FileUtils;
33  import org.waarp.common.logging.WaarpLogLevel;
34  import org.waarp.common.logging.WaarpLogger;
35  import org.waarp.common.logging.WaarpLoggerFactory;
36  import org.waarp.common.logging.WaarpSlf4JLoggerFactory;
37  
38  import java.io.File;
39  import java.io.IOException;
40  import java.util.Arrays;
41  import java.util.Map;
42  
43  /**
44   * IcapScanFile command to ask an ICAP server to scan a file
45   * through network ICAP protocol.<br>
46   * <br>
47   * Options:<br>
48   * -file path_to_file <br>
49   * -to hostname <br>
50   * [-port port, default 1344] <br>
51   * -service name | -model name <br>
52   * [-previewSize size, default none] <br>
53   * [-blockSize size, default 8192] <br>
54   * [-receiveSize size, default 65536] <br>
55   * [-maxSize size, default MAX_INTEGER] <br>
56   * [-timeout in_ms, default equiv to 10 min] <br>
57   * [-errorMove path | -errorDelete | -sendOnError] <br>
58   * [-ignoreNetworkError] <br>
59   * [-ignoreTooBigFileError] <br>
60   * [-keyPreview key -stringPreview string, default none] <br>
61   * [-key204 key -string204 string, default none] <br>
62   * [-key200 key -string200 string, default none] <br>
63   * [-stringHttp string, default none] <br>
64   * [-logger DEBUG|INFO|WARN|ERROR, default none] <br>
65   * <br>
66   * Exit with values:<br>
67   * <ul>
68   *   <li>0: Scan OK</li>
69   *   <li>1: Bad arguments</li>
70   *   <li>2: ICAP protocol error</li>
71   *   <li>3: Network error</li>
72   *   <li>4: Scan KO</li>
73   *   <li>5: Scan KO but post action required in error</li>
74   * </ul>
75   */
76  public class IcapScanFile {
77    private static final WaarpLogger logger =
78        WaarpLoggerFactory.getLogger(IcapScanFile.class);
79  
80    public static final int STATUS_OK = 0;
81    public static final int STATUS_BAD_ARGUMENT = 1;
82    public static final int STATUS_ICAP_ISSUE = 2;
83    public static final int STATUS_NETWORK_ISSUE = 3;
84    public static final int STATUS_KO_SCAN = 4;
85    public static final int STATUS_KO_SCAN_POST_ACTION_ERROR = 5;
86  
87    private static final String ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL =
88        "Arguments cannot be empty or null";
89  
90    private static final String FILE = "file";
91    public static final String FILE_ARG = "-" + FILE;
92    private static final Option FILE_OPTION =
93        Option.builder(FILE).required(true).hasArg(true)
94              .desc("Specify the file path to operate on").build();
95    private static final String TO = "to";
96    public static final String TO_ARG = "-" + TO;
97    private static final Option HOST_OPTION =
98        Option.builder(TO).required(true).hasArg(true)
99              .desc("Specify the requested Host").build();
100   private static final String SERVICE = "service";
101   public static final String SERVICE_ARG = "-" + SERVICE;
102   private static final Option SERVICE_OPTION =
103       Option.builder(SERVICE).required(true).hasArg(true)
104             .desc("Specify the service on remote host to use").build();
105   private static final String MODEL = "model";
106   public static final String MODEL_ARG = "-" + MODEL;
107   private static final Option MODEL_OPTION =
108       Option.builder(MODEL).required(true).hasArg(true)
109             .desc("Specify the model of remote host service to use").build();
110   private static final String PORT_FIELD = "port";
111   private static final Option PORT_OPTION =
112       Option.builder(PORT_FIELD).required(false).hasArg(true)
113             .desc("Specify the port on remote host to use").type(Number.class)
114             .build();
115   private static final String PREVIEW_SIZE = "previewSize";
116   private static final Option PREVIEW_OPTION =
117       Option.builder(PREVIEW_SIZE).required(false).hasArg(true)
118             .desc("Specify the Preview size to use").build();
119   private static final String BLOCK_SIZE = "blockSize";
120   private static final Option BLOCK_OPTION =
121       Option.builder(BLOCK_SIZE).required(false).hasArg(true)
122             .desc("Specify the Block size to use").build();
123   private static final String RECEIVE_SIZE = "receiveSize";
124   private static final Option RECEIVE_OPTION =
125       Option.builder(RECEIVE_SIZE).required(false).hasArg(true)
126             .desc("Specify the Receive size to use").build();
127   private static final String MAX_SIZE = "maxSize";
128   private static final Option MAX_SIZE_OPTION =
129       Option.builder(MAX_SIZE).required(false).hasArg(true)
130             .desc("Specify the Max size to use").build();
131   private static final String TIMEOUT_ARG = "timeout";
132   private static final Option TIMEOUT_OPTION =
133       Option.builder(TIMEOUT_ARG).required(false).hasArg(true)
134             .desc("Specify the timeout on socket to use").build();
135   private static final String ERROR_MOVE = "errorMove";
136   private static final Option ERROR_MOVE_OPTION =
137       Option.builder(ERROR_MOVE).required(false).hasArg(true)
138             .desc("Specify the path to use if wrong scan").build();
139   private static final String ERROR_DELETE = "errorDelete";
140   private static final Option ERROR_DELETE_OPTION =
141       Option.builder(ERROR_DELETE).required(false).hasArg(false)
142             .desc("Specify the error delete action if wrong scan").build();
143   private static final String ERROR_SEND = "sendOnError";
144   public static final String ERROR_SEND_ARG = "-" + ERROR_SEND;
145   private static final Option ERROR_SEND_OPTION =
146       Option.builder(ERROR_SEND).required(false).hasArg(false)
147             .desc("Specify that scan error should be followed by an r66send")
148             .build();
149   private static final String IGNORE_NETWORK_CONTINUE = "ignoreNetworkError";
150   private static final Option IGNORE_NETWORK_CONTINUE_OPTION =
151       Option.builder(IGNORE_NETWORK_CONTINUE).required(false).hasArg(false)
152             .desc("Specify that a network error should not be followed by a ko")
153             .build();
154   private static final String IGNORE_TOO_BIG_FILE_CONTINUE =
155       "ignoreTooBigFileError";
156   private static final Option IGNORE_TOO_BIG_FILE_CONTINUE_OPTION =
157       Option.builder(IGNORE_TOO_BIG_FILE_CONTINUE).required(false).hasArg(false)
158             .desc("Specify that a too big file should not be followed by a ko")
159             .build();
160   private static final String KEY_PREVIEW = "keyPreview";
161   private static final Option PREVIEW_KEY_OPTION =
162       Option.builder(KEY_PREVIEW).required(false).hasArg(true)
163             .desc("Specify the key for Options to validate").build();
164   private static final String STRING_PREVIEW = "stringPreview";
165   private static final Option PREVIEW_STRING_OPTION =
166       Option.builder(STRING_PREVIEW).required(false).hasArg(true)
167             .desc("Specify the substring for key for Options to validate")
168             .build();
169   private static final String KEY_204 = "key204";
170   private static final Option ICAP_204_KEY_OPTION =
171       Option.builder(KEY_204).required(false).hasArg(true)
172             .desc("Specify the key for 204 ICAP to validate").build();
173   private static final String STRING_204 = "string204";
174   private static final Option ICAP_204_STRING_OPTION =
175       Option.builder(STRING_204).required(false).hasArg(true)
176             .desc("Specify the substring for key for 204 ICAP to validate")
177             .build();
178   private static final String KEY_200 = "key200";
179   private static final Option ICAP_200_KEY_OPTION =
180       Option.builder(KEY_200).required(false).hasArg(true)
181             .desc("Specify the key for 200 ICAP to validate").build();
182   private static final String STRING_200 = "string200";
183   private static final Option ICAP_200_STRING_OPTION =
184       Option.builder(STRING_200).required(false).hasArg(true)
185             .desc("Specify the substring for key for 200 ICAP to validate")
186             .build();
187   private static final String STRING_HTTP = "stringHttp";
188   private static final Option HTTP_STRING_OPTION =
189       Option.builder(STRING_HTTP).required(false).hasArg(true)
190             .desc("Specify the substring for HTTP 200 ICAP status to validate")
191             .build();
192   private static final String LOGGER_ARG = "logger";
193   private static final String DEBUG_LEVEL = "DEBUG";
194   private static final String INFO_LEVEL = "INFO";
195   private static final String WARN_LEVEL = "WARN";
196   private static final String ERROR_LEVEL = "ERROR";
197   private static final Option LOGGER_OPTION =
198       Option.builder(LOGGER_ARG).required(false).hasArg(true).desc(
199           "Specify the level of log between " + DEBUG_LEVEL + " | " +
200           INFO_LEVEL + " | " + WARN_LEVEL + " | " + ERROR_LEVEL).build();
201 
202   private static final OptionGroup ERROR_OPTIONS =
203       new OptionGroup().addOption(ERROR_DELETE_OPTION)
204                        .addOption(ERROR_MOVE_OPTION)
205                        .addOption(ERROR_SEND_OPTION);
206   private static final OptionGroup SERVICE_OPTIONS =
207       new OptionGroup().addOption(SERVICE_OPTION).addOption(MODEL_OPTION);
208   private static final Options ICAP_OPTIONS =
209       new Options().addOption(FILE_OPTION).addOption(HOST_OPTION)
210                    .addOption(PORT_OPTION).addOptionGroup(SERVICE_OPTIONS)
211                    .addOption(PREVIEW_OPTION).addOption(BLOCK_OPTION)
212                    .addOption(RECEIVE_OPTION).addOption(MAX_SIZE_OPTION)
213                    .addOption(TIMEOUT_OPTION)
214                    .addOption(IGNORE_NETWORK_CONTINUE_OPTION)
215                    .addOption(IGNORE_TOO_BIG_FILE_CONTINUE_OPTION)
216                    .addOption(PREVIEW_KEY_OPTION)
217                    .addOption(PREVIEW_STRING_OPTION)
218                    .addOption(ICAP_200_KEY_OPTION)
219                    .addOption(ICAP_200_STRING_OPTION)
220                    .addOption(ICAP_204_KEY_OPTION)
221                    .addOption(ICAP_204_STRING_OPTION).addOption(LOGGER_OPTION)
222                    .addOption(HTTP_STRING_OPTION).addOptionGroup(ERROR_OPTIONS);
223   private static final Options ICAP_MODEL_OPTIONS =
224       new Options().addOption(PORT_OPTION).addOption(SERVICE_OPTION)
225                    .addOption(PREVIEW_OPTION).addOption(BLOCK_OPTION)
226                    .addOption(RECEIVE_OPTION).addOption(MAX_SIZE_OPTION)
227                    .addOption(TIMEOUT_OPTION)
228                    .addOption(IGNORE_NETWORK_CONTINUE_OPTION)
229                    .addOption(IGNORE_TOO_BIG_FILE_CONTINUE_OPTION)
230                    .addOption(PREVIEW_KEY_OPTION)
231                    .addOption(PREVIEW_STRING_OPTION)
232                    .addOption(ICAP_200_KEY_OPTION)
233                    .addOption(ICAP_200_STRING_OPTION)
234                    .addOption(ICAP_204_KEY_OPTION)
235                    .addOption(ICAP_204_STRING_OPTION).addOption(LOGGER_OPTION)
236                    .addOption(HTTP_STRING_OPTION).addOptionGroup(ERROR_OPTIONS);
237   public static final String SEPARATOR_SEND = "--";
238 
239   // Standard configuration
240   private String serverIP = null;
241   private int port = IcapClient.DEFAULT_ICAP_PORT;
242   private String icapService = null;
243   private IcapModel icapModel = null;
244   private String filepath = null;
245 
246   // Extra configuration
247   private int receiveLength = IcapClient.STD_RECEIVE_LENGTH;
248   private int sendLength = IcapClient.STD_SEND_LENGTH;
249   private int timeout = IcapClient.DEFAULT_TIMEOUT;
250   private String keyIcapPreview = null;
251   private String subStringFromKeyIcapPreview = null;
252   private String substringHttpStatus200 = null;
253   private String keyIcap200 = null;
254   private String subStringFromKeyIcap200 = null;
255   private String keyIcap204 = null;
256   private String subStringFromKeyIcap204 = null;
257   private long maxSize = Integer.MAX_VALUE;
258   private int stdPreviewSize = -1;
259   private String pathMoveError = null;
260   private boolean deleteOnError = false;
261   private boolean sendOnError = false;
262   private boolean ignoreNetworkError = false;
263   private boolean ignoreTooBigFileError = false;
264   private WaarpLogLevel logLevel = null;
265 
266   private Map<String, String> result = null;
267 
268   /**
269    * Private constructor
270    */
271   private IcapScanFile() {
272     // Empty
273   }
274 
275   /**
276    * Partial setter from source (not file, host, icapModel)
277    *
278    * @param from partial source
279    */
280   private IcapScanFile partialSetFrom(final IcapScanFile from) {
281     this.port = from.port;
282     this.icapService = from.icapService;
283     this.receiveLength = from.receiveLength;
284     this.sendLength = from.sendLength;
285     this.timeout = from.timeout;
286     this.keyIcapPreview = from.keyIcapPreview;
287     this.subStringFromKeyIcapPreview = from.subStringFromKeyIcapPreview;
288     this.substringHttpStatus200 = from.substringHttpStatus200;
289     this.keyIcap200 = from.keyIcap200;
290     this.subStringFromKeyIcap200 = from.subStringFromKeyIcap200;
291     this.keyIcap204 = from.keyIcap204;
292     this.subStringFromKeyIcap204 = from.subStringFromKeyIcap204;
293     this.maxSize = from.maxSize;
294     this.stdPreviewSize = from.stdPreviewSize;
295     this.pathMoveError = from.pathMoveError;
296     this.deleteOnError = from.deleteOnError;
297     this.sendOnError = from.sendOnError;
298     this.ignoreNetworkError = from.ignoreNetworkError;
299     this.ignoreTooBigFileError = from.ignoreTooBigFileError;
300     this.logLevel = from.getLogLevel();
301     return this;
302   }
303 
304   /**
305    * Print to standard output the help of this command
306    */
307   public static void printHelp() {
308     final HelpFormatter formatter = new HelpFormatter();
309     formatter.printHelp("IcapScanFile", ICAP_OPTIONS);
310   }
311 
312   /**
313    * If file argument are -file EICARTEST, then a EICAR test file will be
314    * sent.<br><br>
315    * "-file path_to_file <br>
316    * -to hostname <br>
317    * [-port port, default 1344] <br>
318    * -service name | -model name <br>
319    * [-previewSize size, default none] <br>
320    * [-blockSize size, default 8192] <br>
321    * [-receiveSize size, default 65536] <br>
322    * [-maxSize size, default MAX_INTEGER] <br>
323    * [-timeout in_ms, default equiv to 10 min] <br>
324    * [-errorMove path | -errorDelete | -sendOnError] <br>
325    * [-ignoreNetworkError] <br>
326    * [-ignoreTooBigFileError] <br>
327    * [-keyPreview key -stringPreview string, default none] <br>
328    * [-key204 key -string204 string, default none] <br>
329    * [-key200 key -string200 string, default none] <br>
330    * [-stringHttp string, default none] <br>
331    * [-logger DEBUG|INFO|WARN|ERROR, default none]"<br>
332    * <br>
333    *
334    * @param args must be already replaced values (getReplacedValue)
335    *
336    * @return the IcapScanFile
337    *
338    * @throws IcapException if an error occurs during argument parsing
339    */
340   public static IcapScanFile getIcapScanFileArgs(final String[] args)
341       throws IcapException {
342     if (args == null || args.length == 0) {
343       throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
344     }
345     String[] realArgs = args;
346     for (int i = 0; i < args.length; i++) {
347       if (SEPARATOR_SEND.equals(args[i])) {
348         realArgs = Arrays.copyOf(args, i);
349         break;
350       }
351     }
352     return getIcapScanFileArgs(realArgs, ICAP_OPTIONS);
353   }
354 
355   /**
356    * @param args must be already replaced values (getReplacedValue)
357    * @param options the options matcher to use
358    *
359    * @return the IcapScanFile
360    *
361    * @throws IcapException if an error occurs during argument parsing
362    */
363   private static IcapScanFile getIcapScanFileArgs(final String[] args,
364                                                   final Options options)
365       throws IcapException {
366     final IcapScanFile icapScanFile = new IcapScanFile();
367     final CommandLineParser parser = new DefaultParser();
368     try {
369       final CommandLine cmd = parser.parse(options, args, true);
370 
371       if (options != ICAP_MODEL_OPTIONS && cmd.hasOption(MODEL)) {
372         getModelParameters(icapScanFile, cmd);
373       } else {
374         icapScanFile.icapService = cmd.getOptionValue(SERVICE);
375       }
376       icapScanFile.filepath = cmd.getOptionValue(FILE);
377       icapScanFile.serverIP = cmd.getOptionValue(TO);
378       getPort(icapScanFile, cmd);
379       getNumbers(icapScanFile, cmd);
380       getOtherOptions(icapScanFile, cmd);
381       if (cmd.hasOption(LOGGER_ARG)) {
382         final String level =
383             cmd.getOptionValue(LOGGER_ARG).trim().toUpperCase();
384         if (DEBUG_LEVEL.equals(level)) {
385           icapScanFile.logLevel = WaarpLogLevel.DEBUG;
386         } else if (INFO_LEVEL.equals(level)) {
387           icapScanFile.logLevel = WaarpLogLevel.INFO;
388         } else if (WARN_LEVEL.equals(level)) {
389           icapScanFile.logLevel = WaarpLogLevel.WARN;
390         } else if (ERROR_LEVEL.equals(level)) {
391           icapScanFile.logLevel = WaarpLogLevel.ERROR;
392         } else {
393           logger.warn("Unknown log level {}", level);
394         }
395       }
396     } catch (final ParseException e) {
397       throw new IcapException("Parsing error", e,
398                               IcapError.ICAP_ARGUMENT_ERROR);
399     }
400     return icapScanFile;
401   }
402 
403   /**
404    * @param icapScanFile the original IcapScanFile
405    * @param cmd the command to parse
406    *
407    * @throws IcapException if an error occurs during argument parsing
408    */
409   private static void getModelParameters(final IcapScanFile icapScanFile,
410                                          final CommandLine cmd)
411       throws IcapException {
412     try {
413       icapScanFile.icapModel = IcapModel.valueOf(cmd.getOptionValue(MODEL));
414       final IcapScanFile modelIcapScanFile =
415           getIcapScanFileArgs(icapScanFile.icapModel.getDefaultArgs(),
416                               ICAP_MODEL_OPTIONS);
417       icapScanFile.partialSetFrom(modelIcapScanFile);
418     } catch (final IllegalArgumentException e) {
419       throw new IcapException("Parsing error", e,
420                               IcapError.ICAP_ARGUMENT_ERROR);
421     }
422   }
423 
424   /**
425    * Get the other options from command
426    *
427    * @param icapScanFile the current IcapScanFile
428    * @param cmd the command to parse from
429    */
430   private static void getOtherOptions(final IcapScanFile icapScanFile,
431                                       final CommandLine cmd) {
432     if (cmd.hasOption(ERROR_MOVE)) {
433       icapScanFile.pathMoveError = cmd.getOptionValue(ERROR_MOVE);
434     }
435     if (cmd.hasOption(ERROR_DELETE)) {
436       icapScanFile.deleteOnError = true;
437     }
438     if (cmd.hasOption(ERROR_SEND)) {
439       icapScanFile.sendOnError = true;
440     }
441     if (cmd.hasOption(IGNORE_NETWORK_CONTINUE)) {
442       icapScanFile.ignoreNetworkError = true;
443     }
444     if (cmd.hasOption(IGNORE_TOO_BIG_FILE_CONTINUE)) {
445       icapScanFile.ignoreTooBigFileError = true;
446     }
447     if (cmd.hasOption(KEY_PREVIEW)) {
448       icapScanFile.keyIcapPreview = cmd.getOptionValue(KEY_PREVIEW);
449     }
450     if (cmd.hasOption(STRING_PREVIEW)) {
451       icapScanFile.subStringFromKeyIcapPreview =
452           cmd.getOptionValue(STRING_PREVIEW);
453     }
454     if (cmd.hasOption(KEY_204)) {
455       icapScanFile.keyIcap204 = cmd.getOptionValue(KEY_204);
456     }
457     if (cmd.hasOption(STRING_204)) {
458       icapScanFile.subStringFromKeyIcap204 = cmd.getOptionValue(STRING_204);
459     }
460     if (cmd.hasOption(KEY_200)) {
461       icapScanFile.keyIcap200 = cmd.getOptionValue(KEY_200);
462     }
463     if (cmd.hasOption(STRING_200)) {
464       icapScanFile.subStringFromKeyIcap200 = cmd.getOptionValue(STRING_200);
465     }
466     if (cmd.hasOption(STRING_HTTP)) {
467       icapScanFile.substringHttpStatus200 = cmd.getOptionValue(STRING_HTTP);
468     }
469   }
470 
471   /**
472    * Check the numbers
473    *
474    * @param icapScanFile the IcapScanFile object
475    * @param cmd the command line to check On
476    *
477    * @throws IcapException
478    */
479   private static void getNumbers(final IcapScanFile icapScanFile,
480                                  final CommandLine cmd) throws IcapException {
481     try {
482       if (cmd.hasOption(PREVIEW_SIZE)) {
483         icapScanFile.stdPreviewSize =
484             Integer.parseInt(cmd.getOptionValue(PREVIEW_SIZE));
485         if (icapScanFile.stdPreviewSize < 0) {
486           throw new NumberFormatException("Preview size must be positive or 0");
487         }
488       }
489       if (cmd.hasOption(BLOCK_SIZE)) {
490         icapScanFile.sendLength =
491             Integer.parseInt(cmd.getOptionValue(BLOCK_SIZE));
492         if (icapScanFile.sendLength < IcapClient.MINIMAL_SIZE) {
493           throw new NumberFormatException(
494               "Block size must be greater than " + IcapClient.MINIMAL_SIZE);
495         }
496       }
497       if (cmd.hasOption(RECEIVE_SIZE)) {
498         icapScanFile.receiveLength =
499             Integer.parseInt(cmd.getOptionValue(RECEIVE_SIZE));
500         if (icapScanFile.receiveLength < IcapClient.MINIMAL_SIZE) {
501           throw new NumberFormatException(
502               "Receive size must be greater than " + IcapClient.MINIMAL_SIZE);
503         }
504       }
505       if (cmd.hasOption(MAX_SIZE)) {
506         icapScanFile.maxSize = Long.parseLong(cmd.getOptionValue(MAX_SIZE));
507         if (icapScanFile.maxSize < IcapClient.MINIMAL_SIZE) {
508           throw new NumberFormatException(
509               "Max file size must be greater than " + IcapClient.MINIMAL_SIZE);
510         }
511       }
512       if (cmd.hasOption(TIMEOUT_ARG)) {
513         icapScanFile.timeout =
514             Integer.parseInt(cmd.getOptionValue(TIMEOUT_ARG));
515         if (icapScanFile.timeout < IcapClient.MINIMAL_SIZE) {
516           throw new NumberFormatException(
517               "Timeout must be greater than " + IcapClient.MINIMAL_SIZE);
518         }
519       }
520     } catch (final NumberFormatException e) {
521       throw new IcapException("Incorrect Number Format", e,
522                               IcapError.ICAP_ARGUMENT_ERROR);
523     }
524   }
525 
526   /**
527    * Get the port
528    *
529    * @param icapScanFile the IcapScanFile object
530    * @param cmd the command line to check On
531    *
532    * @throws IcapException
533    */
534   private static void getPort(final IcapScanFile icapScanFile,
535                               final CommandLine cmd) throws IcapException {
536     try {
537       if (cmd.hasOption(PORT_FIELD)) {
538         icapScanFile.port = Integer.parseInt(cmd.getOptionValue(PORT_FIELD));
539         if (icapScanFile.port < 0) {
540           throw new NumberFormatException("Port must be positive");
541         }
542       }
543     } catch (final NumberFormatException e) {
544       throw new IcapException("Port incorrect", e,
545                               IcapError.ICAP_ARGUMENT_ERROR);
546     }
547   }
548 
549   /**
550    * Create the IcapClient according to IcapScanFile
551    *
552    * @param icapScanFile used to setup IcapClient
553    *
554    * @return the IcapClient
555    */
556   public static IcapClient getIcapClient(final IcapScanFile icapScanFile) {
557     if (icapScanFile == null) {
558       throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
559     }
560     final IcapClient icapClient =
561         new IcapClient(icapScanFile.serverIP, icapScanFile.port,
562                        icapScanFile.icapService, icapScanFile.stdPreviewSize);
563     icapClient.setSendLength(icapScanFile.sendLength)
564               .setReceiveLength(icapScanFile.receiveLength)
565               .setMaxSize(icapScanFile.maxSize).setTimeout(icapScanFile.timeout)
566               .setKeyIcapPreview(icapScanFile.keyIcapPreview)
567               .setSubStringFromKeyIcapPreview(
568                   icapScanFile.subStringFromKeyIcapPreview)
569               .setKeyIcap204(icapScanFile.keyIcap204)
570               .setSubStringFromKeyIcap204(icapScanFile.subStringFromKeyIcap204)
571               .setKeyIcap200(icapScanFile.keyIcap200)
572               .setSubStringFromKeyIcap200(icapScanFile.subStringFromKeyIcap200)
573               .setSubstringHttpStatus200(icapScanFile.substringHttpStatus200);
574     return icapClient;
575   }
576 
577   /**
578    * Finalize the current ICAP scan when an error occurs
579    *
580    * @param icapScanFile used to get options for Error tasks
581    *
582    * @throws IOException if an error occurs during post error tasks
583    */
584   public static void finalizeOnError(final IcapScanFile icapScanFile)
585       throws IOException {
586     if (icapScanFile == null) {
587       throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
588     }
589     logger.error("Scan is incorrect: {}",
590                  icapScanFile.getResult() == null? "No Result" :
591                      icapScanFile.getResult());
592     if (icapScanFile.deleteOnError) {
593       final File file = new File(icapScanFile.filepath);
594       if (!file.delete()) {
595         logger.error("File cannot be deleted!");
596         throw new IOException("File cannot be deleted!");
597       } else {
598         logger.warn("File is deleted");
599       }
600     } else if (icapScanFile.pathMoveError != null) {
601       final File file = new File(icapScanFile.filepath);
602       final File dir = new File(icapScanFile.pathMoveError);
603       if (dir.exists()) {
604         if (dir.isDirectory()) {
605           try {
606             final File to = new File(dir, file.getName());
607             FileUtils.copy(file, to, true, false);
608             logger.warn("File is moved to " + to.getAbsolutePath());
609           } catch (final Reply550Exception e) {
610             logger.error("Cannot move to directory: {}", e.getMessage());
611             throw new IOException("Cannot move to directory", e);
612           }
613         } else {
614           logger.error("Move path already exists and is not a directory");
615           throw new IOException(
616               "Move path already exists and is not a directory");
617         }
618       } else {
619         if (dir.getParentFile().isDirectory()) {
620           try {
621             FileUtils.copy(file, dir, true, false);
622             logger.warn("File is moved to " + dir.getAbsolutePath());
623           } catch (final Reply550Exception e) {
624             logger.error("Cannot move to file: {}", e.getMessage());
625           }
626         } else {
627           logger.error("Move path is not a directory or existing sub-path");
628           throw new IOException(
629               "Move path is not a directory or existing sub-path");
630         }
631       }
632     }
633   }
634 
635   /**
636    * @return the file path
637    */
638   public final String getFilePath() {
639     return filepath;
640   }
641 
642   /**
643    * @param filePath the file path to use
644    *
645    * @return This
646    */
647   public final IcapScanFile setFilePath(final String filePath) {
648     this.filepath = filePath;
649     return this;
650   }
651 
652   /**
653    * @return the server IP
654    */
655   public final String getServerIP() {
656     return serverIP;
657   }
658 
659   /**
660    * @param serverIP the server IP to use
661    *
662    * @return This
663    */
664   public final IcapScanFile setServerIP(final String serverIP) {
665     this.serverIP = serverIP;
666     return this;
667   }
668 
669   /**
670    * @return the Icap Model if any (null if none)
671    */
672   public final IcapModel getIcapModel() {
673     return icapModel;
674   }
675 
676   /**
677    * @return the path to move in error or null
678    */
679   public final String getPathMoveError() {
680     return pathMoveError;
681   }
682 
683   /**
684    * @return True if the file will be deleted in error
685    */
686   public final boolean isDeleteOnError() {
687     return deleteOnError;
688   }
689 
690   /**
691    * @return True if the send on error option is set
692    */
693   public final boolean isSendOnError() {
694     return sendOnError;
695   }
696 
697   /**
698    * @return True if a network error option is set to ignore such
699    */
700   public final boolean isIgnoreNetworkError() {
701     return ignoreNetworkError;
702   }
703 
704   /**
705    * @return True if a too big file error option is set to ignore such
706    */
707   public final boolean isIgnoreTooBigFileError() {
708     return ignoreTooBigFileError;
709   }
710 
711   /**
712    * @return the Logger Level desired during ICAP operation or null if none
713    */
714   public final WaarpLogLevel getLogLevel() {
715     return logLevel;
716   }
717 
718   /**
719    * @return the Map of key from ICAP if any (null if none)
720    */
721   public final Map<String, String> getResult() {
722     return result;
723   }
724 
725   /**
726    * If file argument are -file EICARTEST, then a EICAR test file will be
727    * sent.<br><br>
728    * "-file path_to_file <br>
729    * -to hostname <br>
730    * [-port port, default 1344] <br>
731    * -service name | -model name <br>
732    * [-previewSize size, default none] <br>
733    * [-blockSize size, default 8192] <br>
734    * [-receiveSize size, default 65536] <br>
735    * [-maxSize size, default MAX_INTEGER] <br>
736    * [-timeout in_ms, default equiv to 10 min] <br>
737    * [-errorMove path | -errorDelete | -sendOnError] <br>
738    * [-ignoreNetworkError] <br>
739    * [-ignoreTooBigFileError] <br>
740    * [-keyPreview key -stringPreview string, default none] <br>
741    * [-key204 key -string204 string, default none] <br>
742    * [-key200 key -string200 string, default none] <br>
743    * [-stringHttp string, default none] <br>
744    * [-logger DEBUG|INFO|WARN|ERROR, default none]"<br>
745    * <br>
746    * <br>
747    * Exit with values:<br>
748    * <ul>
749    *   <li>0: Scan OK</li>
750    *   <li>1: Bad arguments</li>
751    *   <li>2: ICAP protocol error</li>
752    *   <li>3: Network error</li>
753    *   <li>4: Scan KO</li>
754    *   <li>5: Scan KO but post action required in error</li>
755    * </ul>
756    *
757    * @param args to get parameters from
758    */
759   public static void main(final String[] args) {
760     WaarpLoggerFactory.setDefaultFactoryIfNotSame(
761         new WaarpSlf4JLoggerFactory(null));
762     System.exit(scanFile(args));
763   }
764 
765   /**
766    * If file argument are -file EICARTEST, then a EICAR test file will be
767    * sent.<br><br>
768    * "-file path_to_file <br>
769    * -to hostname <br>
770    * [-port port, default 1344] <br>
771    * Not -service name | -model name (should be set through Model)<br>
772    * [-previewSize size, default none] <br>
773    * [-blockSize size, default 8192] <br>
774    * [-receiveSize size, default 65536] <br>
775    * [-maxSize size, default MAX_INTEGER] <br>
776    * [-timeout in_ms, default equiv to 10 min] <br>
777    * [-errorMove path | -errorDelete | -sendOnError] <br>
778    * [-ignoreNetworkError] <br>
779    * [-ignoreTooBigFileError] <br>
780    * [-keyPreview key -stringPreview string, default none] <br>
781    * [-key204 key -string204 string, default none] <br>
782    * [-key200 key -string200 string, default none] <br>
783    * [-stringHttp string, default none] <br>
784    * [-logger DEBUG|INFO|WARN|ERROR, default none]"<br>
785    * <br>
786    *
787    * @param model default model to apply in addition to parameters
788    * @param args to get parameters from
789    *
790    * @return 0 if OK, 1 if arguments are incorrect, 2 if an issue occurs
791    *     during ICAP protool, 3 if a nework error occurs, 4 if scan KO, 5 if
792    *     the post actions are in error while scan is KO
793    */
794   public static int scanFile(final String[] model, final String[] args) {
795     if (model == null || model.length == 0 || args == null ||
796         args.length == 0) {
797       throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
798     }
799     final String[] realArgs = Arrays.copyOf(model, model.length + args.length);
800     System.arraycopy(args, 0, realArgs, model.length, args.length);
801     return scanFile(realArgs);
802   }
803 
804   /**
805    * If file argument are -file EICARTEST, then a EICAR test file will be
806    * sent.<br><br>
807    * "-file path_to_file <br>
808    * -to hostname <br>
809    * [-port port, default 1344] <br>
810    * -service name | -model name
811    * [-previewSize size, default none] <br>
812    * [-blockSize size, default 8192] <br>
813    * [-receiveSize size, default 65536] <br>
814    * [-maxSize size, default MAX_INTEGER] <br>
815    * [-timeout in_ms, default equiv to 10 min] <br>
816    * [-errorMove path | -errorDelete | -sendOnError] <br>
817    * [-ignoreNetworkError] <br>
818    * [-ignoreTooBigFileError] <br>
819    * [-keyPreview key -stringPreview string, default none] <br>
820    * [-key204 key -string204 string, default none] <br>
821    * [-key200 key -string200 string, default none] <br>
822    * [-stringHttp string, default none] <br>
823    * [-logger DEBUG|INFO|WARN|ERROR, default none]"<br>
824    * <br>
825    *
826    * @param args to get parameters from
827    *
828    * @return 0 if OK, 1 if arguments are incorrect, 2 if an issue occurs
829    *     during ICAP protool, 3 if a nework error occurs, 4 if scan KO, 5 if
830    *     the post actions are in error while scan is KO
831    */
832   public static int scanFile(final String[] args) {
833     if (args == null || args.length == 0) {
834       throw new IllegalArgumentException(ARGUMENTS_CANNOT_BE_EMPTY_OR_NULL);
835     }
836     WaarpLoggerFactory.setDefaultFactoryIfNotSame(
837         new WaarpSlf4JLoggerFactory(null));
838     final IcapScanFile icapScanFile;
839     try {
840       icapScanFile = getIcapScanFileArgs(args);
841     } catch (final IcapException e) {
842       printHelp();
843       logger.error("Arguments are incorrect: {}", e.getMessage());
844       return STATUS_BAD_ARGUMENT;
845     }
846     try {
847       return icapScanFile.scanFile();
848     } catch (final IcapException e) {
849       logger.error("Error during scan: {}", e.getMessage());
850       if (e.getError() == IcapError.ICAP_CANT_CONNECT ||
851           e.getError() == IcapError.ICAP_NETWORK_ERROR ||
852           e.getError() == IcapError.ICAP_TIMEOUT_ERROR) {
853         if (icapScanFile.ignoreNetworkError) {
854           return STATUS_OK;
855         }
856         return STATUS_NETWORK_ISSUE;
857       }
858       if (e.getError() == IcapError.ICAP_ARGUMENT_ERROR ||
859           e.getError() == IcapError.ICAP_FILE_LENGTH_ERROR) {
860         if (icapScanFile.ignoreTooBigFileError &&
861             e.getError() == IcapError.ICAP_FILE_LENGTH_ERROR) {
862           return STATUS_OK;
863         }
864         return STATUS_BAD_ARGUMENT;
865       }
866       return STATUS_ICAP_ISSUE;
867     } catch (final IOException e) {
868       logger.error("Moving file is in error: {}", e.getMessage());
869       return STATUS_KO_SCAN_POST_ACTION_ERROR;
870     }
871   }
872 
873   /**
874    * Scan a file through ICAP antivirus server from IcapScanFile
875    *
876    * @return 0 if scan is OK, 1 if the file is infected
877    *
878    * @throws IcapException if an error occurs during connection or ICAP protocol
879    * @throws IOException if the file is infected and a post error action is
880    *     in error
881    */
882   public final int scanFile() throws IcapException, IOException {
883     final WaarpLogLevel waarpLogLevel = getLogLevel();
884     WaarpLogLevel oldLevel = null;
885     try {
886       if (waarpLogLevel != null) {
887         if (logger.isTraceEnabled()) {
888           oldLevel = WaarpLogLevel.TRACE;
889         } else if (logger.isDebugEnabled()) {
890           oldLevel = WaarpLogLevel.DEBUG;
891         } else if (logger.isInfoEnabled()) {
892           oldLevel = WaarpLogLevel.INFO;
893         } else if (logger.isWarnEnabled()) {
894           oldLevel = WaarpLogLevel.WARN;
895         } else if (logger.isErrorEnabled()) {
896           oldLevel = WaarpLogLevel.ERROR;
897         }
898         WaarpLoggerFactory.setLogLevel(waarpLogLevel);
899       }
900       final IcapClient icapClient = getIcapClient(this);
901       if (icapClient.scanFile(filepath)) {
902         icapClient.close();
903         logger.info("File is OK");
904         return STATUS_OK;
905       } else {
906         icapClient.close();
907         result = icapClient.getFinalResult();
908         finalizeOnError(this);
909         return STATUS_KO_SCAN;
910       }
911     } finally {
912       if (waarpLogLevel != null && oldLevel != null) {
913         WaarpLoggerFactory.setLogLevel(oldLevel);
914       }
915     }
916   }
917 }