winhttp.c 43.3 KB
Newer Older
1
/*
Edward Thomson committed
2
 * Copyright (C) the libgit2 contributors. All rights reserved.
3 4 5 6 7
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */

8 9
#include "common.h"

10 11 12 13 14 15 16 17
#ifdef GIT_WINHTTP

#include "git2.h"
#include "git2/transport.h"
#include "buffer.h"
#include "posix.h"
#include "netops.h"
#include "smart.h"
18 19
#include "remote.h"
#include "repository.h"
20
#include "http.h"
21
#include "git2/sys/credential.h"
22

23
#include <wincrypt.h>
24
#include <winhttp.h>
25 26

/* For IInternetSecurityManager zone check */
27 28 29
#include <objbase.h>
#include <urlmon.h>

30 31 32 33
#define WIDEN2(s) L ## s
#define WIDEN(s) WIDEN2(s)

#define MAX_CONTENT_TYPE_LEN	100
34
#define WINHTTP_OPTION_PEERDIST_EXTENSION_STATE	109
35 36
#define CACHED_POST_BODY_BUF_SIZE	4096
#define UUID_LENGTH_CCH	32
37 38
#define TIMEOUT_INFINITE -1
#define DEFAULT_CONNECT_TIMEOUT 60000
39 40 41 42
#ifndef WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH
#define WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH 0
#endif

43
#ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1
44 45 46
# define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 0x00000200
#endif

47
#ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2
48 49 50
# define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 0x00000800
#endif

51 52 53 54
#ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3
# define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3 0x00002000
#endif

55 56 57 58
#ifndef WINHTTP_NO_CLIENT_CERT_CONTEXT
# define WINHTTP_NO_CLIENT_CERT_CONTEXT NULL
#endif

59 60 61 62
#ifndef HTTP_STATUS_PERMANENT_REDIRECT
# define HTTP_STATUS_PERMANENT_REDIRECT 308
#endif

63 64 65 66
#ifndef DWORD_MAX
# define DWORD_MAX 0xffffffff
#endif

67 68
bool git_http__expect_continue = false;

69 70 71 72
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";
static const char *upload_pack_service_url = "/git-upload-pack";
73 74 75
static const char *receive_pack_service = "receive-pack";
static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive-pack";
static const char *receive_pack_service_url = "/git-receive-pack";
76 77
static const wchar_t *get_verb = L"GET";
static const wchar_t *post_verb = L"POST";
78
static const wchar_t *pragma_nocache = L"Pragma: no-cache";
79
static const wchar_t *transfer_encoding = L"Transfer-Encoding: chunked";
80 81 82 83
static const int no_check_cert_flags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
	SECURITY_FLAG_IGNORE_CERT_DATE_INVALID |
	SECURITY_FLAG_IGNORE_UNKNOWN_CA;

84
#if defined(__MINGW32__)
85 86
static const CLSID CLSID_InternetSecurityManager_mingw =
	{ 0x7B8A2D94, 0x0AC9, 0x11D1,
87
	{ 0x89, 0x6C, 0x00, 0xC0, 0x4F, 0xB6, 0xBF, 0xC4 } };
88 89
static const IID IID_IInternetSecurityManager_mingw =
	{ 0x79EAC9EE, 0xBAF9, 0x11CE,
90
	{ 0x8C, 0x82, 0x00, 0xAA, 0x00, 0x4B, 0xA9, 0x0B } };
91 92 93

# define CLSID_InternetSecurityManager CLSID_InternetSecurityManager_mingw
# define IID_IInternetSecurityManager IID_IInternetSecurityManager_mingw
94 95
#endif

96 97
#define OWNING_SUBTRANSPORT(s) ((winhttp_subtransport *)(s)->parent.subtransport)

98 99
typedef enum {
	GIT_WINHTTP_AUTH_BASIC = 1,
100 101
	GIT_WINHTTP_AUTH_NTLM = 2,
	GIT_WINHTTP_AUTH_NEGOTIATE = 4,
102
	GIT_WINHTTP_AUTH_DIGEST = 8,
103 104
} winhttp_authmechanism_t;

105 106 107 108 109 110
typedef struct {
	git_smart_subtransport_stream parent;
	const char *service;
	const char *service_url;
	const wchar_t *verb;
	HINTERNET request;
111
	wchar_t *request_uri;
112 113 114 115
	char *chunk_buffer;
	unsigned chunk_buffer_len;
	HANDLE post_body;
	DWORD post_body_len;
116
	unsigned sent_request : 1,
117
		received_response : 1,
118 119
		chunked : 1,
		status_sending_request_reached: 1;
120 121 122
} winhttp_stream;

typedef struct {
123
	git_net_url url;
124
	git_credential *cred;
125
	int auth_mechanisms;
126 127 128 129 130 131 132 133 134 135
	bool url_cred_presented;
} winhttp_server;

typedef struct {
	git_smart_subtransport parent;
	transport_smart *owner;

	winhttp_server server;
	winhttp_server proxy;

136 137 138 139
	HINTERNET session;
	HINTERNET connection;
} winhttp_subtransport;

140
static int apply_userpass_credentials(HINTERNET request, DWORD target, int mechanisms, git_credential *cred)
141
{
142
	git_credential_userpass_plaintext *c = (git_credential_userpass_plaintext *)cred;
143
	wchar_t *user = NULL, *pass = NULL;
144
	int user_len = 0, pass_len = 0, error = 0;
145 146 147 148 149 150 151 152 153 154 155
	DWORD native_scheme;

	if (mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE) {
		native_scheme = WINHTTP_AUTH_SCHEME_NEGOTIATE;
	} else if (mechanisms & GIT_WINHTTP_AUTH_NTLM) {
		native_scheme = WINHTTP_AUTH_SCHEME_NTLM;
	} else if (mechanisms & GIT_WINHTTP_AUTH_DIGEST) {
		native_scheme = WINHTTP_AUTH_SCHEME_DIGEST;
	} else if (mechanisms & GIT_WINHTTP_AUTH_BASIC) {
		native_scheme = WINHTTP_AUTH_SCHEME_BASIC;
	} else {
156
		git_error_set(GIT_ERROR_HTTP, "invalid authentication scheme");
157 158 159
		error = -1;
		goto done;
	}
160

161 162
	if ((error = user_len = git__utf8_to_16_alloc(&user, c->username)) < 0)
		goto done;
163

164 165
	if ((error = pass_len = git__utf8_to_16_alloc(&pass, c->password)) < 0)
		goto done;
166

167
	if (!WinHttpSetCredentials(request, target, native_scheme, user, pass, NULL)) {
168
		git_error_set(GIT_ERROR_OS, "failed to set credentials");
169 170 171
		error = -1;
	}

172 173 174 175 176 177 178
done:
	if (user_len > 0)
		git__memzero(user, user_len * sizeof(wchar_t));

	if (pass_len > 0)
		git__memzero(pass, pass_len * sizeof(wchar_t));

179 180 181 182 183 184
	git__free(user);
	git__free(pass);

	return error;
}

185
static int apply_default_credentials(HINTERNET request, DWORD target, int mechanisms)
186
{
187 188
	DWORD autologon_level = WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW;
	DWORD native_scheme = 0;
189

190 191 192
	if ((mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE) != 0) {
		native_scheme = WINHTTP_AUTH_SCHEME_NEGOTIATE;
	} else if ((mechanisms & GIT_WINHTTP_AUTH_NTLM) != 0) {
193 194
		native_scheme = WINHTTP_AUTH_SCHEME_NTLM;
	} else {
195
		git_error_set(GIT_ERROR_HTTP, "invalid authentication scheme");
196
		return -1;
197 198
	}

199 200 201 202 203 204
	/*
	 * Autologon policy must be "low" to use default creds.
	 * This is safe as the user has explicitly requested it.
	 */
	if (!WinHttpSetOption(request, WINHTTP_OPTION_AUTOLOGON_POLICY, &autologon_level, sizeof(DWORD))) {
		git_error_set(GIT_ERROR_OS, "could not configure logon policy");
205 206 207
		return -1;
	}

208 209
	if (!WinHttpSetCredentials(request, target, native_scheme, NULL, NULL, NULL)) {
		git_error_set(GIT_ERROR_OS, "could not configure credentials");
210
		return -1;
211
	}
212

213 214 215
	return 0;
}

