Passed
Push — master ( 744ef1...4426f1 )
by Tim
02:17
created

EntitySource   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 251
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 103
dl 0
loc 251
rs 10
c 0
b 0
f 0
wmc 24

4 Methods

Rating   Name   Duplication   Size   Complexity  
B getMetadata() 0 32 7
A updateCache() 0 21 5
A __construct() 0 15 2
B downloadMetadata() 0 87 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\aggregator2;
6
7
use DOMDocument;
8
use Exception;
9
use SimpleSAML\Configuration;
10
use SimpleSAML\Error;
11
use SimpleSAML\Logger;
12
use SimpleSAML\SAML2\Utils\XPath;
13
use SimpleSAML\SAML2\XML\md\EntitiesDescriptor;
14
use SimpleSAML\SAML2\XML\md\EntityDescriptor;
15
use SimpleSAML\Utils;
16
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
17
use SimpleSAML\XMLSecurity\Key\PublicKey;
18
19
/**
20
 * Class for loading metadata from files and URLs.
21
 *
22
 * @package SimpleSAMLphp
23
 */
24
class EntitySource
25
{
26
    /**
27
     * Our log "location".
28
     *
29
     * @var string
30
     */
31
    protected string $logLoc;
32
33
    /**
34
     * The aggregator we belong to.
35
     *
36
     * @var \SimpleSAML\Module\aggregator2\Aggregator
37
     */
38
    protected Aggregator $aggregator;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\Module\aggregator2\Aggregator 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...
39
40
    /**
41
     * The URL we should fetch it from.
42
     *
43
     * @var string
44
     */
45
    protected string $url;
46
47
    /**
48
     * The SSL CA file that should be used to validate the connection.
49
     *
50
     * @var string|null
51
     */
52
    protected ?string $sslCAFile;
53
54
    /**
55
     * The certificate we should use to validate downloaded metadata.
56
     *
57
     * @var string|null
58
     */
59
    protected ?string $certificate;
60
61
    /**
62
     * The parsed metadata.
63
     *
64
     * @var \SAML2\XML\md\EntitiesDescriptor|\SAML2\XML\md\EntityDescriptor|null
65
     */
66
    protected $metadata = null;
67
68
    /**
69
     * The cache ID.
70
     *
71
     * @var string
72
     */
73
    protected string $cacheId;
74
75
    /**
76
     * The cache tag.
77
     *
78
     * @var string
79
     */
80
    protected string $cacheTag;
81
82
    /**
83
     * Whether we have attempted to update the cache already.
84
     *
85
     * @var bool
86
     */
87
    protected bool $updateAttempted = false;
88
89
90
    /**
91
     * Initialize this EntitySource.
92
     *
93
     * @param \SimpleSAML\Configuration $config  The configuration.
94
     */
95
    public function __construct(Aggregator $aggregator, Configuration $config)
96
    {
97
        $this->logLoc = 'aggregator2:' . $aggregator->getId() . ': ';
98
        $this->aggregator = $aggregator;
99
100
        $this->url = $config->getString('url');
101
        $this->sslCAFile = $config->getOptionalString('ssl.cafile', null);
102
        if ($this->sslCAFile === null) {
103
            $this->sslCAFile = $aggregator->getCAFile();
104
        }
105
106
        $this->certificate = $config->getOptionalString('cert', null);
107
108
        $this->cacheId = sha1($this->url);
109
        $this->cacheTag = sha1(serialize($config));
110
    }
111
112
113
    /**
114
     * Retrieve and parse the metadata.
115
     *
116
     * @return \SimpleSAML\SAML2\XML\md\EntitiesDescriptor|\SAML2\XML\md\EntityDescriptor|null
117
     * The downloaded metadata or NULL if we were unable to download or parse it.
118
     */
119
    private function downloadMetadata()
120
    {
121
        Logger::debug($this->logLoc . 'Downloading metadata from ' . var_export($this->url, true));
122
        $configUtils = new Utils\Config();
123
124
        $context = ['ssl' => []];
125
        if ($this->sslCAFile !== null) {
126
            $context['ssl']['cafile'] = $configUtils->getCertPath($this->sslCAFile);
127
            Logger::debug(
128
                $this->logLoc . 'Validating https connection against CA certificate(s) found in ' .
129
                var_export($context['ssl']['cafile'], true)
130
            );
131
            $context['ssl']['verify_peer'] = true;
132
            $context['ssl']['CN_match'] = parse_url($this->url, PHP_URL_HOST);
133
        }
134
135
        try {
136
            $httpUtils = new Utils\HTTP();
137
            $data = $httpUtils->fetch($this->url, $context, false);
138
        } catch (Error\Exception $e) {
139
            Logger::error($this->logLoc . 'Unable to load metadata from ' . var_export($this->url, true));
140
            return null;
141
        }
142
143
        $doc = new DOMDocument();
144
        /** @var string $data */
145
        $res = $doc->loadXML($data);
146
        if (!$res) {
147
            Logger::error($this->logLoc . 'Error parsing XML from ' . var_export($this->url, true));
148
            return null;
149
        }
150
151
        /** @psalm-var \DOMElement[] $root */
152
        $root = XPath::xpQuery(
153
            $doc->documentElement,
154
            '/saml_metadata:EntityDescriptor|/saml_metadata:EntitiesDescriptor',
155
            XPath::getXPath($doc->documentElement),
156
        );
157
158
        if (count($root) === 0) {
159
            Logger::error(
160
                $this->logLoc . 'No <EntityDescriptor> or <EntitiesDescriptor> in metadata from ' .
161
                var_export($this->url, true)
162
            );
163
            return null;
164
        }
165
166
        if (count($root) > 1) {
167
            Logger::error(
168
                $this->logLoc . 'More than one <EntityDescriptor> or <EntitiesDescriptor> in metadata from ' .
169
                var_export($this->url, true)
170
            );
171
            return null;
172
        }
173
174
        $root = $root[0];
175
        try {
176
            if ($root->localName === 'EntityDescriptor') {
177
                $md = EntityDescriptor::fromXML($root);
178
            } else {
179
                $md = EntitiesDescriptor::fromXML($root);
180
            }
181
        } catch (Exception $e) {
182
            Logger::error(
183
                $this->logLoc . 'Unable to parse metadata from ' .
184
                  var_export($this->url, true) . ': ' . $e->getMessage()
185
            );
186
            return null;
187
        }
188
189
        if ($this->certificate !== null) {
190
            $file = $configUtils->getCertPath($this->certificate);
191
            $certData = file_get_contents($file);
192
            if ($certData === false) {
193
                throw new Exception('Error loading certificate from ' . var_export($file, true));
194
            }
195
196
            $verifier = (new SignatureAlgorithmFactory())->getAlgorithm(
197
                $md->getSignature()->getSignedInfo()->getSignatureMethod()->getAlgorithm(),
198
                PublicKey::fromFile($file),
199
            );
200
201
            $md = $md->verify($verifier);
202
            Logger::debug($this->logLoc . 'Validated signature on metadata from ' . var_export($this->url, true));
203
        }
204
205
        return $md;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $md also could return the type SimpleSAML\SAML2\XML\md\EntityDescriptor which is incompatible with the documented return type SAML2\XML\md\EntityDescr...EntitiesDescriptor|null.
Loading history...
206
    }
207
208
209
    /**
210
     * Attempt to update our cache file.
211
     */
212
    public function updateCache(): void
213
    {
214
        if ($this->updateAttempted) {
215
            return;
216
        }
217
        $this->updateAttempted = true;
218
219
        $this->metadata = $this->downloadMetadata();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->downloadMetadata() of type SimpleSAML\SAML2\XML\md\EntitiesDescriptor is incompatible with the declared type SAML2\XML\md\EntitiesDes...d\EntityDescriptor|null of property $metadata.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
220
        if ($this->metadata === null) {
221
            return;
222
        }
223
224
        $expires = time() + 24 * 60 * 60; // Default expires in one day
225
226
        if ($this->metadata->getValidUntil() !== null && $this->metadata->getValidUntil() < $expires) {
227
            $expires = $this->metadata->getValidUntil();
228
        }
229
230
        $metadataSerialized = serialize($this->metadata);
231
232
        $this->aggregator->addCacheItem($this->cacheId, $metadataSerialized, $expires, $this->cacheTag);
233
    }
234
235
236
    /**
237
     * Retrieve the metadata file.
238
     *
239
     * This function will check its cached copy, to see whether it can be used.
240
     *
241
     * @return \SAML2\XML\md\EntityDescriptor|\SAML2\XML\md\EntitiesDescriptor|null  The downloaded metadata.
242
     */
243
    public function getMetadata()
244
    {
245
        if ($this->metadata !== null) {
246
            /* We have already downloaded the metdata. */
247
            return $this->metadata;
248
        }
249
250
        if (!$this->aggregator->isCacheValid($this->cacheId, $this->cacheTag)) {
251
            $this->updateCache();
252
            /** @psalm-suppress TypeDoesNotContainType */
253
            if ($this->metadata !== null) {
254
                return $this->metadata;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->metadata returns the type SimpleSAML\SAML2\XML\md\EntitiesDescriptor which is incompatible with the documented return type SAML2\XML\md\EntitiesDes...d\EntityDescriptor|null.
Loading history...
255
            }
256
            /* We were unable to update the cache - use cached metadata. */
257
        }
258
259
        $cacheFile = $this->aggregator->getCacheFile($this->cacheId);
260
261
        if (is_null($cacheFile) || !file_exists($cacheFile)) {
262
            Logger::error($this->logLoc . 'No cached metadata available.');
263
            return null;
264
        }
265
266
        Logger::debug($this->logLoc . 'Using cached metadata from ' . var_export($cacheFile, true));
267
268
        $metadata = file_get_contents($cacheFile);
269
        if ($metadata !== false) {
270
            $this->metadata = unserialize($metadata);
271
            return $this->metadata;
272
        }
273
274
        return null;
275
    }
276
}
277