Completed
Push — master ( 4e360d...80bc96 )
by Daan van
07:27
created

SAML2_SOAPClient::send()   F

Complexity

Conditions 20
Paths 408

Size

Total Lines 122
Code Lines 72

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 122
rs 3.5945
cc 20
eloc 72
nc 408
nop 3

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
namespace SAML2;
4
5
use RobRichards\XMLSecLibs\XMLSecurityKey;
6
use SAML2\Exception\RuntimeException;
7
use SimpleSAML_Configuration;
8
use SimpleSAML_Utilities;
9
10
/**
11
 * Implementation of the SAML 2.0 SOAP binding.
12
 *
13
 * @author Shoaib Ali
14
 * @package SimpleSAMLphp
15
 */
16
class SOAPClient
17
{
18
    const START_SOAP_ENVELOPE = '<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"><soap-env:Header/><soap-env:Body>';
19
    const END_SOAP_ENVELOPE = '</soap-env:Body></soap-env:Envelope>';
20
21
    /**
22
     * This function sends the SOAP message to the service location and returns SOAP response
23
     *
24
     * @param  \SAML2\Message            $msg         The request that should be sent.
25
     * @param  \SimpleSAML_Configuration $srcMetadata The metadata of the issuer of the message.
26
     * @param  \SimpleSAML_Configuration $dstMetadata The metadata of the destination of the message.
27
     * @return \SAML2\Message            The response we received.
28
     * @throws \Exception
29
     */
30
    public function send(Message $msg, SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata = null)
31
    {
32
        $issuer = $msg->getIssuer();
33
34
        $ctxOpts = array(
35
            'ssl' => array(
36
                'capture_peer_cert' => true,
37
            ),
38
        );
39
40
        // Determine if we are going to do a MutualSSL connection between the IdP and SP  - Shoaib
41
        if ($srcMetadata->hasValue('saml.SOAPClient.certificate')) {
42
            $cert = $srcMetadata->getValue('saml.SOAPClient.certificate');
43
            if ($cert !== false) {
44
                $ctxOpts['ssl']['local_cert'] = SimpleSAML_Utilities::resolveCert(
45
                    $srcMetadata->getString('saml.SOAPClient.certificate')
46
                );
47
                if ($srcMetadata->hasValue('saml.SOAPClient.privatekey_pass')) {
48
                    $ctxOpts['ssl']['passphrase'] = $srcMetadata->getString('saml.SOAPClient.privatekey_pass');
49
                }
50
            }
51
        } else {
52
            /* Use the SP certificate and privatekey if it is configured. */
53
            $privateKey = SimpleSAML_Utilities::loadPrivateKey($srcMetadata);
54
            $publicKey = SimpleSAML_Utilities::loadPublicKey($srcMetadata);
55
            if ($privateKey !== null && $publicKey !== null && isset($publicKey['PEM'])) {
56
                $keyCertData = $privateKey['PEM'] . $publicKey['PEM'];
57
                $file = SimpleSAML_Utilities::getTempDir() . '/' . sha1($keyCertData) . '.pem';
58
                if (!file_exists($file)) {
59
                    SimpleSAML_Utilities::writeFile($file, $keyCertData);
60
                }
61
                $ctxOpts['ssl']['local_cert'] = $file;
62
                if (isset($privateKey['password'])) {
63
                    $ctxOpts['ssl']['passphrase'] = $privateKey['password'];
64
                }
65
            }
66
        }
67
68
        // do peer certificate verification
69
        if ($dstMetadata !== null) {
70
            $peerPublicKeys = $dstMetadata->getPublicKeys('signing', true);
71
            $certData = '';
72
            foreach ($peerPublicKeys as $key) {
73
                if ($key['type'] !== 'X509Certificate') {
74
                    continue;
75
                }
76
                $certData .= "-----BEGIN CERTIFICATE-----\n" .
77
                    chunk_split($key['X509Certificate'], 64) .
78
                    "-----END CERTIFICATE-----\n";
79
            }
80
            $peerCertFile = SimpleSAML_Utilities::getTempDir() . '/' . sha1($certData) . '.pem';
81
            if (!file_exists($peerCertFile)) {
82
                SimpleSAML_Utilities::writeFile($peerCertFile, $certData);
83
            }
84
            // create ssl context
85
            $ctxOpts['ssl']['verify_peer'] = true;
86
            $ctxOpts['ssl']['verify_depth'] = 1;
87
            $ctxOpts['ssl']['cafile'] = $peerCertFile;
88
        }
89
90
        $context = stream_context_create($ctxOpts);
91
        if ($context === null) {
92
            throw new \Exception('Unable to create SSL stream context');
93
        }
94
95
        $options = array(
96
            'uri' => $issuer,
97
            'location' => $msg->getDestination(),
98
            'stream_context' => $context,
99
        );
100
101
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyhost')) {
102
            $options['proxy_host'] = $srcMetadata->getValue('saml.SOAPClient.proxyhost');
103
        }
104
105
        if ($srcMetadata->hasValue('saml.SOAPClient.proxyport')) {
106
            $options['proxy_port'] = $srcMetadata->getValue('saml.SOAPClient.proxyport');
107
        }
108
109
        $x = new SoapClient(null, $options);
0 ignored issues
show
Unused Code introduced by
The call to SOAPClient::__construct() has too many arguments starting with null.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
110
111
        // Add soap-envelopes
112
        $request = $msg->toSignedXML();
113
        $request = self::START_SOAP_ENVELOPE . $request->ownerDocument->saveXML($request) . self::END_SOAP_ENVELOPE;
114
115
        Utils::getContainer()->debugMessage($request, 'out');
116
117
        $action = 'http://www.oasis-open.org/committees/security';
118
        $version = '1.1';
119
        $destination = $msg->getDestination();
120
121
        /* Perform SOAP Request over HTTP */
122
        $soapresponsexml = $x->__doRequest($request, $destination, $action, $version);
0 ignored issues
show
Bug introduced by
The method __doRequest() does not seem to exist on object<SAML2\SOAPClient>.

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...
123
        if ($soapresponsexml === null || $soapresponsexml === "") {
124
            throw new \Exception('Empty SOAP response, check peer certificate.');
125
        }
126
127
        Utils::getContainer()->debugMessage($soapresponsexml, 'in');
128
129
        // Convert to SAML2\Message (\DOMElement)
130
        try {
131
            $dom = DOMDocumentFactory::fromString($soapresponsexml);
132
        } catch (RuntimeException $e) {
133
            throw new \Exception('Not a SOAP response.', 0, $e);
134
        }
135
136
        $soapfault = $this->getSOAPFault($dom);
137
        if (isset($soapfault)) {
138
            throw new \Exception($soapfault);
139
        }
140
        //Extract the message from the response
141
        $samlresponse = Utils::xpQuery($dom->firstChild, '/soap-env:Envelope/soap-env:Body/*[1]');
142
        $samlresponse = Message::fromXML($samlresponse[0]);
143
144
        /* Add validator to message which uses the SSL context. */
145
        self::addSSLValidator($samlresponse, $context);
146
147
        Utils::getContainer()->getLogger()->debug("Valid ArtifactResponse received from IdP");
148
149
        return $samlresponse;
150
    }