216
static int acquire_url_cred(
217
	git_credential **cred,
218
	unsigned int allowed_types,
219 220
	const char *username,
	const char *password)
221
{
222 223
	if (allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT)
		return git_credential_userpass_plaintext_new(cred, username, password);
224

225 226
	if ((allowed_types & GIT_CREDENTIAL_DEFAULT) && *username == '\0' && *password == '\0')
		return git_credential_default_new(cred);
227 228 229

	return 1;
}
230

231
static int acquire_fallback_cred(
232
	git_credential **cred,
233 234 235 236
	const char *url,
	unsigned int allowed_types)
{
	int error = 1;
237

238 239
	/* If the target URI supports integrated Windows authentication
	 * as an authentication mechanism */
240
	if (GIT_CREDENTIAL_DEFAULT & allowed_types) {
241
		wchar_t *wide_url;
242
		HRESULT hCoInitResult;
243 244

		/* Convert URL to wide characters */
245
		if (git__utf8_to_16_alloc(&wide_url, url) < 0) {
246
			git_error_set(GIT_ERROR_OS, "failed to convert string to wide form");
247 248 249
			return -1;
		}

250
		hCoInitResult = CoInitializeEx(NULL, COINIT_MULTITHREADED);
251

252
		if (SUCCEEDED(hCoInitResult) || hCoInitResult == RPC_E_CHANGED_MODE) {
253 254 255 256 257 258 259 260 261 262 263
			IInternetSecurityManager* pISM;

			/* And if the target URI is in the My Computer, Intranet, or Trusted zones */
			if (SUCCEEDED(CoCreateInstance(&CLSID_InternetSecurityManager, NULL,
				CLSCTX_ALL, &IID_IInternetSecurityManager, (void **)&pISM))) {
				DWORD dwZone;

				if (SUCCEEDED(pISM->lpVtbl->MapUrlToZone(pISM, wide_url, &dwZone, 0)) &&
					(URLZONE_LOCAL_MACHINE == dwZone ||
					URLZONE_INTRANET == dwZone ||
					URLZONE_TRUSTED == dwZone)) {
264
					git_credential *existing = *cred;
265 266 267 268 269

					if (existing)
						existing->free(existing);

					/* Then use default Windows credentials to authenticate this request */
270
					error = git_credential_default_new(cred);
271 272 273 274 275
				}

				pISM->lpVtbl->Release(pISM);
			}

Aaron Franke committed
276
			/* Only uninitialize if the call to CoInitializeEx was successful. */
277 278
			if (SUCCEEDED(hCoInitResult))
				CoUninitialize();
279 280 281 282 283 284 285 286
		}

		git__free(wide_url);
	}

	return error;
}

287 288 289 290 291 292
static int certificate_check(winhttp_stream *s, int valid)
{
	int error;
	winhttp_subtransport *t = OWNING_SUBTRANSPORT(s);
	PCERT_CONTEXT cert_ctx;
	DWORD cert_ctx_size = sizeof(cert_ctx);
293
	git_cert_x509 cert;
294

295
	/* If there is no override, we should fail if WinHTTP doesn't think it's fine */
296
	if (t->owner->certificate_check_cb == NULL && !valid) {
297
		if (!git_error_last())
298
			git_error_set(GIT_ERROR_HTTP, "unknown certificate check failure");
299

300
		return GIT_ECERTIFICATE;
301
	}
302

303
	if (t->owner->certificate_check_cb == NULL || git__strcmp(t->server.url.scheme, "https") != 0)
304 305 306
		return 0;

	if (!WinHttpQueryOption(s->request, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &cert_ctx, &cert_ctx_size)) {
307
		git_error_set(GIT_ERROR_OS, "failed to get server certificate");
308 309 310
		return -1;
	}

311
	git_error_clear();
312
	cert.parent.cert_type = GIT_CERT_X509;
313 314
	cert.data = cert_ctx->pbCertEncoded;
	cert.len = cert_ctx->cbCertEncoded;
315
	error = t->owner->certificate_check_cb((git_cert *) &cert, valid, t->server.url.host, t->owner->message_cb_payload);
316 317
	CertFreeCertificateContext(cert_ctx);

318 319 320
	if (error == GIT_PASSTHROUGH)
		error = valid ? 0 : GIT_ECERTIFICATE;

321
	if (error < 0 && !git_error_last())
322
		git_error_set(GIT_ERROR_HTTP, "user cancelled certificate check");
323 324 325 326

	return error;
}

327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
static void winhttp_stream_close(winhttp_stream *s)
{
	if (s->chunk_buffer) {
		git__free(s->chunk_buffer);
		s->chunk_buffer = NULL;
	}

	if (s->post_body) {
		CloseHandle(s->post_body);
		s->post_body = NULL;
	}

	if (s->request_uri) {
		git__free(s->request_uri);
		s->request_uri = NULL;
	}

	if (s->request) {
		WinHttpCloseHandle(s->request);
		s->request = NULL;
	}

	s->sent_request = 0;
}

352 353 354 355
static int apply_credentials(
	HINTERNET request,
	git_net_url *url,
	int target,
356
	git_credential *creds,
357 358 359 360
	int mechanisms)
{
	int error = 0;

361 362
	GIT_UNUSED(url);

363
	/* If we have creds, just apply them */
364
	if (creds && creds->credtype == GIT_CREDENTIAL_USERPASS_PLAINTEXT)
365
		error = apply_userpass_credentials(request, target, mechanisms, creds);
366
	else if (creds && creds->credtype == GIT_CREDENTIAL_DEFAULT)
367 368 369 370 371
		error = apply_default_credentials(request, target, mechanisms);

	return error;
}

