Completed
Push — master ( b97427...e235cc )
by Raffael
30:35 queued 26:08
created

HostManager::getHosts()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 0
cts 26
cp 0
rs 9.1288
c 0
b 0
f 0
cc 5
nc 4
nop 0
crap 30
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Wopi;
13
14
use GuzzleHttp\ClientInterface as GuzzleHttpClientInterface;
15
use InvalidArgumentException;
16
use phpseclib\Crypt\RSA;
17
use phpseclib\Math\BigInteger;
18
use Psr\Log\LoggerInterface;
19
use Psr\SimpleCache\CacheInterface;
20
21
class HostManager
22
{
23
    /**
24
     * Hosts.
25
     *
26
     * @var array
27
     */
28
    protected $hosts = [];
29
30
    /**
31
     * Client url.
32
     *
33
     * @var string
34
     */
35
    protected $client_url;
36
37
    /**
38
     * HTTP client.
39
     *
40
     * @var GuzzleHttpClientInterface
41
     */
42
    protected $client;
43
44
    /**
45
     * Logger.
46
     *
47
     * @var LoggerInterface
48
     */
49
    protected $logger;
50
51
    /**
52
     * Cache.
53
     *
54
     * @var CacheInterface
55
     */
56
    protected $cache;
57
58
    /**
59
     * Cache ttl.
60
     *
61
     * @var int
62
     */
63
    protected $cache_ttl = 84600;
64
65
    /**
66
     * Validate proof.
67
     *
68
     * @var bool
69
     */
70
    protected $validate_proof = true;
71
72
    /**
73
     * Hosts.
74
     */
75
    public function __construct(GuzzleHttpClientInterface $client, CacheInterface $cache, LoggerInterface $logger, array $config = [])
76
    {
77
        $this->client = $client;
78
        $this->cache = $cache;
79
        $this->logger = $logger;
80
        $this->setOptions($config);
81
    }
82
83
    /**
84
     * Set options.
85
     */
86
    public function setOptions(array $config = []): HostManager
87
    {
88
        foreach ($config as $option => $value) {
89
            switch ($option) {
90
                case 'hosts':
91
                    $this->hosts = (array) $value;
92
93
                    break;
94
                case 'client_url':
95
                    $this->client_url = (string) $value;
96
97
                break;
98
                case 'cache_ttl':
99
                    $this->cache_ttl = (int) $value;
100
101
                break;
102
                case 'validate_proof':
103
                    $this->validate_proof = (bool) $value;
104
105
                break;
106
                default:
107
                    throw new InvalidArgumentexception('invalid option '.$option.' given');
108
            }
109
        }
110
111
        return $this;
112
    }
113
114
    /**
115
     * Get client url.
116
     */
117
    public function getClientUrl(): ?string
118
    {
119
        return $this->client_url;
120
    }
121
122
    /**
123
     * Get session by id.
124
     */
125
    public function getHosts(): array
126
    {
127
        $result = [];
128
129
        foreach ($this->hosts as $url) {
130
            if (!isset($url['url']) || !isset($url['name'])) {
131
                $this->logger->error('skip wopi host entry, either name or url not set', [
132
                    'category' => get_class($this),
133
                ]);
134
135
                continue;
136
            }
137
138
            try {
139
                $result[] = [
140
                    'url' => $url['url'],
141
                    'name' => $url['name'],
142
                    'discovery' => $this->fetchDiscovery($url['url']),
143
                ];
144
            } catch (\Exception $e) {
145
                $this->logger->error('failed to fetch wopi discovery document [{url}]', [
146
                    'category' => get_class($this),
147
                    'url' => $url,
148
                    'exception' => $e,
149
                ]);
150
            }
151
        }
152
153
        return $result;
154
    }
155
156
    /**
157
     * Verify wopi proof key.
158
     *
159
     * @see https://wopi.readthedocs.io/en/latest/scenarios/proofkeys.html
160
     * @see https://github.com/microsoft/Office-Online-Test-Tools-and-Documentation/blob/master/samples/python/proof_keys/__init__.py
161
     */
162
    public function verifyWopiProof(array $data): bool
163
    {
164
        if ($this->validate_proof === false) {
165
            $this->logger->debug('skip wopi proof validation, validate_proof is disabled', [
166
                'category' => get_class($this),
167
            ]);
168
169
            return true;
170
        }
171
172
        foreach ($this->getHosts() as $host) {
173
            if (!isset($host['discovery']['proof-key']['@attributes']['modulus'])) {
174
                $this->logger->debug('skip wopi proof validation, no public keys for wopi host [{host}] provided', [
175
                    'category' => get_class($this),
176
                    'host' => $host['url'],
177
                ]);
178
179
                continue;
180
            }
181
182
            $this->logger->debug('start wopi proof validation for host [{host}]', [
183
                'category' => get_class($this),
184
                'host' => $host['url'],
185
                'data' => $data,
186
            ]);
187
188
            $keys = $host['discovery']['proof-key']['@attributes'];
189
            $pub_key_old = new RSA();
190
            $pub_key_old->setSignatureMode(RSA::SIGNATURE_PKCS1);
191
            $pub_key_old->setHash('sha256');
192
            $key_old = [
193
                'n' => new BigInteger(base64_decode($keys['oldmodulus']), 256),
194
                'e' => new BigInteger(base64_decode($keys['oldexponent']), 256),
195
            ];
196
            $pub_key_old->loadKey($key_old);
197
198
            $pub_key = new RSA();
199
            $pub_key->setSignatureMode(RSA::SIGNATURE_PKCS1);
200
            $pub_key->setHash('sha256');
201
            $key = [
202
                'n' => new BigInteger(base64_decode($keys['modulus']), 256),
203
                'e' => new BigInteger(base64_decode($keys['exponent']), 256),
204
            ];
205
206
            $pub_key->loadKey($key);
207
208
            //php string is already a byte array
209
            $token_bytes = $data['access-token'];
210
            //pack number of token bytes into 32 bit, big endian byte order => 4bytes
211
            $token_length_bytes = pack('N*', strlen($token_bytes));
212
            //php string is already a byte array, specs require url all upper case
213
            $url_bytes = strtoupper($data['host-url']);
214
            //pack number of url bytes into 32 bit, big endian byte order => 4bytes
215
            $url_length_bytes = pack('N*', strlen($url_bytes));
216
            //pack timestamp into 64 bit, big endian byte order => 8bytes
217
            $timestamp_bytes = pack('J*', (int) $data['timestamp']);
218
            //pack number of url bytes into 32 bit, big endian byte order => 4bytes
219
            $timestamp_length_bytes = pack('N*', strlen($timestamp_bytes));
220
221
            $expected = implode('', [
222
                $token_length_bytes,
223
                $token_bytes,
224
                $url_length_bytes,
225
                $url_bytes,
226
                $timestamp_length_bytes,
227
                $timestamp_bytes,
228
            ]);
229
230
            if ($pub_key->verify($expected, base64_decode($data['proof'])) ||
231
              $pub_key->verify($expected, base64_decode($data['proof-old'])) ||
232
              $pub_key_old->verify($expected, base64_decode($data['proof']))) {
233
                $this->logger->debug('wopi proof signature matches', [
234
                    'category' => get_class($this),
235
                ]);
236
237
                return true;
238
            }
239
        }
240
241
        throw new Exception\WopiProofValidationFailed('wopi signature validation failed');
242
    }
243
244
    /**
245
     * Fetch discovery.
246
     */
247
    protected function fetchDiscovery(string $url): array
248
    {
249
        $key = md5($url);
250
251
        if ($this->cache->has($key)) {
252
            return $this->cache->get($key);
253
        }
254
255
        $this->logger->debug('wopi discovery not found in cache, fetch wopi discovery [{url}]', [
256
            'category' => get_class($this),
257
            'url' => $url,
258
        ]);
259
260
        $response = $this->client->request(
261
            'GET',
262
            $url
263
         );
264
265
        $body = $response->getBody()->getContents();
266
        $body = json_decode(json_encode(simplexml_load_string($body), JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR | JSON_OBJECT_AS_ARRAY);
267
268
        $result = $this->cache->set($key, $body, $this->cache_ttl);
269
270
        $this->logger->debug('stored wopi discovery [{url}] in cache for [{ttl}]s', [
271
            'category' => get_class($this),
272
            'url' => $url,
273
            'ttl' => $this->cache_ttl,
274
            'result' => $result,
275
        ]);
276
277
        return $body;
278
    }
279
}
280