SOAPClient::send()   F
last analyzed

Complexity

Conditions 19
Paths 768

Size

Total Lines 136
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 80
nc 768
nop 3
dl 0
loc 136
rs 0.6722
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\SAML2;
6
7
use DOMDocument;
8
use Exception;
9
use SimpleSAML\Configuration;
10
use SimpleSAML\SAML2\Compat\ContainerSingleton;
11
use SimpleSAML\SAML2\XML\samlp\AbstractMessage;
12
use SimpleSAML\SAML2\XML\samlp\MessageFactory;
13
use SimpleSAML\SOAP11\Utils\XPath;
14
use SimpleSAML\SOAP11\XML\Body;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\SOAP11\XML\Body 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...
15
use SimpleSAML\SOAP11\XML\Envelope;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\SOAP11\XML\Envelope 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...
16
use SimpleSAML\SOAP11\XML\Fault;
17
use SimpleSAML\Utils\Config;
18
use SimpleSAML\Utils\Crypto;
19
use SimpleSAML\XML\Chunk;
20
use SimpleSAML\XML\DOMDocumentFactory;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XML\DOMDocumentFactory 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...
21
use SimpleSAML\XMLSecurity\XMLSecurityKey;
0 ignored issues
show
Bug introduced by
The type SimpleSAML\XMLSecurity\XMLSecurityKey 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...
22
use SoapClient as BuiltinSoapClient;
23
24
use function chunk_split;
25
use function file_exists;
26
use function openssl_pkey_get_details;
27
use function openssl_pkey_get_public;
28
use function sha1;
29
use function stream_context_create;
30
use function stream_context_get_options;
31
32
/**
33
 * Implementation of the SAML 2.0 SOAP binding.
34
 *
35
 * @package simplesamlphp/saml2
36
 */