372 373 374 375
static int winhttp_stream_connect(winhttp_stream *s)
{
	winhttp_subtransport *t = OWNING_SUBTRANSPORT(s);
	git_buf buf = GIT_BUF_INIT;
376
	char *proxy_url = NULL;
377
	wchar_t ct[MAX_CONTENT_TYPE_LEN];
378
	LPCWSTR types[] = { L"*/*", NULL };
379
	BOOL peerdist = FALSE;
380
	int error = -1;
381
	unsigned long disable_redirects = WINHTTP_DISABLE_REDIRECTS;
382 383
	int default_timeout = TIMEOUT_INFINITE;
	int default_connect_timeout = DEFAULT_CONNECT_TIMEOUT;
384 385
	DWORD autologon_policy = WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH;

386
	const char *service_url = s->service_url;
387
	size_t i;
388
	const git_proxy_options *proxy_opts;
389

390 391 392
	/* If path already ends in /, remove the leading slash from service_url */
	if ((git__suffixcmp(t->server.url.path, "/") == 0) && (git__prefixcmp(service_url, "/") == 0))
		service_url++;
393
	/* Prepare URL */
394
	git_buf_printf(&buf, "%s%s", t->server.url.path, service_url);
395 396 397 398

	if (git_buf_oom(&buf))
		return -1;

399
	/* Convert URL to wide characters */
400
	if (git__utf8_to_16_alloc(&s->request_uri, git_buf_cstr(&buf)) < 0) {
401
		git_error_set(GIT_ERROR_OS, "failed to convert string to wide form");
402 403
		goto on_error;
	}
404 405 406 407 408

	/* Establish request */
	s->request = WinHttpOpenRequest(
			t->connection,
			s->verb,
409
			s->request_uri,
410 411 412
			NULL,
			WINHTTP_NO_REFERER,
			types,
413
			git__strcmp(t->server.url.scheme, "https") == 0 ? WINHTTP_FLAG_SECURE : 0);
414 415

	if (!s->request) {
416
		git_error_set(GIT_ERROR_OS, "failed to open request");
417 418 419
		goto on_error;
	}

420 421 422 423
	/* Never attempt default credentials; we'll provide them explicitly. */
	if (!WinHttpSetOption(s->request, WINHTTP_OPTION_AUTOLOGON_POLICY, &autologon_policy, sizeof(DWORD)))
		return -1;

424
	if (!WinHttpSetTimeouts(s->request, default_timeout, default_connect_timeout, default_timeout, default_timeout)) {
425
		git_error_set(GIT_ERROR_OS, "failed to set timeouts for WinHTTP");
426 427 428
		goto on_error;
	}

429 430 431
	proxy_opts = &t->owner->proxy;
	if (proxy_opts->type == GIT_PROXY_AUTO) {
		/* Set proxy if necessary */
432
		if (git_remote__get_http_proxy(t->owner->owner, (strcmp(t->server.url.scheme, "https") == 0), &proxy_url) < 0)
433 434
			goto on_error;
	}
435
	else if (proxy_opts->type == GIT_PROXY_SPECIFIED) {
436
		proxy_url = git__strdup(proxy_opts->url);
437
		GIT_ERROR_CHECK_ALLOC(proxy_url);
438
	}
439 440

	if (proxy_url) {
441
		git_buf processed_url = GIT_BUF_INIT;
442
		WINHTTP_PROXY_INFO proxy_info;
443 444
		wchar_t *proxy_wide;

445
		git_net_url_dispose(&t->proxy.url);
446

447
		if ((error = git_net_url_parse(&t->proxy.url, proxy_url)) < 0)
448
			goto on_error;
449

450
		if (strcmp(t->proxy.url.scheme, "http") != 0 && strcmp(t->proxy.url.scheme, "https") != 0) {
451
			git_error_set(GIT_ERROR_HTTP, "invalid URL: '%s'", proxy_url);
452
			error = -1;
453
			goto on_error;
454
		}
455

456
		git_buf_puts(&processed_url, t->proxy.url.scheme);
457 458
		git_buf_PUTS(&processed_url, "://");

459 460 461
		if (git_net_url_is_ipv6(&t->proxy.url))
			git_buf_putc(&processed_url, '[');

462
		git_buf_puts(&processed_url, t->proxy.url.host);
463

464 465 466
		if (git_net_url_is_ipv6(&t->proxy.url))
			git_buf_putc(&processed_url, ']');

467 468
		if (!git_net_url_is_default_port(&t->proxy.url))
			git_buf_printf(&processed_url, ":%s", t->proxy.url.port);
469 470 471

		if (git_buf_oom(&processed_url)) {
			error = -1;
472 473
			goto on_error;
		}
474

475
		/* Convert URL to wide characters */
476
		error = git__utf8_to_16_alloc(&proxy_wide, processed_url.ptr);
477
		git_buf_dispose(&processed_url);
478
		if (error < 0)
479 480
			goto on_error;

481
		proxy_info.dwAccessType = WINHTTP_ACCESS_TYPE_NAMED_PROXY;
482
		proxy_info.lpszProxy = proxy_wide;
483 484 485 486 487 488
		proxy_info.lpszProxyBypass = NULL;

		if (!WinHttpSetOption(s->request,
			WINHTTP_OPTION_PROXY,
			&proxy_info,
			sizeof(WINHTTP_PROXY_INFO))) {
489
			git_error_set(GIT_ERROR_OS, "failed to set proxy");
490
			git__free(proxy_wide);
491 492
			goto on_error;
		}
493 494

		git__free(proxy_wide);
495

496 497
		if ((error = apply_credentials(s->request, &t->proxy.url, WINHTTP_AUTH_TARGET_PROXY, t->proxy.cred, t->proxy.auth_mechanisms)) < 0)
			goto on_error;
498 499
	}

500 501 502
	/* Disable WinHTTP redirects so we can handle them manually. Why, you ask?
	 * http://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/b2ff8879-ab9f-4218-8f09-16d25dff87ae
	 */
503 504 505 506
	if (!WinHttpSetOption(s->request,
		WINHTTP_OPTION_DISABLE_FEATURE,
		&disable_redirects,
		sizeof(disable_redirects))) {
507
			git_error_set(GIT_ERROR_OS, "failed to disable redirects");
508
			error = -1;
509 510 511
			goto on_error;
	}

512 513 514 515 516 517 518 519 520 521
	/* Strip unwanted headers (X-P2P-PeerDist, X-P2P-PeerDistEx) that WinHTTP
	 * adds itself. This option may not be supported by the underlying
	 * platform, so we do not error-check it */
	WinHttpSetOption(s->request,
		WINHTTP_OPTION_PEERDIST_EXTENSION_STATE,
		&peerdist,
		sizeof(peerdist));

	/* Send Pragma: no-cache header */
	if (!WinHttpAddRequestHeaders(s->request, pragma_nocache, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) {
522
		git_error_set(GIT_ERROR_OS, "failed to add a header to the request");
523 524 525
		goto on_error;
	}

526
	if (post_verb == s->verb) {
Ben Straub committed
527
		/* Send Content-Type and Accept headers -- only necessary on a POST */
528
		git_buf_clear(&buf);
Ben Straub committed
529
		if (git_buf_printf(&buf,
530 531
			"Content-Type: application/x-git-%s-request",
			s->service) < 0)
532 533
			goto on_error;

534
		if (git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_buf_cstr(&buf)) < 0) {
535
			git_error_set(GIT_ERROR_OS, "failed to convert content-type to wide characters");
536 537
			goto on_error;
		}
538

539 540
		if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG)-1L,
			WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) {
541
			git_error_set(GIT_ERROR_OS, "failed to add a header to the request");
542 543 544 545 546 547 548 549 550
			goto on_error;
		}

		git_buf_clear(&buf);
		if (git_buf_printf(&buf,
			"Accept: application/x-git-%s-result",
			s->service) < 0)
			goto on_error;

551
		if (git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_buf_cstr(&buf)) < 0) {
552
			git_error_set(GIT_ERROR_OS, "failed to convert accept header to wide characters");
553 554
			goto on_error;
		}
555 556

		if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG)-1L,
Ben Straub committed
557
			WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) {
558
			git_error_set(GIT_ERROR_OS, "failed to add a header to the request");
559 560
			goto on_error;
		}
561 562
	}

563 564 565 566 567
	for (i = 0; i < t->owner->custom_headers.count; i++) {
		if (t->owner->custom_headers.strings[i]) {
			git_buf_clear(&buf);
			git_buf_puts(&buf, t->owner->custom_headers.strings[i]);
			if (git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_buf_cstr(&buf)) < 0) {
568
				git_error_set(GIT_ERROR_OS, "failed to convert custom header to wide characters");
569 570
				goto on_error;
			}
571

572 573
			if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG)-1L,
				WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) {
574
				git_error_set(GIT_ERROR_OS, "failed to add a header to the request");
575
				goto on_error;
576 577 578 579
			}
		}
	}

580
	/* If requested, disable certificate validation */
581
	if (strcmp(t->server.url.scheme, "https") == 0) {
582 583 584 585
		int flags;

		if (t->owner->parent.read_flags(&t->owner->parent, &flags) < 0)
			goto on_error;
586 587
	}

588
	if ((error = apply_credentials(s->request, &t->server.url, WINHTTP_AUTH_TARGET_SERVER, t->server.cred, t->server.auth_mechanisms)) < 0)
