[PATCH] support SSPI for Windows

Chuck.Kirschman at bentley.com Chuck.Kirschman at bentley.com
Thu Dec 1 16:38:53 CST 2011


# HG changeset patch
# User Chuck.Kirschman
# Date 1322778699 18000
# Branch stable
# Node ID 6f36523ce8fc00d836770a1a0f481df0ed27983f
# Parent  351a9292e430e35766c552066ed3e87c557b803b
enable sspi for windows
This patch will add SSPI capabilities for both normal http and ui.usehttp2=true
configurations of Mercurial on Windows. The check for the authorization failure
has to happen at the point of opening the connection and resolved at that time.

diff --git a/mercurial/httpconnection.py b/mercurial/httpconnection.py
--- a/mercurial/httpconnection.py
+++ b/mercurial/httpconnection.py
@@ -16,6 +16,7 @@
 from mercurial import httpclient
 from mercurial import sslutil
 from mercurial import util
+from mercurial import ntlm
 from mercurial.i18n import _

 # moved here from url.py to avoid a cycle
@@ -211,6 +212,13 @@
                 path = '/' + path
             h.request(req.get_method(), path, req.data, headers)
             r = h.getresponse()
+            # authorization schemes that require persistent connections
+            # can be attempted here before urllib2 sees them.
+            if r.status == 401 and 'www-authenticate' in r.headers:
+                auth_methods = [s.strip() for s in
+                                r.headers['www-authenticate'].split(",")]
+                if "NTLM" in auth_methods:
+                    r = ntlm.ntlm_auth_httplib2 (h, req, r)
         except socket.error, err: # XXX what error?
             raise urllib2.URLError(err)

diff --git a/mercurial/keepalive.py b/mercurial/keepalive.py
--- a/mercurial/keepalive.py
+++ b/mercurial/keepalive.py
@@ -116,6 +116,7 @@
 import socket
 import thread
 import urllib2
+import ntlm

 DEBUG = None

@@ -255,6 +256,13 @@
                 self._cm.add(host, h, 0)
                 self._start_transaction(h, req)
                 r = h.getresponse()
+                # authorization schemes that require persistent connections
+                # can be attempted here before urllib2 sees them.
+                if r.status == 401:
+                    auth_methods = [s.strip() for s in
+                                    r.msg.get('WWW-Authenticate').split(",")]
+                    if "NTLM" in auth_methods:
+                        r = ntlm.ntlm_auth (h, req, r)
         except (socket.error, httplib.HTTPException), err:
             raise urllib2.URLError(err)

diff --git a/mercurial/ntlm.py b/mercurial/ntlm.py
new file mode 100644
--- /dev/null
+++ b/mercurial/ntlm.py
@@ -0,0 +1,127 @@
+import base64, string, os
+
+class WindowsNtlmMessageGenerator:
+    def __init__(self, user=None):
+        import win32api, sspi
+        if not user:
+            user = win32api.GetUserName()
+        self.sspi_client = sspi.ClientAuth("NTLM", user)
+
+    def create_auth_req(self):
+        import pywintypes
+        output_buffer = None
+        error_msg     = None
+
+        try:
+            error_msg, output_buffer = self.sspi_client.authorize(None)
+        except pywintypes.error:
+            return None
+
+        auth_req = output_buffer[0].Buffer
+        auth_req = base64.encodestring(auth_req)
+        auth_req = string.replace(auth_req, '\012', '')
+        return auth_req
+
+    def create_challenge_response(self, challenge):
+        import pywintypes
+        input_buffer  = challenge
+        outout_buffer = None
+        error_msg     = None
+
+        try:
+            error_msg, output_buffer = self.sspi_client.authorize(input_buffer)
+        except pywintypes.error:
+            return None
+
+        response_msg = output_buffer[0].Buffer
+        response_msg = base64.encodestring(response_msg)
+        response_msg = string.replace(response_msg, '\012', '')
+        return response_msg
+
+def ntlm_auth (host, req, orig_resp):
+    if os.name == 'nt':
+        ntlm_gen = WindowsNtlmMessageGenerator()
+    else:
+        return orig_resp
+
+    # we have to read the original response before we can send a new request
+    orig_resp.read()
+
+    auth_req_msg = ntlm_gen.create_auth_req()
+    if not auth_req_msg: return orig_resp
+    host.putrequest(req.get_method(), req.get_selector())
+    for origHeader in req.headers:
+        host.putheader (origHeader, req.headers[origHeader])
+    host.putheader('Authorization', 'NTLM' + ' ' + auth_req_msg)
+    host.endheaders()
+
+    if req.has_data():
+        host.send (req.get_data())
+
+    resp = host.getresponse()
+    resp.read()
+
+    challenge = resp.msg.get('WWW-Authenticate')
+    if not challenge: return orig_resp
+    challenge = base64.decodestring(challenge.split()[1])
+
+    chal_response_msg = ntlm_gen.create_challenge_response (challenge)
+    if not chal_response_msg: return orig_resp
+    host.putrequest(req.get_method(), req.get_selector())
+    host.putheader('Authorization', 'NTLM' + ' ' + chal_response_msg)
+    for origHeader in req.headers:
+        host.putheader (origHeader, req.headers[origHeader])
+    host.endheaders()
+
+    if req.has_data():
+        host.send (req.get_data())
+
+    resp = host.getresponse()
+
+    # If our NTLM attempt wasn't successful, return the original response
+    if resp.status != 200 and resp.status != 404:
+        return orig_resp
+
+    return resp
+
+
+def ntlm_auth_httplib2 (host, req, orig_resp):
+    # This is different than ntlm_auth because host and response are different classes
+    if os.name == 'nt':
+        ntlm_gen = WindowsNtlmMessageGenerator()
+    else:
+        return orig_resp
+
+    # we have to read the original response before we can send a new request
+    orig_resp.read()
+
+    auth_req_msg = ntlm_gen.create_auth_req()
+    if not auth_req_msg: return orig_resp
+
+    headers = {}
+    for origHeader in req.headers:
+        headers [origHeader] = req.headers[origHeader]
+    headers['Authorization'] = 'NTLM' + ' ' + auth_req_msg
+    host.request(req.get_method(), req.get_selector(), headers=headers)
+    resp = host.getresponse()
+    resp.read()
+
+    challenge = resp.headers['www-authenticate'] if 'www-authenticate' in resp.headers else None
+    if not challenge: return orig_resp
+    challenge = base64.decodestring(challenge.split()[1])
+
+    chal_response_msg = ntlm_gen.create_challenge_response (challenge)
+    if not chal_response_msg: return orig_resp
+
+    headers = {}
+    for origHeader in req.headers:
+        headers [origHeader] = req.headers[origHeader]
+    headers['Authorization'] = 'NTLM' + ' ' + chal_response_msg
+    host.request(req.get_method(), req.get_selector(), headers=headers)
+    resp = host.getresponse()
+
+    # If our NTLM attempt wasn't successful, return the original response
+    if resp.status != 200 and resp.status != 404:
+        return orig_resp
+
+    return resp

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://selenic.com/pipermail/mercurial-devel/attachments/20111201/5d004313/attachment.html>


More information about the Mercurial-devel mailing list