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

LEConnector   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Test Coverage

Coverage 87.5%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 110
c 1
b 0
f 0
dl 0
loc 247
rs 9.92
ccs 77
cts 88
cp 0.875
wmc 31

9 Methods

Rating   Name   Duplication   Size   Complexity  
A post() 0 3 1
A __construct() 0 7 1
D request() 0 67 18
A getNewNonce() 0 3 2
A signRequestJWK() 0 30 3
A getLEDirectory() 0 8 1
A get() 0 3 1
A head() 0 3 1
A signRequestKid() 0 26 3
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