[PATCH] support SSPI for Windows

23 views
Skip to first unread message

Chuck.K...@bentley.com

unread,
Dec 1, 2011, 5:38:53 PM12/1/11
to mercuri...@selenic.com
# 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
 

Matt Mackall

unread,
Dec 2, 2011, 4:41:58 PM12/2/11
to Chuck.K...@bentley.com, Henrik Stuart, sune.foldager, mercuri...@selenic.com
On Thu, 2011-12-01 at 17:38 -0500, Chuck.K...@bentley.com wrote:
> # 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.

I'm hoping Sune or Henrik can comment on this as they've already done
some work in this area. I suspect we may want to put some of this in an
extension. I understand there are issues here with keeping persistent
connections that we might want to tackle on their own first (ie there's
too much stuff for one patch here).

> _______________________________________________
> Mercurial-devel mailing list
> Mercuri...@selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel


--
Mathematics is the supreme nostalgia of our time.


_______________________________________________
Mercurial-devel mailing list
Mercuri...@selenic.com
http://selenic.com/mailman/listinfo/mercurial-devel

David Pope

unread,
Apr 14, 2012, 1:36:45 PM4/14/12
to mercuri...@googlegroups.com, Chuck.K...@bentley.com, Henrik Stuart, sune.foldager, mercuri...@selenic.com
On Friday, December 2, 2011 4:41:58 PM UTC-5, Matt Mackall wrote:
On Thu, 2011-12-01 at 17:38 -0500, Chuck Kirschman wrote:
> # 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. 

I'm hoping Sune or Henrik can comment on this as they've already done
some work in this area. I suspect we may want to put some of this in an
extension. I understand there are issues here with keeping persistent
connections that we might want to tackle on their own first (ie there's
too much stuff for one patch here).

Hello all,

Have the Windows volunteers (Sune, Henrik, ...?) had a chance to look at this, or further their own work in the area?  I and my company are keenly interested in having SSPI built into Mercurial.  We don't want to risk having developers put their domain passwords in their hgrc files, and our current VCS supports SSPI out-of-the-box so it's one more hurdle for the switchover.  Keyring feels like a band-aid and doesn't help our credibility when advocating the switch.

I saw the recent discussion about the volunteer nature of Mercurial development, which makes perfect sense.  If bodies are needed for testing or developing, I'd be happy to help.  I've done some minor SSPI-related development in the deep murky past, although I doubt that's what's needed here.

-- Dave
(PS, I'm posting using Google Groups despite advice to the contrary, since otherwise I would have had to manually create something approximating a quoted reply (since I just joined the list).  The bad things mentioned in "list etiquette" don't seem to have happened; crossing my fingers...)
Reply all
Reply to author
Forward
0 new messages