Commit 7372573b5f1113b8522e2588fac1c529ddcedb0a

Edward Thomson 2019-10-25T12:22:10

httpclient: support expect/continue Allow users to opt-in to expect/continue handling when sending a POST and we're authenticated with a "connection-based" authentication mechanism like NTLM or Negotiate. If the response is a 100, return to the caller (to allow them to post their body). If the response is *not* a 100, buffer the response for the caller. HTTP expect/continue is generally safe, but some legacy servers have not implemented it correctly. Require it to be opt-in.

diff --git a/include/git2/common.h b/include/git2/common.h
index 4381982..947e408 100644
--- a/include/git2/common.h
+++ b/include/git2/common.h
@@ -203,7 +203,8 @@ typedef enum {
 	GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY,
 	GIT_OPT_GET_PACK_MAX_OBJECTS,
 	GIT_OPT_SET_PACK_MAX_OBJECTS,
-	GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS
+	GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS,
+	GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE
 } git_libgit2_opt_t;
 
 /**
@@ -397,6 +398,11 @@ typedef enum {
  *		> This will cause .keep file existence checks to be skipped when
  *		> accessing packfiles, which can help performance with remote filesystems.
  *
+ *	 opts(GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE, int enabled)
+ *		> When connecting to a server using NTLM or Negotiate
+ *		> authentication, use expect/continue when POSTing data.
+ *		> This option is not available on Windows.
+ *
  * @param option Option key
  * @param ... value to set the option
  * @return 0 on success, <0 on failure
diff --git a/src/settings.c b/src/settings.c
index 28d10ea..6fae49e 100644
--- a/src/settings.c
+++ b/src/settings.c
@@ -25,6 +25,7 @@
 #include "refs.h"
 #include "index.h"
 #include "transports/smart.h"
+#include "transports/http.h"
 #include "streams/openssl.h"
 #include "streams/mbedtls.h"
 
@@ -284,6 +285,10 @@ int git_libgit2_opts(int key, ...)
 		git_disable_pack_keep_file_checks = (va_arg(ap, int) != 0);
 		break;
 
+	case GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE:
+		git_http__expect_continue = (va_arg(ap, int) != 0);
+		break;
+
 	default:
 		git_error_set(GIT_ERROR_INVALID, "invalid option key");
 		error = -1;
diff --git a/src/transports/http.c b/src/transports/http.c
index cd209ef..9e35f23 100644
--- a/src/transports/http.c
+++ b/src/transports/http.c
@@ -25,6 +25,8 @@
 #include "streams/tls.h"
 #include "streams/socket.h"
 
+bool git_http__expect_continue = false;
+
 git_http_auth_scheme auth_schemes[] = {
 	{ GIT_HTTP_AUTH_NEGOTIATE, "Negotiate", GIT_CREDTYPE_DEFAULT, git_http_auth_negotiate },
 	{ GIT_HTTP_AUTH_NTLM, "NTLM", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_ntlm },
@@ -84,6 +86,7 @@ typedef struct {
 	git_cred *cred;
 	unsigned url_cred_presented : 1,
 	    authenticated : 1;
+	git_http_authtype_t prior_authtype;
 
 	git_vector auth_challenges;
 	git_http_auth_context *auth_context;
@@ -1048,8 +1051,10 @@ static void reset_auth_connection(http_server *server)
 	 */
 
 	if (server->authenticated &&
-	    server->auth_context &&
+		server->auth_context &&
 	    server->auth_context->connection_affinity) {
+		server->prior_authtype = server->auth_context->type;
+
 		free_auth_context(server);
 
 		server->url_cred_presented = 0;
diff --git a/src/transports/http.h b/src/transports/http.h
index ddaab0b..6f698c9 100644
--- a/src/transports/http.h
+++ b/src/transports/http.h
@@ -12,6 +12,8 @@
 
 #define GIT_HTTP_REPLAY_MAX 15
 
+extern bool git_http__expect_continue;
+
 GIT_INLINE(int) git_http__user_agent(git_buf *buf)
 {
 	const char *ua = git_libgit2__user_agent();
diff --git a/src/transports/httpclient.c b/src/transports/httpclient.c
index 32580f5..585f04f 100644
--- a/src/transports/httpclient.c
+++ b/src/transports/httpclient.c
@@ -824,7 +824,6 @@ GIT_INLINE(int) server_setup_from_url(
 static void reset_parser(git_http_client *client)
 {
 	http_parser_init(&client->parser, HTTP_RESPONSE);
-	git_buf_clear(&client->read_buf);
 }
 
 static int setup_hosts(
@@ -869,6 +868,17 @@ GIT_INLINE(int) server_create_stream(git_http_server *server)
 	return -1;
 }
 
+GIT_INLINE(void) save_early_response(
+	git_http_client *client,
+	git_http_response *response)
+{
+	/* Buffer the response so we can return it in read_response */
+	client->state = HAS_EARLY_RESPONSE;
+
+	memcpy(&client->early_response, response, sizeof(git_http_response));
+	memset(response, 0, sizeof(git_http_response));
+}
+
 static int proxy_connect(
 	git_http_client *client,
 	git_http_request *request)
@@ -905,11 +915,7 @@ static int proxy_connect(
 	assert(client->state == DONE);
 
 	if (response.status == 407) {
-		/* Buffer the response so we can return it in read_response */
-		client->state = HAS_EARLY_RESPONSE;
-
-		memcpy(&client->early_response, &response, sizeof(response));
-		memset(&response, 0, sizeof(response));
+		save_early_response(client, &response);
 
 		error = GIT_RETRY;
 		goto done;
@@ -1194,6 +1200,7 @@ int git_http_client_send_request(
 	git_http_client *client,
 	git_http_request *request)
 {
+	git_http_response response = {0};
 	int error = -1;
 
 	assert(client && request);
@@ -1220,13 +1227,26 @@ int git_http_client_send_request(
 	    (error = client_write_request(client)) < 0)
 		goto done;
 
+	client->state = SENT_REQUEST;
+
+	if (request->expect_continue) {
+		if ((error = git_http_client_read_response(&response, client)) < 0 ||
+		    (error = git_http_client_skip_body(client)) < 0)
+			goto done;
+
+		error = 0;
+
+		if (response.status != 100) {
+			save_early_response(client, &response);
+			goto done;
+		}
+	}
+
 	if (request->content_length || request->chunked) {
 		client->state = SENDING_BODY;
 		client->request_body_len = request->content_length;
 		client->request_body_remain = request->content_length;
 		client->request_chunked = request->chunked;
-	} else {
-		client->state = SENT_REQUEST;
 	}
 
 	reset_parser(client);
@@ -1235,9 +1255,16 @@ done:
 	if (error == GIT_RETRY)
 		error = 0;
 
+	git_http_response_dispose(&response);
 	return error;
 }
 
+bool git_http_client_has_response(git_http_client *client)
+{
+	return (client->state == HAS_EARLY_RESPONSE ||
+	        client->state > SENT_REQUEST);
+}
+
 int git_http_client_send_body(
 	git_http_client *client,
 	const char *buffer,
diff --git a/src/transports/httpclient.h b/src/transports/httpclient.h
index 4a447cf..cb21866 100644
--- a/src/transports/httpclient.h
+++ b/src/transports/httpclient.h
@@ -91,6 +91,17 @@ extern int git_http_client_send_request(
 	git_http_client *client,
 	git_http_request *request);
 
+/*
+ * After sending a request, there may already be a response to read --
+ * either because there was a non-continue response to an expect: continue
+ * request, or because the server pipelined a response to us before we even
+ * sent the request.  Examine the state.
+ *
+ * @param client the client to examine
+ * @return true if there's already a response to read, false otherwise
+ */
+extern bool git_http_client_has_response(git_http_client *client);
+
 /**
  * Sends the given buffer to the remote as part of the request body.  The
  * request must have specified either a content_length or the chunked flag.
diff --git a/src/transports/winhttp.c b/src/transports/winhttp.c
index 3360ec9..d8623bf 100644
--- a/src/transports/winhttp.c
+++ b/src/transports/winhttp.c
@@ -57,6 +57,8 @@
 # define DWORD_MAX 0xffffffff
 #endif
 
+bool git_http__expect_continue = false;
+
 static const char *prefix_https = "https://";
 static const char *upload_pack_service = "upload-pack";
 static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack";