ClamAVClient.java
6.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package fi.solita.clamav;
/**
* Created by dario on 19/10/17.
*/
import java.io.*;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* Simple client for ClamAV's clamd scanner. Provides straightforward instream scanning.
*/
public class ClamAVClient {
private String hostName;
private int port;
private int timeout;
// "do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will reply with INSTREAM size limit exceeded and close the connection."
private static final int CHUNK_SIZE = 2048;
private static final int DEFAULT_TIMEOUT = 500;
private static final int PONG_REPLY_LEN = 4;
/**
* @param hostName The hostname of the server running clamav-daemon
* @param port The port that clamav-daemon listens to(By default it might not listen to a port. Check your clamav configuration).
* @param timeout zero means infinite timeout. Not a good idea, but will be accepted.
*/
public ClamAVClient(String hostName, int port, int timeout) {
if (timeout < 0) {
throw new IllegalArgumentException("Negative timeout value does not make sense.");
}
this.hostName = hostName;
this.port = port;
this.timeout = timeout;
}
public ClamAVClient(String hostName, int port) {
this(hostName, port, DEFAULT_TIMEOUT);
}
/**
* Run PING command to clamd to test it is responding.
*
* @return true if the server responded with proper ping reply.
*/
public boolean ping() throws IOException {
try (Socket s = new Socket(hostName,port); OutputStream outs = s.getOutputStream()) {
s.setSoTimeout(timeout);
outs.write(asBytes("zPING\0"));
outs.flush();
byte[] b = new byte[PONG_REPLY_LEN];
InputStream inputStream = s.getInputStream();
int copyIndex = 0;
int readResult;
do {
readResult = inputStream.read(b, copyIndex, Math.max(b.length - copyIndex, 0));
copyIndex += readResult;
} while (readResult > 0);
return Arrays.equals(b, asBytes("PONG"));
}
}
/**
* Streams the given data to the server in chunks. The whole data is not kept in memory.
* This method is preferred if you don't want to keep the data in memory, for instance by scanning a file on disk.
* Since the parameter InputStream is not reset, you can not use the stream afterwards, as it will be left in a EOF-state.
* If your goal is to scan some data, and then pass that data further, consider using {@link #scan(byte[]) scan(byte[] in)}.
* <p>
* Opens a socket and reads the reply. Parameter input stream is NOT closed.
*
* @param is data to scan. Not closed by this method!
* @return server reply
*/
public byte[] scan(InputStream is) throws IOException {
try (Socket s = new Socket(hostName,port); OutputStream outs = new BufferedOutputStream(s.getOutputStream())) {
s.setSoTimeout(timeout);
// handshake
outs.write(asBytes("zINSTREAM\0"));
outs.flush();
byte[] chunk = new byte[CHUNK_SIZE];
try (InputStream clamIs = s.getInputStream()) {
// send data
int read = is.read(chunk);
while (read >= 0) {
// The format of the chunk is: '<length><data>' where <length> is the size of the following data in bytes expressed as a 4 byte unsigned
// integer in network byte order and <data> is the actual chunk. Streaming is terminated by sending a zero-length chunk.
byte[] chunkSize = ByteBuffer.allocate(4).putInt(read).array();
outs.write(chunkSize);
outs.write(chunk, 0, read);
if (clamIs.available() > 0) {
// reply from server before scan command has been terminated.
byte[] reply = assertSizeLimit(readAll(clamIs));
throw new IOException("Scan aborted. Reply from server: " + new String(reply, StandardCharsets.US_ASCII));
}
read = is.read(chunk);
}
// terminate scan
outs.write(new byte[]{0,0,0,0});
outs.flush();
// read reply
return assertSizeLimit(readAll(clamIs));
}
}
}
/**
* Scans bytes for virus by passing the bytes to clamav
*
* @param in data to scan
* @return server reply
**/
public byte[] scan(byte[] in) throws IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(in);
return scan(bis);
}
/**
* Interpret the result from a ClamAV scan, and determine if the result means the data is clean
*
* @param reply The reply from the server after scanning
* @return true if no virus was found according to the clamd reply message
*/
public static boolean isCleanReply(byte[] reply) {
String r = new String(reply, StandardCharsets.US_ASCII);
return (r.contains("OK") && !r.contains("FOUND"));
}
private byte[] assertSizeLimit(byte[] reply) {
String r = new String(reply, StandardCharsets.US_ASCII);
if (r.startsWith("INSTREAM size limit exceeded."))
throw new ClamAVSizeLimitException("Clamd size limit exceeded. Full reply from server: " + r);
return reply;
}
// byte conversion based on ASCII character set regardless of the current system locale
private static byte[] asBytes(String s) {
return s.getBytes(StandardCharsets.US_ASCII);
}
// reads all available bytes from the stream
private static byte[] readAll(InputStream is) throws IOException {
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
byte[] buf = new byte[2000];
int read = 0;
do {
read = is.read(buf);
tmp.write(buf, 0, read);
} while ((read > 0) && (is.available() > 0));
return tmp.toByteArray();
}
}