Commit 84efffc33ab352d68cf981da0d6c8d358244cabc

Edward Thomson 2013-11-13T16:57:51

Introduce git_cred_default for NTLM/SPNEGO auth

diff --git a/include/git2/transport.h b/include/git2/transport.h
index 81ebf4d..fe4883f 100644
--- a/include/git2/transport.h
+++ b/include/git2/transport.h
@@ -31,13 +31,16 @@ GIT_BEGIN_DECL
 /** Authentication type requested */
 typedef enum {
 	/* git_cred_userpass_plaintext */
-	GIT_CREDTYPE_USERPASS_PLAINTEXT =     (1u << 0),
+	GIT_CREDTYPE_USERPASS_PLAINTEXT = (1u << 0),
 
 	/* git_cred_ssh_key */
 	GIT_CREDTYPE_SSH_KEY = (1u << 1),
 
 	/* git_cred_ssh_custom */
-	GIT_CREDTYPE_SSH_CUSTOM =          (1u << 2),
+	GIT_CREDTYPE_SSH_CUSTOM = (1u << 2),
+
+	/* git_cred_default */
+	GIT_CREDTYPE_DEFAULT = (1u << 3),
 } git_credtype_t;
 
 /* The base structure for all credential types */
@@ -48,7 +51,7 @@ struct git_cred {
 	void (*free)(git_cred *cred);
 };
 
-/* A plaintext username and password */
+/** A plaintext username and password */
 typedef struct {
 	git_cred parent;
 	char *username;
@@ -84,6 +87,9 @@ typedef struct git_cred_ssh_custom {
 	void *sign_data;
 } git_cred_ssh_custom;
 
+/** A key for NTLM/Kerberos "default" credentials */
+typedef struct git_cred git_cred_default;
+
 /**
  * Check whether a credential object contains username information.
  *
@@ -151,6 +157,14 @@ GIT_EXTERN(int) git_cred_ssh_custom_new(
 	void *sign_data);
 
 /**
+ * Create a "default" credential usable for Negotiate mechanisms like NTLM
+ * or Kerberos authentication.
+ *
+ * @return 0 for success or an error code for failure
+ */
+GIT_EXTERN(int) git_cred_default_new(git_cred **out);
+
+/**
  * Signature of a function which acquires a credential object.
  *
  * @param cred The newly created credential object.
diff --git a/src/transports/cred.c b/src/transports/cred.c
index cc7fdab..05d2c8d 100644
--- a/src/transports/cred.c
+++ b/src/transports/cred.c
@@ -29,6 +29,10 @@ int git_cred_has_username(git_cred *cred)
 		ret = !!c->username;
 		break;
 	}
+	case GIT_CREDTYPE_DEFAULT: {
+		ret = 0;
+		break;
+	}
 	}
 
 	return ret;
@@ -115,6 +119,13 @@ static void ssh_custom_free(struct git_cred *cred)
 	git__free(c);
 }
 
+static void default_free(struct git_cred *cred)
+{
+	git_cred_default *c = (git_cred_default *)cred;
+
+	git__free(c);
+}
+
 int git_cred_ssh_key_new(
 	git_cred **cred,
 	const char *username,
@@ -191,3 +202,19 @@ int git_cred_ssh_custom_new(
 	*cred = &c->parent;
 	return 0;
 }
+
+int git_cred_default_new(git_cred **cred)
+{
+	git_cred_default *c;
+
+	assert(cred);
+
+	c = git__calloc(1, sizeof(git_cred_default));
+	GITERR_CHECK_ALLOC(c);
+
+	c->credtype = GIT_CREDTYPE_DEFAULT;
+	c->free = default_free;
+
+	*cred = c;
+	return 0;
+}
diff --git a/src/transports/winhttp.c b/src/transports/winhttp.c
index f756645..673cd0f 100644
--- a/src/transports/winhttp.c
+++ b/src/transports/winhttp.c
@@ -52,6 +52,7 @@ static const int no_check_cert_flags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
 
 typedef enum {
 	GIT_WINHTTP_AUTH_BASIC = 1,
+	GIT_WINHTTP_AUTH_NEGOTIATE = 2,
 } winhttp_authmechanism_t;
 
 typedef struct {
@@ -138,6 +139,22 @@ on_error:
 	return error;
 }
 
+static int apply_default_credentials(HINTERNET request)
+{
+	/* If we are explicitly asked to deliver default credentials, turn set
+	 * the security level to low which will guarantee they are delivered.
+	 * The default is "medium" which applies to the intranet and sounds
+	 * like it would correspond to Internet Explorer security zones, but
+	 * in fact does not.
+	 */
+	DWORD data = WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW;
+
+	if (!WinHttpSetOption(request, WINHTTP_OPTION_AUTOLOGON_POLICY, &data, sizeof(DWORD)))
+		return -1;
+
+	return 0;
+}
+
 static int winhttp_stream_connect(winhttp_stream *s)
 {
 	winhttp_subtransport *t = OWNING_SUBTRANSPORT(s);
@@ -317,6 +334,11 @@ static int winhttp_stream_connect(winhttp_stream *s)
 		t->auth_mechanism == GIT_WINHTTP_AUTH_BASIC &&
 		apply_basic_credential(s->request, t->cred) < 0)
 		goto on_error;
