Completed
Pull Request — master (#2)
by Tim
02:59
created

EntitySource::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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