Wednesday, July 9, 2008

Exchange Form-Based Authentication and WebDAV in Python.

Since I've become employed fulltime at RIT, in addition to my part-time work with Synacor and trying to maintain something resembling a sleep schedule...I haven't had too much time to write. I did complete a successful test of the gcal sync software and then realized that IMAP won't allow me to place appointments into it making 2 way syncing impossible. So I have been forced to try to re-architect the script using Exchange's poorly documented WebDAV API.
WebDAV is supposed to be an XML web service that goes over HTTP/HTTPS. Unfortunately Microsoft can't do anything right. DAV uses several non-standard HTTP methods, like SEARCH, PROPFIND, PROPSET...instead of the standard HTTP GET, POST, and PUT methods.
Microsoft Exchange also has a bit of a quirk called Form-Based Authentication. When you enable Outlook Web Access, this is the only way to authenticate. Without OWA you can use Basic HTTP Authentication which is pretty easy. With OWA you have to do some interesting maneuvering to get your query in. From a theoretical point of view, what you need to do is simple:
  1. Fake an HTTP Post to the owaauth.dll file.
  2. Receive the response headers and pull the cookies, store them.
  3. Send these cookies back to Exchange while making your "special" DAV SEARCH request.
However, implementing this gets a little annoying. I've been using Python...which has a pretty slick high-level network protocol API, called urllib2. Which can automatically create HTTPS connections, parse HTTP response headers and grep out the cookies, create requests and urlencode GET and POST strings, all sorts of happy things. Unfortunately this library only deals with GET and POST, unless you write your own "handlers". I wasn't sure how to go about extending the library so I chose to go the dirty hack route and implement the functionality I needed at the low level.


import httplib,urllib2,cookielib,sys,getpass,Cookie

exchserv = raw_input('Exchange server: ')
loginex = raw_input('Enter Exchange Username: ')
passex = getpass.getpass('Enter Exchange Password: ')
cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPSHandler(),urllib2.HTTPCookieProcessor(cj))
urllib2.install_opener(opener)
owabody = 'destination=https://'+exchserv+'/exchange/'+loginex+'/&username=main\'+loginex+'&password='+passex
owaheaders = {'Content-Type': 'application/x-www-form-urlencoded',
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.1.4322)',
'Host': exchserv}
owareq = urllib2.Request('https://'+exchserv+'/exchweb/bin/auth/owaauth.dll',owabody,owaheaders)
owa = urllib2.urlopen(owareq)


conn = httplib.HTTPSConnection(exchserv)
conn.set_debuglevel(99999999999)

#there is nothing I like better than manually constructing HTTP headers.
conn.putrequest('SEARCH','/exchange/'+loginex+'/')
conn.putheader('Content-Type','text/xml')
conn.putheader('Content-Length', len(davapptquery))

stringofdeath = ''
for i in cj:
stringofdeath += i.name+'='+i.value+';'
conn.putheader('Cookie', stringofdeath)

conn.endheaders()
conn.send(davapptquery)

resp = conn.getresponse()


Where davapptquery is an unholy string containing a SQL query wrapped in some XML.
I pretty much suck at python, but this does work. Someone better could probably more easily create a handler for exchange's DAV insanity.

No comments: