Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
41.03% covered (danger)
41.03%
16 / 39
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
SynchronousStreamTransport
41.03% covered (danger)
41.03%
16 / 39
0.00% covered (danger)
0.00%
0 / 1
25.61
0.00% covered (danger)
0.00%
0 / 1
 request
41.03% covered (danger)
41.03%
16 / 39
0.00% covered (danger)
0.00%
0 / 1
25.61
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\Transport;
22
23use Amp\Cancellation;
24use Ndrstmr\Icap\Config;
25use Ndrstmr\Icap\Exception\IcapConnectionException;
26
27/**
28 * Blocking transport using plain PHP stream sockets.
29 *
30 * TLS is explicitly NOT supported here — the synchronous transport is
31 * intended for quick CLI / test usage; production deployments should
32 * use {@see AsyncAmpTransport} (TLS, streaming, cancellations).
33 *
34 * Hardened per finding I of the consolidated review:
35 *   - connect + read/write timeouts come from Config, not a hard 5 s;
36 *   - every branch closes the socket via try/finally;
37 *   - the read loop enforces Config::maxResponseSize to defend
38 *     against a hostile server sending unbounded bytes.
39 */
40final class SynchronousStreamTransport implements TransportInterface
41{
42    private const int READ_CHUNK_SIZE = 8192;
43
44    /**
45     * @param iterable<string> $rawRequest
46     * @return \Amp\Future<string>
47     */
48    #[\Override]
49    public function request(Config $config, iterable $rawRequest, ?Cancellation $cancellation = null): \Amp\Future
50    {
51        if ($config->getTlsContext() !== null) {
52            throw new IcapConnectionException(
53                'SynchronousStreamTransport does not support TLS; use AsyncAmpTransport for icaps://.',
54            );
55        }
56
57        // Cancellation is honoured opportunistically: we check it
58        // between read/write iterations. The blocking PHP stream API
59        // doesn't allow interrupting an in-flight syscall, so a hung
60        // server is still bounded only by stream_set_timeout(). The
61        // async transport gives true cancellation semantics.
62        $cancellation?->throwIfRequested();
63
64        $errno = 0;
65        $errstr = '';
66        $address = sprintf('tcp://%s:%d', $config->host, $config->port);
67        $stream = @stream_socket_client(
68            $address,
69            $errno,
70            $errstr,
71            $config->getSocketTimeout(),
72            STREAM_CLIENT_CONNECT,
73        );
74        if ($stream === false) {
75            throw new IcapConnectionException(
76                sprintf('Connection to %s failed: %s', $address, $errstr !== '' ? $errstr : 'unknown error'),
77            );
78        }
79
80        try {
81            stream_set_timeout($stream, (int) $config->getStreamTimeout());
82
83            foreach ($rawRequest as $chunk) {
84                $cancellation?->throwIfRequested();
85                if ($chunk !== '') {
86                    fwrite($stream, $chunk);
87                }
88            }
89
90            $reader = new ResponseFrameReader(
91                maxResponseSize: $config->getMaxResponseSize(),
92                maxHeaderLineLength: $config->getMaxHeaderLineLength(),
93            );
94            $response = $reader->readFrom(static function () use ($stream, $cancellation): ?string {
95                $cancellation?->throwIfRequested();
96                if (feof($stream)) {
97                    return null;
98                }
99                $read = fread($stream, self::READ_CHUNK_SIZE);
100                if ($read === false || $read === '') {
101                    return null;
102                }
103                return $read;
104            });
105
106            return \Amp\Future::complete($response);
107        } finally {
108            fclose($stream);
109        }
110    }
111}