Completed
Pull Request — master (#8)
by John
02:04
created

LEConnector::request()   D

Complexity

Conditions 18
Paths 3

Size

Total Lines 67
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 35.6804

Importance

Changes 0
Metric Value
cc 18
eloc 46
c 0
b 0
f 0
nc 3
nop 3
dl 0
loc 67
rs 4.8666
ccs 18
cts 29
cp 0.6207
crap 35.6804

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace LEClient;
4
5
use LEClient\Exceptions\LEConnectorException;
6
7
/**
8
 * LetsEncrypt Connector class, containing the functions necessary to sign with JSON Web Key and Key ID, and perform GET, POST and HEAD requests.
9
 *
10
 * PHP version 5.2.0
11
 *
12
 * MIT License
13
 *
14
 * Copyright (c) 2018 Youri van Weegberg
15
 *
16
 * Permission is hereby granted, free of charge, to any person obtaining a copy
17
 * of this software and associated documentation files (the "Software"), to deal
18
 * in the Software without restriction, including without limitation the rights
19
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
 * copies of the Software, and to permit persons to whom the Software is
21
 * furnished to do so, subject to the following conditions:
22
 *
23
 * The above copyright notice and this permission notice shall be included in all
24
 * copies or substantial portions of the Software.
25
 *
26
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
 * SOFTWARE.
33
 *
34
 * @author     Youri van Weegberg <[email protected]>
35
 * @copyright  2018 Youri van Weegberg
36
 * @license    https://opensource.org/licenses/mit-license.php  MIT License
37
 * @link       https://github.com/yourivw/LEClient
38
 * @since      Class available since Release 1.0.0
39
 */
40
class LEConnector
41
{
42
	public $baseURL;
43
	public $accountKeys;
44
45
	private $nonce;
46
47
	public $keyChange;
48
	public $newAccount;
49
    public $newNonce;
50
	public $newOrder;
51
	public $revokeCert;
52
53
	public $accountURL;
54 14
	public $accountDeactivated = false;
55
56
	private $log;
57
58
    /**
59
     * Initiates the LetsEncrypt Connector class.
60
     *
61 14
     * @param int 		$log			The level of logging. Defaults to no logging. LOG_OFF, LOG_STATUS, LOG_DEBUG accepted.
62 14
     * @param string	$baseURL 		The LetsEncrypt server URL to make requests to.
63 14
     * @param array		$accountKeys 	Array containing location of account keys files.
64 14
     */
65
	public function __construct($log, $baseURL, $accountKeys)
66 14
	{
67 10
		$this->baseURL = $baseURL;
68 10
		$this->accountKeys = $accountKeys;
69
		$this->log = $log;
70
		$this->getLEDirectory();
71
		$this->getNewNonce();
72
	}
73 14
74
    /**
75 14
     * Requests the LetsEncrypt Directory and stores the necessary URLs in this LetsEncrypt Connector instance.
76 10
     */
77 10
	private function getLEDirectory()
78 10
	{
79 10
		$req = $this->get('/directory');
80 10
		$this->keyChange = $req['body']['keyChange'];
81 10
		$this->newAccount = $req['body']['newAccount'];
82
		$this->newNonce = $req['body']['newNonce'];
83
		$this->newOrder = $req['body']['newOrder'];
84
		$this->revokeCert = $req['body']['revokeCert'];
85
	}
86 10
87
    /**
88 10
     * Requests a new nonce from the LetsEncrypt server and stores it in this LetsEncrypt Connector instance.
89
     */
90 10
	private function getNewNonce()
91
	{
92
		if($this->head($this->newNonce)['status'] !== 200) throw LEConnectorException::NoNewNonceException();
93
	}
94
95 10
    /**
96
     * Makes a Curl request.
97
     *
98
     * @param string	$method	The HTTP method to use. Accepting GET, POST and HEAD requests.
99
     * @param string 	$URL 	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
100
     * @param object 	$data  	The body to attach to a POST request. Expected as a JSON encoded string.
101
     *
102
     * @return array 	Returns an array with the keys 'request', 'header', 'status' and 'body'.
103
     */
104
	private function request($method, $URL, $data = null)
105
	{
106
		if($this->accountDeactivated) throw LEConnectorException::AccountDeactivatedException();
107
108
		$headers = array('Accept: application/json', 'Content-Type: application/jose+json');
109
		$requestURL = preg_match('~^http~', $URL) ? $URL : $this->baseURL . $URL;
110
        $handle = curl_init();
111
        curl_setopt($handle, CURLOPT_URL, $requestURL);
112
        curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
113
        curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
114
        curl_setopt($handle, CURLOPT_HEADER, true);
115
116
        switch ($method) {
117
            case 'GET':
118
                break;
119
            case 'POST':
120
                curl_setopt($handle, CURLOPT_POST, true);
121
                curl_setopt($handle, CURLOPT_POSTFIELDS, $data);
122
                break;
123
			case 'HEAD':
124
				curl_setopt($handle, CURLOPT_CUSTOMREQUEST, 'HEAD');
125
				curl_setopt($handle, CURLOPT_NOBODY, true);
126
				break;
127
			default:
128
				throw LEConnectorException::MethodNotSupportedException($method);
129
				break;
130
        }
131
        $response = curl_exec($handle);
132
133
        if(curl_errno($handle)) {
134
            throw LEConnectorException::CurlErrorException(curl_error($handle));
135
        }
136 14
137
        $headerSize = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
138 14
        $statusCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
139 2
140
        $header = substr($response, 0, $headerSize);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type true; however, parameter $string of 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

140
        $header = substr(/** @scrutinizer ignore-type */ $response, 0, $headerSize);
Loading history...
141
        $body = substr($response, $headerSize);
142 14
		$jsonbody = json_decode($body, true);
143
		$jsonresponse = array(
144 14
            'request' => $method . ' ' . $requestURL,
145 14
            'header' => $header,
146 2
            'status' => $statusCode,
147
            'body' => $jsonbody === null ? $body : $jsonbody,
148
        );
149 14
		if($this->log instanceof \Psr\Log\LoggerInterface) 
0 ignored issues
show
introduced by
$this->log is never a sub-type of Psr\Log\LoggerInterface.
Loading history...
150
		{
151
			$this->log->debug($method . ' response received', $jsonresponse);
152 14
		}
153 4
		elseif($this->log >= LEClient::LOG_DEBUG) LEFunctions::log($jsonresponse);
0 ignored issues
show
Bug introduced by
$jsonresponse of type array<string,mixed|string> is incompatible with the type object expected by parameter $data of LEClient\LEFunctions::log(). ( Ignorable by Annotation )

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

153
		elseif($this->log >= LEClient::LOG_DEBUG) LEFunctions::log(/** @scrutinizer ignore-type */ $jsonresponse);
Loading history...
154
		
155 2
		if(preg_match('~Replay\-Nonce: (\S+)~i', $header, $matches))
156 2
		{
157 2
			$this->nonce = trim($matches[1]);
158 2
		}
159 2
		else
160 2
		{
161
			if($method == 'POST') $this->getNewNonce(); // Not expecting a new nonce with GET and HEAD requests.
162
		}
163 2
164 2
		if((($method == 'POST' OR $method == 'GET') AND $statusCode !== 200 AND $statusCode !== 201) OR
165
			($method == 'HEAD' AND $statusCode !== 200))
166
		{
167
			throw LEConnectorException::InvalidResponseException($jsonresponse);
168
		}
169
170
        return $jsonresponse;
171
	}
172
173 10
    /**
174
     * Makes a GET request.
175 10
     *
176
     * @param string	$url 	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
177
     *
178 10
     * @return array 	Returns an array with the keys 'request', 'header', 'status' and 'body'.
179
     */
180 10
	public function get($url)
181
	{
182 10
		return $this->request('GET', $url);
183 10
	}
184 10
185 10
	/**
186 10
     * Makes a POST request.
187
     *
188
     * @param string 	$url	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
189
	 * @param object 	$data	The body to attach to a POST request. Expected as a json string.
190 10
     *
191 10
     * @return array 	Returns an array with the keys 'request', 'header', 'status' and 'body'.
192 10
     */
193 10
	public function post($url, $data = null)
194
	{
195
		return $this->request('POST', $url, $data);
196
	}
197
198
	/**
199
     * Makes a HEAD request.
200
     *
201 10
     * @param string 	$url	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
202 10
     *
203 10
     * @return array	Returns an array with the keys 'request', 'header', 'status' and 'body'.
204 10
     */
205 10
	public function head($url)
206
	{
207
		return $this->request('HEAD', $url);
208
	}
209
210 10
    /**
211
     * Generates a JSON Web Key signature to attach to the request.
212
     *
213 10
     * @param array 	$payload		The payload to add to the signature.
214
     * @param string	$url 			The URL to use in the signature.
215 10
     * @param string 	$privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. Defaults to accountKeys[private_key].
216 10
     *
217 10
     * @return string	Returns a JSON encoded string containing the signature.
218 10
     */
219 2
	public function signRequestJWK($payload, $url, $privateKeyFile = '')
220
    {
221 10
		if($privateKeyFile == '') $privateKeyFile = $this->accountKeys['private_key'];
222
		$privateKey = openssl_pkey_get_private(file_get_contents($privateKeyFile));
223
        $details = openssl_pkey_get_details($privateKey);
224
225
        $protected = array(
226
            "alg" => "RS256",
227
            "jwk" => array(
228
                "kty" => "RSA",
229
                "n" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["n"]),
230
                "e" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["e"]),
231 14
            ),
232
			"nonce" => $this->nonce,
233 14
			"url" => $url
234
        );
235
236
        $payload64 = LEFunctions::Base64UrlSafeEncode(str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload));
