IcapClient.java
/*
* This file is part of Waarp Project (named also Waarp or GG).
*
* Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
* tags. See the COPYRIGHT.txt in the distribution for a full listing of
* individual contributors.
*
* All Waarp Project is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Waarp . If not, see <http://www.gnu.org/licenses/>.
*/
package org.waarp.icap;
import com.google.common.io.Files;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.utility.ParametersChecker;
import org.waarp.common.utility.WaarpStringUtils;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static org.waarp.common.file.filesystembased.FilesystemBasedFileImpl.*;
/**
* The IcapClient allows to do 3 actions:</br>
* <ul>
* <li>connect(): which allows to initialize the connection with the ICAP
* server</li>
* <li>close(): forces the client to disconnect from the ICAP server</li>
* <li>scanFile(path): send a file for a scan by the ICAP server</li>
* </ul>
* </br>
* This code is inspired from 2 sources:</br>
* <ul>
* <li>https://github.com/Baekalfen/ICAP-avscan</li>
* <li>https://github.com/claudineyns/icap-client</li>
* </ul>
* </br>
* This reflects the RFC 3507 and errata as of 2010/04/17.
*/
public class IcapClient implements Closeable {
/**
* Default ICAP port
*/
public static final int DEFAULT_ICAP_PORT = 1344;
private static final WaarpLogger logger =
WaarpLoggerFactory.getLogger(IcapClient.class);
static final int STD_RECEIVE_LENGTH = 64 * 1024;
static final int STD_SEND_LENGTH = 8192;
static final int DEFAULT_TIMEOUT = 10 * 60 * 60000;// 10 min
public static final String VERSION = "ICAP/1.0";
private static final String USER_AGENT = "Waarp ICAP Client/1.0";
public static final String TERMINATOR = "\r\n";
public static final String ICAP_TERMINATOR = TERMINATOR + TERMINATOR;
public static final String HTTP_TERMINATOR = "0" + TERMINATOR + TERMINATOR;
private static final String STATUS_CODE = "StatusCode";
private static final String PREVIEW = "Preview";
private static final String OPTIONS = "OPTIONS";
private static final String HOST_HEADER = "Host: ";
private static final String USER_AGENT_HEADER = "User-Agent: ";
private static final String RESPMOD = "RESPMOD";
private static final String ENCAPSULATED_NULL_BODY =
"Encapsulated: null-body=0";
static final int MINIMAL_SIZE = 100;
private static final String GET_REQUEST = "GET /";
private static final String INCOMPATIBLE_ARGUMENT = "Incompatible argument";
private static final String TIMEOUT_OCCURS_WITH_THE_SERVER =
"Timeout occurs with the Server";
private static final String TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE =
"Timeout occurs with the Server {}:{} since {}";
public static final String EICARTEST = "EICARTEST";
// Standard configuration
private final String serverIP;
private final int port;
private final String icapService;
private final int setPreviewSize;
// Extra configuration
private int receiveLength = STD_RECEIVE_LENGTH;
private int sendLength = STD_SEND_LENGTH;
private String keyIcapPreview = null;
private String subStringFromKeyIcapPreview = null;
private String substringHttpStatus200 = null;
private String keyIcap200 = null;
private String subStringFromKeyIcap200 = null;
private String keyIcap204 = null;
private String subStringFromKeyIcap204 = null;
private long maxSize = Integer.MAX_VALUE;
private int timeout = DEFAULT_TIMEOUT;
private int stdPreviewSize = -1;
// Accessibe data
private Map<String, String> finalResult = null;
// Internal data
private Socket client;
private OutputStream out;
InputStream in;
private int offset;
/**
* This creates the ICAP client without connecting immediately to the ICAP
* server. When the ICAP client will connect, it will ask for the preview
* size to the ICAP Server.
*
* @param serverIP The IP address to connect to.
* @param port The port in the host to use.
* @param icapService The service to use (fx "avscan").
*/
public IcapClient(final String serverIP, final int port,
final String icapService) {
this(serverIP, port, icapService, -1);
}
/**
* This creates the ICAP client without connecting immediately to the ICAP
* server. When the ICAP client will connect, it will not ask for the preview
* size to the ICAP Server but uses the default specified value.
*
* @param serverIP The IP address to connect to.
* @param port The port in the host to use.
* @param icapService The service to use (fx "avscan").
* @param previewSize Amount of bytes to send as preview.
*/
public IcapClient(final String serverIP, final int port,
final String icapService, final int previewSize) {
if (ParametersChecker.isEmpty(icapService)) {
throw new IllegalArgumentException("IcapService must not be empty");
}
this.icapService = icapService;
if (ParametersChecker.isEmpty(serverIP)) {
throw new IllegalArgumentException("Server IP must not be empty");
}
this.serverIP = serverIP;
if (port <= 0) {
this.port = DEFAULT_ICAP_PORT;
} else {
this.port = port;
}
this.setPreviewSize = previewSize;
this.stdPreviewSize = Math.max(0, previewSize);
}
/**
* Try to connect to the server and if the preview size is not specified,
* it will also resolve the options of the ICAP server.</br>
*
* If the client is still connected, it will first disconnect before
* reconnecting to the ICAP Server.</br>
*
* Note that every attempts of connection will retry to issue an OPTIONS
* request if necessary (if preview size is not set to a fixed value already).
*
* @throws IcapException if an issue occurs during the connection or
* response (the connection is already closed)
*/
public final IcapClient connect() throws IcapException {
if (finalResult != null) {
finalResult.clear();
finalResult = null;
}
if (client != null) {
close();
}
logger.debug("Try connect to {}:{} service {}", serverIP, port,
icapService);
try {
// Initialize connection
client = new Socket(serverIP, port);
client.setReuseAddress(true);
client.setKeepAlive(true);
client.setSoTimeout(timeout);
client.setTcpNoDelay(false);
// Opening out stream
out = client.getOutputStream();
// Opening in stream
in = client.getInputStream();
if (setPreviewSize < 0) {
getFromServerPreviewSize();
}
logger.debug("Connected with Preview Size = {}", stdPreviewSize);
return this;
} catch (final SocketTimeoutException e) {
close();
logger.error(TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE, serverIP, port,
e.getMessage());
throw new IcapException(TIMEOUT_OCCURS_WITH_THE_SERVER, e,
IcapError.ICAP_TIMEOUT_ERROR);
} catch (final ConnectException e) {
close();
logger.error("Could not connect to server {}:{} since {}", serverIP, port,
e.getMessage());
throw new IcapException("Could not connect with the server", e,
IcapError.ICAP_CANT_CONNECT);
} catch (final IOException e) {
close();
logger.error("Could not connect to server {}:{} since {}", serverIP, port,
e.getMessage());
throw new IcapException("Could not connect with the server", e,
IcapError.ICAP_NETWORK_ERROR);
} catch (final IcapException e) {
close();
throw e;
}
}
/**
* Get the Preview Size from the SERVER using ICAP OPTIONS command
*
* @throws IcapException
*/
private void getFromServerPreviewSize() throws IcapException {
// Check the preview size from the ICAP Server response to OPTIONS
final String parseMe = getOptions();
finalResult = parseHeader(parseMe);
if (checkAgainstIcapHeader(finalResult, STATUS_CODE, "200", false)) {
final String tempString = finalResult.get(PREVIEW);
if (tempString != null) {
stdPreviewSize = Integer.parseInt(tempString);
if (stdPreviewSize < 0) {
stdPreviewSize = 0;
}
if (!checkAgainstIcapHeader(finalResult, keyIcapPreview,
subStringFromKeyIcapPreview, true)) {
close();
logger.error("Could not validate preview from server");
throw new IcapException("Could not validate preview from server",
IcapError.ICAP_SERVER_MISSING_INFO);
}
} else {
close();
logger.error("Could not get preview size from server");
throw new IcapException("Could not get preview size from server",
IcapError.ICAP_SERVER_MISSING_INFO);
}
} else {
close();
logger.error("Could not get options from server {}:{} service {}",
serverIP, port, icapService);
throw new IcapException("Could not get options from server",
IcapError.ICAP_SERVER_MISSING_INFO);
}
}
@Override
public final void close() {
if (client != null) {
try {
client.close();
} catch (final IOException ignored) {
// Nothing
}
client = null;
}
if (in != null) {
try {
in.close();
} catch (final IOException ignored) {
// Nothing
}
in = null;
}
if (out != null) {
try {
out.close();
} catch (final IOException ignored) {
// Nothing
}
out = null;
}
}
/**
* Given a file path, it will send the file to the server and return true,
* if the server accepts the file. Visa-versa, false if the server rejects
* it.</br>
*
* Note that if the client is not connected, it will first call connect().
*
* @param filename Relative or absolute file path to a file. If filename is
* EICARTEST, then a build on the fly EICAR test file is sent.
*
* @return Returns true when no infection is found.
*
* @throws IcapException if an error occurs (network, file reading,
* bad headers)
*/
public final boolean scanFile(final String filename) throws IcapException {
if (ParametersChecker.isEmpty(filename)) {
throw new IllegalArgumentException("Filename must not be empty");
}
if (client == null) {
connect();
}
if (finalResult != null) {
finalResult.clear();
finalResult = null;
}
InputStream inputStream = null;
final long length;
if (EICARTEST.equals(filename)) {
// Special file to test from EICAR Test file
final ClassLoader classLoader = IcapClient.class.getClassLoader();
final File fileSrc1 =
new File(classLoader.getResource("eicar.com-part1.txt").getFile());
final File fileSrc2 =
new File(classLoader.getResource("eicar.com-part2.txt").getFile());
if (fileSrc1.exists() && fileSrc2.exists()) {
try {
final byte[] array1 = Files.toByteArray(fileSrc1);
final byte[] array2 = Files.toByteArray(fileSrc2);
final byte[] array =
Arrays.copyOf(array1, array1.length + array2.length);
System.arraycopy(array2, 0, array, array1.length, array2.length);
inputStream = new ByteArrayInputStream(array);
length = array.length;
} catch (final IOException e) {
logger.error("File EICAR TEST does not exist: {}", e.getMessage());
throw new IcapException("File EICAR TEST cannot be found",
IcapError.ICAP_ARGUMENT_ERROR);
}
} else {
logger.error("File EICAR TEST does not exist");
throw new IcapException("File EICAR TEST cannot be found",
IcapError.ICAP_ARGUMENT_ERROR);
}
} else {
final File file = new File(filename);
if (!canRead(file)) {
logger.error("File does not exist: {}", file.getAbsolutePath());
throw new IcapException(
"File cannot be found: " + file.getAbsolutePath(),
IcapError.ICAP_ARGUMENT_ERROR);
}
length = file.length();
if (length > maxSize) {
logger.error("File size {} exceed limit of {}: {}", length, maxSize,
file.getAbsolutePath());
throw new IcapException(
"File exceed limit size: " + file.getAbsolutePath(),
IcapError.ICAP_FILE_LENGTH_ERROR);
}
try {
inputStream = new FileInputStream(file);
} catch (final FileNotFoundException e) {
logger.error("Could not find file {} since {}", filename,
e.getMessage());
throw new IcapException("File cannot be found: " + filename, e,
IcapError.ICAP_ARGUMENT_ERROR);
}
}
try {
return scanFile(filename, inputStream, length);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (final IOException ignored) {
// Nothing
}
}
close();
}
}
/**
* @return the Server IP
*/
public final String getServerIP() {
return serverIP;
}
/**
* @return the port
*/
public final int getPort() {
return port;
}
/**
* @return the ICAP service
*/
public final String getIcapService() {
return icapService;
}
/**
* @return the current Preview size
*/
public final int getPreviewSize() {
return stdPreviewSize;
}
/**
* @param previewSize the receive length to set
*
* @return This
*/
public final IcapClient setPreviewSize(final int previewSize) {
if (previewSize < 0) {
logger.error(INCOMPATIBLE_ARGUMENT);
throw new IllegalArgumentException("Preview cannot be 0 or positive");
}
this.stdPreviewSize = previewSize;
return this;
}
/**
* @return the current Receive length
*/
public final int getReceiveLength() {
return receiveLength;
}
/**
* @param receiveLength the receive length to set
*
* @return This
*/
public final IcapClient setReceiveLength(final int receiveLength) {
if (receiveLength < MINIMAL_SIZE) {
logger.error(INCOMPATIBLE_ARGUMENT);
throw new IllegalArgumentException(
"Receive length cannot be less than " + MINIMAL_SIZE);
}
this.receiveLength = receiveLength;
return this;
}
/**
* @return the current Send length
*/
public final int getSendLength() {
return sendLength;
}
/**
* @param sendLength the send length to set
*
* @return This
*/
public final IcapClient setSendLength(final int sendLength) {
if (sendLength < MINIMAL_SIZE) {
logger.error(INCOMPATIBLE_ARGUMENT);
throw new IllegalArgumentException(
"Send length cannot be less than " + MINIMAL_SIZE);
}
this.sendLength = sendLength;
return this;
}
/**
* @return the current max file size (default being Integer.MAX_VALUE)
*/
public final long getMaxSize() {
return maxSize;
}
/**
* @param maxSize the maximum file size to set
*
* @return This
*/
public final IcapClient setMaxSize(final long maxSize) {
if (maxSize < MINIMAL_SIZE) {
logger.error(INCOMPATIBLE_ARGUMENT);
throw new IllegalArgumentException(
"Maximum file size length cannot be less than " + MINIMAL_SIZE);
}
this.maxSize = maxSize;
return this;
}
/**
* @return the current time out for connection
*/
public final long getTimeout() {
return timeout;
}
/**
* @param timeout the timeout to use on connection
*
* @return This
*/
public final IcapClient setTimeout(final int timeout) {
this.timeout = timeout;
return this;
}
/**
* @return the current key in ICAP headers to find with 200 status in PREVIEW
* (or null if none)
*/
public final String getKeyIcapPreview() {
return keyIcapPreview;
}
/**
* @param keyIcapPreview the key in ICAP headers to find with 200 status in
* PREVIEW (or null if none)
*
* @return This
*/
public final IcapClient setKeyIcapPreview(final String keyIcapPreview) {
if (ParametersChecker.isEmpty(keyIcapPreview)) {
this.keyIcapPreview = null;
} else {
this.keyIcapPreview = keyIcapPreview;
}
return this;
}
/**
* @return the current subString to find in key ICAP header with 200 status
* in PREVIEW (or null if none)
*/
public final String getSubStringFromKeyIcapPreview() {
return subStringFromKeyIcapPreview;
}
/**
* @param subStringFromKeyIcapPreview the subString to find in key ICAP header
* with 200 status in PREVIEW (or null if none)
*
* @return This
*/
public final IcapClient setSubStringFromKeyIcapPreview(
final String subStringFromKeyIcapPreview) {
if (ParametersChecker.isEmpty(subStringFromKeyIcapPreview)) {
this.subStringFromKeyIcapPreview = null;
} else {
this.subStringFromKeyIcapPreview = subStringFromKeyIcapPreview;
}
return this;
}
/**
* @return the current subString to find in Http with 200 status
* (or null if none)
*/
public final String getSubstringHttpStatus200() {
return substringHttpStatus200;
}
/**
* @param substringHttpStatus200 the subString to find in Http with 200 status
* (or null if none)
*
* @return This
*/
public final IcapClient setSubstringHttpStatus200(
final String substringHttpStatus200) {
if (ParametersChecker.isEmpty(substringHttpStatus200)) {
this.substringHttpStatus200 = null;
} else {
this.substringHttpStatus200 = substringHttpStatus200;
}
return this;
}
/**
* @return the current key in ICAP headers to find with 200 status
* (or null if none)
*/
public final String getKeyIcap200() {
return keyIcap200;
}
/**
* @param keyIcap200 the key in ICAP headers to find with 200 status
* (or null if none)
*
* @return This
*/
public final IcapClient setKeyIcap200(final String keyIcap200) {
if (ParametersChecker.isEmpty(keyIcap200)) {
this.keyIcap200 = null;
} else {
this.keyIcap200 = keyIcap200;
}
return this;
}
/**
* @return the current subString to find in key ICAP header with 200 status
* (or null if none)
*/
public final String getSubStringFromKeyIcap200() {
return subStringFromKeyIcap200;
}
/**
* @param subStringFromKeyIcap200 the subString to find in key ICAP header with 200 status
* (or null if none)
*
* @return This
*/
public final IcapClient setSubStringFromKeyIcap200(
final String subStringFromKeyIcap200) {
if (ParametersChecker.isEmpty(subStringFromKeyIcap200)) {
this.subStringFromKeyIcap200 = null;
} else {
this.subStringFromKeyIcap200 = subStringFromKeyIcap200;
}
return this;
}
/**
* @return the current key in ICAP headers to find with 204 status
* (or null if none)
*/
public final String getKeyIcap204() {
return keyIcap204;
}
/**
* @param keyIcap204 the key in ICAP headers to find with 204 status
* (or null if none)
*
* @return This
*/
public final IcapClient setKeyIcap204(final String keyIcap204) {
if (ParametersChecker.isEmpty(keyIcap204)) {
this.keyIcap204 = null;
} else {
this.keyIcap204 = keyIcap204;
}
return this;
}
/**
* @return the current subString to find in key ICAP header with 204 status
* (or null if none)
*/
public final String getSubStringFromKeyIcap204() {
return subStringFromKeyIcap204;
}
/**
* @param subStringFromKeyIcap204 the subString to find in key ICAP header with 204 status
* (or null if none)
*
* @return This
*/
public final IcapClient setSubStringFromKeyIcap204(
final String subStringFromKeyIcap204) {
if (ParametersChecker.isEmpty(subStringFromKeyIcap204)) {
this.subStringFromKeyIcap204 = null;
} else {
this.subStringFromKeyIcap204 = subStringFromKeyIcap204;
}
return this;
}
/**
* @return the current map of result (null if none)
*/
public final Map<String, String> getFinalResult() {
return finalResult;
}
/**
* Automatically asks for the servers available options and returns the raw
* response as a String.
*
* @return String of the servers response
*
* @throws IcapException if an error occurs (network, bad headers)
*/
private final String getOptions() throws IcapException {
// Send OPTIONS header and receive response
// Sending
final StringBuilder builder = new StringBuilder();
addIcapUri(builder, OPTIONS);
final String requestHeader =
builder.append(ENCAPSULATED_NULL_BODY).append(ICAP_TERMINATOR)
.toString();
sendString(requestHeader, true);
// Receiving
return getHeaderIcap();
}
/**
* Real method to send file for scanning through RESPMOD request
*
* @param originalFilename the original filename
* @param fileInStream the file inputStream
* @param fileSize the file size
*
* @return True if the scan is OK, else False if the scan is KO
*
* @throws IcapException if an error occurs (network, file reading,
* bad headers)
*/
private boolean scanFile(final String originalFilename,
final InputStream fileInStream, final long fileSize)
throws IcapException {
final int previewSize = sendIcapHttpScanRequest(originalFilename, fileSize);
// Sending preview or, if smaller than previewSize, the whole file.
if (previewSize == 0) {
// Send an empty preview
logger.debug("Empty PREVIEW");
final StringBuilder builder = new StringBuilder();
builder.append(Integer.toHexString(previewSize)).append(TERMINATOR);
builder.append(HTTP_TERMINATOR);
sendString(builder.toString(), true);
} else {
logger.debug("PREVIEW of {}", previewSize);
final byte[] chunk = new byte[previewSize];
final int read = readChunk(fileInStream, chunk, previewSize);
if (read != previewSize) {
logger.warn("Read file size {} is less than preview size {}", read,
previewSize);
}
// Send the preview
final StringBuilder builder = new StringBuilder();
builder.append(Integer.toHexString(read)).append(TERMINATOR);
sendString(builder.toString());
sendBytes(chunk, read);
sendString(TERMINATOR);
if (fileSize <= previewSize) {
logger.debug("PREVIEW and COMPLETE");
sendString("0; ieof" + ICAP_TERMINATOR, true);
} else {
logger.debug("PREVIEW but could send more");
sendString(HTTP_TERMINATOR, true);
}
}
// Parse the response: It might be "100 continue" as
// a "go" for the rest of the file or a stop there already.
if (fileSize > previewSize) {
final int preview = checkPreview();
if (preview != 0) {
logger.debug("PREVIEW is enough and status {}", preview == 1);
return preview == 1;
}
logger.debug("PREVIEW is not enough");
sendNextFileChunks(fileInStream);
}
return checkFinalResponse();
}
/**
* Send the Icap Http Scan Reaquest
*
* @param originalFilename
* @param fileSize
*
* @return the preview size
*
* @throws IcapException
*/
private int sendIcapHttpScanRequest(final String originalFilename,
final long fileSize)
throws IcapException {
// HTTP part of header
final String resHeader;
final StringBuilder builder = new StringBuilder(GET_REQUEST);
try {
builder.append(
URLEncoder.encode(originalFilename, WaarpStringUtils.UTF_8))
.append(" HTTP/1.1").append(TERMINATOR);
builder.append(HOST_HEADER).append(serverIP).append(":").append(port)
.append(ICAP_TERMINATOR);
resHeader = builder.toString();
} catch (final UnsupportedEncodingException e) {
logger.error("Unsupported Encoding: {}", e.getMessage());
throw new IcapException(e.getMessage(), e, IcapError.ICAP_INTERNAL_ERROR);
}
builder.append("HTTP/1.1 200 OK").append(TERMINATOR);
builder.append("Transfer-Encoding: chunked").append(TERMINATOR);
builder.append("Content-Length: ").append(fileSize).append(ICAP_TERMINATOR);
final String resBody = builder.toString();
int previewSize = stdPreviewSize;
if (fileSize < stdPreviewSize) {
previewSize = (int) fileSize;
}
// ICAP part of header
builder.setLength(0);
addIcapUri(builder, RESPMOD);
builder.append(PREVIEW).append(": ").append(previewSize).append(TERMINATOR);
builder.append("Encapsulated: req-hdr=0, res-hdr=")
.append(resHeader.length()).append(", res-body=")
.append(resBody.length()).append(ICAP_TERMINATOR);
builder.append(resBody);
final String requestBuffer = builder.toString();
sendString(requestBuffer);
return previewSize;
}
/**
* Common part of ICAP URI between OPTIONS and RESPMOD
*
* @param builder the empty StringBuilder
* @param method the method to associate with this ICAP URI
*/
private void addIcapUri(final StringBuilder builder, final String method) {
builder.append(method).append(" icap://").append(serverIP).append("/")
.append(icapService).append(" ").append(VERSION).append(TERMINATOR);
builder.append(HOST_HEADER).append(serverIP).append(TERMINATOR);
builder.append(USER_AGENT_HEADER).append(USER_AGENT).append(TERMINATOR);
builder.append("Allow: 204").append(TERMINATOR);
}
/**
* Check the preview for the file scanning request
*
* @return 1 or -1 if the antivirus already validated/invalidated
* the file, or 0 if the next chunks are needed
*
* @throws IcapException if any error occurs (network, file reading,
* bad headers)
*/
private int checkPreview() throws IcapException {
final int status;
final String parseMe = getHeaderIcap();
finalResult = parseHeader(parseMe);
final String tempString = finalResult.get(STATUS_CODE);
if (tempString != null) {
status = Integer.parseInt(tempString);
switch (status) {
case 100:
logger.debug("Recv ICAP Preview Status Continue");
return 0; //Continue transfer
case 200:
logger.info("Recv ICAP Preview Status Abort");
return -1;
case 204:
logger.debug("Recv ICAP Preview Status Accepted");
return 1;
case 404:
logger.error("404: ICAP Service not found");
throw new IcapException("404: ICAP Service not found",
IcapError.ICAP_SERVER_SERVICE_UNKNOWN);
default:
logger.error("Server returned unknown status code: {}", status);
throw new IcapException(
"Server returned unknown status code: " + status,
IcapError.ICAP_SERVER_UNKNOWN_CODE);
}
}
logger.error("Server returned unknown status code");
throw new IcapException("Server returned unknown status code",
IcapError.ICAP_SERVER_UNKNOWN_CODE);
}
/**
* Check the final response for the file scanning request
*
* @return True if validated file, False if not
*
* @throws IcapException if any error occurs (network, file reading,
* bad headers)
*/
private boolean checkFinalResponse() throws IcapException {
final int status;
String parseMe = getHeaderIcap();
finalResult = parseHeader(parseMe);
final String tempString = finalResult.get(STATUS_CODE);
if (tempString != null) {
status = Integer.parseInt(tempString);
if (status == 204) {
// Unmodified
logger.debug("Almost final status is {}", status);
return checkAgainstIcapHeader(finalResult, keyIcap204,
subStringFromKeyIcap204, true);
}
if (status == 200) {
// OK - The ICAP status is ok, but the encapsulated HTTP status might
// likely be different or another key in ICAP status
logger.debug("Almost final status is {}", status);
boolean finalStatus = checkAgainstIcapHeader(finalResult, keyIcap200,
subStringFromKeyIcap200,
false);
if (ParametersChecker.isNotEmpty(substringHttpStatus200)) {
parseMe = getHeaderHttp();
logger.warn("{} contains {} = {}", parseMe, substringHttpStatus200,
parseMe.contains(substringHttpStatus200));
finalStatus |= parseMe.contains(substringHttpStatus200);
} else {
if (logger.isTraceEnabled()) {
getHeaderHttp();
}
}
logger.info("Final status with check {}", finalStatus);
return finalStatus;
}
}
logger.error("Unrecognized or no status code in response header");
throw new IcapException("Unrecognized or no status code in response header",
IcapError.ICAP_SERVER_UNKNOWN_CODE);
}
/**
* @param responseMap the header map
* @param key the key to find out
* @param subValue the sub value to find in the value associated with the key
* @param defaultValue the default Value to return if key or subvalue are null
*
* @return True if the key exists and its value contains the subValue or
* default value if key or subValue are null
*/
private boolean checkAgainstIcapHeader(final Map<String, String> responseMap,
final String key,
final String subValue,
final boolean defaultValue) {
if (key != null && subValue != null) {
final String value = responseMap.get(key);
return value != null && value.contains(subValue);
}
return defaultValue;
}
/**
* Send the next chunks for the file
*
* @param fileInStream the file inputStream to read from
*
* @throws IcapException if any error occurs (network, file reading,
* bad headers)
*/
private void sendNextFileChunks(final InputStream fileInStream)
throws IcapException {
// Sending remaining part of file
final byte[] buffer = new byte[sendLength];
int len = readChunk(fileInStream, buffer, sendLength);
while (len != -1) {
sendString(Integer.toHexString(len) + TERMINATOR);
sendBytes(buffer, len);
sendString(TERMINATOR);
len = readChunk(fileInStream, buffer, sendLength);
}
// Ending file transfer
sendString(HTTP_TERMINATOR, true);
logger.debug("End of chunks");
}
/**
* Read from inputChannel into the buffer the asked length at most
*
* @param fileInputStream the file inputStream to read from
* @param buffer the buffer to write bytes read
* @param length the maximum length to read
*
* @return -1 if no byte are available, else the size in bytes effectively
* read
*
* @throws IcapException if an error while reading the file occurs
*/
final int readChunk(final InputStream fileInputStream, final byte[] buffer,
final int length) throws IcapException {
if (buffer.length < length) {
logger.error("Buffer is too small {} for reading file per {}",
buffer.length, length);
throw new IcapException("Buffer is too small for reading file",
IcapError.ICAP_INTERNAL_ERROR);
}
int sizeOut = 0;
int toRead = length;
while (sizeOut < length) {
try {
final int read = fileInputStream.read(buffer, sizeOut, toRead);
if (read <= 0) {
break;
}
sizeOut += read;
toRead -= read;
} catch (final IOException e) {
logger.error("File cannot be read: {}", e.getMessage());
throw new IcapException("File cannot be read", e,
IcapError.ICAP_INTERNAL_ERROR);
}
}
if (sizeOut <= 0) {
return -1;
}
return sizeOut;
}
/**
* @return the header for Http part of Icap
*
* @throws IcapException for network errors
*/
final String getHeaderHttp() throws IcapException {
final byte[] buffer = new byte[receiveLength];
try {
return getHeader(HTTP_TERMINATOR, buffer);
} catch (final IcapException e) {
final String finalHeaders =
new String(buffer, 0, offset, WaarpStringUtils.UTF8);
switch (e.getError()) {
case ICAP_SERVER_HEADER_WITHOUT_TERMINATOR:
// Returns the buffer as is
logger.debug("RECV HTTP Headers not ended\n{}", finalHeaders);
return finalHeaders;
case ICAP_SERVER_HEADER_EXCEED_CAPACITY:
// Returns the buffer as is
logger.debug("RECV HTTP Headers exceed capacity\n{}", finalHeaders);
return finalHeaders;
default:
break;
}
throw e;
}
}
/**
* @return the header for Icap
*
* @throws IcapException if the terminator is not found or the buffer is
* too small
*/
final String getHeaderIcap() throws IcapException {
final byte[] buffer = new byte[receiveLength];
return getHeader(ICAP_TERMINATOR, buffer);
}
/**
* Receive an expected ICAP or HTTP header as response of a request. The
* returned String should be parsed with parseHeader()
*
* @param terminator the terminator to use
*
* @return String of the raw response
*
* @throws IcapException if a network error is raised or if the header
* is wrong
*/
private String getHeader(final String terminator, final byte[] buffer)
throws IcapException {
final byte[] endOfHeader = terminator.getBytes(WaarpStringUtils.UTF8);
final int[] endOfHeaderInt = new int[endOfHeader.length];
final int[] marks = new int[endOfHeader.length];
for (int i = 0; i < endOfHeader.length; i++) {
endOfHeaderInt[i] = endOfHeader[i];
marks[i] = -1;
}
int reader = -1;
offset = 0;
// "in" is read 1 by 1 to ensure we read only ICAP headers or HTTP headers
try {
// first part is to secure against DOS
while ((offset < receiveLength) && ((reader = in.read()) != -1)) {
marks[0] = marks[1];
marks[1] = marks[2];
marks[2] = marks[3];
if (endOfHeader.length == 4) {
marks[3] = reader;
} else {
marks[3] = marks[4];
marks[4] = reader;
}
buffer[offset] = (byte) reader;
offset++;
// 13 is the smallest possible message "ICAP/1.0 xxx "
if (offset > endOfHeader.length + 13 &&
Arrays.equals(endOfHeaderInt, marks)) {
final String finalHeaders =
new String(buffer, 0, offset, WaarpStringUtils.UTF8);
logger.debug("RECV {} Headers:{}\n{}",
terminator.length() == 4? "ICAP" : "HTTP", offset,
finalHeaders);
return finalHeaders;
}
}
} catch (final SocketTimeoutException e) {
logger.error(TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE, serverIP, port,
e.getMessage());
throw new IcapException(TIMEOUT_OCCURS_WITH_THE_SERVER, e,
IcapError.ICAP_TIMEOUT_ERROR);
} catch (final IOException e) {
logger.error("Response cannot be read: {}", e.getMessage());
throw new IcapException("Response cannot be read", e,
IcapError.ICAP_NETWORK_ERROR);
}
if (reader == -1) {
logger.warn("Response is not complete while reading {}", offset);
throw new IcapException(
"Error in getHeader() method: response is not complete: " + offset,
IcapError.ICAP_SERVER_HEADER_WITHOUT_TERMINATOR);
}
logger.warn("Response cannot be read since size exceed maximum {}",
receiveLength);
throw new IcapException(
"Error in getHeader() method: received message too long",
IcapError.ICAP_SERVER_HEADER_EXCEED_CAPACITY);
}
/**
* Given a raw response header as a String, it will parse through it and return a HashMap of the result
*/
private Map<String, String> parseHeader(final String response) {
final Map<String, String> headers = new HashMap<String, String>();
/*
* SAMPLE:
* ICAP/1.0 204 Unmodified
* Server: C-ICAP/0.1.6
* Connection: keep-alive
* ISTag: CI0001-000-0978-6918203
*/
// The status code is located between the first 2 whitespaces.
// Read status code
final int x = response.indexOf(' ');
final int y = response.indexOf(' ', x + 2);
final String statusCode = response.substring(x + 1, y);
headers.put(STATUS_CODE, statusCode);
// Each line in the sample is ended with "\r\n".
// When (i+2==response.length()) The end of the header have been reached.
// The +=2 is added to skip the "\r\n".
// Read headers
int i = response.indexOf(TERMINATOR, y);
i += 2;
while (i + 2 < response.length() && response.substring(i).contains(":")) {
int n = response.indexOf(':', i);
final String key = response.substring(i, n).trim();
n += 2;
i = response.indexOf(TERMINATOR, n);
final String value = response.substring(n, i).trim();
headers.put(key, value);
i += 2;
}
logger.debug("RECV ICAP Headers:\n{}", headers);
return headers;
}
/**
* Sends a String through the socket connection. Used for sending ICAP/HTTP headers.
*
* @param requestHeader to send
*
* @throws IcapException if a network error is raised
*/
private void sendString(final String requestHeader) throws IcapException {
sendString(requestHeader, false);
}
/**
* Sends a String through the socket connection. Used for sending ICAP/HTTP headers.
*
* @param requestHeader to send
* @param withFlush if flush is necessary
*
* @throws IcapException if a network error is raised
*/
private void sendString(final String requestHeader, final boolean withFlush)
throws IcapException {
try {
out.write(requestHeader.getBytes(WaarpStringUtils.UTF8));
if (withFlush) {
out.flush();
}
} catch (final SocketTimeoutException e) {
logger.error(TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE, serverIP, port,
e.getMessage());
throw new IcapException(TIMEOUT_OCCURS_WITH_THE_SERVER, e,
IcapError.ICAP_TIMEOUT_ERROR);
} catch (final IOException e) {
logger.error("Client cannot communicate with ICAP Server: {}",
e.getMessage());
throw new IcapException("Client cannot communicate with ICAP Server", e,
IcapError.ICAP_NETWORK_ERROR);
}
}
/**
* Sends bytes of data from a byte-array through the socket connection.
*
* @param chunk The byte-array to send
*
* @throws IcapException if a network error is raised
*/
private void sendBytes(final byte[] chunk, final int length)
throws IcapException {
try {
out.write(chunk, 0, length);
} catch (final SocketTimeoutException e) {
logger.error(TIMEOUT_OCCURS_WITH_THE_SERVER_SINCE, serverIP, port,
e.getMessage());
throw new IcapException(TIMEOUT_OCCURS_WITH_THE_SERVER, e,
IcapError.ICAP_TIMEOUT_ERROR);
} catch (final IOException e) {
logger.error("Client cannot communicate with ICAP Server: {}",
e.getMessage());
throw new IcapException("Writing to ICAP Server cannot be done", e,
IcapError.ICAP_NETWORK_ERROR);
}
}
}