589
		goto on_error;
590

591 592
	/* We've done everything up to calling WinHttpSendRequest. */

593
	error = 0;
594 595

on_error:
596 597 598
	if (error < 0)
		winhttp_stream_close(s);

599
	git__free(proxy_url);
600
	git_buf_dispose(&buf);
601
	return error;
602 603
}

604 605
static int parse_unauthorized_response(
	int *allowed_types,
606 607
	int *allowed_mechanisms,
	HINTERNET request)
608
{
609
	DWORD supported, first, target;
610 611

	*allowed_types = 0;
612
	*allowed_mechanisms = 0;
613

614 615
	/* WinHttpQueryHeaders() must be called before WinHttpQueryAuthSchemes().
	 * We can assume this was already done, since we know we are unauthorized.
616 617
	 */
	if (!WinHttpQueryAuthSchemes(request, &supported, &first, &target)) {
618
		git_error_set(GIT_ERROR_OS, "failed to parse supported auth schemes");
619 620
		return -1;
	}
621

622
	if (WINHTTP_AUTH_SCHEME_NTLM & supported) {
623 624
		*allowed_types |= GIT_CREDENTIAL_USERPASS_PLAINTEXT;
		*allowed_types |= GIT_CREDENTIAL_DEFAULT;
625
		*allowed_mechanisms |= GIT_WINHTTP_AUTH_NTLM;
626
	}
627

628
	if (WINHTTP_AUTH_SCHEME_NEGOTIATE & supported) {
629
		*allowed_types |= GIT_CREDENTIAL_DEFAULT;
630
		*allowed_mechanisms |= GIT_WINHTTP_AUTH_NEGOTIATE;
631 632 633
	}

	if (WINHTTP_AUTH_SCHEME_BASIC & supported) {
634
		*allowed_types |= GIT_CREDENTIAL_USERPASS_PLAINTEXT;
635
		*allowed_mechanisms |= GIT_WINHTTP_AUTH_BASIC;
636 637
	}

638
	if (WINHTTP_AUTH_SCHEME_DIGEST & supported) {
639
		*allowed_types |= GIT_CREDENTIAL_USERPASS_PLAINTEXT;
640 641 642
		*allowed_mechanisms |= GIT_WINHTTP_AUTH_DIGEST;
	}

643 644 645 646 647 648 649 650 651
	return 0;
}

static int write_chunk(HINTERNET request, const char *buffer, size_t len)
{
	DWORD bytes_written;
	git_buf buf = GIT_BUF_INIT;

	/* Chunk header */
lhchavez committed
652
	git_buf_printf(&buf, "%"PRIXZ"\r\n", len);
653 654 655 656 657

	if (git_buf_oom(&buf))
		return -1;

	if (!WinHttpWriteData(request,
658
		git_buf_cstr(&buf),	(DWORD)git_buf_len(&buf),
659
		&bytes_written)) {
660
		git_buf_dispose(&buf);
661
		git_error_set(GIT_ERROR_OS, "failed to write chunk header");
662
		return -1;
663 664
	}

665
	git_buf_dispose(&buf);
666 667 668

	/* Chunk body */
	if (!WinHttpWriteData(request,
669
		buffer, (DWORD)len,
670
		&bytes_written)) {
671
		git_error_set(GIT_ERROR_OS, "failed to write chunk");
672 673 674 675 676 677 678
		return -1;
	}

	/* Chunk footer */
	if (!WinHttpWriteData(request,
		"\r\n", 2,
		&bytes_written)) {
679
		git_error_set(GIT_ERROR_OS, "failed to write chunk footer");
680 681 682 683
		return -1;
	}

	return 0;
684 685
}

686 687 688 689 690 691
static int winhttp_close_connection(winhttp_subtransport *t)
{
	int ret = 0;

	if (t->connection) {
		if (!WinHttpCloseHandle(t->connection)) {
692
			git_error_set(GIT_ERROR_OS, "unable to close connection");
693 694 695 696 697 698 699 700
			ret = -1;
		}

		t->connection = NULL;
	}

	if (t->session) {
		if (!WinHttpCloseHandle(t->session)) {
701
			git_error_set(GIT_ERROR_OS, "unable to close session");
702 703 704 705 706 707 708 709 710
			ret = -1;
		}

		t->session = NULL;
	}

	return ret;
}

711 712 713 714 715 716 717 718 719
static void CALLBACK winhttp_status(
	HINTERNET connection,
	DWORD_PTR ctx,
	DWORD code,
	LPVOID info,
	DWORD info_len)
{
	DWORD status;

720 721 722
	GIT_UNUSED(connection);
	GIT_UNUSED(info_len);

723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750
	switch (code) {
		case WINHTTP_CALLBACK_STATUS_SECURE_FAILURE:
			status = *((DWORD *)info);

			if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_CN_INVALID))
				git_error_set(GIT_ERROR_HTTP, "SSL certificate issued for different common name");
			else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_DATE_INVALID))
				git_error_set(GIT_ERROR_HTTP, "SSL certificate has expired");
			else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CA))
				git_error_set(GIT_ERROR_HTTP, "SSL certificate signed by unknown CA");
			else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CERT))
				git_error_set(GIT_ERROR_HTTP, "SSL certificate is invalid");
			else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED))
				git_error_set(GIT_ERROR_HTTP, "certificate revocation check failed");
			else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REVOKED))
				git_error_set(GIT_ERROR_HTTP, "SSL certificate was revoked");
			else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_SECURITY_CHANNEL_ERROR))
				git_error_set(GIT_ERROR_HTTP, "security libraries could not be loaded");
			else
				git_error_set(GIT_ERROR_HTTP, "unknown security error %lu", status);

			break;
		
		case WINHTTP_CALLBACK_STATUS_SENDING_REQUEST:
			((winhttp_stream *) ctx)->status_sending_request_reached = 1;

			break;
	}
751 752
}

753
static int winhttp_connect(
754
	winhttp_subtransport *t)
