Skip to content

Commit ad499ee

Browse files
authored
Merge pull request #445 from owncloud/bugfix/443
[ICAP] Stop reading the response after headers are read
2 parents 45084d4 + 7bc4744 commit ad499ee

File tree

5 files changed

+95
-87
lines changed

5 files changed

+95
-87
lines changed

l10n/es.js

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ OC.L10N.register(
1616
"Saved" : "Guardado",
1717
"Virus detected! Can't upload the file %s" : "¡Virus detectado! No se puede cargar el archivo %s",
1818
"Malware detected" : "Malware detectado",
19+
"A malware or virus was detected, your upload was deleted. In doubt or for details please contact your system administrator" : "Se detectó un malware o virus, su carga se eliminó. En caso de duda o para obtener más detalles, póngase en contacto con el administrador del sistema",
1920
"Greetings {user}," : "Hola, {user}:",
2021
"Sorry, but a malware was detected in a file you tried to upload and it had to be deleted." : "Lo sentimos, pero se ha detectado un malware en un archivo que trata de subir y tuvo que ser eliminado.",
2122
"This email is a notification from {host}. Please, do not reply." : "Esta es una notificación automática de {host}. Sírvase no responderla.",
@@ -25,12 +26,15 @@ OC.L10N.register(
2526
"Executable" : "Ejecutable",
2627
"Daemon" : "Demonio",
2728
"Daemon (Socket)" : "Demonio (Socket)",
29+
"Daemon (ICAP)" : "Daemon (ICAP)",
2830
"Socket" : "Socket",
2931
"Clamav Socket" : "Clamav Socket",
3032
"Host" : "Servidor",
3133
"Hostname or IP address of Antivirus Host" : "Nombre del host o dirección IP del host del Antivirus.",
3234
"Port" : "Puerto",
3335
"Port number of Antivirus Host, 1-65535" : "Puerto del host del antivirus, 1-65535",
36+
"ICAP request service. Possible values: \"avscan\" for clamav or \"req\" for Kaspersky ScanEngine" : "Servicio de solicitud ICAP. Valores posibles: \"avscan\" para clamav o \"req\" para Kaspersky ScanEngine",
37+
"ICAP response header holding the virus information. Possible values: X-Virus-ID or X-Infection-Found" : "Encabezado de respuesta ICAP, que contiene la información del virus. Valores posibles: X-Virus-ID o X-Infection-Found",
3438
"Path to clamscan" : "Ruta al clamscan",
3539
"Path to clamscan executable" : "Ruta al ejecutable de ClamScan",
3640
"Extra command line options (comma-separated)" : "Opciones extra de la línea de comandos (separadas por comas)",

l10n/es.json

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"Saved" : "Guardado",
1515
"Virus detected! Can't upload the file %s" : "¡Virus detectado! No se puede cargar el archivo %s",
1616
"Malware detected" : "Malware detectado",
17+
"A malware or virus was detected, your upload was deleted. In doubt or for details please contact your system administrator" : "Se detectó un malware o virus, su carga se eliminó. En caso de duda o para obtener más detalles, póngase en contacto con el administrador del sistema",
1718
"Greetings {user}," : "Hola, {user}:",
1819
"Sorry, but a malware was detected in a file you tried to upload and it had to be deleted." : "Lo sentimos, pero se ha detectado un malware en un archivo que trata de subir y tuvo que ser eliminado.",
1920
"This email is a notification from {host}. Please, do not reply." : "Esta es una notificación automática de {host}. Sírvase no responderla.",
@@ -23,12 +24,15 @@
2324
"Executable" : "Ejecutable",
2425
"Daemon" : "Demonio",
2526
"Daemon (Socket)" : "Demonio (Socket)",
27+
"Daemon (ICAP)" : "Daemon (ICAP)",
2628
"Socket" : "Socket",
2729
"Clamav Socket" : "Clamav Socket",
2830
"Host" : "Servidor",
2931
"Hostname or IP address of Antivirus Host" : "Nombre del host o dirección IP del host del Antivirus.",
3032
"Port" : "Puerto",
3133
"Port number of Antivirus Host, 1-65535" : "Puerto del host del antivirus, 1-65535",
34+
"ICAP request service. Possible values: \"avscan\" for clamav or \"req\" for Kaspersky ScanEngine" : "Servicio de solicitud ICAP. Valores posibles: \"avscan\" para clamav o \"req\" para Kaspersky ScanEngine",
35+
"ICAP response header holding the virus information. Possible values: X-Virus-ID or X-Infection-Found" : "Encabezado de respuesta ICAP, que contiene la información del virus. Valores posibles: X-Virus-ID o X-Infection-Found",
3236
"Path to clamscan" : "Ruta al clamscan",
3337
"Path to clamscan executable" : "Ruta al ejecutable de ClamScan",
3438
"Extra command line options (comma-separated)" : "Opciones extra de la línea de comandos (separadas por comas)",

lib/Scanner/ICAPClient.php

+83-84
Original file line numberDiff line numberDiff line change
@@ -95,28 +95,13 @@ public function getRequest(string $method, string $service, array $body = [], ar
9595
return $request;
9696
}
9797

98-
public function options(string $service): array {
99-
$request = $this->getRequest('OPTIONS', $service);
100-
$response = $this->send($request);
101-
102-
return $this->parseResponse($response);
103-
}
104-
105-
public function respmod(string $service, array $body = [], array $headers = []): array {
106-
$request = $this->getRequest('RESPMOD', $service, $body, $headers);
107-
$response = $this->send($request);
108-
109-
return $this->parseResponse($response);
110-
}
111-
11298
public function reqmod(string $service, array $body = [], array $headers = []): array {
11399
$request = $this->getRequest('REQMOD', $service, $body, $headers);
114100
$response = $this->send($request);
115-
116-
return $this->parseResponse($response);
101+
return $response;
117102
}
118103

119-
private function send(string $request): string {
104+
private function send(string $request): array {
120105
$this->connect();
121106
// Shut stupid uncontrolled messaging up - we handle errors on our own
122107
if (@\fwrite($this->writeHandle, $request) === false) {
@@ -125,90 +110,104 @@ private function send(string $request): string {
125110
);
126111
}
127112

128-
# McAfee seems to not properly close the socket once all response bytes are sent to the client
129-
# we use a 10 sec time out on receiving data
130-
\stream_set_timeout($this->writeHandle, 10, 0);
131-
$response = '';
132-
while ($buffer = \fread($this->writeHandle, 2048)) {
133-
$response .= $buffer;
113+
$headers = [];
114+
$resHdr = [];
115+
$protocol = $this->readIcapStatusLine();
116+
117+
// McAfee seems to not properly close the socket once all response bytes are sent to the client
118+
// So if ICAP status is 204 we just stop reading
119+
if ($protocol['code'] !== 204) {
120+
$headers = $this->readHeaders();
121+
if (isset($headers['Encapsulated'])) {
122+
$resHdr = $this->parseResHdr($headers['Encapsulated']);
123+
}
134124
}
135125

136126
$this->disconnect();
137-
return $response;
127+
return [
128+
'protocol' => $protocol,
129+
'headers' => $headers,
130+
'body' => ['res-hdr' => $resHdr]
131+
];
138132
}
139133

140-
private function parseResponse(string $response): array {
141-
$responseArray = [
142-
'protocol' => [],
143-
'headers' => [],
144-
'body' => [],
145-
'rawBody' => ''
134+
private function readIcapStatusLine(): array {
135+
$icapHeader = \trim(\fgets($this->writeHandle));
136+
$numValues = \sscanf($icapHeader, "ICAP/%d.%d %d %s", $v1, $v2, $code, $status);
137+
if ($numValues !== 4) {
138+
throw new RuntimeException("Unknown ICAP response: \"$icapHeader\"");
139+
}
140+
return [
141+
'protocolVersion' => "$v1.$v2",
142+
'code' => $code,
143+
'status' => $status,
146144
];
145+
}
147146

148-
foreach (\preg_split('/\r?\n/', $response) as $line) {
149-
if ($responseArray['protocol'] === []) {
150-
if (\strpos($line, 'ICAP/') !== 0) {
151-
throw new RuntimeException("Unknown ICAP response: \"$response\"");
152-
}
153-
154-
$parts = \preg_split('/\ +/', $line, 3);
155-
156-
$responseArray['protocol'] = [
157-
'icap' => $parts[0] ?? '',
158-
'code' => $parts[1] ?? '',
159-
'message' => $parts[2] ?? '',
160-
];
161-
147+
private function parseResHdr(string $headerValue): array {
148+
$encapsulatedHeaders = [];
149+
$encapsulatedParts = \explode(",", $headerValue);
150+
foreach ($encapsulatedParts as $encapsulatedPart) {
151+
$pieces = \explode("=", \trim($encapsulatedPart));
152+
if ($pieces[1] === "0") {
162153
continue;
163154
}
155+
$rawEncapsulatedHeaders = \fread($this->writeHandle, $pieces[1]);
156+
$encapsulatedHeaders = $this->parseEncapsulatedHeaders($rawEncapsulatedHeaders);
157+
// According to the spec we have a single res-hdr part and are not interested in res-body content
158+
break;
159+
}
160+
return $encapsulatedHeaders;
161+
}
164162

165-
if ($line === '') {
163+
private function readHeaders(): array {
164+
$headers = [];
165+
$prevString = "";
166+
while ($headerString = \fgets($this->writeHandle)) {
167+
$trimmedHeaderString = \trim($headerString);
168+
if ($prevString === "" && $trimmedHeaderString === "") {
166169
break;
167170
}
168-
169-
$parts = \preg_split('/:\ /', $line, 2);
170-
if (isset($parts[0])) {
171-
$responseArray['headers'][$parts[0]] = $parts[1] ?? '';
171+
list($headerName, $headerValue) = $this->parseHeader($trimmedHeaderString);
172+
if ($headerName !== '') {
173+
$headers[$headerName] = $headerValue;
174+
if ($headerName == "Encapsulated") {
175+
break;
176+
}
172177
}
178+
$prevString = $trimmedHeaderString;
173179
}
180+
return $headers;
181+
}
174182

175-
$body = \preg_split('/\r?\n\r?\n/', $response, 2);
176-
if (isset($body[1])) {
177-
$responseArray['rawBody'] = $body[1];
178-
179-
if (\array_key_exists('Encapsulated', $responseArray['headers'])) {
180-
$encapsulated = [];
181-
$params = \explode(", ", $responseArray['headers']['Encapsulated']);
182-
183-
foreach ($params as $param) {
184-
$parts = \explode("=", $param);
185-
if (\count($parts) !== 2) {
186-
continue;
187-
}
188-
189-
$encapsulated[$parts[0]] = $parts[1];
190-
}
191-
192-
foreach ($encapsulated as $section => $offset) {
193-
$data = \substr($body[1], (int)$offset);
194-
switch ($section) {
195-
case 'req-hdr':
196-
case 'res-hdr':
197-
$responseArray['body'][$section] = \preg_split('/\r?\n\r?\n/', $data, 2)[0];
198-
break;
199-
200-
case 'req-body':
201-
case 'res-body':
202-
$parts = \preg_split('/\r?\n/', $data, 2);
203-
if (\count($parts) === 2) {
204-
$responseArray['body'][$section] = \substr($parts[1], 0, \hexdec($parts[0]));
205-
}
206-
break;
207-
}
208-
}
183+
private function parseEncapsulatedHeaders(string $headerString) : array {
184+
$headers = [];
185+
$split = \preg_split('/\r?\n/', \trim($headerString));
186+
$statusLine = \array_shift($split);
187+
if ($statusLine !== null) {
188+
$headers['HTTP_STATUS'] = $statusLine;
189+
}
190+
foreach (\preg_split('/\r?\n/', $headerString) as $line) {
191+
if ($line === '') {
192+
continue;
193+
}
194+
list($name, $value) = $this->parseHeader($line);
195+
if ($name !== '') {
196+
$headers[$name] = $value;
209197
}
210198
}
211199

212-
return $responseArray;
200+
return $headers;
201+
}
202+
203+
private function parseHeader(string $headerString): array {
204+
$name = '';
205+
$value = '';
206+
$parts = \preg_split('/:\ /', $headerString, 2);
207+
if (isset($parts[0])) {
208+
$name = $parts[0];
209+
$value = $parts[1] ?? '';
210+
}
211+
return [$name, $value];
213212
}
214213
}

lib/Scanner/ICAPScanner.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,16 @@ public function completeAsyncScan() {
5353
], [
5454
'Allow' => 204
5555
]);
56-
$code = $response['protocol']['code'] ?? '500';
57-
if ($code === '200' || $code === '204') {
56+
$code = $response['protocol']['code'] ?? 500;
57+
if ($code === 200 || $code === 204) {
5858
// c-icap/clamav reports this header
5959
$virus = $response['headers'][$this->virusHeader] ?? false;
6060
if ($virus) {
6161
return Status::create(Status::SCANRESULT_INFECTED, $virus);
6262
}
6363

6464
// kaspersky(pre 2020 product editions) and McAfee handling
65-
$respHeader = $response['body']['res-hdr'] ?? '';
65+
$respHeader = $response['body']['res-hdr']['HTTP_STATUS'] ?? '';
6666
if (\strpos($respHeader, '403 Forbidden') || \strpos($respHeader, '403 VirusFound')) {
6767
$message = $this->l10n->t('A malware or virus was detected, your upload was deleted. In doubt or for details please contact your system administrator');
6868
return Status::create(Status::SCANRESULT_INFECTED, $message);

tests/unit/Scanner/IcapScannerTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ protected function setUp(): void {
3333

3434
$logger = $this->createMock(ILogger::class);
3535
$l10n = $this->createMock(IL10N::class);
36+
$l10n->method('t')->will($this->returnArgument(0));
3637

3738
# for local testing replace 'icap' with the ip of the clamav instance
3839
$config->method('getAvHost')->willReturn('icap');

0 commit comments

Comments
 (0)