151
152
    /**
153
     * Add a signature validator based on a SSL context.
154
     *
155
     * @param \SAML2\Message $msg     The message we should add a validator to.
156
     * @param resource      $context The stream context.
157
     */
158
    private static function addSSLValidator(Message $msg, $context)
159
    {
160
        $options = stream_context_get_options($context);
161
        if (!isset($options['ssl']['peer_certificate'])) {
162
            return;
163
        }
164
165
        //$out = '';
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
166
        //openssl_x509_export($options['ssl']['peer_certificate'], $out);
0 ignored issues
show
Unused Code Comprehensibility introduced by
86% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
167
168
        $key = openssl_pkey_get_public($options['ssl']['peer_certificate']);
169
        if ($key === false) {
170
            Utils::getContainer()->getLogger()->warning('Unable to get public key from peer certificate.');
171
172
            return;
173
        }
174
175
        $keyInfo = openssl_pkey_get_details($key);
176
        if ($keyInfo === false) {
177
            Utils::getContainer()->getLogger()->warning('Unable to get key details from public key.');
178
179
            return;
180
        }
181
182
        if (!isset($keyInfo['key'])) {
183
            Utils::getContainer()->getLogger()->warning('Missing key in public key details.');
184
185
            return;
186
        }
187
188
        $msg->addValidator(array('\SAML2\SOAPClient', 'validateSSL'), $keyInfo['key']);
189
    }
190
191
    /**
192
     * Validate a SOAP message against the certificate on the SSL connection.
193
     *
194
     * @param string         $data The public key that was used on the connection.
195
     * @param XMLSecurityKey $key  The key we should validate the certificate against.
196
     * @throws \Exception
197
     */
198
    public static function validateSSL($data, XMLSecurityKey $key)
199
    {
200
        assert('is_string($data)');
201
202
        $keyInfo = openssl_pkey_get_details($key->key);
203
        if ($keyInfo === false) {
204
            throw new \Exception('Unable to get key details from XMLSecurityKey.');
205
        }
206
207
        if (!isset($keyInfo['key'])) {
208
            throw new \Exception('Missing key in public key details.');
209
        }
210
211
        if ($keyInfo['key'] !== $data) {
212
            Utils::getContainer()->getLogger()->debug('Key on SSL connection did not match key we validated against.');
213
214
            return;
215
        }
216
217
        Utils::getContainer()->getLogger()->debug('Message validated based on SSL certificate.');
218
    }
219
220
    /*
221
     * Extracts the SOAP Fault from SOAP message
222
     * @param $soapmessage Soap response needs to be type DOMDocument
223
     * @return $soapfaultstring string|null
0 ignored issues
show
Documentation introduced by
The doc-type $soapfaultstring could not be parsed: Unknown type name "$soapfaultstring" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
224
     */
225
    private function getSOAPFault($soapMessage)
226
    {
227
        $soapFault = Utils::xpQuery($soapMessage->firstChild, '/soap-env:Envelope/soap-env:Body/soap-env:Fault');
228
229
        if (empty($soapFault)) {
230
            /* No fault. */
231
232
            return null;
233
        }
234
        $soapFaultElement = $soapFault[0];
235
        // There is a fault element but we haven't found out what the fault string is
236
        $soapFaultString = "Unknown fault string found";
237
        // find out the fault string
238
        $faultStringElement =   Utils::xpQuery($soapFaultElement, './soap-env:faultstring') ;
239
        if (!empty($faultStringElement)) {
240
            return $faultStringElement[0]->textContent;
241
        }
242
243
        return $soapFaultString;
244
    }
245
}
246