Passed
Push — master ( 37848b...ef0c2d )
by Tim
02:21
created

SOAPClient::send()   F

Complexity

Conditions 19
Paths 768

Size

Total Lines 134
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 19
eloc 79
c 2
b 0
f 0
nc 768
nop 3
dl 0
loc 134
rs 0.6722

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

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

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

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