755
{
756
	wchar_t *wide_host = NULL;
757
	int32_t port;
758 759 760
	wchar_t *wide_ua = NULL;
	git_buf ipv6 = GIT_BUF_INIT, ua = GIT_BUF_INIT;
	const char *host;
761
	int error = -1;
762 763
	int default_timeout = TIMEOUT_INFINITE;
	int default_connect_timeout = DEFAULT_CONNECT_TIMEOUT;
764 765 766
	DWORD protocols =
		WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 |
		WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 |
767 768
		WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 |
		WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3;
769

770 771 772
	t->session = NULL;
	t->connection = NULL;

773
	/* Prepare port */
774 775
	if (git__strntol32(&port, t->server.url.port,
			   strlen(t->server.url.port), NULL, 10) < 0)
776 777 778 779 780 781 782 783 784 785 786
		goto on_error;

	/* IPv6? Add braces around the host. */
	if (git_net_url_is_ipv6(&t->server.url)) {
		if (git_buf_printf(&ipv6, "[%s]", t->server.url.host) < 0)
			goto on_error;

		host = ipv6.ptr;
	} else {
		host = t->server.url.host;
	}
787 788

	/* Prepare host */
789
	if (git__utf8_to_16_alloc(&wide_host, host) < 0) {
790
		git_error_set(GIT_ERROR_OS, "unable to convert host to wide characters");
791
		goto on_error;
792
	}
793

794

795 796
	if (git_http__user_agent(&ua) < 0)
		goto on_error;
797 798

	if (git__utf8_to_16_alloc(&wide_ua, git_buf_cstr(&ua)) < 0) {
799
		git_error_set(GIT_ERROR_OS, "unable to convert host to wide characters");
800
		goto on_error;
801 802
	}

803 804
	/* Establish session */
	t->session = WinHttpOpen(
805
		wide_ua,
806 807 808 809 810 811
		WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
		WINHTTP_NO_PROXY_NAME,
		WINHTTP_NO_PROXY_BYPASS,
		0);

	if (!t->session) {
812
		git_error_set(GIT_ERROR_OS, "failed to init WinHTTP");
813
		goto on_error;
814 815
	}

816
	/*
817 818
	 * Do a best-effort attempt to enable TLS 1.3 and 1.2 but allow this to
	 * fail; if TLS 1.2 or 1.3 support is not available for some reason,
819 820
	 * ignore the failure (it will keep the default protocols).
	 */
821
	if (WinHttpSetOption(t->session,
822 823
		WINHTTP_OPTION_SECURE_PROTOCOLS,
		&protocols,
824 825 826 827 828 829 830
		sizeof(protocols)) == FALSE) {
		protocols &= ~WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3;
		WinHttpSetOption(t->session,
			WINHTTP_OPTION_SECURE_PROTOCOLS,
			&protocols,
			sizeof(protocols));
	}
831

832
	if (!WinHttpSetTimeouts(t->session, default_timeout, default_connect_timeout, default_timeout, default_timeout)) {
833
		git_error_set(GIT_ERROR_OS, "failed to set timeouts for WinHTTP");
834 835
		goto on_error;
	}
836

837

838 839 840
	/* Establish connection */
	t->connection = WinHttpConnect(
		t->session,
841
		wide_host,
842 843 844 845
		(INTERNET_PORT) port,
		0);

	if (!t->connection) {
846
		git_error_set(GIT_ERROR_OS, "failed to connect to host");
847
		goto on_error;
848 849
	}

850 851 852 853 854 855
	if (WinHttpSetStatusCallback(
			t->connection,
			winhttp_status,
			WINHTTP_CALLBACK_FLAG_SECURE_FAILURE | WINHTTP_CALLBACK_FLAG_SEND_REQUEST,
			0
		) == WINHTTP_INVALID_STATUS_CALLBACK) {
856
		git_error_set(GIT_ERROR_OS, "failed to set status callback");
857 858 859
		goto on_error;
	}

860 861 862
	error = 0;

on_error:
863 864 865
	if (error < 0)
		winhttp_close_connection(t);

866
	git_buf_dispose(&ua);
867
	git_buf_dispose(&ipv6);
868
	git__free(wide_host);
869
	git__free(wide_ua);
870 871

	return error;
872 873
}

874
static int do_send_request(winhttp_stream *s, size_t len, bool chunked)
875
{
876 877 878
	int attempts;
	bool success;

879 880 881 882 883
	if (len > DWORD_MAX) {
		SetLastError(ERROR_NOT_ENOUGH_MEMORY);
		return -1;
	}

884
	for (attempts = 0; attempts < 5; attempts++) {
885
		if (chunked) {
886 887 888
			success = WinHttpSendRequest(s->request,
				WINHTTP_NO_ADDITIONAL_HEADERS, 0,
				WINHTTP_NO_REQUEST_DATA, 0,
889
				WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH, (DWORD_PTR)s);
890 891 892 893
		} else {
			success = WinHttpSendRequest(s->request,
				WINHTTP_NO_ADDITIONAL_HEADERS, 0,
				WINHTTP_NO_REQUEST_DATA, 0,
894
				(DWORD)len, (DWORD_PTR)s);
895
		}
896

lhchavez committed
897
		if (success || GetLastError() != (DWORD)SEC_E_BUFFER_TOO_SMALL)
898
			break;
899 900
	}

901
	return success ? 0 : -1;
902 903
}

904
static int send_request(winhttp_stream *s, size_t len, bool chunked)
905
{
906
	int request_failed = 1, error, attempts = 0;
907
	DWORD ignore_flags, send_request_error;
908

909
	git_error_clear();
910

911
	while (request_failed && attempts++ < 3) {
912 913
		int cert_valid = 1;
		int client_cert_requested = 0;
914 915 916 917 918
		request_failed = 0;
		if ((error = do_send_request(s, len, chunked)) < 0) {
			send_request_error = GetLastError();
			request_failed = 1;
			switch (send_request_error) {
919
				case ERROR_WINHTTP_SECURE_FAILURE:
920 921
					cert_valid = 0;
					break;
922
				case ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED:
923 924
					client_cert_requested = 1;
					break;
925
				default:
926 927 928
					git_error_set(GIT_ERROR_OS, "failed to send request");
					return -1;
			}
929
		}
930

931 932 933 934 935 936 937
		/*
		 * Only check the certificate if we were able to reach the sending request phase, or
		 * received a secure failure error. Otherwise, the server certificate won't be available
		 * since the request wasn't able to complete (e.g. proxy auth required)
		 */
		if (!cert_valid ||
			(!request_failed && s->status_sending_request_reached)) {
938 939 940 941
			git_error_clear();
			if ((error = certificate_check(s, cert_valid)) < 0) {
				if (!git_error_last())
					git_error_set(GIT_ERROR_OS, "user cancelled certificate check");
942

943 944 945
				return error;
			}
		}
946

947 948 949
		/* if neither the request nor the certificate check returned errors, we're done */
		if (!request_failed)
			return 0;
950

951 952 953 954 955 956 957
		if (!cert_valid) {
			ignore_flags = no_check_cert_flags;
			if (!WinHttpSetOption(s->request, WINHTTP_OPTION_SECURITY_FLAGS, &ignore_flags, sizeof(ignore_flags))) {
				git_error_set(GIT_ERROR_OS, "failed to set security options");
				return -1;
			}
		}
958

959 960 961 962 963 964 965 966 967 968
		if (client_cert_requested) {
			/*
			 * Client certificates are not supported, explicitly tell the server that
			 * (it's possible a client certificate was requested but is not required)
			 */
			if (!WinHttpSetOption(s->request, WINHTTP_OPTION_CLIENT_CERT_CONTEXT, WINHTTP_NO_CLIENT_CERT_CONTEXT, 0)) {
				git_error_set(GIT_ERROR_OS, "failed to set client cert context");
				return -1;
			}
		}
969 970 971 972 973
	}

	return error;
}

974 975 976 977
static int acquire_credentials(
	HINTERNET request,
	winhttp_server *server,
	const char *url_str,
978
	git_credential_acquire_cb cred_cb,
979 980 981 982 983 984 985 986 987
	void *cred_cb_payload)
{
	int allowed_types;
	int error = 1;

	if (parse_unauthorized_response(&allowed_types, &server->auth_mechanisms, request) < 0)
		return -1;

	if (allowed_types) {
988
		git_credential_free(server->cred);
989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003
		server->cred = NULL;

		/* Start with URL-specified credentials, if there were any. */
		if (!server->url_cred_presented && server->url.username && server->url.password) {
			error = acquire_url_cred(&server->cred, allowed_types, server->url.username, server->url.password);
			server->url_cred_presented = 1;

			if (error < 0)
				return error;
		}

		/* Next use the user-defined callback, if there is one. */
		if (error > 0 && cred_cb) {
			error = cred_cb(&server->cred, url_str, server->url.username, allowed_types, cred_cb_payload);

1004
			/* Treat GIT_PASSTHROUGH as though git_credential_acquire_cb isn't set */
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026
			if (error == GIT_PASSTHROUGH)
				error = 1;
			else if (error < 0)
				return error;
		}

		/* Finally, invoke the fallback default credential lookup. */
		if (error > 0) {
			error = acquire_fallback_cred(&server->cred, url_str, allowed_types);

			if (error < 0)
				return error;
		}
	}

	/*
	 * No error occurred but we could not find appropriate credentials.
	 * This behaves like a pass-through.
	 */
	return error;
}

