/* * Copyright (C) 2009-2012 the libgit2 contributors * * 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 <winhttp.h> #pragma comment(lib, "winhttp") #define WIDEN2(s) L ## s #define WIDEN(s) WIDEN2(s) #define MAX_CONTENT_TYPE_LEN 100 #define WINHTTP_OPTION_PEERDIST_EXTENSION_STATE 109 static const char *prefix_http = "http://"; 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 wchar_t *get_verb = L"GET"; static const wchar_t *post_verb = L"POST"; static const wchar_t *basic_authtype = L"Basic"; static const wchar_t *pragma_nocache = L"Pragma: no-cache"; static const int no_check_cert_flags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_UNKNOWN_CA; #define OWNING_SUBTRANSPORT(s) ((winhttp_subtransport *)(s)->parent.subtransport) typedef enum { GIT_WINHTTP_AUTH_BASIC = 1, } winhttp_authmechanism_t; typedef struct { git_smart_subtransport_stream parent; const char *service; const char *service_url; const wchar_t *verb; HINTERNET request; unsigned sent_request : 1, received_response : 1; } winhttp_stream; typedef struct { git_smart_subtransport parent; transport_smart *owner; const char *path; char *host; char *port; git_cred *cred; int auth_mechanism; HINTERNET session; HINTERNET connection; unsigned use_ssl : 1; } winhttp_subtransport; static int apply_basic_credential(HINTERNET request, git_cred *cred) { git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; git_buf buf = GIT_BUF_INIT, raw = GIT_BUF_INIT; wchar_t *wide = NULL; int error = -1, wide_len; git_buf_printf(&raw, "%s:%s", c->username, c->password); if (git_buf_oom(&raw) || git_buf_puts(&buf, "Authorization: Basic ") < 0 || git_buf_put_base64(&buf, git_buf_cstr(&raw), raw.size) < 0) goto on_error; wide_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, git_buf_cstr(&buf), -1, NULL, 0); if (!wide_len) { giterr_set(GITERR_OS, "Failed to measure string for wide conversion"); goto on_error; } wide = (wchar_t *)git__malloc(wide_len * sizeof(wchar_t)); if (!wide) goto on_error; if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, git_buf_cstr(&buf), -1, wide, wide_len)) { giterr_set(GITERR_OS, "Failed to convert string to wide form"); goto on_error; } if (!WinHttpAddRequestHeaders(request, wide, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) { giterr_set(GITERR_OS, "Failed to add a header to the request"); goto on_error; } error = 0; on_error: /* We were dealing with plaintext passwords, so clean up after ourselves a bit. */ if (wide) memset(wide, 0x0, wide_len * sizeof(wchar_t)); if (buf.size) memset(buf.ptr, 0x0, buf.size); if (raw.size) memset(raw.ptr, 0x0, raw.size); git__free(wide); git_buf_free(&buf); git_buf_free(&raw); return error; } static int winhttp_stream_connect(winhttp_stream *s) { winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); git_buf buf = GIT_BUF_INIT; wchar_t url[GIT_WIN_PATH], ct[MAX_CONTENT_TYPE_LEN]; wchar_t *types[] = { L"*/*", NULL }; BOOL peerdist = FALSE; /* Prepare URL */ git_buf_printf(&buf, "%s%s", t->path, s->service_url); if (git_buf_oom(&buf)) return -1; git__utf8_to_16(url, GIT_WIN_PATH, git_buf_cstr(&buf)); /* Establish request */ s->request = WinHttpOpenRequest( t->connection, s->verb, url, NULL, WINHTTP_NO_REFERER, types, t->use_ssl ? WINHTTP_FLAG_SECURE : 0); if (!s->request) { giterr_set(GITERR_OS, "Failed to open request"); 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; } /* Send Content-Type header -- only necessary on a POST */ if (post_verb == s->verb) { git_buf_clear(&buf); if (git_buf_printf(&buf, "Content-Type: application/x-git-%s-request", s->service) < 0) goto on_error; git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_buf_cstr(&buf)); if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) { giterr_set(GITERR_OS, "Failed to add a header to the request"); goto on_error; } } /* If requested, disable certificate validation */ if (t->use_ssl) { int flags; if (t->owner->parent.read_flags(&t->owner->parent, &flags) < 0) goto on_error; if ((GIT_TRANSPORTFLAGS_NO_CHECK_CERT & flags) && !WinHttpSetOption(s->request, WINHTTP_OPTION_SECURITY_FLAGS, (LPVOID)&no_check_cert_flags, sizeof(no_check_cert_flags))) { giterr_set(GITERR_OS, "Failed to set options to ignore cert errors"); 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 && t->auth_mechanism == GIT_WINHTTP_AUTH_BASIC && apply_basic_credential(s->request, t->cred) < 0) goto on_error; /* We've done everything up to calling WinHttpSendRequest. */ return 0; on_error: git_buf_free(&buf); return -1; } static int parse_unauthorized_response( HINTERNET request, int *allowed_types, int *auth_mechanism) { DWORD index, buf_size, last_error; int error = 0; wchar_t *buf = NULL; *allowed_types = 0; for (index = 0; ; index++) { /* Make a first call to ask for the size of the buffer to allocate * to hold the WWW-Authenticate header */ if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_WWW_AUTHENTICATE, WINHTTP_HEADER_NAME_BY_INDEX, WINHTTP_NO_OUTPUT_BUFFER, &buf_size, &index)) { last_error = GetLastError(); if (ERROR_WINHTTP_HEADER_NOT_FOUND == last_error) { /* End of enumeration */ break; } else if (ERROR_INSUFFICIENT_BUFFER == last_error) { git__free(buf); buf = (wchar_t *)git__malloc(buf_size); if (!buf) { error = -1; break; } } else { giterr_set(GITERR_OS, "Failed to read WWW-Authenticate header"); error = -1; break; } } /* Actually receive the data into our now-allocated buffer */ if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_WWW_AUTHENTICATE, WINHTTP_HEADER_NAME_BY_INDEX, buf, &buf_size, &index)) { giterr_set(GITERR_OS, "Failed to read WWW-Authenticate header"); error = -1; break; } if (!wcsncmp(buf, basic_authtype, 5) && (buf[5] == L'\0' || buf[5] == L' ')) { *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; *auth_mechanism = GIT_WINHTTP_AUTH_BASIC; } } git__free(buf); 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; replay: /* Connect if necessary */ if (!s->request && winhttp_stream_connect(s) < 0) return -1; if (!s->sent_request && !WinHttpSendRequest(s->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) { giterr_set(GITERR_OS, "Failed to send request"); return -1; } s->sent_request = 1; if (!s->received_response) { DWORD status_code, status_code_length, content_type_length; 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 (!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 retreive status code"); return -1; } /* Handle authentication failures */ if (HTTP_STATUS_DENIED == status_code && get_verb == s->verb && t->owner->cred_acquire_cb) { int allowed_types; if (parse_unauthorized_response(s->request, &allowed_types, &t->auth_mechanism) < 0) return -1; if (allowed_types && (!t->cred || 0 == (t->cred->credtype & allowed_types))) { if (t->owner->cred_acquire_cb(&t->cred, t->owner->url, allowed_types) < 0) return -1; assert(t->cred); WinHttpCloseHandle(s->request); s->request = NULL; s->sent_request = 0; /* 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) snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-result", s->service); else snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-advertisement", s->service); git__utf8_to_16(expected_content_type, MAX_CONTENT_TYPE_LEN, expected_content_type_8); 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, 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( git_smart_subtransport_stream *stream, const char *buffer, size_t len) { winhttp_stream *s = (winhttp_stream *)stream; winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); DWORD bytes_written; if (!s->request && winhttp_stream_connect(s) < 0) return -1; /* Since we have to write the Content-Length header up front, we're * basically limited to a single call to write() per request. */ if (!s->sent_request && !WinHttpSendRequest(s->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, (DWORD)len, 0)) { giterr_set(GITERR_OS, "Failed to send request"); return -1; } s->sent_request = 1; if (!WinHttpWriteData(s->request, (LPCVOID)buffer, (DWORD)len, &bytes_written)) { giterr_set(GITERR_OS, "Failed to send request"); return -1; } assert((DWORD)len == bytes_written); return 0; } static void winhttp_stream_free(git_smart_subtransport_stream *stream) { winhttp_stream *s = (winhttp_stream *)stream; if (s->request) { WinHttpCloseHandle(s->request); s->request = NULL; } git__free(s); } static int winhttp_stream_alloc(winhttp_subtransport *t, git_smart_subtransport_stream **stream) { winhttp_stream *s; if (!stream) return -1; s = (winhttp_stream *)git__calloc(sizeof(winhttp_stream), 1); GITERR_CHECK_ALLOC(s); s->parent.subtransport = &t->parent; s->parent.read = winhttp_stream_read; s->parent.write = winhttp_stream_write; s->parent.free = winhttp_stream_free; *stream = (git_smart_subtransport_stream *)s; return 0; } static int winhttp_connect( winhttp_subtransport *t, const char *url) { wchar_t *ua = L"git/1.0 (libgit2 " WIDEN(LIBGIT2_VERSION) L")"; wchar_t host[GIT_WIN_PATH]; int32_t port; const char *default_port; int ret; if (!git__prefixcmp(url, prefix_http)) { url = url + strlen(prefix_http); default_port = "80"; } if (!git__prefixcmp(url, prefix_https)) { url += strlen(prefix_https); default_port = "443"; t->use_ssl = 1; } if ((ret = gitno_extract_host_and_port(&t->host, &t->port, url, default_port)) < 0) return ret; t->path = strchr(url, '/'); /* Prepare port */ if (git__strtol32(&port, t->port, NULL, 10) < 0) return -1; /* Prepare host */ git__utf8_to_16(host, GIT_WIN_PATH, t->host); /* Establish session */ t->session = WinHttpOpen( 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"); return -1; } /* Establish connection */ t->connection = WinHttpConnect( t->session, host, port, 0); if (!t->connection) { giterr_set(GITERR_OS, "Failed to connect to host"); return -1; } return 0; } static int winhttp_uploadpack_ls( winhttp_subtransport *t, const char *url, git_smart_subtransport_stream **stream) { winhttp_stream *s; if (!t->connection && winhttp_connect(t, url) < 0) return -1; if (winhttp_stream_alloc(t, stream) < 0) return -1; s = (winhttp_stream *)*stream; 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, const char *url, git_smart_subtransport_stream **stream) { winhttp_stream *s; if (!t->connection && winhttp_connect(t, url) < 0) return -1; if (winhttp_stream_alloc(t, stream) < 0) return -1; s = (winhttp_stream *)*stream; s->service = upload_pack_service; s->service_url = upload_pack_service_url; s->verb = post_verb; return 0; } static int winhttp_action( git_smart_subtransport_stream **stream, git_smart_subtransport *smart_transport, const char *url, git_smart_service_t action) { winhttp_subtransport *t = (winhttp_subtransport *)smart_transport; if (!stream) return -1; switch (action) { case GIT_SERVICE_UPLOADPACK_LS: return winhttp_uploadpack_ls(t, url, stream); case GIT_SERVICE_UPLOADPACK: return winhttp_uploadpack(t, url, stream); } *stream = NULL; return -1; } static void winhttp_free(git_smart_subtransport *smart_transport) { winhttp_subtransport *t = (winhttp_subtransport *) smart_transport; git__free(t->host); git__free(t->port); if (t->cred) { t->cred->free(t->cred); t->cred = NULL; } if (t->connection) { WinHttpCloseHandle(t->connection); t->connection = NULL; } if (t->session) { WinHttpCloseHandle(t->session); t->session = NULL; } git__free(t); } int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner) { winhttp_subtransport *t; if (!out) return -1; t = (winhttp_subtransport *)git__calloc(sizeof(winhttp_subtransport), 1); GITERR_CHECK_ALLOC(t); t->owner = (transport_smart *)owner; t->parent.action = winhttp_action; t->parent.free = winhttp_free; *out = (git_smart_subtransport *) t; return 0; } #endif /* GIT_WINHTTP */