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\SOAP\Utils\XPath;
14
use SimpleSAML\SOAP\XML\env_200106\Body;
15
use SimpleSAML\SOAP\XML\env_200106\Envelope;
16
use SimpleSAML\SOAP\XML\env_200106\Fault;
17
use SimpleSAML\Utils\Config;
18
use SimpleSAML\Utils\Crypto;
19
use SimpleSAML\XML\Chunk;
20
use SimpleSAML\XML\DOMDocumentFactory;
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
     * @psalm-suppress UndefinedClass
49
     */
50
    public function send(
51
        AbstractMessage $msg,
52
        Configuration $srcMetadata,
53
        ?Configuration $dstMetadata = null,
54
    ): AbstractMessage {
55
        $issuer = $msg->getIssuer();
56
57
        $ctxOpts = [
58
            'ssl' => [
59
                'capture_peer_cert' => true,
60
                'allow_self_signed' => true,
61
            ],
62
        ];
63
64
        $container = ContainerSingleton::getInstance();
65
66
        // Determine if we are going to do a MutualSSL connection between the IdP and SP  - Shoaib
67
        if ($srcMetadata->hasValue('saml.SOAPClient.certificate')) {
68
            $cert = $srcMetadata->getValue('saml.SOAPClient.certificate');
69
            if ($cert !== false) {
70
                $configUtils = new Config();
71
                $ctxOpts['ssl']['local_cert'] = $configUtils->getCertPath(
72
                    $srcMetadata->getString('saml.SOAPClient.certificate'),
73
                );
74
                if ($srcMetadata->hasValue('saml.SOAPClient.privatekey_pass')) {
75
                    $ctxOpts['ssl']['passphrase'] = $srcMetadata->getString('saml.SOAPClient.privatekey_pass');
76
                }
77
            }
78
        } else {
79
            /* Use the SP certificate and privatekey if it is configured. */
80
            $cryptoUtils = new Crypto();
81
            $privateKey = $cryptoUtils->loadPrivateKey($srcMetadata);
82
            $publicKey = $cryptoUtils->loadPublicKey($srcMetadata);
83
            if ($privateKey !== null && $publicKey !== null && isset($publicKey['PEM'])) {
84
                $keyCertData = $privateKey['PEM'] . $publicKey['PEM'];
85
                $file = $container->getTempDir() . '/' . sha1($keyCertData) . '.pem';
86
                if (!file_exists($file)) {
87
                    $container->writeFile($file, $keyCertData);
88
                }
89
                $ctxOpts['ssl']['local_cert'] = $file;
90
                if (isset($privateKey['password'])) {
91
                    $ctxOpts['ssl']['passphrase'] = $privateKey['password'];
92
                }
93
            }
94
        }
95
96
        // do peer certificate verification
97
        if ($dstMetadata !== null) {
98
            $peerPublicKeys = $dstMetadata->getPublicKeys('signing', true);
99
            $certData = '';
100
            foreach ($peerPublicKeys as $key) {
101
                if ($key['type'] !== 'X509Certificate') {
102
                    continue;
103
                }
104
                $certData .= "-----BEGIN CERTIFICATE-----\n" .
105
                    chunk_split($key['X509Certificate'], 64) .
106
                    "-----END CERTIFICATE-----\n";
107
            }
108
            $peerCertFile = $container->getTempDir() . '/' . sha1($certData) . '.pem';
109
            if (!file_exists($peerCertFile)) {
110
                $container->writeFile($peerCertFile, $certData);
111
            }
112
            // create ssl context
113
            $ctxOpts['ssl']['verify_peer'] = true;
114
            $ctxOpts['ssl']['verify_depth'] = 1;
115
            $ctxOpts['ssl']['cafile'] = $peerCertFile;
116
        }
117
118
        if ($srcMetadata->hasValue('saml.SOAPClient.stream_context.ssl.peer_name')) {
119
            $ctxOpts['ssl']['peer_name'] = $srcMetadata->getString('saml.SOAPClient.stream_context.ssl.peer_name');
120
        }
121
122
        $context = stream_context_create($ctxOpts);
123
124
        $options = [
125
            'uri' => $issuer?->getContent(),
126
            'location' => $msg->getDestination(),
127
            'stream_context' => $context,
128
        ];
129
130
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyhost')) {
131
            $options['proxy_host'] = $srcMetadata->getValue('saml.SOAPClient.proxyhost');
132
        }
133
134
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyport')) {
135
            $options['proxy_port'] = $srcMetadata->getValue('saml.SOAPClient.proxyport');
136
        }
137
138
        $destination = $msg->getDestination();
139
        if ($destination === null) {
140
            throw new Exception('Cannot send SOAP message, no destination set.');
141
        }
142
143
        // Add soap-envelopes
144
        $env = (new Envelope(new Body([new Chunk($msg->toXML())])))->toXML();
145
        $request = $env->ownerDocument?->saveXML();
146
147
        $container->debugMessage($request, 'out');
148
149
        $action = 'http://www.oasis-open.org/committees/security';
