diff -Nru pysimplesoap-1.16/AUTHORS.md pysimplesoap-1.16.2/AUTHORS.md --- pysimplesoap-1.16/AUTHORS.md 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/AUTHORS.md 2017-12-03 17:09:02.000000000 +0000 @@ -40,3 +40,4 @@ - jpc - Luka Birsa - Bill Bennert @billwebreply +- Sean E. Millichamp diff -Nru pysimplesoap-1.16/debian/changelog pysimplesoap-1.16.2/debian/changelog --- pysimplesoap-1.16/debian/changelog 2018-09-12 15:19:58.000000000 +0000 +++ pysimplesoap-1.16.2/debian/changelog 2018-12-15 17:31:32.000000000 +0000 @@ -1,3 +1,26 @@ +pysimplesoap (1.16.2-1) unstable; urgency=medium + + [ Ondřej Nový ] + * d/control: Set Vcs-* to salsa.debian.org + * d/copyright: Use https protocol in Format field + * d/watch: Use https protocol + * Convert git repository from git-dpm to gbp layout + + [ Raphaël Hertzog ] + * New upstream release. + * Cherry-pick an upstream patch fixing a regression in 1.16.2 + where the SOAPAction header was no longer quoted. + * Add a patch to properly support maxOccurs attribute. + + [ Sandro Tosi ] + * Acknowledge NMU; Closes: #906127 + * debian/control + - bump Standards-Version to 4.2.1 (no changes needed) + * debian/copyright + - extend packaging copyright years + + -- Sandro Tosi Sat, 15 Dec 2018 12:31:32 -0500 + pysimplesoap (1.16-2.1) unstable; urgency=high * Non-maintainer upload. diff -Nru pysimplesoap-1.16/debian/control pysimplesoap-1.16.2/debian/control --- pysimplesoap-1.16/debian/control 2017-01-08 04:51:39.000000000 +0000 +++ pysimplesoap-1.16.2/debian/control 2018-12-15 17:31:32.000000000 +0000 @@ -3,10 +3,10 @@ Priority: optional Maintainer: Sandro Tosi Build-Depends: debhelper (>= 10), python, python3, dh-python, python-setuptools, python3-setuptools -Standards-Version: 3.9.8 +Standards-Version: 4.2.1 Homepage: https://github.com/pysimplesoap/pysimplesoap/ -Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/pysimplesoap.git -Vcs-Browser: https://anonscm.debian.org/cgit/python-modules/packages/pysimplesoap.git +Vcs-Git: https://salsa.debian.org/python-team/modules/pysimplesoap.git +Vcs-Browser: https://salsa.debian.org/python-team/modules/pysimplesoap Package: python-pysimplesoap Architecture: all diff -Nru pysimplesoap-1.16/debian/copyright pysimplesoap-1.16.2/debian/copyright --- pysimplesoap-1.16/debian/copyright 2017-01-08 04:51:39.000000000 +0000 +++ pysimplesoap-1.16.2/debian/copyright 2018-12-15 17:31:32.000000000 +0000 @@ -1,4 +1,4 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: pysimplesoap Source: https://github.com/pysimplesoap/pysimplesoap @@ -7,7 +7,7 @@ License: LGPL-3 Files: debian/* -Copyright: 2015-2017 Sandro Tosi +Copyright: 2015-2018 Sandro Tosi License: LGPL-3 License: LGPL-3 diff -Nru pysimplesoap-1.16/debian/.git-dpm pysimplesoap-1.16.2/debian/.git-dpm --- pysimplesoap-1.16/debian/.git-dpm 2017-01-08 04:51:39.000000000 +0000 +++ pysimplesoap-1.16.2/debian/.git-dpm 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -# see git-dpm(1) from git-dpm package -4362b9ee8ba9710b7d013604a0156fa150157ffe -4362b9ee8ba9710b7d013604a0156fa150157ffe -3b2f32184fce055c4a3eaadecc87f8a3092ef1f8 -3b2f32184fce055c4a3eaadecc87f8a3092ef1f8 -pysimplesoap_1.16.orig.tar.gz -0e00566716510fd4b494f1fab2cefd2fe5181183 -68035 -debianTag="debian/%e%v" -patchedTag="patched/%e%v" -upstreamTag="upstream/%e%u" diff -Nru pysimplesoap-1.16/debian/patches/9c062558f14f7045a0ee1d232cee434d917b0d5b.patch pysimplesoap-1.16.2/debian/patches/9c062558f14f7045a0ee1d232cee434d917b0d5b.patch --- pysimplesoap-1.16/debian/patches/9c062558f14f7045a0ee1d232cee434d917b0d5b.patch 2017-01-08 04:51:39.000000000 +0000 +++ pysimplesoap-1.16.2/debian/patches/9c062558f14f7045a0ee1d232cee434d917b0d5b.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,72 +0,0 @@ -From 4362b9ee8ba9710b7d013604a0156fa150157ffe Mon Sep 17 00:00:00 2001 -From: Michael Grant -Date: Thu, 12 Mar 2015 23:39:23 -0400 -Subject: Fixes Issue#43 - - (https://github.com/pysimplesoap/pysimplesoap/issues/43) "Python3 give syntax - errors when installing from pypi" - -Patch-Name: 9c062558f14f7045a0ee1d232cee434d917b0d5b.patch ---- - pysimplesoap/c14n.py | 8 ++++---- - pysimplesoap/xmlsec.py | 7 ++++--- - 2 files changed, 8 insertions(+), 7 deletions(-) - -diff --git a/pysimplesoap/c14n.py b/pysimplesoap/c14n.py -index be2180a..5749e49 100644 ---- a/pysimplesoap/c14n.py -+++ b/pysimplesoap/c14n.py -@@ -174,7 +174,7 @@ class _implementation: - elif node.nodeType == Node.DOCUMENT_TYPE_NODE: - pass - else: -- raise TypeError, str(node) -+ raise TypeError(str(node)) - - - def _inherit_context(self, node): -@@ -217,7 +217,7 @@ class _implementation: - elif child.nodeType == Node.DOCUMENT_TYPE_NODE: - pass - else: -- raise TypeError, str(child) -+ raise TypeError(str(child)) - handlers[Node.DOCUMENT_NODE] = _do_document - - -@@ -346,9 +346,9 @@ class _implementation: - - if not ns_rendered.has_key(prefix) and not ns_local.has_key(prefix): - if not ns_unused_inherited.has_key(prefix): -- raise RuntimeError,\ -+ raise RuntimeError(\ - 'For exclusive c14n, unable to map prefix "%s" in %s' %( -- prefix, node) -+ prefix, node)) - - ns_local[prefix] = ns_unused_inherited[prefix] - del ns_unused_inherited[prefix] -diff --git a/pysimplesoap/xmlsec.py b/pysimplesoap/xmlsec.py -index 4cb5fa9..2127c26 100644 ---- a/pysimplesoap/xmlsec.py -+++ b/pysimplesoap/xmlsec.py -@@ -203,15 +203,16 @@ def x509_verify(cacert, cert, binary=False): - if __name__ == "__main__": - # basic test of enveloping signature (the reference is a part of the xml) - sample_xml = """data""" -- print canonicalize(sample_xml) -+ output = canonicalize(sample_xml) -+ print (output) - vars = rsa_sign(sample_xml, '#object', "no_encriptada.key", "password") -- print SIGNED_TMPL % vars -+ print (SIGNED_TMPL % vars) - - # basic test of enveloped signature (the reference is the document itself) - sample_xml = """data%s""" - vars = rsa_sign(sample_xml % "", '', "no_encriptada.key", "password", - sign_template=SIGN_ENV_TMPL, c14n_exc=False) -- print sample_xml % (SIGNATURE_TMPL % vars) -+ print (sample_xml % (SIGNATURE_TMPL % vars)) - - # basic signature verification: - public_key = x509_extract_rsa_public_key(open("zunimercado.crt").read()) diff -Nru pysimplesoap-1.16/debian/patches/Add-quotes-to-SOAPAction-header-in-SoapClient.patch pysimplesoap-1.16.2/debian/patches/Add-quotes-to-SOAPAction-header-in-SoapClient.patch --- pysimplesoap-1.16/debian/patches/Add-quotes-to-SOAPAction-header-in-SoapClient.patch 1970-01-01 00:00:00.000000000 +0000 +++ pysimplesoap-1.16.2/debian/patches/Add-quotes-to-SOAPAction-header-in-SoapClient.patch 2018-12-15 17:31:32.000000000 +0000 @@ -0,0 +1,34 @@ +From 07a3203640574c81c83e25c5067046cf76031d9b Mon Sep 17 00:00:00 2001 +From: Daniel Mack +Date: Fri, 2 Mar 2018 09:52:14 +0100 +Subject: [PATCH] Add quotes to SOAPAction header in SoapClient + +According to the SOAP standard, the 'SOAPAction' HTTP field must be +quoted by double quotes unless the URI is empty and 'there is no +indication of the intent of the message' [1]. + +Not having proper quoting leads to gupnp based devices reply with invalid +XML. While there is a proposed fix for that to handle unquoted requests more +gracefully [2], the real issue in SoapClient implementation. + +[1] https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383528 +[2] https://bugzilla.gnome.org/show_bug.cgi?id=793955 + +Bug: https://github.com/pysimplesoap/pysimplesoap/pull/161 +--- + pysimplesoap/client.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/pysimplesoap/client.py b/pysimplesoap/client.py +index 2518bca..48eefad 100755 +--- a/pysimplesoap/client.py ++++ b/pysimplesoap/client.py +@@ -300,7 +300,7 @@ def send(self, method, xml): + } + + if self.action is not None: +- headers['SOAPAction'] = soap_action ++ headers['SOAPAction'] = '"' + soap_action + '"' + + headers.update(self.http_headers) + log.info("POST %s" % location) diff -Nru pysimplesoap-1.16/debian/patches/fix-httplib2-version-check.patch pysimplesoap-1.16.2/debian/patches/fix-httplib2-version-check.patch --- pysimplesoap-1.16/debian/patches/fix-httplib2-version-check.patch 2018-09-12 15:14:52.000000000 +0000 +++ pysimplesoap-1.16.2/debian/patches/fix-httplib2-version-check.patch 2018-12-15 17:31:32.000000000 +0000 @@ -1,15 +1,15 @@ ---- pysimplesoap-1.16.orig/pysimplesoap/transport.py -+++ pysimplesoap-1.16/pysimplesoap/transport.py -@@ -15,6 +15,8 @@ - +--- a/pysimplesoap/transport.py ++++ b/pysimplesoap/transport.py +@@ -16,6 +16,8 @@ import logging + import ssl import sys +from distutils.version import LooseVersion + try: import urllib2 from cookielib import CookieJar -@@ -61,7 +63,7 @@ class TransportBase: +@@ -62,7 +64,7 @@ class TransportBase: # try: import httplib2 @@ -18,10 +18,10 @@ import http.client # httplib2 workaround: check_hostname needs a SSL context with either # CERT_OPTIONAL or CERT_REQUIRED -@@ -91,9 +93,9 @@ else: +@@ -92,9 +94,9 @@ else: log.info("using proxy %s" % proxy) - # set optional parameters according supported httplib2 version + # set optional parameters according to supported httplib2 version - if httplib2.__version__ >= '0.3.0': + if LooseVersion(httplib2.__version__) >= LooseVersion('0.3.0'): kwargs['timeout'] = timeout diff -Nru pysimplesoap-1.16/debian/patches/series pysimplesoap-1.16.2/debian/patches/series --- pysimplesoap-1.16/debian/patches/series 2018-09-12 15:15:03.000000000 +0000 +++ pysimplesoap-1.16.2/debian/patches/series 2018-12-15 17:31:32.000000000 +0000 @@ -1,2 +1,3 @@ -9c062558f14f7045a0ee1d232cee434d917b0d5b.patch +Add-quotes-to-SOAPAction-header-in-SoapClient.patch fix-httplib2-version-check.patch +Support-integer-values-in-maxOccurs-attribute.patch diff -Nru pysimplesoap-1.16/debian/patches/Support-integer-values-in-maxOccurs-attribute.patch pysimplesoap-1.16.2/debian/patches/Support-integer-values-in-maxOccurs-attribute.patch --- pysimplesoap-1.16/debian/patches/Support-integer-values-in-maxOccurs-attribute.patch 1970-01-01 00:00:00.000000000 +0000 +++ pysimplesoap-1.16.2/debian/patches/Support-integer-values-in-maxOccurs-attribute.patch 2018-12-15 17:31:32.000000000 +0000 @@ -0,0 +1,84 @@ +From 0b94b326a534b8ccec3d47317a61e50d0f95c2f3 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Rapha=C3=ABl=20Hertzog?= +Date: Wed, 18 Jul 2018 12:39:08 +0200 +Subject: [PATCH] Support integer values in maxOccurs attribute +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +With the following complex type: +``` + +[...] + + + + + +``` + +pysimplesoap would generate the following key in the services description: +``` +selectedContractList: selectedContractList +``` + +This does not allow the input of multiple values in any way. I was expecting to +be able to pass this: +``` +'selectedContractList': [ {'selectedContract': '...'}, … ] +``` + +But this would result in "TypeError: unhashable type: 'dict'" in the +wsdl_validate_params() method. See full traceback below. + +With this patch, I'm now able to pass the desired structure. + +Full traceback of the unexpected error: +``` +ERROR: test_do_web_payment (__main__.WebPaymentAPITestCase) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "pypayline/tests-soap-services.py", line 101, in test_do_web_payment + cancel_url='http://freexian.com/cancel/', + File "/home/rhertzog/freexian/projets/boutique/python-payline/pypayline/client.py", line 220, in do_web_payment + notificationURL=notification_url + File "/home/rhertzog/freexian/projets/boutique/python-payline/pypayline/backends/soap.py", line 38, in doWebPayment + response = self.soap_client.doWebPayment(**data) + File "/usr/lib/python2.7/dist-packages/pysimplesoap/client.py", line 177, in + return lambda *args, **kwargs: self.wsdl_call(attr, *args, **kwargs) + File "/usr/lib/python2.7/dist-packages/pysimplesoap/client.py", line 351, in wsdl_call + return self.wsdl_call_with_args(method, args, kwargs) + File "/usr/lib/python2.7/dist-packages/pysimplesoap/client.py", line 372, in wsdl_call_with_args + method, params = self.wsdl_call_get_params(method, input, args, kwargs) + File "/usr/lib/python2.7/dist-packages/pysimplesoap/client.py", line 404, in wsdl_call_get_params + valid, errors, warnings = self.wsdl_validate_params(input, all_args) + File "/usr/lib/python2.7/dist-packages/pysimplesoap/client.py", line 474, in wsdl_validate_params + next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct[key], value[key]) + File "/usr/lib/python2.7/dist-packages/pysimplesoap/client.py", line 474, in wsdl_validate_params + next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct[key], value[key]) + File "/usr/lib/python2.7/dist-packages/pysimplesoap/client.py", line 470, in wsdl_validate_params + if key not in struct: +TypeError: unhashable type: 'dict' +``` + +Bug: https://github.com/pysimplesoap/pysimplesoap/pull/169 +--- + pysimplesoap/helpers.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/pysimplesoap/helpers.py b/pysimplesoap/helpers.py +index b6e6ce0..49c07d2 100644 +--- a/pysimplesoap/helpers.py ++++ b/pysimplesoap/helpers.py +@@ -237,7 +237,7 @@ def process_element(elements, element_name, node, element_type, xsd_uri, + key = make_key(type_name, ref_type, fn_namespace) + fn = elements.setdefault(key, Struct(key)) + +- if e['maxOccurs'] == 'unbounded' or (uri == soapenc_uri and type_name == 'Array'): ++ if (e['maxOccurs'] == 'unbounded' or int(e['maxOccurs'] or 0) > 1) or (uri == soapenc_uri and type_name == 'Array'): + # it's an array... TODO: compound arrays? and check ns uri! + if isinstance(fn, Struct): + if len(children) > 1 or (dialect in ('jetty', )): +-- +2.18.0 + diff -Nru pysimplesoap-1.16/debian/watch pysimplesoap-1.16.2/debian/watch --- pysimplesoap-1.16/debian/watch 2017-01-08 04:51:39.000000000 +0000 +++ pysimplesoap-1.16.2/debian/watch 2018-12-15 17:31:32.000000000 +0000 @@ -1,5 +1,5 @@ version=3 #opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ -#http://pypi.debian.net/PySimpleSOAP/PySimpleSOAP-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) +#https://pypi.debian.net/PySimpleSOAP/PySimpleSOAP-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) opts="filenamemangle=s/(?:.*)?v?(\d[\d\.]*)\.tar\.gz/-$1.tar.gz/" \ https://github.com/pysimplesoap/pysimplesoap/tags (?:.*/)?v?(\d[\d\.]*)\.tar\.gz diff -Nru pysimplesoap-1.16/.hgtags pysimplesoap-1.16.2/.hgtags --- pysimplesoap-1.16/.hgtags 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/.hgtags 2017-12-03 17:09:02.000000000 +0000 @@ -6,3 +6,4 @@ f289e16a0972be7d3a64901d713778381c919c2d 1.11-git 1a76eb59cdf5921344c9ec667af7b25ff8774e46 1.12 2ba439cbd4fc525bc2c62c5fe7b3bd9a83ad6336 1.13 +8fd33298cf908656920b52144443676dc953ed62 1.16 diff -Nru pysimplesoap-1.16/license.txt pysimplesoap-1.16.2/license.txt --- pysimplesoap-1.16/license.txt 1970-01-01 00:00:00.000000000 +0000 +++ pysimplesoap-1.16.2/license.txt 2017-12-03 17:09:02.000000000 +0000 @@ -0,0 +1,9 @@ +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by the +Free Software Foundation; either version 3, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. \ No newline at end of file diff -Nru pysimplesoap-1.16/pysimplesoap/c14n.py pysimplesoap-1.16.2/pysimplesoap/c14n.py --- pysimplesoap-1.16/pysimplesoap/c14n.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/pysimplesoap/c14n.py 2017-12-03 17:09:02.000000000 +0000 @@ -174,7 +174,7 @@ elif node.nodeType == Node.DOCUMENT_TYPE_NODE: pass else: - raise TypeError, str(node) + raise TypeError(str(node)) def _inherit_context(self, node): @@ -217,7 +217,7 @@ elif child.nodeType == Node.DOCUMENT_TYPE_NODE: pass else: - raise TypeError, str(child) + raise TypeError(str(child)) handlers[Node.DOCUMENT_NODE] = _do_document @@ -346,9 +346,9 @@ if not ns_rendered.has_key(prefix) and not ns_local.has_key(prefix): if not ns_unused_inherited.has_key(prefix): - raise RuntimeError,\ + raise RuntimeError(\ 'For exclusive c14n, unable to map prefix "%s" in %s' %( - prefix, node) + prefix, node)) ns_local[prefix] = ns_unused_inherited[prefix] del ns_unused_inherited[prefix] diff -Nru pysimplesoap-1.16/pysimplesoap/client.py pysimplesoap-1.16.2/pysimplesoap/client.py --- pysimplesoap-1.16/pysimplesoap/client.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/pysimplesoap/client.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -32,12 +32,11 @@ from .simplexml import SimpleXMLElement, TYPE_MAP, REVERSE_TYPE_MAP, Struct from .transport import get_http_wrapper, set_http_wrapper, get_Http # Utility functions used throughout wsdl_parse, moved aside for readability -from .helpers import fetch, sort_dict, make_key, process_element, \ +from .helpers import Alias, fetch, sort_dict, make_key, process_element, \ postprocess_element, get_message, preprocess_schema, \ get_local_name, get_namespace_prefix, TYPE_MAP, urlsplit from .wsse import UsernameToken - log = logging.getLogger(__name__) class SoapFault(RuntimeError): @@ -80,7 +79,7 @@ sessions=False, soap_server=None, timeout=TIMEOUT, http_headers=None, trace=False, username=None, password=None, - key_file=None, plugins=None, + key_file=None, plugins=None, strict=True, ): """ :param http_headers: Additional HTTP Headers; example: {'Host': 'ipsec.example.com'} @@ -94,6 +93,7 @@ self.xml_request = self.xml_response = '' self.http_headers = http_headers or {} self.plugins = plugins or [] + self.strict = strict # extract the base directory / url for wsdl relative imports: if wsdl and wsdl_basedir == '': # parse the wsdl url, strip the scheme and filename @@ -163,11 +163,7 @@ self.__xml = """ <%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" xmlns:%(ns)s="%(namespace)s"> <%(soap_ns)s:Header/> -<%(soap_ns)s:Body> - <%(ns)s:%(method)s> - - -""" +<%(soap_ns)s:Body><%(ns)s:%(method)s>""" # parse wsdl url self.services = wsdl and self.wsdl_parse(wsdl, cache=cache) @@ -176,7 +172,7 @@ def __getattr__(self, attr): """Return a pseudo-method that can be called""" if not self.services: # not using WSDL? - return lambda self=self, *args, **kwargs: self.call(attr, *args, **kwargs) + return lambda *args, **kwargs: self.call(attr, *args, **kwargs) else: # using WSDL: return lambda *args, **kwargs: self.wsdl_call(attr, *args, **kwargs) @@ -265,9 +261,15 @@ detail = None if detailXml and detailXml.children(): - operation = self.get_operation(method) - fault = operation['faults'][detailXml.children()[0].get_name()] - detail = detailXml.children()[0].unmarshall(fault, strict=False) + if self.services is not None: + operation = self.get_operation(method) + fault_name = detailXml.children()[0].get_name() + # if fault not defined in WSDL, it could be an axis or other + # standard type (i.e. "hostname"), try to convert it to string + fault = operation['faults'].get(fault_name) or unicode + detail = detailXml.children()[0].unmarshall(fault, strict=False) + else: + detail = repr(detailXml.children()) raise SoapFault(unicode(response.faultcode), unicode(response.faultstring), @@ -295,8 +297,11 @@ headers = { 'Content-type': 'text/xml; charset="UTF-8"', 'Content-length': str(len(xml)), - 'SOAPAction': '"%s"' % soap_action } + + if self.action is not None: + headers['SOAPAction'] = soap_action + headers.update(self.http_headers) log.info("POST %s" % location) log.debug('\n'.join(["%s: %s" % (k, v) for k, v in headers.items()])) @@ -369,7 +374,7 @@ # call remote procedure response = self.call(method, *params) # parse results: - resp = response('Body', ns=soap_uri).children().unmarshall(output) + resp = response('Body', ns=soap_uri).children().unmarshall(output, strict=self.strict) return resp and list(resp.values())[0] # pass Response tag children def wsdl_call_get_params(self, method, input, args, kwargs): @@ -399,7 +404,7 @@ valid, errors, warnings = self.wsdl_validate_params(input, all_args) if not valid: raise ValueError('Invalid Args Structure. Errors: %s' % errors) - # sort and filter parameters acording wsdl input structure + # sort and filter parameters according to wsdl input structure tree = sort_dict(input, all_args) root = list(tree.values())[0] params = [] @@ -525,6 +530,7 @@ xsi_uri = 'http://www.w3.org/2001/XMLSchema-instance' def _url_to_xml_tree(self, url, cache, force_download): + """Unmarshall the WSDL at the given url into a tree of SimpleXMLElement nodes""" # Open uri and read xml: xml = fetch(url, self.http, cache, force_download, self.wsdl_basedir, self.http_headers) # Parse WSDL XML: @@ -534,7 +540,7 @@ self.namespace = "" self.documentation = unicode(wsdl('documentation', error=False)) or '' - # some wsdl are splitted down in several files, join them: + # some wsdl are split down in several files, join them: imported_wsdls = {} for element in wsdl.children() or []: if element.get_local_name() in ('import'): @@ -559,6 +565,7 @@ return wsdl def _xml_tree_to_services(self, wsdl, cache, force_download): + """Convert SimpleXMLElement tree representation of the WSDL into pythonic objects""" # detect soap prefix and uri (xmlns attributes of ) xsd_ns = None soap_uris = {} @@ -588,6 +595,18 @@ global_namespaces = {None: self.namespace} # process current wsdl schema (if any, or many if imported): + # + # + # + # + # ... + # or + # + # + # + # + # + for types in wsdl('types', error=False) or []: # avoid issue if schema is not given in the main WSDL file schemas = types('schema', ns=self.xsd_uri, error=False) @@ -694,10 +713,10 @@ op = binding['operations'].setdefault(op_name, {}) op['name'] = op_name op['style'] = op.get('style', style) - if action: + if action is not None: op['action'] = action - # input and/or ouput can be not present! + # input and/or output can be not present! input = operation_node('input', error=False) body = input and input('body', ns=list(soap_uris.values()), error=False) parts_input_body = body and body['parts'] or None @@ -719,7 +738,7 @@ if hdr: headers.update(hdr) else: - pass # not enought info to search the header message: + pass # not enough info to search the header message: op['input'] = get_message(messages, op['input_msg'], parts_input_body, op['parameter_order']) op['header'] = headers @@ -754,9 +773,9 @@ if 'fault_msgs' in op: faults = op['faults'] = {} - for name, msg in op['fault_msgs'].iteritems(): + for msg in op['fault_msgs'].values(): msg_obj = get_message(messages, msg, parts_output_body) - tag_name = msg_obj.keys()[0] + tag_name = list(msg_obj)[0] faults[tag_name] = msg_obj # useless? never used @@ -799,6 +818,13 @@ # create an default service if none is given in the wsdl: if not services: services[''] = {'ports': {'': None}} + + elements = list(e for e in elements.values() if type(e) is type) + sorted(e for e in elements.values() if not(type(e) is type)) + e = None + self.elements = [] + for element in elements: + if e!= element: self.elements.append(element) + e = element return services @@ -864,6 +890,56 @@ log.debug('removing %s' % self.cacert) os.unlink(self.cacert) + def __repr__(self): + s = 'SOAP CLIENT' + s += '\n ELEMENTS' + for e in self.elements: + if isinstance(e, type): + e = e.__name__ + elif isinstance(e, Alias): + e = e.xml_type + elif isinstance(e, Struct) and e.key[1]=='element': + e = repr(e) + else: + continue + s += '\n %s' % e + for service in self.services: + s += '\n SERVICE (%s)' % service + ports = self.services[service]['ports'] + for port in ports: + port = ports[port] + if port['soap_ver'] == None: continue + s += '\n PORT (%s)' % port['name'] + s += '\n Location: %s' % port['location'] + s += '\n Soap ver: %s' % port['soap_ver'] + s += '\n Soap URI: %s' % port['soap_uri'] + s += '\n OPERATIONS' + operations = port['operations'] + for operation in sorted(operations): + operation = self.get_operation(operation) + input = operation.get('input') + input = input and input.values() and list(input.values())[0] + input_str = '' + if isinstance(input, dict): + if 'parameters' not in input or input['parameters']!=None: + for k, v in input.items(): + if isinstance(v, type): + v = v.__name__ + elif isinstance(v, Alias): + v = v.xml_type + elif isinstance(v, Struct): + v = v.key[0] + input_str += '%s: %s, ' % (k, v) + output = operation.get('output') + if output: + output = list(operation['output'].values())[0] + s += '\n %s(%s)' % ( + operation['name'], + input_str[:-2] + ) + s += '\n > %s' % output + + return s def parse_proxy(proxy_str): """Parses proxy address user:pass@host:port into a dict suitable for httplib2""" diff -Nru pysimplesoap-1.16/pysimplesoap/helpers.py pysimplesoap-1.16.2/pysimplesoap/helpers.py --- pysimplesoap-1.16/pysimplesoap/helpers.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/pysimplesoap/helpers.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # later version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -208,9 +208,9 @@ # get the complext element: ref_type = "complexType" key = make_key(type_name, ref_type, fn_namespace) - fn_complex = elements.setdefault(key, Struct()) + fn_complex = elements.setdefault(key, Struct(key)) # create an indirect struct {type_name: ...}: - fn_array = Struct() + fn_array = Struct(key) fn_array[type_name] = fn_complex fn_array.namespaces[None] = fn_namespace # set the default namespace fn_array.qualified = qualified @@ -235,7 +235,7 @@ else: ref_type = "element" key = make_key(type_name, ref_type, fn_namespace) - fn = elements.setdefault(key, Struct()) + fn = elements.setdefault(key, Struct(key)) if e['maxOccurs'] == 'unbounded' or (uri == soapenc_uri and type_name == 'Array'): # it's an array... TODO: compound arrays? and check ns uri! @@ -245,9 +245,11 @@ # {'ClassName': [{'attr1': val1, 'attr2': val2}] fn.array = True else: - # .NET style support (backward compatibility) - # [{'ClassName': {'attr1': val1, 'attr2': val2}] - struct.array = True + # .NET style now matches Jetty style + # {'ClassName': [{'attr1': val1, 'attr2': val2}] + #fn.array = True + #struct.array = True + fn = [fn] else: if len(children) > 1 or dialect in ('jetty',): # Jetty style support @@ -283,11 +285,13 @@ # add the processed element to the main dictionary (if not extension): if new_struct: key = make_key(element_name, element_type, namespace) - elements.setdefault(key, Struct()).update(struct) + elements.setdefault(key, Struct(key)).update(struct) def postprocess_element(elements, processed): """Fix unresolved references""" + #elements variable contains all eelements and complexTypes defined in http://www.w3.org/2001/XMLSchema + # (elements referenced before its definition, thanks .net) # avoid already processed elements: if elements in processed: @@ -303,14 +307,7 @@ warnings.warn(unicode(e), RuntimeWarning) if v.refers_to: # extension base? if isinstance(v.refers_to, dict): - for i, kk in enumerate(v.refers_to): - # extend base -keep orginal order- - if isinstance(v.refers_to, Struct): - elements[k].insert(kk, v.refers_to[kk], i) - # update namespace (avoid ArrayOfKeyValueOfanyTypeanyType) - if isinstance(v.refers_to, Struct) and v.refers_to.namespaces and kk: - elements[k].namespaces[kk] = v.refers_to.namespaces[kk] - elements[k].references[kk] = v.refers_to.references[kk] + extend_element(v, v.refers_to) # clean the reference: v.refers_to = None else: # "alias", just replace @@ -324,6 +321,20 @@ #if n != elements: # TODO: fix recursive elements postprocess_element(n, processed) +def extend_element(element, base): + ''' Recursively extend the elemnet if it has an extension base.''' + ''' Recursion is needed if the extension base itself extends another element.''' + if isinstance(base, dict): + for i, kk in enumerate(base): + # extend base -keep orignal order- + if isinstance(base, Struct): + element.insert(kk, base[kk], i) + # update namespace (avoid ArrayOfKeyValueOfanyTypeanyType) + if isinstance(base, Struct) and base.namespaces and kk: + element.namespaces[kk] = base.namespaces[kk] + element.references[kk] = base.references[kk] + if base.refers_to: + extend_element(element, base.refers_to) def get_message(messages, message_name, part_name, parameter_order=None): if part_name: @@ -446,8 +457,8 @@ return _strptime(s, fmt) except ValueError: try: - # strip utc offset - if s[-3] == ":" and s[-6] in (' ', '-', '+'): + # strip zulu timezone suffix or utc offset + if s[-1] == "Z" or (s[-3] == ":" and s[-6] in (' ', '-', '+')): try: import iso8601 return iso8601.parse_date(s) @@ -466,8 +477,8 @@ except ImportError: pass - warnings.warn('removing unsupported UTC offset. Install `iso8601`, `isodate` or `python-dateutil` package to support it', RuntimeWarning) - s = s[:-6] + warnings.warn('removing unsupported "Z" suffix or UTC offset. Install `iso8601`, `isodate` or `python-dateutil` package to support it', RuntimeWarning) + s = s[:-1] if s[-1] == "Z" else s[:-6] # parse microseconds try: return _strptime(s, fmt + ".%f") @@ -502,6 +513,31 @@ def __repr__(self): return "" % (self.xml_type, self.py_type) + def __eq__(self, other): + return isinstance(other, Alias) and self.xml_type == other.xml_type + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + if isinstance(other, Alias): return self.xml_type > other.xml_type + if isinstance(other, Struct): return False + return True + + def __lt__(self, other): + if isinstance(other, Alias): return self.xml_type < other.xml_type + if isinstance(other, Struct): return True + return False + + def __ge__(self, other): + return self.__gt__(other) or self.__eq__(other) + + def __le__(self, other): + return self.__gt__(other) or self.__eq__(other) + + def __hash__(self): + return hash(self.xml_type) + if sys.version > '3': long = Alias(int, 'long') byte = Alias(str, 'byte') @@ -514,7 +550,7 @@ duration = Alias(str, 'duration') any_uri = Alias(str, 'anyURI') -# Define convertion function (python type): xml schema type +# Define conversion function (python type): xml schema type TYPE_MAP = { unicode: 'string', bool: 'boolean', @@ -552,6 +588,10 @@ REVERSE_TYPE_MAP.update({ 'base64Binary': str, + 'unsignedByte': byte, + 'unsignedInt': int, + 'unsignedLong': long, + 'unsignedShort': short }) # insert str here to avoid collision in REVERSE_TYPE_MAP (i.e. decoding errors) @@ -562,7 +602,8 @@ class Struct(dict): """Minimal ordered dictionary to represent elements (i.e. xsd:sequences)""" - def __init__(self): + def __init__(self, key=None): + self.key = key self.__keys = [] self.array = False self.namespaces = {} # key: element, value: namespace URI @@ -595,6 +636,8 @@ return [(key, self[key]) for key in self.__keys] def update(self, other): + if isinstance(other, Struct) and other.key: + self.key = other.key for k, v in other.items(): self[k] = v # do not change if we are an array but the other is not: @@ -609,19 +652,55 @@ def copy(self): "Make a duplicate" - new = Struct() + new = Struct(self.key) new.update(self) return new + def __eq__(self, other): + return isinstance(other, Struct) and self.key == other.key and self.key != None + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + if isinstance(other, Struct): return (self.key[2], self.key[0], self.key[1]) > (other.key[2], other.key[0], other.key[1]) + return True + + def __lt__(self, other): + if isinstance(other, Struct): return (self.key[2], self.key[0], self.key[1]) < (other.key[2], other.key[0], other.key[1]) + return False + + def __ge__(self, other): + return self.__gt__(other) or self.__eq__(other) + + def __le__(self, other): + return self.__gt__(other) or self.__eq__(other) + + def __hash__(self): + return hash(self.key) + def __str__(self): return "%s" % dict.__str__(self) def __repr__(self): - try: - s = "{%s}" % ", ".join(['%s: %s' % (repr(k), repr(v)) for k, v in self.items()]) - except RuntimeError as e: # maximum recursion depth exceeded - s = "{%s}" % ", ".join(['%s: %s' % (repr(k), unicode(e)) for k, v in self.items()]) - warnings.warn(unicode(e), RuntimeWarning) - if self.array and False: - s = "[%s]" % s + if not self.key: return str(self.keys()) + s = '%s' % self.key[0] + if self.keys(): + s += ' {' + for k, t in self.items(): + is_list = False + if isinstance(t, list): + is_list = True + t = t[0] + if isinstance(t, type): + t = t.__name__ + pass + elif isinstance(t, Alias): + t = t.xml_type + elif isinstance(t, Struct): + t = t.key[0] + if is_list: + t = [t] + s += '%s: %s, ' % (k, t) + s = s[:-2]+'}' return s diff -Nru pysimplesoap-1.16/pysimplesoap/__init__.py pysimplesoap-1.16.2/pysimplesoap/__init__.py --- pysimplesoap-1.16/pysimplesoap/__init__.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/pysimplesoap/__init__.py 2017-12-03 17:09:02.000000000 +0000 @@ -8,7 +8,7 @@ __author_email__ = "reingart@gmail.com" __copyright__ = "Copyright (C) 2013 Mariano Reingart" __license__ = "LGPL 3.0" -__version__ = "1.16" +__version__ = "1.16.2" TIMEOUT = 60 diff -Nru pysimplesoap-1.16/pysimplesoap/server.py pysimplesoap-1.16.2/pysimplesoap/server.py --- pysimplesoap-1.16/pysimplesoap/server.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/pysimplesoap/server.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -207,8 +207,8 @@ etype, evalue, etb = sys.exc_info() log.error(traceback.format_exc()) if self.debug: - detail = ''.join(traceback.format_exception(etype, evalue, etb)) - detail += '\n\nXML REQUEST\n\n' + xml + detail = u''.join(traceback.format_exception(etype, evalue, etb)) + detail += u'\n\nXML REQUEST\n\n' + xml.decode('UTF-8') else: detail = None fault.update({'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), @@ -385,7 +385,7 @@ parse_element(n, v.items(), complex=True) t = "tns:%s" % n else: - raise TypeError("unknonw type v for marshalling" % str(v)) + raise TypeError("unknonw type %s for marshalling" % str(v)) e.add_attribute('type', t) parse_element("%s" % method, args and args.items()) diff -Nru pysimplesoap-1.16/pysimplesoap/simplexml.py pysimplesoap-1.16.2/pysimplesoap/simplexml.py --- pysimplesoap-1.16/pysimplesoap/simplexml.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/pysimplesoap/simplexml.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -325,7 +325,7 @@ #import pdb; pdb.set_trace() """Convert to python values the current serialized xml element""" - # types is a dict of {tag name: convertion function} + # types is a dict of {tag name: conversion function} # strict=False to use default type conversion if not specified # example: types={'p': {'a': int,'b': int}, 'c': [{'d':str}]} # expected xml:

