Client   A
last analyzed

Complexity

Total Complexity 18

Size/Duplication

Total Lines 132
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 1
Metric Value
wmc 18
eloc 62
c 5
b 0
f 1
dl 0
loc 132
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
A head() 0 3 1
A get() 0 3 1
A post() 0 5 1
A __construct() 0 2 1
B makeCurlRequest() 0 58 8
A getCurlHandle() 0 24 2
A parseRawHeaders() 0 16 3
A attachRequestPayload() 0 5 1
1
<?php
2
3
namespace Rogierw\RwAcme\Http;
4
5
use CurlHandle;
6
use Rogierw\RwAcme\Interfaces\HttpClientInterface;
7
8
class Client implements HttpClientInterface
9
{
10
    public function __construct(private readonly int $timeout = 10)
11
    {
12
    }
13
14
    public function head(string $url): Response
15
    {
16
        return $this->makeCurlRequest('head', $url);
17
    }
18
19
    public function get(string $url, array $headers = [], array $arguments = [], int $maxRedirects = 0): Response
20
    {
21
        return $this->makeCurlRequest('get', $url, $headers, $arguments, $maxRedirects);
22
    }
23
24
    public function post(string $url, array $payload = [], array $headers = [], int $maxRedirects = 0): Response
25
    {
26
        $headers = array_merge(['Content-Type: application/jose+json'], $headers);
27
28
        return $this->makeCurlRequest('post', $url, $headers, $payload, $maxRedirects);
29
    }
30
31
    private function makeCurlRequest(
32
        string $httpVerb,
33
        string $fullUrl,
34
        array $headers = [],
35
        array $payload = [],
36
        int $maxRedirects = 0,
37
        int $retries = 3
38
    ): Response {
39
        $allHeaders = array_merge([
40
            'Content-Type: ' . ($httpVerb === 'post') ? 'application/jose+json' : 'application/json',
41
        ], $headers);
42
43
        $curlHandle = $this->getCurlHandle($fullUrl, $allHeaders, $maxRedirects);
44
45
        switch ($httpVerb) {
46
            case 'head':
47
                curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'HEAD');
48
                curl_setopt($curlHandle, CURLOPT_NOBODY, true);
49
                break;
50
51
            case 'get':
52
                curl_setopt($curlHandle, CURLOPT_URL, $fullUrl . '?' . http_build_query($payload));
53
                break;
54
55
            case 'post':
56
                curl_setopt($curlHandle, CURLOPT_POST, true);
57
                $this->attachRequestPayload($curlHandle, $payload);
58
                break;
59
        }
60
61
        $rawResponse = curl_exec($curlHandle);
62
        $headerSize = curl_getinfo($curlHandle, CURLINFO_HEADER_SIZE);
63
        $allHeaders = curl_getinfo($curlHandle);
64
65
        $rawHeaders = mb_substr($rawResponse, 0, $headerSize);
0 ignored issues
show
Bug introduced by
It seems like $rawResponse can also be of type true; however, parameter $string of mb_substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

65
        $rawHeaders = mb_substr(/** @scrutinizer ignore-type */ $rawResponse, 0, $headerSize);
Loading history...
66
        $rawBody = mb_substr($rawResponse, $headerSize);
67
        $body = $rawBody;
68
69
        $allHeaders = array_merge($allHeaders, $this->parseRawHeaders($rawHeaders));
70
71
        if (json_validate($rawBody)) {
72
            $body = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
73
        }
74
75
        $httpCode = $allHeaders['http_code'] ?? null;
76
77
        // Catch HTTP status code 0 when Let's Encrypt API is having problems.
78
        if ($httpCode === 0) {
79
            // Retry.
80
            if ($retries > 0) {
81
                return $this->makeCurlRequest($httpVerb, $fullUrl, $headers, $payload, $maxRedirects, --$retries);
82
            }
83
84
            // Return 504 Gateway Timeout.
85
            $httpCode = 504;
86
        }
87
88
        return new Response($allHeaders, $allHeaders['url'] ?? '', $httpCode, $body);
89
    }
90
91
    private function attachRequestPayload(CurlHandle $curlHandle, array $data): void
92
    {
93
        $encoded = json_encode($data, JSON_THROW_ON_ERROR);
94
95
        curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $encoded);
96
    }
97
98
    private function getCurlHandle(string $fullUrl, array $headers = [], int $maxRedirects = 0): CurlHandle
99
    {
100
        $curlHandle = curl_init();
101
102
        curl_setopt($curlHandle, CURLOPT_URL, $fullUrl);
103
104
        curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge([
105
            'Accept: application/json',
106
        ], $headers));
107
108
        curl_setopt($curlHandle, CURLOPT_USERAGENT, 'rogierw/rw-acme-client');
109
        curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
110
        curl_setopt($curlHandle, CURLOPT_TIMEOUT, $this->timeout);
111
        curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true);
112
        curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
113
        curl_setopt($curlHandle, CURLOPT_ENCODING, '');
114
        curl_setopt($curlHandle, CURLOPT_HEADER, true);
115
116
        if ($maxRedirects > 0) {
117
            curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true);
118
            curl_setopt($curlHandle, CURLOPT_MAXREDIRS, $maxRedirects);
119
        }
120
121
        return $curlHandle;
122
    }
123
124
    private function parseRawHeaders(string $rawHeaders): array
125
    {
126
        $headers = explode("\n", $rawHeaders);
127
        $headersArr = [];
128
129
        foreach ($headers as $header) {
130
            if (!str_contains($header, ':')) {
131
                continue;
132
            }
133
134
            [$name, $value] = explode(':', $header, 2);
135
136
            $headersArr[str_replace('_', '-', strtolower($name))] = trim($value);
137
        }
138
139
        return $headersArr;
140
    }
141
}
142