1027 1028 1029 1030 1031 1032 1033 1034
static int winhttp_stream_read(
	git_smart_subtransport_stream *stream,
	char *buffer,
	size_t buf_size,
	size_t *bytes_read)
{
	winhttp_stream *s = (winhttp_stream *)stream;
	winhttp_subtransport *t = OWNING_SUBTRANSPORT(s);
1035
	DWORD dw_bytes_read;
1036
	char replay_count = 0;
1037
	int error;
1038

1039
replay:
1040
	/* Enforce a reasonable cap on the number of replays */
1041
	if (replay_count++ >= GIT_HTTP_REPLAY_MAX) {
1042
		git_error_set(GIT_ERROR_HTTP, "too many redirects or authentication replays");
1043 1044 1045
		return -1;
	}

1046 1047 1048 1049 1050
	/* Connect if necessary */
	if (!s->request && winhttp_stream_connect(s) < 0)
		return -1;

	if (!s->received_response) {
1051
		DWORD status_code, status_code_length, content_type_length, bytes_written;
1052 1053 1054
		char expected_content_type_8[MAX_CONTENT_TYPE_LEN];
		wchar_t expected_content_type[MAX_CONTENT_TYPE_LEN], content_type[MAX_CONTENT_TYPE_LEN];

1055
		if (!s->sent_request) {
1056

1057
			if ((error = send_request(s, s->post_body_len, false)) < 0)
1058
				return error;
1059 1060 1061 1062 1063

			s->sent_request = 1;
		}

		if (s->chunked) {
1064
			GIT_ASSERT(s->verb == post_verb);
1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076

			/* Flush, if necessary */
			if (s->chunk_buffer_len > 0 &&
				write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0)
				return -1;

			s->chunk_buffer_len = 0;

			/* Write the final chunk. */
			if (!WinHttpWriteData(s->request,
				"0\r\n\r\n", 5,
				&bytes_written)) {
1077
				git_error_set(GIT_ERROR_OS, "failed to write final chunk");
1078 1079 1080 1081 1082 1083 1084 1085 1086 1087
				return -1;
			}
		}
		else if (s->post_body) {
			char *buffer;
			DWORD len = s->post_body_len, bytes_read;

			if (INVALID_SET_FILE_POINTER == SetFilePointer(s->post_body,
					0, 0, FILE_BEGIN) &&
				NO_ERROR != GetLastError()) {
1088
				git_error_set(GIT_ERROR_OS, "failed to reset file pointer");
1089 1090 1091
				return -1;
			}

1092
			buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE);
1093
			GIT_ERROR_CHECK_ALLOC(buffer);
1094 1095 1096 1097 1098

			while (len > 0) {
				DWORD bytes_written;

				if (!ReadFile(s->post_body, buffer,
1099
					min(CACHED_POST_BODY_BUF_SIZE, len),
1100 1101 1102
					&bytes_read, NULL) ||
					!bytes_read) {
					git__free(buffer);
1103
					git_error_set(GIT_ERROR_OS, "failed to read from temp file");
1104 1105 1106 1107 1108 1109
					return -1;
				}

				if (!WinHttpWriteData(s->request, buffer,
					bytes_read, &bytes_written)) {
					git__free(buffer);
1110
					git_error_set(GIT_ERROR_OS, "failed to write data");
1111 1112 1113 1114
					return -1;
				}

				len -= bytes_read;
1115
				GIT_ASSERT(bytes_read == bytes_written);
1116 1117 1118 1119 1120 1121 1122 1123 1124
			}

			git__free(buffer);

			/* Eagerly close the temp file */
			CloseHandle(s->post_body);
			s->post_body = NULL;
		}

1125
		if (!WinHttpReceiveResponse(s->request, 0)) {
1126
			git_error_set(GIT_ERROR_OS, "failed to receive response");
1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137
			return -1;
		}

		/* Verify that we got a 200 back */
		status_code_length = sizeof(status_code);

		if (!WinHttpQueryHeaders(s->request,
			WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
			WINHTTP_HEADER_NAME_BY_INDEX,
			&status_code, &status_code_length,
			WINHTTP_NO_HEADER_INDEX)) {
1138
				git_error_set(GIT_ERROR_OS, "failed to retrieve status code");
1139 1140 1141
				return -1;
		}

1142 1143 1144 1145 1146 1147 1148 1149 1150 1151
		/* The implementation of WinHTTP prior to Windows 7 will not
		 * redirect to an identical URI. Some Git hosters use self-redirects
		 * as part of their DoS mitigation strategy. Check first to see if we
		 * have a redirect status code, and that we haven't already streamed
		 * a post body. (We can't replay a streamed POST.) */
		if (!s->chunked &&
			(HTTP_STATUS_MOVED == status_code ||
			 HTTP_STATUS_REDIRECT == status_code ||
			 (HTTP_STATUS_REDIRECT_METHOD == status_code &&
			  get_verb == s->verb) ||
1152 1153
			 HTTP_STATUS_REDIRECT_KEEP_VERB == status_code ||
			 HTTP_STATUS_PERMANENT_REDIRECT == status_code)) {
1154 1155 1156

			/* Check for Windows 7. This workaround is only necessary on
			 * Windows Vista and earlier. Windows 7 is version 6.1. */
1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168
			wchar_t *location;
			DWORD location_length;
			char *location8;

			/* OK, fetch the Location header from the redirect. */
			if (WinHttpQueryHeaders(s->request,
				WINHTTP_QUERY_LOCATION,
				WINHTTP_HEADER_NAME_BY_INDEX,
				WINHTTP_NO_OUTPUT_BUFFER,
				&location_length,
				WINHTTP_NO_HEADER_INDEX) ||
				GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
1169
				git_error_set(GIT_ERROR_OS, "failed to read Location header");
1170 1171
				return -1;
			}
1172

1173
			location = git__malloc(location_length);
1174
			GIT_ERROR_CHECK_ALLOC(location);
1175 1176 1177 1178 1179 1180 1181

			if (!WinHttpQueryHeaders(s->request,
				WINHTTP_QUERY_LOCATION,
				WINHTTP_HEADER_NAME_BY_INDEX,
				location,
				&location_length,
				WINHTTP_NO_HEADER_INDEX)) {
1182
				git_error_set(GIT_ERROR_OS, "failed to read Location header");
1183
				git__free(location);
1184 1185
				return -1;
			}
1186 1187 1188

			/* Convert the Location header to UTF-8 */
			if (git__utf16_to_8_alloc(&location8, location) < 0) {
1189
				git_error_set(GIT_ERROR_OS, "failed to convert Location header to UTF-8");
1190 1191 1192 1193
				git__free(location);
				return -1;
			}

1194 1195 1196
			git__free(location);

			/* Replay the request */
1197
			winhttp_stream_close(s);
1198 1199 1200

			if (!git__prefixcmp_icase(location8, prefix_https)) {
				/* Upgrade to secure connection; disconnect and start over */
1201
				if (git_net_url_apply_redirect(&t->server.url, location8, s->service_url) < 0) {
1202
					git__free(location8);
1203
					return -1;
1204 1205
				}

1206 1207
				winhttp_close_connection(t);

1208
				if (winhttp_connect(t) < 0)
1209
					return -1;
1210
			}
1211 1212 1213

			git__free(location8);
			goto replay;
1214 1215
		}

1216
		/* Handle authentication failures */
1217 1218 1219 1220 1221 1222 1223 1224 1225 1226
		if (status_code == HTTP_STATUS_DENIED) {
			int error = acquire_credentials(s->request,
				&t->server,
				t->owner->url,
				t->owner->cred_acquire_cb,
				t->owner->cred_acquire_payload);

			if (error < 0) {
				return error;
			} else if (!error) {
1227
				GIT_ASSERT(t->server.cred);
1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240
				winhttp_stream_close(s);
				goto replay;
			}
		} else if (status_code == HTTP_STATUS_PROXY_AUTH_REQ) {
			int error = acquire_credentials(s->request,
				&t->proxy,
				t->owner->proxy.url,
				t->owner->proxy.credentials,
				t->owner->proxy.payload);

			if (error < 0) {
				return error;
			} else if (!error) {
1241
				GIT_ASSERT(t->proxy.cred);
1242 1243
				winhttp_stream_close(s);
				goto replay;
1244 1245 1246
			}
		}

1247
		if (HTTP_STATUS_OK != status_code) {
1248
			git_error_set(GIT_ERROR_HTTP, "request failed with status code: %lu", status_code);
1249 1250 1251 1252 1253
			return -1;
		}

		/* Verify that we got the correct content-type back */
		if (post_verb == s->verb)
1254
			p_snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-result", s->service);
1255
		else
1256
			p_snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-advertisement", s->service);
1257

1258
		if (git__utf8_to_16(expected_content_type, MAX_CONTENT_TYPE_LEN, expected_content_type_8) < 0) {
1259
			git_error_set(GIT_ERROR_OS, "failed to convert expected content-type to wide characters");
1260 1261 1262
			return -1;
		}

1263 1264 1265 1266 1267 1268 1269
		content_type_length = sizeof(content_type);

		if (!WinHttpQueryHeaders(s->request,
			WINHTTP_QUERY_CONTENT_TYPE,
			WINHTTP_HEADER_NAME_BY_INDEX,
			&content_type, &content_type_length,
			WINHTTP_NO_HEADER_INDEX)) {
1270
				git_error_set(GIT_ERROR_OS, "failed to retrieve response content-type");
1271 1272 1273 1274
				return -1;
		}

		if (wcscmp(expected_content_type, content_type)) {
1275
			git_error_set(GIT_ERROR_HTTP, "received unexpected content-type");
1276 1277 1278 1279 1280 1281 1282 1283
			return -1;
		}

		s->received_response = 1;
	}

	if (!WinHttpReadData(s->request,
		(LPVOID)buffer,
1284
		(DWORD)buf_size,
1285
		&dw_bytes_read))
