EntitySource::getMetadata()   B
last analyzed

Complexity

Conditions 7
Paths 8

Size

Total Lines 32
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 8
nop 0
dl 0
loc 32
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\aggregator2;
6
7
use DateInterval;
8
use DateTimeImmutable;
9
use DateTimeZone;
10
use Exception;
11
use SimpleSAML\Configuration;
12
use SimpleSAML\Error;
13
use SimpleSAML\Logger;
14
use SimpleSAML\SAML2\Utils\XPath;
15
use SimpleSAML\SAML2\XML\md\EntitiesDescriptor;
16
use SimpleSAML\SAML2\XML\md\EntityDescriptor;
17
use SimpleSAML\Utils;
18
use SimpleSAML\XML\DOMDocumentFactory;
19
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
20
use SimpleSAML\XMLSecurity\Key\PublicKey;
21
22
use function file_exists;
23
use function file_get_contents;
24
use function is_null;
25
use function parse_url;
26
use function serialize;
27
use function sha1;
28
use function strval;
29
use function time;
30
use function var_export;
31
32
/**
33
 * Class for loading metadata from files and URLs.
34
 *
35
 * @package SimpleSAMLphp
36
 */
37
class EntitySource
38
{
39
    /**
40
     * Our log "location".
41
     *
42
     * @var string
43
     */
44
    protected string $logLoc;
45
46
    /**
47
     * The aggregator we belong to.
48
     *
49
     * @var \SimpleSAML\Module\aggregator2\Aggregator
50
     */
51
    protected Aggregator $aggregator;
52
53
    /**
54
     * The URL we should fetch it from.
55
     *
56
     * @var string
57
     */
58
    protected string $url;
59
60
    /**
61
     * The SSL CA file that should be used to validate the connection.
62
     *
63
     * @var string|null
64
     */
65
    protected ?string $sslCAFile;
66
67
    /**
68
     * The certificate we should use to validate downloaded metadata.
69
     *
70
     * @var string|null
71
     */
72
    protected ?string $certificate;
73
74
    /**
75
     * The parsed metadata.
76
     *
77
     * @var \SimpleSAML\SAML2\XML\md\EntitiesDescriptor|\SimpleSAML\SAML2\XML\md\EntityDescriptor|null
78
     */
79
    protected EntityDescriptor|EntitiesDescriptor|null $metadata = null;
80
81
    /**
82
     * The cache ID.
83
     *
84
     * @var string
85
     */
86
    protected string $cacheId;
87
88
    /**
89
     * The cache tag.
90
     *
91
     * @var string
92
     */
93
    protected string $cacheTag;
94
95
    /**
96
     * Whether we have attempted to update the cache already.
97
     *
98
     * @var bool
99
     */
100
    protected bool $updateAttempted = false;
101
102
103
    /**
104
     * Initialize this EntitySource.
105
     *
106
     * @param \SimpleSAML\Configuration $config  The configuration.
107
     */
108
    public function __construct(Aggregator $aggregator, Configuration $config)
109
    {
110
        $this->logLoc = 'aggregator2:' . $aggregator->getId() . ': ';
111
        $this->aggregator = $aggregator;
112
113
        $this->url = $config->getString('url');
114
        $this->sslCAFile = $config->getOptionalString('ssl.cafile', null);
115
        if ($this->sslCAFile === null) {
116
            $this->sslCAFile = $aggregator->getCAFile();
117
        }
118
119
        $this->certificate = $config->getOptionalString('cert', null);
120
121
        $this->cacheId = sha1($this->url);
122
        $this->cacheTag = sha1(serialize($config));
123
    }
124
125
126
    /**
127
     * Retrieve and parse the metadata.
128
     *
129
     * @return \SimpleSAML\SAML2\XML\md\EntitiesDescriptor|\SimpleSAML\SAML2\XML\md\EntityDescriptor|null
130
     * The downloaded metadata or NULL if we were unable to download or parse it.
131
     */
132
    private function downloadMetadata(): EntitiesDescriptor|EntityDescriptor|null
133
    {
134
        Logger::debug($this->logLoc . 'Downloading metadata from ' . var_export($this->url, true));
135
        $configUtils = new Utils\Config();
136
137
        $context = ['ssl' => []];
138
        if ($this->sslCAFile !== null) {
139
            $context['ssl']['cafile'] = $configUtils->getCertPath($this->sslCAFile);
140
            Logger::debug(
141
                $this->logLoc . 'Validating https connection against CA certificate(s) found in ' .
142
                var_export($context['ssl']['cafile'], true),
143
            );
144
            $context['ssl']['verify_peer'] = true;
145
            $context['ssl']['CN_match'] = parse_url($this->url, PHP_URL_HOST);
146
        }
147
148
        try {
149
            $httpUtils = new Utils\HTTP();
150
            $data = $httpUtils->fetch($this->url, $context, false);
151
        } catch (Error\Exception $e) {
152
            Logger::error($this->logLoc . 'Unable to load metadata from ' . var_export($this->url, true));
153
            return null;
154
        }
155
156
        $doc = DOMDocumentFactory::create();
157
        /** @var string $data */
158
        $res = $doc->loadXML($data);
159
        if (!$res) {
160
            Logger::error($this->logLoc . 'Error parsing XML from ' . var_export($this->url, true));
161
            return null;
162
        }
163
164
        /** @psalm-var \DOMElement[] $root */
165
        $root = XPath::xpQuery(
166
            $doc->documentElement,
167
            '/saml_metadata:EntityDescriptor|/saml_metadata:EntitiesDescriptor',
168
            XPath::getXPath($doc->documentElement),
169
        );
170
171
        if (count($root) === 0) {
172
            Logger::error(
173
                $this->logLoc . 'No <EntityDescriptor> or <EntitiesDescriptor> in metadata from ' .
174
                var_export($this->url, true),
175
            );
176
            return null;
177
        }
178
179
        if (count($root) > 1) {
180
            Logger::error(
181
                $this->logLoc . 'More than one <EntityDescriptor> or <EntitiesDescriptor> in metadata from ' .
182
                var_export($this->url, true),
183
            );
184
            return null;
185
        }
186
187
        $root = $root[0];
188
        try {
189
            if ($root->localName === 'EntityDescriptor') {
190
                $md = EntityDescriptor::fromXML($root);
191
            } else {
192
                $md = EntitiesDescriptor::fromXML($root);
193
            }
194
        } catch (Exception $e) {
195
            Logger::error(
196
                $this->logLoc . 'Unable to parse metadata from ' .
197
                var_export($this->url, true) . ': ' . $e->getMessage(),
198
            );
199
            return null;
200
        }
201
202
        if ($this->certificate !== null) {
203
            $file = $configUtils->getCertPath($this->certificate);
204
            $verifier = (new SignatureAlgorithmFactory())->getAlgorithm(
205
                $md->getSignature()->getSignedInfo()->getSignatureMethod()->getAlgorithm(),
206
                PublicKey::fromFile($file),
207
            );
208
209
            /** @var \SimpleSAML\SAML2\XML\md\EntitiesDescriptor|\SimpleSAML\SAML2\XML\md\EntityDescriptor $md */
210
            $md = $md->verify($verifier);
211
            Logger::debug($this->logLoc . 'Validated signature on metadata from ' . var_export($this->url, true));
212
        }
213
214
        return $md;
215
    }
216
217
218
    /**
219
     * Attempt to update our cache file.
220
     */
221
    public function updateCache(): void
222
    {
223
        if ($this->updateAttempted) {
224
            return;
225
        }
226
        $this->updateAttempted = true;
227
228
        $this->metadata = $this->downloadMetadata();
229
        if ($this->metadata === null) {
230
            return;
231
        }
232
233
        $now = new DateTimeImmutable('@' . strval(time()));
234
        $now = $now->setTimeZone(new DateTimeZone('Z'));
235
        $expires = $now->add(new DateInterval('PT24H'));
236
237
        if ($this->metadata->getValidUntil() !== null && $this->metadata->getValidUntil() < $expires) {
238
            $expires = $this->metadata->getValidUntil();
239
        }
240
241
        $metadataSerialized = serialize($this->metadata);
242
243
        $this->aggregator->addCacheItem($this->cacheId, $metadataSerialized, $expires, $this->cacheTag);
244
    }
245
246
247
    /**
248
     * Retrieve the metadata file.
249
     *
250
     * This function will check its cached copy, to see whether it can be used.
251
     *
252
     * @return \SimpleSAML\SAML2\XML\md\EntityDescriptor|\SimpleSAML\SAML2\XML\md\EntitiesDescriptor|null
253
     *   The downloaded metadata.
254
     */
255
    public function getMetadata(): EntityDescriptor|EntitiesDescriptor|null
256
    {
257
        if ($this->metadata !== null) {
258
            /* We have already downloaded the metdata. */
259
            return $this->metadata;
260
        }
261
262
        if (!$this->aggregator->isCacheValid($this->cacheId, $this->cacheTag)) {
263
            $this->updateCache();
264
            /** @psalm-suppress TypeDoesNotContainType */
265
            if ($this->metadata !== null) {
266
                return $this->metadata;
267
            }
268
            /* We were unable to update the cache - use cached metadata. */
269
        }
270
271
        $cacheFile = $this->aggregator->getCacheFile($this->cacheId);
272
273
        if (is_null($cacheFile) || !file_exists($cacheFile)) {
274
            Logger::error($this->logLoc . 'No cached metadata available.');
275
            return null;
276
        }
277
278
        Logger::debug($this->logLoc . 'Using cached metadata from ' . var_export($cacheFile, true));
279
280
        $metadata = file_get_contents($cacheFile);
281
        if ($metadata !== false) {
282
            $this->metadata = unserialize($metadata);
283
            return $this->metadata;
284
        }
285
286
        return null;
287
    }
288
}
289