SoapHeader4::isNotSecurityAuthenticateMessage()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * amadeus-ws-client
4
 *
5
 * Copyright 2015 Amadeus Benelux NV
6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 * http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 *
19
 * @package Amadeus
20
 * @license https://opensource.org/licenses/Apache-2.0 Apache 2.0
21
 */
22
23
namespace Amadeus\Client\Session\Handler;
24
25
use Amadeus\Client;
26
27
/**
28
 * SoapHeader4: Session Handler for web service applications using Amadeus WS Soap Header v4.
29
 *
30
 * @package Amadeus\Client\Session\Handler
31
 * @author Dieter Devlieghere <[email protected]>
32
 */
33
class SoapHeader4 extends Base
34
{
35
    /**
36
     * XPATH query to retrieve the SOAPAction from the WSDL for a given message.
37
     *
38
     * @var string
39
     */
40
    const XPATH_OPERATION_ACTION = 'string(//wsdl:operation[./@name="%s"]/soap:operation/@soapAction)';
41
    /**
42
     * XPATH query to retrieve the server endpoint from the WSDL.
43
     *
44
     * @var string
45
     */
46
    const XPATH_ENDPOINT = 'string(/wsdl:definitions/wsdl:service/wsdl:port/soap:address/@location)';
47
48
    /**
49
     * SoapHeader - Session
50
     * TransactionStatusCode for starting new sessions.
51
     */
52
    const TRANSACTION_STATUS_CODE_START = 'Start';
53
54
    /**
55
     * SoapHeader - Session
56
     * TransactionStatusCode for active stateful sessions.
57
     */
58
    const TRANSACTION_STATUS_CODE_INSERIES = 'InSeries';
59
60
    /**
61
     * SoapHeader - Session
62
     * TransactionStatusCode for ending sessions.
63
     */
64
    const TRANSACTION_STATUS_CODE_END = 'End';
65
66
    /**
67
     * Switch between stateful & stateless sessions. Default: stateful
68
     *
69
     * @var bool
70
     */
71
    protected $isStateful = true;
72
73
    protected $enableTransactionFlowLink = false;
74
75
    /**
76
     * TransactionFlowLink Consumer ID
77
     *
78
     * @var string|null
79
     */
80
    protected $consumerId;
81
82
    /**
83
     * @param bool $stateful
84
     */
85 240
    public function setStateful($stateful)
86
    {
87 240
        $this->isStateful = $stateful;
88 240
    }
89
90
    /**
91
     * Check whether we are running in stateful mode (true) or in stateless mode (false)
92
     *
93
     * @return bool
94
     */
95 105
    public function isStateful()
96
    {
97 105
        return $this->isStateful;
98
    }
99
100
    /**
101
     * Is the TransactionFlowLink header enabled?
102
     *
103
     * @return bool
104
     */
105 75
    public function isTransactionFlowLinkEnabled()
106
    {
107 75
        return $this->enableTransactionFlowLink;
108
    }
109
110
    /**
111
     * Enable or disable TransactionFlowLink header
112
     *
113
     * @param bool $enabled
114
     */
115 240
    public function setTransactionFlowLink($enabled)
116
    {
117 240
        $this->enableTransactionFlowLink = $enabled;
118 240
    }
119
120
    /**
121
     * Get the TransactionFlowLink Consumer ID
122
     *
123
     * @param bool $generate Whether to generate a consumer ID
124
     * @return string|null
125
     */
126 15
    public function getConsumerId($generate = false)
127
    {
128 15
        if (is_null($this->consumerId) && $generate) {
129 5
            $this->consumerId = $this->generateGuid();
130 2
        }
131
132 15
        return $this->consumerId;
133
    }
134
135
    /**
136
     * Set the TransactionFlowLink Consumer ID
137
     *
138
     * @param string $id
139
     * @return void
140
     */
141 240
    public function setConsumerId($id)
142
    {
143 240
        $this->consumerId = $id;
144 240
    }
145
146
    /**
147
     * Handles authentication & sessions
148
     *
149
     * If authenticated, increment sequence number for next message and set session info to soapheader
150
     * If not, set auth info to soapheader
151
     *
152
     * @uses $this->isAuthenticated
153
     * @uses $this->sessionData
154
     * @param string $messageName
155
     * @param array $messageOptions
156
     */
157 40
    protected function prepareForNextMessage($messageName, $messageOptions)
158
    {
159 40
        if ($this->isAuthenticated === true && is_int($this->sessionData['sequenceNumber'])) {
160 5
            $this->sessionData['sequenceNumber']++;
161 2
        }
162
163 40
        $headers = $this->createSoapHeaders($this->sessionData, $this->params, $messageName, $messageOptions);
164
165 40
        $this->getSoapClient($messageName)->__setSoapHeaders(null);
166 40
        $this->getSoapClient($messageName)->__setSoapHeaders($headers);
167 40
    }
168
169
170
    /**
171
     * Handles post message actions
172
     *
173
     * - look for session info and set status variables
174
     * - checks for message errors?
175
     * - ends terminated sessions
176
     *
177
     * @param string $messageName
178
     * @param string $lastResponse
179
     * @param array $messageOptions
180
     * @param mixed $result
181
     * @return void
182
     */
183 40
    protected function handlePostMessage($messageName, $lastResponse, $messageOptions, $result)
184
    {
185
        //CHECK FOR SESSION DATA:
186 40
        if ($this->isStateful() === true) {
187
            //We need to extract session info
188 15
            $this->sessionData = $this->getSessionDataFromHeader($lastResponse);
189 15
            $this->isAuthenticated = (!empty($this->sessionData['sessionId']) &&
190 15
                !empty($this->sessionData['sequenceNumber']) &&
191 13
                !empty($this->sessionData['securityToken']));
192 6
        } else {
193 25
            $this->isAuthenticated = false;
194
        }
195 40
    }
196
197
    /**
198
     * @param string $responseMsg the full response XML received.
199
     * @return array
200
     */
201 25
    protected function getSessionDataFromHeader($responseMsg)
202
    {
203
        $newSessionData = [
204 25
            'sessionId' => null,
205 10
            'sequenceNumber' => null,
206
            'securityToken' => null
207 10
        ];
208
209 25
        $responseDomDoc = new \DOMDocument('1.0', 'UTF-8');
210 25
        $responseDomDoc->loadXML($responseMsg);
211 25
        $responseDomXpath = new \DOMXPath($responseDomDoc);
212 25
        $responseDomXpath->registerNamespace('awsse', 'http://xml.amadeus.com/2010/06/Session_v3');
213
214 25
        $queryTransactionStatusCode = "string(//awsse:Session/@TransactionStatusCode)";
215
216 25
        $transactionStatusCode = $responseDomXpath->evaluate($queryTransactionStatusCode);
217
218 25
        if (mb_strtolower($transactionStatusCode) !== "end") {
219 15
            $querySessionId = "string(//awsse:Session/awsse:SessionId/text())";
220 15
            $querySequenceNumber = "string(//awsse:Session/awsse:SequenceNumber/text())";
221 15
            $querySecurityToken = "string(//awsse:Session/awsse:SecurityToken/text())";
222
223 15
            $newSessionData['sessionId'] = $responseDomXpath->evaluate($querySessionId);
224 15
            $newSessionData['sequenceNumber'] = $responseDomXpath->evaluate($querySequenceNumber);
225 15
            if (!empty($newSessionData['sequenceNumber'])) {
226 15
                $newSessionData['sequenceNumber'] = (int) $newSessionData['sequenceNumber'];
227 6
            }
228 15
            $newSessionData['securityToken'] = $responseDomXpath->evaluate($querySecurityToken);
229 6
        }
230
231 25
        unset($responseDomDoc, $responseDomXpath);
232
233 25
        return $newSessionData;
234
    }
235
236
    /**
237
     * Create the Soap Headers to be used on the subsequent request.
238
     *
239
     * This depends on the current Session Data (if there is an active session) and
240
     * the Session Handler parameters (to create a new or stateless session)
241
     *
242
     * You can also terminate the session with $doEndSession = true
243
     *
244
     * @param array $sessionData
245
     * @param Client\Params\SessionHandlerParams $params
246
     * @param string $messageName
247
     * @param array $messageOptions
248
     * @return \SoapHeader[]|null
249
     */
250 75
    protected function createSoapHeaders($sessionData, $params, $messageName, $messageOptions)
251
    {
252 75
        $headersToSet = [];
253
254 75
        $wsdlId = $this->getWsdlIdFor($messageName);
255 75
        $wsdl = WsdlAnalyser::$wsdlIds[$wsdlId];
256
257
        //CHECK STATEFUL
258 75
        $stateful = $this->isStateful();
259
260
        //Message ID header
261 75
        array_push(
262 75
            $headersToSet,
263 75
            new \SoapHeader(
264 75
                'http://www.w3.org/2005/08/addressing',
265 75
                'MessageID',
266 75
                $this->generateGuid()
267 30
            )
268 30
        );
269
270
        //Action header
271 75
        array_push(
272 75
            $headersToSet,
273 75
            new \SoapHeader(
274 75
                'http://www.w3.org/2005/08/addressing',
275 75
                'Action',
276 75
                $this->getActionFromWsdl($wsdl, $messageName)
277 30
            )
278 30
        );
279
280
        //To header
281 75
        array_push(
282 75
            $headersToSet,
283 75
            new \SoapHeader(
284 75
                'http://www.w3.org/2005/08/addressing',
285 75
                'To',
286 75
                $this->getEndpointFromWsdl($wsdl, $messageName)
287 30
            )
288 30
        );
289
290
        //TransactionFlowLink header
291 75
        $tfl = $this->isTransactionFlowLinkEnabled();
292 75
        if ($tfl) {
293 10
            $consumerId = $this->getConsumerId(true);
294
295 10
            array_push(
296 10
                $headersToSet,
297 10
                new \SoapHeader(
298 10
                    'http://wsdl.amadeus.com/2010/06/ws/Link_v1',
299 10
                    'TransactionFlowLink',
300 10
                    new Client\Struct\HeaderV4\TransactionFlowLink($consumerId)
301 4
                )
302 4
            );
303 4
        }
304
305
        //Send authentication info headers if not authenticated and not Security_Authenticate message call
306 75
        if ($this->isAuthenticated === false && $this->isNotSecurityAuthenticateMessage($messageName)) {
307
            //Generate nonce, msg creation string & password digest:
308 55
            $password = base64_decode($params->authParams->passwordData);
309 55
            $creation = new \DateTime('now', new \DateTimeZone('UTC'));
310 55
            $t = (float)microtime(true);
311 55
            $micro = sprintf("%03d", ($t - floor($t)) * 1000);
312 55
            $creationString = $this->createDateTimeStringForAuth($creation, $micro);
313 55
            $messageNonce = $this->generateUniqueNonce($params->authParams->nonceBase, $creationString);
314 55
            $encodedNonce = base64_encode($messageNonce);
315 55
            $digest = $this->generatePasswordDigest($password, $creationString, $messageNonce);
316
317 55
            $securityHeaderXml = $this->generateSecurityHeaderRawXml(
318 55
                $params->authParams->userId,
319 44
                $encodedNonce,
320 44
                $digest,
321 22
                $creationString
322 22
            );
323
324
            //Authentication header
325 55
            array_push(
326 55
                $headersToSet,
327 55
                new \SoapHeader(
328 55
                    'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wsswssecurity-secext-1.0.xsd',
329 55
                    'Security',
330 55
                    new \SoapVar($securityHeaderXml, XSD_ANYXML)
331 22
                )
332 22
            );
333
334 55
            if ($stateful === true) {
335
                //Not authenticated but stateful: start session!
336 10
                array_push(
337 10
                    $headersToSet,
338 10
                    new \SoapHeader(
339 10
                        'http://xml.amadeus.com/2010/06/Session_v3',
340 10
                        'Session',
341 10
                        new Client\Struct\HeaderV4\Session(
342 10
                            null,
343 6
                            self::TRANSACTION_STATUS_CODE_START
344 4
                        )
345 4
                    )
346 4
                );
347 4
            }
348
349
            //AMA_SecurityHostedUser header
350 55
            array_push(
351 55
                $headersToSet,
352 55
                new \SoapHeader(
353 55
                    'http://xml.amadeus.com/2010/06/Security_v1',
354 55
                    'AMA_SecurityHostedUser',
355 55
                    new Client\Struct\HeaderV4\SecurityHostedUser(
356 55
                        $params->authParams->officeId,
357 55
                        $params->authParams->originatorTypeCode,
358 55
                        1,
359 55
                        $params->authParams->dutyCode
360 22
                    )
361 22
                )
362 22
            );
363 42
        } elseif ($stateful === true) {
364 15
            array_push(
365 15
                $headersToSet,
366 15
                new \SoapHeader(
367 15
                    'http://xml.amadeus.com/2010/06/Session_v3',
368 15
                    'Session',
369 15
                    new Client\Struct\HeaderV4\Session(
370 15
                        $sessionData,
371 15
                        $this->getStatefulStatusCode($messageName, $messageOptions)
372 6
                    )
373 6
                )
374 6
            );
375 6
        }
376
377 75
        return $headersToSet;
378
    }
379
380
    /**
381
     * Get the Web Services server Endpoint from the WSDL.
382
     *
383
     * @param string $wsdlFilePath
384
     * @param string $messageName
385
     * @return string|null
386
     */
387 75
    protected function getEndpointFromWsdl($wsdlFilePath, $messageName)
388
    {
389 75
        $wsdlId = $this->getWsdlIdFor($messageName);
390
391 75
        return WsdlAnalyser::exaluateXpathQueryOnWsdl(
392 75
            $wsdlId,
393 60
            $wsdlFilePath,
394 45
            self::XPATH_ENDPOINT
395 30
        );
396
    }
397
398
    /**
399
     * Get the SOAPAction for a given message from the WSDL contents.
400
     *
401
     * @param string $wsdlFilePath
402
     * @param string $messageName
403
     * @return string|null
404
     */
405 75
    protected function getActionFromWsdl($wsdlFilePath, $messageName)
406
    {
407 75
        $wsdlId = $this->getWsdlIdFor($messageName);
408
409 75
        return WsdlAnalyser::exaluateXpathQueryOnWsdl(
410 75
            $wsdlId,
411 60
            $wsdlFilePath,
412 75
            sprintf(self::XPATH_OPERATION_ACTION, $messageName)
413 30
        );
414
    }
415
416
    /**
417
     * Generate a GUID
418
     *
419
     * @return string
420
     */
421 75
    protected function generateGuid()
422
    {
423 75
        mt_srand((int)microtime(true));
424 75
        $charId = strtoupper(md5(uniqid(mt_rand(), true)));
425 75
        $hyphen = chr(45); // "-"
426
427 75
        $uuid = substr($charId, 0, 8) . $hyphen
428 75
            . substr($charId, 8, 4) . $hyphen
429 75
            . substr($charId, 12, 4) . $hyphen
430 75
            . substr($charId, 16, 4) . $hyphen
431 75
            . substr($charId, 20, 12);
432
433 75
        return $uuid;
434
    }
435
436
    /**
437
     * @param string $originator
438
     * @param string $nonce
439
     * @param string $pwDigest
440
     * @param string $creationTimeString
441
     * @return string
442
     */
443 60
    protected function generateSecurityHeaderRawXml($originator, $nonce, $pwDigest, $creationTimeString)
444
    {
445
        return '<oas:Security xmlns:oas="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
446
	<oas:UsernameToken xmlns:oas1="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" oas1:Id="UsernameToken-1">
447 60
		<oas:Username>' . $originator . '</oas:Username>
448 60
		<oas:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">' . $nonce . '</oas:Nonce>
449 60
		<oas:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' . $pwDigest . '</oas:Password>
450 60
		<oas1:Created>' . $creationTimeString . '</oas1:Created>
451
	</oas:UsernameToken>
452 24
</oas:Security>';
453
    }
454
455
456
    /**
457
     * @param string $nonceBase
458
     * @param string $creationString
459
     * @return string
460
     */
461 55
    protected function generateUniqueNonce($nonceBase, $creationString)
462
    {
463 55
        return substr(
464 55
            sha1(
465 55
                $nonceBase . $creationString,
466 33
                true
467 22
            ),
468 55
            0,
469 33
            16
470 22
        );
471
    }
472
473
    /**
474
     * Generates a Password Digest following this algorithm:
475
     * HashedPassword = Base64(SHA-1( nonce + created + SHA-1 ( password )))
476
     * as defined in
477
     * https://webservices.amadeus.com/extranet/kdbViewDocument.do?externalId=wikidoc_web_services_embedded_security_implementation_guide_header_entries_ws-security_usernametoken&docStatus=Published&mpId=fla__1__technical
478
     *
479
     * EXAMPLE: with:
480
     *  Nonce in Base 64 = 'PZgFvh5439plJpKpIyf5ucmXhNU='
481
     *  Timestamp = '2013-01-11T09:41:03Z'
482
     *  Clear Password = 'WBSPassword'
483
     * The digest algorithm returns the Encrypted Password in Base 64:
484
     *  HshPwd = 'ic3AOJElVpvkz9ZBKd105Siry28='
485
     *
486
     * @param string $password CLEARTEXT password (NOT the base64 encoded password used in Security_Authenticate)
487
     * @param string $creationString message creation datetime
488
     *                               UTC Format: yyyy-mm-ddTHH:MM:SSZ or yyyy-mm-ddTHH:MM:SS.sssZ
489
     * @param string $messageNonce Random unique string
490
     * @return string The generated Password Digest
491
     */
492 85
    protected function generatePasswordDigest($password, $creationString, $messageNonce)
493
    {
494 85
        return base64_encode(sha1($messageNonce . $creationString . sha1($password, true), true));
495
    }
496
497
    /**
498
     * @param \DateTime $creationDateTime
499
     * @param string $micro
500
     * @return string
501
     */
502 55
    protected function createDateTimeStringForAuth($creationDateTime, $micro)
503
    {
504 55
        $creationDateTime->setTimezone(new \DateTimeZone('UTC'));
505
506 55
        return $creationDateTime->format("Y-m-d\TH:i:s:") . $micro . 'Z';
507
    }
508
509
    /**
510
     * Make SoapClient options for Soap Header 4 handler
511
     *
512
     * @return array
513
     */
514 10
    protected function makeSoapClientOptions()
515
    {
516 10
        $options = $this->soapClientOptions;
517 10
        $options['classmap'] = array_merge(Classmap::$soapheader4map, Classmap::$map);
518
519 10
        if (!empty($this->params->soapClientOptions)) {
520 5
            $options = array_merge($options, $this->params->soapClientOptions);
521 2
        }
522
523 10
        return $options;
524
    }
525
526
    /**
527
     * Check is called message is not Security_Authenticate.
528
     *
529
     * @param $messageName
530
     * @return bool
531
     */
532 60
    protected function isNotSecurityAuthenticateMessage($messageName)
533
    {
534 60
        return 'Security_Authenticate' !== $messageName;
535
    }
536
537
    /**
538
     * Return transaction code for stateful requests.
539
     *
540
     * @param string $messageName name of request message (e.g. Security_Authenticate)
541
     * @param array $messageOptions
542
     * @return string
543
     */
544 15
    private function getStatefulStatusCode($messageName, array $messageOptions)
545
    {
546
        // on security-auth this is always 'Start'
547 15
        if ('Security_Authenticate' === $messageName) {
548 5
            return self::TRANSACTION_STATUS_CODE_START;
549
        }
550
551
        // if endSession is set this will be (the) 'End'
552 10
        if (isset($messageOptions['endSession']) && $messageOptions['endSession'] === true) {
553 5
            return self::TRANSACTION_STATUS_CODE_END;
554
        }
555
556
        // on everything else we assume in-series
557 5
        return self::TRANSACTION_STATUS_CODE_INSERIES;
558
    }
559
}
560