12

holachau @@ -385,7 +385,9 @@ if isinstance(fn, list): # append to existing list (if any) - unnested dict arrays - value = d.setdefault(name, []) - children = node.children() + # If the node has no children then the node itself might + # have multiple occurrences: + children = node.children() or node # TODO: check if this was really needed (get first child only) ##if len(fn[0]) == 1 and children: ## children = children() @@ -394,14 +396,15 @@ for child in (children or []): tmp_dict = child.unmarshall(fn[0], strict) value.extend(tmp_dict.values()) - elif (self.__jetty and len(fn[0]) > 1): - # Jetty array style support [{k, v}] + #elif (self.__jetty and len(fn[0]) > 1): + elif (len(fn[0]) > 1): + # Jetty and now all dialects use array style support [{k, v}] for parent in node: tmp_dict = {} # unmarshall each value & mix for child in (node.children() or []): tmp_dict.update(child.unmarshall(fn[0], strict)) value.append(tmp_dict) - else: # .Net / Java + else: # len(fn[0]) == 0 for child in (children or []): value.append(child.unmarshall(fn[0], strict)) @@ -488,14 +491,22 @@ ns = False for k, v in value: getattr(self, name).marshall(k, v, add_comments=add_comments, ns=ns) - elif isinstance(value, list): # serialize lists + elif isinstance(value, list): # serialize lists name: [value1, value2] + # list elements should be a dict with one element: + # 'vats': [{'vat': {'vat_amount': 50, 'vat_percent': 5}}, {...}] + # or an array of complex types directly (a.k.a. jetty dialect) + # 'vat': [{'vat_amount': 100, 'vat_percent': 21.0}, {...}] child = self.add_child(name, ns=ns) if not add_children_ns: ns = False if add_comments: child.add_comment("Repetitive array of:") - for t in value: + for i, t in enumerate(value): child.marshall(name, t, False, add_comments=add_comments, ns=ns) + # "jetty" arrays: add new base node (if not last) -see abobe- + # TODO: this could be an issue for some arrays of single values + if isinstance(t, dict) and len(t) > 1 and i < len(value) - 1: + child = self.add_child(name, ns=ns) elif isinstance(value, (xml.dom.minidom.CDATASection, basestring)): # do not convert strings or unicodes self.add_child(name, value, ns=ns) elif value is None: # sent a empty tag? diff -Nru pysimplesoap-1.16/pysimplesoap/transport.py pysimplesoap-1.16.2/pysimplesoap/transport.py --- pysimplesoap-1.16/pysimplesoap/transport.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/pysimplesoap/transport.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -14,6 +14,7 @@ import logging +import ssl import sys try: import urllib2 @@ -90,7 +91,7 @@ kwargs['proxy_info'] = httplib2.ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, **proxy) log.info("using proxy %s" % proxy) - # set optional parameters according supported httplib2 version + # set optional parameters according to supported httplib2 version if httplib2.__version__ >= '0.3.0': kwargs['timeout'] = timeout if httplib2.__version__ >= '0.7.0': @@ -121,12 +122,21 @@ raise RuntimeError('proxy is not supported with urllib2 transport') if cacert: raise RuntimeError('cacert is not support with urllib2 transport') + + handlers = [] - self.request_opener = urllib2.urlopen + if ((sys.version_info[0] == 2 and sys.version_info >= (2,7,9)) or + (sys.version_info[0] == 3 and sys.version_info >= (3,2,0))): + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + handlers.append(urllib2.HTTPSHandler(context=context)) + if sessions: - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) - self.request_opener = opener.open - + handlers.append(urllib2.HTTPCookieProcessor(CookieJar())) + + opener = urllib2.build_opener(*handlers) + self.request_opener = opener.open self._timeout = timeout def request(self, url, method="GET", body=None, headers={}): @@ -142,10 +152,8 @@ _http_connectors['urllib2'] = urllib2Transport _http_facilities.setdefault('sessions', []).append('urllib2') -import sys if sys.version_info >= (2, 6): _http_facilities.setdefault('timeout', []).append('urllib2') -del sys # # pycurl support. @@ -191,7 +199,7 @@ c.setopt(c.CAINFO, self.cacert) c.setopt(pycurl.SSL_VERIFYPEER, self.cacert and 1 or 0) c.setopt(pycurl.SSL_VERIFYHOST, self.cacert and 2 or 0) - c.setopt(pycurl.CONNECTTIMEOUT, self.timeout / 6) + c.setopt(pycurl.CONNECTTIMEOUT, self.timeout) c.setopt(pycurl.TIMEOUT, self.timeout) if method == 'POST': c.setopt(pycurl.POST, 1) diff -Nru pysimplesoap-1.16/pysimplesoap/wsse.py pysimplesoap-1.16.2/pysimplesoap/wsse.py --- pysimplesoap-1.16/pysimplesoap/wsse.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/pysimplesoap/wsse.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # later version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -28,6 +28,13 @@ from . import __author__, __copyright__, __license__, __version__ from .simplexml import SimpleXMLElement +import random +import string +from hashlib import sha1 + +def randombytes(N): + return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(N)) + # Namespaces: WSSE_URI = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' @@ -35,6 +42,7 @@ XMLDSIG_URI = "http://www.w3.org/2000/09/xmldsig#" X509v3_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" Base64Binary_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" +PasswordDigest_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest" class UsernameToken: @@ -66,6 +74,39 @@ # TODO: add some password validation callback? pass +class UsernameDigestToken(UsernameToken): + """ + WebService Security extension to add a http digest credentials to xml request + drift -> time difference from the server in seconds, needed for 'Created' header + """ + + def __init__(self, username="", password="", drift=0): + self.username = username + self.password = password + self.drift = datetime.timedelta(seconds=drift) + + def preprocess(self, client, request, method, args, kwargs, headers, soap_uri): + header = request('Header', ns=soap_uri, ) + wsse = header.add_child('wsse:Security', ns=False) + wsse['xmlns:wsse'] = WSSE_URI + wsse['xmlns:wsu'] = WSU_URI + + usertoken = wsse.add_child('wsse:UsernameToken', ns=False) + usertoken.add_child('wsse:Username', self.username, ns=False) + + created = (datetime.datetime.utcnow() + self.drift).isoformat() + 'Z' + usertoken.add_child('wsu:Created', created, ns=False) + + nonce = randombytes(16) + wssenonce = usertoken.add_child('wsse:Nonce', nonce.encode('base64')[:-1], ns=False) + wssenonce['EncodingType'] = Base64Binary_URI + + sha1obj = sha1() + sha1obj.update(nonce + created + self.password) + digest = sha1obj.digest() + password = usertoken.add_child('wsse:Password', digest.encode('base64')[:-1], ns=False) + password['Type'] = PasswordDigest_URI + BIN_TOKEN_TMPL = """ @@ -140,13 +181,13 @@ self.__check(body['xmlns:wsu'], WSU_URI) ref_uri = body['wsu:Id'] signature = wsse("Signature", ns=XMLDSIG_URI) - signed_info = signature("SignedInfo") - signature_value = signature("SignatureValue") + signed_info = signature("SignedInfo", ns=XMLDSIG_URI) + signature_value = signature("SignatureValue", ns=XMLDSIG_URI) # TODO: these sanity checks should be moved to xmlsec? - self.__check(signed_info("Reference")['URI'], "#" + ref_uri) - self.__check(signed_info("SignatureMethod")['Algorithm'], + self.__check(signed_info("Reference", ns=XMLDSIG_URI)['URI'], "#" + ref_uri) + self.__check(signed_info("SignatureMethod", ns=XMLDSIG_URI)['Algorithm'], XMLDSIG_URI + "rsa-sha1") - self.__check(signed_info("Reference")("DigestMethod")['Algorithm'], + self.__check(signed_info("Reference", ns=XMLDSIG_URI)("DigestMethod", ns=XMLDSIG_URI)['Algorithm'], XMLDSIG_URI + "sha1") # TODO: check KeyInfo uses the correct SecurityTokenReference # workaround: copy namespaces so lxml can parse the xml to be signed @@ -157,7 +198,7 @@ ref_xml = xmlsec.canonicalize(repr(body)) # verify the signed hash computed_hash = xmlsec.sha1_hash_digest(ref_xml) - digest_value = str(signed_info("Reference")("DigestValue")) + digest_value = str(signed_info("Reference", ns=XMLDSIG_URI)("DigestValue", ns=XMLDSIG_URI)) if computed_hash != digest_value: raise RuntimeError("WSSE SHA1 hash digests mismatch") # workaround: prepare the signed info (assure the parent ns is present) diff -Nru pysimplesoap-1.16/pysimplesoap/xmlsec.py pysimplesoap-1.16.2/pysimplesoap/xmlsec.py --- pysimplesoap-1.16/pysimplesoap/xmlsec.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/pysimplesoap/xmlsec.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,12 +6,12 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. """Pythonic XML Security Library implementation""" - +from __future__ import print_function import base64 import hashlib import os @@ -203,15 +203,16 @@ if __name__ == "__main__": # basic test of enveloping signature (the reference is a part of the xml) sample_xml = """data""" - print canonicalize(sample_xml) + output = canonicalize(sample_xml) + print (output) vars = rsa_sign(sample_xml, '#object', "no_encriptada.key", "password") - print SIGNED_TMPL % vars + print (SIGNED_TMPL % vars) # basic test of enveloped signature (the reference is the document itself) sample_xml = """data%s""" vars = rsa_sign(sample_xml % "", '', "no_encriptada.key", "password", sign_template=SIGN_ENV_TMPL, c14n_exc=False) - print sample_xml % (SIGNATURE_TMPL % vars) + print (sample_xml % (SIGNATURE_TMPL % vars)) # basic signature verification: public_key = x509_extract_rsa_public_key(open("zunimercado.crt").read()) diff -Nru pysimplesoap-1.16/README.md pysimplesoap-1.16.2/README.md --- pysimplesoap-1.16/README.md 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/README.md 2017-12-03 17:09:02.000000000 +0000 @@ -1,17 +1,65 @@ -Features -======== +PySimpleSOAP / soap2py +====================== -This fork has the following features: +Python simple and lightweight SOAP library for client and server webservices interfaces, aimed to be as small and easy as possible, supporting most common functionality. +Initially it was inspired by [PHP Soap Extension](http://php.net/manual/en/book.soap.php) (mimicking its functionality, simplicity and ease of use), with many advanced features added. + +**Supports Python 3** (same codebase, no need to run 2to3) + +Goals +----- + + * Simple: originally less than 200LOC client/server concrete implementation for easy maintainability and enhancements. + * Flexible: adapted to several SOAP dialects/servers (Java Axis, .Net, WCF, JBoss, "jetty"), with the possibility of fine-tuning XML request and responses + * Pythonic: no artifacts, no class generation, no special types, RPC calls parameters and return values are simple python structures (dicts, list, etc.) + * Dynamic: no definition (WSDL) required, dynamic generation and parsing supported (cached in a pickle file for performance, supporting fixing broken WSDL) + * Easy: simple xml manipulation, including basic serialization and raw object-like access to SOAP messages + * Extensible: supports several HTTP wrappers (httplib2, pycurl, urllib2) for special transport needs over SSL and proxy (ISA) + * WSGI compliant: server dispatcher can be integrated to other python frameworks (web2py, django, etc.) + * Backwards compatible: stable API, no breaking changes between releases + * Lightweight: low memory footprint and fast processing (an order of magnitude in some situations, relative to other implementations) + +History +------- + +Client initially developed for AFIP (Argentina's IRS) web services: electronic invoice, tax bonus, insurance, foreign trade, agriculture, customs, etc. (http://code.google.com/p/pyafipws/wiki/ProjectSummary) + +Now it has been extended to support other webservices like Currency Exchange Control and TrazaMed (National Traceability of Medical Drugs Program) + +Also, includes Server side support (a generic dispatcher, in order to be exposed from web2py service framework, adaptable to other webservers, including examples for standalone WSGI and django) + +Source Code originally available on [GoogleCode](https://code.google.com/p/pysimplesoap) + +Changelog +--------- + +Recent changes (2014/2015): + +* Plug-in system to support for WSSE (Web-Services Security extensions) +* WSSE UsernameToken, UsernameDigestToken and BinaryTokenSignature support +* Pythonic XML Security Library basic implementation (canonicalization, SHA1 hashing and RSA signing / verification using X509 digital certificates) +* Improved SOAP Fault details +* Several fixes (basic python3 support, CDATA, ) +* [Breaking] Fixed bugs that occur when dealing with WSDL's that contain nested complex types. Such WSDL's are commonly generated by .NET WCF services that have been developed using "contract first" style where developers define classes with a full inheritance hierarchy that are implicitly converted to WCF soap services. See Issue #42 (https://github.com/pysimplesoap/pysimplesoap/issues/42). + +Ongoing efforts: + +* Unit Tests update & clean up (removing old tests, better framework, fixing non-deterministic results, etc.) +* WSDL advanced support (unifying nested elements structure dialects) +* Python3 support for WSSE XMLSec (M2Crypto alternatives?) +* Source code refactoring to improve readability and maintainability +* Improving interop with .NET WCF services + +Previous contributed features (circa 2013, forked and merged back): * Corrected support for multiple SOAP ports/bindings * Support for both `import` and `include` stanzas in WSDL * Support for a WSDL base directory to deal with relative pathnames in import/include stanzas -* Somewhat saner traceing/logging (traces now go to log.debug(), which you can handle per module) +* Somewhat saner tracing/logging (traces now go to log.debug(), which you can handle per module) * Somewhat more readable logic (by removing a bunch of helpers to a separate file) - Testing -======= +------- Using Python 2.7+: @@ -20,12 +68,19 @@ Using older Python versions: python -m unittest tests/suite.py - + Code coverage: sudo pip install coverage coverage run tests/suite.py - coverage report -m + coverage report -m coverage html +Support +------- + +For community support, please fell free to fill an [issue](https://github.com/pysimplesoap/pysimplesoap/issues/new) or send an email to [soap@python.org](https://mail.python.org/mailman/listinfo/soap). +Please do not add comment to wiki pages if you have technical questions. + +For priority commercial technical support, you can contact [Mariano Reingart](mailto:reingart@gmail.com) (project creator and main maintainer, see [AUTHORS](AUTHORS.md) for more info). diff -Nru pysimplesoap-1.16/samples/wsaa_py.py pysimplesoap-1.16.2/samples/wsaa_py.py --- pysimplesoap-1.16/samples/wsaa_py.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/samples/wsaa_py.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. diff -Nru pysimplesoap-1.16/setup.py pysimplesoap-1.16.2/setup.py --- pysimplesoap-1.16/setup.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/setup.py 2017-12-03 17:09:02.000000000 +0000 @@ -1,7 +1,11 @@ #!/usr/bin/env python -import setuptools from distutils.core import setup +import os +import subprocess +import sys +import warnings + try: import py2exe from nsis import build_installer @@ -10,17 +14,37 @@ from pysimplesoap import __version__, __author__, __author_email__, __license__ -# in the transition, register both: -for name in ('soap2py', 'PySimpleSOAP'): - setup( - name=name, - version=__version__, - description='Python simple and lightweight SOAP Library', - author=__author__, - author_email=__author_email__, - url='http://code.google.com/p/pysimplesoap', - packages=['pysimplesoap'], - license=__license__, - # console=['client.py'], - cmdclass={"py2exe": build_installer}, - ) +# convert the README and format in restructured text (only when registering) +long_desc = "" +if os.path.exists("README.md") and sys.platform == "linux2": + try: + cmd = ['pandoc', '--from=markdown', '--to=rst', 'README.md'] + long_desc = subprocess.check_output(cmd).decode("utf8") + print("Long DESC", long_desc) + except Exception as e: + warnings.warn("Exception when converting the README format: %s" % e) + +setup( + name='PySimpleSOAP', + version=__version__, + description='Python simple and lightweight SOAP Library', + long_description=long_desc, + author=__author__, + author_email=__author_email__, + url='https://github.com/pysimplesoap/pysimplesoap', + packages=['pysimplesoap'], + license=__license__, + # console=['client.py'], + cmdclass={"py2exe": build_installer}, + classifiers=[ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Communications", + "Topic :: Software Development :: Libraries :: Python Modules", + ], +) diff -Nru pysimplesoap-1.16/tests/afip_test.py pysimplesoap-1.16.2/tests/afip_test.py --- pysimplesoap-1.16/tests/afip_test.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/tests/afip_test.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -42,7 +42,7 @@ def test_wsaa_exception(self): """Test WSAA for SoapFault""" WSDL = "https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl" - client = SoapClient(wsdl=WSDL, ns="web") + client = SoapClient(wsdl=WSDL, trace=False, ns="web") try: resultado = client.loginCms('31867063') except SoapFault as e: @@ -130,3 +130,10 @@ self.assertEqual(resultget['Cbte_nro'], 38) self.assertEqual(resultget['Imp_total'], Decimal('130.21')) self.assertEqual(resultget['Cbte_tipo'], 19) + +if __name__ == '__main__': + #unittest.main() + suite = unittest.TestSuite() + suite.addTest(TestIssues('test_wsaa_exception')) + unittest.TextTestRunner().run(suite) + diff -Nru pysimplesoap-1.16/tests/cfdi_mx_test.py pysimplesoap-1.16.2/tests/cfdi_mx_test.py --- pysimplesoap-1.16/tests/cfdi_mx_test.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/tests/cfdi_mx_test.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. diff -Nru pysimplesoap-1.16/tests/issues_test.py pysimplesoap-1.16.2/tests/issues_test.py --- pysimplesoap-1.16/tests/issues_test.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/tests/issues_test.py 2017-12-03 17:09:02.000000000 +0000 @@ -7,6 +7,8 @@ import socket from xml.parsers.expat import ExpatError from pysimplesoap.client import SoapClient, SimpleXMLElement, SoapFault +from pysimplesoap.helpers import Alias +from pysimplesoap.transport import set_http_wrapper from .dummy_utils import DummyHTTP, TEST_DIR import sys @@ -23,6 +25,17 @@ wsdl='http://uat.destin8.co.uk:80/ChiefEDI/ChiefEDI?wsdl' ) + def test_issue33(self): + """Test SSL/TLS connection to self-signed or invalid hostname""" + # correct hostname is wsaahomo.afip.gov.ar, so avoid cert validation: + wsdl = "https://wsaahomo.afip.gob.ar/ws/services/LoginCms?wsdl" + client = SoapClient(wsdl=wsdl, cert=None, cacert=None) + # TODO: handle incorrect or invalid certificate connection... + + def test_issue33_fix(self): + set_http_wrapper('urllib2') + client = SoapClient(sessions=True) + def test_issue34(self): """Test soap_server SoapClient constructor parameter""" client = SoapClient( @@ -243,6 +256,10 @@ self.assertIsNotNone(question) self.assertNotEqual(question, "") + def test_issue65(self): + client = SoapClient() + valid = client.wsdl_validate_params(Alias(float, 'double'), 10.7) + self.assertTrue(valid[0], valid[1:]) def test_issue66(self): """Verify marshaled requests can be sent with no children""" @@ -647,6 +664,7 @@ if __name__ == '__main__': #unittest.main() suite = unittest.TestSuite() + suite.addTest(TestIssues('test_issue33')) #suite.addTest(TestIssues('test_issue34')) #suite.addTest(TestIssues('test_issue93')) #suite.addTest(TestIssues('test_issue57')) @@ -659,5 +677,5 @@ #suite.addTest(TestIssues('test_issue130')) #suite.addTest(TestIssues('test_issue141')) #suite.addTest(TestIssues('test_issue143')) - suite.addTest(TestIssues('test_issue157')) + #suite.addTest(TestIssues('test_issue157')) unittest.TextTestRunner().run(suite) diff -Nru pysimplesoap-1.16/tests/nfp_br_test.py pysimplesoap-1.16.2/tests/nfp_br_test.py --- pysimplesoap-1.16/tests/nfp_br_test.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/tests/nfp_br_test.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. diff -Nru pysimplesoap-1.16/tests/simplexmlelement_test.py pysimplesoap-1.16.2/tests/simplexmlelement_test.py --- pysimplesoap-1.16/tests/simplexmlelement_test.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/tests/simplexmlelement_test.py 2017-12-03 17:09:02.000000000 +0000 @@ -141,6 +141,29 @@ )}} self.eq(span.unmarshall(d), e) + def test_multiple_element_unmarshall_one(self): + xml = """ + + bar + + """ + span = SimpleXMLElement(xml) + d = {'results': {'foo': [str]}} + e = {'results': {'foo': ['bar']}} + self.eq(span.unmarshall(d), e) + + def test_multiple_element_unmarshall_two(self): + xml = """ + + bar + baz + + """ + span = SimpleXMLElement(xml) + d = {'results': {'foo': [str]}} + e = {'results': {'foo': ['bar', 'baz']}} + self.eq(span.unmarshall(d), e) + def test_basic(self): span = SimpleXMLElement( 'pyar' diff -Nru pysimplesoap-1.16/tests/soapdispatcher_test.py pysimplesoap-1.16.2/tests/soapdispatcher_test.py --- pysimplesoap-1.16/tests/soapdispatcher_test.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/tests/soapdispatcher_test.py 2017-12-03 17:09:02.000000000 +0000 @@ -52,7 +52,7 @@ self.dispatcher.register_function('Echo', echo) def test_classic_dialect(self): - # adder local test (clasic soap dialect) + # adder local test (classic soap dialect) resp = """
5000000.3
3
2011-07-24
""" xml = """ diff -Nru pysimplesoap-1.16/tests/sri_ec_test.py pysimplesoap-1.16.2/tests/sri_ec_test.py --- pysimplesoap-1.16/tests/sri_ec_test.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/tests/sri_ec_test.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. diff -Nru pysimplesoap-1.16/tests/trazamed_test.py pysimplesoap-1.16.2/tests/trazamed_test.py --- pysimplesoap-1.16/tests/trazamed_test.py 2015-01-16 22:11:05.000000000 +0000 +++ pysimplesoap-1.16.2/tests/trazamed_test.py 2017-12-03 17:09:02.000000000 +0000 @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. from __future__ import unicode_literals