1286
	{
1287
		git_error_set(GIT_ERROR_OS, "failed to read data");
1288 1289 1290
		return -1;
	}

1291 1292
	*bytes_read = dw_bytes_read;

1293 1294 1295
	return 0;
}

1296
static int winhttp_stream_write_single(
1297 1298 1299 1300 1301 1302
	git_smart_subtransport_stream *stream,
	const char *buffer,
	size_t len)
{
	winhttp_stream *s = (winhttp_stream *)stream;
	DWORD bytes_written;
1303
	int error;
1304 1305 1306 1307

	if (!s->request && winhttp_stream_connect(s) < 0)
		return -1;

1308 1309
	/* This implementation of write permits only a single call. */
	if (s->sent_request) {
1310
		git_error_set(GIT_ERROR_HTTP, "subtransport configured for only one write");
1311 1312 1313
		return -1;
	}

1314
	if ((error = send_request(s, len, false)) < 0)
1315
		return error;
1316 1317 1318 1319 1320 1321 1322

	s->sent_request = 1;

	if (!WinHttpWriteData(s->request,
			(LPCVOID)buffer,
			(DWORD)len,
			&bytes_written)) {
1323
		git_error_set(GIT_ERROR_OS, "failed to write data");
1324 1325 1326
		return -1;
	}

1327
	GIT_ASSERT((DWORD)len == bytes_written);
1328 1329 1330 1331

	return 0;
}

1332
static int put_uuid_string(LPWSTR buffer, size_t buffer_len_cch)
1333 1334 1335
{
	UUID uuid;
	RPC_STATUS status = UuidCreate(&uuid);
1336
	int result;
1337 1338 1339 1340

	if (RPC_S_OK != status &&
		RPC_S_UUID_LOCAL_ONLY != status &&
		RPC_S_UUID_NO_ADDRESS != status) {
1341
		git_error_set(GIT_ERROR_HTTP, "unable to generate name for temp file");
1342 1343 1344
		return -1;
	}

1345
	if (buffer_len_cch < UUID_LENGTH_CCH + 1) {
1346
		git_error_set(GIT_ERROR_HTTP, "buffer too small for name of temp file");
1347 1348 1349
		return -1;
	}

1350 1351 1352 1353 1354
#if !defined(__MINGW32__) || defined(MINGW_HAS_SECURE_API)
	result = swprintf_s(buffer, buffer_len_cch,
#else
	result = wsprintfW(buffer,
#endif
1355
		L"%08x%04x%04x%02x%02x%02x%02x%02x%02x%02x%02x",
1356 1357 1358 1359
		uuid.Data1, uuid.Data2, uuid.Data3,
		uuid.Data4[0], uuid.Data4[1], uuid.Data4[2], uuid.Data4[3],
		uuid.Data4[4], uuid.Data4[5], uuid.Data4[6], uuid.Data4[7]);

1360
	if (result < UUID_LENGTH_CCH) {
1361
		git_error_set(GIT_ERROR_OS, "unable to generate name for temp file");
1362 1363 1364 1365 1366 1367 1368 1369
		return -1;
	}

	return 0;
}

static int get_temp_file(LPWSTR buffer, DWORD buffer_len_cch)
{
1370
	size_t len;
1371 1372

	if (!GetTempPathW(buffer_len_cch, buffer)) {
1373
		git_error_set(GIT_ERROR_OS, "failed to get temp path");
1374 1375 1376 1377 1378
		return -1;
	}

	len = wcslen(buffer);

1379
	if (buffer[len - 1] != '\\' && len < buffer_len_cch)
1380 1381
		buffer[len++] = '\\';

1382
	if (put_uuid_string(&buffer[len], (size_t)buffer_len_cch - len) < 0)
1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415
		return -1;

	return 0;
}

static int winhttp_stream_write_buffered(
	git_smart_subtransport_stream *stream,
	const char *buffer,
	size_t len)
{
	winhttp_stream *s = (winhttp_stream *)stream;
	DWORD bytes_written;

	if (!s->request && winhttp_stream_connect(s) < 0)
		return -1;

	/* Buffer the payload, using a temporary file so we delegate
	 * memory management of the data to the operating system. */
	if (!s->post_body) {
		wchar_t temp_path[MAX_PATH + 1];

		if (get_temp_file(temp_path, MAX_PATH + 1) < 0)
			return -1;

		s->post_body = CreateFileW(temp_path,
			GENERIC_READ | GENERIC_WRITE,
			FILE_SHARE_DELETE, NULL,
			CREATE_NEW,
			FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE | FILE_FLAG_SEQUENTIAL_SCAN,
			NULL);

		if (INVALID_HANDLE_VALUE == s->post_body) {
			s->post_body = NULL;
1416
			git_error_set(GIT_ERROR_OS, "failed to create temporary file");
1417 1418 1419 1420
			return -1;
		}
	}

1421
	if (!WriteFile(s->post_body, buffer, (DWORD)len, &bytes_written, NULL)) {
1422
		git_error_set(GIT_ERROR_OS, "failed to write to temporary file");
1423 1424 1425
		return -1;
	}

1426
	GIT_ASSERT((DWORD)len == bytes_written);
1427

1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438
	s->post_body_len += bytes_written;

	return 0;
}

static int winhttp_stream_write_chunked(
	git_smart_subtransport_stream *stream,
	const char *buffer,
	size_t len)
{
	winhttp_stream *s = (winhttp_stream *)stream;
1439
	int error;
1440 1441 1442 1443 1444 1445 1446 1447 1448

	if (!s->request && winhttp_stream_connect(s) < 0)
		return -1;

	if (!s->sent_request) {
		/* Send Transfer-Encoding: chunked header */
		if (!WinHttpAddRequestHeaders(s->request,
			transfer_encoding, (ULONG) -1L,
			WINHTTP_ADDREQ_FLAG_ADD)) {
1449
			git_error_set(GIT_ERROR_OS, "failed to add a header to the request");
1450 1451 1452
			return -1;
		}

1453
		if ((error = send_request(s, 0, true)) < 0)
1454
			return error;
1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473

		s->sent_request = 1;
	}

	if (len > CACHED_POST_BODY_BUF_SIZE) {
		/* Flush, if necessary */
		if (s->chunk_buffer_len > 0) {
			if (write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0)
				return -1;

			s->chunk_buffer_len = 0;
		}

		/* Write chunk directly */
		if (write_chunk(s->request, buffer, len) < 0)
			return -1;
	}
	else {
		/* Append as much to the buffer as we can */
1474
		int count = (int)min(CACHED_POST_BODY_BUF_SIZE - s->chunk_buffer_len, len);
1475

1476
		if (!s->chunk_buffer) {
1477
			s->chunk_buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE);
1478 1479
			GIT_ERROR_CHECK_ALLOC(s->chunk_buffer);
		}
1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495

		memcpy(s->chunk_buffer + s->chunk_buffer_len, buffer, count);
		s->chunk_buffer_len += count;
		buffer += count;
		len -= count;

		/* Is the buffer full? If so, then flush */
		if (CACHED_POST_BODY_BUF_SIZE == s->chunk_buffer_len) {
			if (write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0)
				return -1;

			s->chunk_buffer_len = 0;

			/* Is there any remaining data from the source? */
			if (len > 0) {
				memcpy(s->chunk_buffer, buffer, len);
1496
				s->chunk_buffer_len = (unsigned int)len;
1497 1498 1499 1500
			}
		}
	}

1501 1502 1503 1504 1505 1506 1507
	return 0;
}

