Passed
Push — master ( 9ceb95...784902 )
by Russell
10:38
created

Chainpoint::getInfoFromComposer()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 4
nop 1
dl 0
loc 17
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @author  Russell Michell 2018 <[email protected]>
5
 * @package silverstripe-verifiable
6
 */
7
8
namespace PhpTek\Verifiable\Backend;
9
10
use PhpTek\Verifiable\Backend\BackendProvider;
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
use SilverStripe\Core\Injector\Injector;
17
18
/**
19
 * Calls the endpoints of the chainpoint.org (Tierion?) network. Based on the Swagger
20
 * docs found here: https://app.swaggerhub.com/apis/chainpoint/node/1.0.0
21
 *
22
 * @see https://chainpoint.org
23
 * @see https://app.swaggerhub.com/apis/chainpoint/node/1.0.0
24
 * @todo Convert partial JSON proofs into binary format and send as POST to /verify
25
 * @todo When sending verification requests, ensure the stored proof's dates match the response(s)
26
 */
27
class Chainpoint implements BackendProvider
28
{
29
    use Configurable;
30
31
    /**
32
     * @return string
33
     */
34
    public function name() : string
35
    {
36
        return 'chainpoint';
37
    }
38
39
    /**
40
     * @return string
41
     */
42
    public function hashFunc() : string
43
    {
44
        return 'sha256';
45
    }
46
47
    /**
48
     * Send a single hash_id_node to retrieve a proof in binary format from the
49
     * Tierion network.
50
     *
51
     * GETs to the: "/proofs" REST API endpoint.
52
     *
53
     * @param  string $hashIdNode
54
     * @return string (From GuzzleHttp\Stream::getContents()
55
     * @todo Rename to proofs() as per the "gateway" we're calling
56
     * @todo modify to accept an array of hashes
57
     */
58
    public function getProof(string $hashIdNode) : string
59
    {
60
        $response = $this->client("/proofs", 'GET', ['hashids' => [$hashIdNode]]);
61
62
        return $response->getBody()->getContents() ?? '[]';
63
    }
64
65
    /**
66
     * Send an array of hashes for anchoring.
67
     *
68
     * POSTs to the: "/hashes" REST API endpoint.
69
     *
70
     * @param  array $hashes
71
     * @return string (From GuzzleHttp\Stream::getContents()
72
     * @todo Rename to hashes() as per the "gateway" we're calling
73
     */
74
    public function writeHash(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 bool
87
     * @todo See the returned proof's "uris" key, to be able to call a specific URI for proof verification.
88
     */
89
    public function verifyProof(string $proof) : bool
90
    {
91
        // Consult blockchains directly, if so configured and suitable
92
        // blockchain full-nodes are available to our RPC connections
93
        if ((bool) $this->config()->get('direct_verification')) {
94
            return $this->backend->verifyDirect($proof);
0 ignored issues
show
Bug Best Practice introduced by
The property backend does not exist on PhpTek\Verifiable\Backend\Chainpoint. Did you maybe forget to declare it?
Loading history...
95
        }
96
97
        $response = $this->client('/verify', 'POST', ['proofs' => [$proof]]);
98
        $contents = $response->getBody()->getContents();
99
100
        if ($contents === '[]') {
101
            return false;
102
        }
103
104
        return json_decode($contents, true)['status'] === 'verified';
105
    }
106
107
    /**
108
     * For each of this backend's supported blockchain networks, skips any intermediate
109
     * verification steps through the Tieron network, preferring instead to calculate
110
     * proofs ourselves in consultation directly with the relevant networks.
111
     *
112
     * @param  string $proof    The stored JSON-LD chainpoint proof
113
     * @param  array  $networks An array of available blockchains to consult
114
     * @return bool             Returns true if each blockchain found in $network
115
     *                          can verify our proof.
116
     * @todo   Implement via dedicated classes for each configured blockchain network.
117
     * @see    https://runkit.com/tierion/verify-a-chainpoint-proof-directly-using-bitcoin
118
     */
119
    protected function verifyProofDirect(string $proof, array $networks = [])
120
    {
121
        $result = [];
122
123
        foreach ($this->config()->get('blockchain_config') as $config) {
124
            if (in_array($config['name'], $networks)) {
125
                $implementation = ucfirst(strtolower($config['name']));
126
                $node = Injector::inst()->createWithArgs($implementation, [$config]);
127
128
                $result[strtolower($config['name'])] = $node->verifyProof($proof);
129
            }
130
        }
131
132
        return !in_array(false, $result);
133
    }
134
135
    /**
136
     * Return a client to use for all RPC traffic to this backend.
137
     *
138
     * @param  string   $url
139
     * @param  string   $verb
140
     * @param  array    $payload
141
     * @param  bool     $simple  Pass "base_uri" to {@link Client}.
142
     * @return Response Guzzle Response object
0 ignored issues
show
Bug introduced by
The type PhpTek\Verifiable\Backend\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...
143
     * @throws VerifiableBackendException
144
     * @todo Client()->setSslVerification() if required
145
     * @todo Use promises to send concurrent requests: 1). Find a node 2). Pass node URL to second request
146
     * @todo Use 'body' in POST requests?
147
     * @todo Port to dedicated ChainpointClient class
148
     */
149
    protected function client(string $url, string $verb, array $payload = [], bool $simple = true)
150
    {
151
        $verb = strtoupper($verb);
152
        $method = strtolower($verb);
153
        $config = $this->config()->get('client_config');
154
        $client = new Client([
155
            'base_uri' => $simple ? $this->fetchNodeUrl() : '',
156
            'verify' => true,
157
            'timeout'  => $config['timeout'],
158
            'connect_timeout'  => $config['connect_timeout'],
159
            'allow_redirects' => false,
160
            'user-agent' => sprintf(
161
                '%s %s',
162
                \GuzzleHttp\default_user_agent()
163
            )
164
        ]);
165
166
        try {
167
            // json_encodes POSTed data and sends correct Content-Type header
168
            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...
169
                $payload['json'] = $payload;
170
            }
171
172
            return $client->$method($url, $payload);
173
        } catch (RequestException $e) {
174
            throw new VerifiableValidationException($e->getMessage());
175
        }
176
    }
177
178
    /**
179
     * The Tierion network comprises many nodes, some of which may or may not be
180
     * online. Pings a randomly selected resource URL, who's response should contain
181
     * IPs of each advertised node, then calls each until one responds with an
182
     * HTTP 200.
183
     *
184
     * @return string
185
     * @throws VerifiableBackendException
186
     * @todo Set the URL as a class-property and re-use that, rather than re-calling fetchNodeUrl()
187
     * @todo Make this method re-entrant and try a different URL
188
     * @todo Rename method to
189
     */
190
    protected function fetchNodeUrl()
191
    {
192
        $chainpointUrls = $this->config()->get('chainpoint_urls');
193
        $url = $chainpointUrls[rand(0,2)];
194
        $response = $this->client($url, 'GET', [], false);
195
196
        if ($response->getStatusCode() !== 200) {
197
            throw new VerifiableBackendException('Bad response from node source URL');
198
        }
199
200
        foreach (json_decode($response->getBody(), true) as $candidate) {
201
            $response = $this->client($candidate['public_uri'], 'GET', [], false);
202
203
            // If for some reason we don't get a response: Do re-entrant method
204
            if ($response->getStatusCode() === 200) {
205
                return $candidate['public_uri'];
206
            }
207
        }
208
    }
209
210
}
211