150
        /* Perform SOAP Request over HTTP */
151
        $x = new BuiltinSoapClient(null, $options);
152
        $soapresponsexml = $x->__doRequest($request, $destination, $action, SOAP_1_1, false);
153
        if (empty($soapresponsexml)) {
154
            throw new Exception('Empty SOAP response, check peer certificate.');
155
        }
156
157
        Utils::getContainer()->debugMessage($soapresponsexml, 'in');
158
159
        $dom = DOMDocumentFactory::fromString($soapresponsexml);
160
        $env = Envelope::fromXML($dom->documentElement);
161
        $container->debugMessage($env->toXML()->ownerDocument?->saveXML(), 'in');
162
163
        $soapfault = $this->getSOAPFault($dom);
164
        if ($soapfault !== null) {
165
            throw new Exception(
166
                sprintf(
167
                    "Actor: '%s';  Message: '%s';  Code: '%s'",
168
                    $soapfault->getFaultActor()?->getContent(),
169
                    $soapfault->getFaultString()->getContent(),
170
                    $soapfault->getFaultCode()->getContent(),
171
                ),
172
            );
173
        }
174
175
        // Extract the message from the response
176
        /** @var \SimpleSAML\XML\SerializableElementInterface[] $messages */
177
        $messages = $env->getBody()->getElements();
178
        $samlresponse = MessageFactory::fromXML($messages[0]->toXML());
179
180
        /* Add validator to message which uses the SSL context. */
181
        self::addSSLValidator($samlresponse, $context);
182
183
        $container->getLogger()->debug("Valid ArtifactResponse received from IdP");
184
185
        return $samlresponse;
186
    }
187
188
189
    /**
190
     * Add a signature validator based on a SSL context.
191
     *
192
     * @param \SimpleSAML\SAML2\XML\samlp\AbstractMessage $msg The message we should add a validator to.
193
     * @param resource $context The stream context.
194
     */
195
    private static function addSSLValidator(AbstractMessage $msg, $context): void
196
    {
197
        $options = stream_context_get_options($context);
198
        if (!isset($options['ssl']['peer_certificate'])) {
199
            return;
200
        }
201
202
        $container = ContainerSingleton::getInstance();
203
        $key = openssl_pkey_get_public($options['ssl']['peer_certificate']);
204
        if ($key === false) {
205
            $container->getLogger()->warning('Unable to get public key from peer certificate.');
206
            return;
207
        }
208
209
        $keyInfo = openssl_pkey_get_details($key);
210
        if ($keyInfo === false) {
211
            $container->getLogger()->warning('Unable to get key details from public key.');
212
            return;
213
        }
214
215
        if (!isset($keyInfo['key'])) {
216
            $container->getLogger()->warning('Missing key in public key details.');
217
            return;
218
        }
219
220
        $msg->addValidator([SOAPClient::class, 'validateSSL'], $keyInfo['key']);
0 ignored issues
show
Bug introduced by
The method addValidator() does not exist on SimpleSAML\SAML2\XML\samlp\AbstractMessage. ( Ignorable by Annotation )

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

220
        $msg->/** @scrutinizer ignore-call */ 
221
              addValidator([SOAPClient::class, 'validateSSL'], $keyInfo['key']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
221
    }
222
223
224
    /**
225
     * Validate a SOAP message against the certificate on the SSL connection.
226
     *
227
     * @param string $data The public key that was used on the connection.
228
     * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key The key we should validate the certificate against.
229
     * @throws \Exception
230
     */
231
    public static function validateSSL(string $data, XMLSecurityKey $key): void
232
    {
233
        $container = ContainerSingleton::getInstance();
234
235
        /** @psalm-suppress PossiblyNullArgument */
236
        $keyInfo = openssl_pkey_get_details($key->key);
237
        if ($keyInfo === false) {
238
            throw new Exception('Unable to get key details from XMLSecurityKey.');
239
        }
240
241
        if (!isset($keyInfo['key'])) {
242
            throw new Exception('Missing key in public key details.');
243
        }
244
245
        if ($keyInfo['key'] !== $data) {
246
            $container->getLogger()->debug('Key on SSL connection did not match key we validated against.');
247
            return;
248
        }
249
250
        $container->getLogger()->debug('Message validated based on SSL certificate.');
251
    }
252
253
254
    /**
255
     * Extracts the SOAP Fault from SOAP message
256
     *
257
     * @param \DOMDocument $soapMessage Soap response needs to be type DOMDocument
258
     * @return \SimpleSAML\SOAP\XML\env_200106\Fault|null
259
     */
260
    private function getSOAPFault(DOMDocument $soapMessage): ?Fault
261
    {
262
        /** @psalm-suppress PossiblyNullArgument */
263
        $soapFault = XPath::xpQuery(
264
            $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\XML\Utils\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

264
            /** @scrutinizer ignore-type */ $soapMessage->firstChild,
Loading history...
265
            '/env:Envelope/env:Body/env:Fault',
266
            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\SOAP\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

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