/* * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #ifdef GIT_WINHTTP #include "git2.h" #include "git2/transport.h" #include "buffer.h" #include "posix.h" #include "netops.h" #include "smart.h" #include "remote.h" #include "repository.h" #include "global.h" #include #include /* For IInternetSecurityManager zone check */ #include #include #define WIDEN2(s) L ## s #define WIDEN(s) WIDEN2(s) #define MAX_CONTENT_TYPE_LEN 100 #define WINHTTP_OPTION_PEERDIST_EXTENSION_STATE 109 #define CACHED_POST_BODY_BUF_SIZE 4096 #define UUID_LENGTH_CCH 32 #define TIMEOUT_INFINITE -1 #define DEFAULT_CONNECT_TIMEOUT 60000 #ifndef WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH #define WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH 0 #endif #ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS_1_1 # define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 0x00000200 #endif #ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS_1_2 # define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 0x00000800 #endif 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"; 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"; static const wchar_t *get_verb = L"GET"; static const wchar_t *post_verb = L"POST"; static const wchar_t *pragma_nocache = L"Pragma: no-cache"; static const wchar_t *transfer_encoding = L"Transfer-Encoding: chunked"; static const int no_check_cert_flags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_UNKNOWN_CA; #if defined(__MINGW32__) static const CLSID CLSID_InternetSecurityManager_mingw = { 0x7B8A2D94, 0x0AC9, 0x11D1, { 0x89, 0x6C, 0x00, 0xC0, 0x4F, 0xB6, 0xBF, 0xC4 } }; static const IID IID_IInternetSecurityManager_mingw = { 0x79EAC9EE, 0xBAF9, 0x11CE, { 0x8C, 0x82, 0x00, 0xAA, 0x00, 0x4B, 0xA9, 0x0B } }; # define CLSID_InternetSecurityManager CLSID_InternetSecurityManager_mingw # define IID_IInternetSecurityManager IID_IInternetSecurityManager_mingw #endif #define OWNING_SUBTRANSPORT(s) ((winhttp_subtransport *)(s)->parent.subtransport) typedef enum { GIT_WINHTTP_AUTH_BASIC = 1, GIT_WINHTTP_AUTH_NTLM = 2, GIT_WINHTTP_AUTH_NEGOTIATE = 4, GIT_WINHTTP_AUTH_DIGEST = 8, } winhttp_authmechanism_t; typedef struct { git_smart_subtransport_stream parent; const char *service; const char *service_url; const wchar_t *verb; HINTERNET request; wchar_t *request_uri; char *chunk_buffer; unsigned chunk_buffer_len; HANDLE post_body; DWORD post_body_len; unsigned sent_request : 1, received_response : 1, chunked : 1; } winhttp_stream; typedef struct { git_smart_subtransport parent; transport_smart *owner; gitno_connection_data connection_data; gitno_connection_data proxy_connection_data; git_cred *cred; git_cred *url_cred; git_cred *proxy_cred; int auth_mechanisms; HINTERNET session; HINTERNET connection; } winhttp_subtransport; static int _apply_userpass_credential(HINTERNET request, DWORD target, DWORD scheme, git_cred *cred) { git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; wchar_t *user, *pass; int user_len = 0, pass_len = 0, error = 0; if ((error = user_len = git__utf8_to_16_alloc(&user, c->username)) < 0) goto done; if ((error = pass_len = git__utf8_to_16_alloc(&pass, c->password)) < 0) goto done; if (!WinHttpSetCredentials(request, target, scheme, user, pass, NULL)) { giterr_set(GITERR_OS, "failed to set credentials"); error = -1; } 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)); git__free(user); git__free(pass); return error; } static int apply_userpass_credential_proxy(HINTERNET request, git_cred *cred, int mechanisms) { if (GIT_WINHTTP_AUTH_DIGEST & mechanisms) { return _apply_userpass_credential(request, WINHTTP_AUTH_TARGET_PROXY, WINHTTP_AUTH_SCHEME_DIGEST, cred); } return _apply_userpass_credential(request, WINHTTP_AUTH_TARGET_PROXY, WINHTTP_AUTH_SCHEME_BASIC, cred); } static int apply_userpass_credential(HINTERNET request, int mechanisms, git_cred *cred) { DWORD native_scheme; if ((mechanisms & GIT_WINHTTP_AUTH_NTLM) || (mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE)) { native_scheme = WINHTTP_AUTH_SCHEME_NTLM; } else if (mechanisms & GIT_WINHTTP_AUTH_BASIC) { native_scheme = WINHTTP_AUTH_SCHEME_BASIC; } else { giterr_set(GITERR_NET, "invalid authentication scheme"); return -1; } return _apply_userpass_credential(request, WINHTTP_AUTH_TARGET_SERVER, native_scheme, cred); } static int apply_default_credentials(HINTERNET request, int mechanisms) { /* Either the caller explicitly requested that default credentials be passed, * or our fallback credential callback was invoked and checked that the target * URI was in the appropriate Internet Explorer security zone. By setting this * flag, we guarantee that the credentials are delivered by WinHTTP. 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 ((mechanisms & GIT_WINHTTP_AUTH_NTLM) == 0 && (mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE) == 0) { giterr_set(GITERR_NET, "invalid authentication scheme"); return -1; } if (!WinHttpSetOption(request, WINHTTP_OPTION_AUTOLOGON_POLICY, &data, sizeof(DWORD))) return -1; return 0; } static int fallback_cred_acquire_cb( git_cred **cred, const char *url, const char *username_from_url, unsigned int allowed_types, void *payload) { int error = 1; GIT_UNUSED(username_from_url); GIT_UNUSED(payload); /* If the target URI supports integrated Windows authentication * as an authentication mechanism */ if (GIT_CREDTYPE_DEFAULT & allowed_types) { wchar_t *wide_url; /* Convert URL to wide characters */ if (git__utf8_to_16_alloc(&wide_url, url) < 0) { giterr_set(GITERR_OS, "failed to convert string to wide form"); return -1; } if (SUCCEEDED(CoInitializeEx(NULL, COINIT_MULTITHREADED))) { 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)) { git_cred *existing = *cred; if (existing) existing->free(existing); /* Then use default Windows credentials to authenticate this request */ error = git_cred_default_new(cred); } pISM->lpVtbl->Release(pISM); } CoUninitialize(); } git__free(wide_url); } return error; } 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); git_cert_x509 cert; /* If there is no override, we should fail if WinHTTP doesn't think it's fine */ if (t->owner->certificate_check_cb == NULL && !valid) { if (!giterr_last()) giterr_set(GITERR_NET, "unknown certificate check failure"); return GIT_ECERTIFICATE; } if (t->owner->certificate_check_cb == NULL || !t->connection_data.use_ssl) return 0; if (!WinHttpQueryOption(s->request, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &cert_ctx, &cert_ctx_size)) { giterr_set(GITERR_OS, "failed to get server certificate"); return -1; } giterr_clear(); cert.parent.cert_type = GIT_CERT_X509; cert.data = cert_ctx->pbCertEncoded; cert.len = cert_ctx->cbCertEncoded; error = t->owner->certificate_check_cb((git_cert *) &cert, valid, t->connection_data.host, t->owner->cred_acquire_payload); CertFreeCertificateContext(cert_ctx); if (error < 0 && !giterr_last()) giterr_set(GITERR_NET, "user cancelled certificate check"); return error; } 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; } /** * Extract the url and password from a URL. The outputs are pointers * into the input. */ static int userpass_from_url(wchar_t **user, int *user_len, wchar_t **pass, int *pass_len, const wchar_t *url, int url_len) { URL_COMPONENTS components = { 0 }; components.dwStructSize = sizeof(components); /* These tell WinHttpCrackUrl that we're interested in the fields */ components.dwUserNameLength = 1; components.dwPasswordLength = 1; if (!WinHttpCrackUrl(url, url_len, 0, &components)) { giterr_set(GITERR_OS, "failed to extract user/pass from url"); return -1; } *user = components.lpszUserName; *user_len = components.dwUserNameLength; *pass = components.lpszPassword; *pass_len = components.dwPasswordLength; return 0; } #define SCHEME_HTTP "http://" #define SCHEME_HTTPS "https://" static int winhttp_stream_connect(winhttp_stream *s) { winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); git_buf buf = GIT_BUF_INIT; char *proxy_url = NULL; wchar_t ct[MAX_CONTENT_TYPE_LEN]; LPCWSTR types[] = { L"*/*", NULL }; BOOL peerdist = FALSE; int error = -1; unsigned long disable_redirects = WINHTTP_DISABLE_REDIRECTS; int default_timeout = TIMEOUT_INFINITE; int default_connect_timeout = DEFAULT_CONNECT_TIMEOUT; size_t i; const git_proxy_options *proxy_opts; /* Prepare URL */ git_buf_printf(&buf, "%s%s", t->connection_data.path, s->service_url); if (git_buf_oom(&buf)) return -1; /* Convert URL to wide characters */ if (git__utf8_to_16_alloc(&s->request_uri, git_buf_cstr(&buf)) < 0) { giterr_set(GITERR_OS, "failed to convert string to wide form"); goto on_error; } /* Establish request */ s->request = WinHttpOpenRequest( t->connection, s->verb, s->request_uri, NULL, WINHTTP_NO_REFERER, types, t->connection_data.use_ssl ? WINHTTP_FLAG_SECURE : 0); if (!s->request) { giterr_set(GITERR_OS, "failed to open request"); goto on_error; } if (!WinHttpSetTimeouts(s->request, default_timeout, default_connect_timeout, default_timeout, default_timeout)) { giterr_set(GITERR_OS, "failed to set timeouts for WinHTTP"); goto on_error; } proxy_opts = &t->owner->proxy; if (proxy_opts->type == GIT_PROXY_AUTO) { /* Set proxy if necessary */ if (git_remote__get_http_proxy(t->owner->owner, !!t->connection_data.use_ssl, &proxy_url) < 0) goto on_error; } else if (proxy_opts->type == GIT_PROXY_SPECIFIED) { proxy_url = git__strdup(proxy_opts->url); GITERR_CHECK_ALLOC(proxy_url); } if (proxy_url) { git_buf processed_url = GIT_BUF_INIT; WINHTTP_PROXY_INFO proxy_info; wchar_t *proxy_wide; if (!git__prefixcmp(proxy_url, SCHEME_HTTP)) { t->proxy_connection_data.use_ssl = false; } else if (!git__prefixcmp(proxy_url, SCHEME_HTTPS)) { t->proxy_connection_data.use_ssl = true; } else { giterr_set(GITERR_NET, "invalid URL: '%s'", proxy_url); return -1; } gitno_connection_data_free_ptrs(&t->proxy_connection_data); if ((error = gitno_extract_url_parts(&t->proxy_connection_data.host, &t->proxy_connection_data.port, NULL, &t->proxy_connection_data.user, &t->proxy_connection_data.pass, proxy_url, NULL)) < 0) goto on_error; if (t->proxy_connection_data.user && t->proxy_connection_data.pass) { if (t->proxy_cred) { t->proxy_cred->free(t->proxy_cred); } if ((error = git_cred_userpass_plaintext_new(&t->proxy_cred, t->proxy_connection_data.user, t->proxy_connection_data.pass)) < 0) goto on_error; } if (t->proxy_connection_data.use_ssl) git_buf_PUTS(&processed_url, SCHEME_HTTPS); else git_buf_PUTS(&processed_url, SCHEME_HTTP); git_buf_puts(&processed_url, t->proxy_connection_data.host); if (t->proxy_connection_data.port) git_buf_printf(&processed_url, ":%s", t->proxy_connection_data.port); if (git_buf_oom(&processed_url)) { error = -1; goto on_error; } /* Convert URL to wide characters */ error = git__utf8_to_16_alloc(&proxy_wide, processed_url.ptr); git_buf_free(&processed_url); if (error < 0) goto on_error; proxy_info.dwAccessType = WINHTTP_ACCESS_TYPE_NAMED_PROXY; proxy_info.lpszProxy = proxy_wide; proxy_info.lpszProxyBypass = NULL; if (!WinHttpSetOption(s->request, WINHTTP_OPTION_PROXY, &proxy_info, sizeof(WINHTTP_PROXY_INFO))) { giterr_set(GITERR_OS, "failed to set proxy"); git__free(proxy_wide); goto on_error; } git__free(proxy_wide); if (t->proxy_cred) { if (t->proxy_cred->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT) { if ((error = apply_userpass_credential_proxy(s->request, t->proxy_cred, t->auth_mechanisms)) < 0) goto on_error; } } } /* 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 */ if (!WinHttpSetOption(s->request, WINHTTP_OPTION_DISABLE_FEATURE, &disable_redirects, sizeof(disable_redirects))) { giterr_set(GITERR_OS, "failed to disable redirects"); goto on_error; } /* 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)) { giterr_set(GITERR_OS, "failed to add a header to the request"); goto on_error; } if (post_verb == s->verb) { /* Send Content-Type and Accept headers -- only necessary on a POST */ git_buf_clear(&buf); if (git_buf_printf(&buf, "Content-Type: application/x-git-%s-request", s->service) < 0) goto on_error; if (git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_buf_cstr(&buf)) < 0) { giterr_set(GITERR_OS, "failed to convert content-type to wide characters"); goto on_error; } if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG)-1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { giterr_set(GITERR_OS, "failed to add a header to the request"); goto on_error; } git_buf_clear(&buf); if (git_buf_printf(&buf, "Accept: application/x-git-%s-result", s->service) < 0) goto on_error; if (git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_buf_cstr(&buf)) < 0) { giterr_set(GITERR_OS, "failed to convert accept header to wide characters"); goto on_error; } if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG)-1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { giterr_set(GITERR_OS, "failed to add a header to the request"); goto on_error; } } 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) { giterr_set(GITERR_OS, "failed to convert custom header to wide characters"); goto on_error; } if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG)-1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { giterr_set(GITERR_OS, "failed to add a header to the request"); goto on_error; } } } /* If requested, disable certificate validation */ if (t->connection_data.use_ssl) { int flags; if (t->owner->parent.read_flags(&t->owner->parent, &flags) < 0) goto on_error; } /* If we have a credential on the subtransport, apply it to the request */ if (t->cred && t->cred->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT && apply_userpass_credential(s->request, t->auth_mechanisms, t->cred) < 0) goto on_error; else if (t->cred && t->cred->credtype == GIT_CREDTYPE_DEFAULT && apply_default_credentials(s->request, t->auth_mechanisms) < 0) goto on_error; /* If no other credentials have been applied and the URL has username and * password, use those */ if (!t->cred && t->connection_data.user && t->connection_data.pass) { if (!t->url_cred && git_cred_userpass_plaintext_new(&t->url_cred, t->connection_data.user, t->connection_data.pass) < 0) goto on_error; if (apply_userpass_credential(s->request, GIT_WINHTTP_AUTH_BASIC, t->url_cred) < 0) goto on_error; } /* We've done everything up to calling WinHttpSendRequest. */ error = 0; on_error: if (error < 0) winhttp_stream_close(s); git__free(proxy_url); git_buf_free(&buf); return error; } static int parse_unauthorized_response( HINTERNET request, int *allowed_types, int *allowed_mechanisms) { DWORD supported, first, target; *allowed_types = 0; *allowed_mechanisms = 0; /* WinHttpQueryHeaders() must be called before WinHttpQueryAuthSchemes(). * We can assume this was already done, since we know we are unauthorized. */ if (!WinHttpQueryAuthSchemes(request, &supported, &first, &target)) { giterr_set(GITERR_OS, "failed to parse supported auth schemes"); return -1; } if (WINHTTP_AUTH_SCHEME_NTLM & supported) { *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; *allowed_types |= GIT_CREDTYPE_DEFAULT; *allowed_mechanisms = GIT_WINHTTP_AUTH_NEGOTIATE; } if (WINHTTP_AUTH_SCHEME_NEGOTIATE & supported) { *allowed_types |= GIT_CREDTYPE_DEFAULT; *allowed_mechanisms = GIT_WINHTTP_AUTH_NEGOTIATE; } if (WINHTTP_AUTH_SCHEME_BASIC & supported) { *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; *allowed_mechanisms |= GIT_WINHTTP_AUTH_BASIC; } if (WINHTTP_AUTH_SCHEME_DIGEST & supported) { *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; *allowed_mechanisms |= GIT_WINHTTP_AUTH_DIGEST; } 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 */ git_buf_printf(&buf, "%X\r\n", len); if (git_buf_oom(&buf)) return -1; if (!WinHttpWriteData(request, git_buf_cstr(&buf), (DWORD)git_buf_len(&buf), &bytes_written)) { git_buf_free(&buf); giterr_set(GITERR_OS, "failed to write chunk header"); return -1; } git_buf_free(&buf); /* Chunk body */ if (!WinHttpWriteData(request, buffer, (DWORD)len, &bytes_written)) { giterr_set(GITERR_OS, "failed to write chunk"); return -1; } /* Chunk footer */ if (!WinHttpWriteData(request, "\r\n", 2, &bytes_written)) { giterr_set(GITERR_OS, "failed to write chunk footer"); return -1; } return 0; } static int winhttp_close_connection(winhttp_subtransport *t) { int ret = 0; if (t->connection) { if (!WinHttpCloseHandle(t->connection)) { giterr_set(GITERR_OS, "unable to close connection"); ret = -1; } t->connection = NULL; } if (t->session) { if (!WinHttpCloseHandle(t->session)) { giterr_set(GITERR_OS, "unable to close session"); ret = -1; } t->session = NULL; } return ret; } static int user_agent(git_buf *ua) { const char *custom = git_libgit2__user_agent(); git_buf_clear(ua); git_buf_PUTS(ua, "git/1.0 ("); if (custom) git_buf_puts(ua, custom); else git_buf_PUTS(ua, "libgit2 " LIBGIT2_VERSION); return git_buf_putc(ua, ')'); } static void CALLBACK winhttp_status( HINTERNET connection, DWORD_PTR ctx, DWORD code, LPVOID info, DWORD info_len) { DWORD status; if (code != WINHTTP_CALLBACK_STATUS_SECURE_FAILURE) return; status = *((DWORD *)info); if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_CN_INVALID)) giterr_set(GITERR_NET, "SSL certificate issued for different common name"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_DATE_INVALID)) giterr_set(GITERR_NET, "SSL certificate has expired"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CA)) giterr_set(GITERR_NET, "SSL certificate signed by unknown CA"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CERT)) giterr_set(GITERR_NET, "SSL certificate is invalid"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED)) giterr_set(GITERR_NET, "certificate revocation check failed"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REVOKED)) giterr_set(GITERR_NET, "SSL certificate was revoked"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_SECURITY_CHANNEL_ERROR)) giterr_set(GITERR_NET, "security libraries could not be loaded"); else giterr_set(GITERR_NET, "unknown security error %d", status); } static int winhttp_connect( winhttp_subtransport *t) { wchar_t *wide_host; int32_t port; wchar_t *wide_ua; git_buf ua = GIT_BUF_INIT; int error = -1; int default_timeout = TIMEOUT_INFINITE; int default_connect_timeout = DEFAULT_CONNECT_TIMEOUT; DWORD protocols = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2; t->session = NULL; t->connection = NULL; /* Prepare port */ if (git__strntol32(&port, t->connection_data.port, strlen(t->connection_data.port), NULL, 10) < 0) return -1; /* Prepare host */ if (git__utf8_to_16_alloc(&wide_host, t->connection_data.host) < 0) { giterr_set(GITERR_OS, "unable to convert host to wide characters"); return -1; } if ((error = user_agent(&ua)) < 0) { git__free(wide_host); return error; } if (git__utf8_to_16_alloc(&wide_ua, git_buf_cstr(&ua)) < 0) { giterr_set(GITERR_OS, "unable to convert host to wide characters"); git__free(wide_host); git_buf_free(&ua); return -1; } git_buf_free(&ua); /* Establish session */ t->session = WinHttpOpen( wide_ua, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!t->session) { giterr_set(GITERR_OS, "failed to init WinHTTP"); goto on_error; } /* * Do a best-effort attempt to enable TLS 1.2 but allow this to * fail; if TLS 1.2 support is not available for some reason, * ignore the failure (it will keep the default protocols). */ WinHttpSetOption(t->session, WINHTTP_OPTION_SECURE_PROTOCOLS, &protocols, sizeof(protocols)); if (!WinHttpSetTimeouts(t->session, default_timeout, default_connect_timeout, default_timeout, default_timeout)) { giterr_set(GITERR_OS, "failed to set timeouts for WinHTTP"); goto on_error; } /* Establish connection */ t->connection = WinHttpConnect( t->session, wide_host, (INTERNET_PORT) port, 0); if (!t->connection) { giterr_set(GITERR_OS, "failed to connect to host"); goto on_error; } if (WinHttpSetStatusCallback(t->connection, winhttp_status, WINHTTP_CALLBACK_FLAG_SECURE_FAILURE, 0) == WINHTTP_INVALID_STATUS_CALLBACK) { giterr_set(GITERR_OS, "failed to set status callback"); goto on_error; } error = 0; on_error: if (error < 0) winhttp_close_connection(t); git__free(wide_host); git__free(wide_ua); return error; } static int do_send_request(winhttp_stream *s, size_t len, int ignore_length) { if (ignore_length) { if (!WinHttpSendRequest(s->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH, 0)) { return -1; } } else { if (!WinHttpSendRequest(s->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, len, 0)) { return -1; } } return 0; } static int send_request(winhttp_stream *s, size_t len, int ignore_length) { int request_failed = 0, cert_valid = 1, error = 0; DWORD ignore_flags; giterr_clear(); if ((error = do_send_request(s, len, ignore_length)) < 0) { if (GetLastError() != ERROR_WINHTTP_SECURE_FAILURE) { giterr_set(GITERR_OS, "failed to send request"); return -1; } request_failed = 1; cert_valid = 0; } giterr_clear(); if ((error = certificate_check(s, cert_valid)) < 0) { if (!giterr_last()) giterr_set(GITERR_OS, "user cancelled certificate check"); return error; } /* if neither the request nor the certificate check returned errors, we're done */ if (!request_failed) return 0; ignore_flags = no_check_cert_flags; if (!WinHttpSetOption(s->request, WINHTTP_OPTION_SECURITY_FLAGS, &ignore_flags, sizeof(ignore_flags))) { giterr_set(GITERR_OS, "failed to set security options"); return -1; } if ((error = do_send_request(s, len, ignore_length)) < 0) giterr_set(GITERR_OS, "failed to send request with unchecked certificate"); return error; } 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); DWORD dw_bytes_read; char replay_count = 0; int error; replay: /* Enforce a reasonable cap on the number of replays */ if (++replay_count >= 7) { giterr_set(GITERR_NET, "too many redirects or authentication replays"); return -1; } /* Connect if necessary */ if (!s->request && winhttp_stream_connect(s) < 0) return -1; if (!s->received_response) { DWORD status_code, status_code_length, content_type_length, bytes_written; char expected_content_type_8[MAX_CONTENT_TYPE_LEN]; wchar_t expected_content_type[MAX_CONTENT_TYPE_LEN], content_type[MAX_CONTENT_TYPE_LEN]; if (!s->sent_request) { if ((error = send_request(s, s->post_body_len, 0)) < 0) return error; s->sent_request = 1; } if (s->chunked) { assert(s->verb == post_verb); /* 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)) { giterr_set(GITERR_OS, "failed to write final chunk"); 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()) { giterr_set(GITERR_OS, "failed to reset file pointer"); return -1; } buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE); while (len > 0) { DWORD bytes_written; if (!ReadFile(s->post_body, buffer, min(CACHED_POST_BODY_BUF_SIZE, len), &bytes_read, NULL) || !bytes_read) { git__free(buffer); giterr_set(GITERR_OS, "failed to read from temp file"); return -1; } if (!WinHttpWriteData(s->request, buffer, bytes_read, &bytes_written)) { git__free(buffer); giterr_set(GITERR_OS, "failed to write data"); return -1; } len -= bytes_read; assert(bytes_read == bytes_written); } git__free(buffer); /* Eagerly close the temp file */ CloseHandle(s->post_body); s->post_body = NULL; } if (!WinHttpReceiveResponse(s->request, 0)) { giterr_set(GITERR_OS, "failed to receive response"); 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)) { giterr_set(GITERR_OS, "failed to retrieve status code"); return -1; } /* 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) || HTTP_STATUS_REDIRECT_KEEP_VERB == status_code)) { /* Check for Windows 7. This workaround is only necessary on * Windows Vista and earlier. Windows 7 is version 6.1. */ 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) { giterr_set(GITERR_OS, "failed to read Location header"); return -1; } location = git__malloc(location_length); GITERR_CHECK_ALLOC(location); if (!WinHttpQueryHeaders(s->request, WINHTTP_QUERY_LOCATION, WINHTTP_HEADER_NAME_BY_INDEX, location, &location_length, WINHTTP_NO_HEADER_INDEX)) { giterr_set(GITERR_OS, "failed to read Location header"); git__free(location); return -1; } /* Convert the Location header to UTF-8 */ if (git__utf16_to_8_alloc(&location8, location) < 0) { giterr_set(GITERR_OS, "failed to convert Location header to UTF-8"); git__free(location); return -1; } git__free(location); /* Replay the request */ winhttp_stream_close(s); if (!git__prefixcmp_icase(location8, prefix_https)) { /* Upgrade to secure connection; disconnect and start over */ if (gitno_connection_data_from_url(&t->connection_data, location8, s->service_url) < 0) { git__free(location8); return -1; } winhttp_close_connection(t); if (winhttp_connect(t) < 0) return -1; } git__free(location8); goto replay; } /* Handle proxy authentication failures */ if (status_code == HTTP_STATUS_PROXY_AUTH_REQ) { int allowed_types; if (parse_unauthorized_response(s->request, &allowed_types, &t->auth_mechanisms) < 0) return -1; /* TODO: extract the username from the url, no payload? */ if (t->owner->proxy.credentials) { int cred_error = 1; cred_error = t->owner->proxy.credentials(&t->proxy_cred, t->owner->proxy.url, NULL, allowed_types, t->owner->proxy.payload); if (cred_error < 0) return cred_error; } winhttp_stream_close(s); goto replay; } /* Handle authentication failures */ if (HTTP_STATUS_DENIED == status_code && get_verb == s->verb) { int allowed_types; if (parse_unauthorized_response(s->request, &allowed_types, &t->auth_mechanisms) < 0) return -1; if (allowed_types) { int cred_error = 1; git_cred_free(t->cred); t->cred = NULL; /* Start with the user-supplied credential callback, if present */ if (t->owner->cred_acquire_cb) { cred_error = t->owner->cred_acquire_cb(&t->cred, t->owner->url, t->connection_data.user, allowed_types, t->owner->cred_acquire_payload); /* Treat GIT_PASSTHROUGH as though git_cred_acquire_cb isn't set */ if (cred_error == GIT_PASSTHROUGH) cred_error = 1; else if (cred_error < 0) return cred_error; } /* Invoke the fallback credentials acquisition callback if necessary */ if (cred_error > 0) { cred_error = fallback_cred_acquire_cb(&t->cred, t->owner->url, t->connection_data.user, allowed_types, NULL); if (cred_error < 0) return cred_error; } if (!cred_error) { assert(t->cred); winhttp_stream_close(s); /* Successfully acquired a credential */ goto replay; } } } if (HTTP_STATUS_OK != status_code) { giterr_set(GITERR_NET, "request failed with status code: %d", status_code); return -1; } /* Verify that we got the correct content-type back */ if (post_verb == s->verb) p_snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-result", s->service); else p_snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-advertisement", s->service); if (git__utf8_to_16(expected_content_type, MAX_CONTENT_TYPE_LEN, expected_content_type_8) < 0) { giterr_set(GITERR_OS, "failed to convert expected content-type to wide characters"); return -1; } 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)) { giterr_set(GITERR_OS, "failed to retrieve response content-type"); return -1; } if (wcscmp(expected_content_type, content_type)) { giterr_set(GITERR_NET, "received unexpected content-type"); return -1; } s->received_response = 1; } if (!WinHttpReadData(s->request, (LPVOID)buffer, (DWORD)buf_size, &dw_bytes_read)) { giterr_set(GITERR_OS, "failed to read data"); return -1; } *bytes_read = dw_bytes_read; return 0; } static int winhttp_stream_write_single( git_smart_subtransport_stream *stream, const char *buffer, size_t len) { winhttp_stream *s = (winhttp_stream *)stream; DWORD bytes_written; int error; if (!s->request && winhttp_stream_connect(s) < 0) return -1; /* This implementation of write permits only a single call. */ if (s->sent_request) { giterr_set(GITERR_NET, "subtransport configured for only one write"); return -1; } if ((error = send_request(s, len, 0)) < 0) return error; s->sent_request = 1; if (!WinHttpWriteData(s->request, (LPCVOID)buffer, (DWORD)len, &bytes_written)) { giterr_set(GITERR_OS, "failed to write data"); return -1; } assert((DWORD)len == bytes_written); return 0; } static int put_uuid_string(LPWSTR buffer, size_t buffer_len_cch) { UUID uuid; RPC_STATUS status = UuidCreate(&uuid); int result; if (RPC_S_OK != status && RPC_S_UUID_LOCAL_ONLY != status && RPC_S_UUID_NO_ADDRESS != status) { giterr_set(GITERR_NET, "unable to generate name for temp file"); return -1; } if (buffer_len_cch < UUID_LENGTH_CCH + 1) { giterr_set(GITERR_NET, "buffer too small for name of temp file"); return -1; } #if !defined(__MINGW32__) || defined(MINGW_HAS_SECURE_API) result = swprintf_s(buffer, buffer_len_cch, #else result = wsprintfW(buffer, #endif L"%08x%04x%04x%02x%02x%02x%02x%02x%02x%02x%02x", 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]); if (result < UUID_LENGTH_CCH) { giterr_set(GITERR_OS, "unable to generate name for temp file"); return -1; } return 0; } static int get_temp_file(LPWSTR buffer, DWORD buffer_len_cch) { size_t len; if (!GetTempPathW(buffer_len_cch, buffer)) { giterr_set(GITERR_OS, "failed to get temp path"); return -1; } len = wcslen(buffer); if (buffer[len - 1] != '\\' && len < buffer_len_cch) buffer[len++] = '\\'; if (put_uuid_string(&buffer[len], (size_t)buffer_len_cch - len) < 0) 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; giterr_set(GITERR_OS, "failed to create temporary file"); return -1; } } if (!WriteFile(s->post_body, buffer, (DWORD)len, &bytes_written, NULL)) { giterr_set(GITERR_OS, "failed to write to temporary file"); return -1; } assert((DWORD)len == bytes_written); 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; int error; 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)) { giterr_set(GITERR_OS, "failed to add a header to the request"); return -1; } if ((error = send_request(s, 0, 1)) < 0) return error; 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 */ int count = (int)min(CACHED_POST_BODY_BUF_SIZE - s->chunk_buffer_len, len); if (!s->chunk_buffer) s->chunk_buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE); 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); s->chunk_buffer_len = (unsigned int)len; } } } return 0; } static void winhttp_stream_free(git_smart_subtransport_stream *stream) { winhttp_stream *s = (winhttp_stream *)stream; winhttp_stream_close(s); git__free(s); } static int winhttp_stream_alloc(winhttp_subtransport *t, winhttp_stream **stream) { winhttp_stream *s; if (!stream) return -1; s = git__calloc(1, sizeof(winhttp_stream)); GITERR_CHECK_ALLOC(s); s->parent.subtransport = &t->parent; s->parent.read = winhttp_stream_read; s->parent.write = winhttp_stream_write_single; s->parent.free = winhttp_stream_free; *stream = s; return 0; } static int winhttp_uploadpack_ls( winhttp_subtransport *t, winhttp_stream *s) { GIT_UNUSED(t); 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, winhttp_stream *s) { GIT_UNUSED(t); s->service = upload_pack_service; s->service_url = upload_pack_service_url; s->verb = post_verb; return 0; } static int winhttp_receivepack_ls( winhttp_subtransport *t, winhttp_stream *s) { GIT_UNUSED(t); s->service = receive_pack_service; s->service_url = receive_pack_ls_service_url; s->verb = get_verb; return 0; } static int winhttp_receivepack( winhttp_subtransport *t, winhttp_stream *s) { GIT_UNUSED(t); /* WinHTTP only supports Transfer-Encoding: chunked * on Windows Vista (NT 6.0) and higher. */ s->chunked = git_has_win32_version(6, 0, 0); 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; s->verb = post_verb; return 0; } static int winhttp_action( git_smart_subtransport_stream **stream, git_smart_subtransport *subtransport, const char *url, git_smart_service_t action) { winhttp_subtransport *t = (winhttp_subtransport *)subtransport; winhttp_stream *s; int ret = -1; if (!t->connection) if ((ret = gitno_connection_data_from_url(&t->connection_data, url, NULL)) < 0 || (ret = winhttp_connect(t)) < 0) return ret; if (winhttp_stream_alloc(t, &s) < 0) return -1; if (!stream) return -1; switch (action) { case GIT_SERVICE_UPLOADPACK_LS: ret = winhttp_uploadpack_ls(t, s); break; case GIT_SERVICE_UPLOADPACK: 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: assert(0); } if (!ret) *stream = &s->parent; return ret; } static int winhttp_close(git_smart_subtransport *subtransport) { winhttp_subtransport *t = (winhttp_subtransport *)subtransport; gitno_connection_data_free_ptrs(&t->connection_data); memset(&t->connection_data, 0x0, sizeof(gitno_connection_data)); gitno_connection_data_free_ptrs(&t->proxy_connection_data); memset(&t->proxy_connection_data, 0x0, sizeof(gitno_connection_data)); if (t->cred) { t->cred->free(t->cred); t->cred = NULL; } if (t->proxy_cred) { t->proxy_cred->free(t->proxy_cred); t->proxy_cred = NULL; } if (t->url_cred) { t->url_cred->free(t->url_cred); t->url_cred = NULL; } return winhttp_close_connection(t); } static void winhttp_free(git_smart_subtransport *subtransport) { winhttp_subtransport *t = (winhttp_subtransport *)subtransport; winhttp_close(subtransport); git__free(t); } int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner, void *param) { winhttp_subtransport *t; GIT_UNUSED(param); if (!out) return -1; t = git__calloc(1, sizeof(winhttp_subtransport)); GITERR_CHECK_ALLOC(t); t->owner = (transport_smart *)owner; t->parent.action = winhttp_action; t->parent.close = winhttp_close; t->parent.free = winhttp_free; *out = (git_smart_subtransport *) t; return 0; } #endif /* GIT_WINHTTP */