Skip to content

Commit

Permalink
Stop reading the response after headers are read
Browse files Browse the repository at this point in the history
  • Loading branch information
VicDeo committed May 25, 2021
1 parent 8558e0d commit d6d5907
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 87 deletions.
162 changes: 78 additions & 84 deletions lib/Scanner/ICAPClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,28 +95,13 @@ public function getRequest(string $method, string $service, array $body = [], ar
return $request;
}

public function options(string $service): array {
$request = $this->getRequest('OPTIONS', $service);
$response = $this->send($request);

return $this->parseResponse($response);
}

public function respmod(string $service, array $body = [], array $headers = []): array {
$request = $this->getRequest('RESPMOD', $service, $body, $headers);
$response = $this->send($request);

return $this->parseResponse($response);
}

public function reqmod(string $service, array $body = [], array $headers = []): array {
$request = $this->getRequest('REQMOD', $service, $body, $headers);
$response = $this->send($request);

return $this->parseResponse($response);
return $response;
}

private function send(string $request): string {
private function send(string $request): array {
$this->connect();
// Shut stupid uncontrolled messaging up - we handle errors on our own
if (@\fwrite($this->writeHandle, $request) === false) {
Expand All @@ -125,90 +110,99 @@ private function send(string $request): string {
);
}

# McAfee seems to not properly close the socket once all response bytes are sent to the client
# we use a 10 sec time out on receiving data
\stream_set_timeout($this->writeHandle, 10, 0);
$response = '';
while ($buffer = \fread($this->writeHandle, 2048)) {
$response .= $buffer;
// read header
$protocol = $this->readIcapStatusLine();
$headers = $this->readHeaders();
$resHdr = [];
if (isset($headers['Encapsulated'])) {
$resHdr = $this->parseResHdr($headers['Encapsulated']);
}

$this->disconnect();
return $response;
return [
'protocol' => $protocol,
'headers' => $headers,
'body' => ['res-hdr' => $resHdr]
];
}

private function parseResponse(string $response): array {
$responseArray = [
'protocol' => [],
'headers' => [],
'body' => [],
'rawBody' => ''
private function readIcapStatusLine(): array {
$icapHeader = \trim(\fgets($this->writeHandle));
$numValues = \sscanf($icapHeader, "ICAP/%d.%d %d %s", $v1, $v2, $code, $status);
if ($numValues !== 4) {
throw new RuntimeException("Unknown ICAP response: \"$icapHeader\"");
}
return [
'protocolVersion' => "$v1.$v2",
'code' => $code,
'status' => $status,
];
}

foreach (\preg_split('/\r?\n/', $response) as $line) {
if ($responseArray['protocol'] === []) {
if (\strpos($line, 'ICAP/') !== 0) {
throw new RuntimeException("Unknown ICAP response: \"$response\"");
}

$parts = \preg_split('/\ +/', $line, 3);

$responseArray['protocol'] = [
'icap' => $parts[0] ?? '',
'code' => $parts[1] ?? '',
'message' => $parts[2] ?? '',
];

private function parseResHdr(string $headerValue): array {
$encapsulatedHeaders = [];
$encapsulatedParts = \explode(",", $headerValue);
foreach ($encapsulatedParts as $encapsulatedPart) {
$pieces = \explode("=", \trim($encapsulatedPart));
if ($pieces[1] === "0") {
continue;
}
$rawEncapsulatedHeaders = \fread($this->writeHandle, $pieces[1]);
$encapsulatedHeaders = $this->parseEncapsulatedHeaders($rawEncapsulatedHeaders);
// According to the spec we have a single res-hdr part and are not interested in res-body content
break;
}
return $encapsulatedHeaders;
}

if ($line === '') {
private function readHeaders(): array {
$headers = [];
$prevString = "";
while ($headerString = \fgets($this->writeHandle)) {
$trimmedHeaderString = \trim($headerString);
if ($prevString === "" && $trimmedHeaderString === "") {
break;
}

$parts = \preg_split('/:\ /', $line, 2);
if (isset($parts[0])) {
$responseArray['headers'][$parts[0]] = $parts[1] ?? '';
list($headerName, $headerValue) = $this->parseHeader($trimmedHeaderString);
if ($headerName !== '') {
$headers[$headerName] = $headerValue;
if ($headerName == "Encapsulated") {
break;
}
}
$prevString = $trimmedHeaderString;
}
return $headers;
}

$body = \preg_split('/\r?\n\r?\n/', $response, 2);
if (isset($body[1])) {
$responseArray['rawBody'] = $body[1];

if (\array_key_exists('Encapsulated', $responseArray['headers'])) {
$encapsulated = [];
$params = \explode(", ", $responseArray['headers']['Encapsulated']);

foreach ($params as $param) {
$parts = \explode("=", $param);
if (\count($parts) !== 2) {
continue;
}

$encapsulated[$parts[0]] = $parts[1];
}

foreach ($encapsulated as $section => $offset) {
$data = \substr($body[1], (int)$offset);
switch ($section) {
case 'req-hdr':
case 'res-hdr':
$responseArray['body'][$section] = \preg_split('/\r?\n\r?\n/', $data, 2)[0];
break;

case 'req-body':
case 'res-body':
$parts = \preg_split('/\r?\n/', $data, 2);
if (\count($parts) === 2) {
$responseArray['body'][$section] = \substr($parts[1], 0, \hexdec($parts[0]));
}
break;
}
}
private function parseEncapsulatedHeaders(string $headerString) : array {
$headers = [];
$split = \preg_split('/\r?\n/', \trim($headerString));
$statusLine = array_shift($split);
if ($statusLine !== null) {
$headers['HTTP_STATUS'] = $statusLine;
}
foreach (\preg_split('/\r?\n/', $headerString) as $line) {
if ($line === '') {
continue;
}
list($name, $value) = $this->parseHeader($line);
if ($name !== '') {
$headers[$name] = $value;
}
}

return $responseArray;
return $headers;
}

private function parseHeader(string $headerString): array {
$name = '';
$value = '';
$parts = \preg_split('/:\ /', $headerString, 2);
if (isset($parts[0])) {
$name = $parts[0];
$value = $parts[1] ?? '';
}
return [$name, $value];
}
}
6 changes: 3 additions & 3 deletions lib/Scanner/ICAPScanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,16 @@ public function completeAsyncScan() {
], [
'Allow' => 204
]);
$code = $response['protocol']['code'] ?? '500';
if ($code === '200' || $code === '204') {
$code = $response['protocol']['code'] ?? 500;
if ($code === 200 || $code === 204) {
// c-icap/clamav reports this header
$virus = $response['headers'][$this->virusHeader] ?? false;
if ($virus) {
return Status::create(Status::SCANRESULT_INFECTED, $virus);
}

// kaspersky(pre 2020 product editions) and McAfee handling
$respHeader = $response['body']['res-hdr'] ?? '';
$respHeader = $response['body']['res-hdr']['HTTP_STATUS'] ?? '';
if (\strpos($respHeader, '403 Forbidden') || \strpos($respHeader, '403 VirusFound')) {
$message = $this->l10n->t('A malware or virus was detected, your upload was deleted. In doubt or for details please contact your system administrator');
return Status::create(Status::SCANRESULT_INFECTED, $message);
Expand Down
1 change: 1 addition & 0 deletions tests/unit/Scanner/IcapScannerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ protected function setUp(): void {

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

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

0 comments on commit d6d5907

Please sign in to comment.