Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
1 / 1
Config
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
13 / 13
20
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSocketTimeout
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStreamTimeout
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVirusFoundHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVirusFoundHeaders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withVirusFoundHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withVirusFoundHeaders
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 getTlsContext
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withTlsContext
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 getMaxResponseSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxHeaderCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxHeaderLineLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withLimits
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3/**
4 * SPDX-License-Identifier: EUPL-1.2
5 *
6 * This file is part of icap-flow.
7 *
8 * Licensed under the EUPL, Version 1.2 only (the "Licence");
9 * you may not use this work except in compliance with the Licence.
10 * You may obtain a copy of the Licence at:
11 *
12 *     https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the Licence is distributed on an "AS IS" basis,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 */
18
19declare(strict_types=1);
20
21namespace Ndrstmr\Icap;
22
23use Amp\Socket\ClientTlsContext;
24
25/**
26 * Transport, timeout, security and DoS-limit configuration for the
27 * ICAP clients.
28 */
29final readonly class Config
30{
31    private const int DEFAULT_MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
32    private const int DEFAULT_MAX_HEADER_COUNT = 100;
33    private const int DEFAULT_MAX_HEADER_LINE = 8192;
34
35    /** @var list<string> */
36    private array $virusFoundHeaders;
37
38    /**
39     * @param string                $host                Hostname or IP of the ICAP server
40     * @param int                   $port                TCP port, defaults to 1344 (11344 is the common TLS port)
41     * @param float                 $socketTimeout       Timeout in seconds for establishing the connection
42     * @param float                 $streamTimeout       Timeout in seconds for reading/writing
43     * @param string                $virusFoundHeader    Legacy single virus header (back-compat). The client now
44     *                                                   consults a list; this value seeds it when
45     *                                                   $virusFoundHeaders is null.
46     * @param ClientTlsContext|null $tlsContext          When set, the async transport upgrades the connection to TLS (icaps://)
47     * @param int                   $maxResponseSize     DoS ceiling for total bytes accepted from a single ICAP response
48     * @param int                   $maxHeaderCount      DoS ceiling for total header lines in the ICAP head block
49     * @param int                   $maxHeaderLineLength DoS ceiling for a single header line length (bytes)
50     * @param list<string>|null     $virusFoundHeaders   Ordered list of vendor virus-name headers;
51     *                                                   null falls back to [$virusFoundHeader].
52     */
53    public function __construct(
54        public string $host,
55        public int $port = 1344,
56        private float $socketTimeout = 10.0,
57        private float $streamTimeout = 10.0,
58        string $virusFoundHeader = 'X-Virus-Name',
59        private ?ClientTlsContext $tlsContext = null,
60        private int $maxResponseSize = self::DEFAULT_MAX_RESPONSE_SIZE,
61        private int $maxHeaderCount = self::DEFAULT_MAX_HEADER_COUNT,
62        private int $maxHeaderLineLength = self::DEFAULT_MAX_HEADER_LINE,
63        ?array $virusFoundHeaders = null,
64    ) {
65        $this->virusFoundHeaders = $virusFoundHeaders ?? [$virusFoundHeader];
66    }
67
68    public function getSocketTimeout(): float
69    {
70        return $this->socketTimeout;
71    }
72
73    public function getStreamTimeout(): float
74    {
75        return $this->streamTimeout;
76    }
77
78    /**
79     * Legacy accessor — returns the first configured virus header.
80     * Prefer {@see getVirusFoundHeaders()} for new code; this method
81     * stays for back-compat with v1 callers.
82     */
83    public function getVirusFoundHeader(): string
84    {
85        return $this->virusFoundHeaders[0];
86    }
87
88    /**
89     * Ordered list of ICAP headers inspected for infection signals.
90     * The first header that is present in the server's response wins.
91     * Different vendors use different headers — c-icap / ClamAV use
92     * `X-Virus-Name`; Trend Micro reports via `X-Violations-Found`,
93     * ISS Proventia via `X-Infection-Found`, Symantec via `X-Virus-ID`.
94     *
95     * @return list<string>
96     */
97    public function getVirusFoundHeaders(): array
98    {
99        return $this->virusFoundHeaders;
100    }
101
102    public function withVirusFoundHeader(string $headerName): self
103    {
104        return $this->withVirusFoundHeaders([$headerName]);
105    }
106
107    /**
108     * @param list<string> $headerNames
109     */
110    public function withVirusFoundHeaders(array $headerNames): self
111    {
112        if ($headerNames === []) {
113            throw new \InvalidArgumentException('virusFoundHeaders must contain at least one header name.');
114        }
115
116        return new self(
117            host: $this->host,
118            port: $this->port,
119            socketTimeout: $this->socketTimeout,
120            streamTimeout: $this->streamTimeout,
121            virusFoundHeader: $headerNames[0],
122            tlsContext: $this->tlsContext,
123            maxResponseSize: $this->maxResponseSize,
124            maxHeaderCount: $this->maxHeaderCount,
125            maxHeaderLineLength: $this->maxHeaderLineLength,
126            virusFoundHeaders: $headerNames,
127        );
128    }
129
130    public function getTlsContext(): ?ClientTlsContext
131    {
132        return $this->tlsContext;
133    }
134
135    /**
136     * Return a new instance with the supplied TLS context. Pass an
137     * amphp/socket {@see ClientTlsContext}; the async transport will
138     * upgrade the connection via `Socket\connectTls()`.
139     */
140    public function withTlsContext(?ClientTlsContext $tlsContext): self
141    {
142        return new self(
143            host: $this->host,
144            port: $this->port,
145            socketTimeout: $this->socketTimeout,
146            streamTimeout: $this->streamTimeout,
147            virusFoundHeader: $this->virusFoundHeaders[0],
148            tlsContext: $tlsContext,
149            maxResponseSize: $this->maxResponseSize,
150            maxHeaderCount: $this->maxHeaderCount,
151            maxHeaderLineLength: $this->maxHeaderLineLength,
152            virusFoundHeaders: $this->virusFoundHeaders,
153        );
154    }
155
156    public function getMaxResponseSize(): int
157    {
158        return $this->maxResponseSize;
159    }
160
161    public function getMaxHeaderCount(): int
162    {
163        return $this->maxHeaderCount;
164    }
165
166    public function getMaxHeaderLineLength(): int
167    {
168        return $this->maxHeaderLineLength;
169    }
170
171    /**
172     * Return a new instance with DoS limits overridden. Passing null
173     * leaves a limit at its current value.
174     */
175    public function withLimits(
176        ?int $maxResponseSize = null,
177        ?int $maxHeaderCount = null,
178        ?int $maxHeaderLineLength = null,
179    ): self {
180        if ($maxResponseSize !== null && $maxResponseSize < 1) {
181            throw new \InvalidArgumentException('maxResponseSize must be >= 1, got: ' . $maxResponseSize);
182        }
183        if ($maxHeaderCount !== null && $maxHeaderCount < 1) {
184            throw new \InvalidArgumentException('maxHeaderCount must be >= 1, got: ' . $maxHeaderCount);
185        }
186        if ($maxHeaderLineLength !== null && $maxHeaderLineLength < 1) {
187            throw new \InvalidArgumentException('maxHeaderLineLength must be >= 1, got: ' . $maxHeaderLineLength);
188        }
189
190        return new self(
191            host: $this->host,
192            port: $this->port,
193            socketTimeout: $this->socketTimeout,
194            streamTimeout: $this->streamTimeout,
195            virusFoundHeader: $this->virusFoundHeaders[0],
196            tlsContext: $this->tlsContext,
197            maxResponseSize: $maxResponseSize ?? $this->maxResponseSize,
198            maxHeaderCount: $maxHeaderCount ?? $this->maxHeaderCount,
199            maxHeaderLineLength: $maxHeaderLineLength ?? $this->maxHeaderLineLength,
200            virusFoundHeaders: $this->virusFoundHeaders,
201        );
202    }
203}