summaryrefslogtreecommitdiffstats
path: root/lib/cepis/CurlTransport.php
blob: d22949b140b5046edbcbc275e056dda910509243 (plain)
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
<?php

declare(strict_types=1);

namespace Cepis;

/**
 * The default {@see Transport}, built on ext-curl.
 *
 * curl is used directly rather than through an HTTP abstraction: this library
 * makes one GET, and a wrapper for that would cost dependencies without buying
 * anything. Hosts that need their own HTTP layer implement {@see Transport}
 * instead.
 *
 * Each call opens its own handle, so one instance is safe to share.
 */
final class CurlTransport implements Transport
{
    /**
     * @param bool $verifyTls Whether to verify the peer's certificate. Leave
     *     it alone. It exists for a lab against a self-signed local instance,
     *     and turning it off on the public API removes the only thing making
     *     the answer trustworthy.
     * @param int  $maxRedirects How many redirects to follow.
     */
    public function __construct(
        public readonly bool $verifyTls = true,
        public readonly int $maxRedirects = 3,
    ) {
    }

    public function get(string $url, array $headers, float $timeout): Response
    {
        $handle = curl_init();
        if ($handle === false) {
            throw new NetworkException('could not initialise a curl handle');
        }

        $lines = [];
        foreach ($headers as $name => $value) {
            $lines[] = $name . ': ' . $value;
        }

        $milliseconds = max(1, (int) round($timeout * 1000));
        $received = [];

        curl_setopt_array($handle, [
            CURLOPT_URL => $url,
            CURLOPT_HTTPGET => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $lines,
            CURLOPT_CONNECTTIMEOUT_MS => $milliseconds,
            CURLOPT_TIMEOUT_MS => $milliseconds,
            CURLOPT_FOLLOWLOCATION => $this->maxRedirects > 0,
            CURLOPT_MAXREDIRS => $this->maxRedirects,
            CURLOPT_SSL_VERIFYPEER => $this->verifyTls,
            CURLOPT_SSL_VERIFYHOST => $this->verifyTls ? 2 : 0,
            CURLOPT_NOSIGNAL => true,
            CURLOPT_HEADERFUNCTION => static function ($_handle, string $line) use (&$received): int {
                $length = strlen($line);
                $parts = explode(':', $line, 2);
                if (count($parts) === 2) {
                    $received[trim($parts[0])][] = trim($parts[1]);
                }

                return $length;
            },
        ]);

        $body = curl_exec($handle);
        $error = curl_error($handle);
        $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
        unset($handle);

        if ($body === false || $status === 0) {
            throw new NetworkException(sprintf(
                'could not reach %s: %s',
                $url,
                $error !== '' ? $error : 'the request did not complete',
            ));
        }

        return new Response($status, $received, (string) $body);
    }
}