From 1122df8e127a12574ce315be9d7199a7eae61d1f Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 11 Dec 2011 21:20:35 +0100 Subject: [PATCH] WS-Adressing and WS-Security Filters --- src/BeSimple/SoapClient/Curl.php | 8 + src/BeSimple/SoapClient/FilterHelper.php | 172 ++++++ src/BeSimple/SoapClient/Helper.php | 194 ------- src/BeSimple/SoapClient/SoapClient.php | 167 ++++-- src/BeSimple/SoapClient/SoapRequest.php | 47 ++ src/BeSimple/SoapClient/SoapResponse.php | 46 ++ .../SoapClient/WsAddressingFilter.php | 320 ++++++++++ src/BeSimple/SoapClient/WsSecurityFilter.php | 549 ++++++++++++++++++ src/BeSimple/SoapClient/WsdlDownloader.php | 19 +- tests/bootstrap.php | 7 + vendors.php | 1 + 11 files changed, 1270 insertions(+), 260 deletions(-) create mode 100644 src/BeSimple/SoapClient/FilterHelper.php delete mode 100644 src/BeSimple/SoapClient/Helper.php create mode 100644 src/BeSimple/SoapClient/SoapRequest.php create mode 100644 src/BeSimple/SoapClient/SoapResponse.php create mode 100644 src/BeSimple/SoapClient/WsAddressingFilter.php create mode 100644 src/BeSimple/SoapClient/WsSecurityFilter.php diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php index 38bc066..a8e508e 100644 --- a/src/BeSimple/SoapClient/Curl.php +++ b/src/BeSimple/SoapClient/Curl.php @@ -144,6 +144,7 @@ class Curl private function execManualRedirect($redirects = 0) { if ($redirects > $this->followLocationMaxRedirects) { + // TODO Redirection limit reached, aborting return false; } @@ -171,9 +172,11 @@ class Curl } $newUrl = $url['scheme'] . '://' . $url['host'] . $url['path'] . ($url['query'] ? '?' . $url['query'] : ''); curl_setopt($this->ch, CURLOPT_URL, $newUrl); + return $this->execManualRedirect($redirects++); } } + return $response; } @@ -225,8 +228,10 @@ class Curl $errorCodeMapping = $this->getErrorCodeMapping(); $errorNumber = curl_errno($this->ch); if (isset($errorCodeMapping[$errorNumber])) { + return $errorCodeMapping[$errorNumber]; } + return curl_error($this->ch); } @@ -258,6 +263,7 @@ class Curl public function getResponseBody() { $headerSize = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); + return substr($this->response, $headerSize); } @@ -279,6 +285,7 @@ class Curl public function getResponseHeaders() { $headerSize = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); + return substr($this->response, 0, $headerSize); } @@ -300,6 +307,7 @@ class Curl public function getResponseStatusMessage() { preg_match('/HTTP\/(1\.[0-1]+) ([0-9]{3}) (.*)/', $this->response, $matches); + return trim(array_pop($matches)); } } \ No newline at end of file diff --git a/src/BeSimple/SoapClient/FilterHelper.php b/src/BeSimple/SoapClient/FilterHelper.php new file mode 100644 index 0000000..f25e8bf --- /dev/null +++ b/src/BeSimple/SoapClient/FilterHelper.php @@ -0,0 +1,172 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapClient; + +/** + * Soap request/response filter helper for manipulating SOAP messages. + * + * @author Andreas Schamberger + */ +class FilterHelper +{ + /** + * DOMDocument on which the helper functions operate. + * + * @var \DOMDocument + */ + protected $domDocument = null; + + /** + * Namespaces added. + * + * @var array(string=>string) + */ + protected $namespaces = array(); + + /** + * Constructor. + * + * @param \DOMDocument $domDocument + */ + public function __construct(\DOMDocument $domDocument) + { + $this->domDocument = $domDocument; + } + + /** + * Add new soap header. + * + * @param \DOMElement $node + * @param boolean $mustUnderstand + * @param string $actor + * @param string $soapVersion + * @return void + */ + public function addHeaderElement(\DOMElement $node, $mustUnderstand = null, $actor = null, $soapVersion = SOAP_1_1) + { + $root = $this->domDocument->documentElement; + $namespace = $root->namespaceURI; + $prefix = $root->prefix; + if (null !== $mustUnderstand) { + $node->appendChild(new \DOMAttr($prefix . ':mustUnderstand', (int)$mustUnderstand)); + } + if (null !== $actor) { + $attributeName = ($soapVersion == SOAP_1_1) ? 'actor' : 'role'; + $node->appendChild(new \DOMAttr($prefix . ':' . $attributeName, $actor)); + } + $nodeListHeader = $root->getElementsByTagNameNS($namespace, 'Header'); + // add header if not there + if ($nodeListHeader->length == 0) { + // new header element + $header = $this->domDocument->createElementNS($namespace, $prefix . ':Header'); + // try to add it before body + $nodeListBody = $root->getElementsByTagNameNS($namespace, 'Body'); + if ($nodeListBody->length == 0) { + $root->appendChild($header); + } else { + $body = $nodeListBody->item(0); + $header = $body->parentNode->insertBefore($header, $body); + } + $header->appendChild($node); + } else { + $nodeListHeader->item(0)->appendChild($node); + } + } + + /** + * Add new soap body element. + * + * @param \DOMElement $node + * @return void + */ + public function addBodyElement(\DOMElement $node) + { + $root = $this->domDocument->documentElement; + $namespace = $root->namespaceURI; + $prefix = $root->prefix; + $nodeList = $this->domDocument->getElementsByTagNameNS($namespace, 'Body'); + // add body if not there + if ($nodeList->length == 0) { + // new body element + $body = $this->domDocument->createElementNS($namespace, $prefix . ':Body'); + $root->appendChild($body); + $body->appendChild($node); + } else { + $nodeList->item(0)->appendChild($node); + } + } + + /** + * Add new namespace to root tag. + * + * @param string $prefix + * @param string $namespaceURI + * @return void + */ + public function addNamespace($prefix, $namespaceURI) + { + if (!isset($this->namespaces[$namespaceURI])) { + $root = $this->domDocument->documentElement; + $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:' . $prefix, $namespaceURI); + $this->namespaces[$namespaceURI] = $prefix; + } + } + + /** + * Create new element for given namespace. + * + * @param string $namespaceURI + * @param string $name + * @param string $value + * @return \DOMElement + */ + public function createElement($namespaceURI, $name, $value = null) + { + $prefix = $this->namespaces[$namespaceURI]; + + return $this->domDocument->createElementNS($namespaceURI, $prefix . ':' . $name, $value); + } + + /** + * Add new attribute to element with given namespace. + * + * @param \DOMElement $element + * @param string $namespaceURI + * @param string $name + * @param string $value + * @return void + */ + public function setAttribute(\DOMElement $element, $namespaceURI = null, $name, $value) + { + if (null !== $namespaceURI) { + $prefix = $this->namespaces[$namespaceURI]; + $element->setAttributeNS($namespaceURI, $prefix . ':' . $name, $value); + } else { + $element->setAttribute($name, $value); + } + } + + /** + * Register namespace. + * + * @param string $prefix + * @param string $namespaceURI + * @return void + */ + public function registerNamespace($prefix, $namespaceURI) + { + if (!isset($this->namespaces[$namespaceURI])) { + $this->namespaces[$namespaceURI] = $prefix; + } + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/Helper.php b/src/BeSimple/SoapClient/Helper.php deleted file mode 100644 index e22afb9..0000000 --- a/src/BeSimple/SoapClient/Helper.php +++ /dev/null @@ -1,194 +0,0 @@ - - * (c) Francis Besset - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace BeSimple\SoapClient; - -/** - * Soap helper class with static functions that are used in the client and - * server implementations. It also provides namespace and configuration - * constants. - * - * @author Andreas Schamberger - */ -class Helper -{ - /** - * Attachment type: xsd:base64Binary (native in ext/soap). - */ - const ATTACHMENTS_TYPE_BASE64 = 1; - - /** - * Attachment type: MTOM (SOAP Message Transmission Optimization Mechanism). - */ - const ATTACHMENTS_TYPE_MTOM = 2; - - /** - * Attachment type: SWA (SOAP Messages with Attachments). - */ - const ATTACHMENTS_TYPE_SWA = 4; - - /** - * Web Services Security: SOAP Message Security 1.0 (WS-Security 2004) - */ - const NAME_WSS_SMS = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0'; - - /** - * Web Services Security: SOAP Message Security 1.1 (WS-Security 2004) - */ - const NAME_WSS_SMS_1_1 = 'http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1'; - - /** - * Web Services Security UsernameToken Profile 1.0 - */ - const NAME_WSS_UTP = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0'; - - /** - * Web Services Security X.509 Certificate Token Profile - */ - const NAME_WSS_X509 = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0'; - - /** - * Soap 1.1 namespace. - */ - const NS_SOAP_1_1 = 'http://schemas.xmlsoap.org/soap/envelope/'; - - /** - * Soap 1.1 namespace. - */ - const NS_SOAP_1_2 = 'http://www.w3.org/2003/05/soap-envelope/'; - - /** - * Web Services Addressing 1.0 namespace. - */ - const NS_WSA = 'http://www.w3.org/2005/08/addressing'; - - /** - * Web Services Security Extension namespace. - */ - const NS_WSS = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'; - - /** - * Web Services Security Utility namespace. - */ - const NS_WSU = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'; - - /** - * Describing Media Content of Binary Data in XML namespace. - */ - const NS_XMLMIME = 'http://www.w3.org/2004/11/xmlmime'; - - /** - * XML Schema namespace. - */ - const NS_XML_SCHEMA = 'http://www.w3.org/2001/XMLSchema'; - - /** - * XML Schema instance namespace. - */ - const NS_XML_SCHEMA_INSTANCE = 'http://www.w3.org/2001/XMLSchema-instance'; - - /** - * XML-binary Optimized Packaging namespace. - */ - const NS_XOP = 'http://www.w3.org/2004/08/xop/include'; - - /** - * Web Services Addressing 1.0 prefix. - */ - const PFX_WSA = 'wsa'; - - /** - * Web Services Security Extension namespace. - */ - const PFX_WSS = 'wsse'; - - /** - * Web Services Security Utility namespace prefix. - */ - const PFX_WSU = 'wsu'; - - /** - * Describing Media Content of Binary Data in XML namespace prefix. - */ - const PFX_XMLMIME = 'xmlmime'; - - /** - * XML Schema namespace prefix. - */ - const PFX_XML_SCHEMA = 'xsd'; - - /** - * XML Schema instance namespace prefix. - */ - const PFX_XML_SCHEMA_INSTANCE = 'xsi'; - - /** - * XML-binary Optimized Packaging namespace prefix. - */ - const PFX_XOP = 'xop'; - - /** - * Generate a pseudo-random version 4 UUID. - * - * @see http://de.php.net/manual/en/function.uniqid.php#94959 - * @return string - */ - public static function generateUUID() - { - return sprintf( - '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - // 32 bits for "time_low" - mt_rand(0, 0xffff), mt_rand(0, 0xffff), - // 16 bits for "time_mid" - mt_rand(0, 0xffff), - // 16 bits for "time_hi_and_version", - // four most significant bits holds version number 4 - mt_rand(0, 0x0fff) | 0x4000, - // 16 bits, 8 bits for "clk_seq_hi_res", - // 8 bits for "clk_seq_low", - // two most significant bits holds zero and one for variant DCE1.1 - mt_rand(0, 0x3fff) | 0x8000, - // 48 bits for "node" - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) - ); - } - - /** - * Get SOAP namespace for the given $version. - * - * @param int $version SOAP_1_1|SOAP_1_2 - * @return string - */ - public static function getSoapNamespace($version) - { - if ($version === SOAP_1_2) { - return self::NS_SOAP_1_2; - } else { - return self::NS_SOAP_1_1; - } - } - - /** - * Get SOAP version from namespace URI. - * - * @param string $namespace NS_SOAP_1_1|NS_SOAP_1_2 - * @return int SOAP_1_1|SOAP_1_2 - */ - public static function getSoapVersionFromNamespace($namespace) - { - if ($namespace === self::NS_SOAP_1_2) { - return SOAP_1_2; - } else { - return SOAP_1_1; - } - } -} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 79d7e31..a6386ed 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -12,6 +12,8 @@ namespace BeSimple\SoapClient; +use BeSimple\SoapCommon\SoapKernel; + /** * Extended SoapClient that uses a a cURL wrapper for all underlying HTTP * requests in order to use proper authentication for all requests. This also @@ -71,6 +73,13 @@ class SoapClient extends \SoapClient */ private $lastResponse = ''; + /** + * Last response. + * + * @var \BeSimple\SoapCommon\SoapKernel + */ + protected $soapKernel = null; + /** * Constructor. * @@ -89,6 +98,8 @@ class SoapClient extends \SoapClient } $this->curl = new Curl($options); $wsdlFile = $this->loadWsdl($wsdl, $options); + // TODO $wsdlHandler = new WsdlHandler($wsdlFile, $this->soapVersion); + $this->soapKernel = new SoapKernel(); // we want the exceptions option to be set $options['exceptions'] = true; // disable obsolete trace option for native SoapClient as we need to do our own tracing anyways @@ -102,91 +113,88 @@ class SoapClient extends \SoapClient /** * Perform HTTP request with cURL. * - * @param string $request - * @param string $location - * @param string $action - * @return string + * @param SoapRequest $soapRequest + * @return SoapResponse */ - private function __doHttpRequest($request, $location, $action) + private function __doHttpRequest(SoapRequest $soapRequest) { - // $request is if unmodified from SoapClient not a php string type! - $request = (string)$request; - if ($this->soapVersion == SOAP_1_2) { - $headers = array( - 'Content-Type: application/soap+xml; charset=utf-8', - ); - } else { - $headers = array( - 'Content-Type: text/xml; charset=utf-8', - ); - } - // add SOAPAction header - $headers[] = 'SOAPAction: "' . $action . '"'; - // execute request - $responseSuccessfull = $this->curl->exec($location, $request, $headers); + // HTTP headers + $headers = array( + 'Content-Type:' . $soapRequest->getContentType(), + 'SOAPAction: "' . $soapRequest->getAction() . '"', + ); + // execute HTTP request with cURL + $responseSuccessfull = $this->curl->exec($soapRequest->getLocation(), + $soapRequest->getContent(), + $headers); // tracing enabled: store last request header and body if ($this->tracingEnabled === true) { - $this->lastRequestHeaders = $curl->getRequestHeaders(); - $this->lastRequest = $request; + $this->lastRequestHeaders = $this->curl->getRequestHeaders(); + $this->lastRequest = $soapRequest->getContent(); } // in case of an error while making the http request throw a soapFault if ($responseSuccessfull === false) { // get error message from curl $faultstring = $this->curl->getErrorMessage(); - throw new \SoapFault('HTTP', $faultstring); + throw new \SoapFault( 'HTTP', $faultstring ); } // tracing enabled: store last response header and body if ($this->tracingEnabled === true) { $this->lastResponseHeaders = $this->curl->getResponseHeaders(); $this->lastResponse = $this->curl->getResponseBody(); } - $response = $this->curl->getResponseBody(); - // check if we do have a proper soap status code (if not soapfault) -// // TODO -// $responseStatusCode = $this->curl->getResponseStatusCode(); -// if ($responseStatusCode >= 400) { -// $isError = 0; -// $response = trim($response); -// if (strlen($response) == 0) { -// $isError = 1; -// } else { -// $contentType = $this->curl->getResponseContentType(); -// if ($contentType != 'application/soap+xml' -// && $contentType != 'application/soap+xml') { -// if (strncmp($response , "curl->getResponseStatusMessage()); -// } -// } elseif ($responseStatusCode != 200 && $responseStatusCode != 202) { -// $dom = new \DOMDocument('1.0'); -// $dom->loadXML($response); -// if ($dom->getElementsByTagNameNS($dom->documentElement->namespaceURI, 'Fault')->length == 0) { -// throw new \SoapFault('HTTP', 'HTTP response status must be 200 or 202'); -// } -// } - return $response; - } + // wrap response data in SoapResponse object + $soapResponse = SoapResponse::create($this->curl->getResponseBody(), + $soapRequest->getLocation(), + $soapRequest->getAction(), + $soapRequest->getVersion(), + $this->curl->getResponseContentType()); + + return $soapResponse; + } /** * Custom request method to be able to modify the SOAP messages. + * $oneWay parameter is not used at the moment. * * @param string $request * @param string $location * @param string $action * @param int $version - * @param int $one_way 0|1 + * @param int $oneWay 0|1 * @return string */ - public function __doRequest($request, $location, $action, $version, $one_way = 0) + public function __doRequest($request, $location, $action, $version, $oneWay = 0) { - // http request - $response = $this->__doHttpRequest($request, $location, $action); + // wrap request data in SoapRequest object + $soapRequest = SoapRequest::create($request, $location, $action, $version); + + // do actual SOAP request + $soapResponse = $this->__doRequest2($soapRequest); + // return SOAP response to ext/soap - return $response; + return $soapResponse->getContent(); + } + + /** + * Runs the currently registered request filters on the request, performs + * the HTTP request and runs the response filters. + * + * @param SoapRequest $soapRequest + * @return SoapResponse + */ + protected function __doRequest2(SoapRequest $soapRequest) + { + // run SoapKernel on SoapRequest + $soapRequest = $this->soapKernel->filterRequest($soapRequest); + + // perform HTTP request with cURL + $soapResponse = $this->__doHttpRequest($soapRequest); + + // run SoapKernel on SoapResponse + $soapResponse = $this->soapKernel->filterResponse($soapResponse); + + return $soapResponse; } /** @@ -229,6 +237,48 @@ class SoapClient extends \SoapClient return $this->lastResponse; } + /** + * Get SoapKernel instance. + * + * @return \BeSimple\SoapCommon\SoapKernel + */ + public function getSoapKernel() + { + return $this->soapKernel; + } + + // TODO finish + protected function isValidSoapResponse() + { + //check if we do have a proper soap status code (if not soapfault) + $responseStatusCode = $this->curl->getResponseStatusCode(); + $response = $this->curl->getResponseBody(); + if ($responseStatusCode >= 400) { + $isError = 0; + $response = trim($response); + if (strlen($response) == 0) { + $isError = 1; + } else { + $contentType = $this->curl->getResponseContentType(); + if ($contentType != 'application/soap+xml' + && $contentType != 'application/soap+xml') { + if (strncmp($response , "curl->getResponseStatusMessage()); + } + } elseif ($responseStatusCode != 200 && $responseStatusCode != 202) { + $dom = new \DOMDocument('1.0'); + $dom->loadXML($response); + if ($dom->getElementsByTagNameNS($dom->documentElement->namespaceURI, 'Fault')->length == 0) { + throw new \SoapFault('HTTP', 'HTTP response status must be 200 or 202'); + } + } + } + /** * Downloads WSDL files with cURL. Uses all SoapClient options for * authentication. Uses the WSDL_CACHE_* constants and the 'soap.wsdl_*' @@ -257,6 +307,7 @@ class SoapClient extends \SoapClient } catch (\RuntimeException $e) { throw new \SoapFault('WSDL', "SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl . "' : failed to load external entity \"" . $wsdl . "\""); } + return $cacheFileName; } } \ No newline at end of file diff --git a/src/BeSimple/SoapClient/SoapRequest.php b/src/BeSimple/SoapClient/SoapRequest.php new file mode 100644 index 0000000..afe684f --- /dev/null +++ b/src/BeSimple/SoapClient/SoapRequest.php @@ -0,0 +1,47 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapClient; + +use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; +use BeSimple\SoapCommon\SoapMessage; + +/** + * SoapRequest class for SoapClient. Provides factory function for request object. + * + * @author Andreas Schamberger + */ +class SoapRequest extends CommonSoapRequest +{ + /** + * Factory function for SoapRequest. + * + * @param string $content + * @param string $location + * @param string $action + * @param string $version + * @return BeSimple\SoapClient\SoapRequest + */ + public static function create($content, $location, $action, $version) + { + $request = new SoapRequest(); + // $content is if unmodified from SoapClient not a php string type! + $request->setContent((string)$content); + $request->setLocation($location); + $request->setAction($action); + $request->setVersion($version); + $contentType = SoapMessage::getContentTypeForVersion($version); + $request->setContentType($contentType); + + return $request; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/SoapResponse.php b/src/BeSimple/SoapClient/SoapResponse.php new file mode 100644 index 0000000..a9317da --- /dev/null +++ b/src/BeSimple/SoapClient/SoapResponse.php @@ -0,0 +1,46 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapClient; + +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; +use BeSimple\SoapCommon\SoapMessage; + +/** + * SoapResponse class for SoapClient. Provides factory function for response object. + * + * @author Andreas Schamberger + */ +class SoapResponse extends CommonSoapResponse +{ + /** + * Factory function for SoapResponse. + * + * @param string $content + * @param string $location + * @param string $action + * @param string $version + * @param string $contentType + * @return BeSimple\SoapClient\SoapResponse + */ + public static function create($content, $location, $action, $version, $contentType) + { + $response = new SoapResponse(); + $response->setContent($content); + $response->setLocation($location); + $response->setAction($action); + $response->setVersion($version); + $response->setContentType($contentType); + + return $response; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/WsAddressingFilter.php b/src/BeSimple/SoapClient/WsAddressingFilter.php new file mode 100644 index 0000000..67d94c0 --- /dev/null +++ b/src/BeSimple/SoapClient/WsAddressingFilter.php @@ -0,0 +1,320 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapClient; + +use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponseFilter; + +/** + * This plugin implements a subset of the following standards: + * * Web Services Addressing 1.0 - Core + * http://www.w3.org/TR/2006/REC-ws-addr-core + * * Web Services Addressing 1.0 - SOAP Binding + * http://www.w3.org/TR/ws-addr-soap + * + * Per default this plugin uses the SoapClient's $action and $location values + * for wsa:Action and wsa:To. Therefore the only REQUIRED property 'wsa:Action' + * is always set automatically. + * + * Limitation: wsa:From, wsa:FaultTo and wsa:ReplyTo only support the + * wsa:Address element of the endpoint reference at the moment. + * + * @author Andreas Schamberger + */ +class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter +{ + /** + * (2.1) Endpoint reference (EPR) anonymous default address. + * + * Some endpoints cannot be located with a meaningful IRI; this URI is used + * to allow such endpoints to send and receive messages. The precise meaning + * of this URI is defined by the binding of Addressing to a specific + * protocol and/or the context in which the EPR is used. + * + * @see http://www.w3.org/TR/2006/REC-ws-addr-core-20060509/#predefaddr + */ + const ENDPOINT_REFERENCE_ANONYMOUS = 'http://www.w3.org/2005/08/addressing/anonymous'; + + /** + * (2.1) Endpoint reference (EPR) address for discarting messages. + * + * Messages sent to EPRs whose [address] is this value MUST be discarded + * (i.e. not sent). This URI is typically used in EPRs that designate a + * reply or fault endpoint (see section 3.1 Abstract Property Definitions) + * to indicate that no reply or fault message should be sent. + * + * @see http://www.w3.org/TR/2006/REC-ws-addr-core-20060509/#predefaddr + */ + const ENDPOINT_REFERENCE_NONE = 'http://www.w3.org/2005/08/addressing/none'; + + /** + * (3.1) Predefined value for reply. + * + * Indicates that this is a reply to the message identified by the [message id] IRI. + * + * see http://www.w3.org/TR/2006/REC-ws-addr-core-20060509/#predefrels + */ + const RELATIONSHIP_TYPE_REPLY = 'http://www.w3.org/2005/08/addressing/reply'; + + /** + * FaultTo. + * + * @var string + */ + protected $faultTo; + + /** + * From. + * + * @var string + */ + protected $from; + + /** + * MessageId. + * + * @var string + */ + protected $messageId; + + /** + * List of reference parameters associated with this soap message. + * + * @var unknown_type + */ + protected $referenceParametersSet = array(); + + /** + * List of reference parameters recieved with this soap message. + * + * @var unknown_type + */ + protected $referenceParametersRecieved = array(); + + /** + * RelatesTo. + * + * @var string + */ + protected $relatesTo; + + /** + * RelatesTo@RelationshipType. + * + * @var string + */ + protected $relatesToRelationshipType; + + /** + * ReplyTo. + * + * @var string + */ + protected $replyTo; + + /** + * Add additional reference parameters + * + * @param string $ns + * @param string $pfx + * @param string $parameter + * @param string $value + * @return void + */ + public function addReferenceParameter($ns, $pfx, $parameter, $value) + { + $this->referenceParametersSet[] = array( + 'ns' => $ns, + 'pfx' => $pfx, + 'parameter' => $parameter, + 'value' => $value, + ); + } + + /** + * Get additional reference parameters. + * + * @param string $ns + * @param string $parameter + * @return string|null + */ + public function getReferenceParameter($ns, $parameter) + { + if (isset($this->referenceParametersRecieved[$ns][$parameter])) { + + return $this->referenceParametersRecieved[$ns][$parameter]; + } + + return null; + } + + /** + * Set FaultTo address of type xs:anyURI. + * + * @param string $action + * @return void + */ + public function setFaultTo($faultTo) + { + $this->faultTo = $faultTo; + } + + /** + * Set From address of type xs:anyURI. + * + * @param string $action + * @return void + */ + public function setFrom($from) + { + $this->from = $from; + } + + /** + * Set MessageId of type xs:anyURI. + * Default: UUID v4 e.g. 'uuid:550e8400-e29b-11d4-a716-446655440000' + * + * @param string $messageId + * @return void + */ + public function setMessageId($messageId = null) + { + if (null === $messageId) { + $messageId = 'uuid:' . Helper::generateUUID(); + } + $this->messageId = $messageId; + } + + /** + * Set RelatesTo of type xs:anyURI with the optional relationType + * (of type xs:anyURI). + * + * @param string $relatesTo + * @param string $relationType + * @return void + */ + public function setRelatesTo($relatesTo, $relationType = null) + { + $this->relatesTo = $relatesTo; + if (null !== $relationType && $relationType != self::RELATIONSHIP_TYPE_REPLY) { + $this->relatesToRelationshipType = $relationType; + } + } + + /** + * Set ReplyTo address of type xs:anyURI + * Default: self::ENDPOINT_REFERENCE_ANONYMOUS + * + * @param string $replyTo + * @return void + */ + public function setReplyTo($replyTo = null) + { + if (null === $replyTo) { + $replyTo = self::ENDPOINT_REFERENCE_ANONYMOUS; + } + $this->replyTo = $replyTo; + } + + /** + * Modify the given request XML. + * + * @param SoapRequest $dom + * @return void + */ + public function filterRequest(SoapRequest $request) + { + // get \DOMDocument from SOAP request + $dom = $request->getContentDocument(); + + // create FilterHelper + $filterHelper = new FilterHelper($dom); + + // add the neccessary namespaces + $filterHelper->addNamespace(Helper::PFX_WSA, Helper::NS_WSA); + + $action = $filterHelper->createElement(Helper::NS_WSA, 'Action', $request->getAction()); + $filterHelper->addHeaderElement($action); + + $to = $filterHelper->createElement(Helper::NS_WSA, 'To', $request->getLocation()); + $filterHelper->addHeaderElement($to); + + if (null !== $this->faultTo) { + $faultTo = $filterHelper->createElement(Helper::NS_WSA, 'FaultTo'); + $filterHelper->addHeaderElement($faultTo); + + $address = $filterHelper->createElement(Helper::NS_WSA, 'Address', $this->faultTo); + $faultTo->appendChild($address); + } + + if (null !== $this->from) { + $from = $filterHelper->createElement(Helper::NS_WSA, 'From'); + $filterHelper->addHeaderElement($from); + + $address = $filterHelper->createElement(Helper::NS_WSA, 'Address', $this->from); + $from->appendChild($address); + } + + if (null !== $this->messageId) { + $messageId = $filterHelper->createElement(Helper::NS_WSA, 'MessageID', $this->messageId); + $filterHelper->addHeaderElement($messageId); + } + + if (null !== $this->relatesTo) { + $relatesTo = $filterHelper->createElement(Helper::NS_WSA, 'RelatesTo', $this->relatesTo); + if (null !== $this->relatesToRelationshipType) { + $filterHelper->setAttribute($relatesTo, Helper::NS_WSA, 'RelationshipType', $this->relatesToRelationshipType); + } + $filterHelper->addHeaderElement($relatesTo); + } + + if (null !== $this->replyTo) { + $replyTo = $filterHelper->createElement(Helper::NS_WSA, 'ReplyTo'); + $filterHelper->addHeaderElement($replyTo); + + $address = $filterHelper->createElement(Helper::NS_WSA, 'Address', $this->replyTo); + $replyTo->appendChild($address); + } + + foreach ($this->referenceParametersSet as $rp) { + $filterHelper->addNamespace($rp['pfx'], $rp['ns']); + $parameter = $filterHelper->createElement($rp['ns'], $rp['parameter'], $rp['value']); + $filterHelper->setAttribute($parameter, Helper::NS_WSA, 'IsReferenceParameter', 'true'); + $filterHelper->addHeaderElement($parameter); + } + } + + /** + * Modify the given response XML. + * + * @param SoapResponse $response + * @return void + */ + public function filterResponse(SoapResponse $response) + { + // get \DOMDocument from SOAP response + $dom = $response->getContentDocument(); + + $this->referenceParametersRecieved = array(); + $referenceParameters = $dom->getElementsByTagNameNS(Helper::NS_WSA, 'ReferenceParameters')->item(0); + if (null !== $referenceParameters) { + foreach ($referenceParameters->childNodes as $childNode) { + if (!isset($this->referenceParametersRecieved[$childNode->namespaceURI])) { + $this->referenceParametersRecieved[$childNode->namespaceURI] = array(); + } + $this->referenceParametersRecieved[$childNode->namespaceURI][$childNode->localName] = $childNode->nodeValue; + } + } + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/WsSecurityFilter.php b/src/BeSimple/SoapClient/WsSecurityFilter.php new file mode 100644 index 0000000..8eb368c --- /dev/null +++ b/src/BeSimple/SoapClient/WsSecurityFilter.php @@ -0,0 +1,549 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapClient; + +use ass\XmlSecurity\DSig as XmlSecurityDSig; +use ass\XmlSecurity\Enc as XmlSecurityEnc; +use ass\XmlSecurity\Key as XmlSecurityKey; + +use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponseFilter; +use BeSimple\SoapCommon\WsSecurityKey; + +/** + * This plugin implements a subset of the following standards: + * * Web Services Security: SOAP Message Security 1.0 (WS-Security 2004) + * http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0.pdf + * * Web Services Security UsernameToken Profile 1.0 + * http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf + * * Web Services Security X.509 Certificate Token Profile + * http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0.pdf + * + * @author Andreas Schamberger + */ +class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter +{ + /* + * The date format to be used with {@link \DateTime} + */ + const DATETIME_FORMAT = 'Y-m-d\TH:i:s.000\Z'; + + /** + * (UT 3.1) Password type: plain text. + */ + const PASSWORD_TYPE_TEXT = 0; + + /** + * (UT 3.1) Password type: digest. + */ + const PASSWORD_TYPE_DIGEST = 1; + + /** + * (X509 3.2.1) Reference to a Subject Key Identifier + */ + const TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER = 0; + + /** + * (X509 3.2.1) Reference to a Security Token + */ + const TOKEN_REFERENCE_SECURITY_TOKEN = 1; + + /** + * (SMS_1.1 7.3) Key Identifiers + */ + const TOKEN_REFERENCE_THUMBPRINT_SHA1 = 2; + + /** + * Actor. + * + * @var string + */ + protected $actor; + + /** + * (SMS 10) Add security timestamp. + * + * @var boolean + */ + protected $addTimestamp; + + /** + * Encrypt the signature? + * + * @var boolean + */ + protected $encryptSignature; + + /** + * (SMS 10) Security timestamp expires time in seconds. + * + * @var int + */ + protected $expires; + + /** + * (UT 3.1) Password. + * + * @var string + */ + protected $password; + + /** + * (UT 3.1) Password type: text or digest. + * + * @var int + */ + protected $passwordType; + + /** + * Sign all headers. + * + * @var boolean + */ + protected $signAllHeaders; + + /** + * (X509 3.2) Token reference type for encryption. + * + * @var int + */ + protected $tokenReferenceEncryption = null; + + /** + * (X509 3.2) Token reference type for signature. + * + * @var int + */ + protected $tokenReferenceSignature = null; + + /** + * Service WsSecurityKey. + * + * @var \BeSimple\SoapCommon\WsSecurityKey + */ + protected $serviceSecurityKey; + + /** + * (UT 3.1) Username. + * + * @var string + */ + protected $username; + + /** + * User WsSecurityKey. + * + * @var \BeSimple\SoapCommon\WsSecurityKey + */ + protected $userSecurityKey; + + /** + * Constructor. + * + * @param boolean $addTimestamp (SMS 10) Add security timestamp. + * @param int $expires (SMS 10) Security timestamp expires time in seconds. + * @param string $actor + */ + public function __construct($addTimestamp = true, $expires = 300, $actor = null) + { + $this->addTimestamp = $addTimestamp; + $this->expires = $expires; + $this->actor = $actor; + } + + /** + * Add user data. + * + * @param string $username + * @param string $password + * @param int $passwordType self::PASSWORD_TYPE_DIGEST | self::PASSWORD_TYPE_TEXT + * @return void + */ + public function addUserData($username, $password = null, $passwordType = self::PASSWORD_TYPE_DIGEST) + { + $this->username = $username; + $this->password = $password; + $this->passwordType = $passwordType; + } + + /** + * Get service security key. + * + * @param \BeSimple\SoapCommon\WsSecurityKey $serviceSecurityKey + * @return void + */ + public function setServiceSecurityKeyObject(WsSecurityKey $serviceSecurityKey) + { + $this->serviceSecurityKey = $serviceSecurityKey; + } + + /** + * Get user security key. + * + * @param \BeSimple\SoapCommon\WsSecurityKey $userSecurityKey + * @return void + */ + public function setUserSecurityKeyObject(WsSecurityKey $userSecurityKey) + { + $this->userSecurityKey = $userSecurityKey; + } + + /** + * Set security options. + * + * @param int $tokenReference self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER | self::TOKEN_REFERENCE_SECURITY_TOKEN | self::TOKEN_REFERENCE_THUMBPRINT_SHA1 + * @param boolean $encryptSignature + * @return void + */ + public function setSecurityOptionsEncryption($tokenReference, $encryptSignature = false) + { + $this->tokenReferenceEncryption = $tokenReference; + $this->encryptSignature = $encryptSignature; + } + + /** + * Set security options. + * + * @param int $tokenReference self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER | self::TOKEN_REFERENCE_SECURITY_TOKEN | self::TOKEN_REFERENCE_THUMBPRINT_SHA1 + * @param boolean $signAllHeaders + * @return void + */ + public function setSecurityOptionsSignature($tokenReference, $signAllHeaders = false) + { + $this->tokenReferenceSignature = $tokenReference; + $this->signAllHeaders = $signAllHeaders; + } + + /** + * Modify the given request XML. + * + * @param SoapRequest $dom + * @return void + */ + public function filterRequest(SoapRequest $request) + { + // get \DOMDocument from SOAP request + $dom = $request->getContentDocument(); + + // create FilterHelper + $filterHelper = new FilterHelper($dom); + + // add the neccessary namespaces + $filterHelper->addNamespace(Helper::PFX_WSS, Helper::NS_WSS); + $filterHelper->addNamespace(Helper::PFX_WSU, Helper::NS_WSU); + $filterHelper->registerNamespace(XmlSecurityDSig::PFX_XMLDSIG, XmlSecurityDSig::NS_XMLDSIG); + + // init timestamp + $dt = new \DateTime('now', new \DateTimeZone('UTC')); + $createdTimestamp = $dt->format(self::DATETIME_FORMAT); + + // create security header + $security = $filterHelper->createElement(Helper::NS_WSS, 'Security'); + $filterHelper->addHeaderElement($security, true, $this->actor, $request->getSoapVersion()); + + if (true === $this->addTimestamp || null !== $this->expires) { + $timestamp = $filterHelper->createElement(Helper::NS_WSU, 'Timestamp'); + $created = $filterHelper->createElement(Helper::NS_WSU, 'Created', $createdTimestamp); + $timestamp->appendChild($created); + if (null !== $this->expires) { + $dt->modify('+' . $this->expires . ' seconds'); + $expiresTimestamp = $dt->format(self::DATETIME_FORMAT); + $expires = $filterHelper->createElement(Helper::NS_WSU, 'Expires', $expiresTimestamp); + $timestamp->appendChild($expires); + } + $security->appendChild($timestamp); + } + + if (null !== $this->username) { + $usernameToken = $filterHelper->createElement(Helper::NS_WSS, 'UsernameToken'); + $security->appendChild($usernameToken); + + $username = $filterHelper->createElement(Helper::NS_WSS, 'Username', $this->username); + $usernameToken->appendChild($username); + + if (null !== $this->password + && (null === $this->userSecurityKey + || (null !== $this->userSecurityKey && !$this->userSecurityKey->hasPrivateKey()))) { + + if (self::PASSWORD_TYPE_DIGEST === $this->passwordType) { + $nonce = mt_rand(); + $password = base64_encode(sha1($nonce . $createdTimestamp . $this->password , true)); + $passwordType = Helper::NAME_WSS_UTP . '#PasswordDigest'; + } else { + $password = $this->password; + $passwordType = Helper::NAME_WSS_UTP . '#PasswordText'; + } + $password = $filterHelper->createElement(Helper::NS_WSS, 'Password', $password); + $filterHelper->setAttribute($password, null, 'Type', $passwordType); + $usernameToken->appendChild($password); + if (self::PASSWORD_TYPE_DIGEST === $this->passwordType) { + $nonce = $filterHelper->createElement(Helper::NS_WSS, 'Nonce', base64_encode($nonce)); + $usernameToken->appendChild($nonce); + + $created = $filterHelper->createElement(Helper::NS_WSU, 'Created', $createdTimestamp); + $usernameToken->appendChild($created); + } + } + } + + if (null !== $this->userSecurityKey && $this->userSecurityKey->hasKeys()) { + $guid = 'CertId-' . Helper::generateUUID(); + // add token references + $keyInfo = null; + if (null !== $this->tokenReferenceSignature) { + $keyInfo = $this->createKeyInfo($filterHelper, $this->tokenReferenceSignature, $guid, $this->userSecurityKey->getPublicKey()); + } + $nodes = $this->createNodeListForSigning($dom, $security); + $signature = XmlSecurityDSig::createSignature($this->userSecurityKey->getPrivateKey(), XmlSecurityDSig::EXC_C14N, $security, null, $keyInfo); + $options = array( + 'id_ns_prefix' => Helper::PFX_WSU, + 'id_prefix_ns' => Helper::NS_WSU, + ); + foreach ($nodes as $node) { + XmlSecurityDSig::addNodeToSignature($signature, $node, XmlSecurityDSig::SHA1, XmlSecurityDSig::EXC_C14N, $options); + } + XmlSecurityDSig::signDocument($signature, $this->userSecurityKey->getPrivateKey(), XmlSecurityDSig::EXC_C14N); + + $publicCertificate = $this->userSecurityKey->getPublicKey()->getX509Certificate(true); + $binarySecurityToken = $filterHelper->createElement(Helper::NS_WSS, 'BinarySecurityToken', $publicCertificate); + $filterHelper->setAttribute($binarySecurityToken, null, 'EncodingType', Helper::NAME_WSS_SMS . '#Base64Binary'); + $filterHelper->setAttribute($binarySecurityToken, null, 'ValueType', Helper::NAME_WSS_X509 . '#X509v3'); + $filterHelper->setAttribute($binarySecurityToken, Helper::NS_WSU, 'Id', $guid); + $security->insertBefore($binarySecurityToken, $signature); + + // encrypt soap document + if (null !== $this->serviceSecurityKey && $this->serviceSecurityKey->hasKeys()) { + $guid = 'EncKey-' . Helper::generateUUID(); + // add token references + $keyInfo = null; + if (null !== $this->tokenReferenceEncryption) { + $keyInfo = $this->createKeyInfo($filterHelper, $this->tokenReferenceEncryption, $guid, $this->serviceSecurityKey->getPublicKey()); + } + $encryptedKey = XmlSecurityEnc::createEncryptedKey($guid, $this->serviceSecurityKey->getPrivateKey(), $this->serviceSecurityKey->getPublicKey(), $security, $signature, $keyInfo); + $referenceList = XmlSecurityEnc::createReferenceList($encryptedKey); + // token reference to encrypted key + $keyInfo = $this->createKeyInfo($filterHelper, self::TOKEN_REFERENCE_SECURITY_TOKEN, $guid); + $nodes = $this->createNodeListForEncryption($dom, $security); + foreach ($nodes as $node) { + $type = XmlSecurityEnc::ELEMENT; + if ($node->localName == 'Body') { + $type = XmlSecurityEnc::CONTENT; + } + XmlSecurityEnc::encryptNode($node, $type, $this->serviceSecurityKey->getPrivateKey(), $referenceList, $keyInfo); + } + } + } + } + + /** + * Modify the given request XML. + * + * @param SoapResponse $response + * @return void + */ + public function filterResponse(SoapResponse $response) + { + // get \DOMDocument from SOAP response + $dom = $response->getContentDocument(); + + // locate security header + $security = $dom->getElementsByTagNameNS(Helper::NS_WSS, 'Security')->item(0); + if (null !== $security) { + // add SecurityTokenReference resolver for KeyInfo + if (null !== $this->serviceSecurityKey) { + $keyResolver = array($this, 'keyInfoSecurityTokenReferenceResolver'); + XmlSecurityDSig::addKeyInfoResolver(Helper::NS_WSS, 'SecurityTokenReference', $keyResolver); + } + // do we have a reference list in header + $referenceList = XmlSecurityEnc::locateReferenceList($security); + // get a list of encrypted nodes + $encryptedNodes = XmlSecurityEnc::locateEncryptedData($dom, $referenceList); + // decrypt them + if (null !== $encryptedNodes) { + foreach ($encryptedNodes as $encryptedNode) { + XmlSecurityEnc::decryptNode($encryptedNode); + } + } + // locate signature node + $signature = XmlSecurityDSig::locateSignature($security); + if (null !== $signature) { + // verify references + $options = array( + 'id_ns_prefix' => Helper::PFX_WSU, + 'id_prefix_ns' => Helper::NS_WSU, + ); + if (XmlSecurityDSig::verifyReferences($signature, $options) !== true) { + throw new \SoapFault('wsse:FailedCheck', 'The signature or decryption was invalid'); + } + // verify signature + if (XmlSecurityDSig::verifyDocumentSignature($signature) !== true) { + throw new \SoapFault('wsse:FailedCheck', 'The signature or decryption was invalid'); + } + } + } + } + + /** + * Adds the configured KeyInfo to the parentNode. + * + * @param FilterHelper $filterHelper + * @param int $tokenReference + * @param \DOMNode $parentNode + * @param string $guid + * @param \ass\XmlSecurity\Key $xmlSecurityKey + * @param \DOMNode $insertBefore + * @return \DOMElement + */ + protected function createKeyInfo(FilterHelper $filterHelper, $tokenReference, $guid, XmlSecurityKey $xmlSecurityKey = null) + { + $keyInfo = $filterHelper->createElement(XmlSecurityDSig::NS_XMLDSIG, 'KeyInfo'); + $securityTokenReference = $filterHelper->createElement(Helper::NS_WSS, 'SecurityTokenReference'); + $keyInfo->appendChild($securityTokenReference); + // security token + if (self::TOKEN_REFERENCE_SECURITY_TOKEN === $tokenReference) { + $reference = $filterHelper->createElement(Helper::NS_WSS, 'Reference'); + $filterHelper->setAttribute($reference, null, 'URI', '#' . $guid); + if (null !== $xmlSecurityKey) { + $filterHelper->setAttribute($reference, null, 'ValueType', Helper::NAME_WSS_X509 . '#X509v3'); + } + $securityTokenReference->appendChild($reference); + // subject key identifier + } elseif (self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER === $tokenReference && null !== $xmlSecurityKey) { + $keyIdentifier = $filterHelper->createElement(Helper::NS_WSS, 'KeyIdentifier'); + $filterHelper->setAttribute($keyIdentifier, null, 'EncodingType', Helper::NAME_WSS_SMS . '#Base64Binary'); + $filterHelper->setAttribute($keyIdentifier, null, 'ValueType', Helper::NAME_WSS_X509 . '#509SubjectKeyIdentifier'); + $securityTokenReference->appendChild($keyIdentifier); + $certificate = $xmlSecurityKey->getX509SubjectKeyIdentifier(); + $dataNode = new \DOMText($certificate); + $keyIdentifier->appendChild($dataNode); + // thumbprint sha1 + } elseif (self::TOKEN_REFERENCE_THUMBPRINT_SHA1 === $tokenReference && null !== $xmlSecurityKey) { + $keyIdentifier = $filterHelper->createElement(Helper::NS_WSS, 'KeyIdentifier'); + $filterHelper->setAttribute($keyIdentifier, null, 'EncodingType', Helper::NAME_WSS_SMS . '#Base64Binary'); + $filterHelper->setAttribute($keyIdentifier, null, 'ValueType', Helper::NAME_WSS_SMS_1_1 . '#ThumbprintSHA1'); + $securityTokenReference->appendChild($keyIdentifier); + $thumbprintSha1 = base64_encode(sha1(base64_decode($xmlSecurityKey->getX509Certificate(true)), true)); + $dataNode = new \DOMText($thumbprintSha1); + $keyIdentifier->appendChild($dataNode); + } + + return $keyInfo; + } + + /** + * Create a list of \DOMNodes that should be encrypted. + * + * @param \DOMDocument $dom + * @param \DOMElement $security + * @return \DOMNodeList + */ + protected function createNodeListForEncryption(\DOMDocument $dom, \DOMElement $security) + { + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('SOAP-ENV', $dom->documentElement->namespaceURI); + $xpath->registerNamespace('ds', XmlSecurityDSig::NS_XMLDSIG); + if ($this->encryptSignature === true) { + $query = '//ds:Signature | //SOAP-ENV:Body'; + } else { + $query = '//SOAP-ENV:Body'; + } + + return $xpath->query($query); + } + + /** + * Create a list of \DOMNodes that should be signed. + * + * @param \DOMDocument $dom + * @param \DOMElement $security + * @return array(\DOMNode) + */ + protected function createNodeListForSigning(\DOMDocument $dom, \DOMElement $security) + { + $nodes = array(); + $body = $dom->getElementsByTagNameNS($dom->documentElement->namespaceURI, 'Body')->item(0); + if (null !== $body) { + $nodes[] = $body; + } + foreach ($security->childNodes as $node) { + if (XML_ELEMENT_NODE === $node->nodeType) { + $nodes[] = $node; + } + } + if ($this->signAllHeaders) { + foreach ($security->parentNode->childNodes as $node) { + if (XML_ELEMENT_NODE === $node->nodeType && + Helper::NS_WSS !== $node->namespaceURI) { + $nodes[] = $node; + } + } + } + + return $nodes; + } + + /** + * Gets the referenced node for the given URI. + * + * @param \DOMElement $node + * @param string $uri + * @return \DOMElement + */ + protected function getReferenceNodeForUri(\DOMElement $node, $uri) + { + $url = parse_url($uri); + $referenceId = $url['fragment']; + $query = '//*[@'.Helper::PFX_WSU.':Id="'.$referenceId.'" or @Id="'.$referenceId.'"]'; + $xpath = new \DOMXPath($node->ownerDocument); + $xpath->registerNamespace(Helper::PFX_WSU, Helper::NS_WSU); + + return $xpath->query($query)->item(0); + } + + /** + * Tries to resolve a key from the given \DOMElement. + * + * @param \DOMElement $node + * @param string $algorithm + * @return \ass\XmlSecurity\Key|null + */ + public function keyInfoSecurityTokenReferenceResolver(\DOMElement $node, $algorithm) + { + foreach ($node->childNodes as $key) { + if (Helper::NS_WSS === $key->namespaceURI) { + switch ($key->localName) { + case 'KeyIdentifier': + + return $this->serviceSecurityKey->getPublicKey(); + case 'Reference': + $uri = $key->getAttribute('URI'); + $referencedNode = $this->getReferenceNodeForUri($node, $uri); + + if (XmlSecurityEnc::NS_XMLENC === $referencedNode->namespaceURI + && 'EncryptedKey' == $referencedNode->localName) { + $key = XmlSecurityEnc::decryptEncryptedKey($referencedNode, $this->userSecurityKey->getPrivateKey()); + + return XmlSecurityKey::factory($algorithm, $key, XmlSecurityKey::TYPE_PRIVATE); + } else { + //$valueType = $key->getAttribute('ValueType'); + + return $this->serviceSecurityKey->getPublicKey(); + } + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 1894269..f3e1ead 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -12,8 +12,7 @@ namespace BeSimple\SoapClient; -// TODO -//use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\Helper; /** * Downloads WSDL files with cURL. Uses the WSDL_CACHE_* constants and the @@ -30,28 +29,28 @@ class WsdlDownloader * * @var bool */ - private $cacheEnabled; + protected $cacheEnabled; /** * Cache dir. * * @var string */ - private $cacheDir; + protected $cacheDir; /** * Cache TTL. * * @var int */ - private $cacheTtl; + protected $cacheTtl; /** * cURL instance for downloads. * * @var unknown_type */ - private $curl; + protected $curl; /** * Resolve WSDl/XSD includes. @@ -122,8 +121,10 @@ class WsdlDownloader throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); } } + return $cacheFile; } elseif (file_exists($wsdl)) { + return realpath($wsdl); } else { throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); @@ -145,6 +146,7 @@ class WsdlDownloader $isRemoteFile = true; } } + return $isRemoteFile; } @@ -154,7 +156,7 @@ class WsdlDownloader * @param string $xml * @param string $cacheFile * @param unknown_type $parentIsRemote - * @return string + * @return void */ private function resolveRemoteIncludes($xml, $cacheFile, $parentFile = null) { @@ -164,7 +166,7 @@ class WsdlDownloader $xpath->registerNamespace(Helper::PFX_XML_SCHEMA, Helper::NS_XML_SCHEMA); $xpath->registerNamespace('wsdl', 'http://schemas.xmlsoap.org/wsdl/'); // TODO add to Helper // WSDL include/import - $query = './/wsdl:include | .//wsdl:import'; + $query = './/wsdl:include | .//wsdl:import'; // TODO $nodes = $xpath->query($query); if ($nodes->length > 0) { foreach ($nodes as $node) { @@ -247,6 +249,7 @@ class WsdlDownloader if (isset($urlParts['port'])) { $hostname .= ':' . $urlParts['port']; } + return $hostname . implode('/', $parts); } } \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2e0638c..d2872c6 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -16,6 +16,13 @@ spl_autoload_register(function($class) { if (file_exists($path) && is_readable($path)) { require_once $path; + return true; + } + } elseif (0 === strpos($class, 'ass\XmlSecurity\\')) { + $path = __DIR__.'/../vendor/XmlSecurity/src/'.strtr($class, '\\', '/').'.php'; + if (file_exists($path) && is_readable($path)) { + require_once $path; + return true; } } elseif (0 === strpos($class, 'BeSimple\SoapCommon\\')) { diff --git a/vendors.php b/vendors.php index cf763ae..a0e0703 100644 --- a/vendors.php +++ b/vendors.php @@ -25,6 +25,7 @@ if (!is_dir($vendorDir = dirname(__FILE__).'/vendor')) { $deps = array( array('besimple-soapcommon', 'http://github.com/BeSimple/BeSimpleSoapCommon.git', 'origin/HEAD'), + array('XmlSecurity', 'https://github.com/aschamberger/XmlSecurity.git', 'origin/HEAD'), ); foreach ($deps as $dep) {