Completed
Branch master (7e04b6)
by
unknown
06:53
created

RestClient::setCurlDebugging()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?PHP
2
3
namespace Blocktrail\SDK\Connection;
4
5
use GuzzleHttp\Client as Guzzle;
6
use GuzzleHttp\Handler\CurlHandler;
7
use GuzzleHttp\HandlerStack;
8
use GuzzleHttp\Psr7\Request;
9
use GuzzleHttp\Psr7\Uri;
10
use HttpSignatures\Context;
11
use Blocktrail\SDK\Blocktrail;
12
use Blocktrail\SDK\Connection\Exceptions\EndpointSpecificError;
13
use Blocktrail\SDK\Connection\Exceptions\GenericServerError;
14
use Blocktrail\SDK\Connection\Exceptions\ObjectNotFound;
15
use Blocktrail\SDK\Connection\Exceptions\UnknownEndpointSpecificError;
16
use Blocktrail\SDK\Connection\Exceptions\EmptyResponse;
17
use Blocktrail\SDK\Connection\Exceptions\InvalidCredentials;
18
use Blocktrail\SDK\Connection\Exceptions\MissingEndpoint;
19
use Blocktrail\SDK\Connection\Exceptions\GenericHTTPError;
20
use HttpSignatures\GuzzleHttpSignatures;
21
use Psr\Http\Message\ResponseInterface;
22
23
/**
24
 * Class RestClient
25
 *
26
 */