static void winhttp_stream_free(git_smart_subtransport_stream *stream)
{
	winhttp_stream *s = (winhttp_stream *)stream;

1508
	winhttp_stream_close(s);
1509 1510 1511
	git__free(s);
}

1512
static int winhttp_stream_alloc(winhttp_subtransport *t, winhttp_stream **stream)
1513 1514 1515 1516 1517 1518
{
	winhttp_stream *s;

	if (!stream)
		return -1;

1519
	s = git__calloc(1, sizeof(winhttp_stream));
1520
	GIT_ERROR_CHECK_ALLOC(s);
1521 1522 1523

	s->parent.subtransport = &t->parent;
	s->parent.read = winhttp_stream_read;
1524
	s->parent.write = winhttp_stream_write_single;
1525 1526
	s->parent.free = winhttp_stream_free;

1527 1528
	*stream = s;

1529 1530 1531 1532 1533
	return 0;
}

static int winhttp_uploadpack_ls(
	winhttp_subtransport *t,
1534
	winhttp_stream *s)
1535
{
1536 1537
	GIT_UNUSED(t);

1538 1539 1540 1541 1542 1543 1544 1545 1546
	s->service = upload_pack_service;
	s->service_url = upload_pack_ls_service_url;
	s->verb = get_verb;

	return 0;
}

static int winhttp_uploadpack(
	winhttp_subtransport *t,
1547
	winhttp_stream *s)
1548
{
1549 1550
	GIT_UNUSED(t);

1551 1552 1553
	s->service = upload_pack_service;
	s->service_url = upload_pack_service_url;
	s->verb = post_verb;
1554

1555 1556
	return 0;
}
1557

1558 1559 1560 1561
static int winhttp_receivepack_ls(
	winhttp_subtransport *t,
	winhttp_stream *s)
{
1562 1563
	GIT_UNUSED(t);

1564 1565 1566
	s->service = receive_pack_service;
	s->service_url = receive_pack_ls_service_url;
	s->verb = get_verb;
1567

1568 1569
	return 0;
}
1570

1571 1572 1573 1574
static int winhttp_receivepack(
	winhttp_subtransport *t,
	winhttp_stream *s)
{
1575 1576
	GIT_UNUSED(t);

1577 1578
	/* WinHTTP only supports Transfer-Encoding: chunked
	 * on Windows Vista (NT 6.0) and higher. */
1579
	s->chunked = git_has_win32_version(6, 0, 0);
1580 1581 1582 1583 1584 1585 1586 1587

	if (s->chunked)
		s->parent.write = winhttp_stream_write_chunked;
	else
		s->parent.write = winhttp_stream_write_buffered;

	s->service = receive_pack_service;
	s->service_url = receive_pack_service_url;
1588 1589 1590 1591 1592 1593 1594
	s->verb = post_verb;

	return 0;
}

static int winhttp_action(
	git_smart_subtransport_stream **stream,
1595
	git_smart_subtransport *subtransport,
1596 1597 1598
	const char *url,
	git_smart_service_t action)
{
1599 1600 1601 1602
	winhttp_subtransport *t = (winhttp_subtransport *)subtransport;
	winhttp_stream *s;
	int ret = -1;

1603
	if (!t->connection)
1604
		if ((ret = git_net_url_parse(&t->server.url, url)) < 0 ||
1605
			 (ret = winhttp_connect(t)) < 0)
1606
			return ret;
1607 1608 1609

	if (winhttp_stream_alloc(t, &s) < 0)
		return -1;
1610 1611 1612 1613 1614 1615 1616

	if (!stream)
		return -1;

	switch (action)
	{
		case GIT_SERVICE_UPLOADPACK_LS:
1617 1618
			ret = winhttp_uploadpack_ls(t, s);
			break;
1619 1620

		case GIT_SERVICE_UPLOADPACK:
1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632
			ret = winhttp_uploadpack(t, s);
			break;

		case GIT_SERVICE_RECEIVEPACK_LS:
			ret = winhttp_receivepack_ls(t, s);
			break;

		case GIT_SERVICE_RECEIVEPACK:
			ret = winhttp_receivepack(t, s);
			break;

		default:
1633
			GIT_ASSERT(0);
1634 1635
	}

1636 1637 1638 1639
	if (!ret)
		*stream = &s->parent;

	return ret;
1640 1641
}

1642
static int winhttp_close(git_smart_subtransport *subtransport)
1643
{
1644 1645
	winhttp_subtransport *t = (winhttp_subtransport *)subtransport;

1646 1647
	git_net_url_dispose(&t->server.url);
	git_net_url_dispose(&t->proxy.url);
1648

1649 1650 1651
	if (t->server.cred) {
		t->server.cred->free(t->server.cred);
		t->server.cred = NULL;
1652 1653
	}

1654 1655 1656
	if (t->proxy.cred) {
		t->proxy.cred->free(t->proxy.cred);
		t->proxy.cred = NULL;
1657 1658
	}

1659
	return winhttp_close_connection(t);
1660 1661 1662 1663 1664 1665 1666 1667
}

static void winhttp_free(git_smart_subtransport *subtransport)
{
	winhttp_subtransport *t = (winhttp_subtransport *)subtransport;

	winhttp_close(subtransport);

1668 1669 1670
	git__free(t);
}

1671
int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner, void *param)
1672 1673 1674
{
	winhttp_subtransport *t;

1675 1676
	GIT_UNUSED(param);

1677 1678 1679
	if (!out)
		return -1;

1680
	t = git__calloc(1, sizeof(winhttp_subtransport));
1681
	GIT_ERROR_CHECK_ALLOC(t);
1682

1683
	t->owner = (transport_smart *)owner;
1684
	t->parent.action = winhttp_action;
1685
	t->parent.close = winhttp_close;
1686 1687 1688 1689 1690 1691 1692
	t->parent.free = winhttp_free;

	*out = (git_smart_subtransport *) t;
	return 0;
}

#endif /* GIT_WINHTTP */