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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
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";