37
class SOAPClient
38
{
39
    /**
40
     * This function sends the SOAP message to the service location and returns SOAP response
41
     *
42
     * @param \SimpleSAML\SAML2\XML\samlp\AbstractMessage $msg The request that should be sent.
43
     * @param \SimpleSAML\Configuration $srcMetadata The metadata of the issuer of the message.
44
     * @param \SimpleSAML\Configuration $dstMetadata The metadata of the destination of the message.
45
     * @throws \Exception
46
     * @return \SimpleSAML\SAML2\XML\samlp\AbstractMessage The response we received.
47
     */
48
    public function send(
49
        AbstractMessage $msg,
50
        Configuration $srcMetadata,
51
        ?Configuration $dstMetadata = null,
52
    ): AbstractMessage {
53
        $issuer = $msg->getIssuer();
54
55
        $ctxOpts = [
56
            'ssl' => [
57
                'capture_peer_cert' => true,
58
                'allow_self_signed' => true,
59
            ],
60
        ];
61
62
        $container = ContainerSingleton::getInstance();
63
64
        // Determine if we are going to do a MutualSSL connection between the IdP and SP  - Shoaib
65
        if ($srcMetadata->hasValue('saml.SOAPClient.certificate')) {
66
            $cert = $srcMetadata->getValue('saml.SOAPClient.certificate');
67
            if ($cert !== false) {
68
                $configUtils = new Config();
69
                $ctxOpts['ssl']['local_cert'] = $configUtils->getCertPath(
70
                    $srcMetadata->getString('saml.SOAPClient.certificate'),
71
                );
72
                if ($srcMetadata->hasValue('saml.SOAPClient.privatekey_pass')) {
73
                    $ctxOpts['ssl']['passphrase'] = $srcMetadata->getString('saml.SOAPClient.privatekey_pass');
74
                }
75
            }
76
        } else {
77
            /* Use the SP certificate and privatekey if it is configured. */
78
            $cryptoUtils = new Crypto();
79
            $privateKey = $cryptoUtils->loadPrivateKey($srcMetadata);
80
            $publicKey = $cryptoUtils->loadPublicKey($srcMetadata);
81
            if ($privateKey !== null && $publicKey !== null && isset($publicKey['PEM'])) {
82
                $keyCertData = $privateKey['PEM'] . $publicKey['PEM'];
83
                $file = $container->getTempDir() . '/' . sha1($keyCertData) . '.pem';
84
                if (!file_exists($file)) {
85
                    $container->writeFile($file, $keyCertData);
86
                }
87
                $ctxOpts['ssl']['local_cert'] = $file;
88
                if (isset($privateKey['password'])) {
89
                    $ctxOpts['ssl']['passphrase'] = $privateKey['password'];
90
                }
91
            }
92
        }
93
94
        // do peer certificate verification
95
        if ($dstMetadata !== null) {
96
            $peerPublicKeys = $dstMetadata->getPublicKeys('signing', true);
97
            $certData = '';
98
            foreach ($peerPublicKeys as $key) {
99
                if ($key['type'] !== 'X509Certificate') {
100
                    continue;
101
                }
102
                $certData .= "-----BEGIN CERTIFICATE-----\n" .
103
                    chunk_split($key['X509Certificate'], 64) .
104
                    "-----END CERTIFICATE-----\n";
105
            }
106
            $peerCertFile = $container->getTempDir() . '/' . sha1($certData) . '.pem';
107
            if (!file_exists($peerCertFile)) {
108
                $container->writeFile($peerCertFile, $certData);
109
            }
110
            // create ssl context
111
            $ctxOpts['ssl']['verify_peer'] = true;
112
            $ctxOpts['ssl']['verify_depth'] = 1;
113
            $ctxOpts['ssl']['cafile'] = $peerCertFile;
114
        }
115
116
        if ($srcMetadata->hasValue('saml.SOAPClient.stream_context.ssl.peer_name')) {
117
            $ctxOpts['ssl']['peer_name'] = $srcMetadata->getString('saml.SOAPClient.stream_context.ssl.peer_name');
118
        }
119
120
        $context = stream_context_create($ctxOpts);
121
122
        $options = [
123
            'uri' => $issuer?->getContent(),
124
            'location' => $msg->getDestination(),
125
            'stream_context' => $context,
126
        ];
127
128
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyhost')) {
129
            $options['proxy_host'] = $srcMetadata->getValue('saml.SOAPClient.proxyhost');
130
        }
131
132
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyport')) {
133
            $options['proxy_port'] = $srcMetadata->getValue('saml.SOAPClient.proxyport');
134
        }
135
136
        $destination = $msg->getDestination();
137
        if ($destination === null) {
138
            throw new Exception('Cannot send SOAP message, no destination set.');
139
        }
140
141
        // Add soap-envelopes
142
        $env = (new Envelope(new Body([new Chunk($msg->toXML())])))->toXML();
143
        $request = $env->ownerDocument?->saveXML();
144
145
        $container->debugMessage($request, 'out');
146
147
        $action = 'http://www.oasis-open.org/committees/security';
148
        /* Perform SOAP Request over HTTP */
149
        $x = new BuiltinSoapClient(null, $options);
150
        $soapresponsexml = $x->__doRequest($request, $destination, $action, SOAP_1_1, false);
151
        if (empty($soapresponsexml)) {
152
            throw new Exception('Empty SOAP response, check peer certificate.');
153
        }
154
155
        Utils::getContainer()->debugMessage($soapresponsexml, 'in');
156
157
        $dom = DOMDocumentFactory::fromString($soapresponsexml);
158
        $env = Envelope::fromXML($dom->documentElement);
159
        $container->debugMessage($env->toXML()->ownerDocument?->saveXML(), 'in');
160
161
        $soapfault = $this->getSOAPFault($dom);
162
        if ($soapfault !== null) {
163
            throw new Exception(
164
                sprintf(
165
                    "Actor: '%s';  Message: '%s';  Code: '%s'",
166
                    $soapfault->getFaultActor()?->getContent(),
167
                    $soapfault->getFaultString()->getContent(),
168
                    $soapfault->getFaultCode()->getContent(),
169
                ),
170
            );
171
        }
172
173
        // Extract the message from the response
174
        /** @var \SimpleSAML\XML\SerializableElementInterface[] $messages */
175
        $messages = $env->getBody()->getElements();
176
        $samlresponse = MessageFactory::fromXML($messages[0]->toXML());
177
178
        /* Add validator to message which uses the SSL context. */
179
        self::addSSLValidator($samlresponse, $context);
180
181
        $container->getLogger()->debug("Valid ArtifactResponse received from IdP");
182
183
        return $samlresponse;
184
    }
185
186
187
    /**
188
     * Add a signature validator based on a SSL context.
189
     *
190
     * @param \SimpleSAML\SAML2\XML\samlp\AbstractMessage $msg The message we should add a validator to.
191
     * @param resource $context The stream context.
192
     */
193
    private static function addSSLValidator(AbstractMessage $msg, $context): void
194
    {
195
        $options = stream_context_get_options($context);
196
        if (!isset($options['ssl']['peer_certificate'])) {
197
            return;
198
        }
199
200
        $container = ContainerSingleton::getInstance();
201
        $key = openssl_pkey_get_public($options['ssl']['peer_certificate']);
202
        if ($key === false) {
203
            $container->getLogger()->warning('Unable to get public key from peer certificate.');
204
            return;
205
        }
206
207
        $keyInfo = openssl_pkey_get_details($key);
208
        if ($keyInfo === false) {
209
            $container->getLogger()->warning('Unable to get key details from public key.');
210
            return;
211
        }
212
213
        if (!isset($keyInfo['key'])) {
214
            $container->getLogger()->warning('Missing key in public key details.');
215
            return;
216
        }
217
218
        $msg->addValidator([SOAPClient::class, 'validateSSL'], $keyInfo['key']);
219
    }
220
221
222
    /**
223
     * Validate a SOAP message against the certificate on the SSL connection.
224
     *
225
     * @param string $data The public key that was used on the connection.
226
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key The key we should validate the certificate against.
227
     * @throws \Exception
228
     */
229
    public static function validateSSL(string $data, XMLSecurityKey $key): void
230
    {
231
        $container = ContainerSingleton::getInstance();
232
233
        $keyInfo = openssl_pkey_get_details($key->key);
234
        if ($keyInfo === false) {
235
            throw new Exception('Unable to get key details from XMLSecurityKey.');
236
        }
237
238
        if (!isset($keyInfo['key'])) {
239
            throw new Exception('Missing key in public key details.');
240
        }
241
242
        if ($keyInfo['key'] !== $data) {
243
            $container->getLogger()->debug('Key on SSL connection did not match key we validated against.');
244
            return;
245
        }
246
247
        $container->getLogger()->debug('Message validated based on SSL certificate.');
248
    }
249
250
251
    /**
252
     * Extracts the SOAP Fault from SOAP message
253
     *
254
     * @param \DOMDocument $soapMessage Soap response needs to be type DOMDocument
255
     * @return \SimpleSAML\SOAP11\XML\Fault|null
256
     */
257
    private function getSOAPFault(DOMDocument $soapMessage): ?Fault
258
    {
259
        $soapFault = XPath::xpQuery(
260
            $soapMessage->firstChild,
0 ignored issues
show
Bug introduced by
It seems like $soapMessage->firstChild can also be of type null; however, parameter $node of SimpleSAML\XPath\XPath::xpQuery() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

260
            /** @scrutinizer ignore-type */ $soapMessage->firstChild,
Loading history...
261
            '/env:Envelope/env:Body/env:Fault',
262
            XPath::getXPath($soapMessage->firstChild),
0 ignored issues
show
Bug introduced by
It seems like $soapMessage->firstChild can also be of type null; however, parameter $node of SimpleSAML\SOAP11\Utils\XPath::getXPath() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

262
            XPath::getXPath(/** @scrutinizer ignore-type */ $soapMessage->firstChild),
Loading history...
263
        );
264
265
        if (empty($soapFault)) {
266
            /* No fault. */
267
            return null;
268
        }
269
270
        return Fault::fromXML($soapFault[0]);
271
    }
272
}
273