+	else if (t->cred &&
+		t->cred->credtype == GIT_CREDTYPE_DEFAULT &&
+		t->auth_mechanism == GIT_WINHTTP_AUTH_NEGOTIATE &&
+		apply_default_credentials(s->request) < 0)
+		goto on_error;
 
 	/* If no other credentials have been applied and the URL has username and
 	 * password, use those */
@@ -361,6 +383,12 @@ static int parse_unauthorized_response(
 		*auth_mechanism = GIT_WINHTTP_AUTH_BASIC;
 	}
 
+	if ((WINHTTP_AUTH_SCHEME_NTLM & supported) ||
+		(WINHTTP_AUTH_SCHEME_NEGOTIATE & supported)) {
+		*allowed_types |= GIT_CREDTYPE_DEFAULT;
+		*auth_mechanism = GIT_WINHTTP_AUTH_NEGOTIATE;
+	}
+
 	return 0;
 }
 
diff --git a/tests/online/push.c b/tests/online/push.c
index aeb1ab4..be505c3 100644
--- a/tests/online/push.c
+++ b/tests/online/push.c
@@ -9,14 +9,17 @@
 
 static git_repository *_repo;
 
+static char *_remote_url;
+
 static char *_remote_ssh_key;
 static char *_remote_ssh_pubkey;
 static char *_remote_ssh_passphrase;
 
-static char *_remote_url;
 static char *_remote_user;
 static char *_remote_pass;
 
+static char *_remote_default;
+
 static int cred_acquire_cb(git_cred **,	const char *, const char *, unsigned int, void *);
 
 static git_remote *_remote;
@@ -47,11 +50,21 @@ static int cred_acquire_cb(
 	GIT_UNUSED(user_from_url);
 	GIT_UNUSED(payload);
 
+	if (GIT_CREDTYPE_DEFAULT & allowed_types) {
+		if (!_remote_default) {
+			printf("GITTEST_REMOTE_DEFAULT must be set to use NTLM/Negotiate credentials\n");
+			return -1;
+		}
+
+		return git_cred_default_new(cred);
+	}
+
 	if (GIT_CREDTYPE_SSH_KEY & allowed_types) {
 		if (!_remote_user || !_remote_ssh_pubkey || !_remote_ssh_key || !_remote_ssh_passphrase) {
 			printf("GITTEST_REMOTE_USER, GITTEST_REMOTE_SSH_PUBKEY, GITTEST_REMOTE_SSH_KEY and GITTEST_REMOTE_SSH_PASSPHRASE must be set\n");
 			return -1;
 		}
+
 		return git_cred_ssh_key_new(cred, _remote_user, _remote_ssh_pubkey, _remote_ssh_key, _remote_ssh_passphrase);
 	}
 
@@ -298,6 +311,7 @@ void test_online_push__initialize(void)
 	_remote_ssh_key = cl_getenv("GITTEST_REMOTE_SSH_KEY");
 	_remote_ssh_pubkey = cl_getenv("GITTEST_REMOTE_SSH_PUBKEY");
 	_remote_ssh_passphrase = cl_getenv("GITTEST_REMOTE_SSH_PASSPHRASE");
+	_remote_default = cl_getenv("GITTEST_REMOTE_DEFAULT");
 	_remote = NULL;
 
 	if (_remote_url) {