27
class RestClient {
28
29
    const AUTH_HTTP_SIG = 'http-signatures';
30
31
    /**
32
     * @var Guzzle
33
     */
34
    protected $guzzle;
35
36
    /**
37
     * @var string
38
     */
39
    protected $apiKey;
40
41
    /**
42
     * @var string
43
     */
44
    protected $apiEndpoint;
45
46
    /**
47
     * @var string
48
     */
49
    protected $apiVersion;
50
51
    /**
52
     * @var string
53
     */
54
    protected $apiSecret;
55
56
    /**
57
     * @var array
58
     */
59
    protected $options = [];
60
61
    /**
62
     * @var array
63
     */
64
    protected $curlOptions = [];
65
66
    /**
67
     * @var bool
68
     */
69
    protected $verboseErrors = false;
70
71
    public function __construct($apiEndpoint, $apiVersion, $apiKey, $apiSecret) {
72
        $this->apiEndpoint = $apiEndpoint;
73
        $this->apiVersion = $apiVersion;
74
        $this->apiKey = $apiKey;
75
        $this->apiSecret = $apiSecret;
76
77
        $this->guzzle = $this->createGuzzleClient();
78
    }
79
80
    /**
81
     * @param array $options
82
     * @param array $curlOptions
83
     * @return Guzzle
84
     */
85
    protected function createGuzzleClient(array $options = [], array $curlOptions = []) {
86
        $options = $options + $this->options;
87
        $curlOptions = $curlOptions + $this->curlOptions;
88
89
        $context = new Context([
90
            'keys' => [$this->apiKey => $this->apiSecret],
91
            'algorithm' => 'hmac-sha256',
92
            'headers' => ['(request-target)', 'Content-MD5', 'Date'],
93
        ]);
94
95
        $curlHandler = new CurlHandler($curlOptions);
96
        $handler = HandlerStack::create($curlHandler);
97
        $handler->push(GuzzleHttpSignatures::middlewareFromContext($context));
98
99
        return new Guzzle($options + array(
100
            'handler' => $handler,
101
            'base_uri' => $this->apiEndpoint,
102
            'headers' => array(
103
                'User-Agent' => Blocktrail::SDK_USER_AGENT . '/' . Blocktrail::SDK_VERSION
104
            ),
105
            'http_errors' => false,
106
            'connect_timeout' => 3,
107
            'timeout' => 20.0, // tmp until we have a good matrix of all the requests and their expect min/max time
108
            'verify' => true,
109
            'proxy' => '',
110
            'debug' => false,
111
            'config' => array(),
112
            'auth' => '',
113
        ));
114
    }
115
116
    /**
117
     * @return Guzzle
118
     */
119
    public function getGuzzleClient() {
120
        return $this->guzzle;
121
    }
122
123
    /**
124
     * enable CURL debugging output
125
     *
126
     * @param   bool        $debug
127
     */
128
    public function setCurlDebugging($debug = true) {
129
        $this->options['debug'] = $debug;
130
131
        $this->guzzle = $this->createGuzzleClient();
132
    }
133
134
    /**
135
     * enable verbose errors
136
     *
137
     * @param   bool        $verboseErrors
138
     */
139
    public function setVerboseErrors($verboseErrors = true) {
140
        $this->verboseErrors = $verboseErrors;
141
    }
142
        
143
    /**
144
     * set cURL default option on Guzzle client
145
     * @param string    $key
146
     * @param bool      $value
147
     */
148
    public function setCurlDefaultOption($key, $value) {
149
        $this->curlOptions[$key] = $value;
150
151
        $this->guzzle = $this->createGuzzleClient();
152
    }
153
154
    /**
155
     * set the proxy config for Guzzle
156
     *
157
     * @param   $proxy
158
     */
159
    public function setProxy($proxy) {
160
        $this->options['proxy'] = $proxy;
161
162
        $this->guzzle = $this->createGuzzleClient();
163
    }
164
165
    /**
166
     * @param   string          $endpointUrl
167
     * @param   array           $queryString
168
     * @param   string          $auth           http-signatures to enable http-signature signing
169
     * @param   float           $timeout        timeout in seconds
170
     * @return  Response
171
     */
172
    public function get($endpointUrl, $queryString = null, $auth = null, $timeout = null) {
173
        return $this->request('GET', $endpointUrl, $queryString, null, $auth, null, $timeout);
174
    }
175
176
    /**
177
     * @param   string          $endpointUrl
178
     * @param   null            $queryString
179
     * @param   array|string    $postData
180
     * @param   string          $auth           http-signatures to enable http-signature signing
181
     * @param   float           $timeout        timeout in seconds
182
     * @return  Response
183
     */
184
    public function post($endpointUrl, $queryString = null, $postData = '', $auth = null, $timeout = null) {
185
        return $this->request('POST', $endpointUrl, $queryString, $postData, $auth, null, $timeout);
186
    }
187
188
    /**
189
     * @param   string          $endpointUrl
190
     * @param   null            $queryString
191
     * @param   array|string    $putData
192
     * @param   string          $auth           http-signatures to enable http-signature signing
193
     * @param   float           $timeout        timeout in seconds
194
     * @return  Response
195
     */
196
    public function put($endpointUrl, $queryString = null, $putData = '', $auth = null, $timeout = null) {
197
        return $this->request('PUT', $endpointUrl, $queryString, $putData, $auth, null, $timeout);
198
    }
199
200
    /**
201
     * @param   string          $endpointUrl
202
     * @param   null            $queryString
203
     * @param   array|string    $postData
204
     * @param   string          $auth           http-signatures to enable http-signature signing
205
     * @param   float           $timeout        timeout in seconds
206
     * @return  Response
207
     */
208
    public function delete($endpointUrl, $queryString = null, $postData = null, $auth = null, $timeout = null) {
209
        return $this->request('DELETE', $endpointUrl, $queryString, $postData, $auth, 'url', $timeout);
210
    }
211
212
    private static $replaceQuery = ['=' => '%3D', '&' => '%26'];
213
    public static function hasQueryValue(Uri $uri, $key) {
214
        $current = $uri->getQuery();
215
        $key = strtr($key, self::$replaceQuery);
216
217
        if (!$current) {
218
            $result = [];
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
219
        } else {
220
            $result = [];
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
221
            foreach (explode('&', $current) as $part) {
222
                if (explode('=', $part)[0] === $key) {
223
                    return true;
224
                };
225
            }
226
        }
227
228
        return false;
229
    }
230
231
    /**
232
     * generic request executor
233
     *
234
     * @param   string          $method         GET, POST, PUT, DELETE
235
     * @param   string          $endpointUrl
236
     * @param   array           $queryString
237
     * @param   array|string    $body
238
     * @param   string          $auth           http-signatures to enable http-signature signing
239
     * @param   string          $contentMD5Mode body or url
240
     * @param   float           $timeout        timeout in seconds
241
     * @return Request
242
     */
243
    public function buildRequest($method, $endpointUrl, $queryString = null, $body = null, $auth = null, $contentMD5Mode = null, $timeout = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $auth is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $timeout is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
244
        if (is_null($contentMD5Mode)) {
245
            $contentMD5Mode = !is_null($body) ? 'body' : 'url';
246
        }
247
248
        $request = new Request($method, $endpointUrl);
249
        $uri = $request->getUri();
250
251
        if ($queryString) {
252
            foreach ($queryString as $k => $v) {
253
                $uri = Uri::withQueryValue($uri, $k, $v);
0 ignored issues
show
Bug introduced by
It seems like $uri can be null; however, withQueryValue() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
254
            }
255
        }
256
257
        if (!self::hasQueryValue($uri, 'api_key')) {
0 ignored issues
show
Documentation introduced by
$uri is of type object<Psr\Http\Message\UriInterface>|null, but the function expects a object<GuzzleHttp\Psr7\Uri>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
258
            $uri = Uri::withQueryValue($uri, 'api_key', $this->apiKey);
0 ignored issues
show
Bug introduced by
It seems like $uri can be null; however, withQueryValue() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
259
        }
260
261
        // normalize the query string the same way the server expects it
262
        /** @var Request $request */
263
        $request = $request->withUri($uri->withQuery(\Symfony\Component\HttpFoundation\Request::normalizeQueryString($uri->getQuery())));
264
265
        if (!$request->hasHeader('Date')) {
266
            $request = $request->withHeader('Date', $this->getRFC1123DateString());
267
        }
268
269
        if (!is_null($body)) {
270
            if (!$request->hasHeader('Content-Type')) {
271
                $request = $request->withHeader('Content-Type', 'application/json');
272
            }
273
274
            if (!is_string($body)) {
275
                $body = json_encode($body);
276
            }
277
            $request = $request->withBody(\GuzzleHttp\Psr7\stream_for($body));
278
        }
279
280
        // for GET/DELETE requests, MD5 the request URI (excludes domain, includes query strings)
281
        if ($contentMD5Mode == 'body') {
282
            $request = $request->withHeader('Content-MD5', md5((string)$body));
283
        } else {
284
            $request = $request->withHeader('Content-MD5', md5($request->getRequestTarget()));
285
        }
286
287
        return $request;
288
    }
289
290
    /**
291
     * generic request executor
292
     *
293
     * @param   string          $method         GET, POST, PUT, DELETE
294
     * @param   string          $endpointUrl
295
     * @param   array           $queryString
296
     * @param   array|string    $body
297
     * @param   string          $auth           http-signatures to enable http-signature signing
298
     * @param   string          $contentMD5Mode body or url
299
     * @param   float           $timeout        timeout in seconds
300
     * @return Response
301
     */
302
    public function request($method, $endpointUrl, $queryString = null, $body = null, $auth = null, $contentMD5Mode = null, $timeout = null) {
303
        $request = $this->buildRequest($method, $endpointUrl, $queryString, $body, $contentMD5Mode);
304
        $response = $this->guzzle->send($request, ['auth' => $auth, 'timeout' => $timeout]);
305
306
        return $this->responseHandler($response);
307
    }
308
309
    public function responseHandler(ResponseInterface $responseObj) {
310
        $httpResponseCode = (int)$responseObj->getStatusCode();
311
        $httpResponsePhrase = (string)$responseObj->getReasonPhrase();
312
        $body = $responseObj->getBody();
313
314
        if ($httpResponseCode == 200) {
315
            if (!$body) {
316
                throw new EmptyResponse(Blocktrail::EXCEPTION_EMPTY_RESPONSE, $httpResponseCode);
317
            }
318
319
            $result = new Response($httpResponseCode, $body);
320
321
            return $result;
322
        } elseif ($httpResponseCode == 400 || $httpResponseCode == 403) {
323
            $data = json_decode($body, true);
324
325
            if ($data && isset($data['msg'], $data['code'])) {
326
                throw new EndpointSpecificError(!is_string($data['msg']) ? json_encode($data['msg']) : $data['msg'], $data['code']);
327
            } else {
328
                throw new UnknownEndpointSpecificError($this->verboseErrors ? $body : Blocktrail::EXCEPTION_UNKNOWN_ENDPOINT_SPECIFIC_ERROR);
329
            }
330
        } elseif ($httpResponseCode == 401) {
331
            throw new InvalidCredentials($this->verboseErrors ? $body : Blocktrail::EXCEPTION_INVALID_CREDENTIALS, $httpResponseCode);
332
        } elseif ($httpResponseCode == 404) {
333
            if ($httpResponsePhrase == "Endpoint Not Found") {
334
                throw new MissingEndpoint($this->verboseErrors ? $body : Blocktrail::EXCEPTION_MISSING_ENDPOINT, $httpResponseCode);
335
            } else {
336
                throw new ObjectNotFound($this->verboseErrors ? $body : Blocktrail::EXCEPTION_OBJECT_NOT_FOUND, $httpResponseCode);
337
            }
338
        } elseif ($httpResponseCode == 500) {
339
            throw new GenericServerError(Blocktrail::EXCEPTION_GENERIC_SERVER_ERROR . "\nServer Response: " . $body, $httpResponseCode);
340
        } else {
341
            throw new GenericHTTPError(Blocktrail::EXCEPTION_GENERIC_HTTP_ERROR . "\nServer Response: " . $body, $httpResponseCode);
342
        }
343
    }
344
345
    /**
346
     * Returns curent fate time in RFC1123 format, using UTC time zone
347
     *
348
     * @return  string
349
     */
350
    private function getRFC1123DateString() {
351
        $date = new \DateTime(null, new \DateTimeZone("UTC"));
352
        return str_replace("+0000", "GMT", $date->format(\DateTime::RFC1123));
353
    }
354
}
355