Passed
Push — master ( 33103d...13ffe5 )
by Russell
02:55
created

Gateway   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 178
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 23
eloc 52
dl 0
loc 178
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A verify() 0 5 1
A name() 0 3 1
A hashFunc() 0 3 1
B client() 0 36 9
B setDiscoveredNodes() 0 36 8
A proofs() 0 5 1
A hashes() 0 5 1
A getDiscoveredNodes() 0 3 1
1
<?php
2
3
/**
4
 * @author  Russell Michell 2018 <[email protected]>
5
 * @package silverstripe-verifiable
6
 */
7
8
namespace PhpTek\Verifiable\Backend\Chainpoint;
9
10
use PhpTek\Verifiable\Backend\GatewayProvider;
11
use GuzzleHttp\Client;
12
use GuzzleHttp\Exception\RequestException;
13
use SilverStripe\Core\Config\Configurable;
14
use PhpTek\Verifiable\Exception\VerifiableBackendException;
15
use PhpTek\Verifiable\Exception\VerifiableValidationException;
16
17
/**
18
 * Calls the endpoints of the Tierion network's ChainPoint service.
19
 *
20
 * @see https://app.swaggerhub.com/apis/chainpoint/node/1.0.0
21
 * @see https://chainpoint.org
22
 */
23
class Gateway implements GatewayProvider
24
{
25
    use Configurable;
26
27
    /**
28
     * An array of nodes for submitting hashes to.
29
     *
30
     * @var array
31
     */
32
    protected static $discovered_nodes = [];
33
34
    /**
35
     * @return string
36
     */
37
    public function name() : string
38
    {
39
        return 'chainpoint';
40
    }
41
42
    /**
43
     * @return string
44
     */
45
    public function hashFunc() : string
46
    {
47
        return 'sha256';
48
    }
49
50
    /**
51
     * Send a single hash_id_node to retrieve a proof in binary format from the
52
     * Tierion network.
53
     *
54
     * GETs to the: "/proofs" REST API endpoint.
55
     *
56
     * @param  string $hashIdNode
57
     * @return string A v3 ChainpointProof
58
     */
59
    public function proofs(string $hashIdNode) : string
60
    {
61
        $response = $this->client("/proofs/$hashIdNode", 'GET');
62
63
        return $response->getBody()->getContents() ?? '[]';
64
    }
65
66
    /**
67
     * Send an array of hashes for anchoring.
68
     *
69
     * POSTs to the: "/hashes" REST API endpoint.
70
     *
71
     * @param  array $hashes
72
     * @return string (From GuzzleHttp\Stream::getContents()
73
     */
74
    public function hashes(array $hashes) : string
75
    {
76
        $response = $this->client('/hashes', 'POST', ['hashes' => $hashes]);
77
78
        return $response->getBody()->getContents() ?? '[]';
79
    }
80
81
    /**
82
     * Submit a chainpoint proof to the backend for verification.
83
     *
84
     * @param  string $proof A partial or full JSON string, originally received from,
85
     *                       or generated on behalf of a backend.
86
     * @return string
87
     * @todo See the returned proof's "uris" key, to be able to call a specific URI for proof verification.
88
     */
89
    public function verify(string $proof) : string
90
    {
91
        $response = $this->client('/verify', 'POST', ['proofs' => [$proof]]);
92
93
        return $response->getBody()->getContents() ?? '[]';
94
    }
95
96
    /**
97
     * Return a client to use for all RPC traffic to this backend.
98
     *
99
     * @param  string   $url     The absolute or relative URL to make a request to.
100
     * @param  string   $verb    The HTTP verb to use e.g. GET or POST.
101
     * @param  array    $payload The payload to be sent along in GET/POST requests.
102
     * @param  bool     $rel     Is $url relative? If so, pass "base_uri" to {@link Client}.
103
     * @return Response Guzzle   Response object
0 ignored issues
show
Bug introduced by
The type PhpTek\Verifiable\Backend\Chainpoint\Response was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
104
     * @throws VerifiableBackendException
105
     */
106
    protected function client(string $url, string $verb, array $payload = [], bool $rel = true)
107
    {
108
        if ($rel && !$this->getDiscoveredNodes()) {
109
            $this->setDiscoveredNodes();
110
111
            if (!$this->getDiscoveredNodes()) {
112
                // This should _never_ happen..
113
                throw new VerifiableValidationException('No chainpoint nodes discovered!');
114
            }
115
        }
116
117
        $verb = strtoupper($verb);
118
        $method = strtolower($verb);
119
        $config = $this->config()->get('client_config');
120
        $client = new Client([
121
         //   'base_uri' => $rel ? $this->getDiscoveredNodes()[0] : '', // Use a single address only
122
            'verify' => true,
123
            'timeout'  => $config['timeout'],
124
            'connect_timeout'  => $config['connect_timeout'],
125
            'allow_redirects' => false,
126
        ]);
127
128
        try {
129
            // json_encodes POSTed data and sends correct Content-Type header
130
            if ($payload && $verb === 'POST') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $payload of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
131
                $payload['json'] = $payload;
132
            }
133
134
135
            if ($rel) {
136
                $url = ($rel ? $this->getDiscoveredNodes()[0] : '')  . $url;
0 ignored issues
show
introduced by
The condition $rel is always true.
Loading history...
137
            }
138
139
            return $client->$method($url, $payload);
140
        } catch (RequestException $e) {
141
            throw new VerifiableValidationException(sprintf('Upstream network problem: %s', $e->getMessage()));
142
        }
143
    }
144
145
    /**
146
     * The Tierion network comprises many nodes, some of which may or may not be
147
     * online. We therefore randomly select a source of curated (audited) node-IPs
148
     * and for each IP, we ping it until we receive a 200 OK response. For each such
149
     * node, it is then set to $discovered_nodes.
150
     *
151
     * @param  array $usedNodes  Optionally pass some "pre-known" chainpoint nodes
152
     * @return mixed void | null
153
     * @throws VerifiableBackendException
154
     */
155
    public function setDiscoveredNodes($usedNodes = null)
156
    {
157
        if ($usedNodes) {
158
            static::$discovered_nodes = $usedNodes;
159
160
            return;
161
        }
162
163
        if (count(static::$discovered_nodes)) {
164
            return;
165
        }
166
167
        $limit = (int) $this->config()->get('discover_node_count') ?: 1;
168
        $chainpointUrls = $this->config()->get('chainpoint_urls');
169
        $url = $chainpointUrls[rand(0,2)];
170
        $response = $this->client($url, 'GET', [], false);
171
172
        if ($response->getStatusCode() !== 200) {
173
            throw new VerifiableBackendException('Bad response from node source URL');
174
        }
175
176
        $i = 0;
177
178
        foreach (json_decode($response->getBody(), true) as $candidate) {
179
            $response = $this->client($candidate['public_uri'], 'GET', [], false);
180
181
            if ($response->getStatusCode() !== 200) {
182
                continue;
183
            }
184
185
            ++$i; // Only increment with succesful requests
186
187
            static::$discovered_nodes[] = $candidate['public_uri'];
188
189
            if ($i === $limit) {
190
                break;
191
            }
192
        }
193
    }
194
195
    /**
196
     * @return array
197
     */
198
    public function getDiscoveredNodes()
199
    {
200
        return static::$discovered_nodes;
201
    }
202
203
}
204