Completed
Branch master (1da221)
by
unknown
08:31
created

RestClient::request()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

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