This post originated from an RSS feed registered with Python Buzz
by Phillip Pearson.
Original Post: I can't resist a challenge
Feed Title: Second p0st
Feed URL: http://www.myelin.co.nz/post/rss.xml
Feed Description: Tech notes and web hackery from the guy that brought you bzero, Python Community Server, the Blogging Ecosystem and the Internet Topic Exchange
I won't go into the details of how to use it right here, because I've already written them on that link above and in a big comment at the start of the code. The interesting thing is what it took to put this together.
It seems that SOAP is still not very widely supported. The Microsoft.com Web Services require you to use Microsoft and IBM's new WS-Security extension to pass your authentication information. This isn't all that hard, but it's the sort of thing that usually comes for free with your protocol library. From what I can see, .NET and one or two Java libraries support it, but anyone on another platform has to do it all by hand.
Luckily, the web service documentation had some example WS-Security headers I could use as templates. Even so, I spent several hours trying to get SOAPPy to generate the right XML in the SOAP-ENV:Header block, then eventually gave up and switched to the much lighter-weight ZSI library. ZSI doesn't do as much for you, but it also doesn't stop you from doing much. The following snippet generates the XML I need, and a quick hack to ZSI/client.py let me pass it in in place of the authentication header it was expecting.
wsse = Element('wsse:Security')
wsse.set('xmlns:wsse', 'http://schemas.xmlsoap.org/ws/2002/07/secext')
wsse.set('xmlns:wsu', 'http://schemas.xmlsoap.org/ws/2002/07/utility')
ut = SubElement(wsse, 'wsse:UsernameToken')
def mkelem(parent, tagname, text, **attrs):
if tagname.find(':') == -1:
tagname = 'wsse:' + tagname
x = SubElement(parent, tagname)
x.text = text
for k,v in attrs.items():
x.set(k, v)
mkelem(ut, 'Username', mstoken.token)
created = time.strftime('%Y-%m-%dT%H:%M:%SZ')
nonce = sha.new(str(random.random())).digest()
digest = sha.new(nonce + created + mstoken.pin).digest()
mkelem(ut, 'Password', binascii.b2a_base64(digest), Type='wsse:PasswordDigest')
mkelem(ut, 'Nonce', binascii.b2a_base64(nonce))
mkelem(ut, 'wsu:Created', created)
tree = ElementTree(wsse)
wsse = StringIO()
tree.write(wsse)
wsse_header = wsse.getvalue()
After that, ws.microsoft.com started accepting my calls, and it wasn't so hard from then on. Being used to XML-RPC, it feels weird to be encoding and decoding XML all the time when calling remote methods, but a bit of hacking (see the SoapClass class in the code) made most of that fairly trivial.