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 com.google.common.io.Files;
24  import org.waarp.common.logging.WaarpLogger;
25  import org.waarp.common.logging.WaarpLoggerFactory;
26  import org.waarp.common.utility.ParametersChecker;
27  import org.waarp.common.utility.WaarpStringUtils;
28  
29  import java.io.ByteArrayInputStream;
30  import java.io.Closeable;
31  import java.io.File;
32  import java.io.FileInputStream;
33  import java.io.FileNotFoundException;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.io.OutputStream;
37  import java.io.UnsupportedEncodingException;
38  import java.net.ConnectException;
39  import java.net.Socket;
40  import java.net.SocketTimeoutException;
41  import java.net.URLEncoder;
42  import java.util.Arrays;
43  import java.util.HashMap;
44  import java.util.Map;
45  
46  import static org.waarp.common.file.filesystembased.FilesystemBasedFileImpl.*;
47  
48  /**
49   * The IcapClient allows to do 3 actions:</br>
50   * <ul>
51   *   <li>connect(): which allows to initialize the connection with the ICAP
52   *   server</li>
53   *   <li>close(): forces the client to disconnect from the ICAP server</li>
54   *   <li>scanFile(path): send a file for a scan by the ICAP server</li>
55   * </ul>
56   * </br>
57   * This code is inspired from 2 sources:</br>
58   * <ul>
59   *   <li>https://github.com/Baekalfen/ICAP-avscan</li>
60   *   <li>https://github.com/claudineyns/icap-client</li>
61   * </ul>
62   * </br>
63   * This reflects the RFC 3507 and errata as of 2010/04/17.
64   */
65  public class IcapClient implements Closeable {
66    /**
67     * Default ICAP port
68     */
69    public static final int DEFAULT_ICAP_PORT = 1344;
70    private static final WaarpLogger logger =
71        WaarpLoggerFactory.getLogger(IcapClient.class);
72  
73    static final int STD_RECEIVE_LENGTH = 64 * 1024;
74    static final int STD_SEND_LENGTH = 8192;
75    static final int DEFAULT_TIMEOUT = 10 * 60 * 60000;// 10 min
76    public static final String VERSION = "ICAP/1.0";
77    private static final String USER_AGENT = "Waarp ICAP Client/1.0";
78    public static final String TERMINATOR = "\r\n";
79    public static final String ICAP_TERMINATOR = TERMINATOR + TERMINATOR;
80    public static final String HTTP_TERMINATOR = "0" + TERMINATOR + TERMINATOR;
81    private static final String STATUS_CODE = "StatusCode";
82    private static final String PREVIEW = "Preview";
83    private static final String OPTIONS = "OPTIONS";
84    private static final String HOST_HEADER = "Host: ";
85    private static final String USER_AGENT_HEADER = "User-Agent: ";
86    private static final String RESPMOD = "RESPMOD";
87    private static final String ENCAPSULATED_NULL_BODY =
88        "Encapsulated: null-body=0";
89    static final int MINIMAL_SIZE = 100;
90    private static final String GET_REQUEST = "GET /";
91    private static final String INCOMPATIBLE_ARGUMENT = "Incompatible argument";
92    private static final String TIMEOUT_OCCURS_WITH_THE_SERVER =
93        "Timeout occurs with the Server";
94    private static final String TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE =
95        "Timeout occurs with the Server {}:{} since {}";
96    public static final String EICARTEST = "EICARTEST";
97  
98    // Standard configuration
99    private final String serverIP;
100   private final int port;
101   private final String icapService;
102   private final int setPreviewSize;
103 
104   // Extra configuration
105   private int receiveLength = STD_RECEIVE_LENGTH;
106   private int sendLength = STD_SEND_LENGTH;
107   private String keyIcapPreview = null;
108   private String subStringFromKeyIcapPreview = null;
109   private String substringHttpStatus200 = null;
110   private String keyIcap200 = null;
111   private String subStringFromKeyIcap200 = null;
112   private String keyIcap204 = null;
113   private String subStringFromKeyIcap204 = null;
114   private long maxSize = Integer.MAX_VALUE;
115   private int timeout = DEFAULT_TIMEOUT;
116   private int stdPreviewSize = -1;
117 
118   // Accessibe data
119   private Map<String, String> finalResult = null;
120 
121   // Internal data
122   private Socket client;
123   private OutputStream out;
124   InputStream in;
125   private int offset;
126 
127   /**
128    * This creates the ICAP client without connecting immediately to the ICAP
129    * server. When the ICAP client will connect, it will ask for the preview
130    * size to the ICAP Server.
131    *
132    * @param serverIP The IP address to connect to.
133    * @param port The port in the host to use.
134    * @param icapService The service to use (fx "avscan").
135    */
136   public IcapClient(final String serverIP, final int port,
137                     final String icapService) {
138     this(serverIP, port, icapService, -1);
139   }
140 
141   /**
142    * This creates the ICAP client without connecting immediately to the ICAP
143    * server. When the ICAP client will connect, it will not ask for the preview
144    * size to the ICAP Server but uses the default specified value.
145    *
146    * @param serverIP The IP address to connect to.
147    * @param port The port in the host to use.
148    * @param icapService The service to use (fx "avscan").
149    * @param previewSize Amount of bytes to  send as preview.
150    */
151   public IcapClient(final String serverIP, final int port,
152                     final String icapService, final int previewSize) {
153     if (ParametersChecker.isEmpty(icapService)) {
154       throw new IllegalArgumentException("IcapService must not be empty");
155     }
156     this.icapService = icapService;
157     if (ParametersChecker.isEmpty(serverIP)) {
158       throw new IllegalArgumentException("Server IP must not be empty");
159     }
160     this.serverIP = serverIP;
161     if (port <= 0) {
162       this.port = DEFAULT_ICAP_PORT;
163     } else {
164       this.port = port;
165     }
166     this.setPreviewSize = previewSize;
167     this.stdPreviewSize = Math.max(0, previewSize);
168   }
169 
170   /**
171    * Try to connect to the server and if the preview size is not specified,
172    * it will also resolve the options of the ICAP server.</br>
173    *
174    * If the client is still connected, it will first disconnect before
175    * reconnecting to the ICAP Server.</br>
176    *
177    * Note that every attempts of connection will retry to issue an OPTIONS
178    * request if necessary (if preview size is not set to a fixed value already).
179    *
180    * @throws IcapException if an issue occurs during the connection or
181    *     response (the connection is already closed)
182    */
183   public final IcapClient connect() throws IcapException {
184     if (finalResult != null) {
185       finalResult.clear();
186       finalResult = null;
187     }
188     if (client != null) {
189       close();
190     }
191     logger.debug("Try connect to {}:{} service {}", serverIP, port,
192                  icapService);
193     try {
194       // Initialize connection
195       client = new Socket(serverIP, port);
196       client.setReuseAddress(true);
197       client.setKeepAlive(true);
198       client.setSoTimeout(timeout);
199       client.setTcpNoDelay(false);
200       // Opening out stream
201       out = client.getOutputStream();
202       // Opening in stream
203       in = client.getInputStream();
204       if (setPreviewSize < 0) {
205         getFromServerPreviewSize();
206       }
207       logger.debug("Connected with Preview Size = {}", stdPreviewSize);
208       return this;
209     } catch (final SocketTimeoutException e) {
210       close();
211       logger.error(TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE, serverIP, port,
212                    e.getMessage());
213       throw new IcapException(TIMEOUT_OCCURS_WITH_THE_SERVER, e,
214                               IcapError.ICAP_TIMEOUT_ERROR);
215     } catch (final ConnectException e) {
216       close();
217       logger.error("Could not connect to server {}:{} since {}", serverIP, port,
218                    e.getMessage());
219       throw new IcapException("Could not connect with the server", e,
220                               IcapError.ICAP_CANT_CONNECT);
221     } catch (final IOException e) {
222       close();
223       logger.error("Could not connect to server {}:{} since {}", serverIP, port,
224                    e.getMessage());
225       throw new IcapException("Could not connect with the server", e,
226                               IcapError.ICAP_NETWORK_ERROR);
227     } catch (final IcapException e) {
228       close();
229       throw e;
230     }
231   }
232 
233   /**
234    * Get the Preview Size from the SERVER using ICAP OPTIONS command
235    *
236    * @throws IcapException
237    */
238   private void getFromServerPreviewSize() throws IcapException {
239     // Check the preview size from the ICAP Server response to OPTIONS
240     final String parseMe = getOptions();
241     finalResult = parseHeader(parseMe);
242     if (checkAgainstIcapHeader(finalResult, STATUS_CODE, "200", false)) {
243       final String tempString = finalResult.get(PREVIEW);
244       if (tempString != null) {
245         stdPreviewSize = Integer.parseInt(tempString);
246         if (stdPreviewSize < 0) {
247           stdPreviewSize = 0;
248         }
249         if (!checkAgainstIcapHeader(finalResult, keyIcapPreview,
250                                     subStringFromKeyIcapPreview, true)) {
251           close();
252           logger.error("Could not validate preview from server");
253           throw new IcapException("Could not validate preview from server",
254                                   IcapError.ICAP_SERVER_MISSING_INFO);
255         }
256       } else {
257         close();
258         logger.error("Could not get preview size from server");
259         throw new IcapException("Could not get preview size from server",
260                                 IcapError.ICAP_SERVER_MISSING_INFO);
261       }
262     } else {
263       close();
264       logger.error("Could not get options from server {}:{} service {}",
265                    serverIP, port, icapService);
266       throw new IcapException("Could not get options from server",
267                               IcapError.ICAP_SERVER_MISSING_INFO);
268     }
269   }
270 
271   @Override
272   public final void close() {
273     if (client != null) {
274       try {
275         client.close();
276       } catch (final IOException ignored) {
277         // Nothing
278       }
279       client = null;
280     }
281     if (in != null) {
282       try {
283         in.close();
284       } catch (final IOException ignored) {
285         // Nothing
286       }
287       in = null;
288     }
289     if (out != null) {
290       try {
291         out.close();
292       } catch (final IOException ignored) {
293         // Nothing
294       }
295       out = null;
296     }
297   }
298 
299   /**
300    * Given a file path, it will send the file to the server and return true,
301    * if the server accepts the file. Visa-versa, false if the server rejects
302    * it.</br>
303    *
304    * Note that if the client is not connected, it will first call connect().
305    *
306    * @param filename Relative or absolute file path to a file. If filename is
307    *     EICARTEST, then a build on the fly EICAR test file is sent.
308    *
309    * @return Returns true when no infection is found.
310    *
311    * @throws IcapException if an error occurs (network, file reading,
312    *     bad headers)
313    */
314   public final boolean scanFile(final String filename) throws IcapException {
315     if (ParametersChecker.isEmpty(filename)) {
316       throw new IllegalArgumentException("Filename must not be empty");
317     }
318     if (client == null) {
319       connect();
320     }
321     if (finalResult != null) {
322       finalResult.clear();
323       finalResult = null;
324     }
325     InputStream inputStream = null;
326     final long length;
327     if (EICARTEST.equals(filename)) {
328       // Special file to test from EICAR Test file
329       final ClassLoader classLoader = IcapClient.class.getClassLoader();
330       final File fileSrc1 =
331           new File(classLoader.getResource("eicar.com-part1.txt").getFile());
332       final File fileSrc2 =
333           new File(classLoader.getResource("eicar.com-part2.txt").getFile());
334       if (fileSrc1.exists() && fileSrc2.exists()) {
335         try {
336           final byte[] array1 = Files.toByteArray(fileSrc1);
337           final byte[] array2 = Files.toByteArray(fileSrc2);
338           final byte[] array =
339               Arrays.copyOf(array1, array1.length + array2.length);
340           System.arraycopy(array2, 0, array, array1.length, array2.length);
341           inputStream = new ByteArrayInputStream(array);
342           length = array.length;
343         } catch (final IOException e) {
344           logger.error("File EICAR TEST does not exist: {}", e.getMessage());
345           throw new IcapException("File EICAR TEST cannot be found",
346                                   IcapError.ICAP_ARGUMENT_ERROR);
347         }
348       } else {
349         logger.error("File EICAR TEST does not exist");
350         throw new IcapException("File EICAR TEST cannot be found",
351                                 IcapError.ICAP_ARGUMENT_ERROR);
352       }
353     } else {
354       final File file = new File(filename);
355       if (!canRead(file)) {
356         logger.error("File does not exist: {}", file.getAbsolutePath());
357         throw new IcapException(
358             "File cannot be found: " + file.getAbsolutePath(),
359             IcapError.ICAP_ARGUMENT_ERROR);
360       }
361       length = file.length();
362       if (length > maxSize) {
363         logger.error("File size {} exceed limit of {}: {}", length, maxSize,
364                      file.getAbsolutePath());
365         throw new IcapException(
366             "File exceed limit size: " + file.getAbsolutePath(),
367             IcapError.ICAP_FILE_LENGTH_ERROR);
368       }
369       try {
370         inputStream = new FileInputStream(file);
371       } catch (final FileNotFoundException e) {
372         logger.error("Could not find file {} since {}", filename,
373                      e.getMessage());
374         throw new IcapException("File cannot be found: " + filename, e,
375                                 IcapError.ICAP_ARGUMENT_ERROR);
376       }
377     }
378     try {
379       return scanFile(filename, inputStream, length);
380     } finally {
381       if (inputStream != null) {
382         try {
383           inputStream.close();
384         } catch (final IOException ignored) {
385           // Nothing
386         }
387       }
388       close();
389     }
390   }
391 
392   /**
393    * @return the Server IP
394    */
395   public final String getServerIP() {
396     return serverIP;
397   }
398 
399   /**
400    * @return the port
401    */
402   public final int getPort() {
403     return port;
404   }
405 
406   /**
407    * @return the ICAP service
408    */
409   public final String getIcapService() {
410     return icapService;
411   }
412 
413   /**
414    * @return the current Preview size
415    */
416   public final int getPreviewSize() {
417     return stdPreviewSize;
418   }
419 
420   /**
421    * @param previewSize the receive length to set
422    *
423    * @return This
424    */
425   public final IcapClient setPreviewSize(final int previewSize) {
426     if (previewSize < 0) {
427       logger.error(INCOMPATIBLE_ARGUMENT);
428       throw new IllegalArgumentException("Preview cannot be 0 or positive");
429     }
430     this.stdPreviewSize = previewSize;
431     return this;
432   }
433 
434   /**
435    * @return the current Receive length
436    */
437   public final int getReceiveLength() {
438     return receiveLength;
439   }
440 
441   /**
442    * @param receiveLength the receive length to set
443    *
444    * @return This
445    */
446   public final IcapClient setReceiveLength(final int receiveLength) {
447     if (receiveLength < MINIMAL_SIZE) {
448       logger.error(INCOMPATIBLE_ARGUMENT);
449       throw new IllegalArgumentException(
450           "Receive length cannot be less than " + MINIMAL_SIZE);
451     }
452     this.receiveLength = receiveLength;
453     return this;
454   }
455 
456   /**
457    * @return the current Send length
458    */
459   public final int getSendLength() {
460     return sendLength;
461   }
462 
463   /**
464    * @param sendLength the send length to set
465    *
466    * @return This
467    */
468   public final IcapClient setSendLength(final int sendLength) {
469     if (sendLength < MINIMAL_SIZE) {
470       logger.error(INCOMPATIBLE_ARGUMENT);
471       throw new IllegalArgumentException(
472           "Send length cannot be less than " + MINIMAL_SIZE);
473     }
474     this.sendLength = sendLength;
475     return this;
476   }
477 
478   /**
479    * @return the current max file size (default being Integer.MAX_VALUE)
480    */
481   public final long getMaxSize() {
482     return maxSize;
483   }
484 
485   /**
486    * @param maxSize the maximum file size to set
487    *
488    * @return This
489    */
490   public final IcapClient setMaxSize(final long maxSize) {
491     if (maxSize < MINIMAL_SIZE) {
492       logger.error(INCOMPATIBLE_ARGUMENT);
493       throw new IllegalArgumentException(
494           "Maximum file size length cannot be less than " + MINIMAL_SIZE);
495     }
496     this.maxSize = maxSize;
497     return this;
498   }
499 
500   /**
501    * @return the current time out for connection
502    */
503   public final long getTimeout() {
504     return timeout;
505   }
506 
507   /**
508    * @param timeout the timeout to use on connection
509    *
510    * @return This
511    */
512   public final IcapClient setTimeout(final int timeout) {
513     this.timeout = timeout;
514     return this;
515   }
516 
517   /**
518    * @return the current key in ICAP headers to find with 200 status in PREVIEW
519    *     (or null if none)
520    */
521   public final String getKeyIcapPreview() {
522     return keyIcapPreview;
523   }
524 
525   /**
526    * @param keyIcapPreview the key in ICAP headers to find with 200 status in
527    *     PREVIEW (or null if none)
528    *
529    * @return This
530    */
531   public final IcapClient setKeyIcapPreview(final String keyIcapPreview) {
532     if (ParametersChecker.isEmpty(keyIcapPreview)) {
533       this.keyIcapPreview = null;
534     } else {
535       this.keyIcapPreview = keyIcapPreview;
536     }
537     return this;
538   }
539 
540   /**
541    * @return the current subString to find in key ICAP header with 200 status
542    *     in PREVIEW (or null if none)
543    */
544   public final String getSubStringFromKeyIcapPreview() {
545     return subStringFromKeyIcapPreview;
546   }
547 
548   /**
549    * @param subStringFromKeyIcapPreview the subString to find in key ICAP header
550    *     with 200 status in PREVIEW (or null if none)
551    *
552    * @return This
553    */
554   public final IcapClient setSubStringFromKeyIcapPreview(
555       final String subStringFromKeyIcapPreview) {
556     if (ParametersChecker.isEmpty(subStringFromKeyIcapPreview)) {
557       this.subStringFromKeyIcapPreview = null;
558     } else {
559       this.subStringFromKeyIcapPreview = subStringFromKeyIcapPreview;
560     }
561     return this;
562   }
563 
564   /**
565    * @return the current subString to find in Http with 200 status
566    *     (or null if none)
567    */
568   public final String getSubstringHttpStatus200() {
569     return substringHttpStatus200;
570   }
571 
572   /**
573    * @param substringHttpStatus200 the subString to find in Http with 200 status
574    *     (or null if none)
575    *
576    * @return This
577    */
578   public final IcapClient setSubstringHttpStatus200(
579       final String substringHttpStatus200) {
580     if (ParametersChecker.isEmpty(substringHttpStatus200)) {
581       this.substringHttpStatus200 = null;
582     } else {
583       this.substringHttpStatus200 = substringHttpStatus200;
584     }
585     return this;
586   }
587 
588   /**
589    * @return the current key in ICAP headers to find with 200 status
590    *     (or null if none)
591    */
592   public final String getKeyIcap200() {
593     return keyIcap200;
594   }
595 
596   /**
597    * @param keyIcap200 the key in ICAP headers to find with 200 status
598    *     (or null if none)
599    *
600    * @return This
601    */
602   public final IcapClient setKeyIcap200(final String keyIcap200) {
603     if (ParametersChecker.isEmpty(keyIcap200)) {
604       this.keyIcap200 = null;
605     } else {
606       this.keyIcap200 = keyIcap200;
607     }
608     return this;
609   }
610 
611   /**
612    * @return the current subString to find in key ICAP header with 200 status
613    *     (or null if none)
614    */
615   public final String getSubStringFromKeyIcap200() {
616     return subStringFromKeyIcap200;
617   }
618 
619   /**
620    * @param subStringFromKeyIcap200 the subString to find in key ICAP header with 200 status
621    *     (or null if none)
622    *
623    * @return This
624    */
625   public final IcapClient setSubStringFromKeyIcap200(
626       final String subStringFromKeyIcap200) {
627     if (ParametersChecker.isEmpty(subStringFromKeyIcap200)) {
628       this.subStringFromKeyIcap200 = null;
629     } else {
630       this.subStringFromKeyIcap200 = subStringFromKeyIcap200;
631     }
632     return this;
633   }
634 
635   /**
636    * @return the current key in ICAP headers to find with 204 status
637    *     (or null if none)
638    */
639   public final String getKeyIcap204() {
640     return keyIcap204;
641   }
642 
643   /**
644    * @param keyIcap204 the key in ICAP headers to find with 204 status
645    *     (or null if none)
646    *
647    * @return This
648    */
649   public final IcapClient setKeyIcap204(final String keyIcap204) {
650     if (ParametersChecker.isEmpty(keyIcap204)) {
651       this.keyIcap204 = null;
652     } else {
653       this.keyIcap204 = keyIcap204;
654     }
655     return this;
656   }
657 
658   /**
659    * @return the current subString to find in key ICAP header with 204 status
660    *     (or null if none)
661    */
662   public final String getSubStringFromKeyIcap204() {
663     return subStringFromKeyIcap204;
664   }
665 
666   /**
667    * @param subStringFromKeyIcap204 the subString to find in key ICAP header with 204 status
668    *     (or null if none)
669    *
670    * @return This
671    */
672   public final IcapClient setSubStringFromKeyIcap204(
673       final String subStringFromKeyIcap204) {
674     if (ParametersChecker.isEmpty(subStringFromKeyIcap204)) {
675       this.subStringFromKeyIcap204 = null;
676     } else {
677       this.subStringFromKeyIcap204 = subStringFromKeyIcap204;
678     }
679     return this;
680   }
681 
682   /**
683    * @return the current map of result (null if none)
684    */
685   public final Map<String, String> getFinalResult() {
686     return finalResult;
687   }
688 
689   /**
690    * Automatically asks for the servers available options and returns the raw
691    * response as a String.
692    *
693    * @return String of the servers response
694    *
695    * @throws IcapException if an error occurs (network, bad headers)
696    */
697   private final String getOptions() throws IcapException {
698     // Send OPTIONS header and receive response
699     // Sending
700     final StringBuilder builder = new StringBuilder();
701     addIcapUri(builder, OPTIONS);
702     final String requestHeader =
703         builder.append(ENCAPSULATED_NULL_BODY).append(ICAP_TERMINATOR)
704                .toString();
705 
706     sendString(requestHeader, true);
707     // Receiving
708     return getHeaderIcap();
709   }
710 
711   /**
712    * Real method to send file for scanning through RESPMOD request
713    *
714    * @param originalFilename the original filename
715    * @param fileInStream the file inputStream
716    * @param fileSize the file size
717    *
718    * @return True if the scan is OK, else False if the scan is KO
719    *
720    * @throws IcapException if an error occurs (network, file reading,
721    *     bad headers)
722    */
723   private boolean scanFile(final String originalFilename,
724                            final InputStream fileInStream, final long fileSize)
725       throws IcapException {
726     final int previewSize = sendIcapHttpScanRequest(originalFilename, fileSize);
727 
728     // Sending preview or, if smaller than previewSize, the whole file.
729     if (previewSize == 0) {
730       // Send an empty preview
731       logger.debug("Empty PREVIEW");
732       final StringBuilder builder = new StringBuilder();
733       builder.append(Integer.toHexString(previewSize)).append(TERMINATOR);
734       builder.append(HTTP_TERMINATOR);
735       sendString(builder.toString(), true);
736     } else {
737       logger.debug("PREVIEW of {}", previewSize);
738       final byte[] chunk = new byte[previewSize];
739       final int read = readChunk(fileInStream, chunk, previewSize);
740       if (read != previewSize) {
741         logger.warn("Read file size {} is less than preview size {}", read,
742                     previewSize);
743       }
744       // Send the preview
745       final StringBuilder builder = new StringBuilder();
746       builder.append(Integer.toHexString(read)).append(TERMINATOR);
747       sendString(builder.toString());
748       sendBytes(chunk, read);
749       sendString(TERMINATOR);
750       if (fileSize <= previewSize) {
751         logger.debug("PREVIEW and COMPLETE");
752         sendString("0; ieof" + ICAP_TERMINATOR, true);
753       } else {
754         logger.debug("PREVIEW but could send more");
755         sendString(HTTP_TERMINATOR, true);
756       }
757     }
758     // Parse the response: It might be "100 continue" as
759     // a "go" for the rest of the file or a stop there already.
760     if (fileSize > previewSize) {
761       final int preview = checkPreview();
762       if (preview != 0) {
763         logger.debug("PREVIEW is enough and status {}", preview == 1);
764         return preview == 1;
765       }
766       logger.debug("PREVIEW is not enough");
767       sendNextFileChunks(fileInStream);
768     }
769     return checkFinalResponse();
770   }
771 
772   /**
773    * Send the Icap Http Scan Reaquest
774    *
775    * @param originalFilename
776    * @param fileSize
777    *
778    * @return the preview size
779    *
780    * @throws IcapException
781    */
782   private int sendIcapHttpScanRequest(final String originalFilename,
783                                       final long fileSize)
784       throws IcapException {
785     // HTTP part of header
786     final String resHeader;
787     final StringBuilder builder = new StringBuilder(GET_REQUEST);
788     try {
789       builder.append(
790                  URLEncoder.encode(originalFilename, WaarpStringUtils.UTF_8))
791              .append(" HTTP/1.1").append(TERMINATOR);
792       builder.append(HOST_HEADER).append(serverIP).append(":").append(port)
793              .append(ICAP_TERMINATOR);
794       resHeader = builder.toString();
795     } catch (final UnsupportedEncodingException e) {
796       logger.error("Unsupported Encoding: {}", e.getMessage());
797       throw new IcapException(e.getMessage(), e, IcapError.ICAP_INTERNAL_ERROR);
798     }
799     builder.append("HTTP/1.1 200 OK").append(TERMINATOR);
800     builder.append("Transfer-Encoding: chunked").append(TERMINATOR);
801     builder.append("Content-Length: ").append(fileSize).append(ICAP_TERMINATOR);
802     final String resBody = builder.toString();
803 
804     int previewSize = stdPreviewSize;
805     if (fileSize < stdPreviewSize) {
806       previewSize = (int) fileSize;
807     }
808 
809     // ICAP part of header
810     builder.setLength(0);
811     addIcapUri(builder, RESPMOD);
812     builder.append(PREVIEW).append(": ").append(previewSize).append(TERMINATOR);
813     builder.append("Encapsulated: req-hdr=0, res-hdr=")
814            .append(resHeader.length()).append(", res-body=")
815            .append(resBody.length()).append(ICAP_TERMINATOR);
816     builder.append(resBody);
817     final String requestBuffer = builder.toString();
818 
819     sendString(requestBuffer);
820     return previewSize;
821   }
822 
823   /**
824    * Common part of ICAP URI between OPTIONS and RESPMOD
825    *
826    * @param builder the empty StringBuilder
827    * @param method the method to associate with this ICAP URI
828    */
829   private void addIcapUri(final StringBuilder builder, final String method) {
830     builder.append(method).append(" icap://").append(serverIP).append("/")
831            .append(icapService).append(" ").append(VERSION).append(TERMINATOR);
832     builder.append(HOST_HEADER).append(serverIP).append(TERMINATOR);
833     builder.append(USER_AGENT_HEADER).append(USER_AGENT).append(TERMINATOR);
834     builder.append("Allow: 204").append(TERMINATOR);
835   }
836 
837   /**
838    * Check the preview for the file scanning request
839    *
840    * @return 1 or -1 if the antivirus already validated/invalidated
841    *     the file, or 0 if the next chunks are needed
842    *
843    * @throws IcapException if any error occurs (network, file reading,
844    *     bad headers)
845    */
846   private int checkPreview() throws IcapException {
847     final int status;
848     final String parseMe = getHeaderIcap();
849     finalResult = parseHeader(parseMe);
850 
851     final String tempString = finalResult.get(STATUS_CODE);
852     if (tempString != null) {
853       status = Integer.parseInt(tempString);
854       switch (status) {
855         case 100:
856           logger.debug("Recv ICAP Preview Status Continue");
857           return 0; //Continue transfer
858         case 200:
859           logger.info("Recv ICAP Preview Status Abort");
860           return -1;
861         case 204:
862           logger.debug("Recv ICAP Preview Status Accepted");
863           return 1;
864         case 404:
865           logger.error("404: ICAP Service not found");
866           throw new IcapException("404: ICAP Service not found",
867                                   IcapError.ICAP_SERVER_SERVICE_UNKNOWN);
868         default:
869           logger.error("Server returned unknown status code: {}", status);
870           throw new IcapException(
871               "Server returned unknown status code: " + status,
872               IcapError.ICAP_SERVER_UNKNOWN_CODE);
873       }
874     }
875     logger.error("Server returned unknown status code");
876     throw new IcapException("Server returned unknown status code",
877                             IcapError.ICAP_SERVER_UNKNOWN_CODE);
878   }
879 
880   /**
881    * Check the final response for the file scanning request
882    *
883    * @return True if validated file, False if not
884    *
885    * @throws IcapException if any error occurs (network, file reading,
886    *     bad headers)
887    */
888   private boolean checkFinalResponse() throws IcapException {
889     final int status;
890     String parseMe = getHeaderIcap();
891     finalResult = parseHeader(parseMe);
892 
893     final String tempString = finalResult.get(STATUS_CODE);
894     if (tempString != null) {
895       status = Integer.parseInt(tempString);
896 
897       if (status == 204) {
898         // Unmodified
899         logger.debug("Almost final status is {}", status);
900         return checkAgainstIcapHeader(finalResult, keyIcap204,
901                                       subStringFromKeyIcap204, true);
902       }
903 
904       if (status == 200) {
905         // OK - The ICAP status is ok, but the encapsulated HTTP status might
906         // likely be different or another key in ICAP status
907         logger.debug("Almost final status is {}", status);
908         boolean finalStatus = checkAgainstIcapHeader(finalResult, keyIcap200,
909                                                      subStringFromKeyIcap200,
910                                                      false);
911         if (ParametersChecker.isNotEmpty(substringHttpStatus200)) {
912           parseMe = getHeaderHttp();
913           logger.warn("{} contains {} = {}", parseMe, substringHttpStatus200,
914                       parseMe.contains(substringHttpStatus200));
915           finalStatus |= parseMe.contains(substringHttpStatus200);
916         } else {
917           if (logger.isTraceEnabled()) {
918             getHeaderHttp();
919           }
920         }
921         logger.info("Final status with check {}", finalStatus);
922         return finalStatus;
923       }
924     }
925     logger.error("Unrecognized or no status code in response header");
926     throw new IcapException("Unrecognized or no status code in response header",
927                             IcapError.ICAP_SERVER_UNKNOWN_CODE);
928   }
929 
930   /**
931    * @param responseMap the header map
932    * @param key the key to find out
933    * @param subValue the sub value to find in the value associated with the key
934    * @param defaultValue the default Value to return if key or subvalue are null
935    *
936    * @return True if the key exists and its value contains the subValue or
937    *     default value if key or subValue are null
938    */
939   private boolean checkAgainstIcapHeader(final Map<String, String> responseMap,
940                                          final String key,
941                                          final String subValue,
942                                          final boolean defaultValue) {
943     if (key != null && subValue != null) {
944       final String value = responseMap.get(key);
945       return value != null && value.contains(subValue);
946     }
947     return defaultValue;
948   }
949 
950   /**
951    * Send the next chunks for the file
952    *
953    * @param fileInStream the file inputStream to read from
954    *
955    * @throws IcapException if any error occurs (network, file reading,
956    *     bad headers)
957    */
958   private void sendNextFileChunks(final InputStream fileInStream)
959       throws IcapException {
960     // Sending remaining part of file
961     final byte[] buffer = new byte[sendLength];
962     int len = readChunk(fileInStream, buffer, sendLength);
963     while (len != -1) {
964       sendString(Integer.toHexString(len) + TERMINATOR);
965       sendBytes(buffer, len);
966       sendString(TERMINATOR);
967       len = readChunk(fileInStream, buffer, sendLength);
968     }
969     // Ending file transfer
970     sendString(HTTP_TERMINATOR, true);
971     logger.debug("End of chunks");
972   }
973 
974   /**
975    * Read from inputChannel into the buffer the asked length at most
976    *
977    * @param fileInputStream the file inputStream to read from
978    * @param buffer the buffer to write bytes read
979    * @param length the maximum length to read
980    *
981    * @return -1 if no byte are available, else the size in bytes effectively
982    *     read
983    *
984    * @throws IcapException if an error while reading the file occurs
985    */
986   final int readChunk(final InputStream fileInputStream, final byte[] buffer,
987                       final int length) throws IcapException {
988     if (buffer.length < length) {
989       logger.error("Buffer is too small {} for reading file per {}",
990                    buffer.length, length);
991       throw new IcapException("Buffer is too small for reading file",
992                               IcapError.ICAP_INTERNAL_ERROR);
993     }
994     int sizeOut = 0;
995     int toRead = length;
996     while (sizeOut < length) {
997       try {
998         final int read = fileInputStream.read(buffer, sizeOut, toRead);
999         if (read <= 0) {
1000           break;
1001         }
1002         sizeOut += read;
1003         toRead -= read;
1004       } catch (final IOException e) {
1005         logger.error("File cannot be read: {}", e.getMessage());
1006         throw new IcapException("File cannot be read", e,
1007                                 IcapError.ICAP_INTERNAL_ERROR);
1008       }
1009     }
1010     if (sizeOut <= 0) {
1011       return -1;
1012     }
1013     return sizeOut;
1014   }
1015 
1016   /**
1017    * @return the header for Http part of Icap
1018    *
1019    * @throws IcapException for network errors
1020    */
1021   final String getHeaderHttp() throws IcapException {
1022     final byte[] buffer = new byte[receiveLength];
1023     try {
1024       return getHeader(HTTP_TERMINATOR, buffer);
1025     } catch (final IcapException e) {
1026       final String finalHeaders =
1027           new String(buffer, 0, offset, WaarpStringUtils.UTF8);
1028       switch (e.getError()) {
1029         case ICAP_SERVER_HEADER_WITHOUT_TERMINATOR:
1030           // Returns the buffer as is
1031           logger.debug("RECV HTTP Headers not ended\n{}", finalHeaders);
1032           return finalHeaders;
1033         case ICAP_SERVER_HEADER_EXCEED_CAPACITY:
1034           // Returns the buffer as is
1035           logger.debug("RECV HTTP Headers exceed capacity\n{}", finalHeaders);
1036           return finalHeaders;
1037         default:
1038           break;
1039       }
1040       throw e;
1041     }
1042   }
1043 
1044   /**
1045    * @return the header for Icap
1046    *
1047    * @throws IcapException if the terminator is not found or the buffer is
1048    *     too small
1049    */
1050   final String getHeaderIcap() throws IcapException {
1051     final byte[] buffer = new byte[receiveLength];
1052     return getHeader(ICAP_TERMINATOR, buffer);
1053   }
1054 
1055   /**
1056    * Receive an expected ICAP or HTTP header as response of a request. The
1057    * returned String should be parsed with parseHeader()
1058    *
1059    * @param terminator the terminator to use
1060    *
1061    * @return String of the raw response
1062    *
1063    * @throws IcapException if a network error is raised or if the header
1064    *     is wrong
1065    */
1066   private String getHeader(final String terminator, final byte[] buffer)
1067       throws IcapException {
1068     final byte[] endOfHeader = terminator.getBytes(WaarpStringUtils.UTF8);
1069     final int[] endOfHeaderInt = new int[endOfHeader.length];
1070     final int[] marks = new int[endOfHeader.length];
1071     for (int i = 0; i < endOfHeader.length; i++) {
1072       endOfHeaderInt[i] = endOfHeader[i];
1073       marks[i] = -1;
1074     }
1075 
1076     int reader = -1;
1077     offset = 0;
1078     // "in" is read 1 by 1 to ensure we read only ICAP headers or HTTP headers
1079     try {
1080       // first part is to secure against DOS
1081       while ((offset < receiveLength) && ((reader = in.read()) != -1)) {
1082         marks[0] = marks[1];
1083         marks[1] = marks[2];
1084         marks[2] = marks[3];
1085         if (endOfHeader.length == 4) {
1086           marks[3] = reader;
1087         } else {
1088           marks[3] = marks[4];
1089           marks[4] = reader;
1090         }
1091         buffer[offset] = (byte) reader;
1092         offset++;
1093         // 13 is the smallest possible message "ICAP/1.0 xxx "
1094         if (offset > endOfHeader.length + 13 &&
1095             Arrays.equals(endOfHeaderInt, marks)) {
1096           final String finalHeaders =
1097               new String(buffer, 0, offset, WaarpStringUtils.UTF8);
1098           logger.debug("RECV {} Headers:{}\n{}",
1099                        terminator.length() == 4? "ICAP" : "HTTP", offset,
1100                        finalHeaders);
1101           return finalHeaders;
1102         }
1103       }
1104     } catch (final SocketTimeoutException e) {
1105       logger.error(TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE, serverIP, port,
1106                    e.getMessage());
1107       throw new IcapException(TIMEOUT_OCCURS_WITH_THE_SERVER, e,
1108                               IcapError.ICAP_TIMEOUT_ERROR);
1109     } catch (final IOException e) {
1110       logger.error("Response cannot be read: {}", e.getMessage());
1111       throw new IcapException("Response cannot be read", e,
1112                               IcapError.ICAP_NETWORK_ERROR);
1113     }
1114     if (reader == -1) {
1115       logger.warn("Response is not complete while reading {}", offset);
1116       throw new IcapException(
1117           "Error in getHeader() method: response is not complete: " + offset,
1118           IcapError.ICAP_SERVER_HEADER_WITHOUT_TERMINATOR);
1119     }
1120     logger.warn("Response cannot be read since size exceed maximum {}",
1121                 receiveLength);
1122     throw new IcapException(
1123         "Error in getHeader() method: received message too long",
1124         IcapError.ICAP_SERVER_HEADER_EXCEED_CAPACITY);
1125   }
1126 
1127   /**
1128    * Given a raw response header as a String, it will parse through it and return a HashMap of the result
1129    */
1130   private Map<String, String> parseHeader(final String response) {
1131     final Map<String, String> headers = new HashMap<String, String>();
1132 
1133     /*
1134      * SAMPLE:
1135      * ICAP/1.0 204 Unmodified
1136      * Server: C-ICAP/0.1.6
1137      * Connection: keep-alive
1138      * ISTag: CI0001-000-0978-6918203
1139      */
1140     // The status code is located between the first 2 whitespaces.
1141     // Read status code
1142     final int x = response.indexOf(' ');
1143     final int y = response.indexOf(' ', x + 2);
1144     final String statusCode = response.substring(x + 1, y);
1145     headers.put(STATUS_CODE, statusCode);
1146 
1147     // Each line in the sample is ended with "\r\n".
1148     // When (i+2==response.length()) The end of the header have been reached.
1149     // The +=2 is added to skip the "\r\n".
1150     // Read headers
1151     int i = response.indexOf(TERMINATOR, y);
1152     i += 2;
1153     while (i + 2 < response.length() && response.substring(i).contains(":")) {
1154       int n = response.indexOf(':', i);
1155       final String key = response.substring(i, n).trim();
1156 
1157       n += 2;
1158       i = response.indexOf(TERMINATOR, n);
1159       final String value = response.substring(n, i).trim();
1160 
1161       headers.put(key, value);
1162       i += 2;
1163     }
1164     logger.debug("RECV ICAP Headers:\n{}", headers);
1165     return headers;
1166   }
1167 
1168   /**
1169    * Sends a String through the socket connection. Used for sending ICAP/HTTP headers.
1170    *
1171    * @param requestHeader to send
1172    *
1173    * @throws IcapException if a network error is raised
1174    */
1175   private void sendString(final String requestHeader) throws IcapException {
1176     sendString(requestHeader, false);
1177   }
1178 
1179   /**
1180    * Sends a String through the socket connection. Used for sending ICAP/HTTP headers.
1181    *
1182    * @param requestHeader to send
1183    * @param withFlush if flush is necessary
1184    *
1185    * @throws IcapException if a network error is raised
1186    */
1187   private void sendString(final String requestHeader, final boolean withFlush)
1188       throws IcapException {
1189     try {
1190       out.write(requestHeader.getBytes(WaarpStringUtils.UTF8));
1191       if (withFlush) {
1192         out.flush();
1193       }
1194     } catch (final SocketTimeoutException e) {
1195       logger.error(TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE, serverIP, port,
1196                    e.getMessage());
1197       throw new IcapException(TIMEOUT_OCCURS_WITH_THE_SERVER, e,
1198                               IcapError.ICAP_TIMEOUT_ERROR);
1199     } catch (final IOException e) {
1200       logger.error("Client cannot communicate with ICAP Server: {}",
1201                    e.getMessage());
1202       throw new IcapException("Client cannot communicate with ICAP Server", e,
1203                               IcapError.ICAP_NETWORK_ERROR);
1204     }
1205   }
1206 
1207   /**
1208    * Sends bytes of data from a byte-array through the socket connection.
1209    *
1210    * @param chunk The byte-array to send
1211    *
1212    * @throws IcapException if a network error is raised
1213    */
1214   private void sendBytes(final byte[] chunk, final int length)
1215       throws IcapException {
1216     try {
1217       out.write(chunk, 0, length);
1218     } catch (final SocketTimeoutException e) {
1219       logger.error(TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE, serverIP, port,
1220                    e.getMessage());
1221       throw new IcapException(TIMEOUT_OCCURS_WITH_THE_SERVER, e,
1222                               IcapError.ICAP_TIMEOUT_ERROR);
1223     } catch (final IOException e) {
1224       logger.error("Client cannot communicate with ICAP Server: {}",
1225                    e.getMessage());
1226       throw new IcapException("Writing to ICAP Server cannot be done", e,
1227                               IcapError.ICAP_NETWORK_ERROR);
1228     }
1229   }
1230 
1231 }