Completed
Pull Request — master (#85)
by thomas
15:44
created

RestClient::setCurlDebugging()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 4
cp 0
crap 2
rs 9.4285
c 0
b 0
f 0
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 80
    public function __construct($apiEndpoint, $apiVersion, $apiKey, $apiSecret) {
73 80
        $this->apiEndpoint = $apiEndpoint;
74 80
        $this->apiVersion = $apiVersion;
75 80
        $this->apiKey = $apiKey;
76 80
        $this->apiSecret = $apiSecret;
77
78 80
        $this->guzzle = $this->createGuzzleClient();
79 80
    }
80
81
    /**
82
     * @param array $options
83
     * @param array $curlOptions
84
     * @return Guzzle
85
     */
86 80
    protected function createGuzzleClient(array $options = [], array $curlOptions = []) {
87 80
        $options = $options + $this->options;
88 80
        $curlOptions = $curlOptions + $this->curlOptions;
89
90 80
        $context = new Context([
91 80
            'keys' => [$this->apiKey => $this->apiSecret],
92 80
            'algorithm' => 'hmac-sha256',
93
            'headers' => ['(request-target)', 'Content-MD5', 'Date'],
94
        ]);
95
96 80
        $curlHandler = new CurlHandler($curlOptions);
97 80
        $handler = HandlerStack::create($curlHandler);
98 80
        $handler->push(GuzzleHttpSignatures::middlewareFromContext($context));
99
100 80
        return new Guzzle($options + array(
101 80
            'handler' => $handler,
102 80
            'base_uri' => $this->apiEndpoint,
103
            'headers' => array(
104 80
                'User-Agent' => Blocktrail::SDK_USER_AGENT . '/' . Blocktrail::SDK_VERSION,
105
            ),
106
            'http_errors' => false,
107 80
            'connect_timeout' => 3,
108 80
            'timeout' => 20.0, // tmp until we have a good matrix of all the requests and their expect min/max time
109
            'verify' => true,
110 80
            'proxy' => '',
111
            'debug' => false,
112
            'config' => array(),
113 80
            '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 23
    public function get($endpointUrl, $queryString = null, $auth = null, $timeout = null) {
174 23
        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 19
    public function post($endpointUrl, $queryString = null, $postData = '', $auth = null, $timeout = null) {
186 19
        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 1
    public function put($endpointUrl, $queryString = null, $putData = '', $auth = null, $timeout = null) {
198 1
        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 11
    public function delete($endpointUrl, $queryString = null, $postData = null, $auth = null, $timeout = null) {
210 11
        return $this->request('DELETE', $endpointUrl, $queryString, $postData, $auth, 'url', $timeout);
211
    }
212
213
    private static $replaceQuery = ['=' => '%3D', '&' => '%26'];
214 25
    public static function hasQueryValue(Uri $uri, $key) {
215 25
        $current = $uri->getQuery();
216 25
        $key = strtr($key, self::$replaceQuery);
217
218 25
        if (!$current) {
219 25
            $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 19
            $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 19
            foreach (explode('&', $current) as $part) {
223 19
                if (explode('=', $part)[0] === $key) {
224 19
                    return true;
225
                };
226
            }
227
        }
228
229 25
        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 25
    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 25
        if (is_null($contentMD5Mode)) {
246 25
            $contentMD5Mode = !is_null($body) ? 'body' : 'url';
247
        }
248
249 25
        $request = new Request($method, $this->apiEndpoint . $endpointUrl);
250 25
        $uri = $request->getUri();
251
252 25
        if ($queryString) {
253 19
            foreach ($queryString as $k => $v) {
254 19
                $uri = Uri::withQueryValue($uri, $k, $v);
255
            }
256
        }
257
258 25
        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 25
            $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 25
        $request = $request->withUri($uri->withQuery(\Symfony\Component\HttpFoundation\Request::normalizeQueryString($uri->getQuery())));
265
266 25
        if (!$request->hasHeader('Date')) {
267 25
            $request = $request->withHeader('Date', $this->getRFC1123DateString());
268
        }
269
270 25
        if (!is_null($body)) {
271 19
            if (!$request->hasHeader('Content-Type')) {
272 19
                $request = $request->withHeader('Content-Type', 'application/json');
273
            }
274
275 19
            if (!is_string($body)) {
276 19
                $body = json_encode($body);
277
            }
278 19
            $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 25
        if ($contentMD5Mode == 'body') {
283 19
            $request = $request->withHeader('Content-MD5', md5((string)$body));
284
        } else {
285 23
            $request = $request->withHeader('Content-MD5', md5($request->getRequestTarget()));
286
        }
287
288 25
        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 25
    public function request($method, $endpointUrl, $queryString = null, $body = null, $auth = null, $contentMD5Mode = null, $timeout = null) {
304 25
        $request = $this->buildRequest($method, $endpointUrl, $queryString, $body, $auth, $contentMD5Mode);
305 25
        $response = $this->guzzle->send($request, ['auth' => $auth, 'timeout' => $timeout]);
306
307 25
        return $this->responseHandler($response);
308
    }
309
310 25
    public function responseHandler(ResponseInterface $responseObj) {
311 25
        $httpResponseCode = (int)$responseObj->getStatusCode();
312 25
        $httpResponsePhrase = (string)$responseObj->getReasonPhrase();
313 25
        $body = $responseObj->getBody();
314
315 25
        if ($httpResponseCode == 200) {
316 25
            if (!$body) {
317
                throw new EmptyResponse(Blocktrail::EXCEPTION_EMPTY_RESPONSE, $httpResponseCode);
318
            }
319
320 25
            $result = new Response($httpResponseCode, $body);
321
322 25
            return $result;
323 9
        } elseif ($httpResponseCode == 400 || $httpResponseCode == 403) {
324 6
            $data = json_decode($body, true);
325
326 6
            if ($data && isset($data['msg'], $data['code'])) {
327 6
                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 9
        } elseif ($httpResponseCode == 401) {
336 1
            throw new InvalidCredentials($this->verboseErrors ? $body : Blocktrail::EXCEPTION_INVALID_CREDENTIALS, $httpResponseCode);
337 8
        } elseif ($httpResponseCode == 404) {
338 8
            if ($httpResponsePhrase == "Endpoint Not Found") {
339
                throw new MissingEndpoint($this->verboseErrors ? $body : Blocktrail::EXCEPTION_MISSING_ENDPOINT, $httpResponseCode);
340
            } else {
341 8
                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 25
    private function getRFC1123DateString() {
356 25
        $date = new \DateTime(null, new \DateTimeZone("UTC"));
357 25
        return str_replace("+0000", "GMT", $date->format(\DateTime::RFC1123));
358
    }
359
}
360