diff options
author | Edward Thomson <ethomson@edwardthomson.com> | 2019-06-11 08:07:48 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-06-11 08:07:48 +0100 |
commit | 110b5895e0b1c2874d6de01996477a1b7544f22b (patch) | |
tree | 20c0ec3bdb8779d71ea82d17d593cd5564cdba75 | |
parent | f4584a1e8bf3095b90f5564c0f89878ef79855c5 (diff) | |
parent | 7ea8630e04bd0b5f86c6fcc73899433317b8f0fa (diff) | |
download | libgit2-110b5895e0b1c2874d6de01996477a1b7544f22b.tar.gz |
Merge pull request #5052 from libgit2/ethomson/netrefactor
Add NTLM support for HTTP(s) servers and proxies
42 files changed, 5957 insertions, 914 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 1630a7b27..007963c10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,10 @@ OPTION(USE_BUNDLED_ZLIB "Use the bundled version of zlib" OFF) OPTION(DEPRECATE_HARD "Do not include deprecated functions in the library" OFF) SET(REGEX_BACKEND "" CACHE STRING "Regular expression implementation. One of regcomp_l, pcre2, pcre, regcomp, or builtin.") +IF (UNIX) + OPTION(USE_NTLMCLIENT "Enable NTLM support on Unix." ON ) +ENDIF() + IF (UNIX AND NOT APPLE) OPTION(ENABLE_REPRODUCIBLE_BUILDS "Enable reproducible builds" OFF) ENDIF() diff --git a/ci/test.ps1 b/ci/test.ps1 index 06e0ab228..68b53e269 100644 --- a/ci/test.ps1 +++ b/ci/test.ps1 @@ -38,18 +38,25 @@ Write-Host "## Configuring test environment" Write-Host "##############################################################################" if (-not $Env:SKIP_PROXY_TESTS) { + Invoke-WebRequest -Method GET -Uri https://github.com/ethomson/poxyproxy/releases/download/v0.7.0/poxyproxy-0.7.0.jar -OutFile poxyproxy.jar + + Write-Host "" + Write-Host "Starting HTTP proxy (Basic)..." + javaw -jar poxyproxy.jar --port 8080 --credentials foo:bar --auth-type basic --quiet + Write-Host "" - Write-Host "Starting HTTP proxy..." - Invoke-WebRequest -Method GET -Uri https://github.com/ethomson/poxyproxy/releases/download/v0.4.0/poxyproxy-0.4.0.jar -OutFile poxyproxy.jar - javaw -jar poxyproxy.jar -d --port 8080 --credentials foo:bar --quiet + Write-Host "Starting HTTP proxy (NTLM)..." + javaw -jar poxyproxy.jar --port 8090 --credentials foo:bar --auth-type ntlm --quiet } -Write-Host "" -Write-Host "##############################################################################" -Write-Host "## Running (offline) tests" -Write-Host "##############################################################################" +if (-not $Env:SKIP_OFFLINE_TESTS) { + Write-Host "" + Write-Host "##############################################################################" + Write-Host "## Running (offline) tests" + Write-Host "##############################################################################" -run_test offline + run_test offline +} if ($Env:RUN_INVASIVE_TESTS) { Write-Host "" @@ -76,14 +83,24 @@ if (-not $Env:SKIP_ONLINE_TESTS) { } if (-not $Env:SKIP_PROXY_TESTS) { + # Test HTTP Basic authentication Write-Host "" - Write-Host "Running proxy tests" + Write-Host "Running proxy tests (Basic authentication)" Write-Host "" $Env:GITTEST_REMOTE_PROXY_HOST="localhost:8080" $Env:GITTEST_REMOTE_PROXY_USER="foo" $Env:GITTEST_REMOTE_PROXY_PASS="bar" + run_test proxy + # Test NTLM authentication + Write-Host "" + Write-Host "Running proxy tests (NTLM authentication)" + Write-Host "" + + $Env:GITTEST_REMOTE_PROXY_HOST="localhost:8090" + $Env:GITTEST_REMOTE_PROXY_USER="foo" + $Env:GITTEST_REMOTE_PROXY_PASS="bar" run_test proxy $Env:GITTEST_REMOTE_PROXY_HOST=$null diff --git a/ci/test.sh b/ci/test.sh index 136ff2581..e3caa8086 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -78,9 +78,15 @@ if [ -z "$SKIP_GITDAEMON_TESTS" ]; then fi if [ -z "$SKIP_PROXY_TESTS" ]; then - echo "Starting HTTP proxy..." - curl -L https://github.com/ethomson/poxyproxy/releases/download/v0.4.0/poxyproxy-0.4.0.jar >poxyproxy.jar - java -jar poxyproxy.jar -d --address 127.0.0.1 --port 8080 --credentials foo:bar --quiet & + curl -L https://github.com/ethomson/poxyproxy/releases/download/v0.7.0/poxyproxy-0.7.0.jar >poxyproxy.jar + + echo "" + echo "Starting HTTP proxy (Basic)..." + java -jar poxyproxy.jar --address 127.0.0.1 --port 8080 --credentials foo:bar --auth-type basic --quiet & + + echo "" + echo "Starting HTTP proxy (NTLM)..." + java -jar poxyproxy.jar --address 127.0.0.1 --port 8090 --credentials foo:bar --auth-type ntlm --quiet & fi if [ -z "$SKIP_SSH_TESTS" ]; then @@ -175,7 +181,7 @@ fi if [ -z "$SKIP_PROXY_TESTS" ]; then echo "" - echo "Running proxy tests" + echo "Running proxy tests (Basic authentication)" echo "" export GITTEST_REMOTE_PROXY_HOST="localhost:8080" @@ -185,6 +191,18 @@ if [ -z "$SKIP_PROXY_TESTS" ]; then unset GITTEST_REMOTE_PROXY_HOST unset GITTEST_REMOTE_PROXY_USER unset GITTEST_REMOTE_PROXY_PASS + + echo "" + echo "Running proxy tests (NTLM authentication)" + echo "" + + export GITTEST_REMOTE_PROXY_HOST="localhost:8090" + export GITTEST_REMOTE_PROXY_USER="foo" + export GITTEST_REMOTE_PROXY_PASS="bar" + run_test proxy + unset GITTEST_REMOTE_PROXY_HOST + unset GITTEST_REMOTE_PROXY_USER + unset GITTEST_REMOTE_PROXY_PASS fi if [ -z "$SKIP_SSH_TESTS" ]; then diff --git a/deps/ntlmclient/CMakeLists.txt b/deps/ntlmclient/CMakeLists.txt new file mode 100644 index 000000000..393257daf --- /dev/null +++ b/deps/ntlmclient/CMakeLists.txt @@ -0,0 +1,18 @@ +FILE(GLOB SRC_NTLMCLIENT "ntlm.c" "unicode_builtin.c" "util.c") + +ADD_DEFINITIONS(-DNTLM_STATIC=1) + +IF (HTTPS_BACKEND STREQUAL "SecureTransport") + ADD_DEFINITIONS(-DCRYPT_COMMONCRYPTO) + SET(SRC_NTLMCLIENT_CRYPTO "crypt_commoncrypto.c") +ELSEIF (HTTPS_BACKEND STREQUAL "OpenSSL") + ADD_DEFINITIONS(-DCRYPT_OPENSSL) + SET(SRC_NTLMCLIENT_CRYPTO "crypt_openssl.c") +ELSEIF (HTTPS_BACKEND STREQUAL "mbedTLS") + ADD_DEFINITIONS(-DCRYPT_MBEDTLS) + SET(SRC_NTLMCLIENT_CRYPTO "crypt_mbedtls.c") +ELSE () + MESSAGE(FATAL_ERROR "Unable to use libgit2's HTTPS backend (${HTTPS_BACKEND}) for NTLM crypto") +ENDIF() + +ADD_LIBRARY(ntlmclient OBJECT ${SRC_NTLMCLIENT} ${SRC_NTLMCLIENT_CRYPTO}) diff --git a/deps/ntlmclient/compat.h b/deps/ntlmclient/compat.h new file mode 100644 index 000000000..efdf34514 --- /dev/null +++ b/deps/ntlmclient/compat.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#ifndef PRIVATE_COMPAT_H__ +#define PRIVATE_COMPAT_H__ + +#if defined (_MSC_VER) + typedef unsigned char bool; +# ifndef true +# define true 1 +# endif +# ifndef false +# define false 0 +# endif +#else +# include <stdbool.h> +#endif + +#ifdef __linux__ +# include <endian.h> +# define htonll htobe64 +#endif + +#ifndef MIN +# define MIN(x, y) (((x) < (y)) ? (x) : (y)) +#endif + +#endif /* PRIVATE_COMPAT_H__ */ diff --git a/deps/ntlmclient/crypt.h b/deps/ntlmclient/crypt.h new file mode 100644 index 000000000..48be39946 --- /dev/null +++ b/deps/ntlmclient/crypt.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#ifndef PRIVATE_CRYPT_COMMON_H__ +#define PRIVATE_CRYPT_COMMON_H__ + +#if defined(CRYPT_OPENSSL) +# include "crypt_openssl.h" +#elif defined(CRYPT_MBEDTLS) +# include "crypt_mbedtls.h" +#elif defined(CRYPT_COMMONCRYPTO) +# include "crypt_commoncrypto.h" +#else +# error "no crypto support" +#endif + +#define CRYPT_DES_BLOCKSIZE 8 +#define CRYPT_MD4_DIGESTSIZE 16 +#define CRYPT_MD5_DIGESTSIZE 16 + +typedef unsigned char ntlm_des_block[CRYPT_DES_BLOCKSIZE]; + +extern bool ntlm_random_bytes( + ntlm_client *ntlm, + unsigned char *out, + size_t len); + +extern bool ntlm_des_encrypt( + ntlm_des_block *out, + ntlm_des_block *plaintext, + ntlm_des_block *key); + +extern bool ntlm_md4_digest( + unsigned char out[CRYPT_MD4_DIGESTSIZE], + const unsigned char *in, + size_t in_len); + +extern ntlm_hmac_ctx *ntlm_hmac_ctx_init(void); + +extern bool ntlm_hmac_ctx_reset(ntlm_hmac_ctx *ctx); + +extern bool ntlm_hmac_md5_init( + ntlm_hmac_ctx *ctx, + const unsigned char *key, + size_t key_len); + +extern bool ntlm_hmac_md5_update( + ntlm_hmac_ctx *ctx, + const unsigned char *data, + size_t data_len); + +extern bool ntlm_hmac_md5_final( + unsigned char *out, + size_t *out_len, + ntlm_hmac_ctx *ctx); + +extern void ntlm_hmac_ctx_free(ntlm_hmac_ctx *ctx); + +#endif /* PRIVATE_CRYPT_COMMON_H__ */ diff --git a/deps/ntlmclient/crypt_commoncrypto.c b/deps/ntlmclient/crypt_commoncrypto.c new file mode 100644 index 000000000..54a0f097b --- /dev/null +++ b/deps/ntlmclient/crypt_commoncrypto.c @@ -0,0 +1,120 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#include <stdlib.h> +#include <stdint.h> +#include <string.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> + +#include <CommonCrypto/CommonCrypto.h> + +#include "ntlm.h" +#include "crypt.h" + +bool ntlm_random_bytes( + ntlm_client *ntlm, + unsigned char *out, + size_t len) +{ + int fd, ret; + size_t total = 0; + + if ((fd = open("/dev/urandom", O_RDONLY)) < 0) { + ntlm_client_set_errmsg(ntlm, strerror(errno)); + return false; + } + + while (total < len) { + if ((ret = read(fd, out, (len - total))) < 0) { + ntlm_client_set_errmsg(ntlm, strerror(errno)); + return false; + } else if (ret == 0) { + ntlm_client_set_errmsg(ntlm, "unexpected eof on random device"); + return false; + } + + total += ret; + } + + close(fd); + return true; +} + +bool ntlm_des_encrypt( + ntlm_des_block *out, + ntlm_des_block *plaintext, + ntlm_des_block *key) +{ + size_t written; + + CCCryptorStatus result = CCCrypt(kCCEncrypt, + kCCAlgorithmDES, kCCOptionECBMode, + key, sizeof(ntlm_des_block), NULL, + plaintext, sizeof(ntlm_des_block), + out, sizeof(ntlm_des_block), &written); + + return (result == kCCSuccess) ? true : false; +} + +bool ntlm_md4_digest( + unsigned char out[CRYPT_MD4_DIGESTSIZE], + const unsigned char *in, + size_t in_len) +{ + return !!CC_MD4(in, in_len, out); +} + +ntlm_hmac_ctx *ntlm_hmac_ctx_init(void) +{ + return calloc(1, sizeof(ntlm_hmac_ctx)); +} + +bool ntlm_hmac_ctx_reset(ntlm_hmac_ctx *ctx) +{ + memset(ctx, 0, sizeof(ntlm_hmac_ctx)); + return true; +} + +bool ntlm_hmac_md5_init( + ntlm_hmac_ctx *ctx, + const unsigned char *key, + size_t key_len) +{ + CCHmacInit(&ctx->native, kCCHmacAlgMD5, key, key_len); + return true; +} + +bool ntlm_hmac_md5_update( + ntlm_hmac_ctx *ctx, + const unsigned char *data, + size_t data_len) +{ + CCHmacUpdate(&ctx->native, data, data_len); + return true; +} + +bool ntlm_hmac_md5_final( + unsigned char *out, + size_t *out_len, + ntlm_hmac_ctx *ctx) +{ + if (*out_len < CRYPT_MD5_DIGESTSIZE) + return false; + + CCHmacFinal(&ctx->native, out); + + *out_len = CRYPT_MD5_DIGESTSIZE; + return true; +} + +void ntlm_hmac_ctx_free(ntlm_hmac_ctx *ctx) +{ + free(ctx); +} diff --git a/deps/ntlmclient/crypt_commoncrypto.h b/deps/ntlmclient/crypt_commoncrypto.h new file mode 100644 index 000000000..e4075c9f6 --- /dev/null +++ b/deps/ntlmclient/crypt_commoncrypto.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#ifndef PRIVATE_CRYPT_COMMONCRYPTO_H__ +#define PRIVATE_CRYPT_COMMONCRYPTO_H__ + +#include <CommonCrypto/CommonCrypto.h> + +typedef struct { + CCHmacContext native; +} ntlm_hmac_ctx; + +#endif /* PRIVATE_CRYPT_COMMONCRYPTO_H__ */ diff --git a/deps/ntlmclient/crypt_mbedtls.c b/deps/ntlmclient/crypt_mbedtls.c new file mode 100644 index 000000000..bbab02d7d --- /dev/null +++ b/deps/ntlmclient/crypt_mbedtls.c @@ -0,0 +1,145 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#include <stdlib.h> +#include <string.h> + +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/des.h" +#include "mbedtls/entropy.h" +#include "mbedtls/md4.h" + +#include "ntlm.h" +#include "crypt.h" + +bool ntlm_random_bytes( + ntlm_client *ntlm, + unsigned char *out, + size_t len) +{ + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_entropy_context entropy; + bool ret = true; + + const unsigned char personalization[] = { + 0xec, 0xb5, 0xd1, 0x0b, 0x8f, 0x15, 0x1f, 0xc2, + 0xe4, 0x8e, 0xec, 0x36, 0xf7, 0x0a, 0x45, 0x9a, + 0x1f, 0xe1, 0x35, 0x58, 0xb1, 0xcb, 0xfd, 0x8a, + 0x57, 0x5c, 0x75, 0x7d, 0x2f, 0xc9, 0x70, 0xac + }; + + mbedtls_ctr_drbg_init(&ctr_drbg); + mbedtls_entropy_init(&entropy); + + if (mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, + &entropy, personalization, sizeof(personalization)) || + mbedtls_ctr_drbg_random(&ctr_drbg, out, len)) { + ntlm_client_set_errmsg(ntlm, "random generation failed"); + ret = false; + } + + mbedtls_entropy_free(&entropy); + mbedtls_ctr_drbg_free(&ctr_drbg); + + return ret; +} + +bool ntlm_des_encrypt( + ntlm_des_block *out, + ntlm_des_block *plaintext, + ntlm_des_block *key) +{ + mbedtls_des_context ctx; + bool success = false; + + mbedtls_des_init(&ctx); + + if (mbedtls_des_setkey_enc(&ctx, *key) || + mbedtls_des_crypt_ecb(&ctx, *plaintext, *out)) + goto done; + + success = true; + +done: + mbedtls_des_free(&ctx); + return success; +} + +bool ntlm_md4_digest( + unsigned char out[CRYPT_MD4_DIGESTSIZE], + const unsigned char *in, + size_t in_len) +{ + mbedtls_md4_context ctx; + + mbedtls_md4_init(&ctx); + mbedtls_md4_starts(&ctx); + mbedtls_md4_update(&ctx, in, in_len); + mbedtls_md4_finish(&ctx, out); + mbedtls_md4_free(&ctx); + + return true; +} + +ntlm_hmac_ctx *ntlm_hmac_ctx_init(void) +{ + ntlm_hmac_ctx *ctx; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_MD5); + + if ((ctx = calloc(1, sizeof(ntlm_hmac_ctx))) == NULL) + return NULL; + + mbedtls_md_init(&ctx->mbed); + + if (mbedtls_md_setup(&ctx->mbed, info, 1) != 0) { + free(ctx); + return false; + } + + return ctx; +} + +bool ntlm_hmac_ctx_reset(ntlm_hmac_ctx *ctx) +{ + return !mbedtls_md_hmac_reset(&ctx->mbed); +} + +bool ntlm_hmac_md5_init( + ntlm_hmac_ctx *ctx, + const unsigned char *key, + size_t key_len) +{ + return !mbedtls_md_hmac_starts(&ctx->mbed, key, key_len); +} + +bool ntlm_hmac_md5_update( + ntlm_hmac_ctx *ctx, + const unsigned char *in, + size_t in_len) +{ + return !mbedtls_md_hmac_update(&ctx->mbed, in, in_len); +} + +bool ntlm_hmac_md5_final( + unsigned char *out, + size_t *out_len, + ntlm_hmac_ctx *ctx) +{ + if (*out_len < CRYPT_MD5_DIGESTSIZE) + return false; + + return !mbedtls_md_hmac_finish(&ctx->mbed, out); +} + +void ntlm_hmac_ctx_free(ntlm_hmac_ctx *ctx) +{ + if (ctx) { + mbedtls_md_free(&ctx->mbed); + free(ctx); + } +} diff --git a/deps/ntlmclient/crypt_mbedtls.h b/deps/ntlmclient/crypt_mbedtls.h new file mode 100644 index 000000000..eb49a4596 --- /dev/null +++ b/deps/ntlmclient/crypt_mbedtls.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#ifndef PRIVATE_CRYPT_MBEDTLS_H__ +#define PRIVATE_CRYPT_MBEDTLS_H__ + +#include "mbedtls/md.h" + +typedef struct { + mbedtls_md_context_t mbed; +} ntlm_hmac_ctx; + +#endif /* PRIVATE_CRYPT_MBEDTLS_H__ */ diff --git a/deps/ntlmclient/crypt_openssl.c b/deps/ntlmclient/crypt_openssl.c new file mode 100644 index 000000000..785be10e5 --- /dev/null +++ b/deps/ntlmclient/crypt_openssl.c @@ -0,0 +1,130 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#include <stdlib.h> +#include <string.h> + +#include <openssl/rand.h> +#include <openssl/des.h> +#include <openssl/md4.h> +#include <openssl/hmac.h> +#include <openssl/err.h> + +#include "ntlm.h" +#include "compat.h" +#include "util.h" +#include "crypt.h" + +bool ntlm_random_bytes( + ntlm_client *ntlm, + unsigned char *out, + size_t len) +{ + int rc = RAND_bytes(out, len); + + if (rc != 1) { + ntlm_client_set_errmsg(ntlm, ERR_lib_error_string(ERR_get_error())); + return false; + } + + return true; +} + +bool ntlm_des_encrypt( + ntlm_des_block *out, + ntlm_des_block *plaintext, + ntlm_des_block *key) +{ + DES_key_schedule keysched; + + memset(out, 0, sizeof(ntlm_des_block)); + + DES_set_key(key, &keysched); + DES_ecb_encrypt(plaintext, out, &keysched, DES_ENCRYPT); + + return true; +} + +bool ntlm_md4_digest( + unsigned char out[CRYPT_MD4_DIGESTSIZE], + const unsigned char *in, + size_t in_len) +{ + MD4(in, in_len, out); + return true; +} + +#if OPENSSL_VERSION_NUMBER < 0x10100000L +static inline void HMAC_CTX_free(HMAC_CTX *ctx) +{ + if (ctx) + HMAC_CTX_cleanup(ctx); + + free(ctx); +} + +static inline int HMAC_CTX_reset(HMAC_CTX *ctx) +{ + HMAC_CTX_cleanup(ctx); + memzero(ctx, sizeof(HMAC_CTX)); + return 1; +} + +static inline HMAC_CTX *HMAC_CTX_new(void) +{ + return calloc(1, sizeof(HMAC_CTX)); +} +#endif + +ntlm_hmac_ctx *ntlm_hmac_ctx_init(void) +{ + return HMAC_CTX_new(); +} + +bool ntlm_hmac_ctx_reset(ntlm_hmac_ctx *ctx) +{ + return HMAC_CTX_reset(ctx); +} + +bool ntlm_hmac_md5_init( + ntlm_hmac_ctx *ctx, + const unsigned char *key, + size_t key_len) +{ + return HMAC_Init_ex(ctx, key, key_len, EVP_md5(), NULL); +} + +bool ntlm_hmac_md5_update( + ntlm_hmac_ctx *ctx, + const unsigned char *in, + size_t in_len) +{ + return HMAC_Update(ctx, in, in_len); +} + +bool ntlm_hmac_md5_final( + unsigned char *out, + size_t *out_len, + ntlm_hmac_ctx *ctx) +{ + unsigned int len; + + if (*out_len < CRYPT_MD5_DIGESTSIZE) + return false; + + if (!HMAC_Final(ctx, out, &len)) + return false; + + *out_len = len; + return true; +} + +void ntlm_hmac_ctx_free(ntlm_hmac_ctx *ctx) +{ + HMAC_CTX_free(ctx); +} diff --git a/deps/ntlmclient/crypt_openssl.h b/deps/ntlmclient/crypt_openssl.h new file mode 100644 index 000000000..4195db9a5 --- /dev/null +++ b/deps/ntlmclient/crypt_openssl.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#ifndef PRIVATE_CRYPT_OPENSSL_H__ +#define PRIVATE_CRYPT_OPENSSL_H__ + +#include <openssl/hmac.h> + +/* OpenSSL 1.1.0 uses opaque structs, we'll reuse these. */ +#if OPENSSL_VERSION_NUMBER < 0x10100000L +typedef struct hmac_ctx_st ntlm_hmac_ctx; +#else +# define ntlm_hmac_ctx HMAC_CTX +#endif + +#endif /* PRIVATE_CRYPT_OPENSSL_H__ */ diff --git a/deps/ntlmclient/ntlm.c b/deps/ntlmclient/ntlm.c new file mode 100644 index 000000000..9d0f3b8e3 --- /dev/null +++ b/deps/ntlmclient/ntlm.c @@ -0,0 +1,1420 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#include <stdlib.h> +#include <stdint.h> +#include <string.h> +#include <assert.h> +#include <errno.h> +#include <ctype.h> +#include <unistd.h> +#include <fcntl.h> +#include <time.h> +#include <arpa/inet.h> + +#include "ntlm.h" +#include "unicode.h" +#include "utf8.h" +#include "crypt.h" +#include "compat.h" +#include "util.h" + +unsigned char ntlm_client_signature[] = NTLM_SIGNATURE; + +static bool supports_unicode(ntlm_client *ntlm) +{ + return (ntlm->flags & NTLM_CLIENT_DISABLE_UNICODE) ? + false : true; +} + +static inline bool increment_size(size_t *out, size_t incr) +{ + if (SIZE_MAX - *out < incr) { + *out = (size_t)-1; + return false; + } + + *out = *out + incr; + return true; +} + +ntlm_client *ntlm_client_init(ntlm_client_flags flags) +{ + ntlm_client *ntlm = NULL; + + if ((ntlm = malloc(sizeof(ntlm_client))) == NULL) + return NULL; + + memset(ntlm, 0, sizeof(ntlm_client)); + + ntlm->flags = flags; + + if ((ntlm->hmac_ctx = ntlm_hmac_ctx_init()) == NULL || + (ntlm->unicode_ctx = ntlm_unicode_ctx_init(ntlm)) == NULL) { + ntlm_hmac_ctx_free(ntlm->hmac_ctx); + ntlm_unicode_ctx_free(ntlm->unicode_ctx); + free(ntlm); + return NULL; + } + + return ntlm; +} + +void ntlm_client_set_errmsg(ntlm_client *ntlm, const char *errmsg) +{ + ntlm->state = NTLM_STATE_ERROR; + ntlm->errmsg = errmsg; +} + +const char *ntlm_client_errmsg(ntlm_client *ntlm) +{ + assert(ntlm); + return ntlm->errmsg ? ntlm->errmsg : "no error"; +} + +int ntlm_client_set_version( + ntlm_client *ntlm, + uint8_t major, + uint8_t minor, + uint16_t build) +{ + assert(ntlm); + + ntlm->host_version.major = major; + ntlm->host_version.minor = minor; + ntlm->host_version.build = build; + ntlm->host_version.reserved = 0x0f000000; + + ntlm->flags |= NTLM_ENABLE_HOSTVERSION; + + return 0; +} + +int ntlm_client_set_hostname( + ntlm_client *ntlm, + const char *hostname, + const char *domain) +{ + assert(ntlm); + + free(ntlm->hostname); + free(ntlm->hostdomain); + free(ntlm->hostname_utf16); + + ntlm->hostname = NULL; + ntlm->hostdomain = NULL; + ntlm->hostname_utf16 = NULL; + + if (hostname && (ntlm->hostname = strdup(hostname)) == NULL) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return -1; + } + + if (domain && (ntlm->hostdomain = strdup(domain)) == NULL) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return -1; + } + + if (hostname && supports_unicode(ntlm) && !ntlm_unicode_utf8_to_16( + &ntlm->hostname_utf16, + &ntlm->hostname_utf16_len, + ntlm->unicode_ctx, + hostname, + strlen(hostname))) + return -1; + + return 0; +} + +static void free_credentials(ntlm_client *ntlm) +{ + if (ntlm->password) + memzero(ntlm->password, strlen(ntlm->password)); + + if (ntlm->password_utf16) + memzero(ntlm->password_utf16, ntlm->password_utf16_len); + + free(ntlm->username); + free(ntlm->username_upper); + free(ntlm->userdomain); + free(ntlm->password); + + free(ntlm->username_utf16); + free(ntlm->username_upper_utf16); + free(ntlm->userdomain_utf16); + free(ntlm->password_utf16); + + ntlm->username = NULL; + ntlm->username_upper = NULL; + ntlm->userdomain = NULL; + ntlm->password = NULL; + + ntlm->username_utf16 = NULL; + ntlm->username_upper_utf16 = NULL; + ntlm->userdomain_utf16 = NULL; + ntlm->password_utf16 = NULL; +} + +int ntlm_client_set_credentials( + ntlm_client *ntlm, + const char *username, + const char *domain, + const char *password) +{ + assert(ntlm); + + free_credentials(ntlm); + + if ((username && (ntlm->username = strdup(username)) == NULL) || + (domain && (ntlm->userdomain = strdup(domain)) == NULL) || + (password && (ntlm->password = strdup(password)) == NULL)) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return -1; + } + + if (username && supports_unicode(ntlm)) { + if ((ntlm->username_upper = strdup(username)) == NULL) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return -1; + } + utf8upr(ntlm->username_upper); + + if (!ntlm_unicode_utf8_to_16( + &ntlm->username_utf16, + &ntlm->username_utf16_len, + ntlm->unicode_ctx, + ntlm->username, + strlen(ntlm->username))) + return -1; + + if (!ntlm_unicode_utf8_to_16( + &ntlm->username_upper_utf16, + &ntlm->username_upper_utf16_len, + ntlm->unicode_ctx, + ntlm->username_upper, + strlen(ntlm->username_upper))) + return -1; + } + + if (domain && supports_unicode(ntlm) && !ntlm_unicode_utf8_to_16( + &ntlm->userdomain_utf16, + &ntlm->userdomain_utf16_len, + ntlm->unicode_ctx, + ntlm->userdomain, + strlen(ntlm->userdomain))) + return -1; + + return 0; +} + +int ntlm_client_set_target(ntlm_client *ntlm, const char *target) +{ + assert(ntlm); + + free(ntlm->target); + free(ntlm->target_utf16); + + ntlm->target = NULL; + ntlm->target_utf16 = NULL; + + if (target) { + if ((ntlm->target = strdup(target)) == NULL) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return -1; + } + + if (supports_unicode(ntlm) && !ntlm_unicode_utf8_to_16( + &ntlm->target_utf16, + &ntlm->target_utf16_len, + ntlm->unicode_ctx, + ntlm->target, + strlen(ntlm->target))) + return -1; + } + + return 0; +} + +int ntlm_client_set_nonce(ntlm_client *ntlm, uint64_t nonce) +{ + assert(ntlm); + ntlm->nonce = nonce; + return 0; +} + +int ntlm_client_set_timestamp(ntlm_client *ntlm, uint64_t timestamp) +{ + assert(ntlm); + ntlm->timestamp = timestamp; + return 0; +} + +static inline bool write_buf( + ntlm_client *ntlm, + ntlm_buf *out, + const unsigned char *buf, + size_t len) +{ + if (out->len - out->pos < len) { + ntlm_client_set_errmsg(ntlm, "out of buffer space"); + return false; + } + + memcpy(&out->buf[out->pos], buf, len); + out->pos += len; + return true; +} + +static inline bool write_byte( + ntlm_client *ntlm, + ntlm_buf *out, + uint8_t value) +{ + if (out->len - out->pos < 1) { + ntlm_client_set_errmsg(ntlm, "out of buffer space"); + return false; + } + + out->buf[out->pos++] = value; + return true; +} + +static inline bool write_int16( + ntlm_client *ntlm, + ntlm_buf *out, + uint16_t value) +{ + if (out->len - out->pos < 2) { + ntlm_client_set_errmsg(ntlm, "out of buffer space"); + return false; + } + + out->buf[out->pos++] = (value & 0x000000ff); + out->buf[out->pos++] = (value & 0x0000ff00) >> 8; + return true; +} + +static inline bool write_int32( + ntlm_client *ntlm, + ntlm_buf *out, + uint32_t value) +{ + if (out->len - out->pos < 2) { + ntlm_client_set_errmsg(ntlm, "out of buffer space"); + return false; + } + + out->buf[out->pos++] = (value & 0x000000ff); + out->buf[out->pos++] = (value & 0x0000ff00) >> 8; + out->buf[out->pos++] = (value & 0x00ff0000) >> 16; + out->buf[out->pos++] = (value & 0xff000000) >> 24; + return true; +} + +static inline bool write_version( + ntlm_client *ntlm, + ntlm_buf *out, + ntlm_version *version) +{ + return write_byte(ntlm, out, version->major) && + write_byte(ntlm, out, version->minor) && + write_int16(ntlm, out, version->build) && + write_int32(ntlm, out, version->reserved); +} + +static inline bool write_bufinfo( + ntlm_client *ntlm, + ntlm_buf *out, + size_t len, + size_t offset) +{ + if (len > UINT16_MAX) { + ntlm_client_set_errmsg(ntlm, "invalid string, too long"); + return false; + } + + if (offset > UINT32_MAX) { + ntlm_client_set_errmsg(ntlm, "invalid string, invalid offset"); + return false; + } + + return write_int16(ntlm, out, (uint16_t)len) && + write_int16(ntlm, out, (uint16_t)len) && + write_int32(ntlm, out, (uint32_t)offset); +} + +static inline bool read_buf( + unsigned char *out, + ntlm_client *ntlm, + ntlm_buf *message, + size_t len) +{ + if (message->len - message->pos < len) { + ntlm_client_set_errmsg(ntlm, "truncated message"); + return false; + } + + memcpy(out, &message->buf[message->pos], len); + message->pos += len; + + return true; +} + +static inline bool read_byte( + uint8_t *out, + ntlm_client *ntlm, + ntlm_buf *message) +{ + if (message->len - message->pos < 1) { + ntlm_client_set_errmsg(ntlm, "truncated message"); + return false; + } + + *out = message->buf[message->pos++]; + return true; +} + +static inline bool read_int16( + uint16_t *out, + ntlm_client *ntlm, + ntlm_buf *message) +{ + if (message->len - message->pos < 2) { + ntlm_client_set_errmsg(ntlm, "truncated message"); + return false; + } + + *out = + ((message->buf[message->pos] & 0xff)) | + ((message->buf[message->pos+1] & 0xff) << 8); + + message->pos += 2; + return true; +} + +static inline bool read_int32( + uint32_t *out, + ntlm_client *ntlm, + ntlm_buf *message) +{ + if (message->len - message->pos < 4) { + ntlm_client_set_errmsg(ntlm, "truncated message"); + return false; + } + + *out = + ((message->buf[message->pos] & 0xff)) | + ((message->buf[message->pos+1] & 0xff) << 8) | + ((message->buf[message->pos+2] & 0xff) << 16) | + ((message->buf[message->pos+3] & 0xff) << 24); + + message->pos += 4; + return true; +} + +static inline bool read_int64( + uint64_t *out, + ntlm_client *ntlm, + ntlm_buf *message) +{ + if (message->len - message->pos < 8) { + ntlm_client_set_errmsg(ntlm, "truncated message"); + return false; + } + + *out = + ((uint64_t)(message->buf[message->pos] & 0xff)) | + ((uint64_t)(message->buf[message->pos+1] & 0xff) << 8) | + ((uint64_t)(message->buf[message->pos+2] & 0xff) << 16) | + ((uint64_t)(message->buf[message->pos+3] & 0xff) << 24) | + ((uint64_t)(message->buf[message->pos+4] & 0xff) << 32) | + ((uint64_t)(message->buf[message->pos+5] & 0xff) << 40) | + ((uint64_t)(message->buf[message->pos+6] & 0xff) << 48) | + ((uint64_t)(message->buf[message->pos+7] & 0xff) << 56); + + message->pos += 8; + return true; +} + +static inline bool read_version( + ntlm_version *out, + ntlm_client *ntlm, + ntlm_buf *message) +{ + return read_byte(&out->major, ntlm, message) && + read_byte(&out->minor, ntlm, message) && + read_int16(&out->build, ntlm, message) && + read_int32(&out->reserved, ntlm, message); +} + +static inline bool read_bufinfo( + uint16_t *out_len, + uint32_t *out_offset, + ntlm_client *ntlm, + ntlm_buf *message) +{ + uint16_t allocated; + + return read_int16(out_len, ntlm, message) && + read_int16(&allocated, ntlm, message) && + read_int32(out_offset, ntlm, message); +} + +static inline bool read_string_unicode( + char **out, + ntlm_client *ntlm, + ntlm_buf *message, + uint8_t string_len) +{ + size_t out_len; + int ret = ntlm_unicode_utf16_to_8(out, + &out_len, + ntlm->unicode_ctx, + (char *)&message->buf[message->pos], + string_len); + + message->pos += string_len; + + return ret; +} + +static inline bool read_string_ascii( + char **out, + ntlm_client *ntlm, + ntlm_buf *message, + uint8_t string_len) +{ + char *str; + + if ((str = malloc(string_len + 1)) == NULL) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return false; + } + + memcpy(str, &message->buf[message->pos], string_len); + str[string_len] = '\0'; + + message->pos += string_len; + + *out = str; + return true; +} + +static inline bool read_string( + char **out, + ntlm_client *ntlm, + ntlm_buf *message, + uint8_t string_len, + bool unicode) +{ + if (unicode) + return read_string_unicode(out, ntlm, message, string_len); + else + return read_string_ascii(out, ntlm, message, string_len); +} + +static inline bool read_target_info( + char **server_out, + char **domain_out, + char **server_dns_out, + char **domain_dns_out, + ntlm_client *ntlm, + ntlm_buf *message, + bool unicode) +{ + uint16_t block_type, block_len; + bool done = false; + + *server_out = NULL; + *domain_out = NULL; + *server_dns_out = NULL; + *domain_dns_out = NULL; + + while (!done && (message->len - message->pos) >= 4) { + if (!read_int16(&block_type, ntlm, message) || + !read_int16(&block_len, ntlm, message)) { + ntlm_client_set_errmsg(ntlm, "truncated target info block"); + return false; + } + + if (!block_type && block_len) { + ntlm_client_set_errmsg(ntlm, "invalid target info block"); + return -1; + } + + switch (block_type) { + case NTLM_TARGET_INFO_DOMAIN: + if (!read_string(domain_out, ntlm, message, block_len, unicode)) + return -1; + break; + case NTLM_TARGET_INFO_SERVER: + if (!read_string(server_out, ntlm, message, block_len, unicode)) + return -1; + break; + case NTLM_TARGET_INFO_DOMAIN_DNS: + if (!read_string(domain_dns_out, ntlm, message, block_len, unicode)) + return -1; + break; + case NTLM_TARGET_INFO_SERVER_DNS: + if (!read_string(server_dns_out, ntlm, message, block_len, unicode)) + return -1; + break; + case NTLM_TARGET_INFO_END: + done = true; + break; + default: + ntlm_client_set_errmsg(ntlm, "unknown target info block type"); + return -1; + } + } + + if (message->len != message->pos) { + ntlm_client_set_errmsg(ntlm, + "invalid extra data in target info section"); + return false; + } + + return true; +} + +int ntlm_client_negotiate( + const unsigned char **out, + size_t *out_len, + ntlm_client *ntlm) +{ + size_t hostname_len, hostname_offset, domain_len, domain_offset; + uint32_t flags = 0; + + assert(out && out_len && ntlm); + + *out = NULL; + *out_len = 0; + + if (ntlm->state != NTLM_STATE_NEGOTIATE) { + ntlm_client_set_errmsg(ntlm, "ntlm handle in invalid state"); + return -1; + } + + flags |= NTLM_NEGOTIATE_OEM; + + if (supports_unicode(ntlm)) + flags |= NTLM_NEGOTIATE_UNICODE; + + if (!(ntlm->flags & NTLM_CLIENT_DISABLE_NTLM2) || + (ntlm->flags & NTLM_CLIENT_ENABLE_NTLM)) + flags |= NTLM_NEGOTIATE_NTLM; + + if (!(ntlm->flags & NTLM_CLIENT_DISABLE_REQUEST_TARGET)) + flags |= NTLM_NEGOTIATE_REQUEST_TARGET; + + hostname_len = ntlm->hostname ? strlen(ntlm->hostname) : 0; + domain_len = ntlm->hostdomain ? strlen(ntlm->hostdomain) : 0; + + /* Minimum header size */ + ntlm->negotiate.len = 16; + + /* Include space for security buffer descriptors */ + if (domain_len) + increment_size(&ntlm->negotiate.len, 8); + + if (hostname_len) + increment_size(&ntlm->negotiate.len, 8); + + if (ntlm->flags & NTLM_ENABLE_HOSTVERSION) + increment_size(&ntlm->negotiate.len, 8); + + /* Location of security buffers */ + if (hostname_len) { + flags |= NTLM_NEGOTIATE_WORKSTATION_SUPPLIED; + hostname_offset = ntlm->negotiate.len; + increment_size(&ntlm->negotiate.len, hostname_len); + } + + if (domain_len) { + flags |= NTLM_NEGOTIATE_DOMAIN_SUPPLIED; + domain_offset = ntlm->negotiate.len; + increment_size(&ntlm->negotiate.len, domain_len); + } + + if (ntlm->negotiate.len == (size_t)-1) { + ntlm_client_set_errmsg(ntlm, "message too large"); + return -1; + } + + if ((ntlm->negotiate.buf = malloc(ntlm->negotiate.len)) == NULL) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return -1; + } + + memset(ntlm->negotiate.buf, 0, ntlm->negotiate.len); + + if (!write_buf(ntlm, &ntlm->negotiate, + ntlm_client_signature, sizeof(ntlm_client_signature)) || + !write_int32(ntlm, &ntlm->negotiate, 1) || + !write_int32(ntlm, &ntlm->negotiate, flags)) + return -1; + + /* Domain information */ + if (domain_len > 0 && + !write_bufinfo(ntlm, &ntlm->negotiate, domain_len, domain_offset)) + return -1; + + /* Workstation information */ + if (hostname_len > 0 && + !write_bufinfo(ntlm, &ntlm->negotiate, hostname_len, hostname_offset)) + return -1; + + /* Version number */ + if (!!(ntlm->flags & NTLM_ENABLE_HOSTVERSION) && + !write_version(ntlm, &ntlm->negotiate, &ntlm->host_version)) + return -1; + + if (hostname_len > 0) { + assert(hostname_offset == ntlm->negotiate.pos); + if (!write_buf(ntlm, &ntlm->negotiate, + (const unsigned char *)ntlm->hostname, hostname_len)) + return -1; + } + + if (domain_len > 0) { + assert(domain_offset == ntlm->negotiate.pos); + if (!write_buf(ntlm, &ntlm->negotiate, + (const unsigned char *)ntlm->hostdomain, domain_len)) + return -1; + } + + assert(ntlm->negotiate.pos == ntlm->negotiate.len); + + ntlm->state = NTLM_STATE_CHALLENGE; + + *out = ntlm->negotiate.buf; + *out_len = ntlm->negotiate.len; + + return 0; +} + +int ntlm_client_set_challenge( + ntlm_client *ntlm, + const unsigned char *challenge_msg, + size_t challenge_msg_len) +{ + unsigned char signature[8]; + ntlm_buf challenge; + uint32_t type_indicator, header_end; + uint16_t name_len, info_len = 0; + uint32_t name_offset, info_offset = 0; + bool unicode, has_target_info = false; + + assert(ntlm && (challenge_msg || !challenge_msg_len)); + + if (ntlm->state != NTLM_STATE_NEGOTIATE && + ntlm->state != NTLM_STATE_CHALLENGE) { + ntlm_client_set_errmsg(ntlm, "ntlm handle in invalid state"); + return -1; + } + + challenge.buf = (unsigned char *)challenge_msg; + challenge.len = challenge_msg_len; + challenge.pos = 0; + + if (!read_buf(signature, ntlm, &challenge, 8) || + !read_int32(&type_indicator, ntlm, &challenge) || + !read_bufinfo(&name_len, &name_offset, ntlm, &challenge) || + !read_int32(&ntlm->challenge.flags, ntlm, &challenge) || + !read_int64(&ntlm->challenge.nonce, ntlm, &challenge)) + return -1; + + if (memcmp(signature, + ntlm_client_signature, sizeof(ntlm_client_signature)) != 0) { + ntlm_client_set_errmsg(ntlm, "invalid message signature"); + return -1; + } + + if (type_indicator != 2) { + ntlm_client_set_errmsg(ntlm, "invalid message indicator"); + return -1; + } + + /* + * If there's additional space before the data section, that's the + * target information description section. + */ + header_end = challenge.len; + + if (name_offset && name_offset < header_end) + header_end = name_offset; + + if ((header_end - challenge.pos) >= 16) { + has_target_info = true; + } + + if (!has_target_info && + (ntlm->challenge.flags & NTLM_NEGOTIATE_TARGET_INFO)) { + ntlm_client_set_errmsg(ntlm, + "truncated message; expected target info"); + return -1; + } + + /* + * If there's a target info section then advanced over the reserved + * space and read the target information. + */ + if (has_target_info) { + uint64_t reserved; + + if (!read_int64(&reserved, ntlm, &challenge)) { + ntlm_client_set_errmsg(ntlm, + "truncated message; expected reserved space"); + return -1; + } + + if (reserved != 0) { + ntlm_client_set_errmsg(ntlm, + "invalid message; expected reserved space to be empty"); + return -1; + } + + if (!read_bufinfo(&info_len, &info_offset, ntlm, &challenge)) { + ntlm_client_set_errmsg(ntlm, + "truncated message; expected target info"); + return -1; + } + } + + unicode = !!(ntlm->challenge.flags & NTLM_NEGOTIATE_UNICODE); + + /* + * If there's still additional space before the data section, + * that's the server's version information. + */ + if (info_offset && info_offset < header_end) + header_end = info_offset; + + if (ntlm->challenge.flags & NTLM_NEGOTIATE_VERSION) { + if ((header_end - challenge.pos) != sizeof(ntlm_version) || + !read_version(&ntlm->challenge.target_version, + ntlm, &challenge)) { + ntlm_client_set_errmsg(ntlm, + "truncated message; expected version"); + return -1; + } + } + + /* validate data section */ + if ((name_offset && name_offset < challenge.pos) || + challenge.len < name_len || + (challenge.len - name_len) < name_offset) { + ntlm_client_set_errmsg(ntlm, + "invalid message; invalid target name buffer"); + return -1; + } + if ((info_offset && info_offset < challenge.pos) || + challenge.len < info_len || + (challenge.len - info_len) < info_offset) { + ntlm_client_set_errmsg(ntlm, + "invalid message; invalid target info buffer"); + return -1; + } + + /* advance to the data section */ + if (name_len && name_offset) { + challenge.pos = name_offset; + + if (!read_string(&ntlm->challenge.target, + ntlm, &challenge, name_len, unicode)) { + ntlm_client_set_errmsg(ntlm, + "truncated message; truncated target name"); + return -1; + } + } + + if (info_len && info_offset) { + ntlm_buf info_buf; + + challenge.pos = info_offset; + + /* create a copy of the target info; we need the literal data */ + if ((ntlm->challenge.target_info = malloc(info_len)) == NULL) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return -1; + } + + if (!read_buf(ntlm->challenge.target_info, + ntlm, &challenge, info_len)) { + ntlm_client_set_errmsg(ntlm, + "truncated message; truncated target info"); + return -1; + } + + info_buf.buf = ntlm->challenge.target_info; + info_buf.pos = 0; + info_buf.len = info_len; + + /* then set up the target info and parse it */ + if (!read_target_info(&ntlm->challenge.target_server, + &ntlm->challenge.target_domain, + &ntlm->challenge.target_server_dns, + &ntlm->challenge.target_domain_dns, + ntlm, &info_buf, unicode)) + return -1; + + ntlm->challenge.target_info_len = info_len; + } + + ntlm->state = NTLM_STATE_RESPONSE; + + return 0; +} + +uint64_t ntlm_client_challenge_nonce(ntlm_client *ntlm) +{ + return ntlm->challenge.nonce; +} + +const char *ntlm_client_target(ntlm_client *ntlm) +{ + return ntlm->challenge.target; +} + +const char *ntlm_client_target_server(ntlm_client *ntlm) +{ + return ntlm->challenge.target_server; +} + +const char *ntlm_client_target_domain(ntlm_client *ntlm) +{ + return ntlm->challenge.target_domain; +} + +const char *ntlm_client_target_server_dns(ntlm_client *ntlm) +{ + return ntlm->challenge.target_server_dns; +} + +const char *ntlm_client_target_domain_dns(ntlm_client *ntlm) +{ + return ntlm->challenge.target_domain_dns; +} + +#define EVEN_PARITY(a) \ + (!!((a) & 0x01ll) ^ !!((a) & 0x02ll) ^ \ + !!((a) & 0x04ll) ^ !!((a) & 0x08ll) ^ \ + !!((a) & 0x10ll) ^ !!((a) & 0x20ll) ^ \ + !!((a) & 0x40ll) ^ !!((a) & 0x80ll)) + +static void generate_odd_parity(ntlm_des_block *block) +{ + size_t i; + + for (i = 0; i < sizeof(ntlm_des_block); i++) + (*block)[i] |= (1 ^ EVEN_PARITY((*block)[i])); +} + +static void des_key_from_password( + ntlm_des_block *out, + const unsigned char *plaintext, + size_t plaintext_len) +{ + size_t i; + + plaintext_len = MIN(plaintext_len, 7); + + memset(*out, 0, sizeof(ntlm_des_block)); + + for (i = 0; i < plaintext_len; i++) { + size_t j = (7 - i); + uint8_t mask = (0xff >> j); + + (*out)[i] |= ((plaintext[i] & (0xff - mask)) >> i); + (*out)[i+1] |= ((plaintext[i] & mask) << j); + } + + generate_odd_parity(out); +} + +static inline bool generate_lm_hash( + ntlm_des_block out[2], + const char *password) +{ + /* LM encrypts this known plaintext using the password as a key */ + ntlm_des_block plaintext = NTLM_LM_PLAINTEXT; + ntlm_des_block keystr1, keystr2; + size_t keystr1_len, keystr2_len; + ntlm_des_block key1, key2; + size_t password_len, i; + + /* Copy the first 14 characters of the password, uppercased */ + memset(&keystr1, 0, sizeof(keystr1)); + memset(&keystr2, 0, sizeof(keystr2)); + + password_len = password ? strlen(password) : 0; + + /* Split the password into two 7 byte chunks */ + keystr1_len = MIN(7, password_len); + keystr2_len = (password_len > 7) ? MIN(14, password_len) - 7 : 0; + + for (i = 0; i < keystr1_len; i++) + keystr1[i] = (unsigned char)toupper(password[i]); + for (i = 0; i < keystr2_len; i++) + keystr2[i] = (unsigned char)toupper(password[i+7]); + + /* DES encrypt the LM constant using the password as the key */ + des_key_from_password(&key1, keystr1, keystr1_len); + des_key_from_password(&key2, keystr2, keystr2_len); + + return ntlm_des_encrypt(&out[0], &plaintext, &key1) && + ntlm_des_encrypt(&out[1], &plaintext, &key2); +} + +static void des_keys_from_lm_hash(ntlm_des_block out[3], ntlm_des_block lm_hash[2]) +{ + ntlm_des_block split[3]; + + memcpy(&split[0][0], &lm_hash[0][0], 7); + + memcpy(&split[1][0], &lm_hash[0][7], 1); + memcpy(&split[1][1], &lm_hash[1][0], 6); + + memcpy(&split[2][0], &lm_hash[1][6], 2); + + des_key_from_password(&out[0], split[0], 7); + des_key_from_password(&out[1], split[1], 7); + des_key_from_password(&out[2], split[2], 2); +} + +static bool generate_lm_response(ntlm_client *ntlm) +{ + ntlm_des_block lm_hash[2], key[3], lm_response[3]; + ntlm_des_block *challenge = (ntlm_des_block *)&ntlm->challenge.nonce; + + /* Generate the LM hash from the password */ + if (!generate_lm_hash(lm_hash, ntlm->password)) + return false; + + /* Convert that LM hash to three DES keys */ + des_keys_from_lm_hash(key, lm_hash); + + /* Finally, encrypt the challenge with each of these keys */ + if (!ntlm_des_encrypt(&lm_response[0], challenge, &key[0]) || + !ntlm_des_encrypt(&lm_response[1], challenge, &key[1]) || + !ntlm_des_encrypt(&lm_response[2], challenge, &key[2])) + return false; + + memcpy(&ntlm->lm_response[0], lm_response[0], 8); + memcpy(&ntlm->lm_response[8], lm_response[1], 8); + memcpy(&ntlm->lm_response[16], lm_response[2], 8); + + ntlm->lm_response_len = sizeof(ntlm->lm_response); + + return true; +} + +static bool generate_ntlm_hash( + unsigned char out[NTLM_NTLM_HASH_LEN], ntlm_client *ntlm) +{ + /* Generate the LM hash from the (Unicode) password */ + if (ntlm->password && !ntlm_unicode_utf8_to_16( + &ntlm->password_utf16, + &ntlm->password_utf16_len, + ntlm->unicode_ctx, + ntlm->password, + strlen(ntlm->password))) + return false; + + return ntlm_md4_digest(out, + (const unsigned char *)ntlm->password_utf16, + ntlm->password_utf16_len); +} + +static bool generate_ntlm_response(ntlm_client *ntlm) +{ + unsigned char ntlm_hash[NTLM_NTLM_HASH_LEN] = {0}; + ntlm_des_block key[3], ntlm_response[3]; + ntlm_des_block *challenge = + (ntlm_des_block *)&ntlm->challenge.nonce; + + if (!generate_ntlm_hash(ntlm_hash, ntlm)) + return false; + + /* Convert that LM hash to three DES keys */ + des_key_from_password(&key[0], &ntlm_hash[0], 7); + des_key_from_password(&key[1], &ntlm_hash[7], 7); + des_key_from_password(&key[2], &ntlm_hash[14], 2); + + /* Finally, encrypt the challenge with each of these keys */ + if (!ntlm_des_encrypt(&ntlm_response[0], challenge, &key[0]) || + !ntlm_des_encrypt(&ntlm_response[1], challenge, &key[1]) || + !ntlm_des_encrypt(&ntlm_response[2], challenge, &key[2])) + return false; + + memcpy(&ntlm->ntlm_response[0], ntlm_response[0], 8); + memcpy(&ntlm->ntlm_response[8], ntlm_response[1], 8); + memcpy(&ntlm->ntlm_response[16], ntlm_response[2], 8); + + ntlm->ntlm_response_len = sizeof(ntlm->ntlm_response); + return true; +} + +static bool generate_ntlm2_hash( + unsigned char out[NTLM_NTLM2_HASH_LEN], ntlm_client *ntlm) +{ + unsigned char ntlm_hash[NTLM_NTLM_HASH_LEN] = {0}; + const unsigned char *username = NULL, *target = NULL; + size_t username_len = 0, target_len = 0, out_len = NTLM_NTLM2_HASH_LEN; + + if (!generate_ntlm_hash(ntlm_hash, ntlm)) + return false; + + if (ntlm->username_upper_utf16) { + username = (const unsigned char *)ntlm->username_upper_utf16; + username_len = ntlm->username_upper_utf16_len; + } + + if (ntlm->target_utf16) { + target = (const unsigned char *)ntlm->target_utf16; + target_len = ntlm->target_utf16_len; + } + + if (!ntlm_hmac_ctx_reset(ntlm->hmac_ctx) || + !ntlm_hmac_md5_init(ntlm->hmac_ctx, ntlm_hash, sizeof(ntlm_hash)) || + !ntlm_hmac_md5_update(ntlm->hmac_ctx, username, username_len) || + !ntlm_hmac_md5_update(ntlm->hmac_ctx, target, target_len) || + !ntlm_hmac_md5_final(out, &out_len, ntlm->hmac_ctx)) { + ntlm_client_set_errmsg(ntlm, "failed to create HMAC-MD5"); + return false; + } + + assert(out_len == NTLM_NTLM2_HASH_LEN); + return true; +} + +static bool generate_ntlm2_challengehash( + unsigned char out[16], + ntlm_client *ntlm, + unsigned char ntlm2_hash[NTLM_NTLM2_HASH_LEN], + const unsigned char *blob, + size_t blob_len) +{ + size_t out_len = 16; + + if (!ntlm_hmac_ctx_reset(ntlm->hmac_ctx) || + !ntlm_hmac_md5_init(ntlm->hmac_ctx, + ntlm2_hash, NTLM_NTLM2_HASH_LEN) || + !ntlm_hmac_md5_update(ntlm->hmac_ctx, + (const unsigned char *)&ntlm->challenge.nonce, 8) || + !ntlm_hmac_md5_update(ntlm->hmac_ctx, blob, blob_len) || + !ntlm_hmac_md5_final(out, &out_len, ntlm->hmac_ctx)) { + ntlm_client_set_errmsg(ntlm, "failed to create HMAC-MD5"); + return false; + } + + assert(out_len == 16); + return true; +} + +static bool generate_lm2_response(ntlm_client *ntlm, + unsigned char ntlm2_hash[NTLM_NTLM2_HASH_LEN]) +{ + unsigned char lm2_challengehash[16]; + size_t lm2_len = 16; + uint64_t local_nonce; + + local_nonce = htonll(ntlm->nonce); + + if (!ntlm_hmac_ctx_reset(ntlm->hmac_ctx) || + !ntlm_hmac_md5_init(ntlm->hmac_ctx, + ntlm2_hash, NTLM_NTLM2_HASH_LEN) || + !ntlm_hmac_md5_update(ntlm->hmac_ctx, + (const unsigned char *)&ntlm->challenge.nonce, 8) || + !ntlm_hmac_md5_update(ntlm->hmac_ctx, + (const unsigned char *)&local_nonce, 8) || + !ntlm_hmac_md5_final(lm2_challengehash, &lm2_len, ntlm->hmac_ctx)) { + ntlm_client_set_errmsg(ntlm, "failed to create HMAC-MD5"); + return false; + } + + assert(lm2_len == 16); + + memcpy(&ntlm->lm_response[0], lm2_challengehash, 16); + memcpy(&ntlm->lm_response[16], &local_nonce, 8); + + ntlm->lm_response_len = 24; + return true; +} + +static bool generate_timestamp(ntlm_client *ntlm) +{ + if (!ntlm->timestamp) + ntlm->timestamp = (time(NULL) + 11644473600) * 10000000; + + return true; +} + +static bool generate_nonce(ntlm_client *ntlm) +{ + unsigned char buf[8]; + + if (ntlm->nonce) + return true; + + if (!ntlm_random_bytes(ntlm, buf, 8)) + return false; + + memcpy(&ntlm->nonce, buf, sizeof(uint64_t)); + return true; +} + +static bool generate_ntlm2_response(ntlm_client *ntlm) +{ + size_t blob_len, ntlm2_response_len; + uint32_t signature; + uint64_t timestamp, nonce; + unsigned char ntlm2_hash[NTLM_NTLM2_HASH_LEN]; + unsigned char challengehash[16]; + unsigned char *blob; + + if (!generate_timestamp(ntlm) || + !generate_nonce(ntlm) || + !generate_ntlm2_hash(ntlm2_hash, ntlm)) + return false; + + blob_len = ntlm->challenge.target_info_len + 32; + ntlm2_response_len = blob_len + 16; + + if ((ntlm->ntlm2_response = malloc(ntlm2_response_len)) == NULL) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return false; + } + + /* position the blob in the response; we'll use it then return it */ + blob = ntlm->ntlm2_response + 16; + + /* the blob's integer values are in network byte order */ + signature = htonl(0x01010000); + timestamp = htonll(ntlm->timestamp); + nonce = htonll(ntlm->nonce); + + /* construct the blob */ + memcpy(&blob[0], &signature, 4); + memset(&blob[4], 0, 4); + memcpy(&blob[8], ×tamp, 8); + memcpy(&blob[16], &nonce, 8); + memset(&blob[24], 0, 4); + memcpy(&blob[28], ntlm->challenge.target_info, ntlm->challenge.target_info_len); + memset(&blob[28 + ntlm->challenge.target_info_len], 0, 4); + + if (!generate_ntlm2_challengehash(challengehash, ntlm, ntlm2_hash, blob, blob_len)) + return false; + + memcpy(ntlm->ntlm2_response, challengehash, 16); + ntlm->ntlm2_response_len = ntlm2_response_len; + + if (!generate_lm2_response(ntlm, ntlm2_hash)) + return false; + + return true; +} + +int ntlm_client_response( + const unsigned char **out, + size_t *out_len, + ntlm_client *ntlm) +{ + unsigned char *domain, *username, *hostname, *ntlm_rep, *session; + size_t lm_rep_len, lm_rep_offset, ntlm_rep_len, ntlm_rep_offset, + domain_len, domain_offset, username_len, username_offset, + hostname_len, hostname_offset, session_len, session_offset; + uint32_t flags = 0; + bool unicode; + + assert(out && out_len && ntlm); + + *out = NULL; + *out_len = 0; + + if (ntlm->state != NTLM_STATE_RESPONSE) { + ntlm_client_set_errmsg(ntlm, "ntlm handle in invalid state"); + return -1; + } + + /* + * Minimum message size is 64 bytes: + * 8 byte signature, + * 4 byte message indicator, + * 6x8 byte security buffers + * 4 byte flags + */ + ntlm->response.len = 64; + + unicode = supports_unicode(ntlm) && + (ntlm->challenge.flags & NTLM_NEGOTIATE_UNICODE); + + if (unicode) + flags |= NTLM_NEGOTIATE_UNICODE; + else + flags |= NTLM_NEGOTIATE_OEM; + + if (unicode) { + domain = (unsigned char *)ntlm->userdomain_utf16; + domain_len = ntlm->userdomain_utf16_len; + + username = (unsigned char *)ntlm->username_utf16; + username_len = ntlm->username_utf16_len; + + hostname = (unsigned char *)ntlm->hostname_utf16; + hostname_len = ntlm->hostname_utf16_len; + } else { + domain = (unsigned char *)ntlm->userdomain; + domain_len = ntlm->userdomain ? strlen(ntlm->userdomain) : 0; + + username = (unsigned char *)ntlm->username; + username_len = ntlm->username ? strlen(ntlm->username) : 0; + + hostname = (unsigned char *)ntlm->hostname; + hostname_len = ntlm->hostname ? strlen(ntlm->hostname) : 0; + } + + /* Negotiate our requested authentication type with the server's */ + if (!(ntlm->flags & NTLM_CLIENT_DISABLE_NTLM2) && + (ntlm->challenge.flags & NTLM_NEGOTIATE_NTLM)) { + flags |= NTLM_NEGOTIATE_NTLM; + + if (!generate_ntlm2_response(ntlm)) + return -1; + } else if ((ntlm->flags & NTLM_CLIENT_ENABLE_NTLM) && + (ntlm->challenge.flags & NTLM_NEGOTIATE_NTLM)) { + flags |= NTLM_NEGOTIATE_NTLM; + + if (!generate_ntlm_response(ntlm) || + !generate_lm_response(ntlm)) + return -1; + } else if (ntlm->flags & NTLM_CLIENT_ENABLE_LM) { + if (!generate_lm_response(ntlm)) + return -1; + } else { + ntlm_client_set_errmsg(ntlm, + "no encryption options could be negotiated"); + return -1; + } + + domain_offset = ntlm->response.len; + increment_size(&ntlm->response.len, domain_len); + + username_offset = ntlm->response.len; + increment_size(&ntlm->response.len, username_len); + + hostname_offset = ntlm->response.len; + increment_size(&ntlm->response.len, hostname_len); + + lm_rep_len = ntlm->lm_response_len; + lm_rep_offset = ntlm->response.len; + increment_size(&ntlm->response.len, lm_rep_len); + + ntlm_rep = ntlm->ntlm2_response_len ? + ntlm->ntlm2_response : ntlm->ntlm_response; + ntlm_rep_len = ntlm->ntlm2_response_len ? + ntlm->ntlm2_response_len : ntlm->ntlm_response_len; + ntlm_rep_offset = ntlm->response.len; + increment_size(&ntlm->response.len, ntlm_rep_len); + + session = NULL; + session_len = 0; + session_offset = ntlm->response.len; + increment_size(&ntlm->response.len, session_len); + + if (ntlm->response.len == (size_t)-1) { + ntlm_client_set_errmsg(ntlm, "message too large"); + return -1; + } + + if ((ntlm->response.buf = malloc(ntlm->response.len)) == NULL) { + ntlm_client_set_errmsg(ntlm, "out of memory"); + return -1; + } + + memset(ntlm->response.buf, 0, ntlm->response.len); + + if (!write_buf(ntlm, &ntlm->response, + ntlm_client_signature, sizeof(ntlm_client_signature)) || + !write_int32(ntlm, &ntlm->response, 3) || + !write_bufinfo(ntlm, &ntlm->response, lm_rep_len, lm_rep_offset) || + !write_bufinfo(ntlm, &ntlm->response, ntlm_rep_len, ntlm_rep_offset) || + !write_bufinfo(ntlm, &ntlm->response, domain_len, domain_offset) || + !write_bufinfo(ntlm, &ntlm->response, username_len, username_offset) || + !write_bufinfo(ntlm, &ntlm->response, hostname_len, hostname_offset) || + !write_bufinfo(ntlm, &ntlm->response, session_len, session_offset) || + !write_int32(ntlm, &ntlm->response, flags) || + !write_buf(ntlm, &ntlm->response, domain, domain_len) || + !write_buf(ntlm, &ntlm->response, username, username_len) || + !write_buf(ntlm, &ntlm->response, hostname, hostname_len) || + !write_buf(ntlm, &ntlm->response, ntlm->lm_response, lm_rep_len) || + !write_buf(ntlm, &ntlm->response, ntlm_rep, ntlm_rep_len) || + !write_buf(ntlm, &ntlm->response, session, session_len)) + return -1; + + assert(ntlm->response.pos == ntlm->response.len); + + ntlm->state = NTLM_STATE_COMPLETE; + + *out = ntlm->response.buf; + *out_len = ntlm->response.len; + + return 0; +} + +void ntlm_client_reset(ntlm_client *ntlm) +{ + ntlm_client_flags flags; + ntlm_hmac_ctx *hmac_ctx; + ntlm_unicode_ctx *unicode_ctx; + + assert(ntlm); + + free(ntlm->negotiate.buf); + free(ntlm->challenge.target_info); + free(ntlm->challenge.target); + free(ntlm->challenge.target_domain); + free(ntlm->challenge.target_domain_dns); + free(ntlm->challenge.target_server); + free(ntlm->challenge.target_server_dns); + free(ntlm->response.buf); + + free(ntlm->hostname); + free(ntlm->hostname_utf16); + free(ntlm->hostdomain); + + free(ntlm->target); + free(ntlm->target_utf16); + + free(ntlm->ntlm2_response); + + free_credentials(ntlm); + + flags = ntlm->flags; + hmac_ctx = ntlm->hmac_ctx; + unicode_ctx = ntlm->unicode_ctx; + + memset(ntlm, 0, sizeof(struct ntlm_client)); + + ntlm->flags = flags; + ntlm->hmac_ctx = hmac_ctx; + ntlm->unicode_ctx = unicode_ctx; +} + +void ntlm_client_free(ntlm_client *ntlm) +{ + if (!ntlm) + return; + + ntlm_client_reset(ntlm); + + ntlm_hmac_ctx_free(ntlm->hmac_ctx); + ntlm_unicode_ctx_free(ntlm->unicode_ctx); + + free(ntlm); +} diff --git a/deps/ntlmclient/ntlm.h b/deps/ntlmclient/ntlm.h new file mode 100644 index 000000000..0dad91ec0 --- /dev/null +++ b/deps/ntlmclient/ntlm.h @@ -0,0 +1,174 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#ifndef PRIVATE_NTLM_H__ +#define PRIVATE_NTLM_H__ + +#include "ntlmclient.h" +#include "unicode.h" +#include "crypt.h" +#include "compat.h" + +#define NTLM_LM_RESPONSE_LEN 24 +#define NTLM_NTLM_RESPONSE_LEN 24 +#define NTLM_NTLM_HASH_LEN 16 +#define NTLM_NTLM2_HASH_LEN 16 + +#define NTLM_SIGNATURE { 'N', 'T', 'L', 'M', 'S', 'S', 'P', 0x00 } + +#define NTLM_LM_PLAINTEXT { 0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 } + +typedef enum { + NTLM_STATE_NEGOTIATE = 0, + NTLM_STATE_CHALLENGE = 1, + NTLM_STATE_RESPONSE = 2, + NTLM_STATE_ERROR = 3, + NTLM_STATE_COMPLETE = 4, +} ntlm_state; + +typedef struct { + unsigned char *buf; + size_t pos; + size_t len; +} ntlm_buf; + +typedef struct { + uint8_t major; + uint8_t minor; + uint16_t build; + uint32_t reserved; +} ntlm_version; + +typedef struct { + uint32_t flags; + uint64_t nonce; + ntlm_version target_version; + + /* The unparsed target information from the server */ + unsigned char *target_info; + size_t target_info_len; + + /* The target information parsed into usable strings */ + char *target; + char *target_server; + char *target_domain; + char *target_server_dns; + char *target_domain_dns; +} ntlm_challenge; + +struct ntlm_client { + ntlm_client_flags flags; + + ntlm_state state; + + /* crypto contexts */ + ntlm_hmac_ctx *hmac_ctx; + ntlm_unicode_ctx *unicode_ctx; + + /* error message as set by the library */ + const char *errmsg; + + char *hostname; + char *hostdomain; + ntlm_version host_version; + + char *target; + + char *username; + char *username_upper; + char *userdomain; + char *password; + + /* strings as converted to utf16 */ + char *target_utf16; + char *username_utf16; + char *username_upper_utf16; + char *userdomain_utf16; + char *hostname_utf16; + char *password_utf16; + + /* timestamp and nonce; only for debugging */ + uint64_t nonce; + uint64_t timestamp; + + size_t username_utf16_len; + size_t username_upper_utf16_len; + size_t userdomain_utf16_len; + size_t hostname_utf16_len; + size_t password_utf16_len; + size_t target_utf16_len; + + unsigned char lm_response[NTLM_LM_RESPONSE_LEN]; + size_t lm_response_len; + + unsigned char ntlm_response[NTLM_NTLM_RESPONSE_LEN]; + size_t ntlm_response_len; + + unsigned char *ntlm2_response; + size_t ntlm2_response_len; + + ntlm_buf negotiate; + ntlm_challenge challenge; + ntlm_buf response; +}; + +typedef enum { + NTLM_ENABLE_HOSTVERSION = (1 << 31), +} ntlm_client_internal_flags; + +typedef enum { + NTLM_TARGET_INFO_END = 0, + NTLM_TARGET_INFO_SERVER = 1, + NTLM_TARGET_INFO_DOMAIN = 2, + NTLM_TARGET_INFO_SERVER_DNS = 3, + NTLM_TARGET_INFO_DOMAIN_DNS = 4, +} ntlm_target_info_type_t; + +typedef enum { + /* Unicode strings are supported in security buffers */ + NTLM_NEGOTIATE_UNICODE = 0x00000001, + + /* OEM (ANSI) strings are supported in security buffers */ + NTLM_NEGOTIATE_OEM = 0x00000002, + + /* Request the target realm from the server */ + NTLM_NEGOTIATE_REQUEST_TARGET = 0x00000004, + + /* NTLM authentication is supported */ + NTLM_NEGOTIATE_NTLM = 0x00000200, + + /* Negotiate domain name */ + NTLM_NEGOTIATE_DOMAIN_SUPPLIED = 0x00001000, + + /* Negotiate workstation (client) name */ + NTLM_NEGOTIATE_WORKSTATION_SUPPLIED = 0x00002000, + + /* Indicates that a local context is available */ + NTLM_NEGOTIATE_LOCAL_CALL = 0x00004000, + + /* Request a dummy signature */ + NTLM_NEGOTIATE_ALWAYS_SIGN = 0x00008000, + + /* Target (server) is a domain */ + NTLM_NEGOTIATE_TYPE_DOMAIN = 0x00010000, + + /* NTLM2 signing and sealing is supported */ + NTLM_NEGOTIATE_NTLM2_SIGN_AND_SEAL = 0x00080000, + + /* A target information block is included */ + NTLM_NEGOTIATE_TARGET_INFO = 0x00800000, + + /* Version information should be provided */ + NTLM_NEGOTIATE_VERSION = 0x01000000, +} ntlm_negotiate_t; + +extern int ntlm_client_set_nonce(ntlm_client *ntlm, uint64_t nonce); +extern int ntlm_client_set_timestamp(ntlm_client *ntlm, uint64_t timestamp); +extern void ntlm_client_set_errmsg(ntlm_client *ntlm, const char *errmsg); + +#endif /* PRIVATE_NTLM_H__ */ diff --git a/deps/ntlmclient/ntlmclient.h b/deps/ntlmclient/ntlmclient.h new file mode 100644 index 000000000..d109a5c89 --- /dev/null +++ b/deps/ntlmclient/ntlmclient.h @@ -0,0 +1,320 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ +#ifndef INCLUDE_NTLMCLIENT_H__ +#define INCLUDE_NTLMCLIENT_H__ + +#include <stdlib.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#define NTLM_CLIENT_VERSION "0.0.1" +#define NTLM_CLIENT_VERSION_MAJOR 0 +#define NTLM_CLIENT_VERSION_MINOR 0 +#define NTLM_CLIENT_VERSION_TEENY 1 + +typedef struct ntlm_client ntlm_client; + +/* + * Flags for initializing the `ntlm_client` context. A combination of + * these flags can be provided to `ntlm_client_init`. + */ +typedef enum { + /** Default settings for the `ntlm_client`. */ + NTLM_CLIENT_DEFAULTS = 0, + + /** + * Disable Unicode negotiation. By default, strings are converted + * into UTF-16 when supplied to the remote host, but if this flag + * is specified, localizable strings (like username and password) + * will only be sent to the server as they were provided to the + * library. Since the NTLM protocol does not deliver the locale + * information, these will be interpreted by the remote host in + * whatever locale is configured, and likely be corrupted unless + * you limit yourself to ASCII. + * + * You are discouraged from setting this flag. + */ + NTLM_CLIENT_DISABLE_UNICODE = (1 << 0), + + /* + * Enable LM ("Lan Manager") authentication support. By default, + * LM authentication is disabled, since most remote servers have + * disabled support for it, and because it is both trivially + * brute-forced _and_ subject to rainbow table lookups. If this + * flag is enabled, LM is still not used unless NTLM2 support is + * also disabled. + * + * You are discouraged from setting this flag. + */ + NTLM_CLIENT_ENABLE_LM = (1 << 1), + + /* + * Enable NTLM ("Lan Manager") authentication support. By default, + * NTLM authentication is disabled, since most remote servers have + * disabled support for it, due to its weakness. If this flag is + * enabled, NTLM is still not used unless NTLM2 support is also + * disabled. + * + * You are discouraged from setting this flag. + */ + NTLM_CLIENT_ENABLE_NTLM = (1 << 2), + + /* + * Disable NTLM2 authentication support. By default, _only_ NTLM2 + * support is enabled, since most remote servers will only support + * it due to its (relative) lack of weakness. If this flag is + * set, either NTLM or LM (or both) must be explicitly enabled or + * there will be no mechanisms available to use. + * + * You are discouraged from setting this flag. + */ + NTLM_CLIENT_DISABLE_NTLM2 = (1 << 3), + + /* + * Request the target's name. By default, you are expected to + * provide the name of the target you are authenticating to (eg, + * the remote hostname). If set, the remote host will provide + * its idea of its hostname in the challenge message. You may + * then set the authentication target based on it. + */ + NTLM_CLIENT_DISABLE_REQUEST_TARGET = (1 << 4), +} ntlm_client_flags; + + +/** Declare a public function exported for application use. */ +#if __GNUC__ >= 4 && !defined(NTLM_STATIC) +# define NTLM_EXTERN(type) extern \ + __attribute__((visibility("default"))) \ + type +#elif defined(_MSC_VER) && !defined(NTLM_STATIC) +# define NTLM_EXTERN(type) __declspec(dllexport) type +#else +# define NTLM_EXTERN(type) extern type +#endif + +/** + * Initializes an `ntlm_client` context, which can begin sending + * and receiving NTLM authentication messages. + * + * @param flags the `ntlm_client_flag_t`s to use for negotiation. + * @return the `ntlm_client` context, or `NULL` if out-of-memory. + */ +NTLM_EXTERN(ntlm_client *) ntlm_client_init(ntlm_client_flags flags); + +/** + * Gets the error message for the most recent error that occurred. If + * a function returns an error, more details can be retrieved with this + * function. The string returned is a constant string; it should not + * be freed. + * + * @return a constant string containing the error message. + */ +NTLM_EXTERN(const char *) ntlm_client_errmsg(ntlm_client *ntlm); + +/** + * Sets the local hostname and domain. These strings should be in + * ASCII. They will be provided to the remote host during the + * negotiation phase. + * + * @param ntlm the `ntlm_client` context to configure + * @param hostname the hostname of the local machine + * @param domain the domain of the local machine + * @return 0 on success, non-zero on failure + */ +NTLM_EXTERN(int) ntlm_client_set_hostname( + ntlm_client *ntlm, + const char *hostname, + const char *domain); + +/** + * Sets the local operating system version. These numbers are expected + * to correspond to Windows operating system versions; for example + * major version 6, minor version 2, build 9200 would correspond to + * Windows 8 (aka "NT 6.2"). + * + * It is not likely that you need to set the local version. + * + * @param ntlm the `ntlm_client` context to configure + * @param major the major version number of the local operating system + * @param minor the minor version number of the local operating system + * @param build the build number of the local operating system + * @return 0 on success, non-zero on failure + */ +NTLM_EXTERN(int) ntlm_client_set_version( + ntlm_client *ntlm, + uint8_t major, + uint8_t minor, + uint16_t build); + +/** + * Sets the username and password to authenticate with to the remote + * host. Username and password may be specified in UTF-8 but the + * domain should be in ASCII. These will not be sent to the remote host + * but will instead be used to compute the LM, NTLM or NTLM2 responses, + * which will be provided to the remote host during the response phase. + * + * @param ntlm the `ntlm_client` context to configure + * @param username the username to authenticate with + * @param domain the domain of the user authenticating + * @param password the password to authenticate with + * @return 0 on success, non-zero on failure + */ +NTLM_EXTERN(int) ntlm_client_set_credentials( + ntlm_client *ntlm, + const char *username, + const char *domain, + const char *password); + +/** + * Sets the authentication target, your idea of the remote host's + * name. The target should be provided as ASCII. It will be + * provided to the remote host during the response phase. + * + * @param ntlm the `ntlm_client` context to configure + * @param target the name of the authentication target + * @return 0 on success, non-zero on failure + */ +NTLM_EXTERN(int) ntlm_client_set_target( + ntlm_client *ntlm, + const char *target); + +/** + * Gets the remote host's nonce, as it was provided in the challenge + * message. This is an opaque 8 byte value that is used to compute + * the LM, NTLM and NTLM2 responses. + * + * @param ntlm the `ntlm_client` context to query + * @return the challenge from the remote host + */ +NTLM_EXTERN(uint64_t) ntlm_client_challenge_nonce( + ntlm_client *ntlm); + +/** + * Gets the remote hosts's target name, which can be used as the + * authentication target. This will be given as it was provided + * in the challenge message. + * + * @param ntlm the `ntlm_client` context to query + * @return the remote host's target name + */ +NTLM_EXTERN(const char *) ntlm_client_target(ntlm_client *ntlm); + +/** + * Gets the remote hosts's name, which is generally its short name. + * This will be given as it was provided in the challenge message. + * + * @param ntlm the `ntlm_client` context to query + * @return the remote host's server name + */ +NTLM_EXTERN(const char *) ntlm_client_target_server(ntlm_client *ntlm); + +/** + * Gets the remote hosts's domain, which is generally the short or + * NT-style domain name. This will be given as it was provided in + * the challenge message. + * + * @param ntlm the `ntlm_client` context to query + * @return the remote host's domain + */ +NTLM_EXTERN(const char *) ntlm_client_target_domain(ntlm_client *ntlm); + +/** + * Gets the remote hosts's DNS name, which is generally the long-style + * Active Directory or fully-qualified hostname. This will be given + * as it was provided in the challenge message. + * + * @param ntlm the `ntlm_client` context to query + * @return the remote host's DNS name + */ +NTLM_EXTERN(const char *) ntlm_client_target_server_dns(ntlm_client *ntlm); + +/** + * Gets the remote hosts's DNS domain, which is generally the long-style + * Active Directory or fully-qualified domain name. This will be given + * as it was provided in the challenge message. + * + * @param ntlm the `ntlm_client` context to query + * @return the remote host's DNS domain + */ +NTLM_EXTERN(const char *) ntlm_client_target_domain_dns(ntlm_client *ntlm); + +/** + * Computes a negotiation message (aka a "Type 1" message) to begin + * NTLM authentication with the server. The local hostname should be + * set before calling this function (if necessary). This message + * should be delivered to the server to indicate a willingness to begin + * NTLM authentication. This buffer should not be freed by the caller. + * + * @param out a pointer to the negotiation message + * @param out_len a pointer to the length of the negotiation message + * @param ntlm the `ntlm_client` context + * @return 0 on success, non-zero on failure + */ +NTLM_EXTERN(int) ntlm_client_negotiate( + const unsigned char **out, + size_t *out_len, + ntlm_client *ntlm); + +/** + * Parses a challenge message (aka a "Type 2" message) from the server. + * This must be called in order to calculate the response to the + * authentication. + * + * @param ntlm the `ntlm_client` context + * @param message the challenge message from the server + * @param message_len the length of the challenge message + * @return 0 on success, non-zero on failure + */ +NTLM_EXTERN(int) ntlm_client_set_challenge( + ntlm_client *ntlm, + const unsigned char *message, + size_t message_len); + +/** + * Computes a response message (aka a "Type 3" message) to complete + * NTLM authentication with the server. The credentials should be + * set before calling this function. This message should be delivered + * to the server to complete authentication. This buffer should not + * be freed by the caller. + * + * @param out a pointer to the response message + * @param out_len a pointer to the length of the response message + * @param ntlm the `ntlm_client` context + * @return 0 on success, non-zero on failure + */ +NTLM_EXTERN(int) ntlm_client_response( + const unsigned char **out, + size_t *out_len, + ntlm_client *ntlm); + +/** + * Resets an `ntlm_client` context completely, so that authentication + * may be retried. You must set _all_ parameters again, including the + * target, username, password, etc. Once these values are configured + * again, the negotiation can begin. + * + * @param ntlm the `ntlm_client` context to reset + */ +NTLM_EXTERN(void) ntlm_client_reset(ntlm_client *ntlm); + +/** + * Frees an `ntlm_client` context. This should be done to free memory + * belonging to the context. The context cannot be reused. + * + * @param ntlm the `ntlm_client` context to free + */ +NTLM_EXTERN(void) ntlm_client_free(ntlm_client *ntlm); + +#ifdef __cplusplus +} +#endif + +#endif /* INCLUDE_NTLMCLIENT_H__ */ diff --git a/deps/ntlmclient/unicode.h b/deps/ntlmclient/unicode.h new file mode 100644 index 000000000..e3b17bcf7 --- /dev/null +++ b/deps/ntlmclient/unicode.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#ifndef PRIVATE_UNICODE_H__ +#define PRIVATE_UNICODE_H__ + +#include "compat.h" + +#define NTLM_UNICODE_MAX_LEN 2048 + +typedef struct ntlm_unicode_ctx ntlm_unicode_ctx; + +extern ntlm_unicode_ctx *ntlm_unicode_ctx_init(ntlm_client *ntlm); + +bool ntlm_unicode_utf8_to_16( + char **converted, + size_t *converted_len, + ntlm_unicode_ctx *ctx, + const char *string, + size_t string_len); + +bool ntlm_unicode_utf16_to_8( + char **converted, + size_t *converted_len, + ntlm_unicode_ctx *ctx, + const char *string, + size_t string_len); + +extern void ntlm_unicode_ctx_free(ntlm_unicode_ctx *ctx); + +#endif /* PRIVATE_UNICODE_H__ */ diff --git a/deps/ntlmclient/unicode_builtin.c b/deps/ntlmclient/unicode_builtin.c new file mode 100644 index 000000000..e1856cca9 --- /dev/null +++ b/deps/ntlmclient/unicode_builtin.c @@ -0,0 +1,445 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#include <stdlib.h> +#include <stdint.h> + +#include "ntlm.h" +#include "unicode.h" +#include "compat.h" + +struct ntlm_unicode_ctx { + ntlm_client *ntlm; +}; + +typedef unsigned int UTF32; /* at least 32 bits */ +typedef unsigned short UTF16; /* at least 16 bits */ +typedef unsigned char UTF8; /* typically 8 bits */ + +/* Some fundamental constants */ +#define UNI_REPLACEMENT_CHAR (UTF32)0x0000FFFD +#define UNI_MAX_BMP (UTF32)0x0000FFFF +#define UNI_MAX_UTF16 (UTF32)0x0010FFFF +#define UNI_MAX_UTF32 (UTF32)0x7FFFFFFF +#define UNI_MAX_LEGAL_UTF32 (UTF32)0x0010FFFF + +#define UNI_MAX_UTF8_BYTES_PER_CODE_POINT 4 + +typedef enum { + conversionOK, /* conversion successful */ + sourceExhausted, /* partial character in source, but hit end */ + targetExhausted, /* insuff. room in target for conversion */ + sourceIllegal /* source sequence is illegal/malformed */ +} ConversionResult; + +typedef enum { + strictConversion = 0, + lenientConversion +} ConversionFlags; + + +static const int halfShift = 10; /* used for shifting by 10 bits */ + +static const UTF32 halfBase = 0x0010000UL; +static const UTF32 halfMask = 0x3FFUL; + +#define UNI_SUR_HIGH_START (UTF32)0xD800 +#define UNI_SUR_HIGH_END (UTF32)0xDBFF +#define UNI_SUR_LOW_START (UTF32)0xDC00 +#define UNI_SUR_LOW_END (UTF32)0xDFFF +#define false 0 +#define true 1 + +/* --------------------------------------------------------------------- */ + +/* + * Index into the table below with the first byte of a UTF-8 sequence to + * get the number of trailing bytes that are supposed to follow it. + * Note that *legal* UTF-8 values can't have 4 or 5-bytes. The table is + * left as-is for anyone who may want to do such conversion, which was + * allowed in earlier algorithms. + */ +static const char trailingBytesForUTF8[256] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5 +}; + +/* + * Magic values subtracted from a buffer value during UTF8 conversion. + * This table contains as many values as there might be trailing bytes + * in a UTF-8 sequence. + */ +static const UTF32 offsetsFromUTF8[6] = { + 0x00000000UL, 0x00003080UL, 0x000E2080UL, + 0x03C82080UL, 0xFA082080UL, 0x82082080UL }; + +/* + * Once the bits are split out into bytes of UTF-8, this is a mask OR-ed + * into the first byte, depending on how many bytes follow. There are + * as many entries in this table as there are UTF-8 sequence types. + * (I.e., one byte sequence, two byte... etc.). Remember that sequencs + * for *legal* UTF-8 will be 4 or fewer bytes total. + */ +static const UTF8 firstByteMark[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; + +/* --------------------------------------------------------------------- */ + +/* The interface converts a whole buffer to avoid function-call overhead. + * Constants have been gathered. Loops & conditionals have been removed as + * much as possible for efficiency, in favor of drop-through switches. + * (See "Note A" at the bottom of the file for equivalent code.) + * If your compiler supports it, the "isLegalUTF8" call can be turned + * into an inline function. + */ + +static ConversionResult ConvertUTF16toUTF8 ( + const UTF16** sourceStart, const UTF16* sourceEnd, + UTF8** targetStart, UTF8* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF16* source = *sourceStart; + UTF8* target = *targetStart; + while (source < sourceEnd) { + UTF32 ch; + unsigned short bytesToWrite = 0; + const UTF32 byteMask = 0xBF; + const UTF32 byteMark = 0x80; + const UTF16* oldSource = source; /* In case we have to back up because of target overflow. */ + ch = *source++; + /* If we have a surrogate pair, convert to UTF32 first. */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_HIGH_END) { + /* If the 16 bits following the high surrogate are in the source buffer... */ + if (source < sourceEnd) { + UTF32 ch2 = *source; + /* If it's a low surrogate, convert to UTF32. */ + if (ch2 >= UNI_SUR_LOW_START && ch2 <= UNI_SUR_LOW_END) { + ch = ((ch - UNI_SUR_HIGH_START) << halfShift) + + (ch2 - UNI_SUR_LOW_START) + halfBase; + ++source; + } else if (flags == strictConversion) { /* it's an unpaired high surrogate */ + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } else { /* We don't have the 16 bits following the high surrogate. */ + --source; /* return to the high surrogate */ + result = sourceExhausted; + break; + } + } else if (flags == strictConversion) { + /* UTF-16 surrogate values are illegal in UTF-32 */ + if (ch >= UNI_SUR_LOW_START && ch <= UNI_SUR_LOW_END) { + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } + /* Figure out how many bytes the result will require */ + if (ch < (UTF32)0x80) { bytesToWrite = 1; + } else if (ch < (UTF32)0x800) { bytesToWrite = 2; + } else if (ch < (UTF32)0x10000) { bytesToWrite = 3; + } else if (ch < (UTF32)0x110000) { bytesToWrite = 4; + } else { bytesToWrite = 3; + ch = UNI_REPLACEMENT_CHAR; + } + + target += bytesToWrite; + if (target > targetEnd) { + source = oldSource; /* Back up source pointer! */ + target -= bytesToWrite; result = targetExhausted; break; + } + switch (bytesToWrite) { /* note: everything falls through. */ + case 4: *--target = (UTF8)((ch | byteMark) & byteMask); ch >>= 6; + case 3: *--target = (UTF8)((ch | byteMark) & byteMask); ch >>= 6; + case 2: *--target = (UTF8)((ch | byteMark) & byteMask); ch >>= 6; + case 1: *--target = (UTF8)(ch | firstByteMark[bytesToWrite]); + } + target += bytesToWrite; + } + *sourceStart = source; + *targetStart = target; + return result; +} + +/* --------------------------------------------------------------------- */ + +/* + * Utility routine to tell whether a sequence of bytes is legal UTF-8. + * This must be called with the length pre-determined by the first byte. + * If not calling this from ConvertUTF8to*, then the length can be set by: + * length = trailingBytesForUTF8[*source]+1; + * and the sequence is illegal right away if there aren't that many bytes + * available. + * If presented with a length > 4, this returns false. The Unicode + * definition of UTF-8 goes up to 4-byte sequences. + */ + +static inline bool isLegalUTF8(const UTF8 *source, int length) { + UTF8 a; + const UTF8 *srcptr = source+length; + switch (length) { + default: return false; + /* Everything else falls through when "true"... */ + case 4: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return false; + case 3: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return false; + case 2: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return false; + + switch (*source) { + /* no fall-through in this inner switch */ + case 0xE0: if (a < 0xA0) return false; break; + case 0xED: if (a > 0x9F) return false; break; + case 0xF0: if (a < 0x90) return false; break; + case 0xF4: if (a > 0x8F) return false; break; + default: if (a < 0x80) return false; + } + + case 1: if (*source >= 0x80 && *source < 0xC2) return false; + } + if (*source > 0xF4) return false; + return true; +} + +static ConversionResult ConvertUTF8toUTF16 ( + const UTF8** sourceStart, const UTF8* sourceEnd, + UTF16** targetStart, UTF16* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF8* source = *sourceStart; + UTF16* target = *targetStart; + while (source < sourceEnd) { + UTF32 ch = 0; + unsigned short extraBytesToRead = trailingBytesForUTF8[*source]; + if (extraBytesToRead >= sourceEnd - source) { + result = sourceExhausted; break; + } + /* Do this check whether lenient or strict */ + if (!isLegalUTF8(source, extraBytesToRead+1)) { + result = sourceIllegal; + break; + } + /* + * The cases all fall through. See "Note A" below. + */ + switch (extraBytesToRead) { + case 5: ch += *source++; ch <<= 6; /* remember, illegal UTF-8 */ + case 4: ch += *source++; ch <<= 6; /* remember, illegal UTF-8 */ + case 3: ch += *source++; ch <<= 6; + case 2: ch += *source++; ch <<= 6; + case 1: ch += *source++; ch <<= 6; + case 0: ch += *source++; + } + ch -= offsetsFromUTF8[extraBytesToRead]; + + if (target >= targetEnd) { + source -= (extraBytesToRead+1); /* Back up source pointer! */ + result = targetExhausted; break; + } + if (ch <= UNI_MAX_BMP) { /* Target is a character <= 0xFFFF */ + /* UTF-16 surrogate values are illegal in UTF-32 */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END) { + if (flags == strictConversion) { + source -= (extraBytesToRead+1); /* return to the illegal value itself */ + result = sourceIllegal; + break; + } else { + *target++ = UNI_REPLACEMENT_CHAR; + } + } else { + *target++ = (UTF16)ch; /* normal case */ + } + } else if (ch > UNI_MAX_UTF16) { + if (flags == strictConversion) { + result = sourceIllegal; + source -= (extraBytesToRead+1); /* return to the start */ + break; /* Bail out; shouldn't continue */ + } else { + *target++ = UNI_REPLACEMENT_CHAR; + } + } else { + /* target is a character in range 0xFFFF - 0x10FFFF. */ + if (target + 1 >= targetEnd) { + source -= (extraBytesToRead+1); /* Back up source pointer! */ + result = targetExhausted; break; + } + ch -= halfBase; + *target++ = (UTF16)((ch >> halfShift) + UNI_SUR_HIGH_START); + *target++ = (UTF16)((ch & halfMask) + UNI_SUR_LOW_START); + } + } + *sourceStart = source; + *targetStart = target; + return result; +} + + +ntlm_unicode_ctx *ntlm_unicode_ctx_init(ntlm_client *ntlm) +{ + ntlm_unicode_ctx *ctx; + + if ((ctx = malloc(sizeof(ntlm_unicode_ctx))) == NULL) + return NULL; + + ctx->ntlm = ntlm; + return ctx; +} + +typedef enum { + unicode_builtin_utf8_to_16, + unicode_builtin_utf16_to_8 +} unicode_builtin_encoding_direction; + +static inline bool unicode_builtin_encoding_convert( + char **converted, + size_t *converted_len, + ntlm_unicode_ctx *ctx, + const char *string, + size_t string_len, + unicode_builtin_encoding_direction direction) +{ + const char *in_start, *in_end; + char *out, *out_start, *out_end, *new_out; + size_t out_size, out_len; + bool success = false; + ConversionResult result; + + *converted = NULL; + *converted_len = 0; + + in_start = string; + in_end = in_start + string_len; + + /* + * When translating UTF8 to UTF16, these strings are only used + * internally, and we obey the given length, so we can simply + * use a buffer that is 2x the size. Add an extra byte to NUL + * terminate the results (two bytes for UTF16). + */ + if (direction == unicode_builtin_utf8_to_16) + out_size = (string_len * 2 + 2); + else + out_size = (string_len / 2 + 1); + + /* Round to the nearest multiple of 8 */ + out_size = (out_size + 7) & ~7; + + if ((out = malloc(out_size)) == NULL) { + ntlm_client_set_errmsg(ctx->ntlm, "out of memory"); + return false; + } + + out_start = out; + out_end = out_start + out_size; + + /* Make room for NUL termination */ + if (direction == unicode_builtin_utf16_to_8) + out_end--; + + while (true) { + if (direction == unicode_builtin_utf8_to_16) + result = ConvertUTF8toUTF16( + (const UTF8 **)&in_start, (UTF8 *)in_end, + (UTF16 **)&out_start, (UTF16 *)out_end, strictConversion); + else + result = ConvertUTF16toUTF8( + (const UTF16 **)&in_start, (UTF16 *)in_end, + (UTF8 **)&out_start, (UTF8 *)out_end, lenientConversion); + + switch (result) { + case conversionOK: + success = true; + goto done; + case sourceExhausted: + ntlm_client_set_errmsg(ctx->ntlm, + "invalid unicode string; trailing data remains"); + goto done; + case targetExhausted: + break; + case sourceIllegal: + ntlm_client_set_errmsg(ctx->ntlm, + "invalid unicode string; trailing data remains"); + goto done; + default: + ntlm_client_set_errmsg(ctx->ntlm, + "unknown unicode conversion failure"); + goto done; + } + + /* Grow buffer size by 1.5 (rounded up to a multiple of 8) */ + out_size = ((((out_size << 1) - (out_size >> 1)) + 7) & ~7); + + if (out_size > NTLM_UNICODE_MAX_LEN) { + ntlm_client_set_errmsg(ctx->ntlm, + "unicode conversion too large"); + goto done; + } + + if ((new_out = realloc(out, out_size)) == NULL) { + ntlm_client_set_errmsg(ctx->ntlm, "out of memory"); + goto done; + } + + out_len = out_start - out; + + out = new_out; + out_start = new_out + out_len; + out_end = out + out_size; + + /* Make room for NUL termination */ + out_end -= (direction == unicode_builtin_utf8_to_16) ? 2 : 1; + } + +done: + if (!success) { + free(out); + return false; + } + + out_len = (out_start - out); + + /* NUL terminate */ + out[out_len] = '\0'; + + if (direction == unicode_builtin_utf8_to_16) + out[out_len+1] = '\0'; + + *converted = out; + *converted_len = out_len; + return true; +} + +bool ntlm_unicode_utf8_to_16( + char **converted, + size_t *converted_len, + ntlm_unicode_ctx *ctx, + const char *string, + size_t string_len) +{ + return unicode_builtin_encoding_convert(converted, converted_len, + ctx, string, string_len, unicode_builtin_utf8_to_16); +} + +bool ntlm_unicode_utf16_to_8( + char **converted, + size_t *converted_len, + ntlm_unicode_ctx *ctx, + const char *string, + size_t string_len) +{ + return unicode_builtin_encoding_convert(converted, converted_len, + ctx, string, string_len, unicode_builtin_utf16_to_8); +} + +void ntlm_unicode_ctx_free(ntlm_unicode_ctx *ctx) +{ + if (ctx) + free(ctx); +} diff --git a/deps/ntlmclient/unicode_iconv.c b/deps/ntlmclient/unicode_iconv.c new file mode 100644 index 000000000..d1fe07e26 --- /dev/null +++ b/deps/ntlmclient/unicode_iconv.c @@ -0,0 +1,201 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#include <locale.h> +#include <iconv.h> +#include <string.h> +#include <errno.h> + +#include "ntlmclient.h" +#include "unicode.h" +#include "ntlm.h" +#include "compat.h" + +struct ntlm_unicode_ctx { + ntlm_client *ntlm; + iconv_t utf8_to_16; + iconv_t utf16_to_8; +}; + +ntlm_unicode_ctx *ntlm_unicode_ctx_init(ntlm_client *ntlm) +{ + ntlm_unicode_ctx *ctx; + + if ((ctx = calloc(1, sizeof(ntlm_unicode_ctx))) == NULL) + return NULL; + + ctx->ntlm = ntlm; + ctx->utf8_to_16 = (iconv_t)-1; + ctx->utf16_to_8 = (iconv_t)-1; + + return ctx; +} + +typedef enum { + unicode_iconv_utf8_to_16, + unicode_iconv_utf16_to_8 +} unicode_iconv_encoding_direction; + +static inline bool unicode_iconv_init(ntlm_unicode_ctx *ctx) +{ + if (ctx->utf8_to_16 != (iconv_t)-1 || ctx->utf16_to_8 != (iconv_t)-1) + return true; + + if ((ctx->utf8_to_16 = iconv_open("UTF-16LE", "UTF-8")) == (iconv_t)-1 || + (ctx->utf16_to_8 = iconv_open("UTF-8", "UTF-16LE")) == (iconv_t)-1) { + if (errno == EINVAL) + ntlm_client_set_errmsg(ctx->ntlm, + "iconv does not support UTF8 <-> UTF16 conversion"); + else + ntlm_client_set_errmsg(ctx->ntlm, strerror(errno)); + + return false; + } + + return true; +} + +static inline bool unicode_iconv_encoding_convert( + char **converted, + size_t *converted_len, + ntlm_unicode_ctx *ctx, + const char *string, + size_t string_len, + unicode_iconv_encoding_direction direction) +{ + char *in_start, *out_start, *out, *new_out; + size_t in_start_len, out_start_len, out_size, nul_size, ret, written = 0; + iconv_t converter; + + *converted = NULL; + *converted_len = 0; + + if (!unicode_iconv_init(ctx)) + return false; + + /* + * When translating UTF8 to UTF16, these strings are only used + * internally, and we obey the given length, so we can simply + * use a buffer that is 2x the size. When translating from UTF16 + * to UTF8, we may need to return to callers, so we need to NUL + * terminate and expect an extra byte for UTF8, two for UTF16. + */ + if (direction == unicode_iconv_utf8_to_16) { + converter = ctx->utf8_to_16; + out_size = (string_len * 2) + 2; + nul_size = 2; + } else { + converter = ctx->utf16_to_8; + out_size = (string_len / 2) + 1; + nul_size = 1; + } + + /* Round to the nearest multiple of 8 */ + out_size = (out_size + 7) & ~7; + + if ((out = malloc(out_size)) == NULL) { + ntlm_client_set_errmsg(ctx->ntlm, "out of memory"); + return false; + } + + in_start = (char *)string; + in_start_len = string_len; + + while (true) { + out_start = out + written; + out_start_len = (out_size - nul_size) - written; + + ret = iconv(converter, &in_start, &in_start_len, &out_start, &out_start_len); + written = (out_size - nul_size) - out_start_len; + + if (ret == 0) + break; + + if (ret == (size_t)-1 && errno != E2BIG) { + ntlm_client_set_errmsg(ctx->ntlm, strerror(errno)); + goto on_error; + } + + /* Grow buffer size by 1.5 (rounded up to a multiple of 8) */ + out_size = ((((out_size << 1) - (out_size >> 1)) + 7) & ~7); + + if (out_size > NTLM_UNICODE_MAX_LEN) { + ntlm_client_set_errmsg(ctx->ntlm, + "unicode conversion too large"); + goto on_error; + } + + if ((new_out = realloc(out, out_size)) == NULL) { + ntlm_client_set_errmsg(ctx->ntlm, "out of memory"); + goto on_error; + } + + out = new_out; + } + + if (in_start_len != 0) { + ntlm_client_set_errmsg(ctx->ntlm, + "invalid unicode string; trailing data remains"); + goto on_error; + } + + /* NUL terminate */ + out[written] = '\0'; + + if (direction == unicode_iconv_utf8_to_16) + out[written + 1] = '\0'; + + *converted = out; + + if (converted_len) + *converted_len = written; + + return true; + +on_error: + free(out); + return false; +} + +bool ntlm_unicode_utf8_to_16( + char **converted, + size_t *converted_len, + ntlm_unicode_ctx *ctx, + const char *string, + size_t string_len) +{ + return unicode_iconv_encoding_convert( + converted, converted_len, ctx, string, string_len, + unicode_iconv_utf8_to_16); +} + +bool ntlm_unicode_utf16_to_8( + char **converted, + size_t *converted_len, + ntlm_unicode_ctx *ctx, + const char *string, + size_t string_len) +{ + return unicode_iconv_encoding_convert( + converted, converted_len, ctx, string, string_len, + unicode_iconv_utf16_to_8); +} + +void ntlm_unicode_ctx_free(ntlm_unicode_ctx *ctx) +{ + if (!ctx) + return; + + if (ctx->utf16_to_8 != (iconv_t)-1) + iconv_close(ctx->utf16_to_8); + + if (ctx->utf8_to_16 != (iconv_t)-1) + iconv_close(ctx->utf8_to_16); + + free(ctx); +} diff --git a/deps/ntlmclient/utf8.h b/deps/ntlmclient/utf8.h new file mode 100644 index 000000000..a26ae85c3 --- /dev/null +++ b/deps/ntlmclient/utf8.h @@ -0,0 +1,1257 @@ +// The latest version of this library is available on GitHub; +// https://github.com/sheredom/utf8.h + +// This is free and unencumbered software released into the public domain. +// +// Anyone is free to copy, modify, publish, use, compile, sell, or +// distribute this software, either in source code form or as a compiled +// binary, for any purpose, commercial or non-commercial, and by any +// means. +// +// In jurisdictions that recognize copyright laws, the author or authors +// of this software dedicate any and all copyright interest in the +// software to the public domain. We make this dedication for the benefit +// of the public at large and to the detriment of our heirs and +// successors. We intend this dedication to be an overt act of +// relinquishment in perpetuity of all present and future rights to this +// software under copyright law. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// For more information, please refer to <http://unlicense.org/> + +#ifndef SHEREDOM_UTF8_H_INCLUDED +#define SHEREDOM_UTF8_H_INCLUDED + +#if defined(_MSC_VER) +#pragma warning(push) + +// disable 'bytes padding added after construct' warning +#pragma warning(disable : 4820) +#endif + +#include <stddef.h> +#include <stdlib.h> + +#if defined(_MSC_VER) +#pragma warning(pop) +#endif + +#if defined(_MSC_VER) +typedef __int32 utf8_int32_t; +#else +#include <stdint.h> +typedef int32_t utf8_int32_t; +#endif + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wold-style-cast" +#pragma clang diagnostic ignored "-Wcast-qual" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(__clang__) || defined(__GNUC__) +#define utf8_nonnull __attribute__((nonnull)) +#define utf8_pure __attribute__((pure)) +#define utf8_restrict __restrict__ +#define utf8_weak __attribute__((weak)) +#elif defined(_MSC_VER) +#define utf8_nonnull +#define utf8_pure +#define utf8_restrict __restrict +#define utf8_weak __inline +#else +#error Non clang, non gcc, non MSVC compiler found! +#endif + +#ifdef __cplusplus +#define utf8_null NULL +#else +#define utf8_null 0 +#endif + +// Return less than 0, 0, greater than 0 if src1 < src2, src1 == src2, src1 > +// src2 respectively, case insensitive. +utf8_nonnull utf8_pure utf8_weak int utf8casecmp(const void *src1, + const void *src2); + +// Append the utf8 string src onto the utf8 string dst. +utf8_nonnull utf8_weak void *utf8cat(void *utf8_restrict dst, + const void *utf8_restrict src); + +// Find the first match of the utf8 codepoint chr in the utf8 string src. +utf8_nonnull utf8_pure utf8_weak void *utf8chr(const void *src, + utf8_int32_t chr); + +// Return less than 0, 0, greater than 0 if src1 < src2, +// src1 == src2, src1 > src2 respectively. +utf8_nonnull utf8_pure utf8_weak int utf8cmp(const void *src1, + const void *src2); + +// Copy the utf8 string src onto the memory allocated in dst. +utf8_nonnull utf8_weak void *utf8cpy(void *utf8_restrict dst, + const void *utf8_restrict src); + +// Number of utf8 codepoints in the utf8 string src that consists entirely +// of utf8 codepoints not from the utf8 string reject. +utf8_nonnull utf8_pure utf8_weak size_t utf8cspn(const void *src, + const void *reject); + +// Duplicate the utf8 string src by getting its size, malloc'ing a new buffer +// copying over the data, and returning that. Or 0 if malloc failed. +utf8_nonnull utf8_weak void *utf8dup(const void *src); + +// Number of utf8 codepoints in the utf8 string str, +// excluding the null terminating byte. +utf8_nonnull utf8_pure utf8_weak size_t utf8len(const void *str); + +// Return less than 0, 0, greater than 0 if src1 < src2, src1 == src2, src1 > +// src2 respectively, case insensitive. Checking at most n bytes of each utf8 +// string. +utf8_nonnull utf8_pure utf8_weak int utf8ncasecmp(const void *src1, + const void *src2, size_t n); + +// Append the utf8 string src onto the utf8 string dst, +// writing at most n+1 bytes. Can produce an invalid utf8 +// string if n falls partway through a utf8 codepoint. +utf8_nonnull utf8_weak void *utf8ncat(void *utf8_restrict dst, + const void *utf8_restrict src, size_t n); + +// Return less than 0, 0, greater than 0 if src1 < src2, +// src1 == src2, src1 > src2 respectively. Checking at most n +// bytes of each utf8 string. +utf8_nonnull utf8_pure utf8_weak int utf8ncmp(const void *src1, + const void *src2, size_t n); + +// Copy the utf8 string src onto the memory allocated in dst. +// Copies at most n bytes. If there is no terminating null byte in +// the first n bytes of src, the string placed into dst will not be +// null-terminated. If the size (in bytes) of src is less than n, +// extra null terminating bytes are appended to dst such that at +// total of n bytes are written. Can produce an invalid utf8 +// string if n falls partway through a utf8 codepoint. +utf8_nonnull utf8_weak void *utf8ncpy(void *utf8_restrict dst, + const void *utf8_restrict src, size_t n); + +// Similar to utf8dup, except that at most n bytes of src are copied. If src is +// longer than n, only n bytes are copied and a null byte is added. +// +// Returns a new string if successful, 0 otherwise +utf8_nonnull utf8_weak void *utf8ndup(const void *src, size_t n); + +// Locates the first occurence in the utf8 string str of any byte in the +// utf8 string accept, or 0 if no match was found. +utf8_nonnull utf8_pure utf8_weak void *utf8pbrk(const void *str, + const void *accept); + +// Find the last match of the utf8 codepoint chr in the utf8 string src. +utf8_nonnull utf8_pure utf8_weak void *utf8rchr(const void *src, int chr); + +// Number of bytes in the utf8 string str, +// including the null terminating byte. +utf8_nonnull utf8_pure utf8_weak size_t utf8size(const void *str); + +// Number of utf8 codepoints in the utf8 string src that consists entirely +// of utf8 codepoints from the utf8 string accept. +utf8_nonnull utf8_pure utf8_weak size_t utf8spn(const void *src, + const void *accept); + +// The position of the utf8 string needle in the utf8 string haystack. +utf8_nonnull utf8_pure utf8_weak void *utf8str(const void *haystack, + const void *needle); + +// The position of the utf8 string needle in the utf8 string haystack, case +// insensitive. +utf8_nonnull utf8_pure utf8_weak void *utf8casestr(const void *haystack, + const void *needle); + +// Return 0 on success, or the position of the invalid +// utf8 codepoint on failure. +utf8_nonnull utf8_pure utf8_weak void *utf8valid(const void *str); + +// Sets out_codepoint to the next utf8 codepoint in str, and returns the address +// of the utf8 codepoint after the current one in str. +utf8_nonnull utf8_weak void * +utf8codepoint(const void *utf8_restrict str, + utf8_int32_t *utf8_restrict out_codepoint); + +// Returns the size of the given codepoint in bytes. +utf8_weak size_t utf8codepointsize(utf8_int32_t chr); + +// Write a codepoint to the given string, and return the address to the next +// place after the written codepoint. Pass how many bytes left in the buffer to +// n. If there is not enough space for the codepoint, this function returns +// null. +utf8_nonnull utf8_weak void *utf8catcodepoint(void *utf8_restrict str, + utf8_int32_t chr, size_t n); + +// Returns 1 if the given character is lowercase, or 0 if it is not. +utf8_weak int utf8islower(utf8_int32_t chr); + +// Returns 1 if the given character is uppercase, or 0 if it is not. +utf8_weak int utf8isupper(utf8_int32_t chr); + +// Transform the given string into all lowercase codepoints. +utf8_nonnull utf8_weak void utf8lwr(void *utf8_restrict str); + +// Transform the given string into all uppercase codepoints. +utf8_nonnull utf8_weak void utf8upr(void *utf8_restrict str); + +// Make a codepoint lower case if possible. +utf8_weak utf8_int32_t utf8lwrcodepoint(utf8_int32_t cp); + +// Make a codepoint upper case if possible. +utf8_weak utf8_int32_t utf8uprcodepoint(utf8_int32_t cp); + +#undef utf8_weak +#undef utf8_pure +#undef utf8_nonnull + +int utf8casecmp(const void *src1, const void *src2) { + utf8_int32_t src1_cp, src2_cp, src1_orig_cp, src2_orig_cp; + + for (;;) { + src1 = utf8codepoint(src1, &src1_cp); + src2 = utf8codepoint(src2, &src2_cp); + + // Take a copy of src1 & src2 + src1_orig_cp = src1_cp; + src2_orig_cp = src2_cp; + + // Lower the srcs if required + src1_cp = utf8lwrcodepoint(src1_cp); + src2_cp = utf8lwrcodepoint(src2_cp); + + // Check if the lowered codepoints match + if ((0 == src1_orig_cp) && (0 == src2_orig_cp)) { + return 0; + } else if (src1_cp == src2_cp) { + continue; + } + + // If they don't match, then we return which of the original's are less + if (src1_orig_cp < src2_orig_cp) { + return -1; + } else if (src1_orig_cp > src2_orig_cp) { + return 1; + } + } +} + +void *utf8cat(void *utf8_restrict dst, const void *utf8_restrict src) { + char *d = (char *)dst; + const char *s = (const char *)src; + + // find the null terminating byte in dst + while ('\0' != *d) { + d++; + } + + // overwriting the null terminating byte in dst, append src byte-by-byte + while ('\0' != *s) { + *d++ = *s++; + } + + // write out a new null terminating byte into dst + *d = '\0'; + + return dst; +} + +void *utf8chr(const void *src, utf8_int32_t chr) { + char c[5] = {'\0', '\0', '\0', '\0', '\0'}; + + if (0 == chr) { + // being asked to return position of null terminating byte, so + // just run s to the end, and return! + const char *s = (const char *)src; + while ('\0' != *s) { + s++; + } + return (void *)s; + } else if (0 == ((utf8_int32_t)0xffffff80 & chr)) { + // 1-byte/7-bit ascii + // (0b0xxxxxxx) + c[0] = (char)chr; + } else if (0 == ((utf8_int32_t)0xfffff800 & chr)) { + // 2-byte/11-bit utf8 code point + // (0b110xxxxx 0b10xxxxxx) + c[0] = 0xc0 | (char)(chr >> 6); + c[1] = 0x80 | (char)(chr & 0x3f); + } else if (0 == ((utf8_int32_t)0xffff0000 & chr)) { + // 3-byte/16-bit utf8 code point + // (0b1110xxxx 0b10xxxxxx 0b10xxxxxx) + c[0] = 0xe0 | (char)(chr >> 12); + c[1] = 0x80 | (char)((chr >> 6) & 0x3f); + c[2] = 0x80 | (char)(chr & 0x3f); + } else { // if (0 == ((int)0xffe00000 & chr)) { + // 4-byte/21-bit utf8 code point + // (0b11110xxx 0b10xxxxxx 0b10xxxxxx 0b10xxxxxx) + c[0] = 0xf0 | (char)(chr >> 18); + c[1] = 0x80 | (char)((chr >> 12) & 0x3f); + c[2] = 0x80 | (char)((chr >> 6) & 0x3f); + c[3] = 0x80 | (char)(chr & 0x3f); + } + + // we've made c into a 2 utf8 codepoint string, one for the chr we are + // seeking, another for the null terminating byte. Now use utf8str to + // search + return utf8str(src, c); +} + +int utf8cmp(const void *src1, const void *src2) { + const unsigned char *s1 = (const unsigned char *)src1; + const unsigned char *s2 = (const unsigned char *)src2; + + while (('\0' != *s1) || ('\0' != *s2)) { + if (*s1 < *s2) { + return -1; + } else if (*s1 > *s2) { + return 1; + } + + s1++; + s2++; + } + + // both utf8 strings matched + return 0; +} + +int utf8coll(const void *src1, const void *src2); + +void *utf8cpy(void *utf8_restrict dst, const void *utf8_restrict src) { + char *d = (char *)dst; + const char *s = (const char *)src; + + // overwriting anything previously in dst, write byte-by-byte + // from src + while ('\0' != *s) { + *d++ = *s++; + } + + // append null terminating byte + *d = '\0'; + + return dst; +} + +size_t utf8cspn(const void *src, const void *reject) { + const char *s = (const char *)src; + size_t chars = 0; + + while ('\0' != *s) { + const char *r = (const char *)reject; + size_t offset = 0; + + while ('\0' != *r) { + // checking that if *r is the start of a utf8 codepoint + // (it is not 0b10xxxxxx) and we have successfully matched + // a previous character (0 < offset) - we found a match + if ((0x80 != (0xc0 & *r)) && (0 < offset)) { + return chars; + } else { + if (*r == s[offset]) { + // part of a utf8 codepoint matched, so move our checking + // onwards to the next byte + offset++; + r++; + } else { + // r could be in the middle of an unmatching utf8 code point, + // so we need to march it on to the next character beginning, + + do { + r++; + } while (0x80 == (0xc0 & *r)); + + // reset offset too as we found a mismatch + offset = 0; + } + } + } + + // the current utf8 codepoint in src did not match reject, but src + // could have been partway through a utf8 codepoint, so we need to + // march it onto the next utf8 codepoint starting byte + do { + s++; + } while ((0x80 == (0xc0 & *s))); + chars++; + } + + return chars; +} + +size_t utf8size(const void *str); + +void *utf8dup(const void *src) { + const char *s = (const char *)src; + char *n = utf8_null; + + // figure out how many bytes (including the terminator) we need to copy first + size_t bytes = utf8size(src); + + n = (char *)malloc(bytes); + + if (utf8_null == n) { + // out of memory so we bail + return utf8_null; + } else { + bytes = 0; + + // copy src byte-by-byte into our new utf8 string + while ('\0' != s[bytes]) { + n[bytes] = s[bytes]; + bytes++; + } + + // append null terminating byte + n[bytes] = '\0'; + return n; + } +} + +void *utf8fry(const void *str); + +size_t utf8len(const void *str) { + const unsigned char *s = (const unsigned char *)str; + size_t length = 0; + + while ('\0' != *s) { + if (0xf0 == (0xf8 & *s)) { + // 4-byte utf8 code point (began with 0b11110xxx) + s += 4; + } else if (0xe0 == (0xf0 & *s)) { + // 3-byte utf8 code point (began with 0b1110xxxx) + s += 3; + } else if (0xc0 == (0xe0 & *s)) { + // 2-byte utf8 code point (began with 0b110xxxxx) + s += 2; + } else { // if (0x00 == (0x80 & *s)) { + // 1-byte ascii (began with 0b0xxxxxxx) + s += 1; + } + + // no matter the bytes we marched s forward by, it was + // only 1 utf8 codepoint + length++; + } + + return length; +} + +int utf8ncasecmp(const void *src1, const void *src2, size_t n) { + utf8_int32_t src1_cp, src2_cp, src1_orig_cp, src2_orig_cp; + + do { + const unsigned char *const s1 = (const unsigned char *)src1; + const unsigned char *const s2 = (const unsigned char *)src2; + + // first check that we have enough bytes left in n to contain an entire + // codepoint + if (0 == n) { + return 0; + } + + if ((1 == n) && ((0xc0 == (0xe0 & *s1)) || (0xc0 == (0xe0 & *s2)))) { + const utf8_int32_t c1 = (0xe0 & *s1); + const utf8_int32_t c2 = (0xe0 & *s2); + + if (c1 < c2) { + return -1; + } else if (c1 > c2) { + return 1; + } else { + return 0; + } + } + + if ((2 >= n) && ((0xe0 == (0xf0 & *s1)) || (0xe0 == (0xf0 & *s2)))) { + const utf8_int32_t c1 = (0xf0 & *s1); + const utf8_int32_t c2 = (0xf0 & *s2); + + if (c1 < c2) { + return -1; + } else if (c1 > c2) { + return 1; + } else { + return 0; + } + } + + if ((3 >= n) && ((0xf0 == (0xf8 & *s1)) || (0xf0 == (0xf8 & *s2)))) { + const utf8_int32_t c1 = (0xf8 & *s1); + const utf8_int32_t c2 = (0xf8 & *s2); + + if (c1 < c2) { + return -1; + } else if (c1 > c2) { + return 1; + } else { + return 0; + } + } + + src1 = utf8codepoint(src1, &src1_cp); + src2 = utf8codepoint(src2, &src2_cp); + n -= utf8codepointsize(src1_cp); + + // Take a copy of src1 & src2 + src1_orig_cp = src1_cp; + src2_orig_cp = src2_cp; + + // Lower srcs if required + src1_cp = utf8lwrcodepoint(src1_cp); + src2_cp = utf8lwrcodepoint(src2_cp); + + // Check if the lowered codepoints match + if ((0 == src1_orig_cp) && (0 == src2_orig_cp)) { + return 0; + } else if (src1_cp == src2_cp) { + continue; + } + + // If they don't match, then we return which of the original's are less + if (src1_orig_cp < src2_orig_cp) { + return -1; + } else if (src1_orig_cp > src2_orig_cp) { + return 1; + } + } while (0 < n); + + // both utf8 strings matched + return 0; +} + +void *utf8ncat(void *utf8_restrict dst, const void *utf8_restrict src, + size_t n) { + char *d = (char *)dst; + const char *s = (const char *)src; + + // find the null terminating byte in dst + while ('\0' != *d) { + d++; + } + + // overwriting the null terminating byte in dst, append src byte-by-byte + // stopping if we run out of space + do { + *d++ = *s++; + } while (('\0' != *s) && (0 != --n)); + + // write out a new null terminating byte into dst + *d = '\0'; + + return dst; +} + +int utf8ncmp(const void *src1, const void *src2, size_t n) { + const unsigned char *s1 = (const unsigned char *)src1; + const unsigned char *s2 = (const unsigned char *)src2; + + while ((('\0' != *s1) || ('\0' != *s2)) && (0 != n--)) { + if (*s1 < *s2) { + return -1; + } else if (*s1 > *s2) { + return 1; + } + + s1++; + s2++; + } + + // both utf8 strings matched + return 0; +} + +void *utf8ncpy(void *utf8_restrict dst, const void *utf8_restrict src, + size_t n) { + char *d = (char *)dst; + const char *s = (const char *)src; + + // overwriting anything previously in dst, write byte-by-byte + // from src + do { + *d++ = *s++; + } while (('\0' != *s) && (0 != --n)); + + // append null terminating byte + while (0 != n) { + *d++ = '\0'; + n--; + } + + return dst; +} + +void *utf8ndup(const void *src, size_t n) { + const char *s = (const char *)src; + char *c = utf8_null; + size_t bytes = 0; + + // Find the end of the string or stop when n is reached + while ('\0' != s[bytes] && bytes < n) { + bytes++; + } + + // In case bytes is actually less than n, we need to set it + // to be used later in the copy byte by byte. + n = bytes; + + c = (char *)malloc(bytes + 1); + if (utf8_null == c) { + // out of memory so we bail + return utf8_null; + } + + bytes = 0; + + // copy src byte-by-byte into our new utf8 string + while ('\0' != s[bytes] && bytes < n) { + c[bytes] = s[bytes]; + bytes++; + } + + // append null terminating byte + c[bytes] = '\0'; + return c; +} + +void *utf8rchr(const void *src, int chr) { + const char *s = (const char *)src; + const char *match = utf8_null; + char c[5] = {'\0', '\0', '\0', '\0', '\0'}; + + if (0 == chr) { + // being asked to return position of null terminating byte, so + // just run s to the end, and return! + while ('\0' != *s) { + s++; + } + return (void *)s; + } else if (0 == ((int)0xffffff80 & chr)) { + // 1-byte/7-bit ascii + // (0b0xxxxxxx) + c[0] = (char)chr; + } else if (0 == ((int)0xfffff800 & chr)) { + // 2-byte/11-bit utf8 code point + // (0b110xxxxx 0b10xxxxxx) + c[0] = 0xc0 | (char)(chr >> 6); + c[1] = 0x80 | (char)(chr & 0x3f); + } else if (0 == ((int)0xffff0000 & chr)) { + // 3-byte/16-bit utf8 code point + // (0b1110xxxx 0b10xxxxxx 0b10xxxxxx) + c[0] = 0xe0 | (char)(chr >> 12); + c[1] = 0x80 | (char)((chr >> 6) & 0x3f); + c[2] = 0x80 | (char)(chr & 0x3f); + } else { // if (0 == ((int)0xffe00000 & chr)) { + // 4-byte/21-bit utf8 code point + // (0b11110xxx 0b10xxxxxx 0b10xxxxxx 0b10xxxxxx) + c[0] = 0xf0 | (char)(chr >> 18); + c[1] = 0x80 | (char)((chr >> 12) & 0x3f); + c[2] = 0x80 | (char)((chr >> 6) & 0x3f); + c[3] = 0x80 | (char)(chr & 0x3f); + } + + // we've created a 2 utf8 codepoint string in c that is + // the utf8 character asked for by chr, and a null + // terminating byte + + while ('\0' != *s) { + size_t offset = 0; + + while (s[offset] == c[offset]) { + offset++; + } + + if ('\0' == c[offset]) { + // we found a matching utf8 code point + match = s; + s += offset; + } else { + s += offset; + + // need to march s along to next utf8 codepoint start + // (the next byte that doesn't match 0b10xxxxxx) + if ('\0' != *s) { + do { + s++; + } while (0x80 == (0xc0 & *s)); + } + } + } + + // return the last match we found (or 0 if no match was found) + return (void *)match; +} + +void *utf8pbrk(const void *str, const void *accept) { + const char *s = (const char *)str; + + while ('\0' != *s) { + const char *a = (const char *)accept; + size_t offset = 0; + + while ('\0' != *a) { + // checking that if *a is the start of a utf8 codepoint + // (it is not 0b10xxxxxx) and we have successfully matched + // a previous character (0 < offset) - we found a match + if ((0x80 != (0xc0 & *a)) && (0 < offset)) { + return (void *)s; + } else { + if (*a == s[offset]) { + // part of a utf8 codepoint matched, so move our checking + // onwards to the next byte + offset++; + a++; + } else { + // r could be in the middle of an unmatching utf8 code point, + // so we need to march it on to the next character beginning, + + do { + a++; + } while (0x80 == (0xc0 & *a)); + + // reset offset too as we found a mismatch + offset = 0; + } + } + } + + // we found a match on the last utf8 codepoint + if (0 < offset) { + return (void *)s; + } + + // the current utf8 codepoint in src did not match accept, but src + // could have been partway through a utf8 codepoint, so we need to + // march it onto the next utf8 codepoint starting byte + do { + s++; + } while ((0x80 == (0xc0 & *s))); + } + + return utf8_null; +} + +size_t utf8size(const void *str) { + const char *s = (const char *)str; + size_t size = 0; + while ('\0' != s[size]) { + size++; + } + + // we are including the null terminating byte in the size calculation + size++; + return size; +} + +size_t utf8spn(const void *src, const void *accept) { + const char *s = (const char *)src; + size_t chars = 0; + + while ('\0' != *s) { + const char *a = (const char *)accept; + size_t offset = 0; + + while ('\0' != *a) { + // checking that if *r is the start of a utf8 codepoint + // (it is not 0b10xxxxxx) and we have successfully matched + // a previous character (0 < offset) - we found a match + if ((0x80 != (0xc0 & *a)) && (0 < offset)) { + // found a match, so increment the number of utf8 codepoints + // that have matched and stop checking whether any other utf8 + // codepoints in a match + chars++; + s += offset; + break; + } else { + if (*a == s[offset]) { + offset++; + a++; + } else { + // a could be in the middle of an unmatching utf8 codepoint, + // so we need to march it on to the next character beginning, + do { + a++; + } while (0x80 == (0xc0 & *a)); + + // reset offset too as we found a mismatch + offset = 0; + } + } + } + + // if a got to its terminating null byte, then we didn't find a match. + // Return the current number of matched utf8 codepoints + if ('\0' == *a) { + return chars; + } + } + + return chars; +} + +void *utf8str(const void *haystack, const void *needle) { + const char *h = (const char *)haystack; + + // if needle has no utf8 codepoints before the null terminating + // byte then return haystack + if ('\0' == *((const char *)needle)) { + return (void *)haystack; + } + + while ('\0' != *h) { + const char *maybeMatch = h; + const char *n = (const char *)needle; + + while (*h == *n && (*h != '\0' && *n != '\0')) { + n++; + h++; + } + + if ('\0' == *n) { + // we found the whole utf8 string for needle in haystack at + // maybeMatch, so return it + return (void *)maybeMatch; + } else { + // h could be in the middle of an unmatching utf8 codepoint, + // so we need to march it on to the next character beginning, + if ('\0' != *h) { + do { + h++; + } while (0x80 == (0xc0 & *h)); + } + } + } + + // no match + return utf8_null; +} + +void *utf8casestr(const void *haystack, const void *needle) { + const void *h = haystack; + + // if needle has no utf8 codepoints before the null terminating + // byte then return haystack + if ('\0' == *((const char *)needle)) { + return (void *)haystack; + } + + for (;;) { + const void *maybeMatch = h; + const void *n = needle; + utf8_int32_t h_cp, n_cp; + + h = utf8codepoint(h, &h_cp); + n = utf8codepoint(n, &n_cp); + + while ((0 != h_cp) && (0 != n_cp)) { + h_cp = utf8lwrcodepoint(h_cp); + n_cp = utf8lwrcodepoint(n_cp); + + // if we find a mismatch, bail out! + if (h_cp != n_cp) { + break; + } + + h = utf8codepoint(h, &h_cp); + n = utf8codepoint(n, &n_cp); + } + + if (0 == n_cp) { + // we found the whole utf8 string for needle in haystack at + // maybeMatch, so return it + return (void *)maybeMatch; + } + + if (0 == h_cp) { + // no match + return utf8_null; + } + } +} + +void *utf8valid(const void *str) { + const char *s = (const char *)str; + + while ('\0' != *s) { + if (0xf0 == (0xf8 & *s)) { + // ensure each of the 3 following bytes in this 4-byte + // utf8 codepoint began with 0b10xxxxxx + if ((0x80 != (0xc0 & s[1])) || (0x80 != (0xc0 & s[2])) || + (0x80 != (0xc0 & s[3]))) { + return (void *)s; + } + + // ensure that our utf8 codepoint ended after 4 bytes + if (0x80 == (0xc0 & s[4])) { + return (void *)s; + } + + // ensure that the top 5 bits of this 4-byte utf8 + // codepoint were not 0, as then we could have used + // one of the smaller encodings + if ((0 == (0x07 & s[0])) && (0 == (0x30 & s[1]))) { + return (void *)s; + } + + // 4-byte utf8 code point (began with 0b11110xxx) + s += 4; + } else if (0xe0 == (0xf0 & *s)) { + // ensure each of the 2 following bytes in this 3-byte + // utf8 codepoint began with 0b10xxxxxx + if ((0x80 != (0xc0 & s[1])) || (0x80 != (0xc0 & s[2]))) { + return (void *)s; + } + + // ensure that our utf8 codepoint ended after 3 bytes + if (0x80 == (0xc0 & s[3])) { + return (void *)s; + } + + // ensure that the top 5 bits of this 3-byte utf8 + // codepoint were not 0, as then we could have used + // one of the smaller encodings + if ((0 == (0x0f & s[0])) && (0 == (0x20 & s[1]))) { + return (void *)s; + } + + // 3-byte utf8 code point (began with 0b1110xxxx) + s += 3; + } else if (0xc0 == (0xe0 & *s)) { + // ensure the 1 following byte in this 2-byte + // utf8 codepoint began with 0b10xxxxxx + if (0x80 != (0xc0 & s[1])) { + return (void *)s; + } + + // ensure that our utf8 codepoint ended after 2 bytes + if (0x80 == (0xc0 & s[2])) { + return (void *)s; + } + + // ensure that the top 4 bits of this 2-byte utf8 + // codepoint were not 0, as then we could have used + // one of the smaller encodings + if (0 == (0x1e & s[0])) { + return (void *)s; + } + + // 2-byte utf8 code point (began with 0b110xxxxx) + s += 2; + } else if (0x00 == (0x80 & *s)) { + // 1-byte ascii (began with 0b0xxxxxxx) + s += 1; + } else { + // we have an invalid 0b1xxxxxxx utf8 code point entry + return (void *)s; + } + } + + return utf8_null; +} + +void *utf8codepoint(const void *utf8_restrict str, + utf8_int32_t *utf8_restrict out_codepoint) { + const char *s = (const char *)str; + + if (0xf0 == (0xf8 & s[0])) { + // 4 byte utf8 codepoint + *out_codepoint = ((0x07 & s[0]) << 18) | ((0x3f & s[1]) << 12) | + ((0x3f & s[2]) << 6) | (0x3f & s[3]); + s += 4; + } else if (0xe0 == (0xf0 & s[0])) { + // 3 byte utf8 codepoint + *out_codepoint = + ((0x0f & s[0]) << 12) | ((0x3f & s[1]) << 6) | (0x3f & s[2]); + s += 3; + } else if (0xc0 == (0xe0 & s[0])) { + // 2 byte utf8 codepoint + *out_codepoint = ((0x1f & s[0]) << 6) | (0x3f & s[1]); + s += 2; + } else { + // 1 byte utf8 codepoint otherwise + *out_codepoint = s[0]; + s += 1; + } + + return (void *)s; +} + +size_t utf8codepointsize(utf8_int32_t chr) { + if (0 == ((utf8_int32_t)0xffffff80 & chr)) { + return 1; + } else if (0 == ((utf8_int32_t)0xfffff800 & chr)) { + return 2; + } else if (0 == ((utf8_int32_t)0xffff0000 & chr)) { + return 3; + } else { // if (0 == ((int)0xffe00000 & chr)) { + return 4; + } +} + +void *utf8catcodepoint(void *utf8_restrict str, utf8_int32_t chr, size_t n) { + char *s = (char *)str; + + if (0 == ((utf8_int32_t)0xffffff80 & chr)) { + // 1-byte/7-bit ascii + // (0b0xxxxxxx) + if (n < 1) { + return utf8_null; + } + s[0] = (char)chr; + s += 1; + } else if (0 == ((utf8_int32_t)0xfffff800 & chr)) { + // 2-byte/11-bit utf8 code point + // (0b110xxxxx 0b10xxxxxx) + if (n < 2) { + return utf8_null; + } + s[0] = 0xc0 | (char)(chr >> 6); + s[1] = 0x80 | (char)(chr & 0x3f); + s += 2; + } else if (0 == ((utf8_int32_t)0xffff0000 & chr)) { + // 3-byte/16-bit utf8 code point + // (0b1110xxxx 0b10xxxxxx 0b10xxxxxx) + if (n < 3) { + return utf8_null; + } + s[0] = 0xe0 | (char)(chr >> 12); + s[1] = 0x80 | (char)((chr >> 6) & 0x3f); + s[2] = 0x80 | (char)(chr & 0x3f); + s += 3; + } else { // if (0 == ((int)0xffe00000 & chr)) { + // 4-byte/21-bit utf8 code point + // (0b11110xxx 0b10xxxxxx 0b10xxxxxx 0b10xxxxxx) + if (n < 4) { + return utf8_null; + } + s[0] = 0xf0 | (char)(chr >> 18); + s[1] = 0x80 | (char)((chr >> 12) & 0x3f); + s[2] = 0x80 | (char)((chr >> 6) & 0x3f); + s[3] = 0x80 | (char)(chr & 0x3f); + s += 4; + } + + return s; +} + +int utf8islower(utf8_int32_t chr) { return chr != utf8uprcodepoint(chr); } + +int utf8isupper(utf8_int32_t chr) { return chr != utf8lwrcodepoint(chr); } + +void utf8lwr(void *utf8_restrict str) { + void *p, *pn; + utf8_int32_t cp; + + p = (char *)str; + pn = utf8codepoint(p, &cp); + + while (cp != 0) { + const utf8_int32_t lwr_cp = utf8lwrcodepoint(cp); + const size_t size = utf8codepointsize(lwr_cp); + + if (lwr_cp != cp) { + utf8catcodepoint(p, lwr_cp, size); + } + + p = pn; + pn = utf8codepoint(p, &cp); + } +} + +void utf8upr(void *utf8_restrict str) { + void *p, *pn; + utf8_int32_t cp; + + p = (char *)str; + pn = utf8codepoint(p, &cp); + + while (cp != 0) { + const utf8_int32_t lwr_cp = utf8uprcodepoint(cp); + const size_t size = utf8codepointsize(lwr_cp); + + if (lwr_cp != cp) { + utf8catcodepoint(p, lwr_cp, size); + } + + p = pn; + pn = utf8codepoint(p, &cp); + } +} + +utf8_int32_t utf8lwrcodepoint(utf8_int32_t cp) { + if (((0x0041 <= cp) && (0x005a >= cp)) || + ((0x00c0 <= cp) && (0x00d6 >= cp)) || + ((0x00d8 <= cp) && (0x00de >= cp)) || + ((0x0391 <= cp) && (0x03a1 >= cp)) || + ((0x03a3 <= cp) && (0x03ab >= cp))) { + cp += 32; + } else if (((0x0100 <= cp) && (0x012f >= cp)) || + ((0x0132 <= cp) && (0x0137 >= cp)) || + ((0x014a <= cp) && (0x0177 >= cp)) || + ((0x0182 <= cp) && (0x0185 >= cp)) || + ((0x01a0 <= cp) && (0x01a5 >= cp)) || + ((0x01de <= cp) && (0x01ef >= cp)) || + ((0x01f8 <= cp) && (0x021f >= cp)) || + ((0x0222 <= cp) && (0x0233 >= cp)) || + ((0x0246 <= cp) && (0x024f >= cp)) || + ((0x03d8 <= cp) && (0x03ef >= cp))) { + cp |= 0x1; + } else if (((0x0139 <= cp) && (0x0148 >= cp)) || + ((0x0179 <= cp) && (0x017e >= cp)) || + ((0x01af <= cp) && (0x01b0 >= cp)) || + ((0x01b3 <= cp) && (0x01b6 >= cp)) || + ((0x01cd <= cp) && (0x01dc >= cp))) { + cp += 1; + cp &= ~0x1; + } else { + switch (cp) { + default: break; + case 0x0178: cp = 0x00ff; break; + case 0x0243: cp = 0x0180; break; + case 0x018e: cp = 0x01dd; break; + case 0x023d: cp = 0x019a; break; + case 0x0220: cp = 0x019e; break; + case 0x01b7: cp = 0x0292; break; + case 0x01c4: cp = 0x01c6; break; + case 0x01c7: cp = 0x01c9; break; + case 0x01ca: cp = 0x01cc; break; + case 0x01f1: cp = 0x01f3; break; + case 0x01f7: cp = 0x01bf; break; + case 0x0187: cp = 0x0188; break; + case 0x018b: cp = 0x018c; break; + case 0x0191: cp = 0x0192; break; + case 0x0198: cp = 0x0199; break; + case 0x01a7: cp = 0x01a8; break; + case 0x01ac: cp = 0x01ad; break; + case 0x01af: cp = 0x01b0; break; + case 0x01b8: cp = 0x01b9; break; + case 0x01bc: cp = 0x01bd; break; + case 0x01f4: cp = 0x01f5; break; + case 0x023b: cp = 0x023c; break; + case 0x0241: cp = 0x0242; break; + case 0x03fd: cp = 0x037b; break; + case 0x03fe: cp = 0x037c; break; + case 0x03ff: cp = 0x037d; break; + case 0x037f: cp = 0x03f3; break; + case 0x0386: cp = 0x03ac; break; + case 0x0388: cp = 0x03ad; break; + case 0x0389: cp = 0x03ae; break; + case 0x038a: cp = 0x03af; break; + case 0x038c: cp = 0x03cc; break; + case 0x038e: cp = 0x03cd; break; + case 0x038f: cp = 0x03ce; break; + case 0x0370: cp = 0x0371; break; + case 0x0372: cp = 0x0373; break; + case 0x0376: cp = 0x0377; break; + case 0x03f4: cp = 0x03d1; break; + case 0x03cf: cp = 0x03d7; break; + case 0x03f9: cp = 0x03f2; break; + case 0x03f7: cp = 0x03f8; break; + case 0x03fa: cp = 0x03fb; break; + }; + } + + return cp; +} + +utf8_int32_t utf8uprcodepoint(utf8_int32_t cp) { + if (((0x0061 <= cp) && (0x007a >= cp)) || + ((0x00e0 <= cp) && (0x00f6 >= cp)) || + ((0x00f8 <= cp) && (0x00fe >= cp)) || + ((0x03b1 <= cp) && (0x03c1 >= cp)) || + ((0x03c3 <= cp) && (0x03cb >= cp))) { + cp -= 32; + } else if (((0x0100 <= cp) && (0x012f >= cp)) || + ((0x0132 <= cp) && (0x0137 >= cp)) || + ((0x014a <= cp) && (0x0177 >= cp)) || + ((0x0182 <= cp) && (0x0185 >= cp)) || + ((0x01a0 <= cp) && (0x01a5 >= cp)) || + ((0x01de <= cp) && (0x01ef >= cp)) || + ((0x01f8 <= cp) && (0x021f >= cp)) || + ((0x0222 <= cp) && (0x0233 >= cp)) || + ((0x0246 <= cp) && (0x024f >= cp)) || + ((0x03d8 <= cp) && (0x03ef >= cp))) { + cp &= ~0x1; + } else if (((0x0139 <= cp) && (0x0148 >= cp)) || + ((0x0179 <= cp) && (0x017e >= cp)) || + ((0x01af <= cp) && (0x01b0 >= cp)) || + ((0x01b3 <= cp) && (0x01b6 >= cp)) || + ((0x01cd <= cp) && (0x01dc >= cp))) { + cp -= 1; + cp |= 0x1; + } else { + switch (cp) { + default: break; + case 0x00ff: cp = 0x0178; break; + case 0x0180: cp = 0x0243; break; + case 0x01dd: cp = 0x018e; break; + case 0x019a: cp = 0x023d; break; + case 0x019e: cp = 0x0220; break; + case 0x0292: cp = 0x01b7; break; + case 0x01c6: cp = 0x01c4; break; + case 0x01c9: cp = 0x01c7; break; + case 0x01cc: cp = 0x01ca; break; + case 0x01f3: cp = 0x01f1; break; + case 0x01bf: cp = 0x01f7; break; + case 0x0188: cp = 0x0187; break; + case 0x018c: cp = 0x018b; break; + case 0x0192: cp = 0x0191; break; + case 0x0199: cp = 0x0198; break; + case 0x01a8: cp = 0x01a7; break; + case 0x01ad: cp = 0x01ac; break; + case 0x01b0: cp = 0x01af; break; + case 0x01b9: cp = 0x01b8; break; + case 0x01bd: cp = 0x01bc; break; + case 0x01f5: cp = 0x01f4; break; + case 0x023c: cp = 0x023b; break; + case 0x0242: cp = 0x0241; break; + case 0x037b: cp = 0x03fd; break; + case 0x037c: cp = 0x03fe; break; + case 0x037d: cp = 0x03ff; break; + case 0x03f3: cp = 0x037f; break; + case 0x03ac: cp = 0x0386; break; + case 0x03ad: cp = 0x0388; break; + case 0x03ae: cp = 0x0389; break; + case 0x03af: cp = 0x038a; break; + case 0x03cc: cp = 0x038c; break; + case 0x03cd: cp = 0x038e; break; + case 0x03ce: cp = 0x038f; break; + case 0x0371: cp = 0x0370; break; + case 0x0373: cp = 0x0372; break; + case 0x0377: cp = 0x0376; break; + case 0x03d1: cp = 0x03f4; break; + case 0x03d7: cp = 0x03cf; break; + case 0x03f2: cp = 0x03f9; break; + case 0x03f8: cp = 0x03f7; break; + case 0x03fb: cp = 0x03fa; break; + }; + } + + return cp; +} + +#undef utf8_restrict +#undef utf8_null + +#ifdef __cplusplus +} // extern "C" +#endif + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#endif // SHEREDOM_UTF8_H_INCLUDED diff --git a/deps/ntlmclient/util.c b/deps/ntlmclient/util.c new file mode 100644 index 000000000..d0e3e53be --- /dev/null +++ b/deps/ntlmclient/util.c @@ -0,0 +1,21 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#include <stdlib.h> +#include <stdint.h> + +#include "compat.h" +#include "util.h" + +void memzero(void *data, size_t size) +{ + volatile uint8_t *scan = (volatile uint8_t *)data; + + while (size--) + *scan++ = 0x0; +} diff --git a/deps/ntlmclient/util.h b/deps/ntlmclient/util.h new file mode 100644 index 000000000..1c1806ba3 --- /dev/null +++ b/deps/ntlmclient/util.h @@ -0,0 +1,14 @@ +/* + * Copyright (c) Edward Thomson. All rights reserved. + * + * This file is part of ntlmclient, distributed under the MIT license. + * For full terms and copyright information, and for third-party + * copyright information, see the included LICENSE.txt file. + */ + +#ifndef PRIVATE_UTIL_H__ +#define PRIVATE_UTIL_H__ + +extern void memzero(void *data, size_t size); + +#endif /* PRIVATE_UTIL_H__ */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bf51c45bd..647bb097a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -396,6 +396,15 @@ ELSE() ENDIF() ADD_FEATURE_INFO(SSH GIT_SSH "SSH transport support") +# Optional external dependency: ntlmclient +IF (USE_NTLMCLIENT) + SET(GIT_NTLM 1) + ADD_SUBDIRECTORY("${libgit2_SOURCE_DIR}/deps/ntlmclient" "${libgit2_BINARY_DIR}/deps/ntlmclient") + LIST(APPEND LIBGIT2_INCLUDES "${libgit2_SOURCE_DIR}/deps/ntlmclient") + LIST(APPEND LIBGIT2_OBJECTS "$<TARGET_OBJECTS:ntlmclient>") +ENDIF() +ADD_FEATURE_INFO(ntlmclient GIT_NTLM "NTLM authentication support for Unix") + # Optional external dependency: libgssapi IF (USE_GSSAPI) FIND_PACKAGE(GSSAPI) diff --git a/src/features.h.in b/src/features.h.in index 4090fadb5..f2931cb11 100644 --- a/src/features.h.in +++ b/src/features.h.in @@ -25,8 +25,10 @@ #cmakedefine GIT_SSH 1 #cmakedefine GIT_SSH_MEMORY_CREDENTIALS 1 +#cmakedefine GIT_NTLM 1 #cmakedefine GIT_GSSAPI 1 #cmakedefine GIT_WINHTTP 1 +#cmakedefine GIT_NTLM 1 #cmakedefine GIT_HTTPS 1 #cmakedefine GIT_OPENSSL 1 diff --git a/src/net.c b/src/net.c new file mode 100644 index 000000000..e35abbb94 --- /dev/null +++ b/src/net.c @@ -0,0 +1,184 @@ +/* + * 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 "net.h" +#include "netops.h" + +#include <ctype.h> +#include "git2/errors.h" + +#include "posix.h" +#include "buffer.h" +#include "http_parser.h" +#include "global.h" + +#define DEFAULT_PORT_HTTP "80" +#define DEFAULT_PORT_HTTPS "443" +#define DEFAULT_PORT_GIT "9418" +#define DEFAULT_PORT_SSH "22" + +static const char *default_port_for_scheme(const char *scheme) +{ + if (strcmp(scheme, "http") == 0) + return DEFAULT_PORT_HTTP; + else if (strcmp(scheme, "https") == 0) + return DEFAULT_PORT_HTTPS; + else if (strcmp(scheme, "git") == 0) + return DEFAULT_PORT_GIT; + else if (strcmp(scheme, "ssh") == 0) + return DEFAULT_PORT_SSH; + + return NULL; +} + +int git_net_url_parse(git_net_url *url, const char *given) +{ + struct http_parser_url u = {0}; + bool has_scheme, has_host, has_port, has_path, has_query, has_userinfo; + git_buf scheme = GIT_BUF_INIT, + host = GIT_BUF_INIT, + port = GIT_BUF_INIT, + path = GIT_BUF_INIT, + username = GIT_BUF_INIT, + password = GIT_BUF_INIT, + query = GIT_BUF_INIT; + int error = GIT_EINVALIDSPEC; + + if (http_parser_parse_url(given, strlen(given), false, &u)) { + git_error_set(GIT_ERROR_NET, "malformed URL '%s'", given); + goto done; + } + + has_scheme = !!(u.field_set & (1 << UF_SCHEMA)); + has_host = !!(u.field_set & (1 << UF_HOST)); + has_port = !!(u.field_set & (1 << UF_PORT)); + has_path = !!(u.field_set & (1 << UF_PATH)); + has_query = !!(u.field_set & (1 << UF_QUERY)); + has_userinfo = !!(u.field_set & (1 << UF_USERINFO)); + + if (has_scheme) { + const char *url_scheme = given + u.field_data[UF_SCHEMA].off; + size_t url_scheme_len = u.field_data[UF_SCHEMA].len; + git_buf_put(&scheme, url_scheme, url_scheme_len); + git__strntolower(scheme.ptr, scheme.size); + } else { + git_error_set(GIT_ERROR_NET, "malformed URL '%s'", given); + goto done; + } + + if (has_host) { + const char *url_host = given + u.field_data[UF_HOST].off; + size_t url_host_len = u.field_data[UF_HOST].len; + git_buf_decode_percent(&host, url_host, url_host_len); + } + + if (has_port) { + const char *url_port = given + u.field_data[UF_PORT].off; + size_t url_port_len = u.field_data[UF_PORT].len; + git_buf_put(&port, url_port, url_port_len); + } else { + const char *default_port = default_port_for_scheme(scheme.ptr); + + if (default_port == NULL) { + git_error_set(GIT_ERROR_NET, "unknown scheme for URL '%s'", given); + goto done; + } + + git_buf_puts(&port, default_port); + } + + if (has_path) { + const char *url_path = given + u.field_data[UF_PATH].off; + size_t url_path_len = u.field_data[UF_PATH].len; + git_buf_decode_percent(&path, url_path, url_path_len); + } else { + git_buf_puts(&path, "/"); + } + + if (has_query) { + const char *url_query = given + u.field_data[UF_QUERY].off; + size_t url_query_len = u.field_data[UF_QUERY].len; + git_buf_decode_percent(&query, url_query, url_query_len); + } + + if (has_userinfo) { + const char *url_userinfo = given + u.field_data[UF_USERINFO].off; + size_t url_userinfo_len = u.field_data[UF_USERINFO].len; + const char *colon = memchr(url_userinfo, ':', url_userinfo_len); + + if (colon) { + const char *url_username = url_userinfo; + size_t url_username_len = colon - url_userinfo; + const char *url_password = colon + 1; + size_t url_password_len = url_userinfo_len - (url_username_len + 1); + + git_buf_decode_percent(&username, url_username, url_username_len); + git_buf_decode_percent(&password, url_password, url_password_len); + } else { + git_buf_decode_percent(&username, url_userinfo, url_userinfo_len); + } + } + + if (git_buf_oom(&scheme) || + git_buf_oom(&host) || + git_buf_oom(&port) || + git_buf_oom(&path) || + git_buf_oom(&query) || + git_buf_oom(&username) || + git_buf_oom(&password)) + return -1; + + url->scheme = git_buf_detach(&scheme); + url->host = git_buf_detach(&host); + url->port = git_buf_detach(&port); + url->path = git_buf_detach(&path); + url->query = git_buf_detach(&query); + url->username = git_buf_detach(&username); + url->password = git_buf_detach(&password); + + error = 0; + +done: + git_buf_dispose(&scheme); + git_buf_dispose(&host); + git_buf_dispose(&port); + git_buf_dispose(&path); + git_buf_dispose(&query); + git_buf_dispose(&username); + git_buf_dispose(&password); + return error; +} + +int git_net_url_is_default_port(git_net_url *url) +{ + return (strcmp(url->port, default_port_for_scheme(url->scheme)) == 0); +} + +void git_net_url_swap(git_net_url *a, git_net_url *b) +{ + git_net_url tmp = GIT_NET_URL_INIT; + + memcpy(&tmp, a, sizeof(git_net_url)); + memcpy(a, b, sizeof(git_net_url)); + memcpy(b, &tmp, sizeof(git_net_url)); +} + +void git_net_url_dispose(git_net_url *url) +{ + if (url->username) + git__memzero(url->username, strlen(url->username)); + + if (url->password) + git__memzero(url->password, strlen(url->password)); + + git__free(url->scheme); url->scheme = NULL; + git__free(url->host); url->host = NULL; + git__free(url->port); url->port = NULL; + git__free(url->path); url->path = NULL; + git__free(url->username); url->username = NULL; + git__free(url->password); url->password = NULL; +} diff --git a/src/net.h b/src/net.h new file mode 100644 index 000000000..6df129089 --- /dev/null +++ b/src/net.h @@ -0,0 +1,36 @@ +/* + * 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_net_h__ +#define INCLUDE_net_h__ + +#include "common.h" + +typedef struct git_net_url { + char *scheme; + char *host; + char *port; + char *path; + char *query; + char *username; + char *password; +} git_net_url; + +#define GIT_NET_URL_INIT { NULL } + +/** Parses a string containing a URL into a structure. */ +int git_net_url_parse(git_net_url *url, const char *str); + +/** Returns nonzero if the URL is on the default port. */ +int git_net_url_is_default_port(git_net_url *url); + +/** Swaps the contents of one URL for another. */ +void git_net_url_swap(git_net_url *a, git_net_url *b); + +/** Disposes the contents of the structure. */ +void git_net_url_dispose(git_net_url *url); + +#endif diff --git a/src/netops.c b/src/netops.c index ecbc2aebe..708f694e3 100644 --- a/src/netops.c +++ b/src/netops.c @@ -119,192 +119,79 @@ int gitno__match_host(const char *pattern, const char *host) return -1; } -static const char *default_port_http = "80"; -static const char *default_port_https = "443"; - -const char *gitno__default_port( - gitno_connection_data *data) -{ - return data->use_ssl ? default_port_https : default_port_http; -} - -static const char *prefix_http = "http://"; -static const char *prefix_https = "https://"; - -int gitno_connection_data_from_url( - gitno_connection_data *data, - const char *url, +int gitno_connection_data_handle_redirect( + git_net_url *url, + const char *redirect_str, const char *service_suffix) { - int error = -1; - const char *default_port = NULL, *path_search_start = NULL; - char *original_host = NULL; - - /* service_suffix is optional */ - assert(data && url); + git_net_url tmp = GIT_NET_URL_INIT; + int error = 0; - /* Save these for comparison later */ - original_host = data->host; - data->host = NULL; - gitno_connection_data_free_ptrs(data); + assert(url && redirect_str); - if (!git__prefixcmp(url, prefix_http)) { - path_search_start = url + strlen(prefix_http); - default_port = default_port_http; + if (redirect_str[0] == '/') { + git__free(url->path); - if (data->use_ssl) { - git_error_set(GIT_ERROR_NET, "redirect from HTTPS to HTTP is not allowed"); - goto cleanup; + if ((url->path = git__strdup(redirect_str)) == NULL) { + error = -1; + goto done; } - } else if (!git__prefixcmp(url, prefix_https)) { - path_search_start = url + strlen(prefix_https); - default_port = default_port_https; - data->use_ssl = true; - } else if (url[0] == '/') - default_port = gitno__default_port(data); - - if (!default_port) { - git_error_set(GIT_ERROR_NET, "unrecognized URL prefix"); - goto cleanup; - } + } else { + git_net_url *original = url; - error = gitno_extract_url_parts( - &data->host, &data->port, &data->path, &data->user, &data->pass, - url, default_port); + if ((error = git_net_url_parse(&tmp, redirect_str)) < 0) + goto done; - if (url[0] == '/') { - /* Relative redirect; reuse original host name and port */ - path_search_start = url; - git__free(data->host); - data->host = original_host; - original_host = NULL; - } + /* Validate that this is a legal redirection */ - if (!error) { - const char *path = strchr(path_search_start, '/'); - size_t pathlen = strlen(path); - size_t suffixlen = service_suffix ? strlen(service_suffix) : 0; - - if (suffixlen && - !memcmp(path + pathlen - suffixlen, service_suffix, suffixlen)) { - git__free(data->path); - data->path = git__strndup(path, pathlen - suffixlen); - } else { - git__free(data->path); - data->path = git__strdup(path); - } + if (original->scheme && + strcmp(original->scheme, tmp.scheme) != 0 && + strcmp(tmp.scheme, "https") != 0) { + git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'", + original->scheme, tmp.scheme); - /* Check for errors in the resulting data */ - if (original_host && url[0] != '/' && strcmp(original_host, data->host)) { - git_error_set(GIT_ERROR_NET, "cross host redirect not allowed"); error = -1; + goto done; } - } -cleanup: - if (original_host) git__free(original_host); - return error; -} + if (original->host && + git__strcasecmp(original->host, tmp.host) != 0) { + git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'", + original->host, tmp.host); -void gitno_connection_data_free_ptrs(gitno_connection_data *d) -{ - git__free(d->host); d->host = NULL; - git__free(d->port); d->port = NULL; - git__free(d->path); d->path = NULL; - git__free(d->user); d->user = NULL; - git__free(d->pass); d->pass = NULL; -} - -int gitno_extract_url_parts( - char **host_out, - char **port_out, - char **path_out, - char **username_out, - char **password_out, - const char *url, - const char *default_port) -{ - struct http_parser_url u = {0}; - bool has_host, has_port, has_path, has_userinfo; - git_buf host = GIT_BUF_INIT, - port = GIT_BUF_INIT, - path = GIT_BUF_INIT, - username = GIT_BUF_INIT, - password = GIT_BUF_INIT; - int error = 0; + error = -1; + goto done; + } - if (http_parser_parse_url(url, strlen(url), false, &u)) { - git_error_set(GIT_ERROR_NET, "malformed URL '%s'", url); - error = GIT_EINVALIDSPEC; - goto done; + git_net_url_swap(url, &tmp); } - has_host = !!(u.field_set & (1 << UF_HOST)); - has_port = !!(u.field_set & (1 << UF_PORT)); - has_path = !!(u.field_set & (1 << UF_PATH)); - has_userinfo = !!(u.field_set & (1 << UF_USERINFO)); + /* Remove the service suffix if it was given to us */ + if (service_suffix) { + const char *service_query = strchr(service_suffix, '?'); + size_t suffix_len = service_query ? + (size_t)(service_query - service_suffix) : strlen(service_suffix); + size_t path_len = strlen(url->path); - if (has_host) { - const char *url_host = url + u.field_data[UF_HOST].off; - size_t url_host_len = u.field_data[UF_HOST].len; - git_buf_decode_percent(&host, url_host, url_host_len); - } + if (suffix_len && path_len >= suffix_len) { + size_t suffix_offset = path_len - suffix_len; - if (has_port) { - const char *url_port = url + u.field_data[UF_PORT].off; - size_t url_port_len = u.field_data[UF_PORT].len; - git_buf_put(&port, url_port, url_port_len); - } else { - git_buf_puts(&port, default_port); - } + if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 && + (!service_query || git__strcmp(url->query, service_query + 1) == 0)) { + /* Ensure we leave a minimum of '/' as the path */ + if (suffix_offset == 0) + suffix_offset++; - if (has_path && path_out) { - const char *url_path = url + u.field_data[UF_PATH].off; - size_t url_path_len = u.field_data[UF_PATH].len; - git_buf_decode_percent(&path, url_path, url_path_len); - } else if (path_out) { - git_error_set(GIT_ERROR_NET, "invalid url, missing path"); - error = GIT_EINVALIDSPEC; - goto done; - } + url->path[suffix_offset] = '\0'; - if (has_userinfo) { - const char *url_userinfo = url + u.field_data[UF_USERINFO].off; - size_t url_userinfo_len = u.field_data[UF_USERINFO].len; - const char *colon = memchr(url_userinfo, ':', url_userinfo_len); - - if (colon) { - const char *url_username = url_userinfo; - size_t url_username_len = colon - url_userinfo; - const char *url_password = colon + 1; - size_t url_password_len = url_userinfo_len - (url_username_len + 1); - - git_buf_decode_percent(&username, url_username, url_username_len); - git_buf_decode_percent(&password, url_password, url_password_len); - } else { - git_buf_decode_percent(&username, url_userinfo, url_userinfo_len); + git__free(url->query); + url->query = NULL; + } } } - if (git_buf_oom(&host) || - git_buf_oom(&port) || - git_buf_oom(&path) || - git_buf_oom(&username) || - git_buf_oom(&password)) - return -1; - - *host_out = git_buf_detach(&host); - *port_out = git_buf_detach(&port); - if (path_out) - *path_out = git_buf_detach(&path); - *username_out = git_buf_detach(&username); - *password_out = git_buf_detach(&password); - done: - git_buf_dispose(&host); - git_buf_dispose(&port); - git_buf_dispose(&path); - git_buf_dispose(&username); - git_buf_dispose(&password); + git_net_url_dispose(&tmp); return error; } + diff --git a/src/netops.h b/src/netops.h index f376bd911..4c4bf78b0 100644 --- a/src/netops.h +++ b/src/netops.h @@ -11,6 +11,7 @@ #include "posix.h" #include "stream.h" +#include "net.h" #ifdef GIT_OPENSSL # include <openssl/ssl.h> @@ -64,38 +65,15 @@ int gitno_recv(gitno_buffer *buf); void gitno_consume(gitno_buffer *buf, const char *ptr); void gitno_consume_n(gitno_buffer *buf, size_t cons); -typedef struct gitno_connection_data { - char *host; - char *port; - char *path; - char *user; - char *pass; - bool use_ssl; -} gitno_connection_data; - /* * This replaces all the pointers in `data` with freshly-allocated strings, * that the caller is responsible for freeing. * `gitno_connection_data_free_ptrs` is good for this. */ -int gitno_connection_data_from_url( - gitno_connection_data *data, +int gitno_connection_data_handle_redirect( + git_net_url *data, const char *url, const char *service_suffix); -/* This frees all the pointers IN the struct, but not the struct itself. */ -void gitno_connection_data_free_ptrs(gitno_connection_data *data); - -int gitno_extract_url_parts( - char **host, - char **port, - char **path, - char **username, - char **password, - const char *url, - const char *default_port); - -const char *gitno__default_port(gitno_connection_data *data); - #endif diff --git a/src/transports/auth.c b/src/transports/auth.c index 6c69282b4..773e3020a 100644 --- a/src/transports/auth.c +++ b/src/transports/auth.c @@ -13,7 +13,6 @@ static int basic_next_token( git_buf *out, git_http_auth_context *ctx, - const char *header_name, git_cred *c) { git_cred_userpass_plaintext *cred; @@ -32,9 +31,8 @@ static int basic_next_token( git_buf_printf(&raw, "%s:%s", cred->username, cred->password); if (git_buf_oom(&raw) || - git_buf_printf(out, "%s: Basic ", header_name) < 0 || - git_buf_encode_base64(out, git_buf_cstr(&raw), raw.size) < 0 || - git_buf_puts(out, "\r\n") < 0) + git_buf_puts(out, "Basic ") < 0 || + git_buf_encode_base64(out, git_buf_cstr(&raw), raw.size) < 0) goto on_error; error = 0; @@ -50,24 +48,26 @@ on_error: static git_http_auth_context basic_context = { GIT_AUTHTYPE_BASIC, GIT_CREDTYPE_USERPASS_PLAINTEXT, + 0, NULL, basic_next_token, + NULL, NULL }; int git_http_auth_basic( - git_http_auth_context **out, const gitno_connection_data *connection_data) + git_http_auth_context **out, const git_net_url *url) { - GIT_UNUSED(connection_data); + GIT_UNUSED(url); *out = &basic_context; return 0; } int git_http_auth_dummy( - git_http_auth_context **out, const gitno_connection_data *connection_data) + git_http_auth_context **out, const git_net_url *url) { - GIT_UNUSED(connection_data); + GIT_UNUSED(url); *out = NULL; return 0; diff --git a/src/transports/auth.h b/src/transports/auth.h index e5cf7eff0..aeea6ce4c 100644 --- a/src/transports/auth.h +++ b/src/transports/auth.h @@ -16,6 +16,7 @@ typedef enum { GIT_AUTHTYPE_BASIC = 1, GIT_AUTHTYPE_NEGOTIATE = 2, + GIT_AUTHTYPE_NTLM = 4, } git_http_authtype_t; typedef struct git_http_auth_context git_http_auth_context; @@ -27,11 +28,17 @@ struct git_http_auth_context { /** Supported credentials */ git_credtype_t credtypes; + /** Connection affinity or request affinity */ + unsigned connection_affinity : 1; + /** 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, const char *header_name, git_cred *cred); + int (*next_token)(git_buf *out, git_http_auth_context *ctx, git_cred *cred); + + /** Examines if all tokens have been presented. */ + int (*is_complete)(git_http_auth_context *ctx); /** Frees the authentication context */ void (*free)(git_http_auth_context *ctx); @@ -50,15 +57,15 @@ typedef struct { /** Function to initialize an authentication context */ int (*init_context)( git_http_auth_context **out, - const gitno_connection_data *connection_data); + const git_net_url *url); } git_http_auth_scheme; int git_http_auth_dummy( git_http_auth_context **out, - const gitno_connection_data *connection_data); + const git_net_url *url); int git_http_auth_basic( git_http_auth_context **out, - const gitno_connection_data *connection_data); + const git_net_url *url); #endif diff --git a/src/transports/auth_negotiate.c b/src/transports/auth_negotiate.c index 25c865c15..f0f2b08a4 100644 --- a/src/transports/auth_negotiate.c +++ b/src/transports/auth_negotiate.c @@ -73,7 +73,6 @@ static int negotiate_set_challenge( static int negotiate_next_token( git_buf *buf, git_http_auth_context *c, - const char *header_name, git_cred *cred) { http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; @@ -156,9 +155,8 @@ static int negotiate_next_token( goto done; } - git_buf_printf(buf, "%s: Negotiate ", header_name); + git_buf_puts(buf, "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; @@ -170,6 +168,15 @@ done: return error; } +static int negotiate_is_complete(git_http_auth_context *c) +{ + http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; + + assert(ctx); + + return (ctx->complete == 1); +} + static void negotiate_context_free(git_http_auth_context *c) { http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; @@ -194,7 +201,7 @@ static void negotiate_context_free(git_http_auth_context *c) static int negotiate_init_context( http_auth_negotiate_context *ctx, - const gitno_connection_data *connection_data) + const git_net_url *url) { OM_uint32 status_major, status_minor; gss_OID item, *oid; @@ -235,7 +242,7 @@ static int negotiate_init_context( } git_buf_puts(&ctx->target, "HTTP@"); - git_buf_puts(&ctx->target, connection_data->host); + git_buf_puts(&ctx->target, url->host); if (git_buf_oom(&ctx->target)) return -1; @@ -248,7 +255,7 @@ static int negotiate_init_context( int git_http_auth_negotiate( git_http_auth_context **out, - const gitno_connection_data *connection_data) + const git_net_url *url) { http_auth_negotiate_context *ctx; @@ -257,15 +264,17 @@ int git_http_auth_negotiate( ctx = git__calloc(1, sizeof(http_auth_negotiate_context)); GIT_ERROR_CHECK_ALLOC(ctx); - if (negotiate_init_context(ctx, connection_data) < 0) { + if (negotiate_init_context(ctx, url) < 0) { git__free(ctx); return -1; } ctx->parent.type = GIT_AUTHTYPE_NEGOTIATE; ctx->parent.credtypes = GIT_CREDTYPE_DEFAULT; + ctx->parent.connection_affinity = 1; ctx->parent.set_challenge = negotiate_set_challenge; ctx->parent.next_token = negotiate_next_token; + ctx->parent.is_complete = negotiate_is_complete; ctx->parent.free = negotiate_context_free; *out = (git_http_auth_context *)ctx; diff --git a/src/transports/auth_negotiate.h b/src/transports/auth_negotiate.h index 15a528aaf..d3f2ba2a1 100644 --- a/src/transports/auth_negotiate.h +++ b/src/transports/auth_negotiate.h @@ -16,7 +16,7 @@ extern int git_http_auth_negotiate( git_http_auth_context **out, - const gitno_connection_data *connection_data); + const git_net_url *url); #else diff --git a/src/transports/auth_ntlm.c b/src/transports/auth_ntlm.c new file mode 100644 index 000000000..eff09bd8a --- /dev/null +++ b/src/transports/auth_ntlm.c @@ -0,0 +1,222 @@ +/* + * 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 "common.h" +#include "buffer.h" +#include "auth.h" +#include "auth_ntlm.h" + +#ifdef GIT_NTLM + +#include "ntlm.h" + +typedef struct { + git_http_auth_context parent; + ntlm_client *ntlm; + char *challenge; + bool complete; +} http_auth_ntlm_context; + +static int ntlm_set_challenge( + git_http_auth_context *c, + const char *challenge) +{ + http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; + + assert(ctx && challenge); + + git__free(ctx->challenge); + + ctx->challenge = git__strdup(challenge); + GIT_ERROR_CHECK_ALLOC(ctx->challenge); + + return 0; +} + +static int ntlm_set_credentials(http_auth_ntlm_context *ctx, git_cred *_cred) +{ + git_cred_userpass_plaintext *cred; + const char *sep, *username; + char *domain = NULL, *domainuser = NULL; + int error = 0; + + assert(_cred->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT); + cred = (git_cred_userpass_plaintext *)_cred; + + if ((sep = strchr(cred->username, '\\')) != NULL) { + domain = strndup(cred->username, (sep - cred->username)); + GIT_ERROR_CHECK_ALLOC(domain); + + domainuser = strdup(sep + 1); + GIT_ERROR_CHECK_ALLOC(domainuser); + + username = domainuser; + } else { + username = cred->username; + } + + if (ntlm_client_set_credentials(ctx->ntlm, + username, domain, cred->password) < 0) { + git_error_set(GIT_ERROR_NET, "could not set credentials: %s", + ntlm_client_errmsg(ctx->ntlm)); + error = -1; + goto done; + } + +done: + git__free(domain); + git__free(domainuser); + return error; +} + +static int ntlm_next_token( + git_buf *buf, + git_http_auth_context *c, + git_cred *cred) +{ + http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; + git_buf input_buf = GIT_BUF_INIT; + const unsigned char *msg; + size_t challenge_len, msg_len; + int error = -1; + + assert(buf && ctx && ctx->ntlm); + + challenge_len = ctx->challenge ? strlen(ctx->challenge) : 0; + + if (ctx->complete) + ntlm_client_reset(ctx->ntlm); + + /* + * Set us complete now since it's the default case; the one + * incomplete case (successfully created a client request) + * will explicitly set that it requires a second step. + */ + ctx->complete = true; + + if (cred && ntlm_set_credentials(ctx, cred) != 0) + goto done; + + if (challenge_len < 4) { + git_error_set(GIT_ERROR_NET, "no ntlm challenge sent from server"); + goto done; + } else if (challenge_len == 4) { + if (memcmp(ctx->challenge, "NTLM", 4) != 0) { + git_error_set(GIT_ERROR_NET, "server did not request NTLM"); + goto done; + } + + if (ntlm_client_negotiate(&msg, &msg_len, ctx->ntlm) != 0) { + git_error_set(GIT_ERROR_NET, "ntlm authentication failed: %s", + ntlm_client_errmsg(ctx->ntlm)); + goto done; + } + + ctx->complete = false; + } else { + if (memcmp(ctx->challenge, "NTLM ", 5) != 0) { + git_error_set(GIT_ERROR_NET, "challenge from server was not NTLM"); + goto done; + } + + if (git_buf_decode_base64(&input_buf, + ctx->challenge + 5, challenge_len - 5) < 0) { + git_error_set(GIT_ERROR_NET, "invalid NTLM challenge from server"); + goto done; + } + + if (ntlm_client_set_challenge(ctx->ntlm, + (const unsigned char *)input_buf.ptr, input_buf.size) != 0) { + git_error_set(GIT_ERROR_NET, "ntlm challenge failed: %s", + ntlm_client_errmsg(ctx->ntlm)); + goto done; + } + + if (ntlm_client_response(&msg, &msg_len, ctx->ntlm) != 0) { + git_error_set(GIT_ERROR_NET, "ntlm authentication failed: %s", + ntlm_client_errmsg(ctx->ntlm)); + goto done; + } + } + + git_buf_puts(buf, "NTLM "); + git_buf_encode_base64(buf, (const char *)msg, msg_len); + + if (git_buf_oom(buf)) + goto done; + + error = 0; + +done: + git_buf_dispose(&input_buf); + return error; +} + +static int ntlm_is_complete(git_http_auth_context *c) +{ + http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; + + assert(ctx); + return (ctx->complete == true); +} + +static void ntlm_context_free(git_http_auth_context *c) +{ + http_auth_ntlm_context *ctx = (http_auth_ntlm_context *)c; + + ntlm_client_free(ctx->ntlm); + git__free(ctx->challenge); + git__free(ctx); +} + +static int ntlm_init_context( + http_auth_ntlm_context *ctx, + const git_net_url *url) +{ + GIT_UNUSED(url); + + if ((ctx->ntlm = ntlm_client_init(NTLM_CLIENT_DEFAULTS)) == NULL) { + git_error_set_oom(); + return -1; + } + + return 0; +} + +int git_http_auth_ntlm( + git_http_auth_context **out, + const git_net_url *url) +{ + http_auth_ntlm_context *ctx; + + GIT_UNUSED(url); + + *out = NULL; + + ctx = git__calloc(1, sizeof(http_auth_ntlm_context)); + GIT_ERROR_CHECK_ALLOC(ctx); + + if (ntlm_init_context(ctx, url) < 0) { + git__free(ctx); + return -1; + } + + ctx->parent.type = GIT_AUTHTYPE_NTLM; + ctx->parent.credtypes = GIT_CREDTYPE_USERPASS_PLAINTEXT; + ctx->parent.connection_affinity = 1; + ctx->parent.set_challenge = ntlm_set_challenge; + ctx->parent.next_token = ntlm_next_token; + ctx->parent.is_complete = ntlm_is_complete; + ctx->parent.free = ntlm_context_free; + + *out = (git_http_auth_context *)ctx; + + return 0; +} + +#endif /* GIT_NTLM */ diff --git a/src/transports/auth_ntlm.h b/src/transports/auth_ntlm.h new file mode 100644 index 000000000..5b42b2b8e --- /dev/null +++ b/src/transports/auth_ntlm.h @@ -0,0 +1,35 @@ +/* + * 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_transports_auth_ntlm_h__ +#define INCLUDE_transports_auth_ntlm_h__ + +#include "git2.h" +#include "auth.h" + +#ifdef GIT_NTLM + +#if defined(GIT_OPENSSL) +# define CRYPT_OPENSSL +#elif defined(GIT_MBEDTLS) +# define CRYPT_MBEDTLS +#elif defined(GIT_SECURE_TRANSPORT) +# define CRYPT_COMMONCRYPTO +#endif + +extern int git_http_auth_ntlm( + git_http_auth_context **out, + const git_net_url *url); + +#else + +#define git_http_auth_ntlm git_http_auth_dummy + +#endif /* GIT_NTLM */ + +#endif + diff --git a/src/transports/git.c b/src/transports/git.c index 9fd3b47fc..e48b7f961 100644 --- a/src/transports/git.c +++ b/src/transports/git.c @@ -192,8 +192,9 @@ static int _git_uploadpack_ls( const char *url, git_smart_subtransport_stream **stream) { - char *host=NULL, *port=NULL, *path=NULL, *user=NULL, *pass=NULL; + git_net_url urldata = GIT_NET_URL_INIT; const char *stream_url = url; + const char *host, *port; git_proto_stream *s; int error; @@ -202,17 +203,15 @@ static int _git_uploadpack_ls( if (!git__prefixcmp(url, prefix_git)) stream_url += strlen(prefix_git); - if ((error = gitno_extract_url_parts(&host, &port, &path, &user, &pass, url, GIT_DEFAULT_PORT)) < 0) + if ((error = git_net_url_parse(&urldata, url)) < 0) return error; - error = git_proto_stream_alloc(t, stream_url, cmd_uploadpack, host, port, stream); + host = urldata.host; + port = urldata.port ? urldata.port : GIT_DEFAULT_PORT; - git__free(host); - git__free(port); - git__free(path); - git__free(user); - git__free(pass); + error = git_proto_stream_alloc(t, stream_url, cmd_uploadpack, host, port, stream); + git_net_url_dispose(&urldata); if (error < 0) { git_proto_stream_free(*stream); @@ -251,7 +250,7 @@ static int _git_receivepack_ls( const char *url, git_smart_subtransport_stream **stream) { - char *host=NULL, *port=NULL, *path=NULL, *user=NULL, *pass=NULL; + git_net_url urldata = GIT_NET_URL_INIT; const char *stream_url = url; git_proto_stream *s; int error; @@ -260,16 +259,12 @@ static int _git_receivepack_ls( if (!git__prefixcmp(url, prefix_git)) stream_url += strlen(prefix_git); - if ((error = gitno_extract_url_parts(&host, &port, &path, &user, &pass, url, GIT_DEFAULT_PORT)) < 0) + if ((error = git_net_url_parse(&urldata, url)) < 0) return error; - error = git_proto_stream_alloc(t, stream_url, cmd_receivepack, host, port, stream); + error = git_proto_stream_alloc(t, stream_url, cmd_receivepack, urldata.host, urldata.port, stream); - git__free(host); - git__free(port); - git__free(path); - git__free(user); - git__free(pass); + git_net_url_dispose(&urldata); if (error < 0) { git_proto_stream_free(*stream); diff --git a/src/transports/http.c b/src/transports/http.c index 9d77e627f..f1e048ee6 100644 --- a/src/transports/http.c +++ b/src/transports/http.c @@ -12,6 +12,7 @@ #include "git2.h" #include "http_parser.h" #include "buffer.h" +#include "net.h" #include "netops.h" #include "global.h" #include "remote.h" @@ -19,11 +20,13 @@ #include "auth.h" #include "http.h" #include "auth_negotiate.h" +#include "auth_ntlm.h" #include "streams/tls.h" #include "streams/socket.h" git_http_auth_scheme auth_schemes[] = { { GIT_AUTHTYPE_NEGOTIATE, "Negotiate", GIT_CREDTYPE_DEFAULT, git_http_auth_negotiate }, + { GIT_AUTHTYPE_NTLM, "NTLM", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_ntlm }, { GIT_AUTHTYPE_BASIC, "Basic", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_basic }, }; @@ -71,14 +74,18 @@ typedef struct { } http_stream; typedef struct { - gitno_connection_data url; + git_net_url url; git_stream *stream; + git_http_authtype_t authtypes; + git_credtype_t credtypes; + git_cred *cred; - git_cred *url_cred; + unsigned url_cred_presented : 1, + authenticated : 1; git_vector auth_challenges; - git_vector auth_contexts; + git_http_auth_context *auth_context; } http_server; typedef struct { @@ -106,8 +113,10 @@ typedef struct { enum last_cb last_cb; int parse_error; int error; + unsigned request_count; unsigned parse_finished : 1, - replay_count : 3; + keepalive : 1, + replay_count : 4; } http_subtransport; typedef struct { @@ -120,66 +129,24 @@ typedef struct { size_t *bytes_read; } parser_context; -static bool credtype_match(git_http_auth_scheme *scheme, void *data) -{ - unsigned int credtype = *(unsigned int *)data; - - return !!(scheme->credtypes & credtype); -} - -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; - - scheme_len = strlen(scheme_name); - return (strncasecmp(challenge, scheme_name, scheme_len) == 0 && - (challenge[scheme_len] == '\0' || challenge[scheme_len] == ' ')); -} - -static int auth_context_match( - git_http_auth_context **out, - http_server *server, - bool (*scheme_match)(git_http_auth_scheme *scheme, void *data), - void *data) +static git_http_auth_scheme *scheme_for_challenge(const char *challenge) { git_http_auth_scheme *scheme = NULL; - git_http_auth_context *context = NULL, *c; size_t i; - *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; + const char *scheme_name = auth_schemes[i].name; + size_t scheme_len; - /* See if authentication has already started for this scheme */ - git_vector_foreach(&server->auth_contexts, i, c) { - if (c->type == scheme->type) { - context = c; + scheme_len = strlen(scheme_name); + if (strncasecmp(challenge, scheme_name, scheme_len) == 0 && + (challenge[scheme_len] == '\0' || challenge[scheme_len] == ' ')) { + scheme = &auth_schemes[i]; break; } } - if (!context) { - if (scheme->init_context(&context, &server->url) < 0) - return -1; - else if (!context) - return 0; - else if (git_vector_insert(&server->auth_contexts, context) < 0) - return -1; - } - - *out = context; - - return 0; + return scheme; } static int apply_credentials( @@ -187,31 +154,20 @@ static int apply_credentials( http_server *server, const char *header_name) { - git_cred *cred = server->cred; - git_http_auth_context *context; - - /* Apply the credentials given to us in the URL */ - if (!cred && server->url.user && server->url.pass) { - if (!server->url_cred && - git_cred_userpass_plaintext_new(&server->url_cred, - server->url.user, server->url.pass) < 0) - return -1; + git_buf token = GIT_BUF_INIT; + int error = 0; - cred = server->url_cred; - } - - if (!cred) - return 0; + if (!server->auth_context) + goto done; - /* Get or create a context for the best scheme for this cred type */ - if (auth_context_match(&context, server, - credtype_match, &cred->credtype) < 0) - return -1; + if ((error = server->auth_context->next_token(&token, server->auth_context, server->cred)) < 0) + goto done; - if (!context) - return 0; + error = git_buf_printf(buf, "%s: %s\r\n", header_name, token.ptr); - return context->next_token(buf, context, header_name, cred); +done: + git_buf_dispose(&token); + return error; } static int gen_request( @@ -226,7 +182,7 @@ static int gen_request( if (t->proxy_opts.type == GIT_PROXY_SPECIFIED) git_buf_printf(buf, "%s %s://%s:%s%s%s HTTP/1.1\r\n", s->verb, - t->server.url.use_ssl ? "https" : "http", + t->server.url.scheme, t->server.url.host, t->server.url.port, path, s->service_url); @@ -238,9 +194,10 @@ static int gen_request( git_http__user_agent(buf); git_buf_puts(buf, "\r\n"); git_buf_printf(buf, "Host: %s", t->server.url.host); - if (strcmp(t->server.url.port, gitno__default_port(&t->server.url)) != 0) { + + if (!git_net_url_is_default_port(&t->server.url)) git_buf_printf(buf, ":%s", t->server.url.port); - } + git_buf_puts(buf, "\r\n"); if (s->chunked || content_length > 0) { @@ -275,29 +232,86 @@ static int gen_request( return 0; } -static int parse_authenticate_response( - http_server *server, - int *allowed_types) +static int set_authentication_challenge(http_server *server) { - git_http_auth_context *context; + const char *challenge; + + if (git_vector_length(&server->auth_challenges) > 1) { + git_error_set(GIT_ERROR_NET, "received multiple authentication challenges"); + return -1; + } + + challenge = git_vector_get(&server->auth_challenges, 0); + + if (server->auth_context->set_challenge) + return server->auth_context->set_challenge(server->auth_context, challenge); + else + return 0; +} + +static int set_authentication_types(http_server *server) +{ + git_http_auth_scheme *scheme; char *challenge; size_t i; git_vector_foreach(&server->auth_challenges, i, challenge) { - if (auth_context_match(&context, server, - challenge_match, challenge) < 0) - return -1; - else if (!context) - continue; + if ((scheme = scheme_for_challenge(challenge)) != NULL) { + server->authtypes |= scheme->type; + server->credtypes |= scheme->credtypes; + } + } - if (context->set_challenge && - context->set_challenge(context, challenge) < 0) - return -1; + return 0; +} + +static bool auth_context_complete(http_server *server) +{ + /* If there's no is_complete function, we're always complete */ + if (!server->auth_context->is_complete) + return true; + + if (server->auth_context->is_complete(server->auth_context)) + return true; + + return false; +} + +static void free_auth_context(http_server *server) +{ + if (!server->auth_context) + return; + + if (server->auth_context->free) + server->auth_context->free(server->auth_context); + + server->auth_context = NULL; +} + +static int parse_authenticate_response(http_server *server) +{ + /* + * If we think that we've completed authentication (ie, we've either + * sent a basic credential or we've sent the NTLM/Negotiate response) + * but we've got an authentication request from the server then our + * last authentication did not succeed. Start over. + */ + if (server->auth_context && auth_context_complete(server)) { + free_auth_context(server); - *allowed_types |= context->credtypes; + server->authenticated = 0; } - return 0; + /* + * If we've begun authentication, give the challenge to the context. + * Otherwise, set up the types to prepare credentials. + */ + if (git_vector_length(&server->auth_challenges) == 0) + return 0; + else if (server->auth_context) + return set_authentication_challenge(server); + else + return set_authentication_types(server); } static int on_header_ready(http_subtransport *t) @@ -396,55 +410,145 @@ GIT_INLINE(void) free_cred(git_cred **cred) } } +static int apply_url_credentials( + git_cred **cred, + unsigned int allowed_types, + const char *username, + const char *password) +{ + if (allowed_types & GIT_CREDTYPE_USERPASS_PLAINTEXT) + return git_cred_userpass_plaintext_new(cred, username, password); + + if ((allowed_types & GIT_CREDTYPE_DEFAULT) && *username == '\0' && *password == '\0') + return git_cred_default_new(cred); + + return GIT_PASSTHROUGH; +} + +static int init_auth(http_server *server) +{ + git_http_auth_scheme *s, *scheme = NULL; + char *c, *challenge = NULL; + size_t i; + + git_vector_foreach(&server->auth_challenges, i, c) { + s = scheme_for_challenge(c); + + if (s && !!(s->credtypes & server->credtypes)) { + scheme = s; + challenge = c; + break; + } + } + + if (!scheme) { + git_error_set(GIT_ERROR_NET, "no authentication mechanism could be negotiated"); + return -1; + } + + if (scheme->init_context(&server->auth_context, &server->url) < 0) + return -1; + + if (server->auth_context->set_challenge && + server->auth_context->set_challenge(server->auth_context, challenge) < 0) + return -1; + + return 0; +} + static int on_auth_required( - git_cred **creds, http_parser *parser, + http_server *server, const char *url, const char *type, git_cred_acquire_cb callback, - void *callback_payload, - const char *username, - int allowed_types) + void *callback_payload) { parser_context *ctx = (parser_context *) parser->data; http_subtransport *t = ctx->t; - int ret; + int error = 1; + + if (parse_authenticate_response(server) < 0) { + t->parse_error = PARSE_ERROR_GENERIC; + return t->parse_error; + } - if (!allowed_types) { + /* If we're in the middle of challenge/response auth, continue */ + if (parser->status_code == 407 || parser->status_code == 401) { + if (server->auth_context && !auth_context_complete(server)) { + t->parse_error = PARSE_ERROR_REPLAY; + return 0; + } + } + + /* Enforce a reasonable cap on the number of replays */ + if (t->replay_count++ >= GIT_HTTP_REPLAY_MAX) { + git_error_set(GIT_ERROR_NET, "too many redirects or authentication replays"); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + if (!server->credtypes) { git_error_set(GIT_ERROR_NET, "%s requested authentication but did not negotiate mechanisms", type); t->parse_error = PARSE_ERROR_GENERIC; return t->parse_error; } - if (callback) { - free_cred(creds); - ret = callback(creds, url, username, allowed_types, callback_payload); + free_auth_context(server); + free_cred(&server->cred); + + /* Start with URL-specified credentials, if there were any. */ + if (!server->url_cred_presented && server->url.username && server->url.password) { + error = apply_url_credentials(&server->cred, server->credtypes, server->url.username, server->url.password); + server->url_cred_presented = 1; - if (ret == GIT_PASSTHROUGH) { + if (error == GIT_PASSTHROUGH) { /* treat GIT_PASSTHROUGH as if callback isn't set */ - } else if (ret < 0) { - t->error = ret; - t->parse_error = PARSE_ERROR_EXT; - return t->parse_error; - } else { - assert(*creds); - - if (!((*creds)->credtype & allowed_types)) { - git_error_set(GIT_ERROR_NET, "%s credential provider returned an invalid cred type", type); - t->parse_error = PARSE_ERROR_GENERIC; - return t->parse_error; - } + error = 1; + } + } - /* Successfully acquired a credential. */ - t->parse_error = PARSE_ERROR_REPLAY; - return 0; + if (error > 0 && callback) { + error = callback(&server->cred, url, server->url.username, server->credtypes, callback_payload); + + if (error == GIT_PASSTHROUGH) { + /* treat GIT_PASSTHROUGH as if callback isn't set */ + error = 1; } } - git_error_set(GIT_ERROR_NET, "%s authentication required but no callback set", - type); - t->parse_error = PARSE_ERROR_GENERIC; - return t->parse_error; + if (error > 0) { + git_error_set(GIT_ERROR_NET, "%s authentication required but no callback set", + type); + t->parse_error = PARSE_ERROR_GENERIC; + return t->parse_error; + } else if (error < 0) { + t->error = error; + t->parse_error = PARSE_ERROR_EXT; + return t->parse_error; + } + + assert(server->cred); + + if (!(server->cred->credtype & server->credtypes)) { + git_error_set(GIT_ERROR_NET, "%s credential provider returned an invalid cred type", type); + t->parse_error = PARSE_ERROR_GENERIC; + return t->parse_error; + } + + /* Successfully acquired a credential. Start an auth context. */ + if (init_auth(server) < 0) { + t->parse_error = PARSE_ERROR_GENERIC; + return t->parse_error; + } + + t->parse_error = PARSE_ERROR_REPLAY; + return 0; +} + +static void on_auth_success(http_server *server) +{ + server->url_cred_presented = 0; + server->authenticated = 1; } static int on_headers_complete(http_parser *parser) @@ -453,50 +557,35 @@ 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 proxy_auth_types = 0, server_auth_types = 0; - - /* Enforce a reasonable cap on the number of replays */ - if (t->replay_count++ >= GIT_HTTP_REPLAY_MAX) { - git_error_set(GIT_ERROR_NET, "too many redirects or authentication replays"); - return t->parse_error = PARSE_ERROR_GENERIC; - } /* Both parse_header_name and parse_header_value are populated * and ready for consumption. */ - if (VALUE == t->last_cb) - if (on_header_ready(t) < 0) - return t->parse_error = PARSE_ERROR_GENERIC; - - /* - * Capture authentication headers for the proxy or final endpoint, - * these may be 407/401 (authentication is not complete) or a 200 - * (informing us that auth has completed). - */ - if (parse_authenticate_response(&t->proxy, &proxy_auth_types) < 0 || - parse_authenticate_response(&t->server, &server_auth_types) < 0) + if (t->last_cb == VALUE && on_header_ready(t) < 0) return t->parse_error = PARSE_ERROR_GENERIC; /* Check for a proxy authentication failure. */ if (parser->status_code == 407 && get_verb == s->verb) - return on_auth_required(&t->proxy.cred, + return on_auth_required( parser, + &t->proxy, t->proxy_opts.url, SERVER_TYPE_PROXY, t->proxy_opts.credentials, - t->proxy_opts.payload, - t->proxy.url.user, - proxy_auth_types); + t->proxy_opts.payload); + else + on_auth_success(&t->proxy); /* Check for an authentication failure. */ if (parser->status_code == 401 && get_verb == s->verb) - return on_auth_required(&t->server.cred, + return on_auth_required( parser, + &t->server, t->owner->url, SERVER_TYPE_REMOTE, t->owner->cred_acquire_cb, - t->owner->cred_acquire_payload, - t->server.url.user, - server_auth_types); + t->owner->cred_acquire_payload); + else + on_auth_success(&t->server); /* Check for a redirect. * Right now we only permit a redirect to the same hostname. */ @@ -507,7 +596,7 @@ static int on_headers_complete(http_parser *parser) parser->status_code == 308) && t->location) { - if (gitno_connection_data_from_url(&t->server.url, t->location, s->service_url) < 0) + if (gitno_connection_data_handle_redirect(&t->server.url, t->location, s->service_url) < 0) return t->parse_error = PARSE_ERROR_GENERIC; /* Set the redirect URL on the stream. This is a transfer of @@ -569,6 +658,7 @@ static int on_message_complete(http_parser *parser) http_subtransport *t = ctx->t; t->parse_finished = 1; + t->keepalive = http_should_keep_alive(parser); return 0; } @@ -578,13 +668,6 @@ 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 there's no buffer set, we're explicitly ignoring the body. */ if (ctx->buffer) { if (ctx->buf_size < len) { @@ -613,6 +696,7 @@ static void clear_parser_state(http_subtransport *t) t->last_cb = NONE; t->parse_error = 0; t->parse_finished = 0; + t->keepalive = 0; git_buf_dispose(&t->parse_header_name); git_buf_init(&t->parse_header_name, 0); @@ -676,7 +760,7 @@ static int load_proxy_config(http_subtransport *t) git_proxy_init_options(&t->proxy_opts, GIT_PROXY_OPTIONS_VERSION); if ((error = git_remote__get_http_proxy(t->owner->owner, - !!t->server.url.use_ssl, &t->proxy_url)) < 0) + !strcmp(t->server.url.scheme, "https"), &t->proxy_url)) < 0) return error; if (!t->proxy_url) @@ -698,20 +782,14 @@ static int load_proxy_config(http_subtransport *t) return -1; } - if ((error = gitno_connection_data_from_url(&t->proxy.url, t->proxy_opts.url, NULL)) < 0) - return error; - - if (t->proxy.url.use_ssl) { - git_error_set(GIT_ERROR_NET, "SSL connections to proxy are not supported"); - return -1; - } + git_net_url_dispose(&t->proxy.url); - return error; + return git_net_url_parse(&t->proxy.url, t->proxy_opts.url); } static int check_certificate( git_stream *stream, - gitno_connection_data *url, + git_net_url *url, int is_valid, git_transport_certificate_check_cb cert_cb, void *cert_cb_payload) @@ -740,7 +818,7 @@ static int check_certificate( static int stream_connect( git_stream *stream, - gitno_connection_data *url, + git_net_url *url, git_transport_certificate_check_cb cert_cb, void *cb_payload) { @@ -782,18 +860,10 @@ static int proxy_headers_complete(http_parser *parser) { parser_context *ctx = (parser_context *) parser->data; http_subtransport *t = ctx->t; - int proxy_auth_types = 0; - - /* Enforce a reasonable cap on the number of replays */ - if (t->replay_count++ >= GIT_HTTP_REPLAY_MAX) { - git_error_set(GIT_ERROR_NET, "too many redirects or authentication replays"); - return t->parse_error = PARSE_ERROR_GENERIC; - } /* Both parse_header_name and parse_header_value are populated * and ready for consumption. */ - if (VALUE == t->last_cb) - if (on_header_ready(t) < 0) + if (t->last_cb == VALUE && on_header_ready(t) < 0) return t->parse_error = PARSE_ERROR_GENERIC; /* @@ -801,19 +871,32 @@ static int proxy_headers_complete(http_parser *parser) * these may be 407/401 (authentication is not complete) or a 200 * (informing us that auth has completed). */ - if (parse_authenticate_response(&t->proxy, &proxy_auth_types) < 0) + if (parse_authenticate_response(&t->proxy) < 0) return t->parse_error = PARSE_ERROR_GENERIC; + /* If we're in the middle of challenge/response auth, continue */ + if (parser->status_code == 407) { + if (t->proxy.auth_context && !auth_context_complete(&t->proxy)) { + t->parse_error = PARSE_ERROR_REPLAY; + return 0; + } + } + + /* Enforce a reasonable cap on the number of replays */ + if (t->replay_count++ >= GIT_HTTP_REPLAY_MAX) { + git_error_set(GIT_ERROR_NET, "too many redirects or authentication replays"); + return t->parse_error = PARSE_ERROR_GENERIC; + } + /* Check for a proxy authentication failure. */ if (parser->status_code == 407) - return on_auth_required(&t->proxy.cred, + return on_auth_required( parser, + &t->proxy, t->proxy_opts.url, SERVER_TYPE_PROXY, t->proxy_opts.credentials, - t->proxy_opts.payload, - t->proxy.url.user, - proxy_auth_types); + t->proxy_opts.payload); if (parser->status_code != 200) { git_error_set(GIT_ERROR_NET, "unexpected status code from proxy: %d", @@ -834,6 +917,7 @@ static int proxy_connect( static http_parser_settings proxy_parser_settings = {0}; size_t bytes_read = 0, bytes_parsed; parser_context ctx; + bool auth_replay; int error; /* Use the parser settings only to parser headers. */ @@ -845,6 +929,8 @@ static int proxy_connect( replay: clear_parser_state(t); + auth_replay = false; + gitno_buffer_setup_fromstream(proxy_stream, &t->parse_buffer, t->parse_buffer_data, @@ -862,8 +948,17 @@ replay: while (!bytes_read && !t->parse_finished) { t->parse_buffer.offset = 0; - if ((error = gitno_recv(&t->parse_buffer)) < 0) + if ((error = gitno_recv(&t->parse_buffer)) < 0) { goto done; + } else if (error == 0 && t->request_count > 0) { + /* Server closed a keep-alive socket; reconnect. */ + auth_replay = true; + goto done; + } else if (error == 0) { + git_error_set(GIT_ERROR_NET, "unexpected disconnection from server"); + error = -1; + goto done; + } /* * This call to http_parser_execute will invoke the on_* @@ -892,10 +987,9 @@ replay: } /* Replay the request with authentication headers. */ - if (PARSE_ERROR_REPLAY == t->parse_error) - goto replay; - - if (t->parse_error < 0) { + if (PARSE_ERROR_REPLAY == t->parse_error) { + auth_replay = true; + } else if (t->parse_error < 0) { error = t->parse_error == PARSE_ERROR_EXT ? PARSE_ERROR_EXT : -1; goto done; } @@ -909,6 +1003,15 @@ replay: } } + t->request_count++; + + if (auth_replay) { + if (t->keepalive && t->parse_finished) + goto replay; + + return PARSE_ERROR_REPLAY; + } + if ((error = git_tls_stream_wrap(out, proxy_stream, t->server.url.host)) == 0) error = stream_connect(*out, &t->server.url, t->owner->certificate_check_cb, @@ -920,22 +1023,45 @@ replay: */ t->proxy_opts.type = GIT_PROXY_NONE; t->replay_count = 0; + t->request_count = 0; done: return error; } +static void reset_auth_connection(http_server *server) +{ + /* + * If we've authenticated and we're doing "normal" + * authentication with a request affinity (Basic, Digest) + * then we want to _keep_ our context, since authentication + * survives even through non-keep-alive connections. If + * we've authenticated and we're doing connection-based + * authentication (NTLM, Negotiate) - indicated by the presence + * of an `is_complete` callback - then we need to restart + * authentication on a new connection. + */ + + if (server->authenticated && + server->auth_context && + server->auth_context->connection_affinity) { + free_auth_context(server); + + server->url_cred_presented = 0; + server->authenticated = 0; + } +} + static int http_connect(http_subtransport *t) { - gitno_connection_data *url; + git_net_url *url; git_stream *proxy_stream = NULL, *stream = NULL; git_transport_certificate_check_cb cert_cb; void *cb_payload; int error; - if (t->connected && - http_should_keep_alive(&t->parser) && - t->parse_finished) +auth_replay: + if (t->connected && t->keepalive && t->parse_finished) return 0; if ((error = load_proxy_config(t)) < 0) @@ -953,7 +1079,12 @@ static int http_connect(http_subtransport *t) t->proxy.stream = NULL; } + reset_auth_connection(&t->server); + reset_auth_connection(&t->proxy); + t->connected = 0; + t->keepalive = 0; + t->request_count = 0; if (t->proxy_opts.type == GIT_PROXY_SPECIFIED) { url = &t->proxy.url; @@ -965,7 +1096,7 @@ static int http_connect(http_subtransport *t) cb_payload = t->owner->message_cb_payload; } - if (url->use_ssl) + if (strcmp(url->scheme, "https") == 0) error = git_tls_stream_new(&stream, url->host, url->port); else error = git_socket_stream_new(&stream, url->host, url->port); @@ -982,18 +1113,24 @@ static int http_connect(http_subtransport *t) * an HTTPS connection, then we need to build a CONNECT tunnel. */ if (t->proxy_opts.type == GIT_PROXY_SPECIFIED && - t->server.url.use_ssl) { + strcmp(t->server.url.scheme, "https") == 0) { proxy_stream = stream; stream = NULL; - if ((error = proxy_connect(&stream, proxy_stream, t)) < 0) + error = proxy_connect(&stream, proxy_stream, t); + + if (error == PARSE_ERROR_REPLAY) { + git_stream_close(proxy_stream); + git_stream_free(proxy_stream); + goto auth_replay; + } else if (error < 0) { goto on_error; + } } t->proxy.stream = proxy_stream; t->server.stream = stream; t->connected = 1; - t->replay_count = 0; return 0; on_error: @@ -1020,27 +1157,23 @@ static int http_stream_read( http_subtransport *t = OWNING_SUBTRANSPORT(s); parser_context ctx; size_t bytes_parsed; + git_buf request = GIT_BUF_INIT; + bool auth_replay; + int error = 0; replay: *bytes_read = 0; + auth_replay = false; assert(t->connected); if (!s->sent_request) { - git_buf request = GIT_BUF_INIT; - + git_buf_clear(&request); clear_parser_state(t); - if (gen_request(&request, s, 0) < 0) - return -1; - - if (git_stream__write_full(t->server.stream, request.ptr, - request.size, 0) < 0) { - git_buf_dispose(&request); - return -1; - } - - git_buf_dispose(&request); + if ((error = gen_request(&request, s, 0)) < 0 || + (error = git_stream__write_full(t->server.stream, request.ptr, request.size, 0)) < 0) + goto done; s->sent_request = 1; } @@ -1050,17 +1183,17 @@ replay: assert(s->verb == post_verb); /* Flush, if necessary */ - if (s->chunk_buffer_len > 0 && - write_chunk(t->server.stream, - s->chunk_buffer, s->chunk_buffer_len) < 0) - return -1; + if (s->chunk_buffer_len > 0) { + if ((error = write_chunk(t->server.stream, s->chunk_buffer, s->chunk_buffer_len)) < 0) + goto done; - s->chunk_buffer_len = 0; + s->chunk_buffer_len = 0; + } /* Write the final chunk. */ - if (git_stream__write_full(t->server.stream, - "0\r\n\r\n", 5, 0) < 0) - return -1; + if ((error = git_stream__write_full(t->server.stream, + "0\r\n\r\n", 5, 0)) < 0) + goto done; } s->received_response = 1; @@ -1068,7 +1201,6 @@ replay: while (!*bytes_read && !t->parse_finished) { size_t data_offset; - int error; /* * Make the parse_buffer think it's as full of data as @@ -1078,26 +1210,37 @@ replay: * data_offset is the actual data offset from which we * should tell the parser to start reading. */ - if (buf_size >= t->parse_buffer.len) { + if (buf_size >= t->parse_buffer.len) t->parse_buffer.offset = 0; - } else { + else t->parse_buffer.offset = t->parse_buffer.len - buf_size; - } data_offset = t->parse_buffer.offset; - if (gitno_recv(&t->parse_buffer) < 0) - return -1; + if ((error = gitno_recv(&t->parse_buffer)) < 0) { + goto done; + } else if (error == 0 && t->request_count > 0) { + /* Server closed a keep-alive socket; reconnect. */ + auth_replay = true; + goto done; + } else if (error == 0) { + git_error_set(GIT_ERROR_NET, "unexpected disconnection from server"); + error = -1; + goto done; + } - /* This call to http_parser_execute will result in invocations of the - * on_* family of callbacks. The most interesting of these is - * on_body_fill_buffer, which is called when data is ready to be copied - * into the target buffer. We need to marshal the buffer, buf_size, and - * bytes_read parameters to this callback. */ + /* + * This call to http_parser_execute will result in invocations + * of the on_* family of callbacks, including on_body_fill_buffer + * which will write into the target buffer. Set up the buffer + * for it to write into _unless_ we got an auth failure; in + * that case we only care about the headers and don't need to + * bother copying the body. + */ ctx.t = t; ctx.s = s; - ctx.buffer = buffer; - ctx.buf_size = buf_size; + ctx.buffer = auth_replay ? NULL : buffer; + ctx.buf_size = auth_replay ? 0 : buf_size; ctx.bytes_read = bytes_read; /* Set the context, call the parser, then unset the context. */ @@ -1110,33 +1253,40 @@ replay: t->parser.data = NULL; - /* If there was a handled authentication failure, then parse_error - * will have signaled us that we should replay the request. */ - if (PARSE_ERROR_REPLAY == t->parse_error) { - s->sent_request = 0; - - if ((error = http_connect(t)) < 0) - return error; - - goto replay; - } - - if (t->parse_error == PARSE_ERROR_EXT) { - return t->error; + /* On a 401, read the rest of the response then retry. */ + if (t->parse_error == PARSE_ERROR_REPLAY) { + auth_replay = true; + } else if (t->parse_error == PARSE_ERROR_EXT) { + error = t->error; + goto done; + } else if (t->parse_error < 0) { + error = -1; + goto done; } - if (t->parse_error < 0) - return -1; - if (bytes_parsed != t->parse_buffer.offset - data_offset) { git_error_set(GIT_ERROR_NET, "HTTP parser error: %s", http_errno_description((enum http_errno)t->parser.http_errno)); - return -1; + error = -1; + goto done; } } - return 0; + t->request_count++; + + if (auth_replay) { + s->sent_request = 0; + + if ((error = http_connect(t)) < 0) + return error; + + goto replay; + } + +done: + git_buf_dispose(&request); + return error; } static int http_stream_write_chunked( @@ -1378,7 +1528,7 @@ static int http_action( * that would be insecure in plaintext (eg, HTTP Basic). */ if ((!t->server.url.host || !t->server.url.port || !t->server.url.path) && - (ret = gitno_connection_data_from_url(&t->server.url, url, NULL)) < 0) + (ret = git_net_url_parse(&t->server.url, url)) < 0) return ret; assert(t->server.url.host && t->server.url.port && t->server.url.path); @@ -1404,19 +1554,6 @@ static int http_action( return -1; } -static void free_auth_contexts(git_vector *contexts) -{ - git_http_auth_context *context; - size_t i; - - git_vector_foreach(contexts, i, context) { - if (context->free) - context->free(context); - } - - git_vector_clear(contexts); -} - static int http_close(git_smart_subtransport *subtransport) { http_subtransport *t = GIT_CONTAINER_OF(subtransport, http_subtransport, parent); @@ -1438,18 +1575,16 @@ static int http_close(git_smart_subtransport *subtransport) } free_cred(&t->server.cred); - free_cred(&t->server.url_cred); free_cred(&t->proxy.cred); - free_cred(&t->proxy.url_cred); - free_auth_contexts(&t->server.auth_contexts); - free_auth_contexts(&t->proxy.auth_contexts); + free_auth_context(&t->server); + free_auth_context(&t->proxy); - gitno_connection_data_free_ptrs(&t->server.url); - memset(&t->server.url, 0x0, sizeof(gitno_connection_data)); + t->server.url_cred_presented = false; + t->proxy.url_cred_presented = false; - gitno_connection_data_free_ptrs(&t->proxy.url); - memset(&t->proxy.url, 0x0, sizeof(gitno_connection_data)); + git_net_url_dispose(&t->server.url); + git_net_url_dispose(&t->proxy.url); git__free(t->proxy_url); t->proxy_url = NULL; @@ -1462,9 +1597,6 @@ static void http_free(git_smart_subtransport *subtransport) http_subtransport *t = GIT_CONTAINER_OF(subtransport, http_subtransport, parent); http_close(subtransport); - - git_vector_free(&t->server.auth_contexts); - git_vector_free(&t->proxy.auth_contexts); git__free(t); } diff --git a/src/transports/http.h b/src/transports/http.h index b09475755..ddaab0b45 100644 --- a/src/transports/http.h +++ b/src/transports/http.h @@ -10,7 +10,7 @@ #include "buffer.h" -#define GIT_HTTP_REPLAY_MAX 7 +#define GIT_HTTP_REPLAY_MAX 15 GIT_INLINE(int) git_http__user_agent(git_buf *buf) { diff --git a/src/transports/ssh.c b/src/transports/ssh.c index 9b5d8a528..caa3a17f5 100644 --- a/src/transports/ssh.c +++ b/src/transports/ssh.c @@ -14,6 +14,7 @@ #include "global.h" #include "git2.h" #include "buffer.h" +#include "net.h" #include "netops.h" #include "smart.h" #include "cred.h" @@ -258,8 +259,7 @@ static int ssh_stream_alloc( } static int git_ssh_extract_url_parts( - char **host, - char **username, + git_net_url *urldata, const char *url) { char *colon, *at; @@ -271,11 +271,11 @@ static int git_ssh_extract_url_parts( at = strchr(url, '@'); if (at) { start = at + 1; - *username = git__substrdup(url, at - url); - GIT_ERROR_CHECK_ALLOC(*username); + urldata->username = git__substrdup(url, at - url); + GIT_ERROR_CHECK_ALLOC(urldata->username); } else { start = url; - *username = NULL; + urldata->username = NULL; } if (colon == NULL || (colon < start)) { @@ -283,8 +283,8 @@ static int git_ssh_extract_url_parts( return -1; } - *host = git__substrdup(start, colon - start); - GIT_ERROR_CHECK_ALLOC(*host); + urldata->host = git__substrdup(start, colon - start); + GIT_ERROR_CHECK_ALLOC(urldata->host); return 0; } @@ -506,14 +506,15 @@ static int _git_ssh_session_create( return 0; } +#define SSH_DEFAULT_PORT "22" + static int _git_ssh_setup_conn( ssh_subtransport *t, const char *url, const char *cmd, git_smart_subtransport_stream **stream) { - char *host=NULL, *port=NULL, *path=NULL, *user=NULL, *pass=NULL; - const char *default_port="22"; + git_net_url urldata = GIT_NET_URL_INIT; int auth_methods, error = 0; size_t i; ssh_stream *s; @@ -535,19 +536,22 @@ static int _git_ssh_setup_conn( const char *p = ssh_prefixes[i]; if (!git__prefixcmp(url, p)) { - if ((error = gitno_extract_url_parts(&host, &port, &path, &user, &pass, url, default_port)) < 0) + if ((error = git_net_url_parse(&urldata, url)) < 0) goto done; goto post_extract; } } - if ((error = git_ssh_extract_url_parts(&host, &user, url)) < 0) + if ((error = git_ssh_extract_url_parts(&urldata, url)) < 0) goto done; - port = git__strdup(default_port); - GIT_ERROR_CHECK_ALLOC(port); + + if (urldata.port == NULL) + urldata.port = git__strdup(SSH_DEFAULT_PORT); + + GIT_ERROR_CHECK_ALLOC(urldata.port); post_extract: - if ((error = git_socket_stream_new(&s->io, host, port)) < 0 || + if ((error = git_socket_stream_new(&s->io, urldata.host, urldata.port)) < 0 || (error = git_stream_connect(s->io)) < 0) goto done; @@ -583,7 +587,7 @@ post_extract: cert_ptr = &cert; - error = t->owner->certificate_check_cb((git_cert *) cert_ptr, 0, host, t->owner->message_cb_payload); + error = t->owner->certificate_check_cb((git_cert *) cert_ptr, 0, urldata.host, t->owner->message_cb_payload); if (error < 0 && error != GIT_PASSTHROUGH) { if (!git_error_last()) @@ -594,21 +598,21 @@ post_extract: } /* we need the username to ask for auth methods */ - if (!user) { + if (!urldata.username) { if ((error = request_creds(&cred, t, NULL, GIT_CREDTYPE_USERNAME)) < 0) goto done; - user = git__strdup(((git_cred_username *) cred)->username); + urldata.username = git__strdup(((git_cred_username *) cred)->username); cred->free(cred); cred = NULL; - if (!user) + if (!urldata.username) goto done; - } else if (user && pass) { - if ((error = git_cred_userpass_plaintext_new(&cred, user, pass)) < 0) + } else if (urldata.username && urldata.password) { + if ((error = git_cred_userpass_plaintext_new(&cred, urldata.username, urldata.password)) < 0) goto done; } - if ((error = list_auth_methods(&auth_methods, session, user)) < 0) + if ((error = list_auth_methods(&auth_methods, session, urldata.username)) < 0) goto done; error = GIT_EAUTH; @@ -622,10 +626,10 @@ post_extract: cred = NULL; } - if ((error = request_creds(&cred, t, user, auth_methods)) < 0) + if ((error = request_creds(&cred, t, urldata.username, auth_methods)) < 0) goto done; - if (strcmp(user, git_cred__username(cred))) { + if (strcmp(urldata.username, git_cred__username(cred))) { git_error_set(GIT_ERROR_SSH, "username does not match previous request"); error = -1; goto done; @@ -662,11 +666,7 @@ done: if (cred) cred->free(cred); - git__free(host); - git__free(port); - git__free(path); - git__free(user); - git__free(pass); + git_net_url_dispose(&urldata); return error; } diff --git a/src/transports/winhttp.c b/src/transports/winhttp.c index f922cb480..6392a7d1c 100644 --- a/src/transports/winhttp.c +++ b/src/transports/winhttp.c @@ -101,23 +101,43 @@ typedef struct { } winhttp_stream; typedef struct { - git_smart_subtransport parent; - transport_smart *owner; - gitno_connection_data connection_data; - gitno_connection_data proxy_connection_data; + git_net_url url; git_cred *cred; - git_cred *url_cred; - git_cred *proxy_cred; int auth_mechanisms; + bool url_cred_presented; +} winhttp_server; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + + winhttp_server server; + winhttp_server proxy; + HINTERNET session; HINTERNET connection; } winhttp_subtransport; -static int _apply_userpass_credential(HINTERNET request, DWORD target, DWORD scheme, git_cred *cred) +static int apply_userpass_credentials(HINTERNET request, DWORD target, int mechanisms, git_cred *cred) { git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; - wchar_t *user, *pass; + wchar_t *user = NULL, *pass = NULL; int user_len = 0, pass_len = 0, error = 0; + DWORD native_scheme; + + if (mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE) { + native_scheme = WINHTTP_AUTH_SCHEME_NEGOTIATE; + } else if (mechanisms & GIT_WINHTTP_AUTH_NTLM) { + native_scheme = WINHTTP_AUTH_SCHEME_NTLM; + } else if (mechanisms & GIT_WINHTTP_AUTH_DIGEST) { + native_scheme = WINHTTP_AUTH_SCHEME_DIGEST; + } else if (mechanisms & GIT_WINHTTP_AUTH_BASIC) { + native_scheme = WINHTTP_AUTH_SCHEME_BASIC; + } else { + git_error_set(GIT_ERROR_NET, "invalid authentication scheme"); + error = -1; + goto done; + } if ((error = user_len = git__utf8_to_16_alloc(&user, c->username)) < 0) goto done; @@ -125,7 +145,7 @@ static int _apply_userpass_credential(HINTERNET request, DWORD target, DWORD sch if ((error = pass_len = git__utf8_to_16_alloc(&pass, c->password)) < 0) goto done; - if (!WinHttpSetCredentials(request, target, scheme, user, pass, NULL)) { + if (!WinHttpSetCredentials(request, target, native_scheme, user, pass, NULL)) { git_error_set(GIT_ERROR_OS, "failed to set credentials"); error = -1; } @@ -143,77 +163,58 @@ done: return error; } -static int apply_userpass_credential_proxy(HINTERNET request, git_cred *cred, int mechanisms) +static int apply_default_credentials(HINTERNET request, DWORD target, 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; + DWORD autologon_level = WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW; + DWORD native_scheme = 0; - if ((mechanisms & GIT_WINHTTP_AUTH_NTLM) || - (mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE)) { + if ((mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE) != 0) { + native_scheme = WINHTTP_AUTH_SCHEME_NEGOTIATE; + } else if ((mechanisms & GIT_WINHTTP_AUTH_NTLM) != 0) { native_scheme = WINHTTP_AUTH_SCHEME_NTLM; - } else if (mechanisms & GIT_WINHTTP_AUTH_BASIC) { - native_scheme = WINHTTP_AUTH_SCHEME_BASIC; } else { git_error_set(GIT_ERROR_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; - DWORD native_scheme = 0; - - if ((mechanisms & GIT_WINHTTP_AUTH_NTLM) != 0) - native_scheme = WINHTTP_AUTH_SCHEME_NTLM; - - if ((mechanisms & GIT_WINHTTP_AUTH_NEGOTIATE) != 0) - native_scheme = WINHTTP_AUTH_SCHEME_NEGOTIATE; - - if (!native_scheme) { - git_error_set(GIT_ERROR_NET, "invalid authentication scheme"); + /* + * Autologon policy must be "low" to use default creds. + * This is safe as the user has explicitly requested it. + */ + if (!WinHttpSetOption(request, WINHTTP_OPTION_AUTOLOGON_POLICY, &autologon_level, sizeof(DWORD))) { + git_error_set(GIT_ERROR_OS, "could not configure logon policy"); return -1; } - if (!WinHttpSetOption(request, WINHTTP_OPTION_AUTOLOGON_POLICY, &data, sizeof(DWORD))) - return -1; - - if (!WinHttpSetCredentials(request, WINHTTP_AUTH_TARGET_SERVER, native_scheme, NULL, NULL, NULL)) + if (!WinHttpSetCredentials(request, target, native_scheme, NULL, NULL, NULL)) { + git_error_set(GIT_ERROR_OS, "could not configure credentials"); return -1; + } return 0; } -static int fallback_cred_acquire_cb( +static int acquire_url_cred( git_cred **cred, - const char *url, - const char *username_from_url, unsigned int allowed_types, - void *payload) + const char *username, + const char *password) { - int error = 1; + if (allowed_types & GIT_CREDTYPE_USERPASS_PLAINTEXT) + return git_cred_userpass_plaintext_new(cred, username, password); + + if ((allowed_types & GIT_CREDTYPE_DEFAULT) && *username == '\0' && *password == '\0') + return git_cred_default_new(cred); + + return 1; +} - GIT_UNUSED(username_from_url); - GIT_UNUSED(payload); +static int acquire_fallback_cred( + git_cred **cred, + const char *url, + unsigned int allowed_types) +{ + int error = 1; /* If the target URI supports integrated Windows authentication * as an authentication mechanism */ @@ -253,9 +254,9 @@ static int fallback_cred_acquire_cb( pISM->lpVtbl->Release(pISM); } - if (SUCCEEDED(hCoInitResult)) - /* Only unitialize if the call to CoInitializeEx was successful. */ - CoUninitialize(); + /* Only unitialize if the call to CoInitializeEx was successful. */ + if (SUCCEEDED(hCoInitResult)) + CoUninitialize(); } git__free(wide_url); @@ -280,7 +281,7 @@ static int certificate_check(winhttp_stream *s, int valid) return GIT_ECERTIFICATE; } - if (t->owner->certificate_check_cb == NULL || !t->connection_data.use_ssl) + if (t->owner->certificate_check_cb == NULL || git__strcmp(t->server.url.scheme, "https") != 0) return 0; if (!WinHttpQueryOption(s->request, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &cert_ctx, &cert_ctx_size)) { @@ -292,7 +293,7 @@ static int certificate_check(winhttp_stream *s, int valid) 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->message_cb_payload); + error = t->owner->certificate_check_cb((git_cert *) &cert, valid, t->server.url.host, t->owner->message_cb_payload); CertFreeCertificateContext(cert_ctx); if (error == GIT_PASSTHROUGH) @@ -329,8 +330,23 @@ static void winhttp_stream_close(winhttp_stream *s) s->sent_request = 0; } -#define SCHEME_HTTP "http://" -#define SCHEME_HTTPS "https://" +static int apply_credentials( + HINTERNET request, + git_net_url *url, + int target, + git_cred *creds, + int mechanisms) +{ + int error = 0; + + /* If we have creds, just apply them */ + if (creds && creds->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT) + error = apply_userpass_credentials(request, target, mechanisms, creds); + else if (creds && creds->credtype == GIT_CREDTYPE_DEFAULT) + error = apply_default_credentials(request, target, mechanisms); + + return error; +} static int winhttp_stream_connect(winhttp_stream *s) { @@ -344,11 +360,13 @@ static int winhttp_stream_connect(winhttp_stream *s) unsigned long disable_redirects = WINHTTP_DISABLE_REDIRECTS; int default_timeout = TIMEOUT_INFINITE; int default_connect_timeout = DEFAULT_CONNECT_TIMEOUT; + DWORD autologon_policy = WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH; + size_t i; const git_proxy_options *proxy_opts; /* Prepare URL */ - git_buf_printf(&buf, "%s%s", t->connection_data.path, s->service_url); + git_buf_printf(&buf, "%s%s", t->server.url.path, s->service_url); if (git_buf_oom(&buf)) return -1; @@ -367,13 +385,17 @@ static int winhttp_stream_connect(winhttp_stream *s) NULL, WINHTTP_NO_REFERER, types, - t->connection_data.use_ssl ? WINHTTP_FLAG_SECURE : 0); + git__strcmp(t->server.url.scheme, "https") == 0 ? WINHTTP_FLAG_SECURE : 0); if (!s->request) { git_error_set(GIT_ERROR_OS, "failed to open request"); goto on_error; } + /* Never attempt default credentials; we'll provide them explicitly. */ + if (!WinHttpSetOption(s->request, WINHTTP_OPTION_AUTOLOGON_POLICY, &autologon_policy, sizeof(DWORD))) + return -1; + if (!WinHttpSetTimeouts(s->request, default_timeout, default_connect_timeout, default_timeout, default_timeout)) { git_error_set(GIT_ERROR_OS, "failed to set timeouts for WinHTTP"); goto on_error; @@ -382,7 +404,7 @@ static int winhttp_stream_connect(winhttp_stream *s) 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) + if (git_remote__get_http_proxy(t->owner->owner, (strcmp(t->server.url.scheme, "https") == 0), &proxy_url) < 0) goto on_error; } else if (proxy_opts->type == GIT_PROXY_SPECIFIED) { @@ -395,38 +417,24 @@ static int winhttp_stream_connect(winhttp_stream *s) 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 { - git_error_set(GIT_ERROR_NET, "invalid URL: '%s'", proxy_url); - return -1; - } - - gitno_connection_data_free_ptrs(&t->proxy_connection_data); + git_net_url_dispose(&t->proxy.url); - 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) + if ((error = git_net_url_parse(&t->proxy.url, proxy_url)) < 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 (strcmp(t->proxy.url.scheme, "http") != 0 && strcmp(t->proxy.url.scheme, "https") != 0) { + git_error_set(GIT_ERROR_NET, "invalid URL: '%s'", proxy_url); + error = -1; + 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.url.scheme); + git_buf_PUTS(&processed_url, "://"); - 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); + git_buf_puts(&processed_url, t->proxy.url.host); + + if (!git_net_url_is_default_port(&t->proxy.url)) + git_buf_printf(&processed_url, ":%s", t->proxy.url.port); if (git_buf_oom(&processed_url)) { error = -1; @@ -454,13 +462,8 @@ static int winhttp_stream_connect(winhttp_stream *s) 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; - } - } - + if ((error = apply_credentials(s->request, &t->proxy.url, WINHTTP_AUTH_TARGET_PROXY, t->proxy.cred, t->proxy.auth_mechanisms)) < 0) + goto on_error; } /* Disable WinHTTP redirects so we can handle them manually. Why, you ask? @@ -471,6 +474,7 @@ static int winhttp_stream_connect(winhttp_stream *s) &disable_redirects, sizeof(disable_redirects))) { git_error_set(GIT_ERROR_OS, "failed to disable redirects"); + error = -1; goto on_error; } @@ -543,32 +547,15 @@ static int winhttp_stream_connect(winhttp_stream *s) } /* If requested, disable certificate validation */ - if (t->connection_data.use_ssl) { + if (strcmp(t->server.url.scheme, "https") == 0) { 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) + if ((error = apply_credentials(s->request, &t->server.url, WINHTTP_AUTH_TARGET_SERVER, t->server.cred, t->server.auth_mechanisms)) < 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. */ @@ -584,9 +571,9 @@ on_error: } static int parse_unauthorized_response( - HINTERNET request, int *allowed_types, - int *allowed_mechanisms) + int *allowed_mechanisms, + HINTERNET request) { DWORD supported, first, target; @@ -741,12 +728,12 @@ static int winhttp_connect( t->connection = NULL; /* Prepare port */ - if (git__strntol32(&port, t->connection_data.port, - strlen(t->connection_data.port), NULL, 10) < 0) + if (git__strntol32(&port, t->server.url.port, + strlen(t->server.url.port), NULL, 10) < 0) return -1; /* Prepare host */ - if (git__utf8_to_16_alloc(&wide_host, t->connection_data.host) < 0) { + if (git__utf8_to_16_alloc(&wide_host, t->server.url.host) < 0) { git_error_set(GIT_ERROR_OS, "unable to convert host to wide characters"); return -1; } @@ -890,6 +877,59 @@ static int send_request(winhttp_stream *s, size_t len, int ignore_length) return error; } +static int acquire_credentials( + HINTERNET request, + winhttp_server *server, + const char *url_str, + git_cred_acquire_cb cred_cb, + void *cred_cb_payload) +{ + int allowed_types; + int error = 1; + + if (parse_unauthorized_response(&allowed_types, &server->auth_mechanisms, request) < 0) + return -1; + + if (allowed_types) { + git_cred_free(server->cred); + server->cred = NULL; + + /* Start with URL-specified credentials, if there were any. */ + if (!server->url_cred_presented && server->url.username && server->url.password) { + error = acquire_url_cred(&server->cred, allowed_types, server->url.username, server->url.password); + server->url_cred_presented = 1; + + if (error < 0) + return error; + } + + /* Next use the user-defined callback, if there is one. */ + if (error > 0 && cred_cb) { + error = cred_cb(&server->cred, url_str, server->url.username, allowed_types, cred_cb_payload); + + /* Treat GIT_PASSTHROUGH as though git_cred_acquire_cb isn't set */ + if (error == GIT_PASSTHROUGH) + error = 1; + else if (error < 0) + return error; + } + + /* Finally, invoke the fallback default credential lookup. */ + if (error > 0) { + error = acquire_fallback_cred(&server->cred, url_str, allowed_types); + + if (error < 0) + return error; + } + } + + /* + * No error occurred but we could not find appropriate credentials. + * This behaves like a pass-through. + */ + return error; +} + static int winhttp_stream_read( git_smart_subtransport_stream *stream, char *buffer, @@ -1062,7 +1102,7 @@ replay: 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) { + if (gitno_connection_data_handle_redirect(&t->server.url, location8, s->service_url) < 0) { git__free(location8); return -1; } @@ -1077,67 +1117,34 @@ replay: 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 (status_code == HTTP_STATUS_DENIED) { + int error = acquire_credentials(s->request, + &t->server, + t->owner->url, + t->owner->cred_acquire_cb, + t->owner->cred_acquire_payload); + + if (error < 0) { + return error; + } else if (!error) { + assert(t->server.cred); + winhttp_stream_close(s); + goto replay; + } + } else if (status_code == HTTP_STATUS_PROXY_AUTH_REQ) { + int error = acquire_credentials(s->request, + &t->proxy, + t->owner->proxy.url, + t->owner->proxy.credentials, + t->owner->proxy.payload); + + if (error < 0) { + return error; + } else if (!error) { + assert(t->proxy.cred); + winhttp_stream_close(s); + goto replay; } } @@ -1496,7 +1503,7 @@ static int winhttp_action( int ret = -1; if (!t->connection) - if ((ret = gitno_connection_data_from_url(&t->connection_data, url, NULL)) < 0 || + if ((ret = git_net_url_parse(&t->server.url, url)) < 0 || (ret = winhttp_connect(t)) < 0) return ret; @@ -1538,24 +1545,17 @@ 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; - } + git_net_url_dispose(&t->server.url); + git_net_url_dispose(&t->proxy.url); - if (t->proxy_cred) { - t->proxy_cred->free(t->proxy_cred); - t->proxy_cred = NULL; + if (t->server.cred) { + t->server.cred->free(t->server.cred); + t->server.cred = NULL; } - if (t->url_cred) { - t->url_cred->free(t->url_cred); - t->url_cred = NULL; + if (t->proxy.cred) { + t->proxy.cred->free(t->proxy.cred); + t->proxy.cred = NULL; } return winhttp_close_connection(t); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aaa8ed109..e39fd6f7b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,4 +62,4 @@ ADD_TEST(invasive "${libgit2_BINARY_DIR}/libgit2_clar" -v -score::ftruncate -sf ADD_TEST(online "${libgit2_BINARY_DIR}/libgit2_clar" -v -sonline) ADD_TEST(gitdaemon "${libgit2_BINARY_DIR}/libgit2_clar" -v -sonline::push) ADD_TEST(ssh "${libgit2_BINARY_DIR}/libgit2_clar" -v -sonline::push -sonline::clone::ssh_cert -sonline::clone::ssh_with_paths) -ADD_TEST(proxy "${libgit2_BINARY_DIR}/libgit2_clar" -v -sonline::clone::proxy_credentials_in_url -sonline::clone::proxy_credentials_request) +ADD_TEST(proxy "${libgit2_BINARY_DIR}/libgit2_clar" -v -sonline::clone::proxy) diff --git a/tests/network/redirect.c b/tests/network/redirect.c new file mode 100644 index 000000000..3fc0b1826 --- /dev/null +++ b/tests/network/redirect.c @@ -0,0 +1,113 @@ +#include "clar_libgit2.h" +#include "net.h" +#include "netops.h" + +static git_net_url conndata; + +void test_network_redirect__initialize(void) +{ + memset(&conndata, 0, sizeof(conndata)); +} + +void test_network_redirect__cleanup(void) +{ + git_net_url_dispose(&conndata); +} + +void test_network_redirect__redirect_http(void) +{ + cl_git_pass(git_net_url_parse(&conndata, + "http://example.com/foo/bar/baz")); + cl_git_pass(gitno_connection_data_handle_redirect(&conndata, + "http://example.com/foo/bar/baz", "bar/baz")); + cl_assert_equal_s(conndata.scheme, "http"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "80"); + cl_assert_equal_s(conndata.path, "/foo/"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); +} + +void test_network_redirect__redirect_ssl(void) +{ + cl_git_pass(git_net_url_parse(&conndata, + "https://example.com/foo/bar/baz")); + cl_git_pass(gitno_connection_data_handle_redirect(&conndata, + "https://example.com/foo/bar/baz", "bar/baz")); + cl_assert_equal_s(conndata.scheme, "https"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "443"); + cl_assert_equal_s(conndata.path, "/foo/"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); +} + +void test_network_redirect__redirect_leaves_root_path(void) +{ + cl_git_pass(git_net_url_parse(&conndata, + "https://example.com/foo/bar/baz")); + cl_git_pass(gitno_connection_data_handle_redirect(&conndata, + "https://example.com/foo/bar/baz", "/foo/bar/baz")); + cl_assert_equal_s(conndata.scheme, "https"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "443"); + cl_assert_equal_s(conndata.path, "/"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); +} + +void test_network_redirect__redirect_encoded_username_password(void) +{ + cl_git_pass(git_net_url_parse(&conndata, + "https://user%2fname:pass%40word%zyx%v@example.com/foo/bar/baz")); + cl_git_pass(gitno_connection_data_handle_redirect(&conndata, + "https://user%2fname:pass%40word%zyx%v@example.com/foo/bar/baz", "bar/baz")); + cl_assert_equal_s(conndata.scheme, "https"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "443"); + cl_assert_equal_s(conndata.path, "/foo/"); + cl_assert_equal_s(conndata.username, "user/name"); + cl_assert_equal_s(conndata.password, "pass@word%zyx%v"); +} + +void test_network_redirect__redirect_cross_host_denied(void) +{ + cl_git_pass(git_net_url_parse(&conndata, "https://bar.com/bar/baz")); + cl_git_fail_with(gitno_connection_data_handle_redirect(&conndata, + "https://foo.com/bar/baz", NULL), + -1); +} + +void test_network_redirect__redirect_http_downgrade_denied(void) +{ + cl_git_pass(git_net_url_parse(&conndata, "https://foo.com/bar/baz")); + cl_git_fail_with(gitno_connection_data_handle_redirect(&conndata, + "http://foo.com/bar/baz", NULL), + -1); +} + +void test_network_redirect__redirect_relative(void) +{ + cl_git_pass(git_net_url_parse(&conndata, "http://foo.com/bar/baz/biff")); + cl_git_pass(gitno_connection_data_handle_redirect(&conndata, + "/zap/baz/biff?bam", NULL)); + cl_assert_equal_s(conndata.scheme, "http"); + cl_assert_equal_s(conndata.host, "foo.com"); + cl_assert_equal_s(conndata.port, "80"); + cl_assert_equal_s(conndata.path, "/zap/baz/biff?bam"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); +} + +void test_network_redirect__redirect_relative_ssl(void) +{ + cl_git_pass(git_net_url_parse(&conndata, "https://foo.com/bar/baz/biff")); + cl_git_pass(gitno_connection_data_handle_redirect(&conndata, + "/zap/baz/biff?bam", NULL)); + cl_assert_equal_s(conndata.scheme, "https"); + cl_assert_equal_s(conndata.host, "foo.com"); + cl_assert_equal_s(conndata.port, "443"); + cl_assert_equal_s(conndata.path, "/zap/baz/biff?bam"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); +} diff --git a/tests/network/urlparse.c b/tests/network/urlparse.c index 4a3096baa..c2362e628 100644 --- a/tests/network/urlparse.c +++ b/tests/network/urlparse.c @@ -1,220 +1,144 @@ #include "clar_libgit2.h" -#include "netops.h" +#include "net.h" -static char *host, *port, *path, *user, *pass; -static gitno_connection_data conndata; +static git_net_url conndata; void test_network_urlparse__initialize(void) { - host = port = path = user = pass = NULL; memset(&conndata, 0, sizeof(conndata)); } void test_network_urlparse__cleanup(void) { -#define FREE_AND_NULL(x) if (x) { git__free(x); x = NULL; } - FREE_AND_NULL(host); - FREE_AND_NULL(port); - FREE_AND_NULL(path); - FREE_AND_NULL(user); - FREE_AND_NULL(pass); - - gitno_connection_data_free_ptrs(&conndata); + git_net_url_dispose(&conndata); } void test_network_urlparse__trivial(void) { - cl_git_pass(gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "http://example.com/resource", "8080")); - cl_assert_equal_s(host, "example.com"); - cl_assert_equal_s(port, "8080"); - cl_assert_equal_s(path, "/resource"); - cl_assert_equal_p(user, NULL); - cl_assert_equal_p(pass, NULL); + cl_git_pass(git_net_url_parse(&conndata, "http://example.com/resource")); + cl_assert_equal_s(conndata.scheme, "http"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "80"); + cl_assert_equal_s(conndata.path, "/resource"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 1); } void test_network_urlparse__root(void) { - cl_git_pass(gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "http://example.com/", "8080")); - cl_assert_equal_s(host, "example.com"); - cl_assert_equal_s(port, "8080"); - cl_assert_equal_s(path, "/"); - cl_assert_equal_p(user, NULL); - cl_assert_equal_p(pass, NULL); + cl_git_pass(git_net_url_parse(&conndata, "http://example.com/")); + cl_assert_equal_s(conndata.scheme, "http"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "80"); + cl_assert_equal_s(conndata.path, "/"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 1); } -void test_network_urlparse__just_hostname(void) +void test_network_urlparse__implied_root(void) { - cl_git_fail_with(GIT_EINVALIDSPEC, - gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "http://example.com", "8080")); + cl_git_pass(git_net_url_parse(&conndata, "http://example.com")); + cl_assert_equal_s(conndata.scheme, "http"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "80"); + cl_assert_equal_s(conndata.path, "/"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 1); +} + +void test_network_urlparse__implied_root_custom_port(void) +{ + cl_git_pass(git_net_url_parse(&conndata, "http://example.com:42")); + cl_assert_equal_s(conndata.scheme, "http"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "42"); + cl_assert_equal_s(conndata.path, "/"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 0); } void test_network_urlparse__encoded_password(void) { - cl_git_pass(gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "https://user:pass%2fis%40bad@hostname.com:1234/", "1")); - cl_assert_equal_s(host, "hostname.com"); - cl_assert_equal_s(port, "1234"); - cl_assert_equal_s(path, "/"); - cl_assert_equal_s(user, "user"); - cl_assert_equal_s(pass, "pass/is@bad"); + cl_git_pass(git_net_url_parse(&conndata, + "https://user:pass%2fis%40bad@hostname.com:1234/")); + cl_assert_equal_s(conndata.scheme, "https"); + cl_assert_equal_s(conndata.host, "hostname.com"); + cl_assert_equal_s(conndata.port, "1234"); + cl_assert_equal_s(conndata.path, "/"); + cl_assert_equal_s(conndata.username, "user"); + cl_assert_equal_s(conndata.password, "pass/is@bad"); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 0); } void test_network_urlparse__user(void) { - cl_git_pass(gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "https://user@example.com/resource", "8080")); - cl_assert_equal_s(host, "example.com"); - cl_assert_equal_s(port, "8080"); - cl_assert_equal_s(path, "/resource"); - cl_assert_equal_s(user, "user"); - cl_assert_equal_p(pass, NULL); + cl_git_pass(git_net_url_parse(&conndata, + "https://user@example.com/resource")); + cl_assert_equal_s(conndata.scheme, "https"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "443"); + cl_assert_equal_s(conndata.path, "/resource"); + cl_assert_equal_s(conndata.username, "user"); + cl_assert_equal_p(conndata.password, NULL); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 1); } void test_network_urlparse__user_pass(void) { /* user:pass@hostname.tld/resource */ - cl_git_pass(gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "https://user:pass@example.com/resource", "8080")); - cl_assert_equal_s(host, "example.com"); - cl_assert_equal_s(port, "8080"); - cl_assert_equal_s(path, "/resource"); - cl_assert_equal_s(user, "user"); - cl_assert_equal_s(pass, "pass"); + cl_git_pass(git_net_url_parse(&conndata, + "https://user:pass@example.com/resource")); + cl_assert_equal_s(conndata.scheme, "https"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "443"); + cl_assert_equal_s(conndata.path, "/resource"); + cl_assert_equal_s(conndata.username, "user"); + cl_assert_equal_s(conndata.password, "pass"); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 1); } void test_network_urlparse__port(void) { /* hostname.tld:port/resource */ - cl_git_pass(gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "https://example.com:9191/resource", "8080")); - cl_assert_equal_s(host, "example.com"); - cl_assert_equal_s(port, "9191"); - cl_assert_equal_s(path, "/resource"); - cl_assert_equal_p(user, NULL); - cl_assert_equal_p(pass, NULL); + cl_git_pass(git_net_url_parse(&conndata, + "https://example.com:9191/resource")); + cl_assert_equal_s(conndata.scheme, "https"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "9191"); + cl_assert_equal_s(conndata.path, "/resource"); + cl_assert_equal_p(conndata.username, NULL); + cl_assert_equal_p(conndata.password, NULL); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 0); } void test_network_urlparse__user_port(void) { /* user@hostname.tld:port/resource */ - cl_git_pass(gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "https://user@example.com:9191/resource", "8080")); - cl_assert_equal_s(host, "example.com"); - cl_assert_equal_s(port, "9191"); - cl_assert_equal_s(path, "/resource"); - cl_assert_equal_s(user, "user"); - cl_assert_equal_p(pass, NULL); + cl_git_pass(git_net_url_parse(&conndata, + "https://user@example.com:9191/resource")); + cl_assert_equal_s(conndata.scheme, "https"); + cl_assert_equal_s(conndata.host, "example.com"); + cl_assert_equal_s(conndata.port, "9191"); + cl_assert_equal_s(conndata.path, "/resource"); + cl_assert_equal_s(conndata.username, "user"); + cl_assert_equal_p(conndata.password, NULL); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 0); } void test_network_urlparse__user_pass_port(void) { /* user:pass@hostname.tld:port/resource */ - cl_git_pass(gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "https://user:pass@example.com:9191/resource", "8080")); - cl_assert_equal_s(host, "example.com"); - cl_assert_equal_s(port, "9191"); - cl_assert_equal_s(path, "/resource"); - cl_assert_equal_s(user, "user"); - cl_assert_equal_s(pass, "pass"); -} - -void test_network_urlparse__optional_path(void) -{ - cl_git_fail(gitno_extract_url_parts(&host, &port, &path, &user, &pass, - "https://user:pass@example.com:9191", "8080")); - - cl_git_pass(gitno_extract_url_parts(&host, &port, NULL, &user, &pass, - "https://user:pass@example.com:9191", "8080")); -} - -void test_network_urlparse__connection_data_http(void) -{ - cl_git_pass(gitno_connection_data_from_url(&conndata, - "http://example.com/foo/bar/baz", "bar/baz")); - cl_assert_equal_s(conndata.host, "example.com"); - cl_assert_equal_s(conndata.port, "80"); - cl_assert_equal_s(conndata.path, "/foo/"); - cl_assert_equal_p(conndata.user, NULL); - cl_assert_equal_p(conndata.pass, NULL); - cl_assert_equal_i(conndata.use_ssl, false); -} - -void test_network_urlparse__connection_data_ssl(void) -{ - cl_git_pass(gitno_connection_data_from_url(&conndata, - "https://example.com/foo/bar/baz", "bar/baz")); + cl_git_pass(git_net_url_parse(&conndata, + "https://user:pass@example.com:9191/resource")); + cl_assert_equal_s(conndata.scheme, "https"); cl_assert_equal_s(conndata.host, "example.com"); - cl_assert_equal_s(conndata.port, "443"); - cl_assert_equal_s(conndata.path, "/foo/"); - cl_assert_equal_p(conndata.user, NULL); - cl_assert_equal_p(conndata.pass, NULL); - cl_assert_equal_i(conndata.use_ssl, true); -} - -void test_network_urlparse__encoded_username_password(void) -{ - cl_git_pass(gitno_connection_data_from_url(&conndata, - "https://user%2fname:pass%40word%zyx%v@example.com/foo/bar/baz", "bar/baz")); - cl_assert_equal_s(conndata.host, "example.com"); - cl_assert_equal_s(conndata.port, "443"); - cl_assert_equal_s(conndata.path, "/foo/"); - cl_assert_equal_s(conndata.user, "user/name"); - cl_assert_equal_s(conndata.pass, "pass@word%zyx%v"); - cl_assert_equal_i(conndata.use_ssl, true); -} - -void test_network_urlparse__connection_data_cross_host_redirect(void) -{ - conndata.host = git__strdup("bar.com"); - cl_git_fail_with(gitno_connection_data_from_url(&conndata, - "https://foo.com/bar/baz", NULL), - -1); -} - -void test_network_urlparse__connection_data_http_downgrade(void) -{ - conndata.use_ssl = true; - cl_git_fail_with(gitno_connection_data_from_url(&conndata, - "http://foo.com/bar/baz", NULL), - -1); -} - -void test_network_urlparse__connection_data_relative_redirect(void) -{ - cl_git_pass(gitno_connection_data_from_url(&conndata, - "http://foo.com/bar/baz/biff", NULL)); - cl_git_pass(gitno_connection_data_from_url(&conndata, - "/zap/baz/biff?bam", NULL)); - cl_assert_equal_s(conndata.host, "foo.com"); - cl_assert_equal_s(conndata.port, "80"); - cl_assert_equal_s(conndata.path, "/zap/baz/biff?bam"); - cl_assert_equal_p(conndata.user, NULL); - cl_assert_equal_p(conndata.pass, NULL); - cl_assert_equal_i(conndata.use_ssl, false); -} - -void test_network_urlparse__connection_data_relative_redirect_ssl(void) -{ - cl_git_pass(gitno_connection_data_from_url(&conndata, - "https://foo.com/bar/baz/biff", NULL)); - cl_git_pass(gitno_connection_data_from_url(&conndata, - "/zap/baz/biff?bam", NULL)); - cl_assert_equal_s(conndata.host, "foo.com"); - cl_assert_equal_s(conndata.port, "443"); - cl_assert_equal_s(conndata.path, "/zap/baz/biff?bam"); - cl_assert_equal_p(conndata.user, NULL); - cl_assert_equal_p(conndata.pass, NULL); - cl_assert_equal_i(conndata.use_ssl, true); -} - -/* Run this under valgrind */ -void test_network_urlparse__connection_data_cleanup(void) -{ - cl_git_pass(gitno_connection_data_from_url(&conndata, - "http://foo.com/bar/baz/biff", "baz/biff")); - cl_git_pass(gitno_connection_data_from_url(&conndata, - "https://foo.com/bar/baz/biff", "baz/biff")); + cl_assert_equal_s(conndata.port, "9191"); + cl_assert_equal_s(conndata.path, "/resource"); + cl_assert_equal_s(conndata.username, "user"); + cl_assert_equal_s(conndata.password, "pass"); + cl_assert_equal_i(git_net_url_is_default_port(&conndata), 0); } diff --git a/tests/online/clone.c b/tests/online/clone.c index 0d0334cbb..b7042f3d6 100644 --- a/tests/online/clone.c +++ b/tests/online/clone.c @@ -682,12 +682,6 @@ void test_online_clone__ssh_memory_auth(void) cl_git_pass(git_clone(&g_repo, _remote_url, "./foo", &g_options)); } -void test_online_clone__url_with_no_path_returns_EINVALIDSPEC(void) -{ - cl_git_fail_with(git_clone(&g_repo, "http://github.com", "./foo", &g_options), - GIT_EINVALIDSPEC); -} - static int fail_certificate_check(git_cert *cert, int valid, const char *host, void *payload) { GIT_UNUSED(cert); @@ -847,3 +841,25 @@ void test_online_clone__proxy_auto_not_detected(void) cl_git_pass(git_clone(&g_repo, "http://github.com/libgit2/TestGitRepository", "./foo", &g_options)); } + +void test_online_clone__proxy_cred_callback_after_failed_url_creds(void) +{ + git_buf url = GIT_BUF_INIT; + + if (!_remote_proxy_host || !_remote_proxy_user || !_remote_proxy_pass) + cl_skip(); + + cl_git_pass(git_buf_printf(&url, "%s://invalid_user_name:INVALID_pass_WORD@%s/", + _remote_proxy_scheme ? _remote_proxy_scheme : "http", + _remote_proxy_host)); + + g_options.fetch_opts.proxy_opts.type = GIT_PROXY_SPECIFIED; + g_options.fetch_opts.proxy_opts.url = url.ptr; + g_options.fetch_opts.proxy_opts.credentials = proxy_cred_cb; + g_options.fetch_opts.proxy_opts.certificate_check = proxy_cert_cb; + called_proxy_creds = 0; + cl_git_pass(git_clone(&g_repo, "http://github.com/libgit2/TestGitRepository", "./foo", &g_options)); + cl_assert(called_proxy_creds); + + git_buf_dispose(&url); +} |