Merge branch 'master' of github.com:web2py/web2py

This commit is contained in:
mdipierro
2016-06-26 00:30:42 -05:00
29 changed files with 2372 additions and 822 deletions
+4 -3
View File
@@ -10,6 +10,7 @@ import datetime
import copy import copy
import gluon.contenttype import gluon.contenttype
import gluon.fileutils import gluon.fileutils
from gluon._compat import iteritems
try: try:
import pygraphviz as pgv import pygraphviz as pgv
@@ -267,7 +268,7 @@ def select():
else: else:
rows = db(query, ignore_common_filters=True).select( rows = db(query, ignore_common_filters=True).select(
*fields, limitby=(start, stop)) *fields, limitby=(start, stop))
except Exception, e: except Exception as e:
import traceback import traceback
tb = traceback.format_exc() tb = traceback.format_exc()
(rows, nrows) = ([], 0) (rows, nrows) = ([], 0)
@@ -286,7 +287,7 @@ def select():
import_csv(db[request.vars.table], import_csv(db[request.vars.table],
request.vars.csvfile.file) request.vars.csvfile.file)
response.flash = T('data uploaded') response.flash = T('data uploaded')
except Exception, e: except Exception as e:
response.flash = DIV(T('unable to parse csv file'), PRE(str(e))) response.flash = DIV(T('unable to parse csv file'), PRE(str(e)))
# end handle upload csv # end handle upload csv
@@ -454,7 +455,7 @@ def ccache():
except (KeyError, ZeroDivisionError): except (KeyError, ZeroDivisionError):
ram['ratio'] = 0 ram['ratio'] = 0
for key, value in cache.ram.storage.iteritems(): for key, value in iteritems(cache.ram.storage):
if hp: if hp:
ram['bytes'] += hp.iso(value[1]).size ram['bytes'] += hp.iso(value[1]).size
ram['objects'] += hp.iso(value[1]).count ram['objects'] += hp.iso(value[1]).count
+20 -15
View File
@@ -15,6 +15,7 @@ from gluon.utils import web2py_uuid
from gluon.tools import Config from gluon.tools import Config
from gluon.compileapp import find_exposed_functions from gluon.compileapp import find_exposed_functions
from glob import glob from glob import glob
from gluon._compat import iteritems, PY2
import shutil import shutil
import platform import platform
@@ -23,7 +24,7 @@ try:
if git.__version__ < '0.3.1': if git.__version__ < '0.3.1':
raise ImportError("Your version of git is %s. Upgrade to 0.3.1 or better." % git.__version__) raise ImportError("Your version of git is %s. Upgrade to 0.3.1 or better." % git.__version__)
have_git = True have_git = True
except ImportError, e: except ImportError as e:
have_git = False have_git = False
GIT_MISSING = 'Requires gitpython module, but not installed or incompatible version: %s' % e GIT_MISSING = 'Requires gitpython module, but not installed or incompatible version: %s' % e
@@ -81,7 +82,10 @@ def safe_open(a, b):
def close(self): def close(self):
pass pass
return tmp() return tmp()
return open(a, b) if PY2 or 'b' in b:
return open(a, b)
else:
return open(a, b, encoding="utf8")
def safe_read(a, b='r'): def safe_read(a, b='r'):
@@ -123,6 +127,7 @@ def index():
redirect(send) redirect(send)
elif failed_login_count() >= allowed_number_of_attempts: elif failed_login_count() >= allowed_number_of_attempts:
time.sleep(2 ** allowed_number_of_attempts) time.sleep(2 ** allowed_number_of_attempts)
print('4033')
raise HTTP(403) raise HTTP(403)
elif request.vars.password: elif request.vars.password:
if verify_password(request.vars.password[:1024]): if verify_password(request.vars.password[:1024]):
@@ -262,7 +267,7 @@ def site():
new_repo = git.Repo.clone_from(form_update.vars.url, target) new_repo = git.Repo.clone_from(form_update.vars.url, target)
session.flash = T('new application "%s" imported', session.flash = T('new application "%s" imported',
form_update.vars.name) form_update.vars.name)
except git.GitCommandError, err: except git.GitCommandError as err:
session.flash = T('Invalid git repository specified.') session.flash = T('Invalid git repository specified.')
redirect(URL(r=request)) redirect(URL(r=request))
@@ -272,7 +277,7 @@ def site():
f = urllib.urlopen(form_update.vars.url) f = urllib.urlopen(form_update.vars.url)
if f.code == 404: if f.code == 404:
raise Exception("404 file not found") raise Exception("404 file not found")
except Exception, e: except Exception as e:
session.flash = \ session.flash = \
DIV(T('Unable to download app because:'), PRE(repr(e))) DIV(T('Unable to download app because:'), PRE(repr(e)))
redirect(URL(r=request)) redirect(URL(r=request))
@@ -313,7 +318,7 @@ def site():
if FILTER_APPS: if FILTER_APPS:
apps = [f for f in apps if f in FILTER_APPS] apps = [f for f in apps if f in FILTER_APPS]
apps = sorted(apps, lambda a, b: cmp(a.upper(), b.upper())) apps = sorted(apps, key=lambda a: a.upper())
myplatform = platform.python_version() myplatform = platform.python_version()
return dict(app=None, apps=apps, myversion=myversion, myplatform=myplatform, return dict(app=None, apps=apps, myversion=myversion, myplatform=myplatform,
form_create=form_create, form_update=form_update) form_create=form_create, form_update=form_update)
@@ -347,7 +352,7 @@ def pack():
else: else:
fname = 'web2py.app.%s.compiled.w2p' % app fname = 'web2py.app.%s.compiled.w2p' % app
filename = app_pack_compiled(app, request, raise_ex=True) filename = app_pack_compiled(app, request, raise_ex=True)
except Exception, e: except Exception as e:
filename = None filename = None
if filename: if filename:
@@ -421,7 +426,7 @@ def pack_custom():
fname = 'web2py.app.%s.w2p' % app fname = 'web2py.app.%s.w2p' % app
try: try:
filename = app_pack(app, request, raise_ex=True, filenames=files) filename = app_pack(app, request, raise_ex=True, filenames=files)
except Exception, e: except Exception as e:
filename = None filename = None
if filename: if filename:
response.headers['Content-Type'] = 'application/w2p' response.headers['Content-Type'] = 'application/w2p'
@@ -732,7 +737,7 @@ def edit():
try: try:
code = request.vars.data.rstrip().replace('\r\n', '\n') + '\n' code = request.vars.data.rstrip().replace('\r\n', '\n') + '\n'
compile(code, path, "exec", _ast.PyCF_ONLY_AST) compile(code, path, "exec", _ast.PyCF_ONLY_AST)
except Exception, e: except Exception as e:
# offset calculation is only used for textarea (start/stop) # offset calculation is only used for textarea (start/stop)
start = sum([len(line) + 1 for l, line start = sum([len(line) + 1 for l, line
in enumerate(request.vars.data.split("\n")) in enumerate(request.vars.data.split("\n"))
@@ -757,11 +762,11 @@ def edit():
# Lets try to reload the modules # Lets try to reload the modules
try: try:
mopath = '.'.join(request.args[2:])[:-3] mopath = '.'.join(request.args[2:])[:-3]
exec 'import applications.%s.modules.%s' % ( exec('import applications.%s.modules.%s' % (
request.args[0], mopath) request.args[0], mopath))
reload(sys.modules['applications.%s.modules.%s' reload(sys.modules['applications.%s.modules.%s'
% (request.args[0], mopath)]) % (request.args[0], mopath)])
except Exception, e: except Exception as e:
response.flash = DIV( response.flash = DIV(
T('failed to reload module because:'), PRE(repr(e))) T('failed to reload module because:'), PRE(repr(e)))
@@ -1151,7 +1156,7 @@ def design():
# Get all languages # Get all languages
langpath = os.path.join(apath(app, r=request), 'languages') langpath = os.path.join(apath(app, r=request), 'languages')
languages = dict([(lang, info) for lang, info languages = dict([(lang, info) for lang, info
in read_possible_languages(langpath).iteritems() in iteritems(read_possible_languages(langpath))
if info[2] != 0]) # info[2] is langfile_mtime: if info[2] != 0]) # info[2] is langfile_mtime:
# get only existed files # get only existed files
@@ -1287,7 +1292,7 @@ def plugin():
# Get all languages # Get all languages
languages = sorted([lang + '.py' for lang, info in languages = sorted([lang + '.py' for lang, info in
T.get_possible_languages_info().iteritems() iteritems(T.get_possible_languages_info())
if info[2] != 0]) # info[2] is langfile_mtime: if info[2] != 0]) # info[2] is langfile_mtime:
# get only existed files # get only existed files
@@ -1468,7 +1473,7 @@ def create_file():
redirect(URL('edit', redirect(URL('edit',
args=[os.path.join(request.vars.location, filename)], vars=vars)) args=[os.path.join(request.vars.location, filename)], vars=vars))
except Exception, e: except Exception as e:
if not isinstance(e, HTTP): if not isinstance(e, HTTP):
session.flash = T('cannot create file') session.flash = T('cannot create file')
@@ -1661,7 +1666,7 @@ def errors():
pickel=error, causer=error_causer, pickel=error, causer=error_causer,
last_line=last_line, hash=hash, last_line=last_line, hash=hash,
ticket=fn.ticket_id) ticket=fn.ticket_id)
except AttributeError, e: except AttributeError as e:
tk_db(tk_table.id == fn.id).delete() tk_db(tk_table.id == fn.id).delete()
tk_db.commit() tk_db.commit()
+1
View File
@@ -700,6 +700,7 @@
'Welcome to web2py': 'Vitejte ve Web2py aplikaci.', 'Welcome to web2py': 'Vitejte ve Web2py aplikaci.',
'Welcome to web2py!': 'Vítejte ve Web2py aplikaci.', 'Welcome to web2py!': 'Vítejte ve Web2py aplikaci.',
'Which called the function %s located in the file %s': 'která zavolala funkci %s v souboru (kontroléru) %s.', 'Which called the function %s located in the file %s': 'která zavolala funkci %s v souboru (kontroléru) %s.',
'Working...': 'Pracuji...',
'WSGI reference name': 'jméno WSGI reference', 'WSGI reference name': 'jméno WSGI reference',
'YES': 'ANO', 'YES': 'ANO',
'Yes': 'Ano', 'Yes': 'Ano',
+1 -1
View File
@@ -122,7 +122,7 @@
{{=XML(snapshot.get('response','no response available in snapshot'))}} {{=XML(snapshot.get('response','no response available in snapshot'))}}
</div> </div>
</div> </div>
{{except Exception, e:}} {{except Exception as e:}}
<!-- this should not happen, just in case... (cannot output normal hmtl as we don't know current open tags) --> <!-- this should not happen, just in case... (cannot output normal hmtl as we don't know current open tags) -->
{{import traceback;tb=traceback.format_exc().replace("\n","\\n") }} {{import traceback;tb=traceback.format_exc().replace("\n","\\n") }}
<script language='javascript'>alert("Exception during snapshot rendering: {{=tb}} ");</script> <script language='javascript'>alert("Exception during snapshot rendering: {{=tb}} ");</script>
+1 -1
View File
@@ -117,7 +117,7 @@
<div class="hide inspect" id="session"><h6>session</h6>{{=BEAUTIFY(snapshot['session'])}}</div> <div class="hide inspect" id="session"><h6>session</h6>{{=BEAUTIFY(snapshot['session'])}}</div>
<div class="hide inspect" id="response"><h6>response</h6>{{=BEAUTIFY(snapshot['response'])}}</div> <div class="hide inspect" id="response"><h6>response</h6>{{=BEAUTIFY(snapshot['response'])}}</div>
</div> </div>
{{except Exception, e:}} {{except Exception as e:}}
<!-- this should not happen, just in case... (cannot output normal hmtl as we don't know current open tags) --> <!-- this should not happen, just in case... (cannot output normal hmtl as we don't know current open tags) -->
{{import traceback;tb=traceback.format_exc().replace("\n","\\n") }} {{import traceback;tb=traceback.format_exc().replace("\n","\\n") }}
<script language='javascript'>alert("Exception during snapshot rendering: {{=tb}} ");</script> <script language='javascript'>alert("Exception during snapshot rendering: {{=tb}} ");</script>
+1 -1
View File
@@ -61,7 +61,7 @@
{{if hasattr(T,'get_possible_languages_info'):}} {{if hasattr(T,'get_possible_languages_info'):}}
- {{=T('Admin language')}}</span> - {{=T('Admin language')}}</span>
<select name="adminlanguage" onchange="var date = new Date();cookieDate=date.setTime(date.getTime()+(100*24*60*60*1000));document.cookie='adminLanguage='+this.options[this.selectedIndex].id+'; expires='+cookieDate+'; path=/';window.location.reload()"> <select name="adminlanguage" onchange="var date = new Date();cookieDate=date.setTime(date.getTime()+(100*24*60*60*1000));document.cookie='adminLanguage='+this.options[this.selectedIndex].id+'; expires='+cookieDate+'; path=/';window.location.reload()">
{{for langinfo in sorted([(code,info[1]) for code,info in T.get_possible_languages_info().iteritems() if code != 'default']):}} {{for langinfo in sorted([(code,info[1]) for code,info in iteritems(T.get_possible_languages_info()) if code != 'default']):}}
<option {{=T.accepted_language==langinfo[0] and 'selected' or ''}} {{='id='+langinfo[0]}} >{{=langinfo[1]}}</option> <option {{=T.accepted_language==langinfo[0] and 'selected' or ''}} {{='id='+langinfo[0]}} >{{=langinfo[1]}}</option>
{{pass}} {{pass}}
</select> </select>
@@ -10,6 +10,7 @@ import datetime
import copy import copy
import gluon.contenttype import gluon.contenttype
import gluon.fileutils import gluon.fileutils
from gluon._compat import iteritems
try: try:
import pygraphviz as pgv import pygraphviz as pgv
@@ -267,7 +268,7 @@ def select():
else: else:
rows = db(query, ignore_common_filters=True).select( rows = db(query, ignore_common_filters=True).select(
*fields, limitby=(start, stop)) *fields, limitby=(start, stop))
except Exception, e: except Exception as e:
import traceback import traceback
tb = traceback.format_exc() tb = traceback.format_exc()
(rows, nrows) = ([], 0) (rows, nrows) = ([], 0)
@@ -286,7 +287,7 @@ def select():
import_csv(db[request.vars.table], import_csv(db[request.vars.table],
request.vars.csvfile.file) request.vars.csvfile.file)
response.flash = T('data uploaded') response.flash = T('data uploaded')
except Exception, e: except Exception as e:
response.flash = DIV(T('unable to parse csv file'), PRE(str(e))) response.flash = DIV(T('unable to parse csv file'), PRE(str(e)))
# end handle upload csv # end handle upload csv
@@ -454,7 +455,7 @@ def ccache():
except (KeyError, ZeroDivisionError): except (KeyError, ZeroDivisionError):
ram['ratio'] = 0 ram['ratio'] = 0
for key, value in cache.ram.storage.iteritems(): for key, value in iteritems(cache.ram.storage):
if hp: if hp:
ram['bytes'] += hp.iso(value[1]).size ram['bytes'] += hp.iso(value[1]).size
ram['objects'] += hp.iso(value[1]).count ram['objects'] += hp.iso(value[1]).count
@@ -44,7 +44,7 @@ def test_soap_sub():
try: try:
ret = client.SubIntegers(a=3, b=2) ret = client.SubIntegers(a=3, b=2)
result = ret['SubResult'] result = ret['SubResult']
except SoapFault, sf: except SoapFault as sf:
result = sf result = sf
response.view = "soap_examples/generic.html" response.view = "soap_examples/generic.html"
return dict(xml_request=client.xml_request, return dict(xml_request=client.xml_request,
+1 -1
View File
@@ -28,7 +28,7 @@ def get_content(b=None,
try: try:
openedfile = openfile() openedfile = openfile()
except Exception, IOError: except (Exception, IOError):
l = 'en' l = 'en'
openedfile = openfile() openedfile = openfile()
+2 -1
View File
@@ -10,6 +10,7 @@ import datetime
import copy import copy
import gluon.contenttype import gluon.contenttype
import gluon.fileutils import gluon.fileutils
from gluon._compat import iteritems
try: try:
import pygraphviz as pgv import pygraphviz as pgv
@@ -454,7 +455,7 @@ def ccache():
except (KeyError, ZeroDivisionError): except (KeyError, ZeroDivisionError):
ram['ratio'] = 0 ram['ratio'] = 0
for key, value in cache.ram.storage.iteritems(): for key, value in iteritems(cache.ram.storage):
if hp: if hp:
ram['bytes'] += hp.iso(value[1]).size ram['bytes'] += hp.iso(value[1]).size
ram['objects'] += hp.iso(value[1]).count ram['objects'] += hp.iso(value[1]).count
+3 -3
View File
@@ -14,9 +14,9 @@
'(requires internet access)': '(vyžaduje připojení k internetu)', '(requires internet access)': '(vyžaduje připojení k internetu)',
'(requires internet access, experimental)': '(vyžaduje internetové připojení, experimentální)', '(requires internet access, experimental)': '(vyžaduje internetové připojení, experimentální)',
'(something like "it-it")': '(například "cs-cz")', '(something like "it-it")': '(například "cs-cz")',
'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '@markmin\x01(soubor **gluon/contrib/plural_rules/%s.py** nenalezen)', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(soubor **gluon/contrib/plural_rules/%s.py** nenalezen)',
'@markmin\x01An error occured, please [[reload %s]] the page': '@markmin\x01Došlo k chybě, prosím [[obnovte stránku %s]]', '@markmin\x01An error occured, please [[reload %s]] the page': 'Došlo k chybě, prosím [[obnovte stránku %s]]',
'@markmin\x01Searching: **%s** %%{file}': '@markmin\x01Hledání: **%s** %%{soubor}', '@markmin\x01Searching: **%s** %%{file}': 'Hledání: **%s** %%{soubor}',
'About': 'O programu', 'About': 'O programu',
'About application': 'O aplikaci', 'About application': 'O aplikaci',
'Access Control': 'Řízení přístupu', 'Access Control': 'Řízení přístupu',
+3 -2
View File
@@ -14,7 +14,8 @@ names = localhost:*, 127.0.0.1:*, *:*, *
[db] [db]
uri = sqlite://storage.sqlite uri = sqlite://storage.sqlite
migrate = true migrate = true
pool_size = 10 ; ignored for sqlite ; ignored for sqlite
pool_size = 10
; smtp address and credentials ; smtp address and credentials
[smtp] [smtp]
@@ -27,4 +28,4 @@ ssl = true
; form styling ; form styling
[forms] [forms]
formstyle = bootstrap3_inline formstyle = bootstrap3_inline
separator = separator =
+2
View File
@@ -30,6 +30,7 @@ if PY2:
from string import maketrans from string import maketrans
from types import ClassType from types import ClassType
import cgi import cgi
import cookielib
reduce = reduce reduce = reduce
hashlib_md5 = hashlib.md5 hashlib_md5 = hashlib.md5
iterkeys = lambda d: d.iterkeys() iterkeys = lambda d: d.iterkeys()
@@ -94,6 +95,7 @@ else:
from email.charset import Charset, add_charset, QP as charset_QP from email.charset import Charset, add_charset, QP as charset_QP
from urllib.request import FancyURLopener, urlopen from urllib.request import FancyURLopener, urlopen
from urllib.parse import quote as urllib_quote, unquote as urllib_unquote, urlencode from urllib.parse import quote as urllib_quote, unquote as urllib_unquote, urlencode
from http import cookiejar as cookielib
import html import html
hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8')) hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8'))
iterkeys = lambda d: iter(d.keys()) iterkeys = lambda d: iter(d.keys())
+6 -1
View File
@@ -18,7 +18,7 @@ import fnmatch
import os import os
import copy import copy
import random import random
from gluon._compat import builtin, PY2, unicodeT, to_native, basestring from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring
from gluon.storage import Storage, List from gluon.storage import Storage, List
from gluon.template import parse_template from gluon.template import parse_template
from gluon.restricted import restricted, compile2 from gluon.restricted import restricted, compile2
@@ -395,6 +395,11 @@ _base_environment_['SQLField'] = Field # for backward compatibility
_base_environment_['SQLFORM'] = SQLFORM _base_environment_['SQLFORM'] = SQLFORM
_base_environment_['SQLTABLE'] = SQLTABLE _base_environment_['SQLTABLE'] = SQLTABLE
_base_environment_['LOAD'] = LOAD _base_environment_['LOAD'] = LOAD
# For an easier PY3 migration
_base_environment_['PY2'] = PY2
_base_environment_['to_native'] = to_native
_base_environment_['to_bytes'] = to_bytes
_base_environment_['iteritems'] = iteritems
def build_environment(request, response, session, store_current=True): def build_environment(request, response, session, store_current=True):
""" """
+1 -1
View File
@@ -8,7 +8,7 @@ __author__ = "Mariano Reingart"
__author_email__ = "reingart@gmail.com" __author_email__ = "reingart@gmail.com"
__copyright__ = "Copyright (C) 2013 Mariano Reingart" __copyright__ = "Copyright (C) 2013 Mariano Reingart"
__license__ = "LGPL 3.0" __license__ = "LGPL 3.0"
__version__ = "1.11" __version__ = "1.16"
TIMEOUT = 60 TIMEOUT = 60
+433
View File
@@ -0,0 +1,433 @@
#! /usr/bin/env python
'''XML Canonicalization
Patches Applied to xml.dom.ext.c14n:
http://sourceforge.net/projects/pyxml/
[ 1444526 ] c14n.py: http://www.w3.org/TR/xml-exc-c14n/ fix
-- includes [ 829905 ] c14n.py fix for bug #825115,
Date Submitted: 2003-10-24 23:43
-- include dependent namespace declarations declared in ancestor nodes
(checking attributes and tags),
-- handle InclusiveNamespaces PrefixList parameter
This module generates canonical XML of a document or element.
http://www.w3.org/TR/2001/REC-xml-c14n-20010315
and includes a prototype of exclusive canonicalization
http://www.w3.org/Signature/Drafts/xml-exc-c14n
Requires PyXML 0.7.0 or later.
Known issues if using Ft.Lib.pDomlette:
1. Unicode
2. does not white space normalize attributes of type NMTOKEN and ID?
3. seems to be include "\n" after importing external entities?
Note, this version processes a DOM tree, and consequently it processes
namespace nodes as attributes, not from a node's namespace axis. This
permits simple document and element canonicalization without
XPath. When XPath is used, the XPath result node list is passed and used to
determine if the node is in the XPath result list, but little else.
Authors:
"Joseph M. Reagle Jr." <reagle@w3.org>
"Rich Salz" <rsalz@zolera.com>
$Date: 2006-03-30 23:47:16 +0000 (Thu, 30 Mar 2006) $ by $Author: boverhof $
'''
_copyright = '''Copyright 2001, Zolera Systems Inc. All Rights Reserved.
Copyright 2001, MIT. All Rights Reserved.
Distributed under the terms of:
Python 2.0 License or later.
http://www.python.org/2.0.1/license.html
or
W3C Software License
http://www.w3.org/Consortium/Legal/copyright-software-19980720
'''
import string
from xml.dom import Node
try:
from xml.ns import XMLNS
except:
class XMLNS:
BASE = "http://www.w3.org/2000/xmlns/"
XML = "http://www.w3.org/XML/1998/namespace"
try:
import cStringIO
StringIO = cStringIO
except ImportError:
import StringIO
_attrs = lambda E: (E.attributes and E.attributes.values()) or []
_children = lambda E: E.childNodes or []
_IN_XML_NS = lambda n: n.name.startswith("xmlns")
_inclusive = lambda n: n.unsuppressedPrefixes == None
# Does a document/PI has lesser/greater document order than the
# first element?
_LesserElement, _Element, _GreaterElement = range(3)
def _sorter(n1,n2):
'''_sorter(n1,n2) -> int
Sorting predicate for non-NS attributes.'''
i = cmp(n1.namespaceURI, n2.namespaceURI)
if i: return i
return cmp(n1.localName, n2.localName)
def _sorter_ns(n1,n2):
'''_sorter_ns((n,v),(n,v)) -> int
"(an empty namespace URI is lexicographically least)."'''
if n1[0] == 'xmlns': return -1
if n2[0] == 'xmlns': return 1
return cmp(n1[0], n2[0])
def _utilized(n, node, other_attrs, unsuppressedPrefixes):
'''_utilized(n, node, other_attrs, unsuppressedPrefixes) -> boolean
Return true if that nodespace is utilized within the node'''
if n.startswith('xmlns:'):
n = n[6:]
elif n.startswith('xmlns'):
n = n[5:]
if (n=="" and node.prefix in ["#default", None]) or \
n == node.prefix or n in unsuppressedPrefixes:
return 1
for attr in other_attrs:
if n == attr.prefix: return 1
# For exclusive need to look at attributes
if unsuppressedPrefixes is not None:
for attr in _attrs(node):
if n == attr.prefix: return 1
return 0
def _inclusiveNamespacePrefixes(node, context, unsuppressedPrefixes):
'''http://www.w3.org/TR/xml-exc-c14n/
InclusiveNamespaces PrefixList parameter, which lists namespace prefixes that
are handled in the manner described by the Canonical XML Recommendation'''
inclusive = []
if node.prefix:
usedPrefixes = ['xmlns:%s' %node.prefix]
else:
usedPrefixes = ['xmlns']
for a in _attrs(node):
if a.nodeName.startswith('xmlns') or not a.prefix: continue
usedPrefixes.append('xmlns:%s' %a.prefix)
unused_namespace_dict = {}
for attr in context:
n = attr.nodeName
if n in unsuppressedPrefixes:
inclusive.append(attr)
elif n.startswith('xmlns:') and n[6:] in unsuppressedPrefixes:
inclusive.append(attr)
elif n.startswith('xmlns') and n[5:] in unsuppressedPrefixes:
inclusive.append(attr)
elif attr.nodeName in usedPrefixes:
inclusive.append(attr)
elif n.startswith('xmlns:'):
unused_namespace_dict[n] = attr.value
return inclusive, unused_namespace_dict
#_in_subset = lambda subset, node: not subset or node in subset
_in_subset = lambda subset, node: subset is None or node in subset # rich's tweak
class _implementation:
'''Implementation class for C14N. This accompanies a node during it's
processing and includes the parameters and processing state.'''
# Handler for each node type; populated during module instantiation.
handlers = {}
def __init__(self, node, write, **kw):
'''Create and run the implementation.'''
self.write = write
self.subset = kw.get('subset')
self.comments = kw.get('comments', 0)
self.unsuppressedPrefixes = kw.get('unsuppressedPrefixes')
nsdict = kw.get('nsdict', { 'xml': XMLNS.XML, 'xmlns': XMLNS.BASE })
# Processing state.
self.state = (nsdict, {'xml':''}, {}, {}) #0422
if node.nodeType == Node.DOCUMENT_NODE:
self._do_document(node)
elif node.nodeType == Node.ELEMENT_NODE:
self.documentOrder = _Element # At document element
if not _inclusive(self):
inherited,unused = _inclusiveNamespacePrefixes(node, self._inherit_context(node),
self.unsuppressedPrefixes)
self._do_element(node, inherited, unused=unused)
else:
inherited = self._inherit_context(node)
self._do_element(node, inherited)
elif node.nodeType == Node.DOCUMENT_TYPE_NODE:
pass
else:
raise TypeError(str(node))
def _inherit_context(self, node):
'''_inherit_context(self, node) -> list
Scan ancestors of attribute and namespace context. Used only
for single element node canonicalization, not for subset
canonicalization.'''
# Collect the initial list of xml:foo attributes.
xmlattrs = filter(_IN_XML_NS, _attrs(node))
# Walk up and get all xml:XXX attributes we inherit.
inherited, parent = [], node.parentNode
while parent and parent.nodeType == Node.ELEMENT_NODE:
for a in filter(_IN_XML_NS, _attrs(parent)):
n = a.localName
if n not in xmlattrs:
xmlattrs.append(n)
inherited.append(a)
parent = parent.parentNode
return inherited
def _do_document(self, node):
'''_do_document(self, node) -> None
Process a document node. documentOrder holds whether the document
element has been encountered such that PIs/comments can be written
as specified.'''
self.documentOrder = _LesserElement
for child in node.childNodes:
if child.nodeType == Node.ELEMENT_NODE:
self.documentOrder = _Element # At document element
self._do_element(child)
self.documentOrder = _GreaterElement # After document element
elif child.nodeType == Node.PROCESSING_INSTRUCTION_NODE:
self._do_pi(child)
elif child.nodeType == Node.COMMENT_NODE:
self._do_comment(child)
elif child.nodeType == Node.DOCUMENT_TYPE_NODE:
pass
else:
raise TypeError(str(child))
handlers[Node.DOCUMENT_NODE] = _do_document
def _do_text(self, node):
'''_do_text(self, node) -> None
Process a text or CDATA node. Render various special characters
as their C14N entity representations.'''
if not _in_subset(self.subset, node): return
s = string.replace(node.data, "&", "&amp;")
s = string.replace(s, "<", "&lt;")
s = string.replace(s, ">", "&gt;")
s = string.replace(s, "\015", "&#xD;")
if s: self.write(s)
handlers[Node.TEXT_NODE] = _do_text
handlers[Node.CDATA_SECTION_NODE] = _do_text
def _do_pi(self, node):
'''_do_pi(self, node) -> None
Process a PI node. Render a leading or trailing #xA if the
document order of the PI is greater or lesser (respectively)
than the document element.
'''
if not _in_subset(self.subset, node): return
W = self.write
if self.documentOrder == _GreaterElement: W('\n')
W('<?')
W(node.nodeName)
s = node.data
if s:
W(' ')
W(s)
W('?>')
if self.documentOrder == _LesserElement: W('\n')
handlers[Node.PROCESSING_INSTRUCTION_NODE] = _do_pi
def _do_comment(self, node):
'''_do_comment(self, node) -> None
Process a comment node. Render a leading or trailing #xA if the
document order of the comment is greater or lesser (respectively)
than the document element.
'''
if not _in_subset(self.subset, node): return
if self.comments:
W = self.write
if self.documentOrder == _GreaterElement: W('\n')
W('<!--')
W(node.data)
W('-->')
if self.documentOrder == _LesserElement: W('\n')
handlers[Node.COMMENT_NODE] = _do_comment
def _do_attr(self, n, value):
''''_do_attr(self, node) -> None
Process an attribute.'''
W = self.write
W(' ')
W(n)
W('="')
s = string.replace(value, "&", "&amp;")
s = string.replace(s, "<", "&lt;")
s = string.replace(s, '"', '&quot;')
s = string.replace(s, '\011', '&#x9')
s = string.replace(s, '\012', '&#xA')
s = string.replace(s, '\015', '&#xD')
W(s)
W('"')
def _do_element(self, node, initial_other_attrs = [], unused = None):
'''_do_element(self, node, initial_other_attrs = [], unused = {}) -> None
Process an element (and its children).'''
# Get state (from the stack) make local copies.
# ns_parent -- NS declarations in parent
# ns_rendered -- NS nodes rendered by ancestors
# ns_local -- NS declarations relevant to this element
# xml_attrs -- Attributes in XML namespace from parent
# xml_attrs_local -- Local attributes in XML namespace.
# ns_unused_inherited -- not rendered namespaces, used for exclusive
ns_parent, ns_rendered, xml_attrs = \
self.state[0], self.state[1].copy(), self.state[2].copy() #0422
ns_unused_inherited = unused
if unused is None:
ns_unused_inherited = self.state[3].copy()
ns_local = ns_parent.copy()
inclusive = _inclusive(self)
xml_attrs_local = {}
# Divide attributes into NS, XML, and others.
other_attrs = []
in_subset = _in_subset(self.subset, node)
for a in initial_other_attrs + _attrs(node):
if a.namespaceURI == XMLNS.BASE:
n = a.nodeName
if n == "xmlns:": n = "xmlns" # DOM bug workaround
ns_local[n] = a.nodeValue
elif a.namespaceURI == XMLNS.XML:
if inclusive or (in_subset and _in_subset(self.subset, a)): #020925 Test to see if attribute node in subset
xml_attrs_local[a.nodeName] = a #0426
else:
if _in_subset(self.subset, a): #020925 Test to see if attribute node in subset
other_attrs.append(a)
# # TODO: exclusive, might need to define xmlns:prefix here
# if not inclusive and a.prefix is not None and not ns_rendered.has_key('xmlns:%s' %a.prefix):
# ns_local['xmlns:%s' %a.prefix] = ??
#add local xml:foo attributes to ancestor's xml:foo attributes
xml_attrs.update(xml_attrs_local)
# Render the node
W, name = self.write, None
if in_subset:
name = node.nodeName
if not inclusive:
if node.prefix is not None:
prefix = 'xmlns:%s' %node.prefix
else:
prefix = 'xmlns'
if not ns_rendered.has_key(prefix) and not ns_local.has_key(prefix):
if not ns_unused_inherited.has_key(prefix):
raise RuntimeError(\
'For exclusive c14n, unable to map prefix "%s" in %s' %(
prefix, node))
ns_local[prefix] = ns_unused_inherited[prefix]
del ns_unused_inherited[prefix]
W('<')
W(name)
# Create list of NS attributes to render.
ns_to_render = []
for n,v in ns_local.items():
# If default namespace is XMLNS.BASE or empty,
# and if an ancestor was the same
if n == "xmlns" and v in [ XMLNS.BASE, '' ] \
and ns_rendered.get('xmlns') in [ XMLNS.BASE, '', None ]:
continue
# "omit namespace node with local name xml, which defines
# the xml prefix, if its string value is
# http://www.w3.org/XML/1998/namespace."
if n in ["xmlns:xml", "xml"] \
and v in [ 'http://www.w3.org/XML/1998/namespace' ]:
continue
# If not previously rendered
# and it's inclusive or utilized
if (n,v) not in ns_rendered.items():
if inclusive or _utilized(n, node, other_attrs, self.unsuppressedPrefixes):
ns_to_render.append((n, v))
elif not inclusive:
ns_unused_inherited[n] = v
# Sort and render the ns, marking what was rendered.
ns_to_render.sort(_sorter_ns)
for n,v in ns_to_render:
self._do_attr(n, v)
ns_rendered[n]=v #0417
# If exclusive or the parent is in the subset, add the local xml attributes
# Else, add all local and ancestor xml attributes
# Sort and render the attributes.
if not inclusive or _in_subset(self.subset,node.parentNode): #0426
other_attrs.extend(xml_attrs_local.values())
else:
other_attrs.extend(xml_attrs.values())
other_attrs.sort(_sorter)
for a in other_attrs:
self._do_attr(a.nodeName, a.value)
W('>')
# Push state, recurse, pop state.
state, self.state = self.state, (ns_local, ns_rendered, xml_attrs, ns_unused_inherited)
for c in _children(node):
_implementation.handlers[c.nodeType](self, c)
self.state = state
if name: W('</%s>' % name)
handlers[Node.ELEMENT_NODE] = _do_element
def Canonicalize(node, output=None, **kw):
'''Canonicalize(node, output=None, **kw) -> UTF-8
Canonicalize a DOM document/element node and all descendents.
Return the text; if output is specified then output.write will
be called to output the text and None will be returned
Keyword parameters:
nsdict: a dictionary of prefix:uri namespace entries
assumed to exist in the surrounding context
comments: keep comments if non-zero (default is 0)
subset: Canonical XML subsetting resulting from XPath
(default is [])
unsuppressedPrefixes: do exclusive C14N, and this specifies the
prefixes that should be inherited.
'''
if output:
apply(_implementation, (node, output.write), kw)
else:
s = StringIO.StringIO()
apply(_implementation, (node, s.write), kw)
return s.getvalue()
+478 -260
View File
@@ -6,7 +6,7 @@
# version. # version.
# #
# This program is distributed in the hope that it will be useful, but # 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 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details. # for more details.
@@ -21,28 +21,30 @@ try:
import cPickle as pickle import cPickle as pickle
except ImportError: except ImportError:
import pickle import pickle
import copy
import hashlib import hashlib
import logging import logging
import os import os
import tempfile import tempfile
import warnings
from . import __author__, __copyright__, __license__, __version__, TIMEOUT from . import __author__, __copyright__, __license__, __version__, TIMEOUT
from .simplexml import SimpleXMLElement, TYPE_MAP, REVERSE_TYPE_MAP, OrderedDict from .simplexml import SimpleXMLElement, TYPE_MAP, REVERSE_TYPE_MAP, Struct
from .transport import get_http_wrapper, set_http_wrapper, get_Http from .transport import get_http_wrapper, set_http_wrapper, get_Http
# Utility functions used throughout wsdl_parse, moved aside for readability # 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, \ postprocess_element, get_message, preprocess_schema, \
get_local_name, get_namespace_prefix, TYPE_MAP, urlsplit get_local_name, get_namespace_prefix, TYPE_MAP, urlsplit
from .wsse import UsernameToken
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class SoapFault(RuntimeError): class SoapFault(RuntimeError):
def __init__(self, faultcode, faultstring): def __init__(self, faultcode, faultstring, detail=None):
self.faultcode = faultcode self.faultcode = faultcode
self.faultstring = faultstring self.faultstring = faultstring
RuntimeError.__init__(self, faultcode, faultstring) self.detail = detail
RuntimeError.__init__(self, faultcode, faultstring, detail)
def __unicode__(self): def __unicode__(self):
return '%s: %s' % (self.faultcode, self.faultstring) return '%s: %s' % (self.faultcode, self.faultstring)
@@ -54,8 +56,9 @@ class SoapFault(RuntimeError):
return self.__unicode__().encode('ascii', 'ignore') return self.__unicode__().encode('ascii', 'ignore')
def __repr__(self): def __repr__(self):
return "SoapFault(%s, %s)" % (repr(self.faultcode), return "SoapFault(faultcode = %s, faultstring %s, detail = %s)" % (repr(self.faultcode),
repr(self.faultstring)) repr(self.faultstring),
repr(self.detail))
# soap protocol specification & namespace # soap protocol specification & namespace
@@ -76,30 +79,38 @@ class SoapClient(object):
sessions=False, soap_server=None, timeout=TIMEOUT, sessions=False, soap_server=None, timeout=TIMEOUT,
http_headers=None, trace=False, http_headers=None, trace=False,
username=None, password=None, username=None, password=None,
key_file=None, plugins=None, strict=True,
): ):
""" """
:param http_headers: Additional HTTP Headers; example: {'Host': 'ipsec.example.com'} :param http_headers: Additional HTTP Headers; example: {'Host': 'ipsec.example.com'}
""" """
self.certssl = cert self.certssl = cert
self.keyssl = None self.keyssl = key_file
self.location = location # server location (url) self.location = location # server location (url)
self.action = action # SOAP base action self.action = action # SOAP base action
self.namespace = namespace # message self.namespace = namespace # message
self.exceptions = exceptions # lanzar execpiones? (Soap Faults) self.exceptions = exceptions # lanzar execpiones? (Soap Faults)
self.xml_request = self.xml_response = '' self.xml_request = self.xml_response = ''
self.http_headers = http_headers or {} self.http_headers = http_headers or {}
self.plugins = plugins or []
self.strict = strict
# extract the base directory / url for wsdl relative imports: # extract the base directory / url for wsdl relative imports:
if wsdl and wsdl_basedir == '': if wsdl and wsdl_basedir == '':
# parse the wsdl url, strip the scheme and filename # parse the wsdl url, strip the scheme and filename
url_scheme, netloc, path, query, fragment = urlsplit(wsdl) url_scheme, netloc, path, query, fragment = urlsplit(wsdl)
wsdl_basedir = os.path.dirname(netloc + path) wsdl_basedir = os.path.dirname(netloc + path)
self.wsdl_basedir = wsdl_basedir self.wsdl_basedir = wsdl_basedir
# shortcut to print all debugging info and sent / received xml messages # shortcut to print all debugging info and sent / received xml messages
if trace: if trace:
logging.basicConfig(level=logging.DEBUG) if trace is True:
level = logging.DEBUG # default logging level
else:
level = trace # use the provided level
logging.basicConfig(level=level)
log.setLevel(level)
if not soap_ns and not ns: if not soap_ns and not ns:
self.__soap_ns = 'soap' # 1.1 self.__soap_ns = 'soap' # 1.1
elif not soap_ns and ns: elif not soap_ns and ns:
@@ -112,7 +123,7 @@ class SoapClient(object):
# SOAP Header support # SOAP Header support
self.__headers = {} # general headers self.__headers = {} # general headers
self.__call_headers = None # OrderedDict to be marshalled for RPC Call self.__call_headers = None # Struct to be marshalled for RPC Call
# check if the Certification Authority Cert is a string and store it # check if the Certification Authority Cert is a string and store it
if cacert and cacert.startswith('-----BEGIN CERTIFICATE-----'): if cacert and cacert.startswith('-----BEGIN CERTIFICATE-----'):
@@ -130,7 +141,10 @@ class SoapClient(object):
if username and password: if username and password:
if hasattr(self.http, 'add_credentials'): if hasattr(self.http, 'add_credentials'):
self.http.add_credentials(username, password) self.http.add_credentials(username, password)
if cert and key_file:
if hasattr(self.http, 'add_certificate'):
self.http.add_certificate(key=key_file, cert=cert, domain='')
# namespace prefix, None to use xmlns attribute or False to not use it: # namespace prefix, None to use xmlns attribute or False to not use it:
self.__ns = ns self.__ns = ns
@@ -149,11 +163,7 @@ class SoapClient(object):
self.__xml = """<?xml version="1.0" encoding="UTF-8"?> self.__xml = """<?xml version="1.0" encoding="UTF-8"?>
<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" xmlns:%(ns)s="%(namespace)s"> <%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" xmlns:%(ns)s="%(namespace)s">
<%(soap_ns)s:Header/> <%(soap_ns)s:Header/>
<%(soap_ns)s:Body> <%(soap_ns)s:Body><%(ns)s:%(method)s></%(ns)s:%(method)s></%(soap_ns)s:Body></%(soap_ns)s:Envelope>"""
<%(ns)s:%(method)s>
</%(ns)s:%(method)s>
</%(soap_ns)s:Body>
</%(soap_ns)s:Envelope>"""
# parse wsdl url # parse wsdl url
self.services = wsdl and self.wsdl_parse(wsdl, cache=cache) self.services = wsdl and self.wsdl_parse(wsdl, cache=cache)
@@ -162,7 +172,7 @@ class SoapClient(object):
def __getattr__(self, attr): def __getattr__(self, attr):
"""Return a pseudo-method that can be called""" """Return a pseudo-method that can be called"""
if not self.services: # not using WSDL? 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: else: # using WSDL:
return lambda *args, **kwargs: self.wsdl_call(attr, *args, **kwargs) return lambda *args, **kwargs: self.wsdl_call(attr, *args, **kwargs)
@@ -175,12 +185,13 @@ class SoapClient(object):
""" """
#TODO: method != input_message #TODO: method != input_message
# Basic SOAP request: # Basic SOAP request:
soap_uri = soap_namespaces[self.__soap_ns]
xml = self.__xml % dict(method=method, # method tag name xml = self.__xml % dict(method=method, # method tag name
namespace=self.namespace, # method ns uri namespace=self.namespace, # method ns uri
ns=self.__ns, # method ns prefix ns=self.__ns, # method ns prefix
soap_ns=self.__soap_ns, # soap prefix & uri soap_ns=self.__soap_ns, # soap prefix & uri
soap_uri=soap_namespaces[self.__soap_ns]) soap_uri=soap_uri)
request = SimpleXMLElement(xml, namespace=self.__ns and self.namespace, request = SimpleXMLElement(xml, namespace=self.__ns and self.namespace,
prefix=self.__ns) prefix=self.__ns)
request_headers = kwargs.pop('headers', None) request_headers = kwargs.pop('headers', None)
@@ -191,18 +202,21 @@ class SoapClient(object):
else: else:
parameters = args parameters = args
if parameters and isinstance(parameters[0], SimpleXMLElement): if parameters and isinstance(parameters[0], SimpleXMLElement):
body = request('Body', ns=list(soap_namespaces.values()),)
# remove default body parameter (method name)
delattr(body, method)
# merge xmlelement parameter ("raw" - already marshalled) # merge xmlelement parameter ("raw" - already marshalled)
if parameters[0].children() is not None: body.import_node(parameters[0])
for param in parameters[0].children():
getattr(request, method).import_node(param)
for k,v in parameters[0].attributes().items():
getattr(request, method)[k] = v
elif parameters: elif parameters:
# marshall parameters: # marshall parameters:
use_ns = None if (self.__soap_server == "jetty" or self.qualified is False) else True use_ns = None if (self.__soap_server == "jetty" or self.qualified is False) else True
for k, v in parameters: # dict: tag=valor for k, v in parameters: # dict: tag=valor
getattr(request, method).marshall(k, v, ns=use_ns) if hasattr(v, "namespaces") and use_ns:
elif not self.__soap_server in ('oracle',) or self.__soap_server in ('jbossas6',): ns = v.namespaces.get(None, True)
else:
ns = use_ns
getattr(request, method).marshall(k, v, ns=ns)
elif self.__soap_server in ('jbossas6',):
# JBossAS-6 requires no empty method parameters! # JBossAS-6 requires no empty method parameters!
delattr(request("Body", ns=list(soap_namespaces.values()),), method) delattr(request("Body", ns=list(soap_namespaces.values()),), method)
@@ -210,15 +224,12 @@ class SoapClient(object):
if self.__headers and not self.services: if self.__headers and not self.services:
self.__call_headers = dict([(k, v) for k, v in self.__headers.items() self.__call_headers = dict([(k, v) for k, v in self.__headers.items()
if not k.startswith('wsse:')]) if not k.startswith('wsse:')])
# always extract WS Security header and send it # always extract WS Security header and send it (backward compatible)
if 'wsse:Security' in self.__headers: if 'wsse:Security' in self.__headers and not self.plugins:
#TODO: namespaces too hardwired, clean-up... warnings.warn("Replace wsse:Security with UsernameToken plugin",
header = request('Header', ns=list(soap_namespaces.values()),) DeprecationWarning)
k = 'wsse:Security' self.plugins.append(UsernameToken())
v = self.__headers[k]
header.marshall(k, v, ns=False, add_children_ns=False)
header(k)['xmlns:wsse'] = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
#<wsse:UsernameToken xmlns:wsu='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'>
if self.__call_headers: if self.__call_headers:
header = request('Header', ns=list(soap_namespaces.values()),) header = request('Header', ns=list(soap_namespaces.values()),)
for k, v in self.__call_headers.items(): for k, v in self.__call_headers.items():
@@ -236,18 +247,46 @@ class SoapClient(object):
for subheader in request_headers.children(): for subheader in request_headers.children():
header.import_node(subheader) header.import_node(subheader)
# do pre-processing using plugins (i.e. WSSE signing)
for plugin in self.plugins:
plugin.preprocess(self, request, method, args, kwargs,
self.__headers, soap_uri)
self.xml_request = request.as_xml() self.xml_request = request.as_xml()
self.xml_response = self.send(method, self.xml_request) self.xml_response = self.send(method, self.xml_request)
response = SimpleXMLElement(self.xml_response, namespace=self.namespace, response = SimpleXMLElement(self.xml_response, namespace=self.namespace,
jetty=self.__soap_server in ('jetty',)) jetty=self.__soap_server in ('jetty',))
if self.exceptions and response("Fault", ns=list(soap_namespaces.values()), error=False): if self.exceptions and response("Fault", ns=list(soap_namespaces.values()), error=False):
raise SoapFault(unicode(response.faultcode), unicode(response.faultstring)) detailXml = response("detail", ns=list(soap_namespaces.values()), error=False)
detail = None
if detailXml and detailXml.children():
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),
detail)
# do post-processing using plugins (i.e. WSSE signature verification)
for plugin in self.plugins:
plugin.postprocess(self, response, method, args, kwargs,
self.__headers, soap_uri)
return response return response
def send(self, method, xml): def send(self, method, xml):
"""Send SOAP request using HTTP""" """Send SOAP request using HTTP"""
if self.location == 'test': return if self.location == 'test': return
# location = '%s' % self.location #?op=%s" % (self.location, method) # location = '%s' % self.location #?op=%s" % (self.location, method)
http_method = str('POST')
location = str(self.location) location = str(self.location)
if self.services: if self.services:
@@ -258,15 +297,25 @@ class SoapClient(object):
headers = { headers = {
'Content-type': 'text/xml; charset="UTF-8"', 'Content-type': 'text/xml; charset="UTF-8"',
'Content-length': str(len(xml)), 'Content-length': str(len(xml)),
'SOAPAction': '"%s"' % soap_action
} }
if self.action is not None:
headers['SOAPAction'] = soap_action
headers.update(self.http_headers) headers.update(self.http_headers)
log.info("POST %s" % location) log.info("POST %s" % location)
log.debug('\n'.join(["%s: %s" % (k, v) for k, v in headers.items()])) log.debug('\n'.join(["%s: %s" % (k, v) for k, v in headers.items()]))
log.debug(xml) log.debug(xml)
if sys.version < '3':
# Ensure http_method, location and all headers are binary to prevent
# UnicodeError inside httplib.HTTPConnection._send_output.
# httplib in python3 do the same inside itself, don't need to convert it here
headers = dict((str(k), str(v)) for k, v in headers.items())
response, content = self.http.request( response, content = self.http.request(
location, 'POST', body=xml, headers=headers) location, http_method, body=xml, headers=headers)
self.response = response self.response = response
self.content = content self.content = content
@@ -298,6 +347,10 @@ class SoapClient(object):
return operation return operation
def wsdl_call(self, method, *args, **kwargs): def wsdl_call(self, method, *args, **kwargs):
"""Pre and post process SOAP call, input and output parameters using WSDL"""
return self.wsdl_call_with_args(method, args, kwargs)
def wsdl_call_with_args(self, method, args, kwargs):
"""Pre and post process SOAP call, input and output parameters using WSDL""" """Pre and post process SOAP call, input and output parameters using WSDL"""
soap_uri = soap_namespaces[self.__soap_ns] soap_uri = soap_namespaces[self.__soap_ns]
operation = self.get_operation(method) operation = self.get_operation(method)
@@ -308,23 +361,23 @@ class SoapClient(object):
header = operation.get('header') header = operation.get('header')
if 'action' in operation: if 'action' in operation:
self.action = operation['action'] self.action = operation['action']
if 'namespace' in operation: if 'namespace' in operation:
self.namespace = operation['namespace'] or '' self.namespace = operation['namespace'] or ''
self.qualified = operation['qualified'] self.qualified = operation['qualified']
# construct header and parameters # construct header and parameters
if header: if header:
self.__call_headers = sort_dict(header, self.__headers) self.__call_headers = sort_dict(header, self.__headers)
method, params = self.wsdl_call_get_params(method, input, *args, **kwargs) method, params = self.wsdl_call_get_params(method, input, args, kwargs)
# call remote procedure # call remote procedure
response = self.call(method, *params) response = self.call(method, *params)
# parse results: # 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 return resp and list(resp.values())[0] # pass Response tag children
def wsdl_call_get_params(self, method, input, *args, **kwargs): def wsdl_call_get_params(self, method, input, args, kwargs):
"""Build params from input and args/kwargs""" """Build params from input and args/kwargs"""
params = inputname = inputargs = None params = inputname = inputargs = None
all_args = {} all_args = {}
@@ -338,10 +391,9 @@ class SoapClient(object):
for idx, arg in enumerate(args): for idx, arg in enumerate(args):
key = list(inputargs.keys())[idx] key = list(inputargs.keys())[idx]
if isinstance(arg, dict): if isinstance(arg, dict):
if key in arg: if key not in arg:
d[key] = arg[key] raise KeyError('Unhandled key %s. use client.help(method)' % key)
else: d[key] = arg[key]
raise KeyError('Unhandled key %s. use client.help(method)')
else: else:
d[key] = arg d[key] = arg
all_args.update({inputname: d}) all_args.update({inputname: d})
@@ -352,7 +404,17 @@ class SoapClient(object):
valid, errors, warnings = self.wsdl_validate_params(input, all_args) valid, errors, warnings = self.wsdl_validate_params(input, all_args)
if not valid: if not valid:
raise ValueError('Invalid Args Structure. Errors: %s' % errors) raise ValueError('Invalid Args Structure. Errors: %s' % errors)
params = list(sort_dict(input, all_args).values())[0].items() # sort and filter parameters according to wsdl input structure
tree = sort_dict(input, all_args)
root = list(tree.values())[0]
params = []
# make a params tuple list suitable for self.call(method, *params)
for k, v in root.items():
# fix referenced namespaces as info is lost when calling call
root_ns = root.namespaces[k]
if not root.references[k] and isinstance(v, Struct):
v.namespaces[None] = root_ns
params.append((k, v))
# TODO: check style and document attributes # TODO: check style and document attributes
if self.__soap_server in ('axis', ): if self.__soap_server in ('axis', ):
# use the operation name # use the operation name
@@ -368,7 +430,7 @@ class SoapClient(object):
return (method, params) return (method, params)
def wsdl_validate_params(self, struct, value): def wsdl_validate_params(self, struct, value):
"""Validate the arguments (actual values) for the parameters structure. """Validate the arguments (actual values) for the parameters structure.
Fail for any invalid arguments or type mismatches.""" Fail for any invalid arguments or type mismatches."""
errors = [] errors = []
warnings = [] warnings = []
@@ -378,15 +440,15 @@ class SoapClient(object):
if type(struct) == type(value): if type(struct) == type(value):
typematch = True typematch = True
if not isinstance(struct, dict) and isinstance(value, dict): if not isinstance(struct, dict) and isinstance(value, dict):
typematch = True # struct can be an OrderedDict typematch = True # struct can be a dict or derived (Struct)
else: else:
typematch = False typematch = False
if struct == str: if struct == str:
struct = unicode # fix for py2 vs py3 string handling struct = unicode # fix for py2 vs py3 string handling
if not isinstance(struct, (list, dict, tuple)) and struct in TYPE_MAP.keys(): if not isinstance(struct, (list, dict, tuple)) and struct in TYPE_MAP.keys():
if not type(value) == struct: if not type(value) == struct and value is not None:
try: try:
struct(value) # attempt to cast input to parameter type struct(value) # attempt to cast input to parameter type
except: except:
@@ -459,6 +521,313 @@ class SoapClient(object):
headers, headers,
) )
soap_ns_uris = {
'http://schemas.xmlsoap.org/wsdl/soap/': 'soap11',
'http://schemas.xmlsoap.org/wsdl/soap12/': 'soap12',
}
wsdl_uri = 'http://schemas.xmlsoap.org/wsdl/'
xsd_uri = 'http://www.w3.org/2001/XMLSchema'
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:
wsdl = SimpleXMLElement(xml, namespace=self.wsdl_uri)
# Extract useful data:
self.namespace = ""
self.documentation = unicode(wsdl('documentation', error=False)) or ''
# 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'):
wsdl_namespace = element['namespace']
wsdl_location = element['location']
if wsdl_location is None:
log.warning('WSDL location not provided for %s!' % wsdl_namespace)
continue
if wsdl_location in imported_wsdls:
log.warning('WSDL %s already imported!' % wsdl_location)
continue
imported_wsdls[wsdl_location] = wsdl_namespace
log.debug('Importing wsdl %s from %s' % (wsdl_namespace, wsdl_location))
# Open uri and read xml:
xml = fetch(wsdl_location, self.http, cache, force_download, self.wsdl_basedir, self.http_headers)
# Parse imported XML schema (recursively):
imported_wsdl = SimpleXMLElement(xml, namespace=self.xsd_uri)
# merge the imported wsdl into the main document:
wsdl.import_node(imported_wsdl)
# warning: do not process schemas to avoid infinite recursion!
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 <definitions>)
xsd_ns = None
soap_uris = {}
for k, v in wsdl[:]:
if v in self.soap_ns_uris and k.startswith('xmlns:'):
soap_uris[get_local_name(k)] = v
if v == self.xsd_uri and k.startswith('xmlns:'):
xsd_ns = get_local_name(k)
elements = {} # element: type def
messages = {} # message: element
port_types = {} # port_type_name: port_type
bindings = {} # binding_name: binding
services = {} # service_name: service
# check axis2 namespace at schema types attributes (europa.eu checkVat)
if "http://xml.apache.org/xml-soap" in dict(wsdl[:]).values():
# get the sub-namespace in the first schema element (see issue 8)
if wsdl('types', error=False):
schema = wsdl.types('schema', ns=self.xsd_uri)
attrs = dict(schema[:])
self.namespace = attrs.get('targetNamespace', self.namespace)
if not self.namespace or self.namespace == "urn:DefaultNamespace":
self.namespace = wsdl['targetNamespace'] or self.namespace
imported_schemas = {}
global_namespaces = {None: self.namespace}
# process current wsdl schema (if any, or many if imported):
# <wsdl:definitions>
# <wsdl:types>
# <xs:schema>
# <xs:element>
# <xs:complexType>...</xs:complexType>
# or
# <xs:.../>
# </xs:element>
# </xs:schema>
# </wsdl:types>
# </wsdl:definitions>
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)
for schema in schemas or []:
preprocess_schema(schema, imported_schemas, elements, self.xsd_uri,
self.__soap_server, self.http, cache,
force_download, self.wsdl_basedir,
global_namespaces=global_namespaces)
# 2nd phase: alias, postdefined elements, extend bases, convert lists
postprocess_element(elements, [])
for message in wsdl.message:
for part in message('part', error=False) or []:
element = {}
element_name = part['element']
if not element_name:
# some implementations (axis) uses type instead
element_name = part['type']
type_ns = get_namespace_prefix(element_name)
type_uri = part.get_namespace_uri(type_ns)
part_name = part['name'] or None
if type_uri == self.xsd_uri:
element_name = get_local_name(element_name)
fn = REVERSE_TYPE_MAP.get(element_name, None)
element = {part_name: fn}
# emulate a true Element (complexType) for rpc style
if (message['name'], part_name) not in messages:
od = Struct()
od.namespaces[None] = type_uri
messages[(message['name'], part_name)] = {message['name']: od}
else:
od = messages[(message['name'], part_name)].values()[0]
od.namespaces[part_name] = type_uri
od.references[part_name] = False
od.update(element)
else:
element_name = get_local_name(element_name)
fn = elements.get(make_key(element_name, 'element', type_uri))
if not fn:
# some axis servers uses complexType for part messages (rpc)
fn = elements.get(make_key(element_name, 'complexType', type_uri))
od = Struct()
od[part_name] = fn
od.namespaces[None] = type_uri
od.namespaces[part_name] = type_uri
od.references[part_name] = False
element = {message['name']: od}
else:
element = {element_name: fn}
messages[(message['name'], part_name)] = element
for port_type_node in wsdl.portType:
port_type_name = port_type_node['name']
port_type = port_types[port_type_name] = {}
operations = port_type['operations'] = {}
for operation_node in port_type_node.operation:
op_name = operation_node['name']
op = operations[op_name] = {}
op['style'] = operation_node['style']
op['parameter_order'] = (operation_node['parameterOrder'] or "").split(" ")
op['documentation'] = unicode(operation_node('documentation', error=False)) or ''
if operation_node('input', error=False):
op['input_msg'] = get_local_name(operation_node.input['message'])
ns = get_namespace_prefix(operation_node.input['message'])
op['namespace'] = operation_node.get_namespace_uri(ns)
if operation_node('output', error=False):
op['output_msg'] = get_local_name(operation_node.output['message'])
#Get all fault message types this operation may return
fault_msgs = op['fault_msgs'] = {}
faults = operation_node('fault', error=False)
if faults is not None:
for fault in operation_node('fault', error=False):
fault_msgs[fault['name']] = get_local_name(fault['message'])
for binding_node in wsdl.binding:
port_type_name = get_local_name(binding_node['type'])
if port_type_name not in port_types:
# Invalid port type
continue
port_type = port_types[port_type_name]
binding_name = binding_node['name']
soap_binding = binding_node('binding', ns=list(soap_uris.values()), error=False)
transport = soap_binding and soap_binding['transport'] or None
style = soap_binding and soap_binding['style'] or None # rpc
binding = bindings[binding_name] = {
'name': binding_name,
'operations': copy.deepcopy(port_type['operations']),
'port_type_name': port_type_name,
'transport': transport,
'style': style,
}
for operation_node in binding_node.operation:
op_name = operation_node['name']
op_op = operation_node('operation', ns=list(soap_uris.values()), error=False)
action = op_op and op_op['soapAction']
op = binding['operations'].setdefault(op_name, {})
op['name'] = op_name
op['style'] = op.get('style', style)
if action is not None:
op['action'] = action
# 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
# parse optional header messages (some implementations use more than one!)
parts_input_headers = []
headers = input and input('header', ns=list(soap_uris.values()), error=False)
for header in headers or []:
hdr = {'message': header['message'], 'part': header['part']}
parts_input_headers.append(hdr)
if 'input_msg' in op:
headers = {} # base header message structure
for input_header in parts_input_headers:
header_msg = get_local_name(input_header.get('message'))
header_part = get_local_name(input_header.get('part'))
# warning: some implementations use a separate message!
hdr = get_message(messages, header_msg or op['input_msg'], header_part)
if hdr:
headers.update(hdr)
else:
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
try:
element = list(op['input'].values())[0]
ns_uri = element.namespaces[None]
qualified = element.qualified
except (AttributeError, KeyError) as e:
# TODO: fix if no parameters parsed or "variants"
ns_uri = op['namespace']
qualified = None
if ns_uri:
op['namespace'] = ns_uri
op['qualified'] = qualified
# Remove temporary property
del op['input_msg']
else:
op['input'] = None
op['header'] = None
output = operation_node('output', error=False)
body = output and output('body', ns=list(soap_uris.values()), error=False)
parts_output_body = body and body['parts'] or None
if 'output_msg' in op:
op['output'] = get_message(messages, op['output_msg'], parts_output_body)
# Remove temporary property
del op['output_msg']
else:
op['output'] = None
if 'fault_msgs' in op:
faults = op['faults'] = {}
for msg in op['fault_msgs'].values():
msg_obj = get_message(messages, msg, parts_output_body)
tag_name = list(msg_obj)[0]
faults[tag_name] = msg_obj
# useless? never used
parts_output_headers = []
headers = output and output('header', ns=list(soap_uris.values()), error=False)
for header in headers or []:
hdr = {'message': header['message'], 'part': header['part']}
parts_output_headers.append(hdr)
for service in wsdl("service", error=False) or []:
service_name = service['name']
if not service_name:
continue # empty service?
serv = services.setdefault(service_name, {})
ports = serv['ports'] = {}
serv['documentation'] = service['documentation'] or ''
for port in service.port:
binding_name = get_local_name(port['binding'])
if not binding_name in bindings:
continue # unknown binding
binding = ports[port['name']] = copy.deepcopy(bindings[binding_name])
address = port('address', ns=list(soap_uris.values()), error=False)
location = address and address['location'] or None
soap_uri = address and soap_uris.get(address.get_prefix())
soap_ver = soap_uri and self.soap_ns_uris.get(soap_uri)
binding.update({
'location': location,
'service_name': service_name,
'soap_uri': soap_uri,
'soap_ver': soap_ver,
})
# 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
def wsdl_parse(self, url, cache=False): def wsdl_parse(self, url, cache=False):
"""Parse Web Service Description v1.1""" """Parse Web Service Description v1.1"""
@@ -477,7 +846,6 @@ class SoapClient(object):
f.close() f.close()
# sanity check: # sanity check:
if pkl['version'][:-1] != __version__.split(' ')[0][:-1] or pkl['url'] != url: if pkl['version'][:-1] != __version__.split(' ')[0][:-1] or pkl['url'] != url:
import warnings
warnings.warn('version or url mismatch! discarding cached wsdl', RuntimeWarning) warnings.warn('version or url mismatch! discarding cached wsdl', RuntimeWarning)
log.debug('Version: %s %s' % (pkl['version'], __version__)) log.debug('Version: %s %s' % (pkl['version'], __version__))
log.debug('URL: %s %s' % (pkl['url'], url)) log.debug('URL: %s %s' % (pkl['url'], url))
@@ -487,211 +855,11 @@ class SoapClient(object):
self.documentation = pkl['documentation'] self.documentation = pkl['documentation']
return pkl['services'] return pkl['services']
soap_ns = {
'http://schemas.xmlsoap.org/wsdl/soap/': 'soap11',
'http://schemas.xmlsoap.org/wsdl/soap12/': 'soap12',
}
wsdl_uri = 'http://schemas.xmlsoap.org/wsdl/'
xsd_uri = 'http://www.w3.org/2001/XMLSchema'
xsi_uri = 'http://www.w3.org/2001/XMLSchema-instance'
# always return an unicode object: # always return an unicode object:
REVERSE_TYPE_MAP['string'] = str REVERSE_TYPE_MAP['string'] = str
# Open uri and read xml: wsdl = self._url_to_xml_tree(url, cache, force_download)
xml = fetch(url, self.http, cache, force_download, self.wsdl_basedir) services = self._xml_tree_to_services(wsdl, cache, force_download)
# Parse WSDL XML:
wsdl = SimpleXMLElement(xml, namespace=wsdl_uri)
# Extract useful data:
self.namespace = ""
self.documentation = unicode(wsdl('documentation', error=False)) or ''
# some wsdl are splitted down in several files, join them:
imported_wsdls = {}
for element in wsdl.children() or []:
if element.get_local_name() in ('import'):
wsdl_namespace = element['namespace']
wsdl_location = element['location']
if wsdl_location is None:
log.warning('WSDL location not provided for %s!' % wsdl_namespace)
continue
if wsdl_location in imported_wsdls:
log.warning('WSDL %s already imported!' % wsdl_location)
continue
imported_wsdls[wsdl_location] = wsdl_namespace
log.debug('Importing wsdl %s from %s' % (wsdl_namespace, wsdl_location))
# Open uri and read xml:
xml = fetch(wsdl_location, self.http, cache, force_download, self.wsdl_basedir)
# Parse imported XML schema (recursively):
imported_wsdl = SimpleXMLElement(xml, namespace=xsd_uri)
# merge the imported wsdl into the main document:
wsdl.import_node(imported_wsdl)
# warning: do not process schemas to avoid infinite recursion!
# detect soap prefix and uri (xmlns attributes of <definitions>)
xsd_ns = None
soap_uris = {}
for k, v in wsdl[:]:
if v in soap_ns and k.startswith('xmlns:'):
soap_uris[get_local_name(k)] = v
if v == xsd_uri and k.startswith('xmlns:'):
xsd_ns = get_local_name(k)
services = {}
bindings = {} # binding_name: binding
operations = {} # operation_name: operation
port_type_bindings = {} # port_type_name: binding
messages = {} # message: element
elements = {} # element: type def
for service in wsdl.service:
service_name = service['name']
if not service_name:
continue # empty service?
serv = services.setdefault(service_name, {'ports': {}})
serv['documentation'] = service['documentation'] or ''
for port in service.port:
binding_name = get_local_name(port['binding'])
operations[binding_name] = {}
address = port('address', ns=list(soap_uris.values()), error=False)
location = address and address['location'] or None
soap_uri = address and soap_uris.get(address.get_prefix())
soap_ver = soap_uri and soap_ns.get(soap_uri)
bindings[binding_name] = {'name': binding_name,
'service_name': service_name,
'location': location,
'soap_uri': soap_uri,
'soap_ver': soap_ver, }
serv['ports'][port['name']] = bindings[binding_name]
for binding in wsdl.binding:
binding_name = binding['name']
soap_binding = binding('binding', ns=list(soap_uris.values()), error=False)
transport = soap_binding and soap_binding['transport'] or None
port_type_name = get_local_name(binding['type'])
bindings[binding_name].update({
'port_type_name': port_type_name,
'transport': transport, 'operations': {},
})
if port_type_name not in port_type_bindings:
port_type_bindings[port_type_name] = []
port_type_bindings[port_type_name].append(bindings[binding_name])
for operation in binding.operation:
op_name = operation['name']
op = operation('operation', ns=list(soap_uris.values()), error=False)
action = op and op['soapAction']
d = operations[binding_name].setdefault(op_name, {})
bindings[binding_name]['operations'][op_name] = d
d.update({'name': op_name})
d['parts'] = {}
# input and/or ouput can be not present!
input = operation('input', error=False)
body = input and input('body', ns=list(soap_uris.values()), error=False)
d['parts']['input_body'] = body and body['parts'] or None
output = operation('output', error=False)
body = output and output('body', ns=list(soap_uris.values()), error=False)
d['parts']['output_body'] = body and body['parts'] or None
header = input and input('header', ns=list(soap_uris.values()), error=False)
d['parts']['input_header'] = header and {'message': header['message'], 'part': header['part']} or None
header = output and output('header', ns=list(soap_uris.values()), error=False)
d['parts']['output_header'] = header and {'message': header['message'], 'part': header['part']} or None
if action:
d['action'] = action
# check axis2 namespace at schema types attributes (europa.eu checkVat)
if "http://xml.apache.org/xml-soap" in dict(wsdl[:]).values():
# get the sub-namespace in the first schema element (see issue 8)
if wsdl('types', error=False):
schema = wsdl.types('schema', ns=xsd_uri)
attrs = dict(schema[:])
self.namespace = attrs.get('targetNamespace', self.namespace)
if not self.namespace or self.namespace == "urn:DefaultNamespace":
self.namespace = wsdl['targetNamespace'] or self.namespace
imported_schemas = {}
global_namespaces = {None: self.namespace}
# process current wsdl schema (if any):
if wsdl('types', error=False):
for schema in wsdl.types('schema', ns=xsd_uri):
preprocess_schema(schema, imported_schemas, elements, xsd_uri,
self.__soap_server, self.http, cache,
force_download, self.wsdl_basedir,
global_namespaces=global_namespaces)
# 2nd phase: alias, postdefined elements, extend bases, convert lists
postprocess_element(elements, [])
for message in wsdl.message:
for part in message('part', error=False) or []:
element = {}
element_name = part['element']
if not element_name:
# some implementations (axis) uses type instead
element_name = part['type']
type_ns = get_namespace_prefix(element_name)
type_uri = wsdl.get_namespace_uri(type_ns)
if type_uri == xsd_uri:
element_name = get_local_name(element_name)
fn = REVERSE_TYPE_MAP.get(element_name, None)
element = {part['name']: fn}
# emulate a true Element (complexType)
list(messages.setdefault((message['name'], None), {message['name']: OrderedDict()}).values())[0].update(element)
else:
element_name = get_local_name(element_name)
fn = elements.get(make_key(element_name, 'element', type_uri))
if not fn:
# some axis servers uses complexType for part messages
fn = elements.get(make_key(element_name, 'complexType', type_uri))
element = {message['name']: {part['name']: fn}}
else:
element = {element_name: fn}
messages[(message['name'], part['name'])] = element
for port_type in wsdl.portType:
port_type_name = port_type['name']
for binding in port_type_bindings.get(port_type_name, []):
for operation in port_type.operation:
op_name = operation['name']
op = operations[binding['name']][op_name]
op['documentation'] = unicode(operation('documentation', error=False)) or ''
if binding['soap_ver']:
#TODO: separe operation_binding from operation (non SOAP?)
if operation('input', error=False):
input_msg = get_local_name(operation.input['message'])
input_header = op['parts'].get('input_header')
if input_header:
header_msg = get_local_name(input_header.get('message'))
header_part = get_local_name(input_header.get('part'))
# warning: some implementations use a separate message!
header = get_message(messages, header_msg or input_msg, header_part)
else:
header = None # not enought info to search the header message:
op['input'] = get_message(messages, input_msg, op['parts'].get('input_body'))
op['header'] = header
try:
element = list(op['input'].values())[0]
ns_uri = element.namespace
qualified = element.qualified
except AttributeError:
# TODO: fix if no parameters parsed or "variants"
ns = get_namespace_prefix(operation.input['message'])
ns_uri = operation.get_namespace_uri(ns)
qualified = None
if ns_uri:
op['namespace'] = ns_uri
op['qualified'] = qualified
else:
op['input'] = None
op['header'] = None
if operation('output', error=False):
output_msg = get_local_name(operation.output['message'])
op['output'] = get_message(messages, output_msg, op['parts'].get('output_body'))
else:
op['output'] = None
# dump the full service/port/operation map # dump the full service/port/operation map
#log.debug(pprint.pformat(services)) #log.debug(pprint.pformat(services))
@@ -722,6 +890,56 @@ class SoapClient(object):
log.debug('removing %s' % self.cacert) log.debug('removing %s' % self.cacert)
os.unlink(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): def parse_proxy(proxy_str):
"""Parses proxy address user:pass@host:port into a dict suitable for httplib2""" """Parses proxy address user:pass@host:port into a dict suitable for httplib2"""
+300 -83
View File
@@ -1,12 +1,12 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU Lesser General Public License as published by
# Free Software Foundation; either version 3, or (at your option) any later # the Free Software Foundation; either version 3, or (at your option) any
# version. # later version.
# #
# This program is distributed in the hope that it will be useful, but # 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 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details. # for more details.
@@ -38,7 +38,7 @@ from . import __author__, __copyright__, __license__, __version__
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''): def fetch(url, http, cache=False, force_download=False, wsdl_basedir='', headers={}):
"""Download a document from a URL, save it locally if cache enabled""" """Download a document from a URL, save it locally if cache enabled"""
# check / append a valid schema if not given: # check / append a valid schema if not given:
@@ -46,12 +46,13 @@ def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''):
if not url_scheme in ('http', 'https', 'file'): if not url_scheme in ('http', 'https', 'file'):
for scheme in ('http', 'https', 'file'): for scheme in ('http', 'https', 'file'):
try: try:
path = os.path.normpath(os.path.join(wsdl_basedir, url))
if not url.startswith("/") and scheme in ('http', 'https'): if not url.startswith("/") and scheme in ('http', 'https'):
tmp_url = "%s://%s" % (scheme, os.path.join(wsdl_basedir, url)) tmp_url = "%s://%s" % (scheme, path)
else: else:
tmp_url = "%s:%s" % (scheme, os.path.join(wsdl_basedir, url)) tmp_url = "%s:%s" % (scheme, path)
log.debug('Scheme not found, trying %s' % scheme) log.debug('Scheme not found, trying %s' % scheme)
return fetch(tmp_url, http, cache, force_download, wsdl_basedir) return fetch(tmp_url, http, cache, force_download, wsdl_basedir, headers)
except Exception as e: except Exception as e:
log.error(e) log.error(e)
raise RuntimeError('No scheme given for url: %s' % url) raise RuntimeError('No scheme given for url: %s' % url)
@@ -72,7 +73,7 @@ def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''):
xml = f.read() xml = f.read()
else: else:
log.info('GET %s using %s' % (url, http._wrapper_version)) log.info('GET %s using %s' % (url, http._wrapper_version))
response, xml = http.request(url, 'GET', None, {}) response, xml = http.request(url, 'GET', None, headers)
if cache: if cache:
log.info('Writing file %s' % filename) log.info('Writing file %s' % filename)
if not os.path.isdir(cache): if not os.path.isdir(cache):
@@ -86,7 +87,7 @@ def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''):
def sort_dict(od, d): def sort_dict(od, d):
"""Sort parameters (same order as xsd:sequence)""" """Sort parameters (same order as xsd:sequence)"""
if isinstance(od, dict): if isinstance(od, dict):
ret = OrderedDict() ret = Struct()
for k in od.keys(): for k in od.keys():
v = d.get(k) v = d.get(k)
# don't append null tags! # don't append null tags!
@@ -96,8 +97,9 @@ def sort_dict(od, d):
elif isinstance(v, list): elif isinstance(v, list):
v = [sort_dict(od[k][0], v1) for v1 in v] v = [sort_dict(od[k][0], v1) for v1 in v]
ret[k] = v ret[k] = v
if hasattr(od, 'namespace'): if hasattr(od, 'namespaces'):
ret.namespace = od.namespace ret.namespaces.update(od.namespaces)
ret.references.update(od.references)
ret.qualified = od.qualified ret.qualified = od.qualified
return ret return ret
else: else:
@@ -116,15 +118,21 @@ def make_key(element_name, element_type, namespace):
return (element_name, eltype, namespace) return (element_name, eltype, namespace)
def process_element(elements, element_name, node, element_type, xsd_uri, dialect, namespace, qualified=None, def process_element(elements, element_name, node, element_type, xsd_uri,
soapenc_uri = 'http://schemas.xmlsoap.org/soap/encoding/'): dialect, namespace, qualified=None,
"""Parse and define simple element types""" soapenc_uri='http://schemas.xmlsoap.org/soap/encoding/',
struct=None):
"""Parse and define simple element types as Struct objects"""
log.debug('Processing element %s %s' % (element_name, element_type)) log.debug('Processing element %s %s' % (element_name, element_type))
# iterate over inner tags of the element definition:
for tag in node: for tag in node:
# sanity checks (skip superfluous xml tags, resolve aliases, etc.):
if tag.get_local_name() in ('annotation', 'documentation'): if tag.get_local_name() in ('annotation', 'documentation'):
continue continue
elif tag.get_local_name() in ('element', 'restriction'): elif tag.get_local_name() in ('element', 'restriction', 'list'):
log.debug('%s has no children! %s' % (element_name, tag)) log.debug('%s has no children! %s' % (element_name, tag))
children = tag # element "alias"? children = tag # element "alias"?
alias = True alias = True
@@ -134,11 +142,21 @@ def process_element(elements, element_name, node, element_type, xsd_uri, dialect
else: else:
log.debug('%s has no children! %s' % (element_name, tag)) log.debug('%s has no children! %s' % (element_name, tag))
continue # TODO: abstract? continue # TODO: abstract?
d = OrderedDict()
d.namespace = namespace # check if extending a previous processed element ("extension"):
d.qualified = qualified new_struct = struct is None
if new_struct:
struct = Struct()
struct.namespaces[None] = namespace # set the default namespace
struct.qualified = qualified
# iterate over the element's components (sub-elements):
for e in children: for e in children:
# extract type information from xml attributes / children:
t = e['type'] t = e['type']
if not t:
t = e['itemType'] # xs:list
if not t: if not t:
t = e['base'] # complexContent (extension)! t = e['base'] # complexContent (extension)!
if not t: if not t:
@@ -147,37 +165,58 @@ def process_element(elements, element_name, node, element_type, xsd_uri, dialect
# "anonymous" elements had no type attribute but children # "anonymous" elements had no type attribute but children
if e['name'] and e.children(): if e['name'] and e.children():
# create a type name to process the children # create a type name to process the children
t = "%s_%s" % (element_name, e['name']) t = "%s_%s" % (element_name, e['name'])
c = e.children() c = e.children()
et = c.get_local_name() et = c.get_local_name()
c = c.children() c = c.children()
process_element(elements, t, c, et, xsd_uri, dialect, namespace, qualified) process_element(elements, t, c, et, xsd_uri, dialect,
namespace, qualified)
else: else:
t = 'anyType' # no type given! t = 'anyType' # no type given!
# extract namespace uri and type from xml attribute:
t = t.split(":") t = t.split(":")
if len(t) > 1: if len(t) > 1:
ns, type_name = t ns, type_name = t
else: else:
ns, type_name = None, t[0] ns, type_name = None, t[0]
if element_name == type_name and not alias and len(children) > 1:
continue # abort to prevent infinite recursion
uri = ns and e.get_namespace_uri(ns) or xsd_uri uri = ns and e.get_namespace_uri(ns) or xsd_uri
# look for the conversion function (python type)
if uri in (xsd_uri, soapenc_uri) and type_name != 'Array': if uri in (xsd_uri, soapenc_uri) and type_name != 'Array':
# look for the type, None == any # look for the type, None == any
fn = REVERSE_TYPE_MAP.get(type_name, None) fn = REVERSE_TYPE_MAP.get(type_name, None)
elif uri == soapenc_uri and type_name == 'Array': if tag.get_local_name() == 'list':
# simple list type (values separated by spaces)
fn = lambda s: [fn(v) for v in s.split(" ")]
elif (uri == soapenc_uri and type_name == 'Array'):
# arrays of simple types (look at the attribute tags): # arrays of simple types (look at the attribute tags):
fn = [] fn = []
for a in e.children(): for a in e.children():
for k, v in a[:]: for k, v in a[:]:
if k.endswith(":arrayType"): if k.endswith(":arrayType"):
type_name = v type_name = v
fn_namespace = None
if ":" in type_name: if ":" in type_name:
type_name = type_name[type_name.index(":")+1:] fn_uri, type_name = type_name.split(":")
fn_namespace = e.get_namespace_uri(fn_uri)
if "[]" in type_name: if "[]" in type_name:
type_name = type_name[:type_name.index("[]")] type_name = type_name[:type_name.index("[]")]
fn.append(REVERSE_TYPE_MAP.get(type_name, None)) # get the scalar conversion function (if any)
fn_array = REVERSE_TYPE_MAP.get(type_name, None)
if fn_array is None and type_name != "anyType" and fn_namespace:
# get the complext element:
ref_type = "complexType"
key = make_key(type_name, ref_type, fn_namespace)
fn_complex = elements.setdefault(key, Struct(key))
# create an indirect struct {type_name: ...}:
fn_array = Struct(key)
fn_array[type_name] = fn_complex
fn_array.namespaces[None] = fn_namespace # set the default namespace
fn_array.qualified = qualified
fn.append(fn_array)
else: else:
# not a simple python type / conversion function not available
fn = None fn = None
if not fn: if not fn:
@@ -189,87 +228,149 @@ def process_element(elements, element_name, node, element_type, xsd_uri, dialect
for k, v in e[:]: for k, v in e[:]:
if k.startswith("xmlns:"): if k.startswith("xmlns:"):
# get the namespace uri from the element # get the namespace uri from the element
fn_namespace = v fn_namespace = v
fn = elements.setdefault(make_key(type_name, 'complexType', fn_namespace), OrderedDict()) # create and store an empty python element (dict) filled later
if not e['ref']:
ref_type = "complexType"
else:
ref_type = "element"
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 (uri == soapenc_uri and type_name == 'Array'):
# it's an array... TODO: compound arrays? and check ns uri! # it's an array... TODO: compound arrays? and check ns uri!
if isinstance(fn, OrderedDict): if isinstance(fn, Struct):
if len(children) > 1 and dialect in ('jetty',): if len(children) > 1 or (dialect in ('jetty', )):
# Jetty style support # Jetty style support
# {'ClassName': [{'attr1': val1, 'attr2': val2}] # {'ClassName': [{'attr1': val1, 'attr2': val2}]
fn.array = True fn.array = True
else: else:
# .NET style support (backward compatibility) # .NET style now matches Jetty style
# [{'ClassName': {'attr1': val1, 'attr2': val2}] # {'ClassName': [{'attr1': val1, 'attr2': val2}]
d.array = True #fn.array = True
#struct.array = True
fn = [fn]
else: else:
if dialect in ('jetty',): if len(children) > 1 or dialect in ('jetty',):
# scalar support [{'attr1': [val1]}] # Jetty style support
# scalar array support {'attr1': [val1]}
fn = [fn] fn = [fn]
else: else:
d.array = True # Jetty.NET style support (backward compatibility)
# scalar array support [{'attr1': val1}]
struct.array = True
# store the sub-element python type (function) in the element dict
if (e['name'] is not None and not alias) or e['ref']: if (e['name'] is not None and not alias) or e['ref']:
e_name = e['name'] or type_name # for refs, use the type name e_name = e['name'] or type_name # for refs, use the type name
d[e_name] = fn struct[e_name] = fn
struct.references[e_name] = e['ref']
struct.namespaces[e_name] = namespace # set the element namespace
else: else:
log.debug('complexContent/simpleType/element %s = %s' % (element_name, type_name)) log.debug('complexContent/simpleType/element %s = %s' % (element_name, type_name))
d[None] = fn # use None to point this is a complex element reference
struct.refers_to = fn
if e is not None and e.get_local_name() == 'extension' and e.children(): if e is not None and e.get_local_name() == 'extension' and e.children():
# extend base element (if ComplexContent only!):
if isinstance(fn, Struct) and fn.refers_to:
base_struct = fn.refers_to
else:
# TODO: check if this actually works for SimpleContent
base_struct = None
# extend base element: # extend base element:
process_element(elements, element_name, e.children(), element_type, xsd_uri, dialect, namespace, qualified) process_element(elements, element_name, e.children(),
elements.setdefault(make_key(element_name, element_type, namespace), OrderedDict()).update(d) element_type, xsd_uri, dialect, namespace,
qualified, struct=base_struct)
# 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(key)).update(struct)
def postprocess_element(elements, processed): def postprocess_element(elements, processed):
"""Fix unresolved references (elements referenced before its definition, thanks .net)""" """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: # avoid already processed elements:
if elements in processed: if elements in processed:
return return
processed.append(elements) processed.append(elements)
for k, v in elements.items(): for k, v in elements.items():
if isinstance(v, OrderedDict): if isinstance(v, Struct):
if v != elements: # TODO: fix recursive elements if v != elements: # TODO: fix recursive elements
postprocess_element(v, processed) try:
if None in v and v[None]: # extension base? postprocess_element(v, processed)
if isinstance(v[None], dict): except RuntimeError as e: # maximum recursion depth exceeded
for i, kk in enumerate(v[None]): warnings.warn(unicode(e), RuntimeWarning)
# extend base -keep orginal order- if v.refers_to: # extension base?
if v[None] is not None: if isinstance(v.refers_to, dict):
elements[k].insert(kk, v[None][kk], i) extend_element(v, v.refers_to)
del v[None] # clean the reference:
v.refers_to = None
else: # "alias", just replace else: # "alias", just replace
log.debug('Replacing %s = %s' % (k, v[None])) ##log.debug('Replacing %s = %s' % (k, v.refers_to))
elements[k] = v[None] elements[k] = v.refers_to
#break
if v.array: if v.array:
elements[k] = [v] # convert arrays to python lists elements[k] = [v] # convert arrays to python lists
if isinstance(v, list): if isinstance(v, list):
for n in v: # recurse list for n in v: # recurse list
if isinstance(n, (OrderedDict, list)): if isinstance(n, (Struct, list)):
#if n != elements: # TODO: fix recursive elements #if n != elements: # TODO: fix recursive elements
postprocess_element(n, processed) 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): def get_message(messages, message_name, part_name, parameter_order=None):
if part_name: if part_name:
# get the specific part of the message: # get the specific part of the message:
return messages.get((message_name, part_name)) return messages.get((message_name, part_name))
else: else:
# get the first part for the specified message: # get the first part for the specified message:
parts = {}
for (message_name_key, part_name_key), message in messages.items(): for (message_name_key, part_name_key), message in messages.items():
if message_name_key == message_name: if message_name_key == message_name:
return message parts[part_name_key] = message
if len(parts)>1:
# merge (sorted by parameter_order for rpc style)
new_msg = None
for part_name_key in parameter_order:
part = parts.get(part_name_key)
if not part:
log.error('Part %s not found for %s' % (part_name_key, message_name))
elif not new_msg:
new_msg = part.copy()
else:
new_msg[message_name].update(part[message_name])
return new_msg
elif parts:
return list(parts.values())[0]
#return parts.values()[0]
get_local_name = lambda s: s and str((':' in s) and s.split(':')[1] or s) get_local_name = lambda s: s and str((':' in s) and s.split(':')[1] or s)
get_namespace_prefix = lambda s: s and str((':' in s) and s.split(':')[0] or None) get_namespace_prefix = lambda s: s and str((':' in s) and s.split(':')[0] or None)
def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces=None, qualified=False): def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect,
http, cache, force_download, wsdl_basedir,
global_namespaces=None, qualified=False):
"""Find schema elements and complex types""" """Find schema elements and complex types"""
from .simplexml import SimpleXMLElement # here to avoid recursive imports from .simplexml import SimpleXMLElement # here to avoid recursive imports
@@ -290,7 +391,7 @@ def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http
for ns in local_namespaces.values(): for ns in local_namespaces.values():
if ns not in global_namespaces: if ns not in global_namespaces:
global_namespaces[ns] = 'ns%s' % len(global_namespaces) global_namespaces[ns] = 'ns%s' % len(global_namespaces)
for element in schema.children() or []: for element in schema.children() or []:
if element.get_local_name() in ('import', 'include',): if element.get_local_name() in ('import', 'include',):
schema_namespace = element['namespace'] schema_namespace = element['namespace']
@@ -306,9 +407,15 @@ def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http
# Open uri and read xml: # Open uri and read xml:
xml = fetch(schema_location, http, cache, force_download, wsdl_basedir) xml = fetch(schema_location, http, cache, force_download, wsdl_basedir)
# recalculate base path for relative schema locations
path = os.path.normpath(os.path.join(wsdl_basedir, schema_location))
path = os.path.dirname(path)
# Parse imported XML schema (recursively): # Parse imported XML schema (recursively):
imported_schema = SimpleXMLElement(xml, namespace=xsd_uri) imported_schema = SimpleXMLElement(xml, namespace=xsd_uri)
preprocess_schema(imported_schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces, qualified) preprocess_schema(imported_schema, imported_schemas, elements,
xsd_uri, dialect, http, cache, force_download,
path, global_namespaces, qualified)
element_type = element.get_local_name() element_type = element.get_local_name()
if element_type in ('element', 'complexType', "simpleType"): if element_type in ('element', 'complexType', "simpleType"):
@@ -319,7 +426,9 @@ def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http
if element.get_local_name() == 'complexType': if element.get_local_name() == 'complexType':
children = element.children() children = element.children()
elif element.get_local_name() == 'simpleType': elif element.get_local_name() == 'simpleType':
children = element('restriction', ns=xsd_uri) children = element('restriction', ns=xsd_uri, error=False)
if not children:
children = element.children() # xs:list
elif element.get_local_name() == 'element' and element['type']: elif element.get_local_name() == 'element' and element['type']:
children = element children = element
else: else:
@@ -329,7 +438,8 @@ def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http
elif element.get_local_name() == 'element': elif element.get_local_name() == 'element':
children = element children = element
if children: if children:
process_element(elements, element_name, children, element_type, xsd_uri, dialect, namespace, qualified) process_element(elements, element_name, children, element_type,
xsd_uri, dialect, namespace, qualified)
# simplexml utilities: # simplexml utilities:
@@ -347,10 +457,28 @@ def datetime_u(s):
return _strptime(s, fmt) return _strptime(s, fmt)
except ValueError: except ValueError:
try: try:
# strip utc offset # strip zulu timezone suffix or utc offset
if s[-3] == ":" and s[-6] in (' ', '-', '+'): if s[-1] == "Z" or (s[-3] == ":" and s[-6] in (' ', '-', '+')):
warnings.warn('removing unsupported UTC offset', RuntimeWarning) try:
s = s[:-6] import iso8601
return iso8601.parse_date(s)
except ImportError:
pass
try:
import isodate
return isodate.parse_datetime(s)
except ImportError:
pass
try:
import dateutil.parser
return dateutil.parser.parse(s)
except ImportError:
pass
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 # parse microseconds
try: try:
return _strptime(s, fmt + ".%f") return _strptime(s, fmt + ".%f")
@@ -363,6 +491,7 @@ def datetime_u(s):
s = s[:s.index(".")] s = s[:s.index(".")]
return _strptime(s, fmt) return _strptime(s, fmt)
datetime_m = lambda dt: dt.isoformat() datetime_m = lambda dt: dt.isoformat()
date_u = lambda s: _strptime(s[0:10], "%Y-%m-%d").date() date_u = lambda s: _strptime(s[0:10], "%Y-%m-%d").date()
date_m = lambda d: d.strftime("%Y-%m-%d") date_m = lambda d: d.strftime("%Y-%m-%d")
@@ -370,7 +499,8 @@ time_u = lambda s: _strptime(s, "%H:%M:%S").time()
time_m = lambda d: d.strftime("%H%M%S") time_m = lambda d: d.strftime("%H%M%S")
bool_u = lambda s: {'0': False, 'false': False, '1': True, 'true': True}[s] bool_u = lambda s: {'0': False, 'false': False, '1': True, 'true': True}[s]
bool_m = lambda s: {False: 'false', True: 'true'}[s] bool_m = lambda s: {False: 'false', True: 'true'}[s]
decimal_m = lambda d: '{0:f}'.format(d)
float_m = lambda f: '{0:.10f}'.format(f)
# aliases: # aliases:
class Alias(object): class Alias(object):
@@ -383,6 +513,31 @@ class Alias(object):
def __repr__(self): def __repr__(self):
return "<alias '%s' for '%s'>" % (self.xml_type, self.py_type) return "<alias '%s' for '%s'>" % (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': if sys.version > '3':
long = Alias(int, 'long') long = Alias(int, 'long')
byte = Alias(str, 'byte') byte = Alias(str, 'byte')
@@ -392,8 +547,10 @@ integer = Alias(long, 'integer')
DateTime = datetime.datetime DateTime = datetime.datetime
Date = datetime.date Date = datetime.date
Time = datetime.time Time = datetime.time
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 = { TYPE_MAP = {
unicode: 'string', unicode: 'string',
bool: 'boolean', bool: 'boolean',
@@ -407,15 +564,22 @@ TYPE_MAP = {
Decimal: 'decimal', Decimal: 'decimal',
datetime.datetime: 'dateTime', datetime.datetime: 'dateTime',
datetime.date: 'date', datetime.date: 'date',
datetime.time: 'time',
duration: 'duration',
any_uri: 'anyURI',
} }
TYPE_MARSHAL_FN = { TYPE_MARSHAL_FN = {
datetime.datetime: datetime_m, datetime.datetime: datetime_m,
datetime.date: date_m, datetime.date: date_m,
bool: bool_m datetime.time: time_m,
float: float_m,
Decimal: decimal_m,
bool: bool_m,
} }
TYPE_UNMARSHAL_FN = { TYPE_UNMARSHAL_FN = {
datetime.datetime: datetime_u, datetime.datetime: datetime_u,
datetime.date: date_u, datetime.date: date_u,
datetime.time: time_u,
bool: bool_u, bool: bool_u,
str: unicode, str: unicode,
} }
@@ -424,19 +588,27 @@ REVERSE_TYPE_MAP = dict([(v, k) for k, v in TYPE_MAP.items()])
REVERSE_TYPE_MAP.update({ REVERSE_TYPE_MAP.update({
'base64Binary': str, 'base64Binary': str,
'unsignedByte': byte,
'unsignedInt': int,
'unsignedLong': long,
'unsignedShort': short
}) })
# insert str here to avoid collision in REVERSE_TYPE_MAP (i.e. decoding errors) # insert str here to avoid collision in REVERSE_TYPE_MAP (i.e. decoding errors)
if str not in TYPE_MAP: if str not in TYPE_MAP:
TYPE_MAP[str] = 'string' TYPE_MAP[str] = 'string'
class OrderedDict(dict): class Struct(dict):
"""Minimal ordered dictionary for xsd:sequences""" """Minimal ordered dictionary to represent elements (i.e. xsd:sequences)"""
def __init__(self):
def __init__(self, key=None):
self.key = key
self.__keys = [] self.__keys = []
self.array = False self.array = False
self.namespace = None self.namespaces = {} # key: element, value: namespace URI
self.references = {} # key: element, value: reference name
self.refers_to = None # "symbolic linked" struct
self.qualified = None self.qualified = None
def __setitem__(self, key, value): def __setitem__(self, key, value):
@@ -464,26 +636,71 @@ class OrderedDict(dict):
return [(key, self[key]) for key in self.__keys] return [(key, self[key]) for key in self.__keys]
def update(self, other): def update(self, other):
if isinstance(other, Struct) and other.key:
self.key = other.key
for k, v in other.items(): for k, v in other.items():
self[k] = v self[k] = v
# do not change if we are an array but the other is not: # do not change if we are an array but the other is not:
if isinstance(other, OrderedDict) and not self.array: if isinstance(other, Struct) and not self.array:
self.array = other.array self.array = other.array
if isinstance(other, OrderedDict) and not self.namespace: if isinstance(other, Struct):
self.namespace = other.namespace # TODO: check replacing default ns is a regression
self.namespaces.update(other.namespaces)
self.references.update(other.references)
self.qualified = other.qualified self.qualified = other.qualified
self.refers_to = other.refers_to
def copy(self): def copy(self):
"Make a duplicate" "Make a duplicate"
new = OrderedDict() new = Struct(self.key)
new.update(self) new.update(self)
return new 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): def __str__(self):
return "%s" % dict.__str__(self) return "%s" % dict.__str__(self)
def __repr__(self): def __repr__(self):
s = "{%s}" % ", ".join(['%s: %s' % (repr(k), repr(v)) for k, v in self.items()]) if not self.key: return str(self.keys())
if self.array and False: s = '%s' % self.key[0]
s = "[%s]" % s 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 return s
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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 MERCHANTIBILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
"""Pythonic simple SOAP Client plugins"""
from __future__ import unicode_literals
import sys
if sys.version > '3':
basestring = unicode = str
import datetime
from decimal import Decimal
import os
import logging
import hashlib
import warnings
from . import __author__, __copyright__, __license__, __version__
class WSSE:
def preprocess(self, request):
header = request('Header')
def postprocess(response):
return response
+33 -9
View File
@@ -6,7 +6,7 @@
# version. # version.
# #
# This program is distributed in the hope that it will be useful, but # 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 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details. # for more details.
@@ -39,6 +39,13 @@ log = logging.getLogger(__name__)
NS_RX = re.compile(r'xmlns:(\w+)="(.+?)"') NS_RX = re.compile(r'xmlns:(\w+)="(.+?)"')
class SoapFault(Exception):
def __init__(self, faultcode=None, faultstring=None, detail=None):
self.faultcode = faultcode or self.__class__.__name__
self.faultstring = faultstring or ''
self.detail = detail
class SoapDispatcher(object): class SoapDispatcher(object):
"""Simple Dispatcher for SOAP Server""" """Simple Dispatcher for SOAP Server"""
@@ -115,6 +122,9 @@ class SoapDispatcher(object):
def register_function(self, name, fn, returns=None, args=None, doc=None): def register_function(self, name, fn, returns=None, args=None, doc=None):
self.methods[name] = fn, returns, args, doc or getattr(fn, "__doc__", "") self.methods[name] = fn, returns, args, doc or getattr(fn, "__doc__", "")
def response_element_name(self, method):
return '%sResponse' % method
def dispatch(self, xml, action=None, fault=None): def dispatch(self, xml, action=None, fault=None):
"""Receive and process SOAP call, returns the xml""" """Receive and process SOAP call, returns the xml"""
# a dict can be sent in fault to expose it to the caller # a dict can be sent in fault to expose it to the caller
@@ -137,7 +147,8 @@ class SoapDispatcher(object):
# detect soap prefix and uri (xmlns attributes of Envelope) # detect soap prefix and uri (xmlns attributes of Envelope)
for k, v in request[:]: for k, v in request[:]:
if v in ("http://schemas.xmlsoap.org/soap/envelope/", if v in ("http://schemas.xmlsoap.org/soap/envelope/",
"http://www.w3.org/2003/05/soap-env",): "http://www.w3.org/2003/05/soap-env",
"http://www.w3.org/2003/05/soap-envelope",):
soap_ns = request.attributes()[k].localName soap_ns = request.attributes()[k].localName
soap_uri = request.attributes()[k].value soap_uri = request.attributes()[k].value
@@ -184,13 +195,20 @@ class SoapDispatcher(object):
ret = function(**args) ret = function(**args)
log.debug('dispathed method returns: %s', ret) log.debug('dispathed method returns: %s', ret)
except SoapFault as e:
fault.update({
'faultcode': "%s.%s" % (soap_fault_code, e.faultcode),
'faultstring': e.faultstring,
'detail': e.detail
})
except Exception: # This shouldn't be one huge try/except except Exception: # This shouldn't be one huge try/except
import sys import sys
etype, evalue, etb = sys.exc_info() etype, evalue, etb = sys.exc_info()
log.error(traceback.format_exc()) log.error(traceback.format_exc())
if self.debug: if self.debug:
detail = ''.join(traceback.format_exception(etype, evalue, etb)) detail = u''.join(traceback.format_exception(etype, evalue, etb))
detail += '\n\nXML REQUEST\n\n' + xml detail += u'\n\nXML REQUEST\n\n' + xml.decode('UTF-8')
else: else:
detail = None detail = None
fault.update({'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), fault.update({'faultcode': "%s.%s" % (soap_fault_code, etype.__name__),
@@ -235,7 +253,7 @@ class SoapDispatcher(object):
body.marshall("%s:Fault" % soap_ns, fault, ns=False) body.marshall("%s:Fault" % soap_ns, fault, ns=False)
else: else:
# return normal value # return normal value
res = body.add_child("%sResponse" % name, ns=self.namespace) res = body.add_child(self.response_element_name(name), ns=self.namespace)
if not prefix: if not prefix:
res['xmlns'] = self.namespace # add target namespace res['xmlns'] = self.namespace # add target namespace
@@ -251,7 +269,7 @@ class SoapDispatcher(object):
"%s vs %s" % (str(returns_types), str(ret))) "%s vs %s" % (str(returns_types), str(ret)))
if not complex_type or not types_ok: if not complex_type or not types_ok:
# backward compatibility for scalar and simple types # backward compatibility for scalar and simple types
res.marshall(returns_types.keys()[0], ret, ) res.marshall(list(returns_types.keys())[0], ret, )
else: else:
# new style for complex classes # new style for complex classes
for k, v in ret.items(): for k, v in ret.items():
@@ -367,7 +385,7 @@ class SoapDispatcher(object):
parse_element(n, v.items(), complex=True) parse_element(n, v.items(), complex=True)
t = "tns:%s" % n t = "tns:%s" % n
else: else:
raise TypeError("unknonw type v for marshalling" % str(v)) raise TypeError("unknonw type %s for marshalling" % str(v))
e.add_attribute('type', t) e.add_attribute('type', t)
parse_element("%s" % method, args and args.items()) parse_element("%s" % method, args and args.items())
@@ -452,7 +470,13 @@ class SOAPHandler(BaseHTTPRequestHandler):
def do_POST(self): def do_POST(self):
"""SOAP POST gateway""" """SOAP POST gateway"""
request = self.rfile.read(int(self.headers.getheader('content-length'))) request = self.rfile.read(int(self.headers.get('content-length')))
# convert xml request to unicode (according to request headers)
if sys.version < '3':
encoding = self.headers.getparam("charset")
else:
encoding = self.headers.get_param("charset")
request = request.decode(encoding)
fault = {} fault = {}
# execute the method # execute the method
response = self.server.dispatcher.dispatch(request, fault=fault) response = self.server.dispatcher.dispatch(request, fault=fault)
@@ -576,7 +600,7 @@ if __name__ == "__main__":
namespace="http://example.com/sample.wsdl", namespace="http://example.com/sample.wsdl",
soap_ns='soap', soap_ns='soap',
trace=True, trace=True,
ns=False ns="ns0",
) )
p = {'a': 1, 'b': 2} p = {'a': 1, 'b': 2}
c = [{'d': '1.20'}, {'d': '2.01'}] c = [{'d': '1.20'}, {'d': '2.01'}]
+79 -29
View File
@@ -6,7 +6,7 @@
# version. # version.
# #
# This program is distributed in the hope that it will be useful, but # 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 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details. # for more details.
@@ -28,7 +28,7 @@ from . import __author__, __copyright__, __license__, __version__
# Utility functions used for marshalling, moved aside for readability # Utility functions used for marshalling, moved aside for readability
from .helpers import TYPE_MAP, TYPE_MARSHAL_FN, TYPE_UNMARSHAL_FN, \ from .helpers import TYPE_MAP, TYPE_MARSHAL_FN, TYPE_UNMARSHAL_FN, \
REVERSE_TYPE_MAP, OrderedDict, Date, Decimal REVERSE_TYPE_MAP, Struct, Date, Decimal
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -79,7 +79,10 @@ class SimpleXMLElement(object):
element = self.__document.createElementNS(self.__ns, name) element = self.__document.createElementNS(self.__ns, name)
# don't append null tags! # don't append null tags!
if text is not None: if text is not None:
element.appendChild(self.__document.createTextNode(text)) if isinstance(text, xml.dom.minidom.CDATASection):
element.appendChild(self.__document.createCDATASection(text.data))
else:
element.appendChild(self.__document.createTextNode(text))
self._element.appendChild(element) self._element.appendChild(element)
return SimpleXMLElement( return SimpleXMLElement(
elements=[element], elements=[element],
@@ -117,10 +120,15 @@ class SimpleXMLElement(object):
else: else:
return self.__document.toprettyxml(encoding='UTF-8') return self.__document.toprettyxml(encoding='UTF-8')
def __repr__(self): if sys.version > '3':
"""Return the XML representation of this tag""" def __repr__(self):
# NOTE: do not use self.as_xml('UTF-8') as it returns the whole xml doc """Return the XML representation of this tag"""
return self._element.toxml('UTF-8') return self._element.toxml()
else:
def __repr__(self):
"""Return the XML representation of this tag"""
# NOTE: do not use self.as_xml('UTF-8') as it returns the whole xml doc
return self._element.toxml('UTF-8')
def get_name(self): def get_name(self):
"""Return the tag name of this node""" """Return the tag name of this node"""
@@ -182,6 +190,10 @@ class SimpleXMLElement(object):
for k, v in value.items(): for k, v in value.items():
self.add_attribute(k, v) self.add_attribute(k, v)
def __delitem__(self, item):
"Remove an attribute"
self._element.removeAttribute(item)
def __call__(self, tag=None, ns=None, children=False, root=False, def __call__(self, tag=None, ns=None, children=False, root=False,
error=True, ): error=True, ):
"""Search (even in child nodes) and return a child tag by name""" """Search (even in child nodes) and return a child tag by name"""
@@ -284,17 +296,17 @@ class SimpleXMLElement(object):
def __unicode__(self): def __unicode__(self):
"""Returns the unicode text nodes of the current element""" """Returns the unicode text nodes of the current element"""
if self._element.childNodes: rc = ''
rc = "" for node in self._element.childNodes:
for node in self._element.childNodes: if node.nodeType == node.TEXT_NODE or node.nodeType == node.CDATA_SECTION_NODE:
if node.nodeType == node.TEXT_NODE: rc = rc + node.data
rc = rc + node.data return rc
return rc
return ''
def __str__(self): if sys.version > '3':
"""Returns the str text nodes of the current element""" __str__ = __unicode__
return self.__unicode__() else:
def __str__(self):
return self.__unicode__().encode('utf-8')
def __int__(self): def __int__(self):
"""Returns the integer value of the current element""" """Returns the integer value of the current element"""
@@ -313,7 +325,7 @@ class SimpleXMLElement(object):
#import pdb; pdb.set_trace() #import pdb; pdb.set_trace()
"""Convert to python values the current serialized xml element""" """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 # strict=False to use default type conversion if not specified
# example: types={'p': {'a': int,'b': int}, 'c': [{'d':str}]} # example: types={'p': {'a': int,'b': int}, 'c': [{'d':str}]}
# expected xml: <p><a>1</a><b>2</b></p><c><d>hola</d><d>chau</d> # expected xml: <p><a>1</a><b>2</b></p><c><d>hola</d><d>chau</d>
@@ -329,7 +341,7 @@ class SimpleXMLElement(object):
if ref_node['id'] == href: if ref_node['id'] == href:
node = ref_node node = ref_node
ref_name_type = ref_node['xsi:type'].split(":")[1] ref_name_type = ref_node['xsi:type'].split(":")[1]
break break
try: try:
if isinstance(types, dict): if isinstance(types, dict):
@@ -341,12 +353,29 @@ class SimpleXMLElement(object):
else: else:
fn = types fn = types
except (KeyError, ) as e: except (KeyError, ) as e:
xmlns = node['xmlns'] or node.get_namespace_uri(node.get_prefix())
if 'xsi:type' in node.attributes().keys(): if 'xsi:type' in node.attributes().keys():
xsd_type = node['xsi:type'].split(":")[1] xsd_type = node['xsi:type'].split(":")[1]
try: try:
fn = REVERSE_TYPE_MAP[xsd_type] # get fn type from SOAP-ENC:arrayType="xsd:string[28]"
if xsd_type == 'Array':
array_type = [k for k,v in node[:] if 'arrayType' in k][0]
xsd_type = node[array_type].split(":")[1]
if "[" in xsd_type:
xsd_type = xsd_type[:xsd_type.index("[")]
fn = [REVERSE_TYPE_MAP[xsd_type]]
else:
fn = REVERSE_TYPE_MAP[xsd_type]
except: except:
fn = None # ignore multirefs! fn = None # ignore multirefs!
elif xmlns == "http://www.w3.org/2001/XMLSchema":
# self-defined schema, return the SimpleXMLElement
# TODO: parse to python types if <s:element ref="s:schema"/>
fn = None
elif None in types:
# <s:any/>, return the SimpleXMLElement
# TODO: check position of None if inside <s:sequence>
fn = None
elif strict: elif strict:
raise TypeError("Tag: %s invalid (type not found)" % (name,)) raise TypeError("Tag: %s invalid (type not found)" % (name,))
else: else:
@@ -356,7 +385,9 @@ class SimpleXMLElement(object):
if isinstance(fn, list): if isinstance(fn, list):
# append to existing list (if any) - unnested dict arrays - # append to existing list (if any) - unnested dict arrays -
value = d.setdefault(name, []) 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) # TODO: check if this was really needed (get first child only)
##if len(fn[0]) == 1 and children: ##if len(fn[0]) == 1 and children:
## children = children() ## children = children()
@@ -365,14 +396,15 @@ class SimpleXMLElement(object):
for child in (children or []): for child in (children or []):
tmp_dict = child.unmarshall(fn[0], strict) tmp_dict = child.unmarshall(fn[0], strict)
value.extend(tmp_dict.values()) value.extend(tmp_dict.values())
elif (self.__jetty and len(fn[0]) > 1): #elif (self.__jetty and len(fn[0]) > 1):
# Jetty array style support [{k, v}] elif (len(fn[0]) > 1):
# Jetty and now all dialects use array style support [{k, v}]
for parent in node: for parent in node:
tmp_dict = {} # unmarshall each value & mix tmp_dict = {} # unmarshall each value & mix
for child in (node.children() or []): for child in (node.children() or []):
tmp_dict.update(child.unmarshall(fn[0], strict)) tmp_dict.update(child.unmarshall(fn[0], strict))
value.append(tmp_dict) value.append(tmp_dict)
else: # .Net / Java else: # len(fn[0]) == 0
for child in (children or []): for child in (children or []):
value.append(child.unmarshall(fn[0], strict)) value.append(child.unmarshall(fn[0], strict))
@@ -446,9 +478,12 @@ class SimpleXMLElement(object):
for k, v in value.items(): for k, v in value.items():
if not add_children_ns: if not add_children_ns:
ns = False ns = False
else: elif hasattr(value, 'namespaces'):
# for children, use the wsdl element target namespace: # for children, use the wsdl element target namespace:
ns = getattr(value, 'namespace', None) ns = value.namespaces.get(k)
else:
# simple type
ns = None
child.marshall(k, v, add_comments=add_comments, ns=ns) child.marshall(k, v, add_comments=add_comments, ns=ns)
elif isinstance(value, tuple): # serialize tuple (<key>value</key>) elif isinstance(value, tuple): # serialize tuple (<key>value</key>)
child = add_child and self.add_child(name, ns=ns) or self child = add_child and self.add_child(name, ns=ns) or self
@@ -456,15 +491,23 @@ class SimpleXMLElement(object):
ns = False ns = False
for k, v in value: for k, v in value:
getattr(self, name).marshall(k, v, add_comments=add_comments, ns=ns) 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) child = self.add_child(name, ns=ns)
if not add_children_ns: if not add_children_ns:
ns = False ns = False
if add_comments: if add_comments:
child.add_comment("Repetitive array of:") 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) child.marshall(name, t, False, add_comments=add_comments, ns=ns)
elif isinstance(value, basestring): # do not convert strings or unicodes # "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) self.add_child(name, value, ns=ns)
elif value is None: # sent a empty tag? elif value is None: # sent a empty tag?
self.add_child(name, ns=ns) self.add_child(name, ns=ns)
@@ -480,3 +523,10 @@ class SimpleXMLElement(object):
def import_node(self, other): def import_node(self, other):
x = self.__document.importNode(other._element, True) # deep copy x = self.__document.importNode(other._element, True) # deep copy
self._element.appendChild(x) self._element.appendChild(x)
def write_c14n(self, output=None, exclusive=True):
"Generate the canonical version of the XML node"
from . import c14n
xml = c14n.Canonicalize(self._element, output,
unsuppressedPrefixes=[] if exclusive else None)
return xml
+19 -11
View File
@@ -6,7 +6,7 @@
# version. # version.
# #
# This program is distributed in the hope that it will be useful, but # 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 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details. # for more details.
@@ -14,6 +14,7 @@
import logging import logging
import ssl
import sys import sys
try: try:
import urllib2 import urllib2
@@ -23,7 +24,7 @@ except ImportError:
from http.cookiejar import CookieJar from http.cookiejar import CookieJar
from . import __author__, __copyright__, __license__, __version__, TIMEOUT from . import __author__, __copyright__, __license__, __version__, TIMEOUT
from .simplexml import SimpleXMLElement, TYPE_MAP, OrderedDict from .simplexml import SimpleXMLElement, TYPE_MAP, Struct
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -83,14 +84,14 @@ else:
_wrapper_name = 'httplib2' _wrapper_name = 'httplib2'
def __init__(self, timeout, proxy=None, cacert=None, sessions=False): def __init__(self, timeout, proxy=None, cacert=None, sessions=False):
##httplib2.debuglevel=4 # httplib2.debuglevel=4
kwargs = {} kwargs = {}
if proxy: if proxy:
import socks import socks
kwargs['proxy_info'] = httplib2.ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, **proxy) kwargs['proxy_info'] = httplib2.ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, **proxy)
log.info("using proxy %s" % 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': if httplib2.__version__ >= '0.3.0':
kwargs['timeout'] = timeout kwargs['timeout'] = timeout
if httplib2.__version__ >= '0.7.0': if httplib2.__version__ >= '0.7.0':
@@ -121,12 +122,21 @@ class urllib2Transport(TransportBase):
raise RuntimeError('proxy is not supported with urllib2 transport') raise RuntimeError('proxy is not supported with urllib2 transport')
if cacert: if cacert:
raise RuntimeError('cacert is not support with urllib2 transport') 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: if sessions:
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) handlers.append(urllib2.HTTPCookieProcessor(CookieJar()))
self.request_opener = opener.open
opener = urllib2.build_opener(*handlers)
self.request_opener = opener.open
self._timeout = timeout self._timeout = timeout
def request(self, url, method="GET", body=None, headers={}): def request(self, url, method="GET", body=None, headers={}):
@@ -142,10 +152,8 @@ class urllib2Transport(TransportBase):
_http_connectors['urllib2'] = urllib2Transport _http_connectors['urllib2'] = urllib2Transport
_http_facilities.setdefault('sessions', []).append('urllib2') _http_facilities.setdefault('sessions', []).append('urllib2')
import sys
if sys.version_info >= (2, 6): if sys.version_info >= (2, 6):
_http_facilities.setdefault('timeout', []).append('urllib2') _http_facilities.setdefault('timeout', []).append('urllib2')
del sys
# #
# pycurl support. # pycurl support.
@@ -191,7 +199,7 @@ else:
c.setopt(c.CAINFO, self.cacert) c.setopt(c.CAINFO, self.cacert)
c.setopt(pycurl.SSL_VERIFYPEER, self.cacert and 1 or 0) 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.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) c.setopt(pycurl.TIMEOUT, self.timeout)
if method == 'POST': if method == 'POST':
c.setopt(pycurl.POST, 1) c.setopt(pycurl.POST, 1)
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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.
"""Pythonic simple SOAP Client plugins for WebService Security extensions"""
from __future__ import unicode_literals
import sys
if sys.version > '3':
basestring = unicode = str
import datetime
from decimal import Decimal
import os
import logging
import hashlib
import warnings
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'
WSU_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
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:
"WebService Security extension to add a basic credentials to xml request"
def __init__(self, username="", password=""):
self.token = {
'wsse:UsernameToken': {
'wsse:Username': username,
'wsse:Password': password,
}
}
def preprocess(self, client, request, method, args, kwargs, headers, soap_uri):
"Add basic credentials to outgoing message"
# always extract WS Security header and send it
header = request('Header', ns=soap_uri, )
k = 'wsse:Security'
# for backward compatibility, use header if given:
if k in headers:
self.token = headers[k]
# convert the token to xml
header.marshall(k, self.token, ns=False, add_children_ns=False)
header(k)['xmlns:wsse'] = WSSE_URI
#<wsse:UsernameToken xmlns:wsu='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'>
def postprocess(self, client, response, method, args, kwargs, headers, soap_uri):
"Analyze incoming credentials"
# 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 = """<?xml version="1.0" encoding="UTF-8"?>
<wsse:Security soapenv:mustUnderstand="1" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:BinarySecurityToken EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" wsu:Id="CertId-45851B081998E431E8132880700036719" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
%(certificate)s</wsse:BinarySecurityToken>
<ds:Signature Id="Signature-13" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
%(signed_info)s
<ds:SignatureValue>%(signature_value)s</ds:SignatureValue>
<ds:KeyInfo Id="KeyId-45851B081998E431E8132880700036720">
<wsse:SecurityTokenReference wsu:Id="STRId-45851B081998E431E8132880700036821" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsse:Reference URI="#CertId-45851B081998E431E8132880700036719" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
</wsse:SecurityTokenReference>
</ds:KeyInfo>
</ds:Signature>
</wsse:Security>
"""
class BinaryTokenSignature:
"WebService Security extension to add a basic signature to xml request"
def __init__(self, certificate="", private_key="", password=None, cacert=None):
# read the X509v3 certificate (PEM)
self.certificate = ''.join([line for line in open(certificate)
if not line.startswith("---")])
self.private_key = private_key
self.password = password
self.cacert = cacert
def preprocess(self, client, request, method, args, kwargs, headers, soap_uri):
"Sign the outgoing SOAP request"
# get xml elements:
body = request('Body', ns=soap_uri, )
header = request('Header', ns=soap_uri, )
# prepare body xml attributes to be signed (reference)
body['wsu:Id'] = "id-14"
body['xmlns:wsu'] = WSU_URI
# workaround: copy namespaces so lxml can parse the xml to be signed
for attr, value in request[:]:
if attr.startswith("xmlns"):
body[attr] = value
# use the internal tag xml representation (not the full xml document)
ref_xml = repr(body)
# sign using RSA-SHA1 (XML Security)
from . import xmlsec
vars = xmlsec.rsa_sign(ref_xml, "#id-14",
self.private_key, self.password)
vars['certificate'] = self.certificate
# generate the xml (filling the placeholders)
wsse = SimpleXMLElement(BIN_TOKEN_TMPL % vars)
header.import_node(wsse)
def postprocess(self, client, response, method, args, kwargs, headers, soap_uri):
"Verify the signature of the incoming response"
from . import xmlsec
# get xml elements:
body = response('Body', ns=soap_uri, )
header = response('Header', ns=soap_uri, )
wsse = header("Security", ns=WSSE_URI)
cert = wsse("BinarySecurityToken", ns=WSSE_URI)
# check that the cert (binary token) is coming in the correct format:
self.__check(cert["EncodingType"], Base64Binary_URI)
self.__check(cert["ValueType"], X509v3_URI)
# extract the certificate (in DER to avoid new line & padding issues!)
cert_der = str(cert).decode("base64")
public_key = xmlsec.x509_extract_rsa_public_key(cert_der, binary=True)
# validate the certificate using the certification authority:
if not self.cacert:
warnings.warn("No CA provided, WSSE not validating certificate")
elif not xmlsec.x509_verify(self.cacert, cert_der, binary=True):
raise RuntimeError("WSSE certificate validation failed")
# check body xml attributes was signed correctly (reference)
self.__check(body['xmlns:wsu'], WSU_URI)
ref_uri = body['wsu:Id']
signature = wsse("Signature", ns=XMLDSIG_URI)
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", ns=XMLDSIG_URI)['URI'], "#" + ref_uri)
self.__check(signed_info("SignatureMethod", ns=XMLDSIG_URI)['Algorithm'],
XMLDSIG_URI + "rsa-sha1")
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
for attr, value in response[:]:
if attr.startswith("xmlns"):
body[attr] = value
# use the internal tag xml representation (not the full xml document)
ref_xml = xmlsec.canonicalize(repr(body))
# verify the signed hash
computed_hash = xmlsec.sha1_hash_digest(ref_xml)
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)
signed_info['xmlns'] = XMLDSIG_URI
xml = repr(signed_info)
# verify the signature using RSA-SHA1 (XML Security)
ok = xmlsec.rsa_verify(xml, str(signature_value), public_key)
if not ok:
raise RuntimeError("WSSE RSA-SHA1 signature verification failed")
# TODO: remove any unsigned part from the xml?
def __check(self, value, expected, msg="WSSE sanity check failed"):
if value != expected:
raise RuntimeError(msg)
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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.
"""Pythonic XML Security Library implementation"""
from __future__ import print_function
import base64
import hashlib
import os
from cStringIO import StringIO
from M2Crypto import BIO, EVP, RSA, X509, m2
# if lxml is not installed, use c14n.py native implementation
try:
import lxml.etree
except ImportError:
lxml = None
# Features:
# * Uses M2Crypto and lxml (libxml2) but it is independent from libxmlsec1
# * Sign, Verify, Encrypt & Decrypt XML documents
# Enveloping templates ("by reference": signature is parent):
SIGN_REF_TMPL = """
<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<Reference URI="%(ref_uri)s">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>%(digest_value)s</DigestValue>
</Reference>
</SignedInfo>
"""
SIGNED_TMPL = """
<?xml version="1.0" encoding="UTF-8"?>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
%(signed_info)s
<SignatureValue>%(signature_value)s</SignatureValue>
%(key_info)s
%(ref_xml)s
</Signature>
"""
# Enveloped templates (signature is child, the reference is the root object):
SIGN_ENV_TMPL = """
<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>%(digest_value)s</DigestValue>
</Reference>
</SignedInfo>
"""
SIGNATURE_TMPL = """<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
%(signed_info)s
<SignatureValue>%(signature_value)s</SignatureValue>
%(key_info)s
</Signature>"""
KEY_INFO_RSA_TMPL = """
<KeyInfo>
<KeyValue>
<RSAKeyValue>
<Modulus>%(modulus)s</Modulus>
<Exponent>%(exponent)s</Exponent>
</RSAKeyValue>
</KeyValue>
</KeyInfo>
"""
KEY_INFO_X509_TMPL = """
<KeyInfo>
<X509Data>
<X509IssuerSerial>
<X509IssuerName>%(issuer_name)s</X509IssuerName>
<X509SerialNumber>%(serial_number)s</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</KeyInfo>
"""
def canonicalize(xml, c14n_exc=True):
"Return the canonical (c14n) form of the xml document for hashing"
# UTF8, normalization of line feeds/spaces, quoting, attribute ordering...
output = StringIO()
if lxml is not None:
# use faster libxml2 / lxml canonicalization function if available
et = lxml.etree.parse(StringIO(xml))
et.write_c14n(output, exclusive=c14n_exc)
else:
# use pure-python implementation: c14n.py (avoid recursive import)
from .simplexml import SimpleXMLElement
SimpleXMLElement(xml).write_c14n(output, exclusive=c14n_exc)
return output.getvalue()
def sha1_hash_digest(payload):
"Create a SHA1 hash and return the base64 string"
return base64.b64encode(hashlib.sha1(payload).digest())
def rsa_sign(xml, ref_uri, private_key, password=None, cert=None, c14n_exc=True,
sign_template=SIGN_REF_TMPL, key_info_template=KEY_INFO_RSA_TMPL):
"Sign an XML document usign RSA (templates: enveloped -ref- or enveloping)"
# normalize the referenced xml (to compute the SHA1 hash)
ref_xml = canonicalize(xml, c14n_exc)
# create the signed xml normalized (with the referenced uri and hash value)
signed_info = sign_template % {'ref_uri': ref_uri,
'digest_value': sha1_hash_digest(ref_xml)}
signed_info = canonicalize(signed_info, c14n_exc)
# Sign the SHA1 digest of the signed xml using RSA cipher
pkey = RSA.load_key(private_key, lambda *args, **kwargs: password)
signature = pkey.sign(hashlib.sha1(signed_info).digest())
# build the mapping (placeholders) to create the final xml signed message
return {
'ref_xml': ref_xml, 'ref_uri': ref_uri,
'signed_info': signed_info,
'signature_value': base64.b64encode(signature),
'key_info': key_info(pkey, cert, key_info_template),
}
def rsa_verify(xml, signature, key, c14n_exc=True):
"Verify a XML document signature usign RSA-SHA1, return True if valid"
# load the public key (from buffer or filename)
if key.startswith("-----BEGIN PUBLIC KEY-----"):
bio = BIO.MemoryBuffer(key)
rsa = RSA.load_pub_key_bio(bio)
else:
rsa = RSA.load_pub_key(certificate)
# create the digital envelope
pubkey = EVP.PKey()
pubkey.assign_rsa(rsa)
# do the cryptographic validation (using the default sha1 hash digest)
pubkey.reset_context(md='sha1')
pubkey.verify_init()
# normalize and feed the signed xml to be verified
pubkey.verify_update(canonicalize(xml, c14n_exc))
ret = pubkey.verify_final(base64.b64decode(signature))
return ret == 1
def key_info(pkey, cert, key_info_template):
"Convert private key (PEM) to XML Signature format (RSAKeyValue/X509Data)"
exponent = base64.b64encode(pkey.e[4:])
modulus = m2.bn_to_hex(m2.mpi_to_bn(pkey.n)).decode("hex").encode("base64")
x509 = x509_parse_cert(cert) if cert else None
return key_info_template % {
'modulus': modulus,
'exponent': exponent,
'issuer_name': x509.get_issuer().as_text() if x509 else "",
'serial_number': x509.get_serial_number() if x509 else "",
}
# Miscellaneous certificate utility functions:
def x509_parse_cert(cert, binary=False):
"Create a X509 certificate from binary DER, plain text PEM or filename"
if binary:
bio = BIO.MemoryBuffer(cert)
x509 = X509.load_cert_bio(bio, X509.FORMAT_DER)
elif cert.startswith("-----BEGIN CERTIFICATE-----"):
bio = BIO.MemoryBuffer(cert)
x509 = X509.load_cert_bio(bio, X509.FORMAT_PEM)
else:
x509 = X509.load_cert(cert, 1)
return x509
def x509_extract_rsa_public_key(cert, binary=False):
"Return the public key (PEM format) from a X509 certificate"
x509 = x509_parse_cert(cert, binary)
return x509.get_pubkey().get_rsa().as_pem()
def x509_verify(cacert, cert, binary=False):
"Validate the certificate's authenticity using a certification authority"
ca = x509_parse_cert(cacert)
crt = x509_parse_cert(cert, binary)
return crt.verify(ca.get_pubkey())
if __name__ == "__main__":
# basic test of enveloping signature (the reference is a part of the xml)
sample_xml = """<Object xmlns="http://www.w3.org/2000/09/xmldsig#" Id="object">data</Object>"""
output = canonicalize(sample_xml)
print (output)
vars = rsa_sign(sample_xml, '#object', "no_encriptada.key", "password")
print (SIGNED_TMPL % vars)
# basic test of enveloped signature (the reference is the document itself)
sample_xml = """<?xml version="1.0" encoding="UTF-8"?><Object>data%s</Object>"""
vars = rsa_sign(sample_xml % "", '', "no_encriptada.key", "password",
sign_template=SIGN_ENV_TMPL, c14n_exc=False)
print (sample_xml % (SIGNATURE_TMPL % vars))
# basic signature verification:
public_key = x509_extract_rsa_public_key(open("zunimercado.crt").read())
assert rsa_verify(vars['signed_info'], vars['signature_value'], public_key,
c14n_exc=False)
File diff suppressed because it is too large Load Diff
+16 -14
View File
@@ -16,11 +16,9 @@ mostly for testing purposes
Some examples at the bottom. Some examples at the bottom.
""" """
from __future__ import print_function from __future__ import print_function
from gluon._compat import urllib2, cookielib, iteritems, to_native, urlencode, to_bytes
import re import re
import time import time
import urllib
import urllib2
import cookielib
DEFAULT_HEADERS = { DEFAULT_HEADERS = {
@@ -85,10 +83,10 @@ class WebClient(object):
# copy headers from dict to list of key,value # copy headers from dict to list of key,value
headers_list = [] headers_list = []
for key, value in self.default_headers.iteritems(): for key, value in iteritems(self.default_headers):
if not key in headers: if not key in headers:
headers[key] = value headers[key] = value
for key, value in headers.iteritems(): for key, value in iteritems(headers):
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):
for v in value: for v in value:
headers_list.append((key, v)) headers_list.append((key, v))
@@ -96,7 +94,7 @@ class WebClient(object):
headers_list.append((key, value)) headers_list.append((key, value))
# move cookies to headers # move cookies to headers
for key, value in cookies.iteritems(): for key, value in iteritems(cookies):
headers_list.append(('Cookie', '%s=%s' % (key, value))) headers_list.append(('Cookie', '%s=%s' % (key, value)))
# add headers to request # add headers to request
@@ -120,25 +118,29 @@ class WebClient(object):
data['_formkey'] = self.forms[data['_formname']] data['_formkey'] = self.forms[data['_formname']]
# time the POST request # time the POST request
data = urllib.urlencode(data, doseq=True) data = urlencode(data, doseq=True)
else: else:
self.method = 'GET' if method=='auto' else method self.method = 'GET' if method=='auto' else method
data = None data = None
t0 = time.time() t0 = time.time()
self.response = opener.open(self.url, data) self.response = opener.open(self.url, to_bytes(data))
self.time = time.time() - t0 self.time = time.time() - t0
except urllib2.HTTPError as error: except urllib2.HTTPError as er:
error = er
# catch HTTP errors # catch HTTP errors
self.time = time.time() - t0 self.time = time.time() - t0
self.response = error self.response = er
if hasattr(self.response, 'getcode'): if hasattr(self.response, 'getcode'):
self.status = self.response.getcode() self.status = self.response.getcode()
else:#python2.5 else:#python2.5
self.status = None self.status = None
self.text = self.response.read() self.text = to_native(self.response.read())
self.headers = dict(self.response.headers) # In PY3 self.response.headers are case sensitive
self.headers = dict()
for h in self.response.headers:
self.headers[h.lower()] = self.response.headers[h]
# treat web2py tickets as special types of errors # treat web2py tickets as special types of errors
if error is not None: if error is not None:
@@ -156,7 +158,7 @@ class WebClient(object):
# check is a new session id has been issued, symptom of broken session # check is a new session id has been issued, symptom of broken session
if self.session_regex is not None: if self.session_regex is not None:
for cookie, value in self.cookies.iteritems(): for cookie, value in iteritems(self.cookies):
match = self.session_regex.match(cookie) match = self.session_regex.match(cookie)
if match: if match:
name = match.group('name') name = match.group('name')
@@ -166,7 +168,7 @@ class WebClient(object):
# find all forms and formkeys in page # find all forms and formkeys in page
self.forms = {} self.forms = {}
for match in FORM_REGEX.finditer(self.text): for match in FORM_REGEX.finditer(to_native(self.text)):
self.forms[match.group('formname')] = match.group('formkey') self.forms[match.group('formname')] = match.group('formkey')
# log this request # log this request
+2 -2
View File
@@ -110,7 +110,7 @@ class HTTP(Exception):
if status[:1] == '4': if status[:1] == '4':
if not body: if not body:
body = status body = status
if isinstance(body, str): if isinstance(body, (str, bytes, bytearray)):
headers['Content-Length'] = len(body) headers['Content-Length'] = len(body)
rheaders = [] rheaders = []
for k, v in iteritems(headers): for k, v in iteritems(headers):
@@ -121,7 +121,7 @@ class HTTP(Exception):
responder(status, rheaders) responder(status, rheaders)
if env.get('request_method', '') == 'HEAD': if env.get('request_method', '') == 'HEAD':
return [''] return ['']
elif isinstance(body, str): elif isinstance(body, (str, bytes, bytearray)):
return [body] return [body]
elif hasattr(body, '__iter__'): elif hasattr(body, '__iter__'):
return body return body
+1 -1
View File
@@ -20,9 +20,9 @@ from .test_serializers import *
from .test_languages import * from .test_languages import *
from .test_compileapp import * from .test_compileapp import *
from .test_appadmin import * from .test_appadmin import *
from .test_web import *
if sys.version[:3] == '2.7': if sys.version[:3] == '2.7':
from .test_is_url import * from .test_is_url import *
from .test_scheduler import * from .test_scheduler import *
from .test_web import *
from .test_old_doctests import * from .test_old_doctests import *
+2 -1
View File
@@ -16,7 +16,7 @@ from .fix_path import fix_sys_path
fix_sys_path(__file__) fix_sys_path(__file__)
from gluon.contrib.webclient import WebClient from gluon.contrib.webclient import WebClient
from gluon._compat import urllib2 from gluon._compat import urllib2, PY2
webserverprocess = None webserverprocess = None
@@ -110,6 +110,7 @@ class TestWeb(LiveTest):
assert('expires' in s.headers) assert('expires' in s.headers)
assert(s.headers['cache-control'].startswith('max-age')) assert(s.headers['cache-control'].startswith('max-age'))
@unittest.skipIf(not(PY2), 'skip PY3 testSoap')
def testSoap(self): def testSoap(self):
# test soap server implementation # test soap server implementation
from gluon.contrib.pysimplesoap.client import SoapClient, SoapFault from gluon.contrib.pysimplesoap.client import SoapClient, SoapFault