0 ignored issues
show
introduced by
The condition is_array($payload) is always true.
Loading history...
237
        $protected64 = LEFunctions::Base64UrlSafeEncode(json_encode($protected));
238
239
        openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256");
240
        $signed64 = LEFunctions::Base64UrlSafeEncode($signed);
241
242
        $data = array(
243
            'protected' => $protected64,
244 2
            'payload' => $payload64,
245
            'signature' => $signed64
246 2
        );
247
248
        return json_encode($data);
249
    }
250
251
	/**
252
     * Generates a Key ID signature to attach to the request.
253
     *
254
     * @param array 	$payload		The payload to add to the signature.
255
	 * @param string	$kid			The Key ID to use in the signature.
256
     * @param string	$url 			The URL to use in the signature.
257 10
     * @param string 	$privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. Defaults to accountKeys[private_key].
258
     *
259 10
     * @return string	Returns a JSON encoded string containing the signature.
260
     */
261
	public function signRequestKid($payload, $kid, $url, $privateKeyFile = '')
262
    {
263
		if($privateKeyFile == '') $privateKeyFile = $this->accountKeys['private_key'];
264
        $privateKey = openssl_pkey_get_private(file_get_contents($privateKeyFile));
265
        $details = openssl_pkey_get_details($privateKey);
0 ignored issues
show
Unused Code introduced by
The assignment to $details is dead and can be removed.
Loading history...
266
267
        $protected = array(
268
            "alg" => "RS256",
269
            "kid" => $kid,
270
			"nonce" => $this->nonce,
271 4
			"url" => $url
272
        );
273 4
274 4
        $payload64 = LEFunctions::Base64UrlSafeEncode(str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload));
0 ignored issues
show
introduced by
The condition is_array($payload) is always true.
Loading history...
275
        $protected64 = LEFunctions::Base64UrlSafeEncode(json_encode($protected));
276 4
277 4
        openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256");
278
        $signed64 = LEFunctions::Base64UrlSafeEncode($signed);
279
280
        $data = array(
281
            'protected' => $protected64,
282
            'payload' => $payload64,
283 4
            'signature' => $signed64
284
        );
285
286 4
        return json_encode($data);
287
    }
288
}
289