diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f1a97edb..76cca4f22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ OPTION( ANDROID "Build for android NDK" OFF ) OPTION( USE_ICONV "Link with and use iconv library" OFF ) OPTION( USE_SSH "Link with libssh to enable SSH support" ON ) +OPTION( USE_GSSAPI "Link with libgssapi for SPNEGO auth" OFF ) OPTION( VALGRIND "Configure build for valgrind" OFF ) IF(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") @@ -208,6 +209,14 @@ IF (LIBSSH2_FOUND) SET(SSH_LIBRARIES ${LIBSSH2_LIBRARIES}) ENDIF() +# Optional external dependency: libgssapi +IF (USE_GSSAPI) + FIND_PACKAGE(GSSAPI) +ENDIF() +IF (GSSAPI_FOUND) + ADD_DEFINITIONS(-DGIT_GSSAPI) +ENDIF() + # Optional external dependency: iconv IF (USE_ICONV) FIND_PACKAGE(Iconv) @@ -387,6 +396,7 @@ ENDIF() ADD_LIBRARY(git2 ${SRC_H} ${SRC_GIT2} ${SRC_OS} ${SRC_ZLIB} ${SRC_HTTP} ${SRC_REGEX} ${SRC_SHA1} ${WIN_RC}) TARGET_LINK_LIBRARIES(git2 ${SSL_LIBRARIES}) TARGET_LINK_LIBRARIES(git2 ${SSH_LIBRARIES}) +TARGET_LINK_LIBRARIES(git2 ${GSSAPI_LIBRARIES}) TARGET_LINK_LIBRARIES(git2 ${ICONV_LIBRARIES}) TARGET_OS_LIBRARIES(git2) @@ -453,6 +463,7 @@ IF (BUILD_CLAR) TARGET_LINK_LIBRARIES(libgit2_clar ${SSL_LIBRARIES}) TARGET_LINK_LIBRARIES(libgit2_clar ${SSH_LIBRARIES}) + TARGET_LINK_LIBRARIES(libgit2_clar ${GSSAPI_LIBRARIES}) TARGET_LINK_LIBRARIES(libgit2_clar ${ICONV_LIBRARIES}) TARGET_OS_LIBRARIES(libgit2_clar) MSVC_SPLIT_SOURCES(libgit2_clar) diff --git a/cmake/Modules/FindGSSAPI.cmake b/cmake/Modules/FindGSSAPI.cmake new file mode 100644 index 000000000..8520d35df --- /dev/null +++ b/cmake/Modules/FindGSSAPI.cmake @@ -0,0 +1,324 @@ +# - Try to find GSSAPI +# Once done this will define +# +# KRB5_CONFIG - Path to krb5-config +# GSSAPI_ROOT_DIR - Set this variable to the root installation of GSSAPI +# +# Read-Only variables: +# GSSAPI_FLAVOR_MIT - set to TURE if MIT Kerberos has been found +# GSSAPI_FLAVOR_HEIMDAL - set to TRUE if Heimdal Keberos has been found +# GSSAPI_FOUND - system has GSSAPI +# GSSAPI_INCLUDE_DIR - the GSSAPI include directory +# GSSAPI_LIBRARIES - Link these to use GSSAPI +# GSSAPI_DEFINITIONS - Compiler switches required for using GSSAPI +# +#============================================================================= +# Copyright (c) 2013 Andreas Schneider +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# + +find_path(GSSAPI_ROOT_DIR + NAMES + include/gssapi.h + include/gssapi/gssapi.h + HINTS + ${_GSSAPI_ROOT_HINTS} + PATHS + ${_GSSAPI_ROOT_PATHS} +) +mark_as_advanced(GSSAPI_ROOT_DIR) + +if (UNIX) + find_program(KRB5_CONFIG + NAMES + krb5-config + PATHS + ${GSSAPI_ROOT_DIR}/bin + /opt/local/bin) + mark_as_advanced(KRB5_CONFIG) + + if (KRB5_CONFIG) + # Check if we have MIT KRB5 + execute_process( + COMMAND + ${KRB5_CONFIG} --vendor + RESULT_VARIABLE + _GSSAPI_VENDOR_RESULT + OUTPUT_VARIABLE + _GSSAPI_VENDOR_STRING) + + if (_GSSAPI_VENDOR_STRING MATCHES ".*Massachusetts.*") + set(GSSAPI_FLAVOR_MIT TRUE) + else() + execute_process( + COMMAND + ${KRB5_CONFIG} --libs gssapi + RESULT_VARIABLE + _GSSAPI_LIBS_RESULT + OUTPUT_VARIABLE + _GSSAPI_LIBS_STRING) + + if (_GSSAPI_LIBS_STRING MATCHES ".*roken.*") + set(GSSAPI_FLAVOR_HEIMDAL TRUE) + endif() + endif() + + # Get the include dir + execute_process( + COMMAND + ${KRB5_CONFIG} --cflags gssapi + RESULT_VARIABLE + _GSSAPI_INCLUDE_RESULT + OUTPUT_VARIABLE + _GSSAPI_INCLUDE_STRING) + string(REGEX REPLACE "(\r?\n)+$" "" _GSSAPI_INCLUDE_STRING "${_GSSAPI_INCLUDE_STRING}") + string(REGEX REPLACE " *-I" "" _GSSAPI_INCLUDEDIR "${_GSSAPI_INCLUDE_STRING}") + endif() + + if (NOT GSSAPI_FLAVOR_MIT AND NOT GSSAPI_FLAVOR_HEIMDAL) + # Check for HEIMDAL + find_package(PkgConfig) + if (PKG_CONFIG_FOUND) + pkg_check_modules(_GSSAPI heimdal-gssapi) + endif (PKG_CONFIG_FOUND) + + if (_GSSAPI_FOUND) + set(GSSAPI_FLAVOR_HEIMDAL TRUE) + else() + find_path(_GSSAPI_ROKEN + NAMES + roken.h + PATHS + ${GSSAPI_ROOT_DIR}/include + ${_GSSAPI_INCLUDEDIR}) + if (_GSSAPI_ROKEN) + set(GSSAPI_FLAVOR_HEIMDAL TRUE) + endif() + endif () + endif() +endif (UNIX) + +find_path(GSSAPI_INCLUDE_DIR + NAMES + gssapi.h + gssapi/gssapi.h + PATHS + ${GSSAPI_ROOT_DIR}/include + ${_GSSAPI_INCLUDEDIR} +) + +if (GSSAPI_FLAVOR_MIT) + find_library(GSSAPI_LIBRARY + NAMES + gssapi_krb5 + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(KRB5_LIBRARY + NAMES + krb5 + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(K5CRYPTO_LIBRARY + NAMES + k5crypto + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(COM_ERR_LIBRARY + NAMES + com_err + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + if (GSSAPI_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${GSSAPI_LIBRARY} + ) + endif (GSSAPI_LIBRARY) + + if (KRB5_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${KRB5_LIBRARY} + ) + endif (KRB5_LIBRARY) + + if (K5CRYPTO_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${K5CRYPTO_LIBRARY} + ) + endif (K5CRYPTO_LIBRARY) + + if (COM_ERR_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${COM_ERR_LIBRARY} + ) + endif (COM_ERR_LIBRARY) +endif (GSSAPI_FLAVOR_MIT) + +if (GSSAPI_FLAVOR_HEIMDAL) + find_library(GSSAPI_LIBRARY + NAMES + gssapi + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(KRB5_LIBRARY + NAMES + krb5 + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(HCRYPTO_LIBRARY + NAMES + hcrypto + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(COM_ERR_LIBRARY + NAMES + com_err + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(HEIMNTLM_LIBRARY + NAMES + heimntlm + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(HX509_LIBRARY + NAMES + hx509 + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(ASN1_LIBRARY + NAMES + asn1 + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(WIND_LIBRARY + NAMES + wind + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + find_library(ROKEN_LIBRARY + NAMES + roken + PATHS + ${GSSAPI_ROOT_DIR}/lib + ${_GSSAPI_LIBDIR} + ) + + if (GSSAPI_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${GSSAPI_LIBRARY} + ) + endif (GSSAPI_LIBRARY) + + if (KRB5_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${KRB5_LIBRARY} + ) + endif (KRB5_LIBRARY) + + if (HCRYPTO_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${HCRYPTO_LIBRARY} + ) + endif (HCRYPTO_LIBRARY) + + if (COM_ERR_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${COM_ERR_LIBRARY} + ) + endif (COM_ERR_LIBRARY) + + if (HEIMNTLM_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${HEIMNTLM_LIBRARY} + ) + endif (HEIMNTLM_LIBRARY) + + if (HX509_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${HX509_LIBRARY} + ) + endif (HX509_LIBRARY) + + if (ASN1_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${ASN1_LIBRARY} + ) + endif (ASN1_LIBRARY) + + if (WIND_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${WIND_LIBRARY} + ) + endif (WIND_LIBRARY) + + if (ROKEN_LIBRARY) + set(GSSAPI_LIBRARIES + ${GSSAPI_LIBRARIES} + ${WIND_LIBRARY} + ) + endif (ROKEN_LIBRARY) +endif (GSSAPI_FLAVOR_HEIMDAL) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GSSAPI DEFAULT_MSG GSSAPI_LIBRARIES GSSAPI_INCLUDE_DIR) + +if (GSSAPI_INCLUDE_DIRS AND GSSAPI_LIBRARIES) + set(GSSAPI_FOUND TRUE) +endif (GSSAPI_INCLUDE_DIRS AND GSSAPI_LIBRARIES) + +# show the GSSAPI_INCLUDE_DIRS and GSSAPI_LIBRARIES variables only in the advanced view +mark_as_advanced(GSSAPI_INCLUDE_DIRS GSSAPI_LIBRARIES) diff --git a/src/buffer.c b/src/buffer.c index 1bee9d70b..e9c420e16 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -189,10 +189,10 @@ int git_buf_puts(git_buf *buf, const char *string) return git_buf_put(buf, string, strlen(string)); } -static const char b64str[] = +static const char base64_encode[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -int git_buf_put_base64(git_buf *buf, const char *data, size_t len) +int git_buf_encode_base64(git_buf *buf, const char *data, size_t len) { size_t extra = len % 3; uint8_t *write, a, b, c; @@ -207,19 +207,19 @@ int git_buf_put_base64(git_buf *buf, const char *data, size_t len) b = *read++; c = *read++; - *write++ = b64str[a >> 2]; - *write++ = b64str[(a & 0x03) << 4 | b >> 4]; - *write++ = b64str[(b & 0x0f) << 2 | c >> 6]; - *write++ = b64str[c & 0x3f]; + *write++ = base64_encode[a >> 2]; + *write++ = base64_encode[(a & 0x03) << 4 | b >> 4]; + *write++ = base64_encode[(b & 0x0f) << 2 | c >> 6]; + *write++ = base64_encode[c & 0x3f]; } if (extra > 0) { a = *read++; b = (extra > 1) ? *read++ : 0; - *write++ = b64str[a >> 2]; - *write++ = b64str[(a & 0x03) << 4 | b >> 4]; - *write++ = (extra > 1) ? b64str[(b & 0x0f) << 2] : '='; + *write++ = base64_encode[a >> 2]; + *write++ = base64_encode[(a & 0x03) << 4 | b >> 4]; + *write++ = (extra > 1) ? base64_encode[(b & 0x0f) << 2] : '='; *write++ = '='; } @@ -229,10 +229,56 @@ int git_buf_put_base64(git_buf *buf, const char *data, size_t len) return 0; } +/* The inverse of base64_encode, offset by '+' == 43. */ +static const int8_t base64_decode[] = { + 62, + -1, -1, -1, + 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + -1, -1, -1, 0, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + -1, -1, -1, -1, -1, -1, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 +}; + +#define BASE64_DECODE_VALUE(c) (((c) < 43 || (c) > 122) ? -1 : base64_decode[c - 43]) + +int git_buf_decode_base64(git_buf *buf, const char *base64, size_t len) +{ + size_t i; + int8_t a, b, c, d; + size_t orig_size = buf->size; + + assert(len % 4 == 0); + ENSURE_SIZE(buf, buf->size + (len / 4 * 3) + 1); + + for (i = 0; i < len; i += 4) { + if ((a = BASE64_DECODE_VALUE(base64[i])) < 0 || + (b = BASE64_DECODE_VALUE(base64[i+1])) < 0 || + (c = BASE64_DECODE_VALUE(base64[i+2])) < 0 || + (d = BASE64_DECODE_VALUE(base64[i+3])) < 0) { + buf->size = orig_size; + buf->ptr[buf->size] = '\0'; + + giterr_set(GITERR_INVALID, "Invalid base64 input"); + return -1; + } + + buf->ptr[buf->size++] = ((a << 2) | (b & 0x30) >> 4); + buf->ptr[buf->size++] = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); + buf->ptr[buf->size++] = (c & 0x03) << 6 | (d & 0x3f); + } + + buf->ptr[buf->size] = '\0'; + return 0; +} + static const char b85str[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"; -int git_buf_put_base85(git_buf *buf, const char *data, size_t len) +int git_buf_encode_base85(git_buf *buf, const char *data, size_t len) { ENSURE_SIZE(buf, buf->size + (5 * ((len / 4) + !!(len % 4))) + 1); diff --git a/src/buffer.h b/src/buffer.h index 70d6d73b3..8ee4b532c 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -156,10 +156,12 @@ void git_buf_rtrim(git_buf *buf); int git_buf_cmp(const git_buf *a, const git_buf *b); /* Write data as base64 encoded in buffer */ -int git_buf_put_base64(git_buf *buf, const char *data, size_t len); +int git_buf_encode_base64(git_buf *buf, const char *data, size_t len); +/* Decode the given bas64 and write the result to the buffer */ +int git_buf_decode_base64(git_buf *buf, const char *base64, size_t len); /* Write data as "base85" encoded in buffer */ -int git_buf_put_base85(git_buf *buf, const char *data, size_t len); +int git_buf_encode_base85(git_buf *buf, const char *data, size_t len); /* * Insert, remove or replace a portion of the buffer. diff --git a/src/diff_print.c b/src/diff_print.c index fb62a5fc1..43a90b3d8 100644 --- a/src/diff_print.c +++ b/src/diff_print.c @@ -352,7 +352,7 @@ static int print_binary_hunk(diff_print_info *pi, git_blob *old, git_blob *new) else git_buf_putc(pi->buf, (char)chunk_len - 26 + 'a' - 1); - git_buf_put_base85(pi->buf, scan, chunk_len); + git_buf_encode_base85(pi->buf, scan, chunk_len); git_buf_putc(pi->buf, '\n'); if (git_buf_oom(pi->buf)) { diff --git a/src/transports/auth.c b/src/transports/auth.c new file mode 100644 index 000000000..c1154db34 --- /dev/null +++ b/src/transports/auth.c @@ -0,0 +1,71 @@ +/* + * 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. + */ + +#include "git2.h" +#include "buffer.h" +#include "auth.h" + +static int basic_next_token( + git_buf *out, git_http_auth_context *ctx, git_cred *c) +{ + git_cred_userpass_plaintext *cred; + git_buf raw = GIT_BUF_INIT; + int error = -1; + + GIT_UNUSED(ctx); + + if (c->credtype != GIT_CREDTYPE_USERPASS_PLAINTEXT) { + giterr_set(GITERR_INVALID, "invalid credential type for basic auth"); + goto on_error; + } + + cred = (git_cred_userpass_plaintext *)c; + + git_buf_printf(&raw, "%s:%s", cred->username, cred->password); + + if (git_buf_oom(&raw) || + git_buf_puts(out, "Authorization: Basic ") < 0 || + git_buf_encode_base64(out, git_buf_cstr(&raw), raw.size) < 0 || + git_buf_puts(out, "\r\n") < 0) + goto on_error; + + error = 0; + +on_error: + if (raw.size) + git__memzero(raw.ptr, raw.size); + + git_buf_free(&raw); + return error; +} + +static git_http_auth_context basic_context = { + GIT_AUTHTYPE_BASIC, + GIT_CREDTYPE_USERPASS_PLAINTEXT, + NULL, + basic_next_token, + NULL +}; + +int git_http_auth_basic( + git_http_auth_context **out, const gitno_connection_data *connection_data) +{ + GIT_UNUSED(connection_data); + + *out = &basic_context; + return 0; +} + +int git_http_auth_dummy( + git_http_auth_context **out, const gitno_connection_data *connection_data) +{ + GIT_UNUSED(connection_data); + + *out = NULL; + return 0; +} + diff --git a/src/transports/auth.h b/src/transports/auth.h new file mode 100644 index 000000000..52138cf8f --- /dev/null +++ b/src/transports/auth.h @@ -0,0 +1,63 @@ +/* + * 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. + */ + +#ifndef INCLUDE_http_auth_h__ +#define INCLUDE_http_auth_h__ + +#include "git2.h" +#include "netops.h" + +typedef enum { + GIT_AUTHTYPE_BASIC = 1, + GIT_AUTHTYPE_NEGOTIATE = 2, +} git_http_authtype_t; + +typedef struct git_http_auth_context git_http_auth_context; + +struct git_http_auth_context { + /** Type of scheme */ + git_http_authtype_t type; + + /** Supported credentials */ + git_credtype_t credtypes; + + /** Sets the challenge on the authentication context */ + int (*set_challenge)(git_http_auth_context *ctx, const char *challenge); + + /** Gets the next authentication token from the context */ + int (*next_token)(git_buf *out, git_http_auth_context *ctx, git_cred *cred); + + /** Frees the authentication context */ + void (*free)(git_http_auth_context *ctx); +}; + +typedef struct { + /** Type of scheme */ + git_http_authtype_t type; + + /** Name of the scheme (as used in the Authorization header) */ + const char *name; + + /** Credential types this scheme supports */ + git_credtype_t credtypes; + + /** Function to initialize an authentication context */ + int (*init_context)( + git_http_auth_context **out, + const gitno_connection_data *connection_data); +} git_http_auth_scheme; + +int git_http_auth_dummy( + git_http_auth_context **out, + const gitno_connection_data *connection_data); + +int git_http_auth_basic( + git_http_auth_context **out, + const gitno_connection_data *connection_data); + +#endif + diff --git a/src/transports/auth_negotiate.c b/src/transports/auth_negotiate.c new file mode 100644 index 000000000..8b99fc735 --- /dev/null +++ b/src/transports/auth_negotiate.c @@ -0,0 +1,275 @@ +/* + * 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_GSSAPI + +#include "git2.h" +#include "common.h" +#include "buffer.h" +#include "auth.h" + +#include +#include + +static gss_OID_desc negotiate_oid_spnego = + { 6, (void *) "\x2b\x06\x01\x05\x05\x02" }; +static gss_OID_desc negotiate_oid_krb5 = + { 9, (void *) "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" }; + +static gss_OID negotiate_oids[] = + { &negotiate_oid_spnego, &negotiate_oid_krb5, NULL }; + +typedef struct { + git_http_auth_context parent; + unsigned configured : 1, + complete : 1; + git_buf target; + char *challenge; + gss_ctx_id_t gss_context; + gss_OID oid; +} http_auth_negotiate_context; + +static void negotiate_err_set( + OM_uint32 status_major, + OM_uint32 status_minor, + const char *message) +{ + gss_buffer_desc buffer = GSS_C_EMPTY_BUFFER; + OM_uint32 status_display, context = 0; + + if (gss_display_status(&status_display, status_major, GSS_C_GSS_CODE, + GSS_C_NO_OID, &context, &buffer) == GSS_S_COMPLETE) { + giterr_set(GITERR_NET, "%s: %.*s (%d.%d)", + message, (int)buffer.length, (const char *)buffer.value, + status_major, status_minor); + gss_release_buffer(&status_minor, &buffer); + } else { + giterr_set(GITERR_NET, "%s: unknown negotiate error (%d.%d)", + message, status_major, status_minor); + } +} + +static int negotiate_set_challenge( + git_http_auth_context *c, + const char *challenge) +{ + http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; + + assert(ctx && ctx->configured && challenge); + + git__free(ctx->challenge); + + ctx->challenge = git__strdup(challenge); + GITERR_CHECK_ALLOC(ctx->challenge); + + return 0; +} + +static int negotiate_next_token( + git_buf *buf, + git_http_auth_context *c, + git_cred *cred) +{ + http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; + OM_uint32 status_major, status_minor; + gss_buffer_desc target_buffer = GSS_C_EMPTY_BUFFER, + input_token = GSS_C_EMPTY_BUFFER, + output_token = GSS_C_EMPTY_BUFFER; + gss_buffer_t input_token_ptr = GSS_C_NO_BUFFER; + git_buf input_buf = GIT_BUF_INIT; + gss_name_t server = NULL; + gss_OID mech; + size_t challenge_len; + int error = 0; + + assert(buf && ctx && ctx->configured && cred && cred->credtype == GIT_CREDTYPE_DEFAULT); + + if (ctx->complete) + return 0; + + target_buffer.value = (void *)ctx->target.ptr; + target_buffer.length = ctx->target.size; + + status_major = gss_import_name(&status_minor, &target_buffer, + GSS_C_NT_HOSTBASED_SERVICE, &server); + + if (GSS_ERROR(status_major)) { + negotiate_err_set(status_major, status_minor, + "Could not parse principal"); + error = -1; + goto done; + } + + challenge_len = ctx->challenge ? strlen(ctx->challenge) : 0; + + if (challenge_len < 9) { + giterr_set(GITERR_NET, "No negotiate challenge sent from server"); + error = -1; + goto done; + } else if (challenge_len > 9) { + if (git_buf_decode_base64(&input_buf, + ctx->challenge + 10, challenge_len - 10) < 0) { + giterr_set(GITERR_NET, "Invalid negotiate challenge from server"); + error = -1; + goto done; + } + + input_token.value = input_buf.ptr; + input_token.length = input_buf.size; + input_token_ptr = &input_token; + } else if (ctx->gss_context != GSS_C_NO_CONTEXT) { + giterr_set(GITERR_NET, "Could not restart authentication"); + error = -1; + goto done; + } + + mech = &negotiate_oid_spnego; + + if (GSS_ERROR(status_major = gss_init_sec_context( + &status_minor, + GSS_C_NO_CREDENTIAL, + &ctx->gss_context, + server, + mech, + GSS_C_DELEG_FLAG | GSS_C_MUTUAL_FLAG, + GSS_C_INDEFINITE, + GSS_C_NO_CHANNEL_BINDINGS, + input_token_ptr, + NULL, + &output_token, + NULL, + NULL))) { + negotiate_err_set(status_major, status_minor, "Negotiate failure"); + error = -1; + goto done; + } + + /* This message merely told us auth was complete; we do not respond. */ + if (status_major == GSS_S_COMPLETE) { + ctx->complete = 1; + goto done; + } + + git_buf_puts(buf, "Authorization: Negotiate "); + git_buf_encode_base64(buf, output_token.value, output_token.length); + git_buf_puts(buf, "\r\n"); + + if (git_buf_oom(buf)) + error = -1; + +done: + gss_release_name(&status_minor, &server); + gss_release_buffer(&status_minor, (gss_buffer_t) &output_token); + git_buf_free(&input_buf); + return error; +} + +static void negotiate_context_free(git_http_auth_context *c) +{ + http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; + OM_uint32 status_minor; + + if (ctx->gss_context != GSS_C_NO_CONTEXT) { + gss_delete_sec_context( + &status_minor, &ctx->gss_context, GSS_C_NO_BUFFER); + ctx->gss_context = GSS_C_NO_CONTEXT; + } + + git_buf_free(&ctx->target); + + git__free(ctx->challenge); + + ctx->configured = 0; + ctx->complete = 0; + ctx->oid = NULL; + + git__free(ctx); +} + +static int negotiate_init_context( + http_auth_negotiate_context *ctx, + const gitno_connection_data *connection_data) +{ + OM_uint32 status_major, status_minor; + gss_OID item, *oid; + gss_OID_set mechanism_list; + size_t i; + + /* Query supported mechanisms looking for SPNEGO) */ + if (GSS_ERROR(status_major = + gss_indicate_mechs(&status_minor, &mechanism_list))) { + negotiate_err_set(status_major, status_minor, + "could not query mechanisms"); + return -1; + } + + if (mechanism_list) { + for (oid = negotiate_oids; *oid; oid++) { + for (i = 0; i < mechanism_list->count; i++) { + item = &mechanism_list->elements[i]; + + if (item->length == (*oid)->length && + memcmp(item->elements, (*oid)->elements, item->length) == 0) { + ctx->oid = *oid; + break; + } + + } + + if (ctx->oid) + break; + } + } + + gss_release_oid_set(&status_minor, &mechanism_list); + + if (!ctx->oid) { + giterr_set(GITERR_NET, "Negotiate authentication is not supported"); + return -1; + } + + git_buf_puts(&ctx->target, "HTTP@"); + git_buf_puts(&ctx->target, connection_data->host); + + if (git_buf_oom(&ctx->target)) + return -1; + + ctx->gss_context = GSS_C_NO_CONTEXT; + ctx->configured = 1; + + return 0; +} + +int git_http_auth_negotiate( + git_http_auth_context **out, + const gitno_connection_data *connection_data) +{ + http_auth_negotiate_context *ctx; + + *out = NULL; + + ctx = git__calloc(1, sizeof(http_auth_negotiate_context)); + GITERR_CHECK_ALLOC(ctx); + + if (negotiate_init_context(ctx, connection_data) < 0) { + git__free(ctx); + return -1; + } + + ctx->parent.type = GIT_AUTHTYPE_NEGOTIATE; + ctx->parent.credtypes = GIT_CREDTYPE_DEFAULT; + ctx->parent.set_challenge = negotiate_set_challenge; + ctx->parent.next_token = negotiate_next_token; + ctx->parent.free = negotiate_context_free; + + *out = (git_http_auth_context *)ctx; + + return 0; +} + +#endif /* GIT_GSSAPI */ + diff --git a/src/transports/auth_negotiate.h b/src/transports/auth_negotiate.h new file mode 100644 index 000000000..d7270b7ab --- /dev/null +++ b/src/transports/auth_negotiate.h @@ -0,0 +1,27 @@ +/* + * 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. + */ + +#ifndef INCLUDE_auth_negotiate_h__ +#define INCLUDE_auth_negotiate_h__ + +#include "git2.h" +#include "auth.h" + +#ifdef GIT_GSSAPI + +extern int git_http_auth_negotiate( + git_http_auth_context **out, + const gitno_connection_data *connection_data); + +#else + +#define git_http_auth_negotiate git_http_auth_dummy + +#endif /* GIT_GSSAPI */ + +#endif + diff --git a/src/transports/http.c b/src/transports/http.c index ae608ab3d..5c5e5d391 100644 --- a/src/transports/http.c +++ b/src/transports/http.c @@ -11,6 +11,13 @@ #include "buffer.h" #include "netops.h" #include "smart.h" +#include "auth.h" +#include "auth_negotiate.h" + +git_http_auth_scheme auth_schemes[] = { + { GIT_AUTHTYPE_NEGOTIATE, "Negotiate", GIT_CREDTYPE_DEFAULT, git_http_auth_negotiate }, + { GIT_AUTHTYPE_BASIC, "Basic", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_basic }, +}; static const char *upload_pack_service = "upload-pack"; static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; @@ -20,7 +27,6 @@ static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive static const char *receive_pack_service_url = "/git-receive-pack"; static const char *get_verb = "GET"; static const char *post_verb = "POST"; -static const char *basic_authtype = "Basic"; #define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport) @@ -35,10 +41,6 @@ enum last_cb { VALUE }; -typedef enum { - GIT_HTTP_AUTH_BASIC = 1, -} http_authmechanism_t; - typedef struct { git_smart_subtransport_stream parent; const char *service; @@ -58,9 +60,6 @@ typedef struct { transport_smart *owner; gitno_socket socket; gitno_connection_data connection_data; - git_cred *cred; - git_cred *url_cred; - http_authmechanism_t auth_mechanism; bool connected; /* Parser structures */ @@ -76,6 +75,11 @@ typedef struct { enum last_cb last_cb; int parse_error; unsigned parse_finished : 1; + + /* Authentication */ + git_cred *cred; + git_cred *url_cred; + git_vector auth_contexts; } http_subtransport; typedef struct { @@ -88,28 +92,91 @@ typedef struct { size_t *bytes_read; } parser_context; -static int apply_basic_credential(git_buf *buf, git_cred *cred) +static bool credtype_match(git_http_auth_scheme *scheme, void *data) { - git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; - git_buf raw = GIT_BUF_INIT; - int error = -1; + unsigned int credtype = *(unsigned int *)data; - git_buf_printf(&raw, "%s:%s", c->username, c->password); + return !!(scheme->credtypes & credtype); +} - if (git_buf_oom(&raw) || - git_buf_puts(buf, "Authorization: Basic ") < 0 || - git_buf_put_base64(buf, git_buf_cstr(&raw), raw.size) < 0 || - git_buf_puts(buf, "\r\n") < 0) - goto on_error; +static bool challenge_match(git_http_auth_scheme *scheme, void *data) +{ + const char *scheme_name = scheme->name; + const char *challenge = (const char *)data; + size_t scheme_len; - error = 0; + scheme_len = strlen(scheme_name); + return (strncmp(challenge, scheme_name, scheme_len) == 0 && + (challenge[scheme_len] == '\0' || challenge[scheme_len] == ' ')); +} -on_error: - if (raw.size) - memset(raw.ptr, 0x0, raw.size); +static int auth_context_match( + git_http_auth_context **out, + http_subtransport *t, + bool (*scheme_match)(git_http_auth_scheme *scheme, void *data), + void *data) +{ + git_http_auth_scheme *scheme = NULL; + git_http_auth_context *context = NULL, *c; + size_t i; - git_buf_free(&raw); - return error; + *out = NULL; + + for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { + if (scheme_match(&auth_schemes[i], data)) { + scheme = &auth_schemes[i]; + break; + } + } + + if (!scheme) + return 0; + + /* See if authentication has already started for this scheme */ + git_vector_foreach(&t->auth_contexts, i, c) { + if (c->type == scheme->type) { + context = c; + break; + } + } + + if (!context) { + if (scheme->init_context(&context, &t->connection_data) < 0) + return -1; + else if (!context) + return 0; + else if (git_vector_insert(&t->auth_contexts, context) < 0) + return -1; + } + + *out = context; + + return 0; +} + +static int apply_credentials(git_buf *buf, http_subtransport *t) +{ + git_cred *cred = t->cred; + git_http_auth_context *context; + + /* Apply the credentials given to us in the URL */ + if (!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) + return -1; + + cred = t->url_cred; + } + + if (!cred) + return 0; + + /* Get or create a context for the best scheme for this cred type */ + if (auth_context_match(&context, t, credtype_match, &cred->credtype) < 0) + return -1; + + return context->next_token(buf, context, cred); } static int gen_request( @@ -137,19 +204,9 @@ static int gen_request( git_buf_puts(buf, "Accept: */*\r\n"); /* Apply credentials to the request */ - if (t->cred && t->cred->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT && - t->auth_mechanism == GIT_HTTP_AUTH_BASIC && - apply_basic_credential(buf, t->cred) < 0) + if (apply_credentials(buf, t) < 0) return -1; - /* Use url-parsed basic auth if username and password are both provided */ - 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) - return -1; - if (apply_basic_credential(buf, t->url_cred) < 0) return -1; - } - git_buf_puts(buf, "\r\n"); if (git_buf_oom(buf)) @@ -158,20 +215,26 @@ static int gen_request( return 0; } -static int parse_unauthorized_response( +static int parse_authenticate_response( git_vector *www_authenticate, - int *allowed_types, - http_authmechanism_t *auth_mechanism) + http_subtransport *t, + int *allowed_types) { - unsigned i; - char *entry; + git_http_auth_context *context; + char *challenge; + size_t i; - git_vector_foreach(www_authenticate, i, entry) { - if (!strncmp(entry, basic_authtype, 5) && - (entry[5] == '\0' || entry[5] == ' ')) { - *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; - *auth_mechanism = GIT_HTTP_AUTH_BASIC; - } + git_vector_foreach(www_authenticate, i, challenge) { + if (auth_context_match(&context, t, challenge_match, challenge) < 0) + return -1; + else if (!context) + continue; + + if (context->set_challenge && + context->set_challenge(context, challenge) < 0) + return -1; + + *allowed_types |= context->credtypes; } return 0; @@ -248,7 +311,7 @@ static int on_headers_complete(http_parser *parser) http_subtransport *t = ctx->t; http_stream *s = ctx->s; git_buf buf = GIT_BUF_INIT; - int error = 0, no_callback = 0; + int error = 0, no_callback = 0, allowed_auth_types = 0; /* Both parse_header_name and parse_header_value are populated * and ready for consumption. */ @@ -256,26 +319,26 @@ static int on_headers_complete(http_parser *parser) if (on_header_ready(t) < 0) return t->parse_error = PARSE_ERROR_GENERIC; - /* Check for an authentication failure. */ + /* Capture authentication headers which may be a 401 (authentication + * is not complete) or a 200 (simply informing us that auth *is* + * complete.) + */ + if (parse_authenticate_response(&t->www_authenticate, t, + &allowed_auth_types) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; - if (parser->status_code == 401 && - get_verb == s->verb) { + /* Check for an authentication failure. */ + if (parser->status_code == 401 && get_verb == s->verb) { if (!t->owner->cred_acquire_cb) { no_callback = 1; } else { - int allowed_types = 0; - - if (parse_unauthorized_response(&t->www_authenticate, - &allowed_types, &t->auth_mechanism) < 0) - return t->parse_error = PARSE_ERROR_GENERIC; - - if (allowed_types && - (!t->cred || 0 == (t->cred->credtype & allowed_types))) { + if (allowed_auth_types && + (!t->cred || 0 == (t->cred->credtype & allowed_auth_types))) { error = t->owner->cred_acquire_cb(&t->cred, t->owner->url, t->connection_data.user, - allowed_types, + allowed_auth_types, t->owner->cred_acquire_payload); if (error == GIT_PASSTHROUGH) { @@ -286,7 +349,8 @@ static int on_headers_complete(http_parser *parser) assert(t->cred); /* Successfully acquired a credential. */ - return t->parse_error = PARSE_ERROR_REPLAY; + t->parse_error = PARSE_ERROR_REPLAY; + return 0; } } } @@ -324,7 +388,8 @@ static int on_headers_complete(http_parser *parser) t->connected = 0; s->redirect_count++; - return t->parse_error = PARSE_ERROR_REPLAY; + t->parse_error = PARSE_ERROR_REPLAY; + return 0; } /* Check for a 200 HTTP status code. */ @@ -382,6 +447,13 @@ static int on_body_fill_buffer(http_parser *parser, const char *str, size_t len) parser_context *ctx = (parser_context *) parser->data; http_subtransport *t = ctx->t; + /* If our goal is to replay the request (either an auth failure or + * a redirect) then don't bother buffering since we're ignoring the + * content anyway. + */ + if (t->parse_error == PARSE_ERROR_REPLAY) + return 0; + if (ctx->buf_size < len) { giterr_set(GITERR_NET, "Can't fit data in the buffer"); return t->parse_error = PARSE_ERROR_GENERIC; @@ -456,7 +528,7 @@ static int http_connect(http_subtransport *t) if (t->connected && http_should_keep_alive(&t->parser) && - http_body_is_final(&t->parser)) + t->parse_finished) return 0; if (t->socket.socket) @@ -502,10 +574,8 @@ replay: clear_parser_state(t); - if (gen_request(&request, s, 0) < 0) { - giterr_set(GITERR_NET, "Failed to generate request"); + if (gen_request(&request, s, 0) < 0) return -1; - } if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) { git_buf_free(&request); @@ -604,10 +674,8 @@ static int http_stream_write_chunked( clear_parser_state(t); - if (gen_request(&request, s, 0) < 0) { - giterr_set(GITERR_NET, "Failed to generate request"); + if (gen_request(&request, s, 0) < 0) return -1; - } if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) { git_buf_free(&request); @@ -679,10 +747,8 @@ static int http_stream_write_single( clear_parser_state(t); - if (gen_request(&request, s, len) < 0) { - giterr_set(GITERR_NET, "Failed to generate request"); + if (gen_request(&request, s, len) < 0) return -1; - } if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) goto on_error; @@ -849,6 +915,8 @@ static int http_action( static int http_close(git_smart_subtransport *subtransport) { http_subtransport *t = (http_subtransport *) subtransport; + git_http_auth_context *context; + size_t i; clear_parser_state(t); @@ -867,6 +935,13 @@ static int http_close(git_smart_subtransport *subtransport) t->url_cred = NULL; } + git_vector_foreach(&t->auth_contexts, i, context) { + if (context->free) + context->free(context); + } + + git_vector_clear(&t->auth_contexts); + gitno_connection_data_free_ptrs(&t->connection_data); return 0; @@ -878,6 +953,7 @@ static void http_free(git_smart_subtransport *subtransport) http_close(subtransport); + git_vector_free(&t->auth_contexts); git__free(t); } diff --git a/src/transports/winhttp.c b/src/transports/winhttp.c index 32641cd64..1e46dfaee 100644 --- a/src/transports/winhttp.c +++ b/src/transports/winhttp.c @@ -101,7 +101,7 @@ static int apply_basic_credential(HINTERNET request, git_cred *cred) if (git_buf_oom(&raw) || git_buf_puts(&buf, "Authorization: Basic ") < 0 || - git_buf_put_base64(&buf, git_buf_cstr(&raw), raw.size) < 0) + git_buf_encode_base64(&buf, git_buf_cstr(&raw), raw.size) < 0) goto on_error; if ((wide_len = git__utf8_to_16_alloc(&wide, git_buf_cstr(&buf))) < 0) { diff --git a/tests/core/buffer.c b/tests/core/buffer.c index 8310deae1..7482dadbe 100644 --- a/tests/core/buffer.c +++ b/tests/core/buffer.c @@ -748,7 +748,7 @@ void test_core_buffer__unescape(void) assert_unescape("", ""); } -void test_core_buffer__base64(void) +void test_core_buffer__encode_base64(void) { git_buf buf = GIT_BUF_INIT; @@ -759,33 +759,54 @@ void test_core_buffer__base64(void) * 0x 1d 06 21 29 1c 30 * d G h p c w */ - cl_git_pass(git_buf_put_base64(&buf, "this", 4)); + cl_git_pass(git_buf_encode_base64(&buf, "this", 4)); cl_assert_equal_s("dGhpcw==", buf.ptr); git_buf_clear(&buf); - cl_git_pass(git_buf_put_base64(&buf, "this!", 5)); + cl_git_pass(git_buf_encode_base64(&buf, "this!", 5)); cl_assert_equal_s("dGhpcyE=", buf.ptr); git_buf_clear(&buf); - cl_git_pass(git_buf_put_base64(&buf, "this!\n", 6)); + cl_git_pass(git_buf_encode_base64(&buf, "this!\n", 6)); cl_assert_equal_s("dGhpcyEK", buf.ptr); git_buf_free(&buf); } -void test_core_buffer__base85(void) +void test_core_buffer__decode_base64(void) { git_buf buf = GIT_BUF_INIT; - cl_git_pass(git_buf_put_base85(&buf, "this", 4)); + cl_git_pass(git_buf_decode_base64(&buf, "dGhpcw==", 8)); + cl_assert_equal_s("this", buf.ptr); + + git_buf_clear(&buf); + cl_git_pass(git_buf_decode_base64(&buf, "dGhpcyE=", 8)); + cl_assert_equal_s("this!", buf.ptr); + + git_buf_clear(&buf); + cl_git_pass(git_buf_decode_base64(&buf, "dGhpcyEK", 8)); + cl_assert_equal_s("this!\n", buf.ptr); + + cl_git_fail(git_buf_decode_base64(&buf, "This is not a valid base64 string!!!", 36)); + cl_assert_equal_s("this!\n", buf.ptr); + + git_buf_free(&buf); +} + +void test_core_buffer__encode_base85(void) +{ + git_buf buf = GIT_BUF_INIT; + + cl_git_pass(git_buf_encode_base85(&buf, "this", 4)); cl_assert_equal_s("bZBXF", buf.ptr); git_buf_clear(&buf); - cl_git_pass(git_buf_put_base85(&buf, "two rnds", 8)); + cl_git_pass(git_buf_encode_base85(&buf, "two rnds", 8)); cl_assert_equal_s("ba!tca&BaE", buf.ptr); git_buf_clear(&buf); - cl_git_pass(git_buf_put_base85(&buf, "this is base 85 encoded", + cl_git_pass(git_buf_encode_base85(&buf, "this is base 85 encoded", strlen("this is base 85 encoded"))); cl_assert_equal_s("bZBXFAZc?TVqtS-AUHK3Wo~0{WMyOk", buf.ptr); git_buf_clear(&buf); diff --git a/tests/online/clone.c b/tests/online/clone.c index b672a099a..0cd0f3115 100644 --- a/tests/online/clone.c +++ b/tests/online/clone.c @@ -226,9 +226,28 @@ void test_online_clone__cred_callback_failure_return_code_is_tunnelled(void) cl_git_fail_with(git_clone(&g_repo, remote_url, "./foo", &g_options), -1); } +int cred_default( + git_cred **cred, + const char *url, + const char *user_from_url, + unsigned int allowed_types, + void *payload) +{ + GIT_UNUSED(url); + GIT_UNUSED(user_from_url); + GIT_UNUSED(payload); + + if (!(allowed_types & GIT_CREDTYPE_DEFAULT)) + return 0; + + return git_cred_default_new(cred); +} + void test_online_clone__credentials(void) { - /* Remote URL environment variable must be set. User and password are optional. */ + /* Remote URL environment variable must be set. + * User and password are optional. + */ const char *remote_url = cl_getenv("GITTEST_REMOTE_URL"); git_cred_userpass_payload user_pass = { cl_getenv("GITTEST_REMOTE_USER"), @@ -237,8 +256,12 @@ void test_online_clone__credentials(void) if (!remote_url) return; - g_options.remote_callbacks.credentials = git_cred_userpass; - g_options.remote_callbacks.payload = &user_pass; + if (cl_getenv("GITTEST_REMOTE_DEFAULT")) { + g_options.remote_callbacks.credentials = cred_default; + } else { + g_options.remote_callbacks.credentials = git_cred_userpass; + g_options.remote_callbacks.payload = &user_pass; + } cl_git_pass(git_clone(&g_repo, remote_url, "./foo", &g_options)); git_repository_free(g_repo); g_repo = NULL; @@ -346,7 +369,7 @@ void test_online_clone__ssh_with_paths(void) const char *remote_url = cl_getenv("GITTEST_REMOTE_URL"); const char *remote_user = cl_getenv("GITTEST_REMOTE_USER"); - if (!remote_url || !remote_user) + if (!remote_url || !remote_user || strncmp(remote_url, "ssh://", 5) != 0) clar__skip(); g_options.remote_cb = custom_remote_ssh_with_paths; diff --git a/tests/online/push.c b/tests/online/push.c index 6da27bb96..c351827d9 100644 --- a/tests/online/push.c +++ b/tests/online/push.c @@ -204,6 +204,8 @@ static void verify_tracking_branches(git_remote *remote, expected_ref expected_r cl_assert_equal_i(branch_type, GIT_BRANCH_REMOTE); cl_git_pass(git_vector_insert(&actual_refs, git__strdup(git_reference_name(ref)))); + + git_reference_free(ref); } cl_assert_equal_i(error, GIT_ITEROVER); @@ -852,6 +854,9 @@ void test_online_push__notes(void) const char *specs[] = { "refs/notes/commits:refs/notes/commits" }; push_status exp_stats[] = { { "refs/notes/commits", 1 } }; expected_ref exp_refs[] = { { "refs/notes/commits", &expected_oid } }; + const char *specs_del[] = { ":refs/notes/commits" }; + expected_ref exp_refs_del[] = { }; + git_oid_fromstr(&expected_oid, "8461a99b27b7043e58ff6e1f5d2cf07d282534fb"); target_oid = &_oid_b6; @@ -864,5 +869,11 @@ void test_online_push__notes(void) exp_stats, ARRAY_SIZE(exp_stats), exp_refs, ARRAY_SIZE(exp_refs), 0, 1, 1); + /* And make sure to delete the note */ + + do_push(specs_del, ARRAY_SIZE(specs_del), + exp_stats, 1, + exp_refs_del, ARRAY_SIZE(exp_refs_del), 0, 0, 0); + git_signature_free(signature); }